Merge branch 'unstable' into 'main'
Unstable See merge request internship-2025/survey-webapp/survey-webapp!21
This commit is contained in:
commit
66350ff918
43 changed files with 1814 additions and 363 deletions
|
|
@ -24,5 +24,11 @@ public class ApplicationDbContext : SurveyDbContext
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey(s => s.CreatedBy)
|
.HasForeignKey(s => s.CreatedBy)
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
modelBuilder.Entity<Completion>()
|
||||||
|
.HasOne<User>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.CompletedBy)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,321 @@
|
||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using SurveyBackend.Infrastructure.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SurveyBackend.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20250525200829_Completion update of SurveyLib")]
|
||||||
|
partial class CompletionupdateofSurveyLib
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "8.0.15");
|
||||||
|
|
||||||
|
modelBuilder.Entity("GroupUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("GroupsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("UsersId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("GroupsId", "UsersId");
|
||||||
|
|
||||||
|
b.HasIndex("UsersId");
|
||||||
|
|
||||||
|
b.ToTable("GroupUser");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyBackend.Core.Models.Group", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Label")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Groups");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyBackend.Core.Models.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.Answer", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("CompletionId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("QuestionId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("AnswerText")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("CompletionId", "QuestionId");
|
||||||
|
|
||||||
|
b.HasIndex("QuestionId");
|
||||||
|
|
||||||
|
b.ToTable("Answers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.AnswerVariant", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("QuestionId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("QuestionId");
|
||||||
|
|
||||||
|
b.ToTable("AnswerVariants");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.Completion", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("CompletedBy")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("FinishedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SurveyId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CompletedBy");
|
||||||
|
|
||||||
|
b.HasIndex("SurveyId");
|
||||||
|
|
||||||
|
b.ToTable("Completions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Discriminator")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(34)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("SurveyId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SurveyId");
|
||||||
|
|
||||||
|
b.ToTable("Questions");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("QuestionBase");
|
||||||
|
|
||||||
|
b.UseTphMappingStrategy();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.Survey", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("CreatedBy")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedBy");
|
||||||
|
|
||||||
|
b.ToTable("Surveys");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.MultipleAnswerQuestion", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("SurveyLib.Core.Models.QuestionBase");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("MultipleAnswerQuestion");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.SingleAnswerQuestion", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("SurveyLib.Core.Models.QuestionBase");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("SingleAnswerQuestion");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.QuestionVariants.TextQuestion", b =>
|
||||||
|
{
|
||||||
|
b.HasBaseType("SurveyLib.Core.Models.QuestionBase");
|
||||||
|
|
||||||
|
b.HasDiscriminator().HasValue("TextQuestion");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("GroupUser", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SurveyBackend.Core.Models.Group", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("GroupsId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SurveyBackend.Core.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UsersId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.Answer", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SurveyLib.Core.Models.Completion", "Completion")
|
||||||
|
.WithMany("Answers")
|
||||||
|
.HasForeignKey("CompletionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SurveyLib.Core.Models.QuestionBase", "Question")
|
||||||
|
.WithMany("Answers")
|
||||||
|
.HasForeignKey("QuestionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Completion");
|
||||||
|
|
||||||
|
b.Navigation("Question");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.AnswerVariant", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SurveyLib.Core.Models.QuestionBase", "Question")
|
||||||
|
.WithMany("AnswerVariants")
|
||||||
|
.HasForeignKey("QuestionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Question");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.Completion", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SurveyBackend.Core.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CompletedBy")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("SurveyLib.Core.Models.Survey", "Survey")
|
||||||
|
.WithMany("Completions")
|
||||||
|
.HasForeignKey("SurveyId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Survey");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SurveyLib.Core.Models.Survey", "Survey")
|
||||||
|
.WithMany("Questions")
|
||||||
|
.HasForeignKey("SurveyId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Survey");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.Survey", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SurveyBackend.Core.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CreatedBy")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.Completion", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Answers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.QuestionBase", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AnswerVariants");
|
||||||
|
|
||||||
|
b.Navigation("Answers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SurveyLib.Core.Models.Survey", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Completions");
|
||||||
|
|
||||||
|
b.Navigation("Questions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SurveyBackend.Infrastructure.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class CompletionupdateofSurveyLib : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "CompletedBy",
|
||||||
|
table: "Completions",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Completions_CompletedBy",
|
||||||
|
table: "Completions",
|
||||||
|
column: "CompletedBy");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Completions_Users_CompletedBy",
|
||||||
|
table: "Completions",
|
||||||
|
column: "CompletedBy",
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Completions_Users_CompletedBy",
|
||||||
|
table: "Completions");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Completions_CompletedBy",
|
||||||
|
table: "Completions");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CompletedBy",
|
||||||
|
table: "Completions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -119,6 +119,9 @@ namespace SurveyBackend.Infrastructure.Data.Migrations
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int?>("CompletedBy")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<DateTime>("FinishedAt")
|
b.Property<DateTime>("FinishedAt")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
|
@ -127,6 +130,8 @@ namespace SurveyBackend.Infrastructure.Data.Migrations
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CompletedBy");
|
||||||
|
|
||||||
b.HasIndex("SurveyId");
|
b.HasIndex("SurveyId");
|
||||||
|
|
||||||
b.ToTable("Completions");
|
b.ToTable("Completions");
|
||||||
|
|
@ -256,6 +261,11 @@ namespace SurveyBackend.Infrastructure.Data.Migrations
|
||||||
|
|
||||||
modelBuilder.Entity("SurveyLib.Core.Models.Completion", b =>
|
modelBuilder.Entity("SurveyLib.Core.Models.Completion", b =>
|
||||||
{
|
{
|
||||||
|
b.HasOne("SurveyBackend.Core.Models.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CompletedBy")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
b.HasOne("SurveyLib.Core.Models.Survey", "Survey")
|
b.HasOne("SurveyLib.Core.Models.Survey", "Survey")
|
||||||
.WithMany("Completions")
|
.WithMany("Completions")
|
||||||
.HasForeignKey("SurveyId")
|
.HasForeignKey("SurveyId")
|
||||||
|
|
|
||||||
50
SurveyFrontend/package-lock.json
generated
50
SurveyFrontend/package-lock.json
generated
|
|
@ -9,9 +9,13 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/tempo": "^0.1.2",
|
"@formkit/tempo": "^0.1.2",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
|
"chartjs-plugin-annotation": "^3.1.0",
|
||||||
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
"mobx": "^6.13.7",
|
"mobx": "^6.13.7",
|
||||||
"mobx-react": "^9.2.0",
|
"mobx-react": "^9.2.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.5.2",
|
"react-router-dom": "^7.5.2",
|
||||||
"react-textarea-autosize": "^8.5.9",
|
"react-textarea-autosize": "^8.5.9",
|
||||||
|
|
@ -972,6 +976,12 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|
@ -1989,6 +1999,36 @@
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
|
||||||
|
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chartjs-plugin-annotation": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chartjs-plugin-datalabels": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": ">=3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|
@ -3102,6 +3142,16 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-chartjs-2": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": "^4.1.1",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.0.0",
|
"version": "19.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,13 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/tempo": "^0.1.2",
|
"@formkit/tempo": "^0.1.2",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
|
"chartjs-plugin-annotation": "^3.1.0",
|
||||||
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
"mobx": "^6.13.7",
|
"mobx": "^6.13.7",
|
||||||
"mobx-react": "^9.2.0",
|
"mobx-react": "^9.2.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.5.2",
|
"react-router-dom": "^7.5.2",
|
||||||
"react-textarea-autosize": "^8.5.9",
|
"react-textarea-autosize": "^8.5.9",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import {Results} from "./components/Results/Results.tsx";
|
||||||
import {MySurveyList} from "./components/MySurveyList/MySurveyList.tsx";
|
import {MySurveyList} from "./components/MySurveyList/MySurveyList.tsx";
|
||||||
import AuthForm from "./pages/AuthForm/AuthForm.tsx";
|
import AuthForm from "./pages/AuthForm/AuthForm.tsx";
|
||||||
import {SurveyPage} from "./components/SurveyPage/SurveyPage.tsx";
|
import {SurveyPage} from "./components/SurveyPage/SurveyPage.tsx";
|
||||||
|
import CompleteSurvey from "./pages/CompleteSurvey/CompleteSurvey.tsx";
|
||||||
|
import CompletingSurvey from "./components/CompletingSurvey/CompletingSurvey.tsx";
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
return(
|
return(
|
||||||
|
|
@ -31,6 +33,10 @@ const App = () => {
|
||||||
<Route path="results" element={<Results />} />
|
<Route path="results" element={<Results />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path='/complete-survey' element={<CompleteSurvey/>}>
|
||||||
|
<Route index element={<CompletingSurvey/>}/>
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path="*" element={<AuthForm />} />
|
<Route path="*" element={<AuthForm />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|
|
||||||
94
SurveyFrontend/src/api/AnswerApi.ts
Normal file
94
SurveyFrontend/src/api/AnswerApi.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts";
|
||||||
|
|
||||||
|
export interface INewAnswer{
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAnswerVariant extends INewAnswer{
|
||||||
|
surveyId: number;
|
||||||
|
id: number;
|
||||||
|
questionId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAnswerVariants = async (surveyId: number, questionId: number) => {
|
||||||
|
try{
|
||||||
|
const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants`, {
|
||||||
|
...createRequestConfig('GET')
|
||||||
|
})
|
||||||
|
return await handleResponse(response)
|
||||||
|
}catch(err){
|
||||||
|
console.error(`Error receiving response options: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addNewAnswerVariant = async (surveyId: number, questionId: number, answer: INewAnswer) => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Токен отсутствует");
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants`, {
|
||||||
|
...createRequestConfig('POST'),
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: answer.text,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Ошибка: ${response.status}`);
|
||||||
|
}
|
||||||
|
return await handleResponse(response)
|
||||||
|
}
|
||||||
|
catch(err){
|
||||||
|
console.error(`Error adding a new response option: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateAnswerVariant = async (surveyId: number, questionId: number, id: number, answer: INewAnswer): Promise<INewAnswer> => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Токен отсутствует");
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants/${id}`, {
|
||||||
|
...createRequestConfig('PUT'),
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: answer.text,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null);
|
||||||
|
throw new Error(`Ошибка ${response.status}: ${errorData?.message || 'Неизвестная ошибка'}`);
|
||||||
|
}
|
||||||
|
return await handleResponse(response)
|
||||||
|
}
|
||||||
|
catch(err){
|
||||||
|
console.error(`Error updating the response option: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAnswerVariant = async (surveyId: number, questionId: number, id: number) => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Токен отсутствует');
|
||||||
|
}
|
||||||
|
try{
|
||||||
|
const response = await fetch(`${BASE_URL}/surveys/${surveyId}/questions/${questionId}/answerVariants/${id}`, {
|
||||||
|
...createRequestConfig('DELETE'),
|
||||||
|
})
|
||||||
|
const responseData = await handleResponse(response);
|
||||||
|
if (response.ok && !responseData){
|
||||||
|
return {success: true};
|
||||||
|
}
|
||||||
|
return responseData;
|
||||||
|
}
|
||||||
|
catch(err){
|
||||||
|
console.error(`Error deleting a answer: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,16 +10,10 @@ interface IRegistrationData extends IAuthData{
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
}
|
}
|
||||||
//
|
|
||||||
// interface IUserData{
|
|
||||||
// username: string;
|
|
||||||
// firstName: string;
|
|
||||||
// lastName: string;
|
|
||||||
// email: string;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const getCurrentUser = async (): Promise<IRegistrationData> => {
|
export const getCurrentUser = async () => {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error("Токен отсутствует");
|
throw new Error("Токен отсутствует");
|
||||||
}
|
}
|
||||||
|
|
@ -32,6 +26,8 @@ export const getCurrentUser = async (): Promise<IRegistrationData> => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
throw new Error("Сессия истекла. Пожалуйста, войдите снова.");
|
throw new Error("Сессия истекла. Пожалуйста, войдите снова.");
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,10 @@ const createRequestConfig = (method: string, isFormData: boolean = false): Reque
|
||||||
headers: {},
|
headers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Добавляем заголовок авторизации, если есть токен
|
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем Content-Type, если это не FormData
|
|
||||||
if (!isFormData) {
|
if (!isFormData) {
|
||||||
config.headers["Content-Type"] = "application/json";
|
config.headers["Content-Type"] = "application/json";
|
||||||
}
|
}
|
||||||
|
|
@ -39,12 +37,16 @@ const createRequestConfig = (method: string, isFormData: boolean = false): Reque
|
||||||
* @returns Распарсенные данные или ошибку
|
* @returns Распарсенные данные или ошибку
|
||||||
*/
|
*/
|
||||||
const handleResponse = async (response: Response) => {
|
const handleResponse = async (response: Response) => {
|
||||||
// Проверяем, есть ли контент в ответе
|
|
||||||
const responseText = await response.text();
|
const responseText = await response.text();
|
||||||
|
|
||||||
if (!responseText) {
|
if (!responseText) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
throw new Error('Требуется авторизация');
|
||||||
|
}
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return null; // Если ответ пустой, но статус 200, возвращаем null
|
return null;
|
||||||
}
|
}
|
||||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts";
|
import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts";
|
||||||
|
import {IAnswerVariant} from "./AnswerApi.ts";
|
||||||
|
|
||||||
export interface INewQuestion{
|
export interface INewQuestion{
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -8,11 +9,7 @@ export interface INewQuestion{
|
||||||
export interface IQuestion extends INewQuestion {
|
export interface IQuestion extends INewQuestion {
|
||||||
id: number;
|
id: number;
|
||||||
surveyId: number;
|
surveyId: number;
|
||||||
answerVariants: Array<{
|
answerVariants: IAnswerVariant[];
|
||||||
id: number;
|
|
||||||
questionId: number;
|
|
||||||
text: string;
|
|
||||||
}>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addNewQuestion = async (surveyId: number, question: INewQuestion) => {
|
export const addNewQuestion = async (surveyId: number, question: INewQuestion) => {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export interface ISurvey {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
createdBy: number;
|
createdBy: number;
|
||||||
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INewSurvey{
|
export interface INewSurvey{
|
||||||
|
|
@ -51,30 +52,6 @@ export const getAllSurveys = async (): Promise<ISurvey[]> => {
|
||||||
* postNewSurvey - добавление нового опроса
|
* postNewSurvey - добавление нового опроса
|
||||||
* @param survey
|
* @param survey
|
||||||
*/
|
*/
|
||||||
// export const postNewSurvey = async (survey: INewSurvey): Promise<ISurvey> => {
|
|
||||||
// const token = localStorage.getItem("token");
|
|
||||||
// if (!token) {
|
|
||||||
// throw new Error("Токен отсутствует");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// try{
|
|
||||||
// const response = await fetch(`${BASE_URL}/surveys`, {
|
|
||||||
// ...createRequestConfig('POST'),
|
|
||||||
// body: JSON.stringify(survey)
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// // return await handleResponse(response);
|
|
||||||
//
|
|
||||||
// if (response.status === 200) {
|
|
||||||
// return await handleResponse(response);
|
|
||||||
// }
|
|
||||||
// throw new Error(`Ожидался код 200, получен ${response.status}`);
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error(`Error when adding a new survey: ${error}`);
|
|
||||||
// throw error;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const postNewSurvey = async (survey: INewSurvey): Promise<ISurvey> => {
|
export const postNewSurvey = async (survey: INewSurvey): Promise<ISurvey> => {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|
|
||||||
3
SurveyFrontend/src/assets/gmail_groups.svg
Normal file
3
SurveyFrontend/src/assets/gmail_groups.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="58" height="61" viewBox="0 0 58 61" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 50.8333V43.7802C0 42.3399 0.342361 41.0055 1.02708 39.777C1.71181 38.5486 2.65833 37.6166 3.86667 36.9812C4.43056 36.6847 4.97431 36.4093 5.49792 36.1552C6.06181 35.901 6.64583 35.668 7.25 35.4562V50.8333H0ZM9.66667 33.0416C7.65278 33.0416 5.94097 32.3003 4.53125 30.8177C3.12153 29.335 2.41667 27.5347 2.41667 25.4166C2.41667 23.2986 3.12153 21.4982 4.53125 20.0156C5.94097 18.5329 7.65278 17.7916 9.66667 17.7916C11.6806 17.7916 13.3924 18.5329 14.8021 20.0156C16.2118 21.4982 16.9167 23.2986 16.9167 25.4166C16.9167 27.5347 16.2118 29.335 14.8021 30.8177C13.3924 32.3003 11.6806 33.0416 9.66667 33.0416ZM9.66667 27.9583C10.3514 27.9583 10.9153 27.7253 11.3583 27.2593C11.8417 26.751 12.0833 26.1368 12.0833 25.4166C12.0833 24.6965 11.8417 24.1034 11.3583 23.6375C10.9153 23.1291 10.3514 22.875 9.66667 22.875C8.98195 22.875 8.39792 23.1291 7.91458 23.6375C7.47153 24.1034 7.25 24.6965 7.25 25.4166C7.25 26.1368 7.47153 26.751 7.91458 27.2593C8.39792 27.7253 8.98195 27.9583 9.66667 27.9583ZM9.66667 50.8333V43.7166C9.66667 42.2763 10.009 40.9632 10.6938 39.777C11.4188 38.5486 12.3653 37.6166 13.5333 36.9812C16.0306 35.668 18.5681 34.6937 21.1458 34.0583C23.7236 33.3805 26.3417 33.0416 29 33.0416C31.6583 33.0416 34.2764 33.3805 36.8542 34.0583C39.4319 34.6937 41.9694 35.668 44.4667 36.9812C45.6347 37.6166 46.5611 38.5486 47.2458 39.777C47.9708 40.9632 48.3333 42.2763 48.3333 43.7166V50.8333H9.66667ZM14.5 45.75H43.5V43.7166C43.5 43.2507 43.3792 42.827 43.1375 42.4458C42.9361 42.0645 42.6542 41.768 42.2917 41.5562C40.1167 40.4125 37.9215 39.5652 35.7063 39.0145C33.491 38.4215 31.2556 38.125 29 38.125C26.7444 38.125 24.509 38.4215 22.2938 39.0145C20.0785 39.5652 17.8833 40.4125 15.7083 41.5562C15.3458 41.768 15.0437 42.0645 14.8021 42.4458C14.6007 42.827 14.5 43.2507 14.5 43.7166V45.75ZM29 30.5C26.3417 30.5 24.066 29.5045 22.1729 27.5135C20.2799 25.5225 19.3333 23.1291 19.3333 20.3333C19.3333 17.5375 20.2799 15.1441 22.1729 13.1531C24.066 11.1621 26.3417 10.1666 29 10.1666C31.6583 10.1666 33.934 11.1621 35.8271 13.1531C37.7201 15.1441 38.6667 17.5375 38.6667 20.3333C38.6667 23.1291 37.7201 25.5225 35.8271 27.5135C33.934 29.5045 31.6583 30.5 29 30.5ZM29 25.4166C30.3292 25.4166 31.4569 24.9295 32.3833 23.9552C33.35 22.9385 33.8333 21.7312 33.8333 20.3333C33.8333 18.9354 33.35 17.7493 32.3833 16.775C31.4569 15.7583 30.3292 15.25 29 15.25C27.6708 15.25 26.5229 15.7583 25.5563 16.775C24.6299 17.7493 24.1667 18.9354 24.1667 20.3333C24.1667 21.7312 24.6299 22.9385 25.5563 23.9552C26.5229 24.9295 27.6708 25.4166 29 25.4166ZM48.3333 33.0416C46.3194 33.0416 44.6076 32.3003 43.1979 30.8177C41.7882 29.335 41.0833 27.5347 41.0833 25.4166C41.0833 23.2986 41.7882 21.4982 43.1979 20.0156C44.6076 18.5329 46.3194 17.7916 48.3333 17.7916C50.3472 17.7916 52.059 18.5329 53.4688 20.0156C54.8785 21.4982 55.5833 23.2986 55.5833 25.4166C55.5833 27.5347 54.8785 29.335 53.4688 30.8177C52.059 32.3003 50.3472 33.0416 48.3333 33.0416ZM48.3333 27.9583C49.0181 27.9583 49.5819 27.7253 50.025 27.2593C50.5083 26.751 50.75 26.1368 50.75 25.4166C50.75 24.6965 50.5083 24.1034 50.025 23.6375C49.5819 23.1291 49.0181 22.875 48.3333 22.875C47.6486 22.875 47.0646 23.1291 46.5813 23.6375C46.1382 24.1034 45.9167 24.6965 45.9167 25.4166C45.9167 26.1368 46.1382 26.751 46.5813 27.2593C47.0646 27.7253 47.6486 27.9583 48.3333 27.9583ZM50.75 50.8333V35.4562C51.3542 35.668 51.9181 35.901 52.4417 36.1552C53.0056 36.4093 53.5695 36.6847 54.1333 36.9812C55.3417 37.6166 56.2882 38.5486 56.9729 39.777C57.6576 41.0055 58 42.3399 58 43.7802V50.8333H50.75Z" fill="#FEF7FF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
3
SurveyFrontend/src/assets/send.svg
Normal file
3
SurveyFrontend/src/assets/send.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="58" height="58" viewBox="0 0 58 58" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.25 48.3333V9.66663L53.1667 29L7.25 48.3333ZM12.0833 41.0833L40.7208 29L12.0833 16.9166V25.375L26.5833 29L12.0833 32.625V41.0833ZM12.0833 41.0833V29V16.9166V25.375V32.625V41.0833Z" fill="#FEF7FF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 312 B |
|
|
@ -3,17 +3,22 @@ import styles from'./AnswerOption.module.css';
|
||||||
import Delete from '../../assets/delete.svg?react';
|
import Delete from '../../assets/delete.svg?react';
|
||||||
import Single from '../../assets/radio_button_unchecked.svg?react';
|
import Single from '../../assets/radio_button_unchecked.svg?react';
|
||||||
import Multiple from '../../assets/emptyCheckbox.svg?react';
|
import Multiple from '../../assets/emptyCheckbox.svg?react';
|
||||||
|
import SelectedSingle from '../../assets/radio_button_checked.svg?react'
|
||||||
|
import SelectedMultiple from '../../assets/check_box.svg?react';
|
||||||
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
|
|
||||||
interface AnswerOptionProps{
|
interface AnswerOptionProps{
|
||||||
index: number;
|
index: number;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onDelete:(index: number) => void;
|
onDelete?:(index: number) => void;
|
||||||
selectedType: 'single' | 'multiply';
|
selectedType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion';
|
||||||
toggleSelect: () => void;
|
isSelected?: boolean;
|
||||||
|
toggleSelect?: () => void;
|
||||||
|
isCompleteSurveyActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDelete, selectedType, toggleSelect}) => {
|
const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDelete, selectedType, isSelected, toggleSelect, isCompleteSurveyActive = false}) => {
|
||||||
const [currentValue, setCurrentValue] = useState(value);
|
const [currentValue, setCurrentValue] = useState(value);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
|
@ -29,7 +34,6 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
|
||||||
|
|
||||||
const handleTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setCurrentValue(event.target.value);
|
setCurrentValue(event.target.value);
|
||||||
// Автоматическое изменение высоты
|
|
||||||
if (textAreaRef.current) {
|
if (textAreaRef.current) {
|
||||||
textAreaRef.current.style.height = 'auto';
|
textAreaRef.current.style.height = 'auto';
|
||||||
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
|
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
|
||||||
|
|
@ -39,7 +43,6 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing && textAreaRef.current) {
|
if (isEditing && textAreaRef.current) {
|
||||||
textAreaRef.current.focus();
|
textAreaRef.current.focus();
|
||||||
// Установка начальной высоты
|
|
||||||
textAreaRef.current.style.height = 'auto';
|
textAreaRef.current.style.height = 'auto';
|
||||||
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
|
textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`;
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +50,7 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
onChange(currentValue);
|
onChange?.(currentValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
|
@ -67,16 +70,50 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
|
||||||
}
|
}
|
||||||
}, [isEditing]);
|
}, [isEditing]);
|
||||||
|
|
||||||
|
const handleMarkerClick = () => {
|
||||||
|
if (isCompleteSurveyActive && toggleSelect) {
|
||||||
|
toggleSelect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.answer}>
|
<div className={styles.answer}>
|
||||||
|
{isCompleteSurveyActive ? (
|
||||||
<button
|
<button
|
||||||
className={`${styles.buttonMarker} ${isEditing ? styles.editing : ''}`}
|
className={`${styles.buttonMarker} ${isSelected ? styles.selected : ''}`}
|
||||||
onClick={toggleSelect}
|
onClick={handleMarkerClick}
|
||||||
>
|
>
|
||||||
{selectedType === 'single' ? < Single className={styles.answerIcon} /> : <Multiple className={styles.answerIcon} />}
|
{selectedType === 'SingleAnswerQuestion' ? (
|
||||||
|
isSelected ? (
|
||||||
|
<SelectedSingle className={styles.answerIcon} />
|
||||||
|
) : (
|
||||||
|
<Single className={styles.answerIcon} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
isSelected ? (
|
||||||
|
<SelectedMultiple className={styles.answerIcon} />
|
||||||
|
) : (
|
||||||
|
<Multiple className={styles.answerIcon} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
{isEditing ? (
|
) : (
|
||||||
<textarea
|
<button className={styles.buttonMarker}>
|
||||||
|
{selectedType === 'SingleAnswerQuestion' ? (
|
||||||
|
<Single className={styles.answerIcon} />
|
||||||
|
) : (
|
||||||
|
<Multiple className={styles.answerIcon} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isCompleteSurveyActive ? (
|
||||||
|
<button className={styles.textAnswer}>
|
||||||
|
{currentValue || `Ответ ${index}`}
|
||||||
|
</button>
|
||||||
|
) : isEditing ? (
|
||||||
|
<TextareaAutosize
|
||||||
className={styles.answerInput}
|
className={styles.answerInput}
|
||||||
ref={textAreaRef}
|
ref={textAreaRef}
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
|
|
@ -90,12 +127,14 @@ const AnswerOption: React.FC<AnswerOptionProps> = ({index, value, onChange, onDe
|
||||||
{currentValue || `Ответ ${index}`}
|
{currentValue || `Ответ ${index}`}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button className={styles.deleteButton} onClick={() => onDelete(index)}>
|
|
||||||
|
{!isCompleteSurveyActive && (
|
||||||
|
<button className={styles.deleteButton} onClick={() => onDelete?.(index)}>
|
||||||
<Delete />
|
<Delete />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AnswerOption;
|
export default AnswerOption;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
.survey{
|
||||||
|
width: 68%;
|
||||||
|
background-color: #F6F6F6;
|
||||||
|
max-width: 100vw;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 34px 16%;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
|
||||||
|
import QuestionsList, {Question} from "../QuestionsList/QuestionsList.tsx";
|
||||||
|
import {useState} from "react";
|
||||||
|
import styles from './CompletingSurvey.module.css'
|
||||||
|
|
||||||
|
export const CompletingSurvey = () => {
|
||||||
|
const [titleSurvey, setTitleSurvey] = useState("Название опроса");
|
||||||
|
const [descriptionSurvey, setDescriptionSurvey] = useState("");
|
||||||
|
const [questions, setQuestions] = useState<Question[]>([
|
||||||
|
{ id: 1, text: 'Вопрос 1', questionType: 'SingleAnswerQuestion', answerVariants: [{ id: 1, text: 'Ответ 1' },
|
||||||
|
{ id: 2, text: 'Ответ 1' }, { id: 3, text: 'Ответ 1' }]},
|
||||||
|
{ id: 2, text: 'Вопрос 2', questionType: 'MultipleAnswerQuestion', answerVariants: [{ id: 1, text: 'Ответ 1' },
|
||||||
|
{ id: 2, text: 'Ответ 1' }, { id: 3, text: 'Ответ 1' }]}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.survey}>
|
||||||
|
<SurveyInfo
|
||||||
|
titleSurvey={titleSurvey}
|
||||||
|
descriptionSurvey={descriptionSurvey}
|
||||||
|
setDescriptionSurvey={setDescriptionSurvey}
|
||||||
|
setTitleSurvey={setTitleSurvey}
|
||||||
|
/>
|
||||||
|
<QuestionsList
|
||||||
|
questions={questions}
|
||||||
|
setQuestions={setQuestions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CompletingSurvey
|
||||||
|
|
@ -12,8 +12,7 @@
|
||||||
gap: 60px;
|
gap: 60px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 40%;
|
margin-right: 20%;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageLink{
|
.pageLink{
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,13 @@ import {Link, useLocation, useNavigate} from "react-router-dom";
|
||||||
const Header: React.FC = () => {
|
const Header: React.FC = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isCreateSurveyActive = location.pathname.includes('/survey/create');
|
|
||||||
const isSurveyPage = location.pathname.includes('/survey/') && !location.pathname.includes('/survey/create');
|
const isCreateSurveyActive = location.pathname.startsWith('/survey/create');
|
||||||
const isMySurveysPage = location.pathname === '/my-surveys' || isSurveyPage;
|
const isMySurveysActive = location.pathname === '/my-surveys';
|
||||||
|
const isCompleteSurveyActive = location.pathname === '/complete-survey';
|
||||||
|
|
||||||
|
const isSurveyViewPage = location.pathname.startsWith('/survey/') &&
|
||||||
|
!location.pathname.startsWith('/survey/create');
|
||||||
|
|
||||||
const handleLogoClick = () => {
|
const handleLogoClick = () => {
|
||||||
navigate(location.pathname, { replace: true });
|
navigate(location.pathname, { replace: true });
|
||||||
|
|
@ -19,15 +23,26 @@ const Header: React.FC = () => {
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<Logo href={location.pathname} onClick={handleLogoClick} />
|
<Logo href={location.pathname} onClick={handleLogoClick} />
|
||||||
<nav className={styles.pagesNav}>
|
<nav className={styles.pagesNav}>
|
||||||
<Link to='/survey/create/questions'
|
<Link
|
||||||
className={`${styles.pageLink} ${isCreateSurveyActive ? styles.active : ''}`}>
|
to='/survey/create/questions'
|
||||||
|
className={`${styles.pageLink} ${isCreateSurveyActive ? styles.active : ''}`}
|
||||||
|
>
|
||||||
Создать опрос
|
Создать опрос
|
||||||
{isCreateSurveyActive && <hr className={styles.activeLine}/>}
|
{isCreateSurveyActive && <hr className={styles.activeLine}/>}
|
||||||
</Link>
|
</Link>
|
||||||
<Link to='/my-surveys'
|
<Link
|
||||||
className={`${styles.pageLink} ${isMySurveysPage ? styles.active : ''}`}>
|
to='/my-surveys'
|
||||||
|
className={`${styles.pageLink} ${isMySurveysActive || isSurveyViewPage ? styles.active : ''}`}
|
||||||
|
>
|
||||||
Мои опросы
|
Мои опросы
|
||||||
{isMySurveysPage && <hr className={styles.activeLine}/>}
|
{(isMySurveysActive || isSurveyViewPage) && <hr className={styles.activeLine}/>}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to='/complete-survey'
|
||||||
|
className={`${styles.pageLink} ${isCompleteSurveyActive ? styles.active : ''}`}
|
||||||
|
>
|
||||||
|
Прохождение опроса
|
||||||
|
{isCompleteSurveyActive && <hr className={styles.activeLine}/>}
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<Account href={'/profile'} />
|
<Account href={'/profile'} />
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,16 @@ const LoginForm = () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try{
|
try{
|
||||||
|
if (email === '' || password === '')
|
||||||
|
setError('Заполните все поля')
|
||||||
|
else {
|
||||||
const responseData = await authUser({email, password});
|
const responseData = await authUser({email, password});
|
||||||
if (responseData && !responseData.error)
|
if (responseData && !responseData.error)
|
||||||
navigate('/my-surveys');
|
navigate('/my-surveys');
|
||||||
else
|
else
|
||||||
setError('Неверный логин или пароль')
|
setError('Неверный логин или пароль')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch(err){
|
catch(err){
|
||||||
console.error('Ошибка при отправке запроса:', err);
|
console.error('Ошибка при отправке запроса:', err);
|
||||||
setError('Неверный логин или пароль')
|
setError('Неверный логин или пароль')
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,17 @@ export const MySurveyList = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [surveys, setSurveys] = useState<MySurveyItem[]>([]);
|
const [surveys, setSurveys] = useState<MySurveyItem[]>([]);
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
|
||||||
|
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSurvey = async () => {
|
const fetchSurvey = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -24,13 +35,6 @@ export const MySurveyList = () => {
|
||||||
setSurveys(surveysWithStatus);
|
setSurveys(surveysWithStatus);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при получении списка опросов:', error);
|
console.error('Ошибка при получении списка опросов:', error);
|
||||||
|
|
||||||
if (error instanceof Error && error.message.includes("401")) {
|
|
||||||
// Если ошибка 401, перенаправляем на страницу входа
|
|
||||||
navigate('/login');
|
|
||||||
} else {
|
|
||||||
alert("Ошибка при загрузке опросов: " + (error instanceof Error && error.message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchSurvey();
|
fetchSurvey();
|
||||||
|
|
@ -52,12 +56,6 @@ export const MySurveyList = () => {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при удалении опроса:', error);
|
console.error('Ошибка при удалении опроса:', error);
|
||||||
|
|
||||||
if (error instanceof Error && error.message.includes("401")) {
|
|
||||||
navigate('/login');
|
|
||||||
} else {
|
|
||||||
alert("Ошибка при удалении опроса: " + (error instanceof Error ? error.message : 'Неизвестная ошибка'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -76,7 +74,7 @@ export const MySurveyList = () => {
|
||||||
<h1 className={styles.title}>{survey.title}</h1>
|
<h1 className={styles.title}>{survey.title}</h1>
|
||||||
<h2 className={styles.description}>{survey.description}</h2>
|
<h2 className={styles.description}>{survey.description}</h2>
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.date}>Дата создания: {survey.createdBy}</span>
|
<span className={styles.date}>Дата создания: {formatDate(survey.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={`${styles.status} ${
|
<div className={`${styles.status} ${
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
.main {
|
.main {
|
||||||
background-color: #F6F6F6;
|
background-color: #F6F6F6;
|
||||||
width: 100%;
|
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 34px 10%;
|
padding-top: 34px;
|
||||||
|
padding-left: 12%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.survey {
|
.survey {
|
||||||
|
|
|
||||||
|
|
@ -4,65 +4,108 @@ import AddAnswerButton from "../AddAnswerButton/AddAnswerButton.tsx";
|
||||||
import TypeDropdown from "../TypeDropdown/TypeDropdown.tsx";
|
import TypeDropdown from "../TypeDropdown/TypeDropdown.tsx";
|
||||||
import styles from './QuestionItem.module.css'
|
import styles from './QuestionItem.module.css'
|
||||||
import Delete from '../../assets/deleteQuestion.svg?react';
|
import Delete from '../../assets/deleteQuestion.svg?react';
|
||||||
|
import {
|
||||||
|
addNewAnswerVariant,
|
||||||
|
deleteAnswerVariant,
|
||||||
|
getAnswerVariants,
|
||||||
|
updateAnswerVariant
|
||||||
|
} from "../../api/AnswerApi.ts";
|
||||||
|
import {useLocation} from "react-router-dom";
|
||||||
|
import TextareaAutosize from "react-textarea-autosize";
|
||||||
|
|
||||||
interface QuestionItemProps {
|
interface QuestionItemProps {
|
||||||
questionId: number;
|
questionId: number;
|
||||||
initialTextQuestion?: string;
|
initialTextQuestion?: string;
|
||||||
valueQuestion: string;
|
valueQuestion: string;
|
||||||
|
answerVariants: {id?: number, text: string}[];
|
||||||
onChangeQuestion: (valueQuestion: string) => void;
|
onChangeQuestion: (valueQuestion: string) => void;
|
||||||
|
onAnswerVariantsChange: (variants: {id?: number, text: string}[]) => void;
|
||||||
onDeleteQuestion: (index: number) => Promise<void>;
|
onDeleteQuestion: (index: number) => Promise<void>;
|
||||||
selectedType: 'single' | 'multiply'; // Уточняем тип
|
initialQuestionType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion';
|
||||||
setSelectedType: (type: 'single' | 'multiply') => void; // Уточняем тип
|
onQuestionTypeChange: (type: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => void;
|
||||||
|
|
||||||
|
surveyId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuestionItem: React.FC<QuestionItemProps> = ({questionId, initialTextQuestion = `Вопрос ${questionId}`,
|
const QuestionItem: React.FC<QuestionItemProps> = ({
|
||||||
valueQuestion, onChangeQuestion, onDeleteQuestion, setSelectedType, selectedType}) => {
|
questionId,
|
||||||
// const [selectedType, setSelectedType] = useState<'single' | 'multiply'>('single');
|
initialTextQuestion = `Вопрос`,
|
||||||
const [answerOption, setAnswerOption] = useState(['']);
|
valueQuestion,
|
||||||
|
answerVariants: initialAnswerVariants,
|
||||||
|
onChangeQuestion,
|
||||||
|
onAnswerVariantsChange,
|
||||||
|
onDeleteQuestion,
|
||||||
|
initialQuestionType,
|
||||||
|
onQuestionTypeChange,
|
||||||
|
surveyId
|
||||||
|
}) => {
|
||||||
const [textQuestion, setTextQuestion] = useState(initialTextQuestion);
|
const [textQuestion, setTextQuestion] = useState(initialTextQuestion);
|
||||||
const [isEditingQuestion, setIsEditingQuestion] = useState(false);
|
const [isEditingQuestion, setIsEditingQuestion] = useState(false);
|
||||||
|
|
||||||
const [selectedAnswers, setSelectedAnswers] = useState<number[]>([]);
|
const [selectedAnswers, setSelectedAnswers] = useState<number[]>([]);
|
||||||
|
const [questionType, setQuestionType] = useState<'SingleAnswerQuestion' | 'MultipleAnswerQuestion'>(initialQuestionType);
|
||||||
const textareaQuestionRef = useRef<HTMLTextAreaElement>(null);
|
const textareaQuestionRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const handleTypeChange = (type: 'single' | 'multiply') => {
|
const location = useLocation();
|
||||||
setSelectedType(type);
|
const isCompleteSurveyActive = location.pathname === '/complete-survey';
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTextQuestion(valueQuestion);
|
||||||
|
}, [valueQuestion]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setQuestionType(initialQuestionType);
|
||||||
|
}, [initialQuestionType]);
|
||||||
|
|
||||||
|
const handleTypeChange = (type: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => {
|
||||||
|
setQuestionType(type);
|
||||||
|
onQuestionTypeChange(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddAnswer = async () => {
|
||||||
|
if (!surveyId) {
|
||||||
|
onAnswerVariantsChange([...initialAnswerVariants, { text: '' }]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddAnswer = () => {
|
try {
|
||||||
setAnswerOption([...answerOption, '']);
|
const newAnswer = await addNewAnswerVariant(surveyId, questionId, { text: '' });
|
||||||
|
onAnswerVariantsChange([...initialAnswerVariants, {
|
||||||
|
id: newAnswer.id,
|
||||||
|
text: newAnswer.text
|
||||||
|
}]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при добавлении варианта ответа:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleQuestionClick = () => {
|
const handleQuestionClick = () => {
|
||||||
setIsEditingQuestion(true);
|
setIsEditingQuestion(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleTextareaQuestionChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleTextareaQuestionChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setTextQuestion(event.target.value);
|
setTextQuestion(event.target.value);
|
||||||
|
|
||||||
if (textareaQuestionRef.current) {
|
if (textareaQuestionRef.current) {
|
||||||
textareaQuestionRef.current.style.height = 'auto';
|
textareaQuestionRef.current.style.height = 'auto';
|
||||||
textareaQuestionRef.current.style.height = `${textareaQuestionRef.current.scrollHeight}px`;
|
textareaQuestionRef.current.style.height = `${textareaQuestionRef.current.scrollHeight}px`;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSaveQuestion = () => {
|
const handleSaveQuestion = () => {
|
||||||
setIsEditingQuestion(false);
|
setIsEditingQuestion(false);
|
||||||
onChangeQuestion(textQuestion);
|
onChangeQuestion(textQuestion);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleQuestionKeyDown = (keyDownEvent: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleQuestionKeyDown = (keyDownEvent: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (keyDownEvent.key === 'Enter') {
|
if (keyDownEvent.key === 'Enter') {
|
||||||
keyDownEvent.preventDefault();
|
keyDownEvent.preventDefault();
|
||||||
handleSaveQuestion()
|
handleSaveQuestion();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleQuestionBlur = () => {
|
const handleQuestionBlur = () => {
|
||||||
handleSaveQuestion()
|
handleSaveQuestion();
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditingQuestion && textareaQuestionRef.current) {
|
if (isEditingQuestion && textareaQuestionRef.current) {
|
||||||
|
|
@ -72,21 +115,49 @@ const QuestionItem: React.FC<QuestionItemProps> = ({questionId, initialTextQuest
|
||||||
}
|
}
|
||||||
}, [isEditingQuestion]);
|
}, [isEditingQuestion]);
|
||||||
|
|
||||||
const handleAnswerChange = (index: number, value: string) => {
|
const handleAnswerChange = async (index: number, value: string) => {
|
||||||
const newAnswerOption = [...answerOption];
|
const newAnswerVariants = [...initialAnswerVariants];
|
||||||
newAnswerOption[index] = value;
|
newAnswerVariants[index] = { ...newAnswerVariants[index], text: value };
|
||||||
setAnswerOption(newAnswerOption);
|
onAnswerVariantsChange(newAnswerVariants);
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteAnswer = (index: number) => {
|
if (surveyId && newAnswerVariants[index].id) {
|
||||||
const newAnswerOption = answerOption.filter((_, i) => i !== index);
|
try {
|
||||||
setAnswerOption(newAnswerOption);
|
await updateAnswerVariant(
|
||||||
setSelectedAnswers(selectedAnswers.filter((i) => i !== index));
|
surveyId,
|
||||||
|
questionId,
|
||||||
|
newAnswerVariants[index].id!,
|
||||||
|
{ text: value }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при обновлении варианта ответа:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleDeleteAnswer = async (index: number) => {
|
||||||
setTextQuestion(valueQuestion);
|
const answerToDelete = initialAnswerVariants[index];
|
||||||
}, [valueQuestion]);
|
|
||||||
|
if (surveyId && answerToDelete.id) {
|
||||||
|
try {
|
||||||
|
await deleteAnswerVariant(surveyId, questionId, answerToDelete.id);
|
||||||
|
const newAnswerVariants = initialAnswerVariants.filter((_, i) => i !== index);
|
||||||
|
onAnswerVariantsChange(newAnswerVariants);
|
||||||
|
setSelectedAnswers(selectedAnswers.filter((i) => i !== index));
|
||||||
|
|
||||||
|
if (surveyId) {
|
||||||
|
const variants = await getAnswerVariants(surveyId, questionId);
|
||||||
|
const answers = variants.map((v: { id: number, text: string }) => ({ id: v.id, text: v.text }));
|
||||||
|
onAnswerVariantsChange(answers);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при удалении варианта ответа:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const newAnswerVariants = initialAnswerVariants.filter((_, i) => i !== index);
|
||||||
|
onAnswerVariantsChange(newAnswerVariants);
|
||||||
|
setSelectedAnswers(selectedAnswers.filter((i) => i !== index));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteQuestion = async () => {
|
const handleDeleteQuestion = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -97,25 +168,43 @@ const QuestionItem: React.FC<QuestionItemProps> = ({questionId, initialTextQuest
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSelect = (index: number) => {
|
const toggleSelect = (index: number) => {
|
||||||
if (selectedType === 'single') {
|
if (initialQuestionType === 'SingleAnswerQuestion') {
|
||||||
|
// Для одиночного выбора: заменяем массив одним выбранным индексом
|
||||||
setSelectedAnswers([index]);
|
setSelectedAnswers([index]);
|
||||||
} else {
|
} else {
|
||||||
setSelectedAnswers((prev) => {
|
// Для множественного выбора: добавляем/удаляем индекс
|
||||||
if (prev.includes(index)) {
|
setSelectedAnswers(prev =>
|
||||||
return prev.filter((i) => i !== index);
|
prev.includes(index)
|
||||||
} else {
|
? prev.filter(i => i !== index)
|
||||||
return [...prev, index];
|
: [...prev, index]
|
||||||
}
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.questionCard}>
|
<div className={styles.questionCard}>
|
||||||
|
{isCompleteSurveyActive ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.questionContainer}>
|
||||||
|
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
|
||||||
|
</div>
|
||||||
|
{initialAnswerVariants.map((answer, index) => (
|
||||||
|
<AnswerOption
|
||||||
|
key={answer.id || index}
|
||||||
|
selectedType={initialQuestionType}
|
||||||
|
index={index + 1}
|
||||||
|
value={answer.text}
|
||||||
|
isSelected={selectedAnswers.includes(index)}
|
||||||
|
toggleSelect={() => toggleSelect(index)}
|
||||||
|
isCompleteSurveyActive={isCompleteSurveyActive}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className={styles.questionContainer}>
|
<div className={styles.questionContainer}>
|
||||||
<div className={styles.question}>
|
<div className={styles.question}>
|
||||||
{isEditingQuestion ? (
|
{isEditingQuestion ? (
|
||||||
<textarea
|
<TextareaAutosize
|
||||||
className={styles.questionTextarea}
|
className={styles.questionTextarea}
|
||||||
ref={textareaQuestionRef}
|
ref={textareaQuestionRef}
|
||||||
value={textQuestion === initialTextQuestion ? '' : textQuestion}
|
value={textQuestion === initialTextQuestion ? '' : textQuestion}
|
||||||
|
|
@ -130,15 +219,15 @@ const QuestionItem: React.FC<QuestionItemProps> = ({questionId, initialTextQuest
|
||||||
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
|
<h2 className={styles.textQuestion}>{textQuestion || initialTextQuestion}</h2>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<TypeDropdown selectedType={selectedType} onTypeChange={handleTypeChange}/>
|
<TypeDropdown selectedType={questionType} onTypeChange={handleTypeChange}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{answerOption.map((answerText, index) => (
|
{initialAnswerVariants.map((answer, index) => (
|
||||||
<AnswerOption
|
<AnswerOption
|
||||||
key={index}
|
key={answer.id || index}
|
||||||
selectedType={selectedType}
|
selectedType={questionType}
|
||||||
index={index + 1}
|
index={index + 1}
|
||||||
value={answerText}
|
value={answer.text}
|
||||||
onChange={(value) => handleAnswerChange(index, value)}
|
onChange={(value) => handleAnswerChange(index, value)}
|
||||||
onDelete={() => handleDeleteAnswer(index)}
|
onDelete={() => handleDeleteAnswer(index)}
|
||||||
toggleSelect={() => toggleSelect(index)}
|
toggleSelect={() => toggleSelect(index)}
|
||||||
|
|
@ -146,17 +235,16 @@ const QuestionItem: React.FC<QuestionItemProps> = ({questionId, initialTextQuest
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className={styles.questionActions}>
|
<div className={styles.questionActions}>
|
||||||
<AddAnswerButton
|
<AddAnswerButton onClick={handleAddAnswer} />
|
||||||
onClick={handleAddAnswer}
|
|
||||||
/>
|
|
||||||
<button className={styles.deleteQuestionButton} onClick={handleDeleteQuestion}>
|
<button className={styles.deleteQuestionButton} onClick={handleDeleteQuestion}>
|
||||||
Удалить{/**/}
|
Удалить
|
||||||
<Delete className={styles.basketImg}/>
|
<Delete className={styles.basketImg}/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default QuestionItem;
|
export default QuestionItem;
|
||||||
|
|
@ -1,2 +1,16 @@
|
||||||
/*QuestionsList.module.css*/
|
/*QuestionsList.module.css*/
|
||||||
|
|
||||||
|
.departur_button{
|
||||||
|
display: block;
|
||||||
|
margin: 10px auto;
|
||||||
|
padding: 25px 50.5px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: #3788D6;
|
||||||
|
color: white;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 24px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 0 7.4px 0 rgba(154, 202, 247, 1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import React, {useEffect, useState} from "react";
|
import React from "react";
|
||||||
import QuestionItem from "../QuestionItem/QuestionItem.tsx";
|
import QuestionItem from "../QuestionItem/QuestionItem.tsx";
|
||||||
import AddQuestionButton from "../AddQuestionButton/AddQuestionButton.tsx";
|
import AddQuestionButton from "../AddQuestionButton/AddQuestionButton.tsx";
|
||||||
import {deleteQuestion} from "../../api/QuestionApi.ts";
|
import {addNewQuestion, deleteQuestion, getListQuestions} from "../../api/QuestionApi.ts";
|
||||||
|
import {addNewAnswerVariant} from "../../api/AnswerApi.ts";
|
||||||
|
import {useLocation} from "react-router-dom";
|
||||||
|
import styles from './QuestionsList.module.css'
|
||||||
|
|
||||||
interface QuestionsListProps {
|
interface QuestionsListProps {
|
||||||
questions: Question[];
|
questions: Question[];
|
||||||
|
|
@ -12,28 +15,50 @@ interface QuestionsListProps {
|
||||||
export interface Question {
|
export interface Question {
|
||||||
id: number;
|
id: number;
|
||||||
text: string;
|
text: string;
|
||||||
questionType: 'singleanswerquestion' | 'multipleanswerquestion';
|
questionType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion';
|
||||||
|
answerVariants: Array<{
|
||||||
|
id?: number;
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, surveyId}) => {
|
const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, surveyId}) => {
|
||||||
const [selectedType, setSelectedType] = useState<'single' | 'multiply'>('single');
|
const location = useLocation();
|
||||||
|
const isCompleteSurveyActive = location.pathname === '/complete-survey';
|
||||||
|
|
||||||
const [localQuestionId, setLocalQuestionId] = useState(2); // Начинаем с 2, так как первый вопрос имеет ID=1
|
const handleAddQuestion = async () => {
|
||||||
|
if (!surveyId) {
|
||||||
const handleAddQuestion = () => {
|
|
||||||
const newQuestion: Question = {
|
const newQuestion: Question = {
|
||||||
id: localQuestionId,
|
id: questions.length + 1,
|
||||||
text: '',
|
text: '',
|
||||||
questionType: selectedType === 'single' ? 'singleanswerquestion' : 'multipleanswerquestion',
|
questionType: 'SingleAnswerQuestion',
|
||||||
|
answerVariants: [{ text: '' }],
|
||||||
};
|
};
|
||||||
setQuestions([...questions, newQuestion]);
|
setQuestions([...questions, newQuestion]);
|
||||||
setLocalQuestionId(localQuestionId + 1);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newQuestion = await addNewQuestion(surveyId, {
|
||||||
|
title: '',
|
||||||
|
questionType: 'SingleAnswerQuestion'
|
||||||
|
});
|
||||||
|
|
||||||
|
const questionToAdd: Question = {
|
||||||
|
id: newQuestion.id,
|
||||||
|
text: newQuestion.title,
|
||||||
|
questionType: newQuestion.questionType,
|
||||||
|
answerVariants: []
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const newAnswer = await addNewAnswerVariant(surveyId, newQuestion.id, { text: '' });
|
||||||
setLocalQuestionId(questions.length > 0 ?
|
questionToAdd.answerVariants = [{ id: newAnswer.id, text: newAnswer.text }];
|
||||||
Math.max(...questions.map(q => q.id)) + 1 : 1);
|
|
||||||
}, [questions]);
|
setQuestions([...questions, questionToAdd]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при добавлении вопроса:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleQuestionChange = (id: number, value: string) => {
|
const handleQuestionChange = (id: number, value: string) => {
|
||||||
const newQuestions = questions.map((question) =>
|
const newQuestions = questions.map((question) =>
|
||||||
|
|
@ -45,6 +70,8 @@ const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, s
|
||||||
const handleDeleteQuestion = async (id: number) => {
|
const handleDeleteQuestion = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
if (surveyId) {
|
if (surveyId) {
|
||||||
|
const listQuestions = await getListQuestions(surveyId);
|
||||||
|
if (listQuestions.find(q => q.id === id)) {
|
||||||
const response = await deleteQuestion(surveyId, id);
|
const response = await deleteQuestion(surveyId, id);
|
||||||
if (!response?.success) {
|
if (!response?.success) {
|
||||||
throw new Error('Не удалось удалить вопрос на сервере');
|
throw new Error('Не удалось удалить вопрос на сервере');
|
||||||
|
|
@ -57,12 +84,34 @@ const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setQuestions(newQuestions);
|
setQuestions(newQuestions);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
const questionsList = questions.filter(q => q.id !== id);
|
||||||
|
setQuestions(questionsList);
|
||||||
|
return;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при удалении вопроса:', error);
|
console.error('Ошибка при удалении вопроса:', error);
|
||||||
alert('Не удалось удалить вопрос: ' + (error instanceof Error ? error.message : 'Неизвестная ошибка'));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAnswerVariantsChange = (questionId: number, newAnswerVariants: {id?: number, text: string}[]) => {
|
||||||
|
const newQuestions = questions.map(question =>
|
||||||
|
question.id === questionId
|
||||||
|
? {...question, answerVariants: newAnswerVariants}
|
||||||
|
: question
|
||||||
|
);
|
||||||
|
setQuestions(newQuestions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuestionTypeChange = (questionId: number, newType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => {
|
||||||
|
setQuestions(questions.map(question =>
|
||||||
|
question.id === questionId
|
||||||
|
? {...question, questionType: newType}
|
||||||
|
: question
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{questions.map((question) => (
|
{questions.map((question) => (
|
||||||
|
|
@ -70,13 +119,19 @@ const QuestionsList: React.FC<QuestionsListProps> = ({questions, setQuestions, s
|
||||||
key={question.id}
|
key={question.id}
|
||||||
questionId={question.id}
|
questionId={question.id}
|
||||||
valueQuestion={question.text}
|
valueQuestion={question.text}
|
||||||
|
answerVariants={question.answerVariants}
|
||||||
|
onAnswerVariantsChange={(variants) => handleAnswerVariantsChange(question.id, variants)}
|
||||||
onDeleteQuestion={() => handleDeleteQuestion(question.id)}
|
onDeleteQuestion={() => handleDeleteQuestion(question.id)}
|
||||||
onChangeQuestion={(value) => handleQuestionChange(question.id, value)}
|
onChangeQuestion={(value) => handleQuestionChange(question.id, value)}
|
||||||
selectedType={selectedType}
|
initialQuestionType={question.questionType}
|
||||||
setSelectedType={setSelectedType}
|
onQuestionTypeChange={(type) => handleQuestionTypeChange(question.id, type)}
|
||||||
|
surveyId={surveyId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<AddQuestionButton onClick={handleAddQuestion} />
|
{!isCompleteSurveyActive ? <AddQuestionButton onClick={handleAddQuestion} /> : (
|
||||||
|
<button className={styles.departur_button}>Отправить</button>
|
||||||
|
)}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
color: #000000;
|
color: #000000;
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
|
border-bottom: 2px solid rgba(0, 0, 0, 0.2);
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
@ -48,14 +48,13 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Отключаем стиль для input, когда в нём есть данные */
|
|
||||||
.input:not(:placeholder-shown) {
|
.input:not(:placeholder-shown) {
|
||||||
color: black;
|
color: black;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:focus {
|
.input:focus {
|
||||||
border-bottom: 1px solid black; /* Чёрная граница при фокусе */
|
border-bottom: 2px solid black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signUp{
|
.signUp{
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ const RegisterForm = () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try{
|
try{
|
||||||
|
if (email === '' || password === '' || firstName === '' || lastName === '') {
|
||||||
|
setError('Заполните все поля');
|
||||||
|
}
|
||||||
|
else{
|
||||||
const responseData = await registerUser({username, firstName, lastName, email, password});
|
const responseData = await registerUser({username, firstName, lastName, email, password});
|
||||||
if (responseData && !responseData.error) {
|
if (responseData && !responseData.error) {
|
||||||
console.log('Регистрация успешна');
|
console.log('Регистрация успешна');
|
||||||
|
|
@ -44,6 +48,7 @@ const RegisterForm = () => {
|
||||||
setError('Аккаунт с такой почтой уже зарегистрирован');
|
setError('Аккаунт с такой почтой уже зарегистрирован');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
if (err.message.includes('409')) {
|
if (err.message.includes('409')) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,148 @@
|
||||||
/*Results.module.css*/
|
/* Results.module.css */
|
||||||
|
|
||||||
.results{
|
.results {
|
||||||
width: 85%;
|
width: 85%;
|
||||||
|
margin: 19px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statsContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
margin: 30px 0;
|
||||||
|
gap: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 15px;
|
||||||
|
min-height: 180px;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.countAnswer {
|
||||||
|
width: 36%;
|
||||||
|
background-color: #65B953;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion_percentage {
|
||||||
|
width: 36%;
|
||||||
|
background-color: #EEDD59;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
width: 24%;
|
||||||
|
background-color: #A763EB;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statItem h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statItem p {
|
||||||
|
padding: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 40px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countAnswer p,
|
||||||
|
.completion_percentage p {
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imgGroup,
|
||||||
|
.imgSend {
|
||||||
|
width: 58px;
|
||||||
|
height: 61px;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status p {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: auto;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questionContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 25px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.questionContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 40px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.textContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 11px;
|
||||||
|
width: 30%;
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questionContainer h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #000000;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answerCount {
|
||||||
|
color: #000000;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartContainer {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pieContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 450px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 450px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 150px;
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,208 @@
|
||||||
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
|
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
|
||||||
import styles from './Results.module.css'
|
import styles from './Results.module.css';
|
||||||
import {useState} from "react";
|
import {Bar, Pie} from 'react-chartjs-2';
|
||||||
|
import {Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title} from 'chart.js';
|
||||||
|
import {useOutletContext} from "react-router-dom";
|
||||||
|
import {ISurvey} from "../../api/SurveyApi.ts";
|
||||||
|
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
||||||
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
|
import Group from '../../assets/gmail_groups.svg?react';
|
||||||
|
import Send from '../../assets/send.svg?react';
|
||||||
|
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
ArcElement, Tooltip, Legend,
|
||||||
|
CategoryScale, LinearScale, BarElement, Title, ChartDataLabels, annotationPlugin
|
||||||
|
);
|
||||||
|
|
||||||
|
// Типы для данных
|
||||||
|
interface QuestionStats {
|
||||||
|
questionText: string;
|
||||||
|
totalAnswers: number;
|
||||||
|
options: {
|
||||||
|
text: string;
|
||||||
|
percentage: number;
|
||||||
|
}[];
|
||||||
|
isMultipleChoice?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const Results = () => {
|
export const Results = () => {
|
||||||
const [descriptionSurvey, setDescriptionSurvey] = useState('');
|
const { survey, setSurvey } = useOutletContext<{
|
||||||
const [titleSurvey, setTitleSurvey] = useState('Название опроса');
|
survey: ISurvey;
|
||||||
|
setSurvey: (survey: ISurvey) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
return(
|
|
||||||
|
const surveyStats = {
|
||||||
|
totalParticipants: 100,
|
||||||
|
completionPercentage: 80,
|
||||||
|
status: 'Активен',
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
questionText: "Вопрос 1",
|
||||||
|
totalAnswers: 80,
|
||||||
|
options: [
|
||||||
|
{ text: "Вариант 1", percentage: 46 },
|
||||||
|
{ text: "Вариант 2", percentage: 15 },
|
||||||
|
{ text: "Вариант 3", percentage: 39 }
|
||||||
|
],
|
||||||
|
isMultipleChoice: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
questionText: "Вопрос 2",
|
||||||
|
totalAnswers: 100,
|
||||||
|
options: [
|
||||||
|
{ text: "Вариант 1", percentage: 50 },
|
||||||
|
{ text: "Вариант 2", percentage: 20 },
|
||||||
|
{ text: "Вариант 3", percentage: 100 },
|
||||||
|
{ text: "Вариант 4", percentage: 80 }
|
||||||
|
],
|
||||||
|
isMultipleChoice: true
|
||||||
|
}
|
||||||
|
] as QuestionStats[]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Цветовая палитра
|
||||||
|
const colorsForPie = ['#67C587', '#C9EAD4', '#EAF6ED'];
|
||||||
|
const colorsForBar = ['#8979FF'];
|
||||||
|
|
||||||
|
return (
|
||||||
<div className={styles.results}>
|
<div className={styles.results}>
|
||||||
<SurveyInfo
|
<SurveyInfo
|
||||||
titleSurvey={titleSurvey}
|
titleSurvey={survey.title}
|
||||||
descriptionSurvey={descriptionSurvey}
|
descriptionSurvey={survey.description}
|
||||||
setDescriptionSurvey={setDescriptionSurvey}
|
setDescriptionSurvey={(value) => setSurvey({ ...survey, description: value })}
|
||||||
setTitleSurvey={setTitleSurvey}
|
setTitleSurvey={(value) => setSurvey({ ...survey, title: value })}
|
||||||
|
/>
|
||||||
|
<div className={styles.statsContainer}>
|
||||||
|
<div className={`${styles.statItem} ${styles.countAnswer}`}>
|
||||||
|
<h3>Количество ответов</h3>
|
||||||
|
<div className={styles.result}>
|
||||||
|
<p>{surveyStats.totalParticipants}</p>
|
||||||
|
<Group className={styles.imgGroup}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.statItem} ${styles.completion_percentage}`}>
|
||||||
|
<h3>Процент завершения</h3>
|
||||||
|
<div className={styles.result}>
|
||||||
|
<p>{surveyStats.completionPercentage}%</p>
|
||||||
|
<Send className={styles.imgSend}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.statItem} ${styles.status}`}>
|
||||||
|
<h3>Статус опроса</h3>
|
||||||
|
<p>{surveyStats.status}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{surveyStats.questions.map((question, index) => (
|
||||||
|
<div key={index} className={styles.questionContainer}>
|
||||||
|
<div className={styles.questionContent}>
|
||||||
|
<div className={styles.textContainer}>
|
||||||
|
<h3>{question.questionText}</h3>
|
||||||
|
<p className={styles.answerCount}>Ответов: {question.totalAnswers}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.chartContainer}>
|
||||||
|
{question.isMultipleChoice ? (
|
||||||
|
<div className={styles.barContainer}>
|
||||||
|
<Bar
|
||||||
|
data={{
|
||||||
|
labels: question.options.map(opt => opt.text),
|
||||||
|
datasets: [{
|
||||||
|
label: '% выбравших',
|
||||||
|
data: question.options.map(opt => opt.percentage),
|
||||||
|
backgroundColor: colorsForBar,
|
||||||
|
borderColor: colorsForBar,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderSkipped: false,
|
||||||
|
}]
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: { enabled: true },
|
||||||
|
datalabels: { display: false },
|
||||||
|
annotation: {
|
||||||
|
annotations: question.options.map((opt, i) => ({
|
||||||
|
type: 'label',
|
||||||
|
xValue: i,
|
||||||
|
yValue: opt.percentage + 5,
|
||||||
|
content: `${opt.percentage}%`,
|
||||||
|
font: { size: 16, weight: 400 },
|
||||||
|
color: '#000'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
ticks: { callback: (val) => `${val}%` }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
color: '#000000',
|
||||||
|
font: {
|
||||||
|
size: 16,
|
||||||
|
weight: 400
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: { display: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
) : (
|
||||||
}
|
<div className={styles.pieContainer}>
|
||||||
|
<Pie
|
||||||
|
data={{
|
||||||
|
labels: question.options.map(opt => opt.text),
|
||||||
|
datasets: [{
|
||||||
|
data: question.options.map(opt => opt.percentage),
|
||||||
|
backgroundColor: colorsForPie,
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2
|
||||||
|
}]
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'right',
|
||||||
|
labels: {
|
||||||
|
color: '#000000',
|
||||||
|
font: {
|
||||||
|
size: 18,
|
||||||
|
weight: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx) => `${ctx.label}: ${ctx.raw}%`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
datalabels: {
|
||||||
|
formatter: (value) => `${value}%`,
|
||||||
|
color: '#000',
|
||||||
|
font: { weight: 400, size: 16 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: { animateRotate: true }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
.settingSurvey{
|
.settingSurvey{
|
||||||
width: 85%;
|
width: 85%;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.startEndTime{
|
.startEndTime{
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
padding-bottom: 97px;
|
padding-bottom: 97px;
|
||||||
padding-left: 19px;
|
padding-left: 19px;
|
||||||
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.param h2{
|
.param h2{
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,25 @@
|
||||||
import React, {useState} from 'react';
|
import React from 'react';
|
||||||
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
|
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
|
||||||
import styles from "./SettingSurvey.module.css";
|
import styles from "./SettingSurvey.module.css";
|
||||||
import TimeEvent from "../TimeEvent/TimeEvent.tsx";
|
import TimeEvent from "../TimeEvent/TimeEvent.tsx";
|
||||||
|
import SaveButton from "../SaveButton/SaveButton.tsx";
|
||||||
|
import {ISurvey} from "../../api/SurveyApi.ts";
|
||||||
|
import {useOutletContext} from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
const SettingSurvey: React.FC = () => {
|
const SettingSurvey: React.FC = () => {
|
||||||
const [descriptionSurvey, setDescriptionSurvey] = useState('');
|
const { survey, setSurvey } = useOutletContext<{
|
||||||
const [titleSurvey, setTitleSurvey] = useState('Название опроса');
|
survey: ISurvey;
|
||||||
|
setSurvey: (survey: ISurvey) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.settingSurvey}>
|
<div className={styles.settingSurvey}>
|
||||||
<SurveyInfo
|
<SurveyInfo
|
||||||
titleSurvey={titleSurvey}
|
titleSurvey={survey.title}
|
||||||
descriptionSurvey={descriptionSurvey}
|
descriptionSurvey={survey.description}
|
||||||
setDescriptionSurvey={setDescriptionSurvey}
|
setDescriptionSurvey={(value) => setSurvey({ ...survey, description: value })}
|
||||||
setTitleSurvey={setTitleSurvey}
|
setTitleSurvey={(value) => setSurvey({ ...survey, title: value })}
|
||||||
/>
|
/>
|
||||||
<div className={styles.startEndTime}>
|
<div className={styles.startEndTime}>
|
||||||
<TimeEvent title='Время начала'/>
|
<TimeEvent title='Время начала'/>
|
||||||
|
|
@ -23,6 +28,7 @@ const SettingSurvey: React.FC = () => {
|
||||||
<div className={styles.param}>
|
<div className={styles.param}>
|
||||||
<h2>Параметры видимости</h2>
|
<h2>Параметры видимости</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<SaveButton onClick={() => {}}/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import SaveButton from "../SaveButton/SaveButton.tsx";
|
||||||
import {ISurvey, postNewSurvey} from "../../api/SurveyApi.ts";
|
import {ISurvey, postNewSurvey} from "../../api/SurveyApi.ts";
|
||||||
import {addNewQuestion} from "../../api/QuestionApi.ts";
|
import {addNewQuestion} from "../../api/QuestionApi.ts";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
|
import {addNewAnswerVariant} from "../../api/AnswerApi.ts";
|
||||||
|
|
||||||
const Survey: React.FC = () => {
|
const Survey: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -14,25 +15,9 @@ const Survey: React.FC = () => {
|
||||||
const [survey] = useState<ISurvey | null>(null);
|
const [survey] = useState<ISurvey | null>(null);
|
||||||
|
|
||||||
const [questions, setQuestions] = useState<Question[]>([
|
const [questions, setQuestions] = useState<Question[]>([
|
||||||
{ id: 1, text: '', questionType: 'singleanswerquestion'},
|
{ id: 1, text: '', questionType: 'SingleAnswerQuestion', answerVariants: [{ text: '' }]},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// const handleSave = async () => {
|
|
||||||
// const savedSurvey = await postNewSurvey({title: titleSurvey, description: descriptionSurvey});
|
|
||||||
// setSurvey(savedSurvey);
|
|
||||||
// Promise.all(
|
|
||||||
// questions
|
|
||||||
// .map((question) => addNewQuestion( savedSurvey.id, {title: question.text, questionType: question.questionType })),
|
|
||||||
// )
|
|
||||||
// .then(() => {
|
|
||||||
// alert('Все удачно сохранилось');
|
|
||||||
// })
|
|
||||||
// .catch(() => {
|
|
||||||
// alert('Пиздец');
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
const savedSurvey = await postNewSurvey({
|
const savedSurvey = await postNewSurvey({
|
||||||
|
|
@ -40,20 +25,43 @@ const Survey: React.FC = () => {
|
||||||
description: descriptionSurvey
|
description: descriptionSurvey
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(
|
const updatedQuestions: Question[] = [];
|
||||||
questions.map(question =>
|
for (const question of questions) {
|
||||||
addNewQuestion(savedSurvey.id, {
|
const newQuestion = await addNewQuestion(savedSurvey.id, {
|
||||||
title: question.text,
|
title: question.text,
|
||||||
questionType: question.questionType
|
questionType: question.questionType
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const updatedQuestion: Question = {
|
||||||
|
...question,
|
||||||
|
id: newQuestion.id,
|
||||||
|
answerVariants: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (question.answerVariants && question.answerVariants.length > 0) {
|
||||||
|
const newVariants = await Promise.all(
|
||||||
|
question.answerVariants.map(answer =>
|
||||||
|
addNewAnswerVariant(
|
||||||
|
savedSurvey.id,
|
||||||
|
newQuestion.id,
|
||||||
|
{ text: answer.text }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
navigate('/my-surveys');
|
updatedQuestion.answerVariants = newVariants.map((variant: { id: number, text: string }) => ({
|
||||||
|
id: variant.id,
|
||||||
|
text: variant.text
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedQuestions.push(updatedQuestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuestions(updatedQuestions);
|
||||||
|
navigate('/my-surveys');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при сохранении:', error);
|
console.error('Ошибка при сохранении:', error);
|
||||||
alert('Не удалось сохранить опрос');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,37 +7,45 @@
|
||||||
margin-top: 34px;
|
margin-top: 34px;
|
||||||
margin-bottom: 49px;
|
margin-bottom: 49px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
min-height: 191px;
|
/*min-height: 191px;*/
|
||||||
|
/*max-height: 100vh;*/
|
||||||
|
max-height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info{
|
.info{
|
||||||
min-width: 373px;
|
min-width: 373px;
|
||||||
display: block;
|
/*display: block;*/
|
||||||
padding: 35px;
|
padding: 35px;
|
||||||
|
display: flex; /* Добавляем flex */
|
||||||
|
flex-direction: column; /* Элементы в колонку */
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.titleSurvey{
|
.titleSurvey{
|
||||||
|
width: 80%;
|
||||||
display: block;
|
display: block;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0 auto;
|
margin: 0 auto 13px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 23px;
|
/*margin-bottom: 23px;*/
|
||||||
|
/*margin-bottom: 15px;*/
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textareaTitle,
|
.textareaTitle,
|
||||||
.textareaDescrip {
|
.textareaDescrip {
|
||||||
width: 100%;
|
width: 80%;
|
||||||
|
max-width: 100%;
|
||||||
resize: none;
|
resize: none;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0 auto;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
display: block;
|
display: block;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
@ -48,7 +56,7 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
min-height: 40px;
|
min-height: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textareaDescrip {
|
.textareaDescrip {
|
||||||
|
|
@ -67,7 +75,7 @@
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 18px;
|
font-size: 24px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
@ -78,6 +86,15 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.desc{
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: white;
|
||||||
|
max-width: 80%;
|
||||||
|
word-break: break-word;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.descripButton{
|
.descripButton{
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -98,3 +115,10 @@
|
||||||
color: #7D7983;
|
color: #7D7983;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.createdAt{
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #7D7983;
|
||||||
|
}
|
||||||
|
|
@ -2,27 +2,34 @@ import React, {useState, useRef, useEffect} from "react";
|
||||||
import styles from './SurveyInfo.module.css'
|
import styles from './SurveyInfo.module.css'
|
||||||
import AddDescripImg from '../../assets/add_circle.svg?react';
|
import AddDescripImg from '../../assets/add_circle.svg?react';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
|
import {useLocation} from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
interface SurveyInfoProps {
|
interface SurveyInfoProps {
|
||||||
titleSurvey: string;
|
titleSurvey: string;
|
||||||
descriptionSurvey: string;
|
descriptionSurvey: string;
|
||||||
setDescriptionSurvey: (text: string) => void;
|
setDescriptionSurvey?: (text: string) => void;
|
||||||
setTitleSurvey: (text: string) => void;
|
setTitleSurvey?: (text: string) => void;
|
||||||
|
createdAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurvey, descriptionSurvey, setTitleSurvey}) => {
|
const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurvey, descriptionSurvey, setTitleSurvey, createdAt = new Date()}) => {
|
||||||
const [showDescriptionField, setShowDescriptionField] = useState(false);
|
const [showDescriptionField, setShowDescriptionField] = useState(false);
|
||||||
const [showNewTitleField, setShowNewTitleField] = useState(false);
|
const [showNewTitleField, setShowNewTitleField] = useState(false);
|
||||||
const titleTextareaRef = useRef<HTMLTextAreaElement>(null);
|
const titleTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const descriptionTextareaRef = useRef<HTMLTextAreaElement>(null);
|
const descriptionTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const isCompleteSurveyActive = location.pathname === '/complete-survey';
|
||||||
|
const isSurveyViewPage = location.pathname.startsWith('/survey/') &&
|
||||||
|
!location.pathname.startsWith('/survey/create');
|
||||||
|
|
||||||
const handleDescriptionChange = (descripEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleDescriptionChange = (descripEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setDescriptionSurvey(descripEvent.target.value);
|
setDescriptionSurvey?.(descripEvent.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNewTitleChange = (titleEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleNewTitleChange = (titleEvent: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setTitleSurvey(titleEvent.target.value);
|
setTitleSurvey?.(titleEvent.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -64,6 +71,7 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
|
||||||
setShowDescriptionField(true);
|
setShowDescriptionField(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const handleTitleBlur = () => {
|
const handleTitleBlur = () => {
|
||||||
setShowNewTitleField(false);
|
setShowNewTitleField(false);
|
||||||
};
|
};
|
||||||
|
|
@ -72,8 +80,52 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
|
||||||
setShowDescriptionField(false);
|
setShowDescriptionField(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addDate = () => {
|
||||||
|
const year = createdAt.getFullYear();
|
||||||
|
const month = String(createdAt.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(createdAt.getDate()).padStart(2, '0');
|
||||||
|
return `${day}/${month}/${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTitle = () => {
|
||||||
|
if (isCompleteSurveyActive) {
|
||||||
|
return (
|
||||||
|
<button className={styles.titleSurvey}>
|
||||||
|
<h1>{titleSurvey || 'Название опроса'}</h1>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showNewTitleField) {
|
||||||
|
return (
|
||||||
|
<h1 className={styles.titleSurvey}>
|
||||||
|
<TextareaAutosize
|
||||||
|
className={styles.textareaTitle}
|
||||||
|
ref={titleTextareaRef}
|
||||||
|
value={titleSurvey === 'Название опроса' ? '' : titleSurvey}
|
||||||
|
placeholder={'Название опроса'}
|
||||||
|
onChange={handleNewTitleChange}
|
||||||
|
onKeyDown={handleTitleKeyDown}
|
||||||
|
onBlur={handleTitleBlur}
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={styles.titleSurvey} onClick={handleAddNewTitleClick}>
|
||||||
|
<h1>{titleSurvey || 'Название опроса'}</h1>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderDescription = () => {
|
const renderDescription = () => {
|
||||||
|
if (isCompleteSurveyActive) {
|
||||||
|
return descriptionSurvey ? (
|
||||||
|
<p className={styles.desc}>{descriptionSurvey}</p>
|
||||||
|
) : 'Описание';
|
||||||
|
}
|
||||||
|
|
||||||
if (descriptionSurvey && !showDescriptionField) {
|
if (descriptionSurvey && !showDescriptionField) {
|
||||||
return (
|
return (
|
||||||
<button className={styles.description} onClick={handleParagraphClick}>
|
<button className={styles.description} onClick={handleParagraphClick}>
|
||||||
|
|
@ -95,7 +147,8 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={styles.descripButton}
|
className={styles.descripButton}
|
||||||
|
|
@ -106,35 +159,21 @@ const SurveyInfo: React.FC<SurveyInfoProps> = ({titleSurvey, setDescriptionSurve
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.blockInfo}>
|
<div className={styles.blockInfo}>
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
{
|
{renderTitle()}
|
||||||
showNewTitleField ? (
|
|
||||||
<h1 className={styles.titleSurvey}>
|
|
||||||
<TextareaAutosize className={styles.textareaTitle}
|
|
||||||
ref={titleTextareaRef}
|
|
||||||
value={titleSurvey === 'Название опроса' ? '' : titleSurvey}
|
|
||||||
placeholder={'Название опроса'}
|
|
||||||
onChange={handleNewTitleChange}
|
|
||||||
onKeyDown={handleTitleKeyDown}
|
|
||||||
onBlur={handleTitleBlur}
|
|
||||||
/>
|
|
||||||
</h1>
|
|
||||||
) : (
|
|
||||||
<button className={styles.titleSurvey} onClick={handleAddNewTitleClick}>
|
|
||||||
<h1>{titleSurvey || 'Название опроса'}</h1>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{renderDescription()}
|
{renderDescription()}
|
||||||
|
|
||||||
|
{(isSurveyViewPage || isCompleteSurveyActive) && createdAt && (
|
||||||
|
<p className={styles.createdAt}>Дата создания: {addDate()}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SurveyInfo;
|
export default SurveyInfo;
|
||||||
|
|
@ -1,30 +1,180 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
|
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
|
||||||
import QuestionsList, {Question} from "../QuestionsList/QuestionsList.tsx";
|
import QuestionsList, { Question } from "../QuestionsList/QuestionsList.tsx";
|
||||||
import {useEffect, useState} from "react";
|
import { ISurvey, updateSurvey } from "../../api/SurveyApi.ts";
|
||||||
import {getSurveyById, ISurvey, updateSurvey} from "../../api/SurveyApi.ts";
|
import {useOutletContext} from "react-router-dom";
|
||||||
import {useParams} from "react-router-dom";
|
import { addNewQuestion, getListQuestions, updateQuestion, deleteQuestion } from "../../api/QuestionApi.ts";
|
||||||
import {getListQuestions} from "../../api/QuestionApi.ts";
|
|
||||||
import styles from "./SurveyPage.module.css";
|
import styles from "./SurveyPage.module.css";
|
||||||
import SaveButton from "../SaveButton/SaveButton.tsx";
|
import SaveButton from "../SaveButton/SaveButton.tsx";
|
||||||
|
import { addNewAnswerVariant, deleteAnswerVariant, getAnswerVariants, IAnswerVariant, updateAnswerVariant } from "../../api/AnswerApi.ts";
|
||||||
|
|
||||||
|
type ActionType =
|
||||||
|
| 'update-survey'
|
||||||
|
| 'create-question'
|
||||||
|
| 'update-question'
|
||||||
|
| 'delete-question'
|
||||||
|
| 'create-answer'
|
||||||
|
| 'update-answer'
|
||||||
|
| 'delete-answer';
|
||||||
|
|
||||||
|
interface SurveyActionData {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuestionActionData {
|
||||||
|
surveyId: number;
|
||||||
|
id?: number;
|
||||||
|
title: string;
|
||||||
|
questionType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnswerActionData {
|
||||||
|
surveyId: number;
|
||||||
|
questionId: number;
|
||||||
|
id?: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Action {
|
||||||
|
type: ActionType;
|
||||||
|
data: SurveyActionData | QuestionActionData | AnswerActionData;
|
||||||
|
tempId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActionQueue {
|
||||||
|
private actions: Action[] = [];
|
||||||
|
private idMap: Record<number, number> = {};
|
||||||
|
|
||||||
|
add(action: Action) {
|
||||||
|
this.actions.push(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
for (const action of this.actions) {
|
||||||
|
try {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'update-survey':
|
||||||
|
await this.handleUpdateSurvey(action.data as SurveyActionData);
|
||||||
|
break;
|
||||||
|
case 'create-question': {
|
||||||
|
const createdQuestion = await this.handleCreateQuestion(action.data as QuestionActionData);
|
||||||
|
if (action.tempId) {
|
||||||
|
this.idMap[action.tempId] = createdQuestion.id;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'update-question':
|
||||||
|
await this.handleUpdateQuestion(action.data as QuestionActionData & { id: number });
|
||||||
|
break;
|
||||||
|
case 'delete-question':
|
||||||
|
await this.handleDeleteQuestion(action.data as QuestionActionData & { id: number });
|
||||||
|
break;
|
||||||
|
case 'create-answer': {
|
||||||
|
const answerData = action.data as AnswerActionData;
|
||||||
|
await this.handleCreateAnswer({
|
||||||
|
...answerData,
|
||||||
|
questionId: this.idMap[answerData.questionId] || answerData.questionId
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'update-answer': {
|
||||||
|
const updateAnswerData = action.data as AnswerActionData & { id: number };
|
||||||
|
await this.handleUpdateAnswer({
|
||||||
|
...updateAnswerData,
|
||||||
|
questionId: this.idMap[updateAnswerData.questionId] || updateAnswerData.questionId
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'delete-answer': {
|
||||||
|
const deleteAnswerData = action.data as AnswerActionData & { id: number };
|
||||||
|
await this.handleDeleteAnswer({
|
||||||
|
...deleteAnswerData,
|
||||||
|
questionId: this.idMap[deleteAnswerData.questionId] || deleteAnswerData.questionId
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to execute action ${action.type}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleUpdateSurvey(data: SurveyActionData) {
|
||||||
|
return await updateSurvey(data.id, {
|
||||||
|
title: data.title,
|
||||||
|
description: data.description
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCreateQuestion(data: QuestionActionData) {
|
||||||
|
return await addNewQuestion(data.surveyId, {
|
||||||
|
title: data.title,
|
||||||
|
questionType: data.questionType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleUpdateQuestion(data: QuestionActionData & { id: number }) {
|
||||||
|
return await updateQuestion(data.surveyId, data.id, {
|
||||||
|
title: data.title,
|
||||||
|
questionType: data.questionType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleDeleteQuestion(data: QuestionActionData & { id: number }) {
|
||||||
|
return await deleteQuestion(data.surveyId, data.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCreateAnswer(data: AnswerActionData) {
|
||||||
|
return await addNewAnswerVariant(data.surveyId, data.questionId, {
|
||||||
|
text: data.text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleUpdateAnswer(data: AnswerActionData & { id: number }) {
|
||||||
|
try {
|
||||||
|
const result = await updateAnswerVariant(data.surveyId, data.questionId, data.id, {
|
||||||
|
text: data.text
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleDeleteAnswer(data: AnswerActionData & { id: number }) {
|
||||||
|
return await deleteAnswerVariant(data.surveyId, data.questionId, data.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const SurveyPage: React.FC = () => {
|
export const SurveyPage: React.FC = () => {
|
||||||
const [survey, setSurvey] = useState<ISurvey | null>(null);
|
// const [survey, setSurvey] = useState<ISurvey | null>(null);
|
||||||
const [questions, setQuestions] = useState<Question[]>([]);
|
const [questions, setQuestions] = useState<Question[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { surveyId } = useParams<{ surveyId: string }>();
|
// const { surveyId } = useParams<{ surveyId: string }>();
|
||||||
|
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
|
|
||||||
|
const { survey, setSurvey } = useOutletContext<{
|
||||||
|
survey: ISurvey;
|
||||||
|
setSurvey: (survey: ISurvey) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!surveyId) {
|
if (!survey.id) {
|
||||||
console.error('Survey ID is missing');
|
console.error('Survey ID is missing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = parseInt(surveyId);
|
// const id = parseInt(survey.id);
|
||||||
|
const id = survey.id;
|
||||||
if (isNaN(id)) {
|
if (isNaN(id)) {
|
||||||
console.error('Invalid survey ID');
|
console.error('Invalid survey ID');
|
||||||
return;
|
return;
|
||||||
|
|
@ -33,52 +183,132 @@ export const SurveyPage: React.FC = () => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const surveyData = await getSurveyById(id);
|
// const surveyData = await getSurveyById(id);
|
||||||
setSurvey(surveyData);
|
setSurvey(survey);
|
||||||
|
setTitle(survey.title);
|
||||||
setTitle(surveyData.title);
|
setDescription(survey.description);
|
||||||
setDescription(surveyData.description);
|
|
||||||
|
|
||||||
const questionsData = await getListQuestions(id);
|
const questionsData = await getListQuestions(id);
|
||||||
const formattedQuestions = questionsData.map(q => ({
|
const formattedQuestions = await Promise.all(questionsData.map(async q => {
|
||||||
|
const answerVariants = await getAnswerVariants(id, q.id);
|
||||||
|
return {
|
||||||
id: q.id,
|
id: q.id,
|
||||||
text: q.title,
|
text: q.title,
|
||||||
questionType: q.questionType as 'singleanswerquestion' | 'multipleanswerquestion',
|
questionType: q.questionType as 'SingleAnswerQuestion' | 'MultipleAnswerQuestion',
|
||||||
|
answerVariants: answerVariants.map((a: IAnswerVariant) => ({
|
||||||
|
id: a.id,
|
||||||
|
text: a.text
|
||||||
|
}))
|
||||||
|
};
|
||||||
}));
|
}));
|
||||||
setQuestions(formattedQuestions);
|
setQuestions(formattedQuestions);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка:', error);
|
console.error('Ошибка:', error);
|
||||||
setError('Не удалось загрузить опрос')
|
setError('Не удалось загрузить опрос');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [surveyId]);
|
}, [survey.id]);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!survey.id || !survey) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
// const id = parseInt(survey.id);
|
||||||
|
const id = survey.id;
|
||||||
|
const actionQueue = new ActionQueue();
|
||||||
|
|
||||||
|
actionQueue.add({
|
||||||
|
type: 'update-survey',
|
||||||
|
data: { id, title, description } as SurveyActionData
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverQuestions = await getListQuestions(id);
|
||||||
|
|
||||||
|
questions.forEach(question => {
|
||||||
|
const serverQuestion = serverQuestions.find(q => q.id === question.id);
|
||||||
|
if (serverQuestion &&
|
||||||
|
(serverQuestion.title !== question.text ||
|
||||||
|
serverQuestion.questionType !== question.questionType)) {
|
||||||
|
actionQueue.add({
|
||||||
|
type: 'update-question',
|
||||||
|
data: {
|
||||||
|
surveyId: id,
|
||||||
|
id: question.id,
|
||||||
|
title: question.text,
|
||||||
|
questionType: question.questionType // Убедитесь, что передается новый тип
|
||||||
|
} as QuestionActionData & { id: number }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
questions.forEach(question => {
|
||||||
|
question.answerVariants.forEach(answer => {
|
||||||
|
if (!answer.id) {
|
||||||
|
actionQueue.add({
|
||||||
|
type: 'create-answer',
|
||||||
|
data: {
|
||||||
|
surveyId: id,
|
||||||
|
questionId: question.id,
|
||||||
|
text: answer.text
|
||||||
|
} as AnswerActionData
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
actionQueue.add({
|
||||||
|
type: 'update-answer',
|
||||||
|
data: {
|
||||||
|
surveyId: id,
|
||||||
|
questionId: question.id,
|
||||||
|
id: answer.id,
|
||||||
|
text: answer.text
|
||||||
|
} as AnswerActionData & { id: number }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
serverQuestions.forEach(serverQuestion => {
|
||||||
|
if (!questions.some(q => q.id === serverQuestion.id)) {
|
||||||
|
actionQueue.add({
|
||||||
|
type: 'delete-question',
|
||||||
|
data: {
|
||||||
|
surveyId: id,
|
||||||
|
id: serverQuestion.id
|
||||||
|
} as QuestionActionData & { id: number }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await actionQueue.execute();
|
||||||
|
|
||||||
|
const updatedQuestions = await getListQuestions(id);
|
||||||
|
const formattedQuestions = await Promise.all(updatedQuestions.map(async q => {
|
||||||
|
const answerVariants = await getAnswerVariants(id, q.id);
|
||||||
|
return {
|
||||||
|
id: q.id,
|
||||||
|
text: q.title,
|
||||||
|
questionType: q.questionType as 'SingleAnswerQuestion' | 'MultipleAnswerQuestion',
|
||||||
|
answerVariants: answerVariants.map((a: IAnswerVariant) => ({
|
||||||
|
id: a.id,
|
||||||
|
text: a.text
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
setQuestions(formattedQuestions);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка сохранения:', error);
|
||||||
|
setError(`Ошибка сохранения: ${error instanceof Error ? error.message : 'Неизвестная ошибка'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) return <div>Загрузка...</div>;
|
if (loading) return <div>Загрузка...</div>;
|
||||||
if (!survey) return <div>Опрос не найден</div>;
|
if (!survey) return <div>Опрос не найден</div>;
|
||||||
|
|
||||||
const handleSave = async() => {
|
|
||||||
if (!surveyId || !survey) return;
|
|
||||||
|
|
||||||
try{
|
|
||||||
setError(null);
|
|
||||||
const id = parseInt(surveyId);
|
|
||||||
const surveyUpdated = await updateSurvey(id, {
|
|
||||||
title: title,
|
|
||||||
description: description,
|
|
||||||
})
|
|
||||||
setSurvey(surveyUpdated);
|
|
||||||
}
|
|
||||||
catch(error){
|
|
||||||
console.error('Ошибка при сохранении опроса:', error);
|
|
||||||
setError('Не удалось сохранить изменения');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.survey_page}>
|
<div className={styles.survey_page}>
|
||||||
<SurveyInfo
|
<SurveyInfo
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
.inputDate{
|
.inputDate{
|
||||||
border: 3px solid #007AFF26;
|
border: 3px solid #007AFF26;
|
||||||
padding: 12px 107px 12px 21px;
|
padding: 12px 107px 12px 21px;
|
||||||
font-size: 20px; /*??????????????????????? в макете указано bodyLarge/Size*/
|
font-size: 20px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
.inputTime{
|
.inputTime{
|
||||||
border: 3px solid #007AFF26;
|
border: 3px solid #007AFF26;
|
||||||
padding: 12px 42px;
|
padding: 12px 42px;
|
||||||
font-size: 20px; /*??????????????????????? в макете указано bodyLarge/Size*/
|
font-size: 20px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
.selectedTypeIcon {
|
.selectedTypeIcon {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
width: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdownArrow {
|
.dropdownArrow {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import Multiple from '../../assets/check_box.svg?react';
|
||||||
import styles from './TypeDropdown.module.css'
|
import styles from './TypeDropdown.module.css'
|
||||||
|
|
||||||
interface TypeDropdownProps {
|
interface TypeDropdownProps {
|
||||||
selectedType: 'single' | 'multiply';
|
selectedType: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion';
|
||||||
onTypeChange: (type: 'single' | 'multiply') => void;
|
onTypeChange: (type: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TypeDropdown: React.FC<TypeDropdownProps> = ({selectedType, onTypeChange}) => {
|
const TypeDropdown: React.FC<TypeDropdownProps> = ({selectedType, onTypeChange}) => {
|
||||||
|
|
@ -18,7 +18,7 @@ const TypeDropdown: React.FC<TypeDropdownProps> = ({selectedType, onTypeChange})
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = (value: 'single' | 'multiply') => {
|
const handleSelect = (value: 'SingleAnswerQuestion' | 'MultipleAnswerQuestion') => {
|
||||||
onTypeChange(value);
|
onTypeChange(value);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
|
|
@ -39,15 +39,15 @@ const TypeDropdown: React.FC<TypeDropdownProps> = ({selectedType, onTypeChange})
|
||||||
return (
|
return (
|
||||||
<div className={styles.dropdownContainer} ref={dropdownRef}>
|
<div className={styles.dropdownContainer} ref={dropdownRef}>
|
||||||
<button className={styles.dropdownButton} onClick={handleToggle}>
|
<button className={styles.dropdownButton} onClick={handleToggle}>
|
||||||
{selectedType === 'single' ? <Single className={styles.selectedTypeIcon} /> : <Multiple className={styles.selectedTypeIcon} />}
|
{selectedType === 'SingleAnswerQuestion' ? <Single className={styles.selectedTypeIcon} /> : <Multiple className={styles.selectedTypeIcon} />}
|
||||||
{selectedType === "single" ? "Одиночный выбор" : "Множественный выбор"}
|
{selectedType === "SingleAnswerQuestion" ? "Одиночный выбор" : "Множественный выбор"}
|
||||||
{isOpen ? <DropUp className={styles.dropdownArrow} /> : <DropDown className={styles.dropdownArrow}/>}
|
{isOpen ? <DropUp className={styles.dropdownArrow} /> : <DropDown className={styles.dropdownArrow}/>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<ul className={styles.dropdownList}>
|
<ul className={styles.dropdownList}>
|
||||||
<li>
|
<li>
|
||||||
<button onClick={() => handleSelect("single")}
|
<button onClick={() => handleSelect("SingleAnswerQuestion")}
|
||||||
className={styles.dropdownItem}>
|
className={styles.dropdownItem}>
|
||||||
<Single className={styles.dropdownItemIcon}/>{' '}
|
<Single className={styles.dropdownItemIcon}/>{' '}
|
||||||
Одиночный выбор
|
Одиночный выбор
|
||||||
|
|
@ -55,7 +55,7 @@ const TypeDropdown: React.FC<TypeDropdownProps> = ({selectedType, onTypeChange})
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button className={styles.dropdownItem}
|
<button className={styles.dropdownItem}
|
||||||
onClick={() => handleSelect("multiply")}>
|
onClick={() => handleSelect('MultipleAnswerQuestion')}>
|
||||||
<Multiple className={styles.dropdownItemIcon}/>{' '}
|
<Multiple className={styles.dropdownItemIcon}/>{' '}
|
||||||
Множественный выбор
|
Множественный выбор
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.layout{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
14
SurveyFrontend/src/pages/CompleteSurvey/CompleteSurvey.tsx
Normal file
14
SurveyFrontend/src/pages/CompleteSurvey/CompleteSurvey.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import Header from "../../components/Header/Header.tsx";
|
||||||
|
import styles from './CompleteSurvey.module.css'
|
||||||
|
import CompletingSurvey from "../../components/CompletingSurvey/CompletingSurvey.tsx";
|
||||||
|
|
||||||
|
export const CompleteSurvey = () => {
|
||||||
|
return(
|
||||||
|
<div className={styles.layout}>
|
||||||
|
<Header/>
|
||||||
|
<CompletingSurvey/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CompleteSurvey
|
||||||
|
|
@ -3,15 +3,3 @@
|
||||||
.layout{
|
.layout{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main{
|
|
||||||
width: 100%;
|
|
||||||
min-height: 85vh;
|
|
||||||
display: flex;
|
|
||||||
background-color: #F6F6F6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content{
|
|
||||||
width: 100%;
|
|
||||||
margin-left: 8.9%;
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +1,47 @@
|
||||||
import Header from "../../components/Header/Header.tsx";
|
import Header from "../../components/Header/Header.tsx";
|
||||||
import Navigation from "../../components/Navigation/Navigation.tsx";
|
import Navigation from "../../components/Navigation/Navigation.tsx";
|
||||||
import styles from './SurveyCreateAndEditingPage.module.css'
|
import styles from './SurveyCreateAndEditingPage.module.css'
|
||||||
import { Outlet } from "react-router-dom";
|
import {Outlet, useParams, useLocation} from "react-router-dom";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {getSurveyById, ISurvey} from "../../api/SurveyApi.ts";
|
||||||
|
|
||||||
export const SurveyCreateAndEditingPage = () => {
|
export const SurveyCreateAndEditingPage = () => {
|
||||||
|
const { surveyId } = useParams<{ surveyId: string }>();
|
||||||
|
const location = useLocation();
|
||||||
|
const [survey, setSurvey] = useState<ISurvey | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const isCreateMode = location.pathname.includes('/survey/create');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCreateMode || !surveyId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const fetchSurvey = async () => {
|
||||||
|
try {
|
||||||
|
const data = await getSurveyById(parseInt(surveyId));
|
||||||
|
setSurvey(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка загрузки опроса:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSurvey();
|
||||||
|
}, [surveyId, isCreateMode]);
|
||||||
|
|
||||||
|
if (!isCreateMode) {
|
||||||
|
if (loading) return <div>Загрузка...</div>;
|
||||||
|
if (!survey) return <div>Опрос не найден</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
<Header />
|
<Header />
|
||||||
<div className={styles.main}>
|
<div className={styles.main}>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Outlet />
|
<Outlet context={{ survey, setSurvey }}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit a34c2d20fb04c25bc8120264e92f7ca02d7ed617
|
Subproject commit 7d47ab9e60645032bc41daee5535aba2b7eeebdf
|
||||||
Loading…
Add table
Add a link
Reference in a new issue