mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-22 23:17:09 +00:00
feat: authorization
This commit is contained in:
+46
-34
@@ -12,6 +12,7 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"katex": "^0.16.21",
|
||||
"lucide-react": "^0.476.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
@@ -28,9 +29,12 @@
|
||||
"tailwindcss": "^4.0.9",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
@@ -173,69 +177,69 @@
|
||||
|
||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.8", "", { "os": "android", "cpu": "arm" }, "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw=="],
|
||||
"@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.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q=="],
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.9", "", { "os": "android", "cpu": "arm64" }, "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q=="],
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw=="],
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA=="],
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q=="],
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g=="],
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.9", "", { "os": "linux", "cpu": "arm" }, "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA=="],
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.9", "", { "os": "linux", "cpu": "arm" }, "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A=="],
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q=="],
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ=="],
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.9", "", { "os": "linux", "cpu": "none" }, "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg=="],
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw=="],
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw=="],
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.9", "", { "os": "linux", "cpu": "none" }, "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA=="],
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA=="],
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.9", "", { "os": "linux", "cpu": "x64" }, "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ=="],
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.9", "", { "os": "linux", "cpu": "x64" }, "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ=="],
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w=="],
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.8", "", { "os": "win32", "cpu": "x64" }, "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g=="],
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.9", "", { "os": "win32", "cpu": "x64" }, "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw=="],
|
||||
|
||||
"@swc/core": ["@swc/core@1.11.1", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.18" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.1", "@swc/core-darwin-x64": "1.11.1", "@swc/core-linux-arm-gnueabihf": "1.11.1", "@swc/core-linux-arm64-gnu": "1.11.1", "@swc/core-linux-arm64-musl": "1.11.1", "@swc/core-linux-x64-gnu": "1.11.1", "@swc/core-linux-x64-musl": "1.11.1", "@swc/core-win32-arm64-msvc": "1.11.1", "@swc/core-win32-ia32-msvc": "1.11.1", "@swc/core-win32-x64-msvc": "1.11.1" }, "peerDependencies": { "@swc/helpers": "*" }, "optionalPeers": ["@swc/helpers"] }, "sha512-67+lBHZ1lAJQKoOhBHl9DE2iugPYAulRVArZjoF+DnIY3G9wLXCXxw5It0IaCnzvJVvUPxGmr0rHViXKBDP5Vg=="],
|
||||
"@swc/core": ["@swc/core@1.11.5", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.19" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.5", "@swc/core-darwin-x64": "1.11.5", "@swc/core-linux-arm-gnueabihf": "1.11.5", "@swc/core-linux-arm64-gnu": "1.11.5", "@swc/core-linux-arm64-musl": "1.11.5", "@swc/core-linux-x64-gnu": "1.11.5", "@swc/core-linux-x64-musl": "1.11.5", "@swc/core-win32-arm64-msvc": "1.11.5", "@swc/core-win32-ia32-msvc": "1.11.5", "@swc/core-win32-x64-msvc": "1.11.5" }, "peerDependencies": { "@swc/helpers": "*" }, "optionalPeers": ["@swc/helpers"] }, "sha512-EVY7zfpehxhTZXOfy508gb3D78ihoGGmvyiTWtlBPjgIaidP1Xw0naHMD78CWiFlZmeDjKXJufGtsEGOnZdmNA=="],
|
||||
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bJbqZ51JghEZ8WaFetofkfkS3MWsS/V3vDvY+0r+SlLeocZwf8q8/GqcafnElHcU+zLV6yTi13fJwUce6ULiUQ=="],
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GEd1hzEx0mSGkJYMFMGLnrGgjL2rOsOsuYWyjyiA3WLmhD7o+n/EWBDo6mzD/9aeF8dzSPC0TnW216gJbvrNzA=="],
|
||||
|
||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-9GGEoN0uxkLg3KocOVzMfe9c9/DxESXclsL/U2xVLa3pTFB5YnXhiCP5YBT/3Q7nSGLD+R2ALqkNlDoueUjvPw=="],
|
||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.11.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-toz04z9wAClVvQSEY3xzrgyyeWBAfMWcKG4K0ugNvO56h/wczi2ZHRlnAXZW1tghKBk3z6MXqa/srfXgNhffKw=="],
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Lt7l/l0nfSTUzsWcVY3dtOPl5RtgCJ+Ya8IG4Aa3l6c7kLc6Sx4JpylpEIY9yhGidDy/uQ8KUg5kqUPtUrXrvQ=="],
|
||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.11.5", "", { "os": "linux", "cpu": "arm" }, "sha512-5SjmKxXdwbBpsYGTpgeXOXMIjS563/ntRGn8Zc12H/c4VfPrRLGhgbJ/48z2XVFyBLcw7BCHZyFuVX1+ZI3W0Q=="],
|
||||
|
||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-oe826cfuGukctTSpDjk7RJRDEJihQMAzvO5tdWK0wcy+zvMPFyH5Fg6cW0X4ST3M7fcV91/1T/iuiiD2SVamYw=="],
|
||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.11.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-pydIlInHRzRIwB0NHblz3Dx58H/bsi0I5F2deLf9iOmwPNuOGcEEZF1Qatc7YIjP5DFbXK+Dcz+pMUZb2cc2MQ=="],
|
||||
|
||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-ABb4pnYeQp/JBJS5Qd2apTwOzpzrTebQFUiFjk0WgTKIr9T6SL3tLXMjgvbSXIath+1HnbCKFUwDXNQhgGFFTg=="],
|
||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.11.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-LhBHKjkZq5tJF1Lh0NJFpx7ROnCWLckrlIAIdSt9XfOV+zuEXJQOj+NFcM1eNk17GFfFyUMOZyGZxzYq5dveEQ=="],
|
||||
|
||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-E09TcHv40bV0mOHTKquZw0IOcQ+lzzpQjyOhCa7+GBpbS3eg5/35Gu7DfToN2bomz74LPKW/l7jZRG+ZNOYNHQ=="],
|
||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.11.5", "", { "os": "linux", "cpu": "x64" }, "sha512-dCi4xkxXlsk5sQYb3i413Cfh7+wMJeBYTvBZTD5xh+/DgRtIcIJLYJ2tNjWC4/C2i5fj+Ze9bKNSdd8weRWZ3A=="],
|
||||
|
||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cuW4r7GbvQt9uv+rGdYLHUjDvGjHmr1nYE7iFVk6r4i+byZuXBK6M7P1p+/dTzacshOc05I9n/eUV+Hfjp9a3A=="],
|
||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.11.5", "", { "os": "linux", "cpu": "x64" }, "sha512-K0AC4TreM5Oo/tXNXnE/Gf5+5y/HwUdd7xvUjOpZddcX/RlsbYOKWLgOtA3fdFIuta7XC+vrGKmIhm5l70DSVQ=="],
|
||||
|
||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-H8Q78GwaKnCL4isHx8JRTRi6vUU6iMLbpegS2jzWWC1On7EePhkLx2eR8nEsaRIQB6rc3WqdIj74OgOpNoPi7g=="],
|
||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.11.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-wzum8sYUsvPY7kgUfuqVYTgIPYmBC8KPksoNM1fz5UfhudU0ciQuYvUBD47GIGOevaoxhLkjPH4CB95vh1mJ9w=="],
|
||||
|
||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-Rx7cZ0OvqMb16fgmUSlPWQbH1+X355IDJhVQpUlpL+ezD/kkWmJix+4u2GVE/LHrfbdyZ4sjjIzSsCQxJV05Mw=="],
|
||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.11.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-lco7mw0TPRTpVPR6NwggJpjdUkAboGRkLrDHjIsUaR+Y5+0m5FMMkHOMxWXAbrBS5c4ph7QErp4Lma4r9Mn5og=="],
|
||||
|
||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6bEEC/XU1lwYzUXY7BXj3nhe7iBF9+i9dVo+hbiVxXZMrD0LUd+7urOBM3NtVnDsUaR6Ge/g7aR+OfpgYscKOg=="],
|
||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.11.5", "", { "os": "win32", "cpu": "x64" }, "sha512-E+DApLSC6JRK8VkDa4bNsBdD7Qoomx1HvKVZpOXl9v94hUZI5GMExl4vU5isvb+hPWL7rZ0NeI7ITnVLgLJRbA=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/types": ["@swc/types@0.1.18", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-NZghLaQvF3eFdj2DUjGkpwaunbZYaRcxciHINnwA4n3FrLAI8hKFOBqs2wkcOiLQfWkIdfuG6gBkNFrkPNji5g=="],
|
||||
"@swc/types": ["@swc/types@0.1.19", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.0.9", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "tailwindcss": "4.0.9" } }, "sha512-tOJvdI7XfJbARYhxX+0RArAhmuDcczTC46DGCEziqxzzbIaPnfYaIyRT31n4u8lROrsO7Q6u/K9bmQHL2uL1bQ=="],
|
||||
|
||||
@@ -275,6 +279,8 @@
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
|
||||
@@ -283,7 +289,7 @@
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="],
|
||||
"@types/node": ["@types/node@22.13.8", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.0.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g=="],
|
||||
|
||||
@@ -401,7 +407,7 @@
|
||||
|
||||
"eslint": ["eslint@9.21.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.21.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.1.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw=="],
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="],
|
||||
|
||||
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.19", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ=="],
|
||||
|
||||
@@ -511,6 +517,8 @@
|
||||
|
||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||
|
||||
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
@@ -703,7 +711,7 @@
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rollup": ["rollup@4.34.8", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.8", "@rollup/rollup-android-arm64": "4.34.8", "@rollup/rollup-darwin-arm64": "4.34.8", "@rollup/rollup-darwin-x64": "4.34.8", "@rollup/rollup-freebsd-arm64": "4.34.8", "@rollup/rollup-freebsd-x64": "4.34.8", "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", "@rollup/rollup-linux-arm-musleabihf": "4.34.8", "@rollup/rollup-linux-arm64-gnu": "4.34.8", "@rollup/rollup-linux-arm64-musl": "4.34.8", "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", "@rollup/rollup-linux-riscv64-gnu": "4.34.8", "@rollup/rollup-linux-s390x-gnu": "4.34.8", "@rollup/rollup-linux-x64-gnu": "4.34.8", "@rollup/rollup-linux-x64-musl": "4.34.8", "@rollup/rollup-win32-arm64-msvc": "4.34.8", "@rollup/rollup-win32-ia32-msvc": "4.34.8", "@rollup/rollup-win32-x64-msvc": "4.34.8", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ=="],
|
||||
"rollup": ["rollup@4.34.9", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.9", "@rollup/rollup-android-arm64": "4.34.9", "@rollup/rollup-darwin-arm64": "4.34.9", "@rollup/rollup-darwin-x64": "4.34.9", "@rollup/rollup-freebsd-arm64": "4.34.9", "@rollup/rollup-freebsd-x64": "4.34.9", "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", "@rollup/rollup-linux-arm-musleabihf": "4.34.9", "@rollup/rollup-linux-arm64-gnu": "4.34.9", "@rollup/rollup-linux-arm64-musl": "4.34.9", "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", "@rollup/rollup-linux-riscv64-gnu": "4.34.9", "@rollup/rollup-linux-s390x-gnu": "4.34.9", "@rollup/rollup-linux-x64-gnu": "4.34.9", "@rollup/rollup-linux-x64-musl": "4.34.9", "@rollup/rollup-win32-arm64-msvc": "4.34.9", "@rollup/rollup-win32-ia32-msvc": "4.34.9", "@rollup/rollup-win32-x64-msvc": "4.34.9", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
@@ -807,6 +815,10 @@
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
|
||||
|
||||
"zustand": ["zustand@5.0.3", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"katex": "^0.16.21",
|
||||
"lucide-react": "^0.476.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
@@ -33,10 +34,13 @@
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.0.9",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.2"
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.2",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { Routes, Route } from "react-router";
|
||||
import "./styles/globals.css";
|
||||
import Competitions from "./pages/Competitions";
|
||||
import CompetitionPreview from './pages/CompetitionPreview'
|
||||
import CompetitionSession from "./pages/CompetitionSession";
|
||||
|
||||
import { NavbarLayout } from "./widgets/navbar-layout";
|
||||
|
||||
import Competitions from "./pages/Competitions";
|
||||
import CompetitionPreview from "./pages/CompetitionPreview";
|
||||
import CompetitionSession from "./pages/CompetitionSession";
|
||||
import LoginPage from "./pages/Login";
|
||||
import { AuthLayout } from "./widgets/auth-layout";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<NavbarLayout />}>
|
||||
<Route path="/" element={<Competitions />} />
|
||||
<Route path="/competition/:id" element={<CompetitionPreview />} />
|
||||
</Route>
|
||||
<Route
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
|
||||
<Route element={<AuthLayout />}>
|
||||
<Route element={<NavbarLayout />}>
|
||||
<Route path="/" element={<Competitions />} />
|
||||
<Route path="/competition/:id" element={<CompetitionPreview />} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="/competition/:id/tasks/:taskId"
|
||||
element={<CompetitionSession />}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,71 +1,77 @@
|
||||
import React, { useState } from 'react';
|
||||
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
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetClose,
|
||||
} from "@/components/ui/sheet";
|
||||
import { useUserStore } from "@/shared/stores/user";
|
||||
|
||||
const Header = () => {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="bg-card sticky top-0 z-30 flex h-[72px] w-full items-center justify-center">
|
||||
<header className="bg-card sticky top-0 z-30 flex h-[72px] w-full items-center justify-center px-4 sm:px-6">
|
||||
<div className="flex w-full max-w-5xl items-center justify-between">
|
||||
<Link to="/">
|
||||
<DataRush />
|
||||
</Link>
|
||||
<div
|
||||
className="flex items-center gap-1 cursor-pointer hover:opacity-80 transition-opacity px-2 py-1 rounded-md"
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-1 rounded-md px-2 py-1 transition-opacity hover:opacity-80"
|
||||
onClick={() => setIsProfileOpen(true)}
|
||||
>
|
||||
<span className="text-lg font-semibold font-hse-sans">itqdev</span>
|
||||
<span className="font-hse-sans text-lg font-semibold">
|
||||
{user?.username}
|
||||
</span>
|
||||
<ChevronDown size={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Sheet open={isProfileOpen} onOpenChange={setIsProfileOpen}>
|
||||
<SheetContent className="w-[300px] sm:w-[350px] p-0">
|
||||
<SheetHeader className="border-b py-4 px-5">
|
||||
<SheetTitle className="font-hse-sans text-lg font-medium">Профиль</SheetTitle>
|
||||
<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="py-4 px-2">
|
||||
<ProfileOption
|
||||
icon={<User size={18} />}
|
||||
label="Ваш профиль"
|
||||
|
||||
<div className="px-2 py-4">
|
||||
<ProfileOption
|
||||
icon={<User size={18} />}
|
||||
label="Ваш профиль"
|
||||
onClick={() => {
|
||||
setIsProfileOpen(false);
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProfileOption
|
||||
icon={<Settings size={18} />}
|
||||
label="Настройки"
|
||||
|
||||
<ProfileOption
|
||||
icon={<Settings size={18} />}
|
||||
label="Настройки"
|
||||
onClick={() => {
|
||||
setIsProfileOpen(false);
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProfileOption
|
||||
icon={<BarChart2 size={18} />}
|
||||
label="Статистика"
|
||||
|
||||
<ProfileOption
|
||||
icon={<BarChart2 size={18} />}
|
||||
label="Статистика"
|
||||
onClick={() => {
|
||||
setIsProfileOpen(false);
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="border-t mt-2 pt-2">
|
||||
<ProfileOption
|
||||
icon={<LogOut size={18} />}
|
||||
label="Выйти"
|
||||
|
||||
<div className="mt-2 border-t pt-2">
|
||||
<ProfileOption
|
||||
icon={<LogOut size={18} />}
|
||||
label="Выйти"
|
||||
onClick={() => {
|
||||
setIsProfileOpen(false);
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,11 +88,16 @@ interface ProfileOptionProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ProfileOption: React.FC<ProfileOptionProps> = ({ icon, label, onClick, className }) => {
|
||||
const ProfileOption: React.FC<ProfileOptionProps> = ({
|
||||
icon,
|
||||
label,
|
||||
onClick,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<SheetClose asChild>
|
||||
<button
|
||||
className={`flex items-center gap-3 w-full px-3 py-2.5 rounded-md text-left transition-colors hover:bg-gray-100 ${className || ''}`}
|
||||
<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>
|
||||
@@ -96,4 +107,4 @@ const ProfileOption: React.FC<ProfileOptionProps> = ({ icon, label, onClick, cla
|
||||
);
|
||||
};
|
||||
|
||||
export { Header };
|
||||
export { Header };
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -20,9 +20,9 @@ const buttonVariants = cva(
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-12 px-5 py-3 has-[>svg]:px-3 text-lg font-semibold",
|
||||
default: "h-11 px-4 text-base font-semibold rounded-xl",
|
||||
lg: "h-12 px-5 py-3 has-[>svg]:px-3 text-lg font-semibold",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Loader } from "lucide-react";
|
||||
|
||||
export const Spinner = (props: React.ComponentProps<typeof Loader>) => (
|
||||
<Loader className="animate-spin" {...props} />
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Competition } from "@/shared/types";
|
||||
import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks";
|
||||
|
||||
@@ -14,7 +14,7 @@ const CompetitionPage = () => {
|
||||
);
|
||||
|
||||
const handleContinue = () => {
|
||||
if (competition?.id) {
|
||||
if (competition?.id) {
|
||||
if (mockTasks && mockTasks.length > 0) {
|
||||
const firstTaskId = mockTasks[0].id;
|
||||
navigate(`/competition/${competition.id}/tasks/${firstTaskId}`);
|
||||
@@ -43,19 +43,19 @@ const CompetitionPage = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-8">
|
||||
<div className="flex flex-col-reverse gap-8 md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-5">
|
||||
<h1 className="text-[34px] leading-11 font-semibold text-balance">
|
||||
{competition.name}
|
||||
</h1>
|
||||
<div className="text-xl leading-10 font-normal prose prose-lg max-w-none">
|
||||
<ReactMarkdown>
|
||||
{competition.description || ''}
|
||||
</ReactMarkdown>
|
||||
<div className="prose prose-lg max-w-none text-xl leading-10 font-normal">
|
||||
<ReactMarkdown>{competition.description || ""}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-96 *:w-full">
|
||||
<Button onClick={handleContinue}>Продолжить</Button>
|
||||
<div className="w-full *:w-full md:w-96">
|
||||
<Button size={"lg"} onClick={handleContinue}>
|
||||
Продолжить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,4 +63,4 @@ const CompetitionPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CompetitionPage;
|
||||
export default CompetitionPage;
|
||||
|
||||
@@ -11,13 +11,9 @@ export function CompetitionCard({
|
||||
competition,
|
||||
className,
|
||||
}: CompetitionCardProps) {
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
"aspect-square h-full max-h-80 w-auto overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
className={cn("aspect-square h-full w-auto overflow-hidden", className)}
|
||||
>
|
||||
<div className="relative h-full overflow-hidden">
|
||||
<img
|
||||
@@ -40,7 +36,9 @@ export function CompetitionCard({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold">{competition.name}</h3>
|
||||
<h3 className="line-clamp-2 text-xl font-semibold">
|
||||
{competition.name}
|
||||
</h3>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -25,7 +25,7 @@ const CompetitionsPage = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-6 sm:gap-8">
|
||||
<Section>
|
||||
<SectionHeader>
|
||||
<SectionTitle>Мои события</SectionTitle>
|
||||
@@ -50,11 +50,15 @@ const CompetitionsPage = () => {
|
||||
};
|
||||
|
||||
const Section = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="flex flex-col gap-8">{children}</div>;
|
||||
return <div className="flex flex-col gap-6 sm:gap-8">{children}</div>;
|
||||
};
|
||||
|
||||
const SectionHeader = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div className="flex h-[58px] items-center gap-2">{children}</div>;
|
||||
return (
|
||||
<div className="flex min-h-[58px] flex-col items-center justify-center gap-4 sm:flex-row sm:gap-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SectionTitle = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ interface CompetitionGridProps {
|
||||
|
||||
export function CompetitionGrid({ competitions }: CompetitionGridProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-9">
|
||||
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-3 md:gap-7 lg:gap-9">
|
||||
{competitions.map((competition) => (
|
||||
<Link key={competition.id} to={`/competition/${competition.id}`}>
|
||||
<CompetitionCard competition={competition} />
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const Input = ({ label, error, id, ...props }: InputProps) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col items-stretch gap-2">
|
||||
{label && (
|
||||
<label htmlFor={id} className="text-base font-semibold">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
id={id}
|
||||
className="bg-card h-12 rounded-xl border px-4 text-base"
|
||||
{...props}
|
||||
/>
|
||||
{error && <span className="text-red-500">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { DataRush } from "@/components/ui/icons/datarush";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { LoginTab } from "./modules/LoginTab";
|
||||
import { SignupTab } from "./modules/SignupTab";
|
||||
import React from "react";
|
||||
import { getToken } from "@/shared/token";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
const LoginPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
navigate("/");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center gap-10 px-4 py-10 sm:gap-18 sm:py-18">
|
||||
<DataRush size={52} />
|
||||
<div className="flex w-full max-w-96 flex-col items-center gap-7">
|
||||
<h1 className="text-center text-4xl font-semibold">
|
||||
Добро пожаловать!
|
||||
</h1>
|
||||
<Tabs
|
||||
defaultValue="login"
|
||||
className="flex w-full flex-col items-center gap-7"
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="login">Вход</TabsTrigger>
|
||||
<TabsTrigger value="signup">Регистрация</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="login" asChild>
|
||||
<LoginTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="signup" asChild>
|
||||
<SignupTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "../components/input";
|
||||
import { login } from "@/shared/api/auth";
|
||||
import { saveToken } from "@/shared/token";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useState } from "react";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { ApiError } from "@/shared/api";
|
||||
|
||||
export const LoginTab = () => {
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const loginAction = async (
|
||||
formData: FormData,
|
||||
e: React.FormEvent<HTMLFormElement>,
|
||||
) => {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const email = formData.get("email");
|
||||
const password = formData.get("password");
|
||||
|
||||
if (!email || !password) {
|
||||
setError("Неверное имя пользователя или пароль");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await login({
|
||||
email: email.toString(),
|
||||
password: password.toString(),
|
||||
});
|
||||
saveToken(token.token);
|
||||
navigate("/");
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError && (e.status === 400 || e.status === 401)) {
|
||||
setError("Неверное имя пользователя или пароль");
|
||||
} else {
|
||||
setError("Произошла непредвиденная ошибка");
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex w-full flex-col items-stretch gap-8"
|
||||
onSubmit={(e) => loginAction(new FormData(e.currentTarget), e)}
|
||||
>
|
||||
<div className="flex flex-col items-stretch gap-5">
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
label="Почта"
|
||||
placeholder="vdeniske@megazord.pobeda"
|
||||
/>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
label="Пароль"
|
||||
placeholder="Введите пароль"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
{error && <span className="text-red-500">{error}</span>}
|
||||
<Button type="submit">{loading ? <Spinner size={16} /> : "Войти"}</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "../components/input";
|
||||
import { z } from "zod";
|
||||
import React from "react";
|
||||
import { signup } from "@/shared/api/auth";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { saveToken } from "@/shared/token";
|
||||
import { useNavigate } from "react-router";
|
||||
import { ApiError } from "@/shared/api";
|
||||
|
||||
const signupSchema = z.object({
|
||||
email: z.string().email({ message: "Некорректная почта" }).trim(),
|
||||
username: z
|
||||
.string()
|
||||
.min(1, { message: "Имя пользователя должно быть не меньше 1 знака" })
|
||||
.max(50, { message: "Имя пользователя должно быть не больше 50 знаков" })
|
||||
.trim(),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, { message: "Пароль должен быть не меньше 8 знаков" })
|
||||
.regex(/[a-zA-Z]/, {
|
||||
message: "Пароль должен содержать хотя бы одну букву",
|
||||
})
|
||||
.regex(/[0-9]/, { message: "Пароль должен содержать хотя бы одну цифру" })
|
||||
.regex(/[^a-zA-Z0-9]/, {
|
||||
message: "Пароль должен содержать хотя бы один специальный символ",
|
||||
})
|
||||
.trim(),
|
||||
});
|
||||
|
||||
interface SignupFormErrors {
|
||||
username?: string[];
|
||||
email?: string[];
|
||||
password?: string[];
|
||||
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const SignupTab = () => {
|
||||
const navigate = useNavigate();
|
||||
const [errors, setErrors] = React.useState<SignupFormErrors | null>(null);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const signupAction = async (
|
||||
formData: FormData,
|
||||
e: React.FormEvent<HTMLFormElement>,
|
||||
) => {
|
||||
e.preventDefault();
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const validatedFields = signupSchema.safeParse({
|
||||
email: formData.get("email"),
|
||||
username: formData.get("username"),
|
||||
password: formData.get("password"),
|
||||
});
|
||||
|
||||
if (!validatedFields.success) {
|
||||
setErrors(validatedFields.error.flatten().fieldErrors);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await signup({
|
||||
...validatedFields.data,
|
||||
});
|
||||
saveToken(token.token);
|
||||
navigate("/");
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
if (e.status === 400) {
|
||||
setErrors({ message: "Неверные данные" });
|
||||
} else if (e.status === 409) {
|
||||
setErrors({
|
||||
message:
|
||||
"Пользователь с такой почтой или именем пользователя уже существует",
|
||||
});
|
||||
} else {
|
||||
setErrors({ message: "Произошла непредвиденная ошибка" });
|
||||
}
|
||||
} else {
|
||||
setErrors({ message: "Произошла непредвиденная ошибка" });
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex w-full flex-col items-stretch gap-8"
|
||||
onSubmit={(e) => signupAction(new FormData(e.currentTarget), e)}
|
||||
>
|
||||
<div className="flex flex-col items-stretch gap-5">
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
label="Почта"
|
||||
placeholder="vdeniske@megazord.pobeda"
|
||||
error={errors?.email?.at(0)}
|
||||
onChange={() => setErrors(null)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="username"
|
||||
name="username"
|
||||
label="Имя пользователя"
|
||||
placeholder="Введите имя пользователя"
|
||||
error={errors?.username?.at(0)}
|
||||
onChange={() => setErrors(null)}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
label="Пароль"
|
||||
placeholder="Введите пароль"
|
||||
type="password"
|
||||
error={errors?.password?.at(0)}
|
||||
onChange={() => setErrors(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{errors?.message && (
|
||||
<span className="text-red-500">{errors.message}</span>
|
||||
)}
|
||||
|
||||
<Button type="submit" onClick={() => setErrors(null)}>
|
||||
{loading ? <Spinner size={16} /> : "Зарегистрироваться"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { authFetch } from ".";
|
||||
|
||||
interface AuthResponse {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const signup = async (body: {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}) => {
|
||||
return await authFetch<AuthResponse>("/sign-up", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
export const login = async (body: { email: string; password: string }) => {
|
||||
return await authFetch<AuthResponse>("/sign-in", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ofetch } from "ofetch";
|
||||
import { getToken, removeToken } from "../token";
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_ENDPOINT;
|
||||
|
||||
export class ApiError extends Error {
|
||||
response: Response;
|
||||
status: number;
|
||||
|
||||
constructor(response: Response) {
|
||||
super(response.statusText);
|
||||
this.response = response;
|
||||
this.status = response.status;
|
||||
}
|
||||
}
|
||||
|
||||
export const authFetch = ofetch.create({
|
||||
baseURL: BASE_URL,
|
||||
async onResponseError({ response }) {
|
||||
throw new ApiError(response);
|
||||
},
|
||||
});
|
||||
|
||||
export const apiFetch = ofetch.create({
|
||||
baseURL: BASE_URL,
|
||||
headers: {
|
||||
Authorization: "Bearer " + getToken(),
|
||||
},
|
||||
async onResponseError({ response }) {
|
||||
if (response.status === 401) {
|
||||
removeToken();
|
||||
window.location.href = "/login";
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ApiError(response);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { apiFetch } from ".";
|
||||
import { User } from "../types/user";
|
||||
|
||||
export const getCurrentUser = async () => {
|
||||
return await apiFetch<User>("/me");
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { create } from "zustand";
|
||||
import { User } from "../types/user";
|
||||
import { getCurrentUser } from "../api/user";
|
||||
|
||||
interface UserState {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
|
||||
fetchUser: () => Promise<void>;
|
||||
}
|
||||
|
||||
const useUserStore = create<UserState>((set) => ({
|
||||
user: null,
|
||||
loading: true,
|
||||
|
||||
fetchUser: async () => {
|
||||
set({ loading: true });
|
||||
const user = await getCurrentUser();
|
||||
set({ user, loading: false });
|
||||
},
|
||||
}));
|
||||
|
||||
export { useUserStore };
|
||||
@@ -0,0 +1,17 @@
|
||||
import Cookie from "js-cookie";
|
||||
|
||||
export const getToken = () => {
|
||||
return Cookie.get("token");
|
||||
};
|
||||
|
||||
export const saveToken = (token: string) => {
|
||||
Cookie.set("token", token, {
|
||||
secure: true,
|
||||
sameSite: "Strict",
|
||||
expires: 30,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeToken = () => {
|
||||
Cookie.remove("token");
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useUserStore } from "@/shared/stores/user";
|
||||
import React from "react";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export const AuthLayout = () => {
|
||||
const fetchUser = useUserStore((state) => state.fetchUser);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function fetchData() {
|
||||
await fetchUser();
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
@@ -5,8 +5,10 @@ const NavbarLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div className="m-auto mt-6 w-full max-w-5xl">
|
||||
<Outlet />
|
||||
<div className="px-4 sm:px-6">
|
||||
<div className="m-auto mt-6 w-full max-w-5xl">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user