diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock index 02e8791..c5f880c 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", @@ -124,6 +125,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=="], @@ -144,6 +153,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=="], @@ -156,6 +167,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=="], @@ -164,6 +177,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=="], @@ -188,8 +205,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 6a2fce5..27b9a85 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 4bd1cdd..27b0421 100644 --- a/services/frontend/src/components/layout/header.tsx +++ b/services/frontend/src/components/layout/header.tsx @@ -1,19 +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 [isProfileOpen, setIsProfileOpen] = useState(false); return (
@@ -21,90 +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={() => { - setIsProfileOpen(false); - }} - /> -
-
-
-
); }; - -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/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/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/UserProfile/index.tsx b/services/frontend/src/pages/UserProfile/index.tsx deleted file mode 100644 index ec9d1d1..0000000 --- a/services/frontend/src/pages/UserProfile/index.tsx +++ /dev/null @@ -1,398 +0,0 @@ -import React from "react"; -import { User } from "lucide-react"; -import { useUserStore } from "@/shared/stores/user"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; - -const UserProfile = () => { - const user = useUserStore((state) => state.user); - - return ( -
-
-
- {user?.avatar ? ( - {user.username} - ) : ( - - )} -
-
-

{user?.username}

-

- {user?.role || "Участник"} • На платформе с{" "} - {new Date(user?.createdAt || Date.now()).toLocaleDateString("ru-RU", { - year: "numeric", - month: "long", - })} -

-
-
- - - - - Информация - - - Статистика - - - Достижения - - - - - - - - - - - - - - - -
- ); -}; - -const UserInfo = () => { - const user = useUserStore((state) => state.user); - - return ( - - - Личная информация - - -
-
-

- Полное имя -

-

- {user?.fullName || "Не указано"} -

-
- -
-

- Email -

-

{user?.email || "Не указано"}

-
- -
-

- Учебное заведение -

-

- {user?.university || "Не указано"} -

-
- -
-

- Специализация -

-

- {user?.specialization || "Не указано"} -

-
-
- -
-

- О себе -

-

- {user?.bio || "Пользователь пока не добавил информацию о себе."} -

-
-
-
- ); -}; - -const UserStatistics = () => { - // Mock statistics data - const statistics = { - totalCompetitions: 12, - completedCompetitions: 8, - totalScore: 756, - averageScore: 94.5, - bestResult: { - competition: "Олимпиада DANO 2024", - place: 3, - score: 97, - }, - totalTasks: 86, - solvedTasks: 72, - tasksByStatus: { - correct: 58, - partial: 14, - wrong: 9, - unattempted: 5, - }, - }; - - return ( -
-
- - - - -
- -
- - - Лучший результат - - -
-

- {statistics.bestResult.competition} -

-
- Место - - {statistics.bestResult.place} - -
-
- Баллы - - {statistics.bestResult.score} - -
-
-
-
- - - - Решение задач - - -
-
- Всего задач - - {statistics.totalTasks} - -
-
- Решено задач - - {statistics.solvedTasks} - -
-
- -
-

- Статусы решений -

-
-
-
-
-
-
-
-
-
-
-
- - Верно ({statistics.tasksByStatus.correct}) - -
-
-
- - Частично ({statistics.tasksByStatus.partial}) - -
-
-
- - Неверно ({statistics.tasksByStatus.wrong}) - -
-
-
-
-
-
-
- ); -}; - - -const StatCard = ({ title, value }: { title: string; value: number | string }) => ( - - -

{title}

-

{value}

-
-
-); - -const UserAchievements = () => { - const achievements = [ - { - id: 1, - name: "Первые шаги", - description: "Участие в первом соревновании", - imageUrl: "/achievements/first-steps.png", - unlocked: true, - }, - { - id: 2, - name: "Восходящая звезда", - description: "Победа в соревновании", - imageUrl: "/achievements/rising-star.png", - unlocked: true, - }, - { - id: 3, - name: "Мастер кода", - description: "Решите 50 задач на программирование", - imageUrl: "/achievements/code-master.png", - unlocked: true, - }, - { - id: 4, - name: "Бронзовый призер", - description: "Займите 3 место в соревновании", - imageUrl: "/achievements/bronze.png", - unlocked: true, - }, - { - id: 5, - name: "Серебряный призер", - description: "Займите 2 место в соревновании", - imageUrl: "/achievements/silver.png", - unlocked: false, - }, - { - id: 6, - name: "Золотой призер", - description: "Займите 1 место в соревновании", - imageUrl: "/achievements/gold.png", - unlocked: false, - }, - { - id: 7, - name: "Марафонец", - description: "Участвуйте в 10 соревнованиях", - imageUrl: "/achievements/marathon.png", - unlocked: false, - }, - { - id: 8, - name: "Идеальное решение", - description: "Получите максимальные баллы за все задачи в соревновании", - imageUrl: "/achievements/perfect.png", - unlocked: false, - }, - ]; - - return ( -
-

- Разблокировано {achievements.filter(a => a.unlocked).length} из {achievements.length} -

- -
- {achievements.map((achievement) => ( -
-
- {achievement.imageUrl ? ( -
-
-
- ) : ( -
- - {achievement.name.substring(0, 1)} - -
- )} -
-

- {achievement.name} -

-

- {achievement.description} -

-
- ))} -
-
- ); -}; - -export default UserProfile; \ No newline at end of file diff --git a/services/frontend/src/pages/UserProfile/modules/UserAchievements/index.tsx b/services/frontend/src/pages/UserProfile/modules/UserAchievements/index.tsx deleted file mode 100644 index a713aa0..0000000 --- a/services/frontend/src/pages/UserProfile/modules/UserAchievements/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -const UserAchievements = () => { - return ( -
-

- Разблокировано {achievements.filter(a => a.unlocked).length} из {achievements.length} -

- -
- {achievements.map((achievement) => ( -
-
- {achievement.imageUrl ? ( -
-
-
- ) : ( -
- - {achievement.name.substring(0, 1)} - -
- )} -
-

- {achievement.name} -

-

- {achievement.description} -

-
- ))} -
-
- ); -}; - -export default UserAchievements \ No newline at end of file diff --git a/services/frontend/src/pages/UserProfile/modules/UserStatistics/index.tsx b/services/frontend/src/pages/UserProfile/modules/UserStatistics/index.tsx deleted file mode 100644 index e69de29..0000000 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; }