Merge branch 'unstable' into 'main'
A lot on frontend, fix cors on backend See merge request internship-2025/survey-webapp/survey-webapp!12
This commit is contained in:
commit
b667b8cfe9
33 changed files with 806 additions and 195 deletions
|
|
@ -92,6 +92,19 @@ public class Program
|
|||
c.IncludeXmlComments(filePath);
|
||||
});
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll",
|
||||
policyBuilder =>
|
||||
{
|
||||
policyBuilder
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.SetIsOriginAllowedToAllowWildcardSubdomains();
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
|
|
@ -102,6 +115,8 @@ public class Program
|
|||
options.SwaggerEndpoint("/api/swagger/v1/swagger.json", "Survey Backend V1");
|
||||
options.RoutePrefix = "api/swagger";
|
||||
});
|
||||
|
||||
app.UseCors("AllowAll");
|
||||
}
|
||||
|
||||
app.UseMiddleware<ExceptionsMiddleware>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,308 @@
|
|||
// <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("20250420143302_Changing questions logic")]
|
||||
partial class Changingquestionslogic
|
||||
{
|
||||
/// <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("AnswerVariant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SurveyLib.Core.Models.Completion", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("FinishedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("SurveyId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
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<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("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
|
||||
}
|
||||
}
|
||||
}
|
||||
61
SurveyFrontend/package-lock.json
generated
61
SurveyFrontend/package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
|||
"@formkit/tempo": "^0.1.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -1731,6 +1732,15 @@
|
|||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -2626,6 +2636,45 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz",
|
||||
"integrity": "sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"turbo-stream": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.2.tgz",
|
||||
"integrity": "sha512-yk1XW8Fj7gK7flpYBXF3yzd2NbX6P7Kxjvs2b5nu1M04rb5pg/Zc4fGdBNTeT4eDYL2bvzWNyKaIMJX/RKHTTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
|
|
@ -2720,6 +2769,12 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
|
@ -2798,6 +2853,12 @@
|
|||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"@formkit/tempo": "^0.1.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,35 @@
|
|||
import React from 'react';
|
||||
import './App.css'
|
||||
import Questions from './pages/Questions.tsx'
|
||||
import {BrowserRouter, Navigate, Route, Routes} from "react-router-dom";
|
||||
import {SurveyCreateAndEditingPage} from "./pages/SurveyCreateAndEditingPage/SurveyCreateAndEditingPage.tsx";
|
||||
import Survey from "./components/Survey/Survey.tsx";
|
||||
import SettingSurvey from "./components/SettingSurvey/SettingSurvey.tsx";
|
||||
import {MySurveysPage} from "./pages/MySurveysPage/MySurveysPage.tsx";
|
||||
import {Results} from "./components/Results/Results.tsx";
|
||||
import {MySurveyList} from "./components/MySurveyList/MySurveyList.tsx";
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<Questions />
|
||||
)
|
||||
const App = () => {
|
||||
return(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/survey/create/questions" replace />} />
|
||||
|
||||
<Route path="survey/create" element={<SurveyCreateAndEditingPage />}>
|
||||
<Route path="questions" element={<Survey />} />
|
||||
<Route path="settings" element={<SettingSurvey />} />
|
||||
</Route>
|
||||
|
||||
<Route path="my-surveys" element={<MySurveysPage />}>
|
||||
<Route index element={<MySurveyList />} />
|
||||
</Route>
|
||||
|
||||
<Route path='survey/:surveyId' element={<SurveyCreateAndEditingPage />}>
|
||||
<Route path="questions" element={<Survey />} />
|
||||
<Route path="settings" element={<SettingSurvey />} />
|
||||
<Route path="results" element={<Results />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
37
SurveyFrontend/src/api/AuthApi.ts
Normal file
37
SurveyFrontend/src/api/AuthApi.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import {BASE_URL, createRequestConfig, handleResponse} from "./BaseApi.ts";
|
||||
|
||||
interface IAuthData{
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface IRegistrationData extends IAuthData{
|
||||
username: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
}
|
||||
|
||||
export const registerUser = async (data: IRegistrationData) => {
|
||||
try{
|
||||
const response = await fetch(`${BASE_URL}/auth/register`, {
|
||||
...createRequestConfig('POST'), body: JSON.stringify(data),
|
||||
})
|
||||
return await handleResponse(response);
|
||||
} catch (error){
|
||||
console.error("Registration error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const authUser = async (data: IAuthData) => {
|
||||
try{
|
||||
const response = await fetch(`${BASE_URL}/auth/login`, {
|
||||
...createRequestConfig('POST'), body: JSON.stringify(data),
|
||||
})
|
||||
return await handleResponse(response);
|
||||
}
|
||||
catch(error){
|
||||
console.error("Login error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
50
SurveyFrontend/src/api/BaseApi.ts
Normal file
50
SurveyFrontend/src/api/BaseApi.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
const BASE_URL = "https://survey.slavagm.ru/api";
|
||||
|
||||
interface RequestConfig {
|
||||
method: string;
|
||||
headers: Record<string, string>;
|
||||
body?: BodyInit | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создаёт конфигурацию для fetch-запроса
|
||||
* @param method HTTP-метод (GET, POST, PUT, DELETE)
|
||||
* @param isFormData Флаг, указывающий, что отправляется FormData
|
||||
* @returns Конфигурация для fetch-запроса
|
||||
*/
|
||||
const createRequestConfig = (method: string, isFormData: boolean = false): RequestConfig => {
|
||||
const token = localStorage.getItem("accessToken");
|
||||
const config: RequestConfig = {
|
||||
method,
|
||||
headers: {},
|
||||
};
|
||||
|
||||
// Добавляем заголовок авторизации, если есть токен
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Добавляем Content-Type, если это не FormData
|
||||
if (!isFormData) {
|
||||
config.headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
/**
|
||||
* Обрабатывает ответ от сервера
|
||||
* @param response Ответ от fetch
|
||||
* @returns Распарсенные данные или ошибку
|
||||
*/
|
||||
const handleResponse = async (response: Response) => {
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || "Произошла ошибка");
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export { BASE_URL, createRequestConfig, handleResponse };
|
||||
|
|
@ -7,18 +7,27 @@
|
|||
padding: 4.58px 13px 4.58px 4.58px;
|
||||
margin: 26px 33px 27px 0;
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.accountText{
|
||||
width: 100%;
|
||||
gap: 9px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: black;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.accountImg{
|
||||
vertical-align: middle;
|
||||
width: 55px;
|
||||
margin-right: 9px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
/*AddQuestionButton.module.css*/
|
||||
|
||||
.questionButton{
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -6,3 +6,38 @@
|
|||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pagesNav{
|
||||
display: flex;
|
||||
gap: 60px;
|
||||
list-style: none;
|
||||
align-items: center;
|
||||
margin-right: 40%;
|
||||
|
||||
}
|
||||
|
||||
.pageLink{
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #2A6DAE;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: #ffffff;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.active{
|
||||
margin-bottom: -15px;
|
||||
color: #000000;
|
||||
text-decoration-color: #3881C8;
|
||||
}
|
||||
|
||||
.activeLine{
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
border: 1px solid #000000;
|
||||
width: 50%;
|
||||
padding: 0;
|
||||
box-shadow: 0 1px 4px 0 #3881C8;
|
||||
}
|
||||
|
|
@ -1,25 +1,34 @@
|
|||
import React, {useState} from "react";
|
||||
import React from "react";
|
||||
import Logo from "../Logo/Logo.tsx";
|
||||
import Account from "../Account/Account.tsx";
|
||||
import styles from './Header.module.css'
|
||||
import SurveyPagesList from "../SurveyPagesList/SurveyPagesList.tsx";
|
||||
import {Link, useLocation} from "react-router-dom";
|
||||
|
||||
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const [activePage, setActivePage] = useState('Создать опрос');
|
||||
|
||||
const handlePageClick = (name: string)=> {
|
||||
setActivePage(name);
|
||||
}
|
||||
const location = useLocation();
|
||||
const isCreateSurveyActive = location.pathname.includes('/survey/create');
|
||||
const isSurveyPage = location.pathname.includes('/survey/') && !location.pathname.includes('/survey/create');
|
||||
const isMySurveysPage = location.pathname === '/my-surveys' || isSurveyPage;
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<Logo href='' />
|
||||
<SurveyPagesList
|
||||
activePage={activePage}
|
||||
onPageClick = {handlePageClick}
|
||||
/>
|
||||
<Logo href='/' />
|
||||
<nav className={styles.pagesNav}>
|
||||
<Link to='/survey/create/questions'
|
||||
className={`${styles.pageLink} ${isCreateSurveyActive ? styles.active : ''}`}>
|
||||
Создать опрос
|
||||
{isCreateSurveyActive && <hr className={styles.activeLine}/>}
|
||||
</Link>
|
||||
<Link to='/my-surveys'
|
||||
className={`${styles.pageLink} ${isMySurveysPage ? styles.active : ''}`}>
|
||||
Мои опросы
|
||||
{isMySurveysPage && <hr className={styles.activeLine}/>}
|
||||
</Link>
|
||||
</nav>
|
||||
<Account
|
||||
href=''
|
||||
href='/profile'
|
||||
user='Иванов Иван'
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
/*MainComponent.module.css*/
|
||||
|
||||
.mainPage{
|
||||
width: 100%;
|
||||
min-height: 85vh;
|
||||
display: flex;
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import Navigation from "../Navigation/Navigation.tsx";
|
||||
import React, {useState} from "react";
|
||||
import styles from './MainComponent.module.css'
|
||||
import Survey from "../Survey/Survey.tsx";
|
||||
import SettingSurvey from "../SettingSurvey/SettingSurvey.tsx";
|
||||
|
||||
const MainComponent: React.FC = () => {
|
||||
const [activePage, setActivePage] = useState(
|
||||
localStorage.getItem("activePage") || "Вопросы"
|
||||
);
|
||||
|
||||
const handleNavigationClick = (title: string) => {
|
||||
setActivePage(title);
|
||||
localStorage.setItem('activePage', title);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={styles.mainPage}>
|
||||
<Navigation
|
||||
activePage={activePage}
|
||||
onNavigationClick={handleNavigationClick}
|
||||
/>
|
||||
{ activePage === 'Вопросы' && <Survey />}
|
||||
{activePage === 'Настройки' && <SettingSurvey />}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainComponent;
|
||||
60
SurveyFrontend/src/components/MySurveyList/MySurveyList.tsx
Normal file
60
SurveyFrontend/src/components/MySurveyList/MySurveyList.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import styles from './MySurveysList.module.css'
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
interface MySurveyItem{
|
||||
id: string,
|
||||
title: string,
|
||||
description: string,
|
||||
date: string
|
||||
status: 'active' | 'completed'
|
||||
}
|
||||
|
||||
export const MySurveyList = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const surveys: MySurveyItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Опрос 1',
|
||||
description: 'Описание опроса 1',
|
||||
date: '27-04-2025',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Опрос 2',
|
||||
description: 'Описание опроса 2',
|
||||
date: '01-01-2025',
|
||||
status: 'completed',
|
||||
}
|
||||
]
|
||||
|
||||
const handleSurveyClick = (id: string) => {
|
||||
navigate(`/survey/${id}/questions`)
|
||||
}
|
||||
|
||||
return(
|
||||
<div className={styles.main}>
|
||||
{surveys.map((survey) => (
|
||||
<button
|
||||
key={survey.id}
|
||||
className={styles.survey}
|
||||
onClick={() => handleSurveyClick(survey.id)}
|
||||
>
|
||||
<div className={styles.textContent}>
|
||||
<div className={styles.surveyData}>
|
||||
<h1 className={styles.title}>{survey.title}</h1>
|
||||
<h2 className={styles.description}>{survey.description}</h2>
|
||||
</div>
|
||||
<span className={styles.date}>Дата создания: {survey.date}</span>
|
||||
</div>
|
||||
<div className={`${styles.status} ${
|
||||
survey.status === 'active' ? styles.active : styles.completed
|
||||
}`}>
|
||||
{survey.status === 'active' ? 'Активен' : 'Завершён'}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
.main {
|
||||
background-color: #F6F6F6;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
padding: 34px 10%;
|
||||
}
|
||||
|
||||
.survey {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: white;
|
||||
width: 79%;
|
||||
border-radius: 14px;
|
||||
padding: 29px 36px 29px 54px;
|
||||
margin-bottom: 23px;
|
||||
gap: 20px;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.textContent {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status {
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
padding: 15px 47px;
|
||||
border-radius: 15px;
|
||||
color: #FFFFFF;
|
||||
white-space: nowrap;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.completed {
|
||||
background-color: #B0B0B0;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #65B953;
|
||||
}
|
||||
|
||||
.surveyData {
|
||||
margin-bottom: 33px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #7D7983;
|
||||
}
|
||||
|
|
@ -1,15 +1,31 @@
|
|||
import React from 'react'
|
||||
import {useLocation, useNavigate} from 'react-router-dom'
|
||||
import styles from './Navigation.module.css'
|
||||
import NavigationItem from "../NavigationItem/NavigationItem.tsx";
|
||||
import SaveButton from "../SaveButton/SaveButton.tsx";
|
||||
|
||||
interface NavigationProps {
|
||||
onNavigationClick: (title: string) => void;
|
||||
activePage: string
|
||||
}
|
||||
const Navigation: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const Navigation: React.FC<NavigationProps> = ({onNavigationClick, activePage}) => {
|
||||
const items: string[] = ['Вопросы', 'Настройки', 'Результаты']
|
||||
const isSurveyPage = /\/survey\/[^/]+/.test(location.pathname);
|
||||
const isNotCreateSurvey = !location.pathname.includes('/survey/create');
|
||||
const isMySurveysPage = isSurveyPage && isNotCreateSurvey;
|
||||
|
||||
const activePage = location.pathname.split('/').pop() ?? 'questions';
|
||||
|
||||
const baseItems = [
|
||||
{id: 'questions', title: 'Вопросы'},
|
||||
{id: 'settings', title: 'Настройки'}
|
||||
];
|
||||
|
||||
const items = isMySurveysPage
|
||||
? [...baseItems, {id: 'results', title: 'Результаты'}]
|
||||
: baseItems;
|
||||
|
||||
const handleNavigationClick = (pageId: string) => {
|
||||
navigate(`${pageId}`, { relative: 'path' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.navContainer}>
|
||||
|
|
@ -17,15 +33,14 @@ const Navigation: React.FC<NavigationProps> = ({onNavigationClick, activePage})
|
|||
<ul className={styles.navList}>
|
||||
{items.map(item => (
|
||||
<NavigationItem
|
||||
key={item}
|
||||
title={item}
|
||||
isActive={activePage === item}
|
||||
onClick={() => onNavigationClick(item)}
|
||||
key={item.id}
|
||||
title={item.title}
|
||||
isActive={activePage === item.id}
|
||||
onClick={() => handleNavigationClick(item.id)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<SaveButton />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
/*PageSurvey.module.css*/
|
||||
|
||||
.pagesSurveyItem{
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pageSurvey{
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #2A6DAE;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: #ffffff;
|
||||
padding-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.active{
|
||||
color: #000000;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #3881C8;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import styles from './PageSurvey.module.css';
|
||||
|
||||
interface PageSurveyProps{
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
onClick(): void;
|
||||
}
|
||||
|
||||
const PageSurvey: React.FC<PageSurveyProps> = ({name, isActive, onClick}) => {
|
||||
return (
|
||||
<li className={styles.pagesSurveyItem}>
|
||||
<button className={`${styles.pageSurvey} ${isActive ? styles.active : ''}`} onClick={onClick}>
|
||||
{name}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSurvey;
|
||||
5
SurveyFrontend/src/components/Results/Results.module.css
Normal file
5
SurveyFrontend/src/components/Results/Results.module.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/*Results.module.css*/
|
||||
|
||||
.results{
|
||||
width: 85%;
|
||||
}
|
||||
10
SurveyFrontend/src/components/Results/Results.tsx
Normal file
10
SurveyFrontend/src/components/Results/Results.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import SurveyInfo from "../SurveyInfo/SurveyInfo.tsx";
|
||||
import styles from './Results.module.css'
|
||||
|
||||
export const Results = () => {
|
||||
return(
|
||||
<div className={styles.results}>
|
||||
<SurveyInfo />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
/*SettingSurvey.module.css*/
|
||||
|
||||
.settingSurvey{
|
||||
width: 65%;
|
||||
margin-left: 8.9%;
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.startEndTime{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
/*Survey.module.css*/
|
||||
|
||||
.survey{
|
||||
width: 65%;
|
||||
margin-left: 8.9%;
|
||||
width: 85%;
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
.info{
|
||||
min-width: 373px;
|
||||
display: block;
|
||||
padding: 35px; /*подумать нужно ли справа слева отступы*/
|
||||
padding: 35px;
|
||||
}
|
||||
|
||||
.titleSurvey{
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
/*SurveyPagesList.module.css*/
|
||||
|
||||
.listSurveyPages{
|
||||
display: flex;
|
||||
gap: 61px;
|
||||
list-style: none;
|
||||
align-items: center;
|
||||
margin-right: 40%;
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
import styles from './SurveyPagesList.module.css';
|
||||
import PageSurvey from "../PageSurvey/PageSurvey.tsx";
|
||||
|
||||
interface SurveyPagesListProps{
|
||||
activePage: string;
|
||||
onPageClick: (name: string) => void;
|
||||
}
|
||||
|
||||
const SurveyPagesList: React.FC<SurveyPagesListProps> = ({activePage, onPageClick}) => {
|
||||
const listPages: string[] = ['Создать опрос', 'Мои опросы']
|
||||
|
||||
return (
|
||||
<ul className={styles.listSurveyPages}>
|
||||
{listPages.map((page) => (
|
||||
<PageSurvey
|
||||
key={page}
|
||||
name={page}
|
||||
isActive={activePage === page}
|
||||
onClick={() => onPageClick(page)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default SurveyPagesList;
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
width: 23%;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
/*margin-right: 29px;*/
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
/*MySurveysPage.module.css*/
|
||||
|
||||
.layout{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main{
|
||||
width: 100%;
|
||||
min-height: 85vh;
|
||||
display: flex;
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
|
||||
.content{
|
||||
width: 100%;
|
||||
margin-left: 8.9%;
|
||||
}
|
||||
12
SurveyFrontend/src/pages/MySurveysPage/MySurveysPage.tsx
Normal file
12
SurveyFrontend/src/pages/MySurveysPage/MySurveysPage.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import styles from "./MySurveysPage.module.css";
|
||||
import {MySurveyList} from "../../components/MySurveyList/MySurveyList.tsx";
|
||||
import Header from "../../components/Header/Header.tsx";
|
||||
|
||||
export const MySurveysPage = () => {
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
<Header />
|
||||
<MySurveyList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import Header from '../components/Header/Header.tsx'
|
||||
import MainComponent from "../components/MainComponent/MainComponent.tsx";
|
||||
|
||||
const QuestionsPages: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<MainComponent />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuestionsPages;
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
import Header from "../components/Header/Header.tsx";
|
||||
|
||||
|
||||
const Results: React.FC = () => {
|
||||
return (
|
||||
<Header />
|
||||
)
|
||||
}
|
||||
|
||||
export default Results;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import Header from "../components/Header/Header.tsx";
|
||||
import MainComponent from "../components/MainComponent/MainComponent.tsx";
|
||||
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<MainComponent />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/*SurveyCreateAndEditingPage.module.css*/
|
||||
|
||||
.layout{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main{
|
||||
width: 100%;
|
||||
min-height: 85vh;
|
||||
display: flex;
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
|
||||
.content{
|
||||
width: 100%;
|
||||
margin-left: 8.9%;
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import Header from "../../components/Header/Header.tsx";
|
||||
import Navigation from "../../components/Navigation/Navigation.tsx";
|
||||
import styles from './SurveyCreateAndEditingPage.module.css'
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
export const SurveyCreateAndEditingPage = () => {
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
<Header />
|
||||
<div className={styles.main}>
|
||||
<Navigation />
|
||||
<div className={styles.content}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue