feat: profile

This commit is contained in:
moolcoov
2025-03-03 18:07:35 +03:00
parent c2ec50027d
commit ec5584ecf8
14 changed files with 462 additions and 541 deletions
+21
View File
@@ -6,6 +6,7 @@
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@radix-ui/react-dialog": "^1.1.6", "@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-label": "^2.1.2",
"@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2", "@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=="], "@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/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=="], "@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/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-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=="], "@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-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-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=="], "@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-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-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-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-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/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-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=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.9", "", { "os": "android", "cpu": "arm64" }, "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg=="],
+1
View File
@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@radix-ui/react-dialog": "^1.1.6", "@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-label": "^2.1.2",
"@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+3 -3
View File
@@ -10,7 +10,8 @@ import LoginPage from "./pages/Login";
import { AuthLayout } from "./widgets/auth-layout"; import { AuthLayout } from "./widgets/auth-layout";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import ReviewPage from "./pages/Review"; import ReviewPage from "./pages/Review";
import UserProfile from "./pages/UserProfile"; import UserProfile from "./pages/Profile";
import ProfilePage from "./pages/Profile";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -24,14 +25,13 @@ const App = () => {
<Route element={<NavbarLayout />}> <Route element={<NavbarLayout />}>
<Route path="/" element={<Competitions />} /> <Route path="/" element={<Competitions />} />
<Route path="/competition/:id" element={<Competition />} /> <Route path="/competition/:id" element={<Competition />} />
<Route path="/profile" element={<ProfilePage />} />
</Route> </Route>
<Route <Route
path="/competition/:id/tasks/:taskId" path="/competition/:id/tasks/:taskId"
element={<CompetitionSession />} element={<CompetitionSession />}
/> />
<Route path="/profile" element={<UserProfile />} />
</Route> </Route>
<Route path="/review/:token" element={<ReviewPage />} /> <Route path="/review/:token" element={<ReviewPage />} />
@@ -1,19 +1,19 @@
import React, { useState } from "react";
import { DataRush } from "@/components/ui/icons/datarush"; import { DataRush } from "@/components/ui/icons/datarush";
import { ChevronDown, User, Settings, BarChart2, LogOut } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { Link } from "react-router"; import { Link, useNavigate } from "react-router";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetClose,
} from "@/components/ui/sheet";
import { useUserStore } from "@/shared/stores/user"; 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 user = useUserStore((state) => state.user);
const [isProfileOpen, setIsProfileOpen] = useState(false);
return ( return (
<header className="bg-card sticky top-0 z-30 flex h-[72px] w-full items-center justify-center px-4 sm:px-6"> <header className="bg-card sticky top-0 z-30 flex h-[72px] w-full items-center justify-center px-4 sm:px-6">
@@ -21,90 +21,33 @@ const Header = () => {
<Link to="/"> <Link to="/">
<DataRush /> <DataRush />
</Link> </Link>
<div <DropdownMenu>
className="flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 transition-opacity hover:opacity-80" <DropdownMenuTrigger asChild>
onClick={() => setIsProfileOpen(true)} <button className="flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 text-left transition-opacity hover:opacity-80">
> <span className="font-hse-sans text-lg font-semibold">
<span className="font-hse-sans text-lg font-semibold"> {user?.username}
{user?.username} </span>
</span> <ChevronDown size={20} />
<ChevronDown size={20} /> </button>
</div> </DropdownMenuTrigger>
<DropdownMenuContent>
<Link to="/profile">
<DropdownMenuItem>Аккаунт</DropdownMenuItem>
</Link>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onClick={() => {
removeToken();
navigate("/login");
}}
>
Выйти
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
<Sheet open={isProfileOpen} onOpenChange={setIsProfileOpen}>
<SheetContent className="w-[300px] p-0 sm:w-[350px]">
<SheetHeader className="border-b px-5 py-4">
<SheetTitle className="font-hse-sans text-lg font-medium">
Профиль
</SheetTitle>
</SheetHeader>
<div className="px-2 py-4">
<ProfileOption
icon={<User size={18} />}
label="Ваш профиль"
onClick={() => {
setIsProfileOpen(false);
}}
/>
<ProfileOption
icon={<Settings size={18} />}
label="Настройки"
onClick={() => {
setIsProfileOpen(false);
}}
/>
<ProfileOption
icon={<BarChart2 size={18} />}
label="Статистика"
onClick={() => {
setIsProfileOpen(false);
}}
/>
<div className="mt-2 border-t pt-2">
<ProfileOption
icon={<LogOut size={18} />}
label="Выйти"
onClick={() => {
setIsProfileOpen(false);
}}
/>
</div>
</div>
</SheetContent>
</Sheet>
</header> </header>
); );
}; };
interface ProfileOptionProps {
icon: React.ReactNode;
label: string;
onClick: () => void;
className?: string;
}
const ProfileOption: React.FC<ProfileOptionProps> = ({
icon,
label,
onClick,
className,
}) => {
return (
<SheetClose asChild>
<button
className={`flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition-colors hover:bg-gray-100 ${className || ""}`}
onClick={onClick}
>
<span className="text-gray-600">{icon}</span>
<span className="font-hse-sans">{label}</span>
</button>
</SheetClose>
);
};
export { Header };
@@ -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<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive-foreground data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/40 data-[variant=destructive]:focus:text-destructive-foreground data-[variant=destructive]:*:[svg]:!text-destructive-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className,
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className,
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};
@@ -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 <Loading />;
}
if (!user) {
navigate("/");
return;
}
return (
<div className="flex flex-col items-stretch gap-14">
<div className="flex">
<UserInfo user={user} />
<UserAchievements achievements={user.achievements} />
</div>
<UserStats />
</div>
);
};
export default ProfilePage;
@@ -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 (
<section className="flex flex-1 flex-col gap-5">
<h2 className="text-3xl font-semibold">Достижения</h2>
{achievements && (
<div className="grid grid-cols-2 gap-6">
{achievements.map((a) => (
<AchievementDialog key={a.name} achievement={a}>
<AchievementCard achievement={a} />
</AchievementDialog>
))}
</div>
)}
</section>
);
};
const AchievementCard = ({ achievement }: { achievement: Achievement }) => {
return (
<div className="flex cursor-pointer items-center gap-4 text-left">
<div className="aspect-square h-auto w-full max-w-[90px] flex-1">
<img src={achievement.icon} alt={achievement.name} />
</div>
<div className="flex flex-1 flex-col gap-1.5">
<h3 className="text-lg font-semibold">{achievement.name}</h3>
<p className="text-muted-foreground text-sm">
{dayjs(achievement.received_at).format("D MMM YYYY")}
</p>
</div>
</div>
);
};
const AchievementDialog = ({
achievement,
children,
}: {
achievement: Achievement;
children: React.ReactNode;
}) => {
return (
<Dialog>
<DialogTrigger>{children}</DialogTrigger>
<DialogContent>
<div className="flex flex-col items-center gap-4">
<div className="aspect-square h-auto w-full max-w-[140px] flex-1">
<img src={achievement.icon} alt={achievement.name} />
</div>
<div className="flex flex-col items-center gap-1.5 text-center">
<h1 className="text-3xl font-semibold">{achievement.name}</h1>
<p className="text-muted-foreground">
Получено {dayjs(achievement.received_at).format("DD MMMM YYYY")}
</p>
</div>
<p className="text-center text-lg">{achievement.description}</p>
</div>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,21 @@
import { User } from "@/shared/types/user";
export const UserInfo = ({ user }: { user: User }) => {
return (
<section className="flex max-w-[420px] flex-1 flex-col gap-6">
{user.avatar && (
<div className="aspect-square h-auto w-full max-w-[300px] overflow-hidden rounded-full border">
<img
src={user.avatar}
alt={user.username}
className="h-full w-full object-cover object-center"
/>
</div>
)}
<div className="flex flex-col gap-3">
<h1 className="text-4xl font-semibold">{user.username}</h1>
<p className="text-muted-foreground">{user.email}</p>
</div>
</section>
);
};
@@ -0,0 +1,7 @@
export const UserStats = () => {
return (
<div>
<h2 className="text-3xl font-semibold">Аналитика</h2>
</div>
);
};
@@ -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 (
<div className="container mx-auto max-w-5xl px-4 py-8">
<div className="mb-8 flex items-center gap-6">
<div className="flex h-24 w-24 items-center justify-center rounded-full bg-blue-100">
{user?.avatar ? (
<img
src={user.avatar}
alt={user.username}
className="h-24 w-24 rounded-full object-cover"
/>
) : (
<User size={40} className="text-blue-500" />
)}
</div>
<div>
<h1 className="font-hse-sans text-3xl font-bold">{user?.username}</h1>
<p className="font-hse-sans text-gray-500">
{user?.role || "Участник"} На платформе с{" "}
{new Date(user?.createdAt || Date.now()).toLocaleDateString("ru-RU", {
year: "numeric",
month: "long",
})}
</p>
</div>
</div>
<Tabs defaultValue="info" className="w-full">
<TabsList className="mb-6 w-full justify-start">
<TabsTrigger value="info" className="font-hse-sans">
Информация
</TabsTrigger>
<TabsTrigger value="statistics" className="font-hse-sans">
Статистика
</TabsTrigger>
<TabsTrigger value="achievements" className="font-hse-sans">
Достижения
</TabsTrigger>
</TabsList>
<TabsContent value="info">
<UserInfo />
</TabsContent>
<TabsContent value="statistics">
<UserStatistics />
</TabsContent>
<TabsContent value="achievements">
<UserAchievements />
</TabsContent>
</Tabs>
</div>
);
};
const UserInfo = () => {
const user = useUserStore((state) => state.user);
return (
<Card>
<CardHeader>
<CardTitle className="font-hse-sans">Личная информация</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<h3 className="font-hse-sans text-sm font-medium text-gray-500">
Полное имя
</h3>
<p className="font-hse-sans mt-1">
{user?.fullName || "Не указано"}
</p>
</div>
<div>
<h3 className="font-hse-sans text-sm font-medium text-gray-500">
Email
</h3>
<p className="font-hse-sans mt-1">{user?.email || "Не указано"}</p>
</div>
<div>
<h3 className="font-hse-sans text-sm font-medium text-gray-500">
Учебное заведение
</h3>
<p className="font-hse-sans mt-1">
{user?.university || "Не указано"}
</p>
</div>
<div>
<h3 className="font-hse-sans text-sm font-medium text-gray-500">
Специализация
</h3>
<p className="font-hse-sans mt-1">
{user?.specialization || "Не указано"}
</p>
</div>
</div>
<div>
<h3 className="font-hse-sans text-sm font-medium text-gray-500">
О себе
</h3>
<p className="font-hse-sans mt-1">
{user?.bio || "Пользователь пока не добавил информацию о себе."}
</p>
</div>
</CardContent>
</Card>
);
};
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 (
<div className="space-y-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard
title="Всего соревнований"
value={statistics.totalCompetitions}
/>
<StatCard
title="Завершено соревнований"
value={statistics.completedCompetitions}
/>
<StatCard title="Всего баллов" value={statistics.totalScore} />
<StatCard
title="Средний балл"
value={statistics.averageScore.toFixed(1)}
/>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="font-hse-sans">Лучший результат</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<p className="font-hse-sans text-lg font-medium">
{statistics.bestResult.competition}
</p>
<div className="flex items-center justify-between">
<span className="font-hse-sans text-gray-500">Место</span>
<span className="font-hse-sans font-medium">
{statistics.bestResult.place}
</span>
</div>
<div className="flex items-center justify-between">
<span className="font-hse-sans text-gray-500">Баллы</span>
<span className="font-hse-sans font-medium">
{statistics.bestResult.score}
</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="font-hse-sans">Решение задач</CardTitle>
</CardHeader>
<CardContent>
<div className="mb-4 space-y-2">
<div className="flex items-center justify-between">
<span className="font-hse-sans">Всего задач</span>
<span className="font-hse-sans font-medium">
{statistics.totalTasks}
</span>
</div>
<div className="flex items-center justify-between">
<span className="font-hse-sans">Решено задач</span>
<span className="font-hse-sans font-medium">
{statistics.solvedTasks}
</span>
</div>
</div>
<div className="space-y-2">
<h4 className="font-hse-sans text-sm font-medium">
Статусы решений
</h4>
<div className="h-6 w-full overflow-hidden rounded-full bg-gray-200">
<div className="flex h-full">
<div
className="bg-green-500"
style={{
width: `${
(statistics.tasksByStatus.correct /
statistics.totalTasks) *
100
}%`,
}}
></div>
<div
className="bg-yellow-500"
style={{
width: `${
(statistics.tasksByStatus.partial /
statistics.totalTasks) *
100
}%`,
}}
></div>
<div
className="bg-red-500"
style={{
width: `${
(statistics.tasksByStatus.wrong /
statistics.totalTasks) *
100
}%`,
}}
></div>
<div
className="bg-gray-300"
style={{
width: `${
(statistics.tasksByStatus.unattempted /
statistics.totalTasks) *
100
}%`,
}}
></div>
</div>
</div>
<div className="flex justify-between text-xs">
<div className="flex items-center">
<div className="mr-1 h-3 w-3 rounded-full bg-green-500"></div>
<span className="font-hse-sans">
Верно ({statistics.tasksByStatus.correct})
</span>
</div>
<div className="flex items-center">
<div className="mr-1 h-3 w-3 rounded-full bg-yellow-500"></div>
<span className="font-hse-sans">
Частично ({statistics.tasksByStatus.partial})
</span>
</div>
<div className="flex items-center">
<div className="mr-1 h-3 w-3 rounded-full bg-red-500"></div>
<span className="font-hse-sans">
Неверно ({statistics.tasksByStatus.wrong})
</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
};
const StatCard = ({ title, value }: { title: string; value: number | string }) => (
<Card>
<CardContent className="pt-6">
<p className="font-hse-sans text-sm text-gray-500">{title}</p>
<p className="font-hse-sans mt-2 text-3xl font-bold">{value}</p>
</CardContent>
</Card>
);
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 (
<div className="space-y-6">
<h2 className="font-hse-sans text-xl font-semibold">
Разблокировано {achievements.filter(a => a.unlocked).length} из {achievements.length}
</h2>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
{achievements.map((achievement) => (
<div
key={achievement.id}
className={`flex flex-col items-center justify-center rounded-lg p-4 text-center ${
achievement.unlocked ? "" : "opacity-40"
}`}
>
<div className="mb-3 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
{achievement.imageUrl ? (
<div className="relative h-16 w-16 overflow-hidden rounded-full">
<div
className="h-full w-full bg-cover bg-center"
style={{ backgroundImage: `url(${achievement.imageUrl})` }}
></div>
</div>
) : (
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-blue-200 text-blue-700">
<span className="font-hse-sans text-xl font-bold">
{achievement.name.substring(0, 1)}
</span>
</div>
)}
</div>
<h3 className="font-hse-sans text-sm font-medium">
{achievement.name}
</h3>
<p className="font-hse-sans mt-1 text-xs text-gray-500">
{achievement.description}
</p>
</div>
))}
</div>
</div>
);
};
export default UserProfile;
@@ -1,45 +0,0 @@
const UserAchievements = () => {
return (
<div className="space-y-6">
<h2 className="font-hse-sans text-xl font-semibold">
Разблокировано {achievements.filter(a => a.unlocked).length} из {achievements.length}
</h2>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
{achievements.map((achievement) => (
<div
key={achievement.id}
className={`flex flex-col items-center justify-center rounded-lg p-4 text-center ${
achievement.unlocked ? "" : "opacity-40"
}`}
>
<div className="mb-3 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
{achievement.imageUrl ? (
<div className="relative h-16 w-16 overflow-hidden rounded-full">
<div
className="h-full w-full bg-cover bg-center"
style={{ backgroundImage: `url(${achievement.imageUrl})` }}
></div>
</div>
) : (
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-blue-200 text-blue-700">
<span className="font-hse-sans text-xl font-bold">
{achievement.name.substring(0, 1)}
</span>
</div>
)}
</div>
<h3 className="font-hse-sans text-sm font-medium">
{achievement.name}
</h3>
<p className="font-hse-sans mt-1 text-xs text-gray-500">
{achievement.description}
</p>
</div>
))}
</div>
</div>
);
};
export default UserAchievements
@@ -2,4 +2,13 @@ export interface User {
id: string; id: string;
email: string; email: string;
username: string; username: string;
avatar?: string;
achievements?: Achievement[];
}
export interface Achievement {
name: string;
description: string;
received_at: Date;
icon?: string;
} }