diff --git a/compose.yaml b/compose.yaml index a6f946f..42bcd33 100644 --- a/compose.yaml +++ b/compose.yaml @@ -22,6 +22,10 @@ services: restart: false condition: service_healthy required: true + checker: + restart: false + condition: service_healthy + required: true env_file: - path: ./infrastructure/backend/.env.template required: true @@ -384,6 +388,10 @@ services: restart: false condition: service_completed_successfully required: true + minio: + restart: false + condition: service_healthy + required: true env_file: - path: ./infrastructure/checker/.env.template required: true diff --git a/services/backend/api/v1/task/views.py b/services/backend/api/v1/task/views.py index 137e73e..e0f83cc 100644 --- a/services/backend/api/v1/task/views.py +++ b/services/backend/api/v1/task/views.py @@ -117,7 +117,10 @@ def submit_task( return status.FORBIDDEN, ForbiddenError() if task.type == CompetitionTask.CompetitionTaskType.INPUT: - verdict = content.read() == task.correct_answer_file.read() + user_input = content.read() + correct_answer = task.correct_answer_file.read() + verdict = user_input == correct_answer + print(user_input, correct_answer) submission = CompetitionTaskSubmission.objects.create( user=user, task=task, @@ -126,7 +129,7 @@ def submit_task( result={ "correct": verdict }, - earned_points=task.points + earned_points=task.points if verdict else 0 ) if task.type == CompetitionTask.CompetitionTaskType.REVIEW: submission = CompetitionTaskSubmission.objects.create( diff --git a/services/backend/apps/task/migrations/0003_alter_competitiontaskattachment_task.py b/services/backend/apps/task/migrations/0003_alter_competitiontaskattachment_task.py new file mode 100644 index 0000000..a6208ba --- /dev/null +++ b/services/backend/apps/task/migrations/0003_alter_competitiontaskattachment_task.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.6 on 2025-03-03 15:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('task', '0002_remove_competitiontasksubmission_plagiarism_checked_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='competitiontaskattachment', + name='task', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='task.competitiontask', verbose_name='задание'), + ), + ] diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index 77123b2..a23b315 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -111,7 +111,10 @@ class CompetitionTaskAttachment(BaseModel): return f"attachments/{instance.id}/file/{filename}" task = models.ForeignKey( - CompetitionTask, on_delete=models.CASCADE, verbose_name="задание" + CompetitionTask, + on_delete=models.CASCADE, + verbose_name="задание", + related_name="attachments", ) file = models.FileField(upload_to=file_upload_at, verbose_name="файл") bind_at = models.CharField(verbose_name="путь сохранения", max_length=255) diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock index fd86478..3448773 100644 --- a/services/frontend/bun.lock +++ b/services/frontend/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "@monaco-editor/react": "^4.7.0", "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", @@ -125,6 +126,14 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="], + "@floating-ui/core": ["@floating-ui/core@1.6.9", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.2", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], @@ -145,6 +154,8 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.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-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@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-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="], "@radix-ui/react-compose-refs": ["@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-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], @@ -157,6 +168,8 @@ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.5", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "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-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg=="], + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.6", "", { "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-id": "1.1.0", "@radix-ui/react-menu": "2.1.6", "@radix-ui/react-primitive": "2.0.2", "@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-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA=="], + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "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-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA=="], @@ -165,6 +178,10 @@ "@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-menu": ["@radix-ui/react-menu@2.1.6", "", { "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-dismissable-layer": "1.1.5", "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.2", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-popper": "1.2.2", "@radix-ui/react-portal": "1.1.4", "@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-slot": "1.1.2", "@radix-ui/react-use-callback-ref": "1.1.0", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "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-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.2", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "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-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA=="], + "@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=="], @@ -189,8 +206,12 @@ "@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-rect": ["@radix-ui/react-use-rect@1.1.0", "", { "dependencies": { "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ=="], + "@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=="], + "@radix-ui/rect": ["@radix-ui/rect@1.1.0", "", {}, "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="], + "@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 a9fc414..98bd7bf 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -12,6 +12,7 @@ "dependencies": { "@monaco-editor/react": "^4.7.0", "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-slot": "^1.1.2", diff --git a/services/frontend/public/lottie-1.png b/services/frontend/public/lottie-1.png new file mode 100644 index 0000000..b199c39 Binary files /dev/null and b/services/frontend/public/lottie-1.png differ diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index 554e52c..567f0e6 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -10,7 +10,8 @@ import LoginPage from "./pages/Login"; import { AuthLayout } from "./widgets/auth-layout"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import ReviewPage from "./pages/Review"; -import UserProfile from "./pages/UserProfile"; +import UserProfile from "./pages/Profile"; +import ProfilePage from "./pages/Profile"; const queryClient = new QueryClient(); @@ -24,14 +25,13 @@ const App = () => { }> } /> } /> + } /> } /> - - } /> } /> diff --git a/services/frontend/src/components/layout/header.tsx b/services/frontend/src/components/layout/header.tsx index 9ebacc2..27b0421 100644 --- a/services/frontend/src/components/layout/header.tsx +++ b/services/frontend/src/components/layout/header.tsx @@ -1,25 +1,19 @@ -import React, { useState } from "react"; import { DataRush } from "@/components/ui/icons/datarush"; -import { ChevronDown, User, Settings, BarChart2, LogOut } from "lucide-react"; -import { Link } from "react-router"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetClose, -} from "@/components/ui/sheet"; +import { ChevronDown } from "lucide-react"; +import { Link, useNavigate } from "react-router"; import { useUserStore } from "@/shared/stores/user"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { removeToken } from "@/shared/token"; -const Header = () => { +export const Header = () => { + const navigate = useNavigate(); const user = useUserStore((state) => state.user); - const clearUser = useUserStore((state) => state.clearUser); - const [isProfileOpen, setIsProfileOpen] = useState(false); - - const handleLogout = () => { - clearUser(); - setIsProfileOpen(false); - }; return (
@@ -27,88 +21,33 @@ const Header = () => { -
setIsProfileOpen(true)} - > - - {user?.username} - - -
+ + + + + + + Аккаунт + + + + { + removeToken(); + navigate("/login"); + }} + > + Выйти + + + - - - - - - Профиль - - - -
- } - label="Ваш профиль" - onClick={() => { - setIsProfileOpen(false); - }} - /> - - } - label="Настройки" - onClick={() => { - setIsProfileOpen(false); - }} - /> - - } - label="Статистика" - onClick={() => { - setIsProfileOpen(false); - }} - /> - -
- } - label="Выйти" - onClick={handleLogout} - /> -
-
-
-
); }; - -interface ProfileOptionProps { - icon: React.ReactNode; - label: string; - onClick: () => void; - className?: string; -} - -const ProfileOption: React.FC = ({ - icon, - label, - onClick, - className, -}) => { - return ( - - - - ); -}; - -export { Header }; diff --git a/services/frontend/src/components/ui/dialog.tsx b/services/frontend/src/components/ui/dialog.tsx index b8b9407..e6f8814 100644 --- a/services/frontend/src/components/ui/dialog.tsx +++ b/services/frontend/src/components/ui/dialog.tsx @@ -1,31 +1,31 @@ -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { XIcon } from "lucide-react"; -import { cn } from "@/shared/lib/utils" +import { cn } from "@/shared/lib/utils"; function Dialog({ ...props }: React.ComponentProps) { - return + return ; } function DialogTrigger({ ...props }: React.ComponentProps) { - return + return ; } function DialogPortal({ ...props }: React.ComponentProps) { - return + return ; } function DialogClose({ ...props }: React.ComponentProps) { - return + return ; } function DialogOverlay({ @@ -37,11 +37,11 @@ function DialogOverlay({ data-slot="dialog-overlay" className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", - className + className, )} {...props} /> - ) + ); } function DialogContent({ @@ -55,8 +55,8 @@ function DialogContent({ @@ -67,7 +67,7 @@ function DialogContent({ - ) + ); } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { @@ -77,7 +77,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { className={cn("flex flex-col gap-2 text-center sm:text-left", className)} {...props} /> - ) + ); } function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { @@ -86,11 +86,11 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { data-slot="dialog-footer" className={cn( "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", - className + className, )} {...props} /> - ) + ); } function DialogTitle({ @@ -103,7 +103,7 @@ function DialogTitle({ className={cn("text-lg leading-none font-semibold", className)} {...props} /> - ) + ); } function DialogDescription({ @@ -116,7 +116,7 @@ function DialogDescription({ className={cn("text-muted-foreground text-sm", className)} {...props} /> - ) + ); } export { @@ -130,4 +130,4 @@ export { DialogPortal, DialogTitle, DialogTrigger, -} +}; diff --git a/services/frontend/src/components/ui/dropdown-menu.tsx b/services/frontend/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..af8a1ca --- /dev/null +++ b/services/frontend/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; + +import { cn } from "@/shared/lib/utils"; + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: "default" | "destructive"; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/services/frontend/src/pages/Competitions/index.tsx b/services/frontend/src/pages/Competitions/index.tsx index e818fa2..1c43e18 100644 --- a/services/frontend/src/pages/Competitions/index.tsx +++ b/services/frontend/src/pages/Competitions/index.tsx @@ -61,10 +61,10 @@ const CompetitionsPage = () => { - В процессе + Прохожу - Завершенные + Завершено @@ -112,5 +112,4 @@ const SectionTitle = ({ children }: { children: React.ReactNode }) => { return

{children}

; }; - -export default CompetitionsPage; \ No newline at end of file +export default CompetitionsPage; diff --git a/services/frontend/src/pages/Profile/index.tsx b/services/frontend/src/pages/Profile/index.tsx new file mode 100644 index 0000000..d4b20c4 --- /dev/null +++ b/services/frontend/src/pages/Profile/index.tsx @@ -0,0 +1,38 @@ +import { User } from "@/shared/types/user"; +import { UserInfo } from "./widgets/user-info"; +import { UserAchievements } from "./widgets/user-achievements"; +import { UserStats } from "./widgets/user-stats"; +import { useQuery } from "@tanstack/react-query"; +import { getCurrentUser } from "@/shared/api/user"; +import { Loading } from "@/components/ui/loading"; +import { useNavigate } from "react-router"; + +const ProfilePage = () => { + const { data: user, isLoading } = useQuery({ + queryKey: ["user"], + queryFn: getCurrentUser, + }); + + const navigate = useNavigate(); + + if (isLoading) { + return ; + } + + if (!user) { + navigate("/"); + return; + } + + return ( +
+
+ + +
+ +
+ ); +}; + +export default ProfilePage; diff --git a/services/frontend/src/pages/Profile/widgets/user-achievements.tsx b/services/frontend/src/pages/Profile/widgets/user-achievements.tsx new file mode 100644 index 0000000..decd815 --- /dev/null +++ b/services/frontend/src/pages/Profile/widgets/user-achievements.tsx @@ -0,0 +1,69 @@ +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { Achievement } from "@/shared/types/user"; +import dayjs from "dayjs"; + +export const UserAchievements = ({ + achievements, +}: { + achievements?: Achievement[]; +}) => { + return ( +
+

Достижения

+ {achievements && ( +
+ {achievements.map((a) => ( + + + + ))} +
+ )} +
+ ); +}; + +const AchievementCard = ({ achievement }: { achievement: Achievement }) => { + return ( +
+
+ {achievement.name} +
+
+

{achievement.name}

+

+ {dayjs(achievement.received_at).format("D MMM YYYY")} +

+
+
+ ); +}; + +const AchievementDialog = ({ + achievement, + children, +}: { + achievement: Achievement; + children: React.ReactNode; +}) => { + return ( + + {children} + +
+
+ {achievement.name} +
+
+

{achievement.name}

+

+ Получено {dayjs(achievement.received_at).format("DD MMMM YYYY")} +

+
+ +

{achievement.description}

+
+
+
+ ); +}; diff --git a/services/frontend/src/pages/Profile/widgets/user-info.tsx b/services/frontend/src/pages/Profile/widgets/user-info.tsx new file mode 100644 index 0000000..3b3b927 --- /dev/null +++ b/services/frontend/src/pages/Profile/widgets/user-info.tsx @@ -0,0 +1,21 @@ +import { User } from "@/shared/types/user"; + +export const UserInfo = ({ user }: { user: User }) => { + return ( +
+ {user.avatar && ( +
+ {user.username} +
+ )} +
+

{user.username}

+

{user.email}

+
+
+ ); +}; diff --git a/services/frontend/src/pages/Profile/widgets/user-stats.tsx b/services/frontend/src/pages/Profile/widgets/user-stats.tsx new file mode 100644 index 0000000..5cfd4cb --- /dev/null +++ b/services/frontend/src/pages/Profile/widgets/user-stats.tsx @@ -0,0 +1,7 @@ +export const UserStats = () => { + return ( +
+

Аналитика

+
+ ); +}; diff --git a/services/frontend/src/pages/Review/components/review-card.tsx b/services/frontend/src/pages/Review/components/review-card.tsx index 75003f8..14b8d15 100644 --- a/services/frontend/src/pages/Review/components/review-card.tsx +++ b/services/frontend/src/pages/Review/components/review-card.tsx @@ -1,3 +1,4 @@ +import { cn } from "@/shared/lib/utils"; import { Review, ReviewStatus } from "@/shared/types/review"; import dayjs from "dayjs"; @@ -8,28 +9,52 @@ interface ReviewCardProps { export const ReviewCard = ({ review }: ReviewCardProps) => { const id = review.id.split("-").at(-1)?.slice(0, 6); + const score = review.evaluation?.reduce((acc, e) => acc + e.mark, 0); + const maxPoints = review.criteries?.reduce((acc, c) => acc + c.max_value, 0); + + const styles = review.review_status === ReviewStatus.CHECKED && { + "bg-correct text-correct-foreground": (score ?? 0) === (maxPoints ?? 0), + "bg-partial text-partial-foreground": + (score ?? 0) > 0 && (score ?? 0) < (maxPoints ?? 0), + "bg-wrong text-wrong-foreground": (score ?? 0) === 0, + }; + return ( -
-
-

+

+
+

{review.competition_name}

{review.task_title}

-
-
+
+

{id}

- {review.review_status === ReviewStatus.NOT_CHECKED + {review.review_status === ReviewStatus.NOT_CHECKED || + review.review_status === ReviewStatus.CHECKING ? `Дата посылки: ${dayjs(review.submitted_at).format("D MMMM, HH:mm")}` - : `Дата проверки: ${review.checked_at}`} + : `Дата проверки: ${dayjs(review.checked_at).format("D MMMM, HH:mm")}`}

- {review.review_status === ReviewStatus.NOT_CHECKED + {review.review_status === ReviewStatus.NOT_CHECKED || + review.review_status === ReviewStatus.CHECKING ? "Не проверено" - : ""} + : score === 0 + ? "Неверный ответ" + : `Зачтено ${score}/${maxPoints}`}

diff --git a/services/frontend/src/pages/Review/index.tsx b/services/frontend/src/pages/Review/index.tsx index 0701e14..80b1e98 100644 --- a/services/frontend/src/pages/Review/index.tsx +++ b/services/frontend/src/pages/Review/index.tsx @@ -1,5 +1,5 @@ import { Loading } from "@/components/ui/loading"; -import { getReviewer, getReviewerSubmissions } from "@/shared/api/review"; +import { getReviewer, getReviewSubmissions } from "@/shared/api/review"; import { useQuery } from "@tanstack/react-query"; import { useNavigate, useParams } from "react-router"; import { ReviewHeader } from "./modules/review-header"; @@ -8,6 +8,8 @@ import { ReviewsList } from "./modules/reviews-list"; import React from "react"; import { ReviewStatus } from "@/shared/types/review"; +const TokenContext = React.createContext(null); + const ReviewPage = () => { const { token } = useParams<{ token: string }>(); const navigate = useNavigate(); @@ -19,23 +21,29 @@ const ReviewPage = () => { }); const submissionsQuery = useQuery({ queryKey: ["submissions", token], - queryFn: async () => getReviewerSubmissions(token || ""), + queryFn: async () => getReviewSubmissions(token || ""), retry: 0, }); const availableReviews = React.useMemo( () => (submissionsQuery.data?.submissions || []).filter( - (s) => s.review_status === ReviewStatus.NOT_CHECKED, + (s) => + s.review_status === ReviewStatus.NOT_CHECKED || + s.review_status === ReviewStatus.CHECKING, ), [submissionsQuery.data], ); const checkedReviews = React.useMemo( () => - (submissionsQuery.data?.submissions || []).filter( - (s) => s.review_status === ReviewStatus.CHECKED, - ), + (submissionsQuery.data?.submissions || []) + .filter((s) => s.review_status === ReviewStatus.CHECKED) + .sort( + (a, b) => + new Date(b.checked_at ?? "").getTime() - + new Date(a.checked_at ?? "").getTime(), + ), [submissionsQuery.data], ); @@ -49,43 +57,53 @@ const ReviewPage = () => { } return ( -
-
- + +
+
+ - -
-

Решения

- - - Доступные - {availableReviews.length > 0 && ( -
- {availableReviews.length} -
- )} -
- Проверенные -
-
+ +
+

Решения

+ + + Доступные + {availableReviews.length > 0 && ( +
+ {availableReviews.length} +
+ )} +
+ Проверенные +
+
- - - + + + - - - -
+ + + +
+
-
+ ); }; +export const useToken = () => { + const token = React.useContext(TokenContext); + if (!token) { + throw new Error("useToken must be used within a TokenContext.Provider"); + } + return token; +}; + export default ReviewPage; diff --git a/services/frontend/src/pages/Review/modules/review-dialog.tsx b/services/frontend/src/pages/Review/modules/review-dialog.tsx new file mode 100644 index 0000000..b8b001c --- /dev/null +++ b/services/frontend/src/pages/Review/modules/review-dialog.tsx @@ -0,0 +1,300 @@ +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import React from "react"; +import { useToken } from ".."; +import { getReviewSubmission, postReviewEvaluation } from "@/shared/api/review"; +import { Loading } from "@/components/ui/loading"; +import { + Review, + ReviewCriteria, + ReviewEvaluation, +} from "@/shared/types/review"; +import dayjs from "dayjs"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/shared/lib/utils"; +import { ofetch } from "ofetch"; +import { File } from "lucide-react"; + +interface ReviewDialogProps { + reviewId: string; + children: React.ReactNode; +} + +export const ReviewDialog = ({ reviewId, children }: ReviewDialogProps) => { + return ( + + {children} + + + + + ); +}; + +const ReviewScreen = ({ reviewId }: { reviewId: string }) => { + const queryClient = useQueryClient(); + const token = useToken(); + + const { data: review, isLoading } = useQuery({ + queryKey: ["review", reviewId], + queryFn: async () => getReviewSubmission(token, reviewId), + }); + + const [evaluation, setEvaluation] = React.useState<{ + [key: string]: ReviewEvaluation; + }>({}); + + React.useEffect(() => { + if (review?.evaluation) { + setEvaluation( + review.evaluation.reduce( + (acc, e) => { + acc[e.slug] = e; + return acc; + }, + {} as { [key: string]: ReviewEvaluation }, + ), + ); + } + }, [review?.evaluation]); + + const onSubmit = React.useCallback(async () => { + const e: ReviewEvaluation[] | undefined = review?.criteries?.map((c) => { + return ( + evaluation[c.slug] ?? { + slug: c.slug, + mark: 0, + } + ); + }); + + if (!e) { + return; + } + + await postReviewEvaluation(token, reviewId, e); + queryClient.invalidateQueries({ + queryKey: ["submissions", token], + }); + }, [review?.criteries, evaluation, token, queryClient]); + + if (isLoading) { + return ; + } + + if (!review) { + queryClient.invalidateQueries({ + queryKey: ["submissions", token], + }); + return; + } + + return ( +
+
+ + + + +
+ +
+ ); +}; + +const ReviewHeader = ({ review }: { review: Review }) => { + const id = review.id.split("-").at(-1)?.slice(0, 6); + + return ( +
+
+

+ {review.competition_name} +

+

{review.task_title}

+
+ +
+ {id} + + {dayjs(review.submitted_at).format("D MMMM, HH:mm")} +
+
+ ); +}; + +const ReviewDescription = ({ review }: { review: Review }) => { + if (!review.description) { + return; + } + + return ( +
+

Условие

+
+ {review.description} +
+
+ ); +}; + +const ReviewContent = ({ review }: { review: Review }) => { + const extension = review.content.split(".").at(-1); + const filename = review.content.split("/").at(-1); + + const { data: content, isLoading } = useQuery({ + queryKey: ["review-file", review.id], + queryFn: async () => await ofetch(review.content), + }); + + if (isLoading) { + return null; + } + + return ( +
+

Ответ

+ +
+ {extension === "txt" ? ( + content + ) : ( + + + {filename} + + )} +
+
+ ); +}; + +const ReviewCriteriesList = ({ + review, + evaluation, + setEvaluation, +}: { + review: Review; + evaluation: { [key: string]: ReviewEvaluation }; + setEvaluation: React.Dispatch< + React.SetStateAction<{ + [key: string]: ReviewEvaluation; + }> + >; +}) => { + const onChange = React.useCallback( + (slug: string, value?: number) => { + if (!value || isNaN(value)) { + setEvaluation((prev) => ({ ...prev, [slug]: { slug, mark: 0 } })); + return; + } + + if ( + value < 0 || + value > + (review.criteries?.filter((c) => c.slug === slug).at(0)?.max_value ?? + 0) + ) { + return setEvaluation((prev) => ({ + ...prev, + [slug]: { slug, mark: 0 }, + })); + } + + setEvaluation((prev) => ({ ...prev, [slug]: { slug, mark: value } })); + }, + [evaluation], + ); + + return ( +
+

Критерии

+
+ {review.criteries?.map((c) => { + const value = evaluation[c.slug]?.mark; + return ( + + ); + })} +
+
+ ); +}; + +const Criteria = ({ + criteria, + value, + onChange, +}: { + criteria: ReviewCriteria; + value?: number; + onChange?: (slug: string, value: number) => void; +}) => { + return ( +
+
+

{criteria.name}

+

+ Максимальное значение — {criteria.max_value} +

+
+ onChange?.(criteria.slug, Number(e.target.value))} + /> +
+ ); +}; + +const ReviewFooter = ({ + evaluation, + criteries, + onSubmit, +}: { + evaluation: { [key: string]: ReviewEvaluation }; + criteries?: ReviewCriteria[]; + onSubmit: () => Promise; +}) => { + const score = Object.values(evaluation).reduce((acc, e) => acc + e.mark, 0); + const maxScore = criteries?.reduce((acc, c) => acc + c.max_value, 0); + + return ( +
button]:bg-correct-foreground [&>button]:hover:bg-correct-foreground/80 [&>button]:text-correct": + score === maxScore, + "bg-partial *:text-partial-foreground [&>button]:bg-partial-foreground [&>button]:hover:bg-partial-foreground/80 [&>button]:text-partial": + score > 0 && score < (maxScore ?? 0), + "bg-wrong *:text-wrong-foreground [&>button]:bg-wrong-foreground [&>button]:hover:bg-wrong-foreground/80 [&>button]:text-wrong": + score === 0, + })} + > +
+

Итого

+

+ {score <= 0 ? "Неверный ответ" : `Зачтено ${score}/${maxScore}`} +

+
+ +
+ ); +}; diff --git a/services/frontend/src/pages/Review/modules/reviews-list.tsx b/services/frontend/src/pages/Review/modules/reviews-list.tsx index 0715a77..8f50431 100644 --- a/services/frontend/src/pages/Review/modules/reviews-list.tsx +++ b/services/frontend/src/pages/Review/modules/reviews-list.tsx @@ -1,6 +1,7 @@ import { Review } from "@/shared/types/review"; import { ReviewCard } from "../components/review-card"; import { NoReviews } from "./no-reviews"; +import { ReviewDialog } from "./review-dialog"; interface ReviewsListProp { reviews: Review[]; @@ -14,7 +15,9 @@ export const ReviewsList = ({ reviews }: ReviewsListProp) => { return (
{reviews.map((review) => ( - + + + ))}
); diff --git a/services/frontend/src/shared/api/competitions.ts b/services/frontend/src/shared/api/competitions.ts index eea1533..3add5dc 100644 --- a/services/frontend/src/shared/api/competitions.ts +++ b/services/frontend/src/shared/api/competitions.ts @@ -15,6 +15,6 @@ export const getCompetition = async (id: string) => { export const startCompetition = async (competitionId: string) => { return await userFetch(`/competitions/${competitionId}/start`, { - method: 'POST' + method: "POST", }); -}; \ No newline at end of file +}; diff --git a/services/frontend/src/shared/api/review.ts b/services/frontend/src/shared/api/review.ts index 1acc8d0..eecdf23 100644 --- a/services/frontend/src/shared/api/review.ts +++ b/services/frontend/src/shared/api/review.ts @@ -1,12 +1,29 @@ import { apiFetch } from "."; -import { Review, Reviewer } from "../types/review"; +import { Review, Reviewer, ReviewEvaluation } from "../types/review"; export const getReviewer = async (token: string) => { return await apiFetch(`/review/${token}`); }; -export const getReviewerSubmissions = async (token: string) => { +export const getReviewSubmissions = async (token: string) => { return await apiFetch<{ submissions: Review[] }>( `/review/${token}/submissions`, ); }; + +export const getReviewSubmission = async (token: string, reviewId: string) => { + return await apiFetch(`/review/${token}/submissions/${reviewId}`); +}; + +export const postReviewEvaluation = async ( + token: string, + reviewId: string, + evaluation: ReviewEvaluation[], +) => { + return await apiFetch(`/review/${token}/submissions/${reviewId}/evaluate`, { + method: "POST", + body: { + evaluation, + }, + }); +}; diff --git a/services/frontend/src/shared/types/review.ts b/services/frontend/src/shared/types/review.ts index 47e0a14..c194a4e 100644 --- a/services/frontend/src/shared/types/review.ts +++ b/services/frontend/src/shared/types/review.ts @@ -9,7 +9,7 @@ export interface Review { review_status: ReviewStatus; evaluation?: ReviewEvaluation[]; criteries?: ReviewCriteria[]; - submitted_at: Date; + submitted_at: string; competition: string; competition_name: string; task: string; @@ -17,8 +17,9 @@ export interface Review { stdout?: string; result?: {}; earned_points?: number; - checked_at?: Date; + checked_at?: string; task_title: string; + description?: string; } export enum ReviewStatus { diff --git a/services/frontend/src/shared/types/user.ts b/services/frontend/src/shared/types/user.ts index 20c51e2..650c8d8 100644 --- a/services/frontend/src/shared/types/user.ts +++ b/services/frontend/src/shared/types/user.ts @@ -2,4 +2,13 @@ export interface User { id: string; email: string; username: string; + avatar?: string; + achievements?: Achievement[]; +} + +export interface Achievement { + name: string; + description: string; + received_at: Date; + icon?: string; } diff --git a/services/frontend/src/styles/globals.css b/services/frontend/src/styles/globals.css index 860667b..c03142f 100644 --- a/services/frontend/src/styles/globals.css +++ b/services/frontend/src/styles/globals.css @@ -2,8 +2,6 @@ @import "./fonts.css"; @plugin "tailwindcss-animate"; -@custom-variant dark (&:is(.dark *)); - :root { --background: oklch(0.97 0 0); --foreground: oklch(0.145 0 0); @@ -50,45 +48,26 @@ --task-text-partial: oklch(0.639 0.1595 124.48); --task-wrong: oklch(0.906 0.0484 18.08); --task-text-wrong: oklch(0.433 0.17767 29.2339); + + --correct: #d4ffe5; + --correct-foreground: #009b1c; + + --partial: #e7ffd4; + --partial-foreground: #779b00; + + --wrong: #ffd4d4; + --wrong-foreground: #9b0000; + + --checking: #ffffff; + --checking-foreground: #242424; + + --review: #ffec9f; + --review-foreground: #9b7700; } @theme inline { --font-hse-sans: "HSE Sans", system-ui, sans-serif; } -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); -} @theme inline { --color-background: var(--background); @@ -140,11 +119,26 @@ --color-task-text-partial: var(--task-text-partial); --color-task-wrong: var(--task-wrong); --color-task-text-wrong: var(--task-text-wrong); + + --color-correct: var(--correct); + --color-correct-foreground: var(--correct-foreground); + + --color-partial: var(--partial); + --color-partial-foreground: var(--partial-foreground); + + --color-wrong: var(--wrong); + --color-wrong-foreground: var(--wrong-foreground); + + --color-checking: var(--checking); + --color-checking-foreground: var(--checking-foreground); + + --color-review: var(--review); + --color-review-foreground: var(--review-foreground); } @layer base { * { - @apply border-border outline-ring/50 font-hse-sans; + @apply border-border outline-ring/50 font-hse-sans scheme-light; } body { @apply bg-background text-foreground;