From d892fb26961bd1f9f80560f784daa809fe6c3b9c Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Sun, 2 Mar 2025 19:09:27 +0900 Subject: [PATCH] feat: added templates for user profile + competition constructor (if will be), started working on synchronizing front and back session --- services/frontend/bun.lock | 10 + services/frontend/package.json | 2 + services/frontend/src/App.tsx | 25 +- .../frontend/src/components/ui/dialog.tsx | 133 ++++++ services/frontend/src/components/ui/label.tsx | 22 + .../src/components/ui/radio-group.tsx | 43 ++ .../components/ConstructorHeader/index.tsx | 63 +++ .../pages/CompetitionConstructor/index.tsx | 89 ++++ .../components/TaskDescriptionField/index.tsx | 27 ++ .../components/TaskFileAttachments/index.tsx | 92 ++++ .../components/TaskNumberField/index.tsx | 26 ++ .../TaskRequirementsField/index.tsx | 27 ++ .../TaskSolutionTypeSelector/index.tsx | 41 ++ .../modules/TaskCreationModal/index.tsx | 101 +++++ .../CompetitionConstructor/modules/index.tsx | 0 .../components/CompetitionHeader/index.tsx | 2 +- .../src/pages/CompetitionSession/index.tsx | 71 +++- .../components/CompetitionTag/index.tsx | 26 -- .../frontend/src/pages/UserProfile/index.tsx | 398 ++++++++++++++++++ .../modules/UserAchievements/index.tsx | 45 ++ .../modules/UserStatistics/index.tsx | 0 services/frontend/src/shared/api/index.ts | 1 - services/frontend/src/shared/api/session.ts | 82 ++++ services/frontend/src/shared/mocks/mocks.ts | 118 +++++- services/frontend/src/shared/types.ts | 5 + 25 files changed, 1398 insertions(+), 51 deletions(-) create mode 100644 services/frontend/src/components/ui/dialog.tsx create mode 100644 services/frontend/src/components/ui/label.tsx create mode 100644 services/frontend/src/components/ui/radio-group.tsx create mode 100644 services/frontend/src/pages/CompetitionConstructor/components/ConstructorHeader/index.tsx create mode 100644 services/frontend/src/pages/CompetitionConstructor/index.tsx create mode 100644 services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/components/TaskDescriptionField/index.tsx create mode 100644 services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/components/TaskFileAttachments/index.tsx create mode 100644 services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/components/TaskNumberField/index.tsx create mode 100644 services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/components/TaskRequirementsField/index.tsx create mode 100644 services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/components/TaskSolutionTypeSelector/index.tsx create mode 100644 services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/index.tsx create mode 100644 services/frontend/src/pages/CompetitionConstructor/modules/index.tsx delete mode 100644 services/frontend/src/pages/Competitions/components/CompetitionTag/index.tsx create mode 100644 services/frontend/src/pages/UserProfile/index.tsx create mode 100644 services/frontend/src/pages/UserProfile/modules/UserAchievements/index.tsx create mode 100644 services/frontend/src/pages/UserProfile/modules/UserStatistics/index.tsx create mode 100644 services/frontend/src/shared/api/session.ts diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock index 96bf77a..fad1d98 100644 --- a/services/frontend/bun.lock +++ b/services/frontend/bun.lock @@ -6,6 +6,8 @@ "dependencies": { "@monaco-editor/react": "^4.7.0", "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.3", "@tailwindcss/vite": "^4.0.9", @@ -157,12 +159,16 @@ "@radix-ui/react-id": ["@radix-ui/react-id@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw=="], + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="], + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.2.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-roving-focus": "1.1.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ=="], @@ -177,6 +183,10 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.9", "", { "os": "android", "cpu": "arm" }, "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.9", "", { "os": "android", "cpu": "arm64" }, "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg=="], diff --git a/services/frontend/package.json b/services/frontend/package.json index 85a85ec..2134a7d 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -12,6 +12,8 @@ "dependencies": { "@monaco-editor/react": "^4.7.0", "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.3", "@tailwindcss/vite": "^4.0.9", diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index de38f23..a0aa08b 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -1,5 +1,5 @@ -import { Routes, Route } from "react-router"; import "./styles/globals.css"; +import { Routes, Route } from "react-router"; import { NavbarLayout } from "./widgets/navbar-layout"; @@ -8,6 +8,8 @@ import Competition from "./pages/Competition"; import CompetitionSession from "./pages/CompetitionSession"; import LoginPage from "./pages/Login"; import { AuthLayout } from "./widgets/auth-layout"; +import CompetitionConstructor from "./pages/CompetitionConstructor"; +import UserProfile from "./pages/UserProfile"; const App = () => { return ( @@ -24,6 +26,27 @@ const App = () => { path="/competition/:id/tasks/:taskId" element={} /> + + } + /> + + } + /> + + } + /> + + } + /> + ); diff --git a/services/frontend/src/components/ui/dialog.tsx b/services/frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..b8b9407 --- /dev/null +++ b/services/frontend/src/components/ui/dialog.tsx @@ -0,0 +1,133 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/shared/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/services/frontend/src/components/ui/label.tsx b/services/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..73ec5bf --- /dev/null +++ b/services/frontend/src/components/ui/label.tsx @@ -0,0 +1,22 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/shared/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/services/frontend/src/components/ui/radio-group.tsx b/services/frontend/src/components/ui/radio-group.tsx new file mode 100644 index 0000000..89a0f27 --- /dev/null +++ b/services/frontend/src/components/ui/radio-group.tsx @@ -0,0 +1,43 @@ +import * as React from "react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" +import { CircleIcon } from "lucide-react" + +import { cn } from "@/shared/lib/utils" + +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { RadioGroup, RadioGroupItem } diff --git a/services/frontend/src/pages/CompetitionConstructor/components/ConstructorHeader/index.tsx b/services/frontend/src/pages/CompetitionConstructor/components/ConstructorHeader/index.tsx new file mode 100644 index 0000000..04442d1 --- /dev/null +++ b/services/frontend/src/pages/CompetitionConstructor/components/ConstructorHeader/index.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Task } from "@/shared/types"; +import { Settings, Plus } from 'lucide-react'; +import { Button } from "@/components/ui/button"; + +interface ConstructorHeaderProps { + title: string; + tasks: Task[]; + competitionId: string; + onAddTaskClick: () => void; +} + +const ConstructorHeader: React.FC = ({ + title, + tasks, + competitionId, + onAddTaskClick +}) => { + return ( +
+
+
+

+ {title} +

+
+ +
+ + + + + {tasks.map((task) => ( + + {task.number} + + ))} + + +
+
+
+ ); +}; + +export default ConstructorHeader; \ No newline at end of file diff --git a/services/frontend/src/pages/CompetitionConstructor/index.tsx b/services/frontend/src/pages/CompetitionConstructor/index.tsx new file mode 100644 index 0000000..4f7f247 --- /dev/null +++ b/services/frontend/src/pages/CompetitionConstructor/index.tsx @@ -0,0 +1,89 @@ +import { useState } from "react"; +import { useParams, Navigate, useNavigate } from "react-router-dom"; +import { Task, TaskStatus } from "@/shared/types"; +import ConstructorHeader from "./components/ConstructorHeader"; +import TaskCreationModal from "./modules/TaskCreationModal"; + +const CompetitionConstructor = () => { + const { id, taskId } = useParams<{ id: string; taskId?: string }>(); + const navigate = useNavigate(); + const [competitionTitle, setCompetitionTitle] = useState("Новая олимпиада"); + const [tasks, setTasks] = useState([]); + const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); + + const isSettings = taskId === "settings"; + + const handleOpenTaskModal = () => { + setIsTaskModalOpen(true); + }; + + const handleCloseTaskModal = () => { + setIsTaskModalOpen(false); + }; + + const handleCreateTask = (taskData: Partial) => { + const newTask: Task = { + id: `task-${Date.now()}`, + number: taskData.number || `${tasks.length + 1}`, + status: TaskStatus.Uncleared, + solutionType: taskData.solutionType || "input", + description: taskData.description || "", + requirements: taskData.requirements, + attachments: taskData.attachments || [] + }; + + setTasks([...tasks, newTask]); + setIsTaskModalOpen(false); + navigate(`/constructor/${id}/tasks/${newTask.id}`); + }; + + if (!taskId) { + if (tasks.length > 0) { + return ; + } else { + return ; + } + } + + return ( +
+ + + + +
+
+ {isSettings ? ( +
+

Настройки олимпиады

+

+ Здесь будет форма настроек олимпиады +

+
+ ) : ( +
+

+ {`Редактирование задачи ${tasks.find(t => t.id === taskId)?.number || ""}`} +

+

+ Здесь будет форма редактирования задачи +

+
+ )} +
+
+
+ ); +}; + +export default CompetitionConstructor; \ No newline at end of file diff --git a/services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/components/TaskDescriptionField/index.tsx b/services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/components/TaskDescriptionField/index.tsx new file mode 100644 index 0000000..c5f6876 --- /dev/null +++ b/services/frontend/src/pages/CompetitionConstructor/modules/TaskCreationModal/components/TaskDescriptionField/index.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; + +interface TaskDescriptionFieldProps { + description: string; + onChange: (value: string) => void; +} + +const TaskDescriptionField: React.FC = ({ description, onChange }) => { + return ( +
+ +