больше правки по состоянию компонентов
98
front/package-lock.json
generated
@@ -39,6 +39,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "7.52.0",
|
"react-hook-form": "7.52.0",
|
||||||
"react-jwt": "^1.2.2",
|
"react-jwt": "^1.2.2",
|
||||||
|
"react-material-ui-carousel": "^3.4.2",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router": "^7.1.1",
|
"react-router": "^7.1.1",
|
||||||
"react-router-dom": "^6.25.1",
|
"react-router-dom": "^6.25.1",
|
||||||
@@ -9501,6 +9502,52 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "4.1.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-4.1.17.tgz",
|
||||||
|
"integrity": "sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"framesync": "5.3.0",
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"popmotion": "9.3.6",
|
||||||
|
"style-value-types": "4.1.4",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "^0.8.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8 || ^17.0.0",
|
||||||
|
"react-dom": ">=16.8 || ^17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/framer-motion/node_modules/@emotion/is-prop-valid": {
|
||||||
|
"version": "0.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
|
||||||
|
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/memoize": "0.7.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/framer-motion/node_modules/@emotion/memoize": {
|
||||||
|
"version": "0.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
|
||||||
|
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/framesync": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
@@ -9912,6 +9959,12 @@
|
|||||||
"he": "bin/he"
|
"he": "bin/he"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hey-listen": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/hoist-non-react-statics": {
|
"node_modules/hoist-non-react-statics": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||||
@@ -14158,6 +14211,18 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/popmotion": {
|
||||||
|
"version": "9.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-9.3.6.tgz",
|
||||||
|
"integrity": "sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"framesync": "5.3.0",
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"style-value-types": "4.1.4",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
|
||||||
@@ -15900,6 +15965,29 @@
|
|||||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-material-ui-carousel": {
|
||||||
|
"version": "3.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-material-ui-carousel/-/react-material-ui-carousel-3.4.2.tgz",
|
||||||
|
"integrity": "sha512-jUbC5aBWqbbbUOOdUe3zTVf4kMiZFwKJqwhxzHgBfklaXQbSopis4iWAHvEOLcZtSIJk4JAGxKE0CmxDoxvUuw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.7.1",
|
||||||
|
"@emotion/styled": "^11.6.0",
|
||||||
|
"@mui/icons-material": "^5.4.1",
|
||||||
|
"@mui/material": "^5.4.1",
|
||||||
|
"@mui/system": "^5.4.1",
|
||||||
|
"framer-motion": "^4.1.17"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/react": "^11.4.1",
|
||||||
|
"@emotion/styled": "^11.3.0",
|
||||||
|
"@mui/icons-material": "^5.0.0",
|
||||||
|
"@mui/material": "^5.0.0",
|
||||||
|
"@mui/system": "^5.0.0",
|
||||||
|
"react": "^17.0.1 || ^18.0.0",
|
||||||
|
"react-dom": "^17.0.2 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-redux": {
|
"node_modules/react-redux": {
|
||||||
"version": "9.2.0",
|
"version": "9.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
@@ -17465,6 +17553,16 @@
|
|||||||
"webpack": "^5.0.0"
|
"webpack": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-value-types": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hey-listen": "^1.0.8",
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stylehacks": {
|
"node_modules/stylehacks": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "7.52.0",
|
"react-hook-form": "7.52.0",
|
||||||
"react-jwt": "^1.2.2",
|
"react-jwt": "^1.2.2",
|
||||||
|
"react-material-ui-carousel": "^3.4.2",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router": "^7.1.1",
|
"react-router": "^7.1.1",
|
||||||
"react-router-dom": "^6.25.1",
|
"react-router-dom": "^6.25.1",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 211 KiB |
5
front/public/assets/helmet_icon_153945.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<svg data-name="Layer 1" id="Layer_1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title/>
|
||||||
|
<path d="M129.17,418.45a8,8,0,0,1-5.26,10,7.86,7.86,0,0,1-2.38.36,8,8,0,0,1-7.63-5.62l-4.53-14.54a8,8,0,1,1,15.28-4.76Zm67.39-20.83a8,8,0,1,0-15.27,4.75l4.52,14.54a8,8,0,0,0,7.64,5.63,7.87,7.87,0,0,0,2.38-.37,8,8,0,0,0,5.26-10Zm-35.95,3.14a8,8,0,0,0-15.28,4.75l4.52,14.54a8,8,0,0,0,7.64,5.63,8.18,8.18,0,0,0,2.38-.36,8,8,0,0,0,5.26-10ZM433.33,298c-25.41,44.26-54.84,78.43-87.46,101.56-16.81,11.92-59.44,24.56-84.91,32.12-5.32,1.57-9.92,2.94-13.11,4a370.71,370.71,0,0,1-112,17.74c-33.48,0-54.39-5-56-5.36a8,8,0,0,1-5.73-5.39L57.56,389.21v0a8.33,8.33,0,0,1-.22-.86s0-.08,0-.12c0-.24-.08-.49-.1-.73a2.62,2.62,0,0,0,0-.28c0-.19,0-.39,0-.58s0-.2,0-.3a5.42,5.42,0,0,1,.06-.58,2.51,2.51,0,0,1,0-.27c0-.24.09-.47.15-.7a.65.65,0,0,1,0-.13,7.67,7.67,0,0,1,.67-1.63l.1-.18a6.65,6.65,0,0,1,.36-.56l.06-.1.11-.13.36-.45.23-.26.14-.14,45-45A266.27,266.27,0,0,1,103,307.46c0-36.46,7-69.85,19.13-93-32.1,1.69-56,3.35-56.34,3.38a8,8,0,0,1-8.48-9.06c7-51.51,61.36-89.1,93.12-106.92,47.46-26.62,103.43-43.16,146.08-43.16,1.29,0,2.59,0,3.87,0C356.3,60,406.22,90,433.87,139,462,189,461.83,248.39,433.33,298ZM119,307.46a251.73,251.73,0,0,0,1.55,28,8,8,0,0,1,.59,4.8,196,196,0,0,0,7.16,33c24.81-3.44,53.26-9,78.26-17.87a183.14,183.14,0,0,0,28.72-12.84,56.36,56.36,0,0,0,25.1-29.89C283,250,289,222.42,289.07,214.33c-5.77-1.78-28.59-5.45-116.87-2.17-10.5.39-20.92.84-30.91,1.32C127.46,233.5,119,269,119,307.46ZM107.44,355.9,85.59,377.75c7.42-.53,16.47-1.31,26.56-2.44C110.32,369.16,108.74,362.66,107.44,355.9Zm312.5-209c-24.86-44.06-69.7-71-120-72.18q-1.74,0-3.51,0c-39.46,0-93.73,16.14-138.25,41.11-44.7,25.09-74.3,55.71-83,85.36,12.65-.81,35.41-2.2,61.57-3.47h.1c13.26-.64,27.39-1.25,41.53-1.74,33.33-1.15,60.17-1.4,79.77-.74,35.5,1.19,43.28,4.95,45.8,11.92,1.58,4.37,5.78,15.95-28.6,111a72.37,72.37,0,0,1-32.25,38.36,200,200,0,0,1-31.25,14c-50.19,17.85-111.61,22.66-136,23.91l12.12,39c16.47,3,78.12,11.44,155-13,3.34-1.06,8-2.44,13.4-4,22.91-6.8,65.56-19.44,80.2-29.83,30.73-21.78,58.6-54.24,82.85-96.48A143.53,143.53,0,0,0,419.94,146.89Zm-66.66,92.18a15.91,15.91,0,0,1,1.8,12.09L338.7,316.71a16,16,0,0,1-7.66,10l-16.79,9.47a16,16,0,0,1-23.34-17.79L310.5,240a16,16,0,0,1,19.37-11.62l13.58,3.39A15.91,15.91,0,0,1,353.28,239.07Zm-13.72,8.2L326,243.92l-19.54,78.32,16.74-9.43,16.37-65.53h0Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 166 KiB |
BIN
front/public/assets/sponsors/kanistra.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
25
front/public/assets/sponsors/newDiffer.svg
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<svg width="200" height="100" viewBox="0 0 220 110" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="220" height="110" fill="white"/>
|
||||||
|
<g transform="translate(7,28)">
|
||||||
|
<path d="M28.7238 27.0847V16.4433H32.2834V35.6217L20.213 24.7817V35.4231H16.6535V16.2447L28.7238 27.0847Z"
|
||||||
|
fill="#3C1642"/>
|
||||||
|
<path d="M36.3247 35.4231V16.4433H49.1845V19.2624H39.8842V24.7817H48.3549V27.6009H39.8842V32.604H49.5994V35.4231H36.3247Z"
|
||||||
|
fill="#3C1642"/>
|
||||||
|
<path d="M82.5168 35.3932L89.1492 0.0340271H95.7815L86.431 44.4732H79.1462L72.3182 12.151L65.4684 44.4732H58.2054L48.8549 0.0340271H55.4655L62.1196 35.3622L69.1868 0.0340271H75.4278L82.5168 35.3932Z"
|
||||||
|
fill="#1DD3B0"/>
|
||||||
|
<path d="M95.8258 16.4432H103.453C105.3 16.4432 106.968 16.8491 108.458 17.6609C109.93 18.4727 111.09 19.6021 111.937 21.0492C112.767 22.4963 113.182 24.1243 113.182 25.9332C113.182 27.742 112.767 29.37 111.937 30.8171C111.081 32.2818 109.921 33.4113 108.458 34.2054C106.968 35.0172 105.3 35.4231 103.453 35.4231H95.8258V16.4432ZM99.3853 32.6039H103.025C104.863 32.6039 106.424 31.9686 107.709 30.698C108.985 29.4185 109.622 27.8303 109.622 25.9332C109.622 24.0007 108.985 22.4037 107.709 21.1419C106.442 19.8889 104.881 19.2624 103.025 19.2624H99.3853V32.6039Z"
|
||||||
|
fill="#3C1642"/>
|
||||||
|
<path d="M116.407 35.4231V16.4432H119.966V35.4231H116.407Z" fill="#3C1642"/>
|
||||||
|
<path d="M124.008 35.4231V16.4432H137.068V19.2624H127.554V24.7817H136.239V27.6008H127.567V35.4231H124.008Z"
|
||||||
|
fill="#3C1642"/>
|
||||||
|
<path d="M141.11 35.4231V16.4432H154.17V19.2624H144.656V24.7817H153.341V27.6008H144.669V35.4231H141.11Z"
|
||||||
|
fill="#3C1642"/>
|
||||||
|
<path d="M158.211 35.4231V16.4432H171.071V19.2624H161.771V24.7817H170.242V27.6008H161.771V32.6039H171.486V35.4231H158.211Z"
|
||||||
|
fill="#3C1642"/>
|
||||||
|
<path d="M190.328 23.0875C190.328 24.5346 189.957 25.8052 189.217 26.8994C188.468 27.9935 187.41 28.7744 186.046 29.2421L189.966 35.4231H186.367L182.794 29.7053H179.087V35.4231H175.527V16.4432H183.075C185.314 16.4432 187.085 17.0609 188.387 18.2962C189.681 19.5403 190.328 21.1374 190.328 23.0875ZM179.074 19.2624V26.8861H182.713C183.864 26.8861 184.832 26.5199 185.617 25.7876C186.385 25.0552 186.768 24.1552 186.768 23.0875C186.777 22.0286 186.398 21.1242 185.631 20.3742C184.855 19.633 183.882 19.2624 182.713 19.2624H179.074Z"
|
||||||
|
fill="#3C1642"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M28.0759 2.30326C14.3594 2.30326 3.24006 13.2249 3.24006 26.6975C3.24006 40.1701 14.3594 51.0918 28.0759 51.0918H177.091C190.807 51.0918 201.927 40.1701 201.927 26.6975C201.927 13.2249 190.807 2.30326 177.091 2.30326H90.2617V0.0340289H177.091C192.083 0.0340289 204.237 11.9717 204.237 26.6975C204.237 41.4234 192.083 53.361 177.091 53.361H28.0759C13.0835 53.361 0.929749 41.4234 0.929749 26.6975C0.929749 11.9717 13.0835 0.0340271 28.0759 0.0340271H55.6071V2.30326H28.0759Z"
|
||||||
|
fill="#1DD3B0"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
95
front/public/assets/sponsors/riostroy.svg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
front/public/assets/sponsors/sgaz.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
front/public/assets/sponsors/sslm.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
@@ -1,7 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0px"
|
||||||
viewBox="0 0 150.2 54.4" style="enable-background:new 0 0 150.2 54.4;" xml:space="preserve">
|
y="0px"
|
||||||
|
width="150" height="75"
|
||||||
|
viewBox="0 0 160 80" style="enable-background:new 0 0 150 75;" xml:space="preserve">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#989695;}
|
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#989695;}
|
||||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
|
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
|
||||||
@@ -10,7 +12,8 @@
|
|||||||
.st4{fill:#989695;}
|
.st4{fill:#989695;}
|
||||||
.st5{fill:#989695;stroke:#989695;stroke-width:0.1417;stroke-miterlimit:10;}
|
.st5{fill:#989695;stroke:#989695;stroke-width:0.1417;stroke-miterlimit:10;}
|
||||||
</style>
|
</style>
|
||||||
<g>
|
<rect width="160" height="80" fill="white"/>
|
||||||
|
<g transform="translate(5,15)">
|
||||||
<g>
|
<g>
|
||||||
<g>
|
<g>
|
||||||
<path class="st0" d="M25.1,17.4c-0.1-1.5-0.5-2.8-1.2-3.8c-0.3-0.4-0.7-0.8-1.1-1.1c-0.4-0.3-0.9-0.5-1.3-0.5
|
<path class="st0" d="M25.1,17.4c-0.1-1.5-0.5-2.8-1.2-3.8c-0.3-0.4-0.7-0.8-1.1-1.1c-0.4-0.3-0.9-0.5-1.3-0.5
|
||||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
19
front/public/assets/sponsors/woodGrand.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 200" width="400" height="200" fill="#FFFFFF">
|
||||||
|
<rect width="400" height="200" fill="white"/>
|
||||||
|
<g transform="translate(0,72)">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.7399 0L67.8599 39.93L54.2799 59.93L27.1599 20L40.7399 0Z"
|
||||||
|
fill="#C35F1F"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.58 0L40.74 39.99L27.16 59.99L0 20L13.58 0Z"
|
||||||
|
fill="#EC7316"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M67.9001 0L81.4001 20L67.8601 39.93L54.3201 20L67.9001 0Z"
|
||||||
|
fill="#A04F23"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M54.3201 19.9995L56.9101 16.1895L67.8601 39.9295L54.3201 19.9995Z" fill="#85411E"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M27.1599 19.9995L29.7499 16.1895L40.7199 39.9595L27.1599 19.9995Z" fill="#85411E"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M130.49 11.1504H129.4L126.07 34.3404L120.95 11.2004H114.95H114.79H114.04L108.59 35.8404L105.02 11.1604H96.3401L102.34 52.5104H112.6L117.52 31.0404L122.43 52.5104H132.14L138.14 11.1604H130.49V11.1504ZM159.67 11.1504H151.89C149.409 11.1557 147.032 12.1435 145.277 13.8977C143.523 15.6519 142.535 18.0296 142.53 20.5104V43.1504C142.533 45.632 143.52 48.0112 145.274 49.766C147.029 51.5208 149.408 52.5077 151.89 52.5104H159.7C162.182 52.5077 164.561 51.5208 166.316 49.766C168.07 48.0112 169.057 45.632 169.06 43.1504V20.5104C169.055 18.0244 168.063 15.6422 166.302 13.8871C164.541 12.1321 162.156 11.1477 159.67 11.1504ZM160.44 41.8104C160.437 42.3692 160.214 42.9044 159.819 43.2995C159.424 43.6946 158.889 43.9178 158.33 43.9204H153.2C152.641 43.9178 152.106 43.6946 151.711 43.2995C151.316 42.9044 151.093 42.3692 151.09 41.8104V21.8104C151.093 21.2516 151.316 20.7164 151.711 20.3213C152.106 19.9262 152.641 19.703 153.2 19.7004H158.33C158.889 19.703 159.424 19.9262 159.819 20.3213C160.214 20.7164 160.437 21.2516 160.44 21.8104V41.8104ZM384.42 11.1504H367.34V52.5104H384.42C386.901 52.5051 389.279 51.5173 391.033 49.7631C392.787 48.0089 393.775 45.6312 393.78 43.1504V20.5104C393.775 18.0296 392.787 15.6519 391.033 13.8977C389.279 12.1435 386.901 11.1557 384.42 11.1504ZM385.19 41.8104C385.187 42.3709 384.963 42.9076 384.566 43.303C384.168 43.6984 383.631 43.9204 383.07 43.9204H375.89V19.7404H383.03C383.591 19.7404 384.128 19.9624 384.526 20.3578C384.923 20.7532 385.147 21.2899 385.15 21.8504L385.19 41.8104ZM184.09 11.1504H191.89C194.371 11.1557 196.748 12.1435 198.503 13.8977C200.257 15.6519 201.245 18.0296 201.25 20.5104V43.1504C201.247 45.632 200.26 48.0112 198.506 49.766C196.751 51.5208 194.372 52.5077 191.89 52.5104H184.09C181.609 52.5051 179.231 51.5173 177.477 49.7631C175.723 48.0089 174.735 45.6312 174.73 43.1504V20.5104C174.735 18.0296 175.723 15.6519 177.477 13.8977C179.231 12.1435 181.609 11.1557 184.09 11.1504ZM192.039 43.2995C192.434 42.9043 192.657 42.3692 192.66 41.8104V21.8104C192.657 21.2516 192.434 20.7164 192.039 20.3213C191.644 19.9262 191.109 19.703 190.55 19.7004H185.43C184.869 19.7004 184.332 19.9224 183.934 20.3178C183.537 20.7132 183.313 21.2499 183.31 21.8104V41.8104C183.313 42.3709 183.537 42.9076 183.934 43.303C184.332 43.6984 184.869 43.9204 185.43 43.9204H190.55C191.109 43.9178 191.644 43.6946 192.039 43.2995ZM297.78 20.5104V29.3404C297.771 31.1409 297.242 32.9005 296.257 34.4081C295.273 35.9158 293.875 37.1075 292.23 37.8404L298.53 52.5104H290.28H289.12L283.19 38.6604H279.89V52.5104H271.34V11.1504H288.41C290.892 11.1557 293.271 12.1432 295.026 13.8971C296.782 15.651 297.772 18.0287 297.78 20.5104ZM288.563 29.4901C288.959 29.0953 289.185 28.5601 289.19 28.0004L289.15 21.8504C289.147 21.2899 288.923 20.7532 288.526 20.3578C288.128 19.9624 287.591 19.7404 287.03 19.7404H279.89V30.1104H287.07C287.63 30.1078 288.166 29.885 288.563 29.4901ZM360.49 11.1504H353.05V31.2704L343.69 11.1604H335.47V52.5104H344.06V32.3404L353.45 52.5204H361.63V11.1604H360.49V11.1504ZM310.07 11.1504H322.07L323.14 11.1604L331.69 52.5104H322.89L321.56 46.2504H311.56L310.27 52.5004H301.51L310.07 11.1504ZM316.61 21.9704L313.36 37.6704H319.89L316.61 21.9704ZM224.04 11.1504H206.96V52.5104H224.04C226.522 52.5077 228.901 51.5208 230.656 49.766C232.41 48.0112 233.397 45.632 233.4 43.1504V20.5104C233.395 18.0296 232.407 15.6519 230.653 13.8977C228.898 12.1435 226.521 11.1557 224.04 11.1504ZM224.81 41.8104C224.807 42.3692 224.584 42.9043 224.189 43.2995C223.794 43.6946 223.259 43.9178 222.7 43.9204H215.56V19.7404H222.7C223.259 19.743 223.794 19.9662 224.189 20.3613C224.584 20.7564 224.807 21.2916 224.81 21.8504V41.8104ZM248.47 11.1504H256.27C258.587 11.1659 260.816 12.0374 262.529 13.5972C264.242 15.1571 265.318 17.2952 265.55 19.6004C265.583 19.6761 265.6 19.7578 265.6 19.8404V24.5404H257.01V21.8104C257.007 21.2499 256.783 20.7132 256.386 20.3178C255.988 19.9224 255.451 19.7004 254.89 19.7004H249.77C249.211 19.703 248.676 19.9262 248.281 20.3213C247.886 20.7164 247.663 21.2516 247.66 21.8104V41.8104C247.663 42.3692 247.886 42.9043 248.281 43.2995C248.676 43.6946 249.211 43.9178 249.77 43.9204H254.89C255.171 43.9257 255.45 43.8751 255.711 43.7716C255.973 43.668 256.211 43.5136 256.412 43.3173C256.613 43.1209 256.772 42.8866 256.882 42.6279C256.992 42.3692 257.049 42.0913 257.05 41.8104V37.4904H251.16V29.6404H265.64V43.1504C265.635 45.6329 264.646 48.0121 262.889 49.7666C261.133 51.5211 258.753 52.5077 256.27 52.5104H248.47C245.988 52.5077 243.607 51.5211 241.851 49.7666C240.095 48.0121 239.105 45.6329 239.1 43.1504V20.5104C239.108 18.0287 240.098 15.651 241.854 13.8971C243.61 12.1432 245.988 11.1557 248.47 11.1504Z"
|
||||||
|
fill="black"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.6 KiB |
BIN
front/public/regular/Pokrovsk_Cup_2026.pdf
Normal file
@@ -1,403 +1,23 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {Container,} from '@mui/material';
|
||||||
Container,
|
import {CalendarContent} from "../../../components/calendar/CalendarContent";
|
||||||
Typography,
|
import {Footer} from "../../../components/core/Footer";
|
||||||
Box,
|
import CalendarHeader from "../../../components/calendar/CalendarHeader";
|
||||||
Drawer,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
Chip,
|
|
||||||
Select,
|
|
||||||
MenuItem,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
useMediaQuery,
|
|
||||||
useTheme,
|
|
||||||
Paper,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { CalendarToday as CalendarIcon, FilterList as FilterIcon } from '@mui/icons-material';
|
|
||||||
import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
|
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
||||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
|
||||||
import ruLocale from 'date-fns/locale/ru';
|
|
||||||
|
|
||||||
const CHAMPIONSHIP_STAGES = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'SWC Зимний чемпионат',
|
|
||||||
stage: '2‑й этап',
|
|
||||||
date: '2026-02-08',
|
|
||||||
class: 'Юниоры',
|
|
||||||
status: 'Идёт',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Honda Winter Cup',
|
|
||||||
stage: '1‑й этап',
|
|
||||||
date: '2026-01-31',
|
|
||||||
class: 'Pro',
|
|
||||||
status: 'Регистрация открыта',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Кубок Покровска (онлайн)',
|
|
||||||
stage: '1‑й этап',
|
|
||||||
date: '2026-02-01',
|
|
||||||
class: 'Симулятор A',
|
|
||||||
status: 'Предрегистрация',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const CalendarPage = () => {
|
const CalendarPage = () => {
|
||||||
const theme = useTheme();
|
// const [selectedClass, setSelectedClass] = useState('Все');
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
|
||||||
|
|
||||||
const [viewDate, setViewDate] = useState(new Date()); // Дата для отображения месяца
|
|
||||||
const [selectedClass, setSelectedClass] = useState('Все');
|
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
|
||||||
|
|
||||||
// Фильтрованные этапы для текущего месяца/класса
|
|
||||||
const filteredStages = CHAMPIONSHIP_STAGES.filter((stage) => {
|
|
||||||
const stageDate = new Date(stage.date);
|
|
||||||
const isSameMonth = stageDate.getMonth() === viewDate.getMonth();
|
|
||||||
const isSameYear = stageDate.getFullYear() === viewDate.getFullYear();
|
|
||||||
const classMatch = selectedClass === 'Все' || stage.class === selectedClass;
|
|
||||||
return isSameMonth && isSameYear && classMatch;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Группируем этапы по дате
|
|
||||||
const stagesByDate = filteredStages.reduce((acc, stage) => {
|
|
||||||
const dayKey = new Date(stage.date).toDateString();
|
|
||||||
acc[dayKey] = acc[dayKey] || [];
|
|
||||||
acc[dayKey].push(stage);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// Содержимое drawer для мобильных
|
|
||||||
const drawerContent = (
|
|
||||||
<Box sx={{ p: 3, minWidth: 280 }}>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
Этапы на {viewDate.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' })}
|
|
||||||
</Typography>
|
|
||||||
{filteredStages.length === 0 ? (
|
|
||||||
<Typography color="text.secondary" sx={{ mt: 2 }}>
|
|
||||||
Нет этапов в этот день
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<List sx={{ mt: 2 }}>
|
|
||||||
{filteredStages.map((stage) => (
|
|
||||||
<ListItem key={stage.id} sx={{ mb: 1, borderBottom: '1px solid', borderColor: 'divider' }}>
|
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
|
||||||
<Typography variant="subtitle1" fontWeight="bold">
|
|
||||||
{stage.title}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{stage.stage} · {stage.class}
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ mt: 0.5 }}>
|
|
||||||
<Chip
|
|
||||||
label={stage.status}
|
|
||||||
size="small"
|
|
||||||
color={
|
|
||||||
stage.status === 'Идёт'
|
|
||||||
? 'warning'
|
|
||||||
: stage.status === 'Регистрация открыта'
|
|
||||||
? 'success'
|
|
||||||
: 'info'
|
|
||||||
}
|
|
||||||
sx={{ fontSize: '0.8rem' }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth={false} disableGutters sx={{px: {xs: 1, sm: 3, md: 6}}}>
|
<Container maxWidth={false} disableGutters sx={{px: {xs: 1, sm: 3, md: 6}}}>
|
||||||
{/* Шапка */}
|
{/* Шапка */}
|
||||||
<Box sx={{
|
<CalendarHeader selectedClass={'Все'} setSelectedClass={null}/>
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
mb: 4,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 2
|
|
||||||
}}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<CalendarIcon color="primary" />
|
|
||||||
<Typography variant="h4">Календарь чемпионатов</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Фильтры */}
|
|
||||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<FilterIcon />}
|
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
Фильтры
|
|
||||||
</Button>
|
|
||||||
{showFilters && (
|
|
||||||
<FormControl size="small" sx={{ minWidth: 120 }}>
|
|
||||||
<InputLabel>Класс</InputLabel>
|
|
||||||
<Select value={selectedClass} onChange={(e) => setSelectedClass(e.target.value)}>
|
|
||||||
{['Все', 'Юниоры', 'Взрослые', 'Богатыри', '35+', 'Pro', 'Amateur', 'Симулятор A', 'Симулятор B'].map(
|
|
||||||
(cls) => (
|
|
||||||
<MenuItem key={cls} value={cls}>
|
|
||||||
{cls}
|
|
||||||
</MenuItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Основной контент */}
|
{/* Основной контент */}
|
||||||
<Box sx={{
|
<CalendarContent selectedClass={'Все'}/>
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: { xs: '1fr', md: '2fr 1fr' },
|
|
||||||
gap: { xs: 0, md: 4 },
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: 1400,
|
|
||||||
mx: 'auto'
|
|
||||||
}}>
|
|
||||||
{/* Календарь */}
|
|
||||||
<Paper variant="outlined" sx={{ borderRadius: 2, overflow: 'hidden', height: '100%' }}>
|
|
||||||
<LocalizationProvider dateAdapter={AdapterDateFns} locale={ruLocale}>
|
|
||||||
<DateCalendar
|
|
||||||
value={viewDate}
|
|
||||||
onChange={(newValue) => {
|
|
||||||
// Не меняем viewDate при клике на день — только навигация по месяцам
|
|
||||||
if (newValue.getMonth() !== viewDate.getMonth()) {
|
|
||||||
setViewDate(newValue);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
showDaysOutsideCurrentMonth
|
|
||||||
skipDisabledDateSelection
|
|
||||||
sx={{
|
|
||||||
height: '100%',
|
|
||||||
'.MuiDayCalendar-dayButton': {
|
|
||||||
minHeight: { xs: 80, sm: 100 },
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
p: 1,
|
|
||||||
},
|
|
||||||
'.MuiPickersDay-root': {
|
|
||||||
fontSize: { xs: '1rem', sm: '1.1rem' },
|
|
||||||
},
|
|
||||||
// Подсветка дней с событиями
|
|
||||||
'&.MuiPickersDay-root[data-selected="true"]': {
|
|
||||||
backgroundColor: 'primary.main',
|
|
||||||
color: 'white',
|
|
||||||
},
|
|
||||||
'&.MuiPickersDay-root:has(.event-chip)': {
|
|
||||||
border: '2px solid',
|
|
||||||
borderColor: 'primary.main',
|
|
||||||
'&:hover': {
|
|
||||||
borderColor: 'primary.dark',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
dayRenderer={(params) => {
|
|
||||||
const dayKey = params.day.toDateString();
|
|
||||||
const events = stagesByDate[dayKey] || [];
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{ width: '100%', height: '100%' }}>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 500,
|
|
||||||
color: params.outsideCurrentMonth ? 'text.disabled' : 'inherit',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{params.day.getDate()}
|
|
||||||
</Typography>
|
|
||||||
{events.length > 0 && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mt: 0.5,
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 0.25,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{events.map((event, idx) => (
|
|
||||||
<Chip
|
|
||||||
key={idx}
|
|
||||||
label={event.stage}
|
|
||||||
size="small"
|
|
||||||
className="event-chip" // Для CSS-селектора
|
|
||||||
color={
|
|
||||||
event.status === 'Идёт'
|
|
||||||
? 'warning'
|
|
||||||
: event.status === 'Регистрация открыта'
|
|
||||||
? 'success'
|
|
||||||
: 'info'
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
backgroundColor:
|
|
||||||
event.status === 'Идёт'
|
|
||||||
? 'warning.dark'
|
|
||||||
: event.status === 'Регистрация открыта'
|
|
||||||
? 'success.dark'
|
|
||||||
: 'info.dark',
|
|
||||||
color: 'white',
|
|
||||||
fontSize: '0.65rem',
|
|
||||||
fontWeight: 500,
|
|
||||||
px: 0.3,
|
|
||||||
lineHeight: 1.1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</LocalizationProvider>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
{/* Список этапов (только на десктопе) */}
|
|
||||||
{!isMobile && (
|
|
||||||
<Box sx={{ overflowY: 'auto', maxHeight: 'calc(100vh - 200px)' }}>
|
|
||||||
<Paper variant="outlined" sx={{ borderRadius: 2 }}>
|
|
||||||
<Box sx={{ p: 3 }}>
|
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
Этапы на {viewDate.toLocaleDateString('ru-RU', { month: 'long', year: 'numeric' })}
|
|
||||||
</Typography>
|
|
||||||
{filteredStages.length === 0 ? (
|
|
||||||
<Typography color="text.secondary" sx={{ mt: 2 }}>
|
|
||||||
Нет этапов в выбранном периоде
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
filteredStages.map((stage) => (
|
|
||||||
<Box
|
|
||||||
key={stage.id}
|
|
||||||
sx={{
|
|
||||||
p: 2.5,
|
|
||||||
borderRadius: 2,
|
|
||||||
backgroundColor: 'action.hover',
|
|
||||||
mb: 2,
|
|
||||||
'&:last-child': { mb: 0 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle1"
|
|
||||||
fontWeight="bold"
|
|
||||||
noWrap
|
|
||||||
sx={{ mb: 0.5 }}
|
|
||||||
>
|
|
||||||
{stage.title}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
|
||||||
{stage.stage} · {stage.class}
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
||||||
<Chip
|
|
||||||
label={stage.status}
|
|
||||||
size="small"
|
|
||||||
color={
|
|
||||||
stage.status === 'Идёт'
|
|
||||||
? 'warning'
|
|
||||||
: stage.status === 'Регистрация открыта'
|
|
||||||
? 'success'
|
|
||||||
: 'info'
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
backgroundColor:
|
|
||||||
stage.status === 'Идёт'
|
|
||||||
? 'warning.dark'
|
|
||||||
: stage.status === 'Регистрация открыта'
|
|
||||||
? 'success.dark'
|
|
||||||
: 'info.dark',
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ mt: 1.5 }}
|
|
||||||
>
|
|
||||||
{new Date(stage.date).toLocaleDateString('ru-RU', {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
weekday: 'short',
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Drawer для мобильных */}
|
|
||||||
{isMobile && (
|
|
||||||
<Drawer
|
|
||||||
anchor="bottom"
|
|
||||||
open={openDrawer}
|
|
||||||
onClose={() => setOpenDrawer(false)}
|
|
||||||
PaperProps={{ sx: { borderRadius: '16px 16px 0 0' } }}
|
|
||||||
>
|
|
||||||
{drawerContent}
|
|
||||||
</Drawer>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Кнопка для открытия drawer на мобильных */}
|
|
||||||
{isMobile && filteredStages.length > 0 && (
|
|
||||||
<Box sx={{ position: 'fixed', bottom: 16, right: 16 }}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
size="large"
|
|
||||||
onClick={() => setOpenDrawer(true)}
|
|
||||||
startIcon={<CalendarIcon />}
|
|
||||||
sx={{
|
|
||||||
boxShadow: 3,
|
|
||||||
'&:hover': { boxShadow: 4 },
|
|
||||||
borderRadius: 24,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Этапы на сегодня
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Подвал */}
|
{/* Подвал */}
|
||||||
<Divider sx={{ my: 6 }} />
|
<Footer/>
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
align="center"
|
|
||||||
sx={{ mb: 4 }}
|
|
||||||
>
|
|
||||||
© 2026 КартХолл. Календарь соревнований.
|
|
||||||
</Typography>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default CalendarPage;
|
export default CalendarPage;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
CircularProgress
|
CircularProgress
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Launch as LaunchIcon, Download as DownloadIcon } from '@mui/icons-material';
|
import { Launch as LaunchIcon, Download as DownloadIcon } from '@mui/icons-material';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
|
|
||||||
// Импортируем константы (из предыдущего файла)
|
// Импортируем константы (из предыдущего файла)
|
||||||
import { MOCK_STAGES, STAGE_STATUSES } from '../../../data/constants';
|
import { MOCK_STAGES, STAGE_STATUSES } from '../../../data/constants';
|
||||||
@@ -32,7 +32,7 @@ const ChampionshipPage = () => {
|
|||||||
try {
|
try {
|
||||||
// В реальном проекте тут будет fetch(`/api/championships/${id}`)
|
// В реальном проекте тут будет fetch(`/api/championships/${id}`)
|
||||||
// Для примера фильтруем MOCK_STAGES по id этапа (упрощённо)
|
// Для примера фильтруем MOCK_STAGES по id этапа (упрощённо)
|
||||||
const stagesForChampionship = MOCK_STAGES.filter(stage => stage.id === Number(id));
|
const stagesForChampionship = MOCK_STAGES.filter(stage => stage.stage.id === Number(id));
|
||||||
|
|
||||||
if (stagesForChampionship.length === 0) {
|
if (stagesForChampionship.length === 0) {
|
||||||
setError(true);
|
setError(true);
|
||||||
@@ -40,10 +40,10 @@ const ChampionshipPage = () => {
|
|||||||
// Формируем данные чемпионата на основе первого подходящего этапа
|
// Формируем данные чемпионата на основе первого подходящего этапа
|
||||||
const stage = stagesForChampionship[0];
|
const stage = stagesForChampionship[0];
|
||||||
setChampionship({
|
setChampionship({
|
||||||
id: stage.id,
|
id: stage.stage.id,
|
||||||
title: stage.title,
|
title: stage.title,
|
||||||
description: stage.description || 'Информация о чемпионате отсутствует.',
|
description: stage.description || 'Информация о чемпионате отсутствует.',
|
||||||
regulationsUrl: '/dummy-regulations.pdf', // Условный URL регламента
|
regulationsUrl: '/regular/Pokrovsk_Cup_2026.pdf', // Условный URL регламента
|
||||||
stages: MOCK_STAGES.filter(s => s.title === stage.title) // Все этапы этого чемпионата
|
stages: MOCK_STAGES.filter(s => s.title === stage.title) // Все этапы этого чемпионата
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ const ChampionshipPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
<Container maxWidth="lg" sx={{ pt: 4 }}>
|
||||||
{/* Заголовок и основная информация */}
|
{/* Заголовок и основная информация */}
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
{championship.title}
|
{championship.title}
|
||||||
@@ -120,18 +120,21 @@ const ChampionshipPage = () => {
|
|||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
{sortedStages.map((stage) => (
|
{sortedStages.map((stage) => (
|
||||||
<Paper
|
<Paper
|
||||||
key={stage.id}
|
component={Link}
|
||||||
|
to={"/stages/" + stage.stage.id}
|
||||||
|
key={stage.stage.id}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{
|
sx={{
|
||||||
p: 3,
|
p: 3,
|
||||||
'&:hover': { boxShadow: 3 },
|
'&:hover': { boxShadow: 3 },
|
||||||
transition: 'box-shadow 0.2s'
|
transition: 'box-shadow 0.2s',
|
||||||
|
textDecoration: 'none'
|
||||||
}}
|
}}
|
||||||
onClick={() => window.location.replace("/stages/" + stage.id)}
|
onClick={() => window.location.replace()}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||||
<Typography variant="subtitle1" fontWeight={600}>
|
<Typography variant="subtitle1" fontWeight={600}>
|
||||||
{stage.stage}
|
{stage.stage.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Chip
|
<Chip
|
||||||
label={stage.status}
|
label={stage.status}
|
||||||
|
|||||||
@@ -1,193 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {Container,} from '@mui/material';
|
||||||
Box,
|
import ChampionshipHeader from "../../../components/championship/ChampionshipHeader";
|
||||||
Button,
|
import ChampionshipsList from "../../../components/championship/ChampionshipsList";
|
||||||
Card,
|
import {Footer} from "../../../components/core/Footer";
|
||||||
CardActions,
|
|
||||||
CardContent,
|
|
||||||
Container,
|
|
||||||
Grid,
|
|
||||||
IconButton,
|
|
||||||
Paper,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableRow,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import {Add as AddIcon, Edit as EditIcon, Visibility as VisibilityIcon} from '@mui/icons-material';
|
|
||||||
import Divider from "@mui/material/Divider";
|
|
||||||
|
|
||||||
// Данные чемпионатов (можно заменить на API-запрос)
|
|
||||||
const CHAMPIONSHIPS = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'SWC Зимний чемпионат 2025–2026',
|
|
||||||
season: 'Зима 2025–2026',
|
|
||||||
stages: 5,
|
|
||||||
status: 'Идёт',
|
|
||||||
classes: ['Юниоры', 'Взрослые', 'Богатыри', '35+'],
|
|
||||||
startDate: '18.01.2026',
|
|
||||||
endDate: '08.03.2026',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Honda Winter Cup 2026',
|
|
||||||
season: 'Зима 2026',
|
|
||||||
stages: 3,
|
|
||||||
status: 'Регистрация открыта',
|
|
||||||
classes: ['Pro', 'Amateur'],
|
|
||||||
startDate: '31.01.2026',
|
|
||||||
endDate: '28.02.2026',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Кубок Покровска 2026 (онлайн)',
|
|
||||||
season: '2026',
|
|
||||||
stages: 4,
|
|
||||||
status: 'Предрегистрация',
|
|
||||||
classes: ['Симулятор A', 'Симулятор B'],
|
|
||||||
startDate: '01.02.2026',
|
|
||||||
endDate: '25.03.2026',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const ChampionshipsPage = () => {
|
const ChampionshipsPage = () => {
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="xl" sx={{py: 6}}>
|
<Container maxWidth="xl" sx={{pt: 6}}>
|
||||||
{/* Заголовок и кнопка создания */}
|
{/* Заголовок и кнопка создания */}
|
||||||
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4}}>
|
<ChampionshipHeader/>
|
||||||
<Typography variant="h4">
|
|
||||||
Все чемпионаты
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
size={'small'}
|
|
||||||
color="primary"
|
|
||||||
startIcon={<AddIcon/>}
|
|
||||||
href="/championships/create"
|
|
||||||
sx={{px: 3}}
|
|
||||||
>
|
|
||||||
Создать чемпионат
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Список чемпионатов */}
|
{/* Список чемпионатов */}
|
||||||
<Grid container spacing={4}>
|
<ChampionshipsList/>
|
||||||
{CHAMPIONSHIPS.map((champ) => (
|
|
||||||
<Grid item xs={12} sm={6} md={4} key={champ.id}>
|
|
||||||
<Card variant="outlined" sx={{
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'space-between', // Растягивает содержимое, прижимает actions вниз
|
|
||||||
transition: 'transform 0.2s',
|
|
||||||
'&:hover': { transform: 'scale(1.02)'
|
|
||||||
}}}>
|
|
||||||
<CardContent>
|
|
||||||
<Box
|
|
||||||
sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'start', mb: 2}}>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="h6">
|
|
||||||
{champ.title}
|
|
||||||
</Typography>
|
|
||||||
<Tooltip title={champ.status}>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
backgroundColor: champ.status === 'Идёт' ? 'warning.main' :
|
|
||||||
champ.status === 'Регистрация открыта' ? 'success.main' : 'info.main',
|
|
||||||
color: 'white',
|
|
||||||
px: 1,
|
|
||||||
borderRadius: 1,
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{champ.status}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
|
||||||
{champ.season}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Таблица с основными данными */}
|
|
||||||
<Table size="small" sx={{mt: 2}}>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell variant="head">Этапы</TableCell>
|
|
||||||
<TableCell>{champ.stages}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell variant="head">Классы</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{champ.classes.join(', ')}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell variant="head">Начало</TableCell>
|
|
||||||
<TableCell>{champ.startDate}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell variant="head">Конец</TableCell>
|
|
||||||
<TableCell>{champ.endDate}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
<CardActions sx={{ justifyContent: 'flex-end', p: 2 }}>
|
|
||||||
{/* Действия */}
|
|
||||||
<Box sx={{display: 'flex', gap: 1}}>
|
|
||||||
<IconButton
|
|
||||||
component="a"
|
|
||||||
href={`/championships/${champ.id}`}
|
|
||||||
size="small"
|
|
||||||
color="primary"
|
|
||||||
title="Посмотреть детали"
|
|
||||||
>
|
|
||||||
<VisibilityIcon fontSize="small"/>
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
component="a"
|
|
||||||
href={`/championships/${champ.id}/edit`}
|
|
||||||
size="small"
|
|
||||||
color="secondary"
|
|
||||||
title="Редактировать"
|
|
||||||
>
|
|
||||||
<EditIcon fontSize="small"/>
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
</CardActions>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Сообщение, если чемпионатов нет */}
|
|
||||||
{CHAMPIONSHIPS.length === 0 && (
|
|
||||||
<Paper sx={{textAlign: 'center', py: 8}}>
|
|
||||||
<Typography color="text.secondary">
|
|
||||||
Пока нет ни одного чемпионата. Создайте первый!
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
sx={{mt: 2}}
|
|
||||||
href="/championships/create"
|
|
||||||
>
|
|
||||||
Создать чемпионат
|
|
||||||
</Button>
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Подвал */}
|
{/* Подвал */}
|
||||||
<Divider sx={{my: 6}}/>
|
<Footer/>
|
||||||
<Typography variant="body2" color="text.secondary" align="center">
|
|
||||||
© 2026 КартХолл. Управление чемпионатами.
|
|
||||||
</Typography>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
import Box from "@mui/material/Box";
|
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import * as React from "react";
|
|
||||||
import {useEffect, useState} from "react";
|
|
||||||
import Paper from "@mui/material/Paper";
|
|
||||||
import {Autocomplete} from "@mui/material";
|
|
||||||
import TextField from "@mui/material/TextField";
|
|
||||||
import {useAlert} from "../../../hooks/useAlert";
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import {EditCocktailReceipt} from "../../../components/cocktails/EditCocktailReceipt";
|
|
||||||
import {SelectEdit} from "../../../components/cocktails/SelectEdit";
|
|
||||||
import {useSearchParams} from "react-router-dom";
|
|
||||||
import {Loading} from "../../../components/core/Loading";
|
|
||||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
|
||||||
import {styled} from "@mui/material/styles";
|
|
||||||
import {cocktailClient} from "../../../lib/clients/CocktailClient";
|
|
||||||
import {categoryClient} from "../../../lib/clients/CategoryClient";
|
|
||||||
import {glassClient} from "../../../lib/clients/GlassClient";
|
|
||||||
|
|
||||||
const emptyCocktail = {
|
|
||||||
id: null,
|
|
||||||
name: "",
|
|
||||||
alcoholic: "",
|
|
||||||
category: "",
|
|
||||||
components: "",
|
|
||||||
glass: "",
|
|
||||||
image: "",
|
|
||||||
instructions: "",
|
|
||||||
isAllowed: false,
|
|
||||||
rating: {
|
|
||||||
rating: 0,
|
|
||||||
favourite: false
|
|
||||||
},
|
|
||||||
receipt: [],
|
|
||||||
tags: "",
|
|
||||||
video: ""
|
|
||||||
};
|
|
||||||
const alcohol = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: "Алкогольный"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: "Безалкогольный",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
name: "Опционально"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
const VisuallyHiddenInput = styled('input')({
|
|
||||||
clip: 'rect(0 0 0 0)',
|
|
||||||
clipPath: 'inset(50%)',
|
|
||||||
height: 1,
|
|
||||||
overflow: 'hidden',
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
width: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function EditCocktailPage() {
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const {createError, createSuccess, getError} = useAlert();
|
|
||||||
const [cocktails, setCocktails] = useState([]);
|
|
||||||
const [selected, setSelected] = useState(null);
|
|
||||||
const [cocktail, setCocktail] = useState(emptyCocktail);
|
|
||||||
|
|
||||||
const [glass, setGlass] = useState([]);
|
|
||||||
const [category, setCategory] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
cocktailClient.getSimpleList(setCocktails, setSelected, setLoading, createError, searchParams.get("id"))
|
|
||||||
categoryClient.getCategoryList(setCategory, createError);
|
|
||||||
glassClient.getGlassList(setGlass, createError)
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
useEffect(() => cocktailClient.getOneCocktail(selected, setCocktail, getError, emptyCocktail), [selected])
|
|
||||||
const saveHandler = () => cocktailClient.saveChangeCocktail(cocktail, createError, createSuccess)
|
|
||||||
const deleteHandle = () => cocktailClient.deleteCocktailFromEdit(setCocktails, setCocktail, createError, cocktails, cocktail, emptyCocktail)
|
|
||||||
|
|
||||||
const changeCocktailValue = (name, value) => {
|
|
||||||
if (name === "tags") {
|
|
||||||
value = value.join(",");
|
|
||||||
}
|
|
||||||
setCocktail((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
{/*Загрузка*/}
|
|
||||||
<Loading loading={loading}/>
|
|
||||||
{/*Заголовок*/}
|
|
||||||
<Toolbar>
|
|
||||||
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>Коктейли</Typography>
|
|
||||||
</Toolbar>
|
|
||||||
{/*Поиск*/}
|
|
||||||
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 2}}>
|
|
||||||
<Autocomplete
|
|
||||||
disablePortal
|
|
||||||
options={cocktails}
|
|
||||||
onChange={(e, v) => {
|
|
||||||
if (!v) {
|
|
||||||
setCocktail(emptyCocktail);
|
|
||||||
setSelected(null)
|
|
||||||
} else {
|
|
||||||
setSelected(v.id)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
isOptionEqualToValue={(selected, value) => selected.id === value.id}
|
|
||||||
getOptionKey={(selected) => selected.id}
|
|
||||||
getOptionLabel={(selected) => selected.name + (selected.hasError ? " (есть ошибка)" : "")}
|
|
||||||
renderInput={(params) => <TextField {...params} label="Поиск"/>}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
{/*Рабочая область*/}
|
|
||||||
<Paper elevation={6} sx={{my: 2, display: 'grid', p: 1, pb: 2}}>
|
|
||||||
<Stack>
|
|
||||||
<Box hidden={cocktail.id === null} ml={1} mb={1}>
|
|
||||||
<Button color='error' onClick={() => deleteHandle()}>Удалить коктейль</Button>
|
|
||||||
</Box>
|
|
||||||
{/*Фото*/}
|
|
||||||
<Box ml={1}>
|
|
||||||
<img src={cocktail.image} alt={""} width={300} height={300} loading={'eager'}/>
|
|
||||||
</Box>
|
|
||||||
{/*Редактирование ссылки на фото*/}
|
|
||||||
<Stack direction='row' pr={2} m={1} display='relative'>
|
|
||||||
<TextField sx={{width: '75%'}}
|
|
||||||
label={"Ссылка на фото"} variant='outlined' multiline
|
|
||||||
value={!cocktail.image ? "" : cocktail.image}
|
|
||||||
onChange={(e) => changeCocktailValue("image", e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
component="label"
|
|
||||||
role={undefined}
|
|
||||||
variant="contained"
|
|
||||||
tabIndex={-1}
|
|
||||||
startIcon={<CloudUploadIcon/>}
|
|
||||||
sx={{width: '10%', fontSize: 40, ml: 1, pr: 1}}
|
|
||||||
>
|
|
||||||
<VisuallyHiddenInput
|
|
||||||
type="file"
|
|
||||||
accept=".jpg,.jpeg,.png"
|
|
||||||
onChange={(event) => cocktailClient.savePhoto(event, changeCocktailValue, getError)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
</Stack>
|
|
||||||
{/*Название*/}
|
|
||||||
<Box m={1}>
|
|
||||||
<TextField sx={{mr: 1, mb: 2, minWidth: 300}}
|
|
||||||
variant="outlined" label={"Название"}
|
|
||||||
value={cocktail.name}
|
|
||||||
onChange={(e) => changeCocktailValue("name", e.target.value)}/>
|
|
||||||
</Box>
|
|
||||||
{/*Категория, посуда, алкогольность, теги*/}
|
|
||||||
<Box mb={2}>
|
|
||||||
<SelectEdit value={cocktail.category} label={"Категория"} width={300} margin={1}
|
|
||||||
array={category}
|
|
||||||
attributeName={"category"} handler={changeCocktailValue}/>
|
|
||||||
<SelectEdit value={cocktail.glass} label={"Посуда"} width={300} margin={1} array={glass}
|
|
||||||
attributeName={"glass"} handler={changeCocktailValue}/>
|
|
||||||
<SelectEdit value={cocktail.alcoholic} label={"Алкогольность"} width={300} margin={1}
|
|
||||||
array={alcohol}
|
|
||||||
attributeName={"alcoholic"} handler={changeCocktailValue}/>
|
|
||||||
</Box>
|
|
||||||
{/*Рецепт*/}
|
|
||||||
<EditCocktailReceipt receipt={cocktail.receipt} handler={changeCocktailValue}/>
|
|
||||||
|
|
||||||
<Box pr={2} ml={1}>
|
|
||||||
<TextField sx={{width: '100%'}}
|
|
||||||
label={"Инструкция"} variant='outlined' multiline
|
|
||||||
value={!cocktail.instructions ? "" : cocktail.instructions}
|
|
||||||
onChange={(e) => changeCocktailValue("instructions", e.target.value)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
|
||||||
<Box display={'flex'} justifyContent={'flex-end'}>
|
|
||||||
<Button variant='contained' onClick={() => saveHandler()}>Сохранить</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,332 +1,43 @@
|
|||||||
import Grid from "@mui/material/Grid";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {Card, CardContent} from "@mui/material";
|
|
||||||
import {useUser} from "../../../hooks/useUser";
|
import {useUser} from "../../../hooks/useUser";
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import Divider from "@mui/material/Divider";
|
|
||||||
import Container from "@mui/material/Container";
|
import Container from "@mui/material/Container";
|
||||||
import Avatar from "@mui/material/Avatar";
|
import AnnounceBlock from "../../../components/home/AnnounceBlock";
|
||||||
import {Trophy} from "@phosphor-icons/react";
|
import WelcomeBlock from "../../../components/home/WelcomeBlock";
|
||||||
|
import SponsorsBlock from "../../../components/home/SponsorsBlock";
|
||||||
// Компонент таймера (упрощённый)
|
import RentBlock from "../../../components/home/RentBlock";
|
||||||
const CountdownTimer = ({targetDate}) => {
|
import {Footer} from "../../../components/core/Footer";
|
||||||
const [timeLeft, setTimeLeft] = React.useState({});
|
import NextStageTimer from "../../../components/home/NextStageTimer";
|
||||||
|
import LastResult from "../../../components/home/LastResult";
|
||||||
React.useEffect(() => {
|
|
||||||
const updateTimer = () => {
|
|
||||||
const now = new Date().getTime();
|
|
||||||
const distance = new Date(targetDate).getTime() - now;
|
|
||||||
|
|
||||||
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
|
||||||
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
|
||||||
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
|
||||||
|
|
||||||
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
|
||||||
|
|
||||||
setTimeLeft({days, hours, minutes, seconds});
|
|
||||||
};
|
|
||||||
|
|
||||||
const interval = setInterval(updateTimer, 1000);
|
|
||||||
updateTimer();
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [targetDate]);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Typography variant="h6" color="error" sx={{fontWeight: 'bold'}}>
|
|
||||||
До старта: {timeLeft.days}д {timeLeft.hours}ч {timeLeft.minutes}м {timeLeft.seconds}с
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Основной компонент приветственного блока
|
// Основной компонент приветственного блока
|
||||||
const HomePageContent = () => {
|
const HomePageContent = () => {
|
||||||
const {user} = useUser();
|
const {user} = useUser();
|
||||||
|
|
||||||
// Данные чемпионатов (можно подтянуть из API/state)
|
|
||||||
const championships = [
|
|
||||||
{
|
|
||||||
title: 'SWC Зимний чемпионат 2025–2026',
|
|
||||||
stage: '2‑й этап',
|
|
||||||
date: '08.02.2026',
|
|
||||||
link: '/swc-registration',
|
|
||||||
icon: <Trophy fontSize="large"/>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Honda Winter Cup 2026',
|
|
||||||
stage: '1‑й этап',
|
|
||||||
date: '31.01.2026',
|
|
||||||
link: '/hwc-info',
|
|
||||||
icon: <Trophy fontSize="large"/>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Кубок Покровска 2026 (онлайн)',
|
|
||||||
stage: '1‑й этап',
|
|
||||||
date: '01.02.2026',
|
|
||||||
link: '/pokrovsk-sim',
|
|
||||||
icon: <Trophy fontSize="large"/>,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Результаты последнего этапа
|
|
||||||
const results = {
|
|
||||||
Юниоры: ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
|
|
||||||
Взрослые: ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
|
|
||||||
Богатыри: ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
|
|
||||||
'35+': ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Спонсоры (макеты URL)
|
|
||||||
const sponsors = [
|
|
||||||
{name: 'Минеральная вода Ульянка', logo: '/sponsors/ulyanka.png'},
|
|
||||||
{name: 'Gosha Racing Team', logo: '/sponsors/gosha.png'},
|
|
||||||
{name: 'Diff', logo: '/sponsors/diff.png'},
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="xl" sx={{py: 3}}>
|
<Container maxWidth="xl" sx={{py: 3}}>
|
||||||
{/* 1. Приветствие */}
|
{/* 1. Приветствие */}
|
||||||
<Typography variant="h4" gutterBottom>
|
<WelcomeBlock/>
|
||||||
Добро пожаловать на платформу «КартХолл»!
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h6" color="text.secondary" gutterBottom sx={{mb: 4}}>
|
|
||||||
Ваш гид по гоночным чемпионатам, этапам и онлайн‑соревнованиям.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* 2. Карточки анонсов */}
|
{/* 2. Карточки анонсов */}
|
||||||
<Grid container spacing={4} sx={{mb: 6}}>
|
<AnnounceBlock/>
|
||||||
{championships.map((champ, index) => (
|
{/*//todo: вопрос о необходимости*/}
|
||||||
<Grid item xs={12} sm={6} md={4} key={index}>
|
|
||||||
<Card variant="outlined" sx={{
|
|
||||||
height: '100%',
|
|
||||||
transition: 'transform 0.2s',
|
|
||||||
'&:hover': {transform: 'scale(1.02)'}
|
|
||||||
}}>
|
|
||||||
<CardContent>
|
|
||||||
<Box sx={{display: 'flex', alignItems: 'center', mb: 2}}>
|
|
||||||
{champ.icon}
|
|
||||||
<Typography variant="h6" sx={{ml: 1}}>
|
|
||||||
{champ.title}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Typography color="text.secondary" sx={{mb: 1}}>
|
|
||||||
{champ.stage} — {champ.date}
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
href={champ.link}
|
|
||||||
sx={{mt: 2}}
|
|
||||||
>
|
|
||||||
Подробнее
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* 3. Быстрые действия */}
|
{/* 3. Быстрые действия */}
|
||||||
<Box sx={{display: 'flex', flexWrap: 'wrap', gap: 2, mb: 6}}>
|
{/*<QuickActions/>*/}
|
||||||
<Button variant="contained" color="secondary" href="/register">
|
|
||||||
Зарегистрироваться на этап
|
|
||||||
</Button>
|
|
||||||
<Button variant="outlined" href="/calendar">
|
|
||||||
Посмотреть календарь всех этапов
|
|
||||||
</Button>
|
|
||||||
<Button variant="outlined" href="/rules">
|
|
||||||
Правила гонок
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 4. Спонсоры */}
|
{/* 4. Спонсоры */}
|
||||||
<Box sx={{display: 'flex', overflowX: 'auto', py: 2, gap: 3, mb: 4}}>
|
<SponsorsBlock/>
|
||||||
{sponsors.map((sponsor, idx) => (
|
|
||||||
<Avatar
|
|
||||||
key={idx}
|
|
||||||
src={sponsor.logo}
|
|
||||||
alt={sponsor.name}
|
|
||||||
sx={{width: 300, height: 150, borderRadius: '8px'}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{mb: 4}}>
|
|
||||||
Наши партнёры: поддержка чемпионатов и призов
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* 5. Прокат */}
|
{/* 5. Прокат */}
|
||||||
<Box sx={{textAlign: 'center', mb: 6}}>
|
<RentBlock/>
|
||||||
<Typography variant="h5" sx={{mb: 2}}>
|
|
||||||
Не гоняешь? Попробуй прокат!
|
|
||||||
</Typography>
|
|
||||||
<Typography color="text.secondary" sx={{mb: 3}}>
|
|
||||||
Ощутите адреналин за рулём прокатного карта. Доступно ежедневно с 10:00 до 22:00.
|
|
||||||
</Typography>
|
|
||||||
<Button variant="contained" color="success" href="/rent">
|
|
||||||
Забронировать карта
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 6. Таймер до ближайшего этапа (Honda Winter Cup) */}
|
{/* 6. Таймер до ближайшего этапа (Honda Winter Cup) */}
|
||||||
<Box sx={{textAlign: 'center', mb: 6}}>
|
<NextStageTimer/>
|
||||||
<Typography variant="h5" sx={{mb: 2}}>
|
|
||||||
Следующий этап стартует уже:
|
|
||||||
</Typography>
|
|
||||||
<CountdownTimer targetDate="2026-01-31T14:00:00"/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* 7. Результаты последнего этапа */}
|
{/* 7. Результаты последнего этапа */}
|
||||||
<Box sx={{mb: 6}}>
|
<LastResult/>
|
||||||
<Typography variant="h5" sx={{mb: 3}}>
|
|
||||||
Итоги 1‑го этапа SWC (18.01.2026)
|
|
||||||
</Typography>
|
|
||||||
{Object.entries(results).map(([category, winners]) => (
|
|
||||||
<Box key={category} sx={{mb: 2}}>
|
|
||||||
<Typography variant="subtitle1" color="primary">
|
|
||||||
{category}:
|
|
||||||
</Typography>
|
|
||||||
<Typography color="text.secondary">
|
|
||||||
1 место: {winners[0]}, 2 место: {winners[1]}, 3 место: {winners[2]}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
href="/stages"
|
|
||||||
sx={{mt: 3}}
|
|
||||||
>
|
|
||||||
Все результаты
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Разделитель и подвал */}
|
{/* Разделитель и подвал */}
|
||||||
<Divider sx={{my: 4}}/>
|
<Footer/>
|
||||||
<Typography variant="body2" color="text.secondary" align="center">
|
|
||||||
© 2026 КартХолл. Все права защищены.
|
|
||||||
</Typography>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|
||||||
// return (
|
|
||||||
// <Paper sx={{flexGrow: 1, p: 1}}>
|
|
||||||
// <Box sx={{mb: 3, p: 2}}>
|
|
||||||
// <Typography variant="h3" component="div" sx={{flexGrow: 1}}>
|
|
||||||
// Добро пожаловать на платформу «КартХолл»!
|
|
||||||
// </Typography>
|
|
||||||
// <Typography variant="h6" component="div" sx={{flexGrow: 1, mt: 1}}>
|
|
||||||
// Ваш гид по гоночным чемпионатам, этапам и онлайн‑соревнованиям.
|
|
||||||
// </Typography>
|
|
||||||
// </Box>
|
|
||||||
//
|
|
||||||
// <Box sx={{p: 2}}>
|
|
||||||
//
|
|
||||||
// <Grid spacing={1} rowSpacing={1}>
|
|
||||||
// {anounce.map((item, index) => {
|
|
||||||
// return (
|
|
||||||
// // Карточка анонса
|
|
||||||
// <Grid size={3}>
|
|
||||||
// <Paper sx={{p: 2, mb: 1}}>
|
|
||||||
// <Stack>
|
|
||||||
// <Typography variant="h5">{item.championship}</Typography>
|
|
||||||
// </Stack>
|
|
||||||
// <CardMedia
|
|
||||||
// sx={{
|
|
||||||
// loading: "eager",
|
|
||||||
// borderRadius: 2
|
|
||||||
// }}
|
|
||||||
// // onClick={() => handleSelect(row)}
|
|
||||||
// component="img"
|
|
||||||
// alt={item.name}
|
|
||||||
// height="300"
|
|
||||||
//
|
|
||||||
// image={item.photo}
|
|
||||||
// />
|
|
||||||
// <CardContent sx={{pb: 0, pl: 2, pt: 0}}>
|
|
||||||
// <Typography variant="h6"
|
|
||||||
// mt={2}>{item.name} - {item.message} </Typography>
|
|
||||||
// <Typography> </Typography>
|
|
||||||
// {/*<CocktailDescription row={row}/>*/}
|
|
||||||
// </CardContent>
|
|
||||||
// <CardActions>
|
|
||||||
// <Button variant="contained" color="primary" component="div"
|
|
||||||
// // onClick={() => cocktailClient.drinkCocktail(row.id, createSuccess, createError)}
|
|
||||||
// >
|
|
||||||
// <LocalBarIcon fontSize='small'/> Регистрация
|
|
||||||
// </Button>
|
|
||||||
// </CardActions>
|
|
||||||
// </Paper>
|
|
||||||
// </Grid>
|
|
||||||
// )
|
|
||||||
// })}
|
|
||||||
// </Grid>
|
|
||||||
// </Box>
|
|
||||||
//
|
|
||||||
// <Card sx={{p: 2, mt: 3}}>
|
|
||||||
// {/*todo: под вопросом*/}
|
|
||||||
// <Typography variant={'h6'}>Быстрые действия</Typography>
|
|
||||||
// <Stack direction={"row"} spacing={2} sx={{justifyContent: 'center', alignItems: 'center'}}>
|
|
||||||
// <Button variant={"contained"} size={"large"}>
|
|
||||||
// Зарегистрироваться на этап
|
|
||||||
// </Button>
|
|
||||||
// <Button variant={"contained"} size={"large"}>
|
|
||||||
// Посмотреть календарь этапов
|
|
||||||
// </Button>
|
|
||||||
// </Stack>
|
|
||||||
// </Card>
|
|
||||||
//
|
|
||||||
// <Box sx={{p: 2, my: 3}}>
|
|
||||||
// <Stack direction={'row'} sx={{my: 1}}>
|
|
||||||
// {
|
|
||||||
// sponsors.map((item) => {
|
|
||||||
// return (
|
|
||||||
// <Card sx={{m:1}}>
|
|
||||||
// <CardMedia
|
|
||||||
// sx={{
|
|
||||||
// loading: "eager",
|
|
||||||
// borderRadius: 2
|
|
||||||
// }}
|
|
||||||
// // onClick={() => handleSelect(row)}
|
|
||||||
// component="img"
|
|
||||||
// alt={item.alt}
|
|
||||||
// height="60"
|
|
||||||
//
|
|
||||||
// image={item.photo}
|
|
||||||
// />
|
|
||||||
// </Card>
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// </Stack>
|
|
||||||
// <Typography variant={'h5'}>Наши партнёры: поддержка чемпионатов и призов</Typography>
|
|
||||||
// </Box>
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// <Fab sx={{
|
|
||||||
// alpha: '30%',
|
|
||||||
// position: 'sticky',
|
|
||||||
// left: 'calc(100% - 16px)',
|
|
||||||
// bottom: '16px',
|
|
||||||
// color: 'common.white',
|
|
||||||
// bgcolor: blue[600],
|
|
||||||
// '&:hover': {
|
|
||||||
// bgcolor: blue[600],
|
|
||||||
// },
|
|
||||||
// }}
|
|
||||||
// onClick={() => window.window.scrollTo(0, 0)}
|
|
||||||
// aria-label='Expand'
|
|
||||||
// color='inherit'>
|
|
||||||
// <UpIcon/>
|
|
||||||
// </Fab>
|
|
||||||
// </Paper>
|
|
||||||
// );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default HomePageContent;
|
export default HomePageContent;
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
export const anounce = [
|
|
||||||
{
|
|
||||||
championship: 'SWC Зимний чемпионат 2025–2026',
|
|
||||||
name: '2‑й этап',
|
|
||||||
date: '08.02.2026',
|
|
||||||
message: 'Открыта регистрация!',
|
|
||||||
new: false,
|
|
||||||
photo: '/assets/background.png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
championship: 'Honda Winter Cup 2026',
|
|
||||||
name: '1‑й этап',
|
|
||||||
date: '31.01.2026',
|
|
||||||
message: 'Успейте зарегистрироваться: осталось 12 мест',
|
|
||||||
new: true,
|
|
||||||
photo: '/assets/background.png'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
championship: 'Кубок Покровска 2026 (онлайн)',
|
|
||||||
name: '1‑й этап',
|
|
||||||
date: '01.02.2026',
|
|
||||||
message: 'Участвуйте из дома: подключение через симулятор',
|
|
||||||
new: true,
|
|
||||||
photo: '/assets/background.png'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
export const sponsors = [
|
|
||||||
{
|
|
||||||
alt: 'Ульянка',
|
|
||||||
photo: '/assets/logo_ulyanka-1.svg',
|
|
||||||
link: '#'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alt: 'Ульянка',
|
|
||||||
photo: '/assets/logo_ulyanka-1.svg',
|
|
||||||
link: '#'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alt: 'Ульянка',
|
|
||||||
photo: '/assets/logo_ulyanka-1.svg',
|
|
||||||
link: '#'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alt: 'Ульянка',
|
|
||||||
photo: '/assets/logo_ulyanka-1.svg',
|
|
||||||
link: '#'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alt: 'Ульянка',
|
|
||||||
photo: '/assets/logo_ulyanka-1.svg',
|
|
||||||
link: '#'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alt: 'Ульянка',
|
|
||||||
photo: '/assets/logo_ulyanka-1.svg',
|
|
||||||
link: '#'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alt: 'Ульянка',
|
|
||||||
photo: '/assets/logo_ulyanka-1.svg',
|
|
||||||
link: '#'
|
|
||||||
},
|
|
||||||
|
|
||||||
]
|
|
||||||
88
front/src/app/pages/places/PlacePage.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// src/routes/RacetrackDetail.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import {useParams} from 'react-router-dom';
|
||||||
|
import {Box, Card, CardMedia, Container, Typography} from '@mui/material';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
|
import {places} from "../../../data/constants";
|
||||||
|
import Carousel from "react-material-ui-carousel";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
|
||||||
|
import {paths} from "../../../path";
|
||||||
|
|
||||||
|
const PlacePage = () => {
|
||||||
|
const {id} = useParams();
|
||||||
|
// Здесь должен быть код для получения данных трассы по ID
|
||||||
|
let track = places.find((t) => t.id === Number(id));
|
||||||
|
if (!track) {
|
||||||
|
return <Typography>Трасса не найдена</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// track = undefined;
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{py: 4}}>
|
||||||
|
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
|
||||||
|
<Typography variant="h5">{track.name}</Typography>
|
||||||
|
<IconButton component={Link} to={paths.places.places}>
|
||||||
|
<KeyboardReturnIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{mt: 3}}>
|
||||||
|
<Carousel
|
||||||
|
swipe
|
||||||
|
sx={{
|
||||||
|
height: 400, // фиксированная высота карусели
|
||||||
|
'& .MuiCard-root': {
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{track.images.map((item, index) => (
|
||||||
|
<Card key={index} sx={{ width: '100%', height: '100%' }}>
|
||||||
|
<CardMedia
|
||||||
|
image={item}
|
||||||
|
alt={item}
|
||||||
|
component="img"
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
maxHeight: '100%',
|
||||||
|
objectFit: 'cover', // сохраняет пропорции, заполняет контейнер
|
||||||
|
objectPosition: 'center', // центрирует изображение
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
<Box sx={{mt: 3}}>
|
||||||
|
<Typography variant="subtitle1">Адрес:</Typography>
|
||||||
|
<Typography variant="body1">{track.address || 'Не указан'}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{mt: 2}}>
|
||||||
|
<Typography variant="subtitle1">Тип площадки:</Typography>
|
||||||
|
<Typography variant="body1">{track.type}</Typography>
|
||||||
|
</Box>
|
||||||
|
{track.capacity && (
|
||||||
|
<Box sx={{mt: 2}}>
|
||||||
|
<Typography variant="subtitle1">Вместимость:</Typography>
|
||||||
|
<Typography variant="body1">{track.capacity} человек</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box sx={{mt: 2}}>
|
||||||
|
<Typography variant="subtitle1">Контактные данные:</Typography>
|
||||||
|
<Typography variant="body1">{track.contact}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{mt: 2}}>
|
||||||
|
<Typography variant="subtitle1">Номер бронирования:</Typography>
|
||||||
|
<Typography variant="body1">{track.bookingNumber}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlacePage;
|
||||||
64
front/src/app/pages/places/PlacesPage.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// src/pages/RacetracksPage.jsx
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardActionArea,
|
||||||
|
CardMedia,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import {places} from "../../../data/constants";
|
||||||
|
|
||||||
|
const PlacesPage = () => {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
Список гоночных трасс и картодромов
|
||||||
|
</Typography>
|
||||||
|
<Divider sx={{ my: 3 }} />
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '20px' }}>
|
||||||
|
{places.map((track) => (
|
||||||
|
<Card key={track.id} sx={{ width: '300px', height: '100%', p:1}}>
|
||||||
|
<CardActionArea component={Link} to={`/places/${track.id}`}>
|
||||||
|
<CardMedia
|
||||||
|
component="img"
|
||||||
|
height="200"
|
||||||
|
image={track.images[0]}
|
||||||
|
alt={track.name}
|
||||||
|
/>
|
||||||
|
<CardHeader title={track.name} />
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{track.address ? track.address : 'Нет данных об адресе'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Тип: {track.type}
|
||||||
|
</Typography>
|
||||||
|
{track.capacity && (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Вместимость: {track.capacity} человек
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</CardActionArea>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
component={Link}
|
||||||
|
to={`/places/${track.id}`}
|
||||||
|
>
|
||||||
|
Подробнее
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlacesPage;
|
||||||
@@ -1,28 +1,55 @@
|
|||||||
// src/pages/StagePage.jsx
|
import React, {useEffect, useState} from 'react';
|
||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import {
|
import {
|
||||||
Container, Typography, Button, Paper, Box, Divider, Alert, Chip
|
Alert,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
CircularProgress,
|
||||||
|
Container,
|
||||||
|
Paper,
|
||||||
|
Tab,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Tabs,
|
||||||
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Download as DownloadIcon } from '@mui/icons-material';
|
import {Download as DownloadIcon, Print as PrintIcon} from '@mui/icons-material';
|
||||||
import {useParams} from 'react-router-dom';
|
import {useParams} from 'react-router-dom';
|
||||||
import {MOCK_STAGES, STAGE_STATUSES} from '../../../data/constants';
|
import {MOCK_STAGES, STAGE_STATUSES} from '../../../data/constants';
|
||||||
|
import StagesHeader from "../../../components/stages/StagesHeader";
|
||||||
|
|
||||||
const StagePage = () => {
|
const StagePage = () => {
|
||||||
const {id} = useParams();
|
const {id} = useParams();
|
||||||
const [stage, setStage] = useState(null);
|
const [stage, setStage] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState('info'); // info | classes | events
|
||||||
|
const [activeClassTab, setActiveClassTab] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const foundStage = MOCK_STAGES.find(s => s.id === Number(id));
|
const foundStage = MOCK_STAGES.find(s => s.id === Number(id));
|
||||||
if (foundStage) {
|
if (foundStage) {
|
||||||
setStage(foundStage);
|
setStage(foundStage);
|
||||||
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
setError(true);
|
setError(true);
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
if (error) {
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{py: 4, textAlign: 'center'}}>
|
||||||
|
<CircularProgress/>
|
||||||
|
<Typography sx={{mt: 2}}>Загрузка данных этапа...</Typography>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !stage) {
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="lg" sx={{py: 4}}>
|
<Container maxWidth="lg" sx={{py: 4}}>
|
||||||
<Alert severity="error" sx={{mb: 3}}>
|
<Alert severity="error" sx={{mb: 3}}>
|
||||||
@@ -35,103 +62,264 @@ const StagePage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stage) {
|
// Пример данных участников (в реальном проекте — из API)
|
||||||
return null; // или спиннер
|
const participants = {
|
||||||
}
|
'Юниоры': [
|
||||||
|
{id: 1, name: 'Иван Петров', number: 12},
|
||||||
|
{id: 2, name: 'Анна Сидорова', number: 7},
|
||||||
|
// ...
|
||||||
|
],
|
||||||
|
'Взрослые': [
|
||||||
|
{id: 3, name: 'Сергей Иванов', number: 25},
|
||||||
|
// резерв
|
||||||
|
{id: 4, name: 'Алексей Козлов', number: 99, isReserve: true},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const maxParticipantsPerClass = 10;
|
||||||
|
|
||||||
|
// Определяем сценарий
|
||||||
|
const isRegistrationClosed = stage.status === STAGE_STATUSES.PRE_REGISTRATION;
|
||||||
|
const isRegistrationOpen = stage.status === STAGE_STATUSES.REGISTRATION_OPEN;
|
||||||
|
const isInProgress = stage.status === STAGE_STATUSES.GOING;
|
||||||
|
const isCompleted = stage.status === STAGE_STATUSES.COMPLETED;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="lg" sx={{py: 4}}>
|
<Container maxWidth="lg" sx={{py: 4}}>
|
||||||
<Typography variant="h4" gutterBottom>{stage.title}</Typography>
|
{/* Шапка этапа */}
|
||||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
<StagesHeader stage={stage}/>
|
||||||
{stage.stage}
|
|
||||||
|
{/* Табы навигации */}
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(e, newValue) => setActiveTab(newValue)}
|
||||||
|
sx={{mb: 3}}
|
||||||
|
>
|
||||||
|
<Tab label="Информация" value="info"/>
|
||||||
|
{(isRegistrationOpen || isInProgress || isCompleted) && (
|
||||||
|
<Tab label="Классы" value="classes"/>
|
||||||
|
)}
|
||||||
|
{(isInProgress || isCompleted) && <Tab label="События" value="events"/>}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Контент в зависимости от активного таба */}
|
||||||
|
{activeTab === 'info' && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" gutterBottom>О этапе</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
Здесь можно разместить подробную информацию о формате этапа, правилах,
|
||||||
|
требованиях к участникам и т.д.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Paper sx={{ p: 4, mb: 4 }}>
|
{isRegistrationClosed && (
|
||||||
<Typography variant="h5" gutterBottom>Информация об этапе</Typography>
|
<Alert severity="info" sx={{my: 3}}>
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 3 }}>
|
Регистрация откроется позже. Следите за обновлениями!
|
||||||
<Box>
|
</Alert>
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
)}
|
||||||
Дата
|
|
||||||
</Typography>
|
|
||||||
<Typography>
|
|
||||||
{new Date(stage.date).toLocaleDateString('ru-RU', {
|
|
||||||
weekday: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
year: 'numeric'
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
|
||||||
Место проведения
|
|
||||||
</Typography>
|
|
||||||
<Typography>{stage.location}</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
|
||||||
Класс
|
|
||||||
</Typography>
|
|
||||||
<Typography>{stage.class}</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle2" color="text.secondary">
|
|
||||||
Статус
|
|
||||||
</Typography>
|
|
||||||
<Chip
|
|
||||||
label={stage.status}
|
|
||||||
color={
|
|
||||||
stage.status === STAGE_STATUSES.GOING
|
|
||||||
? 'warning'
|
|
||||||
: stage.status === STAGE_STATUSES.REGISTRATION_OPEN
|
|
||||||
? 'success'
|
|
||||||
: stage.status === STAGE_STATUSES.PRE_REGISTRATION
|
|
||||||
? 'info'
|
|
||||||
: 'default'
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
backgroundColor:
|
|
||||||
stage.status === STAGE_STATUSES.GOING
|
|
||||||
? 'warning.dark'
|
|
||||||
: stage.status === STAGE_STATUSES.REGISTRATION_OPEN
|
|
||||||
? 'success.dark'
|
|
||||||
: stage.status === STAGE_STATUSES.PRE_REGISTRATION
|
|
||||||
? 'info.dark'
|
|
||||||
: 'grey.300',
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: '0.8rem'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider sx={{ my: 3 }} />
|
{isRegistrationOpen && stage.registrationLink && (
|
||||||
|
|
||||||
<Typography variant="body1" paragraph>
|
|
||||||
{stage.description}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{stage.registrationLink && (
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="success"
|
color="success"
|
||||||
href={stage.registrationLink}
|
href={stage.registrationLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
startIcon={<DownloadIcon/>}
|
startIcon={<DownloadIcon/>}
|
||||||
sx={{ mt: 2 }}
|
sx={{mt: 3}}
|
||||||
>
|
>
|
||||||
Перейти на регистрацию
|
Зарегистрироваться на этап
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Divider sx={{ my: 4 }} />
|
{activeTab === 'classes' && (
|
||||||
<Typography variant="body2" color="text.secondary" align="center">
|
<Box>
|
||||||
© 2026 КартХолл. Все права защищены.
|
<Tabs
|
||||||
|
value={activeClassTab}
|
||||||
|
onChange={(e, newValue) => setActiveClassTab(newValue)}
|
||||||
|
variant="scrollable"
|
||||||
|
>
|
||||||
|
{Object.keys(participants).map((className, index) => (
|
||||||
|
<Tab key={className} label={className} value={index}/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{Object.entries(participants).map(
|
||||||
|
([className, classParticipants], index) =>
|
||||||
|
index === activeClassTab && (
|
||||||
|
<Box key={className} sx={{mt: 3}}>
|
||||||
|
<Typography variant="h6">{className}</Typography>
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>№</TableCell>
|
||||||
|
<TableCell>Участник</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{classParticipants.map((p) => (
|
||||||
|
<TableRow
|
||||||
|
key={p.id}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: p.isReserve ? 'grey.100' : 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell>{p.number}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{
|
||||||
|
<TableCell>
|
||||||
|
{p.name}
|
||||||
|
{p.isReserve && (
|
||||||
|
<Typography variant="caption" color="text.secondary"
|
||||||
|
sx={{ml: 1}}>
|
||||||
|
(резерв)
|
||||||
</Typography>
|
</Typography>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{/* Индикатор заполненности класса */}
|
||||||
|
<Box sx={{mt: 2, display: 'flex', alignItems: 'center', gap: 1}}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Зарегистрировано: {classParticipants.length} / {maxParticipantsPerClass}
|
||||||
|
</Typography>
|
||||||
|
{classParticipants.length >= maxParticipantsPerClass && (
|
||||||
|
<Chip
|
||||||
|
label="Набор закрыт"
|
||||||
|
color="warning"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Кнопка регистрации (только если открыт приём) */}
|
||||||
|
{isRegistrationOpen && classParticipants.length < maxParticipantsPerClass && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<DownloadIcon/>}
|
||||||
|
sx={{mt: 2}}
|
||||||
|
onClick={() => alert('Переход на форму регистрации')}
|
||||||
|
>
|
||||||
|
Зарегистрироваться в {className}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'events' && (
|
||||||
|
<Box>
|
||||||
|
<Tabs
|
||||||
|
value={activeClassTab}
|
||||||
|
onChange={(e, newValue) => setActiveClassTab(newValue)}
|
||||||
|
variant="scrollable"
|
||||||
|
>
|
||||||
|
<Tab label="Список участников" value={0}/>
|
||||||
|
{stage.hasGeneralQualification ? (
|
||||||
|
<Tab label="Общая квалификация" value={1}/>
|
||||||
|
) : (
|
||||||
|
Object.keys(participants).map((className, idx) => (
|
||||||
|
<Tab key={className} label={`Квалификация ${className}`} value={idx + 1}/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{/* Гонки (по количеству из настроек этапа) */}
|
||||||
|
{[...Array(stage.raceCount || 1)].map((_, raceIdx) => (
|
||||||
|
<Tab
|
||||||
|
key={raceIdx}
|
||||||
|
label={`Гонка ${raceIdx + 1}`}
|
||||||
|
value={stage.hasGeneralQualification ? raceIdx + 2 : raceIdx + 1 + Object.keys(participants).length}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Контент вкладок событий */}
|
||||||
|
{activeClassTab === 0 && (
|
||||||
|
<Box sx={{mt: 3}}>
|
||||||
|
<Typography variant="h6">Список участников</Typography>
|
||||||
|
{/* Здесь можно повторить таблицу участников из вкладки "Классы" */}
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>№</TableCell>
|
||||||
|
<TableCell>Участник</TableCell>
|
||||||
|
<TableCell>Класс</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{Object.entries(participants).flatMap(([className, classParticipants]) =>
|
||||||
|
classParticipants.map(p => (
|
||||||
|
<TableRow key={p.id}>
|
||||||
|
<TableCell>{p.number}</TableCell>
|
||||||
|
<TableCell>{p.name}</TableCell>
|
||||||
|
<TableCell>{className}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{stage.hasGeneralQualification && activeClassTab === 1 && (
|
||||||
|
<Box sx={{mt: 3}}>
|
||||||
|
<Typography variant="h6">Общая квалификация</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
Здесь отображаются результаты общей квалификации.
|
||||||
|
</Typography>
|
||||||
|
{/* Место для таблицы результатов */}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!stage.hasGeneralQualification &&
|
||||||
|
activeClassTab > 0 &&
|
||||||
|
activeClassTab <= Object.keys(participants).length && (
|
||||||
|
<Box sx={{mt: 3}}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Квалификация {Object.keys(participants)[activeClassTab - 1]}
|
||||||
|
</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
Результаты квалификации для выбранного класса.
|
||||||
|
</Typography>
|
||||||
|
{/* Место для таблицы результатов */}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeClassTab >= (stage.hasGeneralQualification ? 2 : Object.keys(participants).length + 1) && (
|
||||||
|
<Box sx={{mt: 3}}>
|
||||||
|
<Typography
|
||||||
|
variant="h6">Гонка {activeClassTab - (stage.hasGeneralQualification ? 1 : Object.keys(participants).length)}</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
Результаты гонки.
|
||||||
|
</Typography>
|
||||||
|
{/* Место для таблицы результатов */}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Действия для завершённого этапа */}
|
||||||
|
{isCompleted && (
|
||||||
|
<Box sx={{mt: 4, textAlign: 'right'}}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<PrintIcon/>}
|
||||||
|
onClick={() => window.print()}
|
||||||
|
>
|
||||||
|
Печать результатов
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StagePage;
|
export default StagePage;
|
||||||
|
|
||||||
|
|||||||
169
front/src/app/pages/users/UserPage.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// src/pages/UserPage.jsx
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
Divider,
|
||||||
|
Button,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
const UserPage = () => {
|
||||||
|
const { userId } = useParams();
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
|
// Пример данных пользователя (в реальном проекте — из API)
|
||||||
|
const user = {
|
||||||
|
id: userId,
|
||||||
|
name: 'Алексей Карпов',
|
||||||
|
avatar: '/images/user-avatar.jpg',
|
||||||
|
email: 'alex@kart-hall.ru',
|
||||||
|
phone: '+7 (999) 123-45-67',
|
||||||
|
joinDate: '2023-05-10',
|
||||||
|
totalRaces: 24,
|
||||||
|
bestTime: '01:23.450', // лучшее время на трассе
|
||||||
|
};
|
||||||
|
|
||||||
|
// История заездов
|
||||||
|
const races = [
|
||||||
|
{ id: 1, event: 'Кубок KartHall 2024', date: '2024-08-15', time: '01:25.120', place: 3 },
|
||||||
|
{ id: 2, event: 'Ночной заезд', date: '2024-09-01', time: '01:24.890', place: 1 },
|
||||||
|
// ...
|
||||||
|
];
|
||||||
|
|
||||||
|
// Активные бронирования
|
||||||
|
const bookings = [
|
||||||
|
{ id: 1, track: 'Картодром KartHall', date: '2024-10-10 18:00', status: 'подтверждено' },
|
||||||
|
{ id: 2, track: 'Виртуальная трасса "Формула У"', date: '2024-10-12 20:00', status: 'ожидает оплаты' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||||
|
{/* Шапка профиля */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 4 }}>
|
||||||
|
<Avatar src={user.avatar} sx={{ width: 80, height: 80 }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5">{user.name}</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Участник с {new Date(user.joinDate).toLocaleDateString('ru-RU')}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Вкладки */}
|
||||||
|
<Tabs value={activeTab} onChange={(e, newValue) => setActiveTab(newValue)} sx={{ mb: 3 }}>
|
||||||
|
<Tab label="Профиль" value={0} />
|
||||||
|
<Tab label="Мои заезды" value={1} />
|
||||||
|
<Tab label="Бронирования" value={2} />
|
||||||
|
<Tab label="Настройки" value={3} />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Контент вкладок */}
|
||||||
|
{activeTab === 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>Личная информация</Typography>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Email" secondary={user.email} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText primary="Телефон" secondary={user.phone} />
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary="Всего заездов"
|
||||||
|
secondary={user.totalRaces}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary="Лучшее время"
|
||||||
|
secondary={user.bestTime}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 1 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>История заездов</Typography>
|
||||||
|
<List>
|
||||||
|
{races.map((race) => (
|
||||||
|
<ListItem key={race.id} divider>
|
||||||
|
<ListItemText
|
||||||
|
primary={race.event}
|
||||||
|
secondary={`Дата: ${race.date} | Время: ${race.time} | Место: ${race.place}`}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 2 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>Мои бронирования</Typography>
|
||||||
|
<List>
|
||||||
|
{bookings.map((booking) => (
|
||||||
|
<ListItem key={booking.id} divider>
|
||||||
|
<ListItemText
|
||||||
|
primary={booking.track}
|
||||||
|
secondary={`Дата: ${booking.date} | Статус: ${booking.status}`}
|
||||||
|
/>
|
||||||
|
{booking.status === 'ожидает оплаты' && (
|
||||||
|
<Button size="small" variant="outlined" color="warning">
|
||||||
|
Оплатить
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 3 && (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>Настройки профиля</Typography>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<Button variant="outlined">Сменить пароль</Button>
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
<ListItem>
|
||||||
|
<Button variant="outlined">Настроить уведомления</Button>
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
<ListItem>
|
||||||
|
<Button variant="outlined" color="error">
|
||||||
|
Удалить аккаунт
|
||||||
|
</Button>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserPage;
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import Dialog from "@mui/material/Dialog";
|
|
||||||
import DialogTitle from "@mui/material/DialogTitle";
|
|
||||||
import * as React from "react";
|
|
||||||
import {useState} from "react";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
|
||||||
import DialogActions from "@mui/material/DialogActions";
|
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import TextField from "@mui/material/TextField";
|
|
||||||
|
|
||||||
export function BarCreateModal({open, setOpen, create, id}) {
|
|
||||||
const [value, setValue] = useState("");
|
|
||||||
return (
|
|
||||||
<Dialog fullWidth={true}
|
|
||||||
open={open} onClose={() => setOpen(false)}
|
|
||||||
sx={{
|
|
||||||
'& .MuiDialog-paper': {
|
|
||||||
margin: '8px',
|
|
||||||
},
|
|
||||||
'& .MuiPaper-root': {
|
|
||||||
width: 'calc(100% - 16px)',
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<DialogTitle>
|
|
||||||
<Typography>Создать список</Typography>
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<TextField sx={{width: '75%'}}
|
|
||||||
label={<Typography pt={'4px'}>
|
|
||||||
Название списка</Typography>} variant='outlined'
|
|
||||||
value={!value ? "" : value}
|
|
||||||
onChange={(e) => setValue(e.target.value)}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => {
|
|
||||||
create(id, value);
|
|
||||||
setValue("");
|
|
||||||
}}>Создать</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
82
front/src/components/calendar/Calendar.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import {DateCalendar} from "@mui/x-date-pickers/DateCalendar";
|
||||||
|
import {Paper} from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import Badge from "@mui/material/Badge";
|
||||||
|
import {PickersDay} from "@mui/x-date-pickers/PickersDay";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const DayRenderer = (params) => {
|
||||||
|
const {stagesByDate = [], day, outsideCurrentMonth, ...other} = params;
|
||||||
|
const dayKey = params.day.date() + "-" + params.day.month();
|
||||||
|
const events = stagesByDate[dayKey] || [];
|
||||||
|
const isStage = events.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
key={params.day.toString()}
|
||||||
|
overlap="circular"
|
||||||
|
badgeContent={isStage ? '🏆' : undefined}
|
||||||
|
sx={{color: 'var(--mui-palette-text-primary)'}}
|
||||||
|
>
|
||||||
|
<PickersDay {...other} outsideCurrentMonth={outsideCurrentMonth} day={day}
|
||||||
|
// disabled
|
||||||
|
sx={{pointerEvents: 'none'}}/>
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Calendar = ({viewDate, setViewDate, stagesByDate}) => {
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DateCalendar
|
||||||
|
defaultValue={dayjs()}
|
||||||
|
onMonthChange={(newValue) => {
|
||||||
|
if (newValue.month() !== viewDate.month) {
|
||||||
|
setViewDate(newValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
showDaysOutsideCurrentMonth
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
'.MuiDayCalendar-dayButton': {
|
||||||
|
minHeight: {xs: 80, sm: 100},
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
p: 1,
|
||||||
|
|
||||||
|
},
|
||||||
|
'.MuiPickersDay-root': {
|
||||||
|
fontSize: {xs: '1rem', sm: '1.1rem'},
|
||||||
|
|
||||||
|
},
|
||||||
|
'&.MuiPickersDay-root[data-selected="true"]': {
|
||||||
|
backgroundColor: 'primary.secondary',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'&.MuiPickersDay-root:has(.event-chip)': {
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
slots={{
|
||||||
|
day: DayRenderer
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
day: {
|
||||||
|
stagesByDate
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Calendar;
|
||||||
57
front/src/components/calendar/CalendarContent.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import {Box, useMediaQuery, useTheme} from "@mui/material";
|
||||||
|
import React, {useState} from "react";
|
||||||
|
import {CHAMPIONSHIP_STAGES} from "../../data/constants";
|
||||||
|
import Calendar from "./Calendar";
|
||||||
|
import MobileCalendarStageList from "./MobileCalendarStageList";
|
||||||
|
import DesktopCalendarStageList from "./DesktopCalendarStageList";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export const CalendarContent = ({selectedClass}) => {
|
||||||
|
const [viewDate, setViewDate] = useState(dayjs()); // Дата для отображения месяца
|
||||||
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
// Фильтрованные этапы для текущего месяца/класса
|
||||||
|
//todo: change from api
|
||||||
|
const filteredStages = CHAMPIONSHIP_STAGES.filter((stage) => {
|
||||||
|
const stageDate = new Date(stage.date);
|
||||||
|
const isSameMonth = stageDate.getMonth() === viewDate.month();
|
||||||
|
const isSameYear = stageDate.getFullYear() === viewDate.year();
|
||||||
|
const classMatch = selectedClass === 'Все' || stage.class === selectedClass;
|
||||||
|
return isSameMonth && isSameYear && classMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Группируем этапы по дате
|
||||||
|
const stagesByDate = filteredStages.reduce((acc, stage) => {
|
||||||
|
const days = dayjs(stage.date);
|
||||||
|
const dayKey = days.date() + "-" + days.month();
|
||||||
|
acc[dayKey] = acc[dayKey] || [];
|
||||||
|
acc[dayKey].push(stage);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Основной контент */}
|
||||||
|
<Box sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: {xs: '1fr', md: '2fr 1fr'},
|
||||||
|
gap: {xs: 2, md: 4},
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 1400,
|
||||||
|
mx: 'auto',
|
||||||
|
mt: 2,
|
||||||
|
}}>
|
||||||
|
{/* Календарь */}
|
||||||
|
<Calendar viewDate={viewDate} setViewDate={setViewDate} stagesByDate={stagesByDate}/>
|
||||||
|
|
||||||
|
{/* Список этапов (только на десктопе) */}
|
||||||
|
{!isMobile && (<DesktopCalendarStageList filteredStages={filteredStages} viewDate={viewDate}/>)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<MobileCalendarStageList isMobile={isMobile} openDrawer={openDrawer} setOpenDrawer={setOpenDrawer}
|
||||||
|
filteredStages={filteredStages} viewDate={viewDate}/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
front/src/components/calendar/CalendarDrawerContent.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import {Box, Chip, ListItemButton, Typography} from "@mui/material";
|
||||||
|
import List from "@mui/material/List";
|
||||||
|
import ListItem from "@mui/material/ListItem";
|
||||||
|
import React from "react";
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
|
|
||||||
|
const CalendarDrawerContent = ({filteredStages, viewDate}) => {
|
||||||
|
return (
|
||||||
|
<Box sx={{p: 3, minWidth: 280}}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Этапы на{' '}
|
||||||
|
{viewDate.format('MMMM YYYY')}
|
||||||
|
</Typography>
|
||||||
|
{/*todo: убрать этот блок, если не понадобиться кнопка для отображения пустых месяцев*/}
|
||||||
|
{filteredStages.length === 0 ? (
|
||||||
|
<Typography color="text.secondary" sx={{mt: 2}}>
|
||||||
|
Нет этапов в этом месяце
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<List sx={{mt: 2}}>
|
||||||
|
{filteredStages.map((stage) => (
|
||||||
|
<ListItem
|
||||||
|
key={stage.id}
|
||||||
|
sx={{
|
||||||
|
mb: 1,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
// '&:hover': {
|
||||||
|
// backgroundColor: 'action.selected',
|
||||||
|
// transform: 'translateY(-2px)',
|
||||||
|
// boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
|
||||||
|
// }
|
||||||
|
}} >
|
||||||
|
<ListItemButton component={Link} to={`/stages/${stage.id}`}
|
||||||
|
// style={{textDecoration: 'none'}}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
width: '100%',
|
||||||
|
px: 1,
|
||||||
|
py: 1.5,
|
||||||
|
borderRadius: 2,
|
||||||
|
'&:hover': {
|
||||||
|
// backgroundColor: 'action.selected',
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
|
||||||
|
}
|
||||||
|
// Убираем hover здесь — он уже на ListItem
|
||||||
|
// transform: 'translateY(-2px)',
|
||||||
|
}}
|
||||||
|
key={stage.id}>
|
||||||
|
<Box
|
||||||
|
width='100%'
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
px: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold" color={'text.primary'}>
|
||||||
|
{stage.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{stage.stage} · {stage.class}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{mt: 0.5}}>
|
||||||
|
<Chip
|
||||||
|
label={stage.status}
|
||||||
|
size="small"
|
||||||
|
color={
|
||||||
|
stage.status === 'Идёт'
|
||||||
|
? 'warning'
|
||||||
|
: stage.status === 'Регистрация открыта'
|
||||||
|
? 'success'
|
||||||
|
: 'info'
|
||||||
|
}
|
||||||
|
sx={{fontSize: '0.8rem'}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarDrawerContent;
|
||||||
51
front/src/components/calendar/CalendarHeader.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {Box, Typography} from "@mui/material";
|
||||||
|
import {CalendarToday as CalendarIcon} from "@mui/icons-material";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
|
||||||
|
const CalendarHeader = (selectedClass, setSelectedClass) => {
|
||||||
|
// const [showFilters, setShowFilters] = useState(false);
|
||||||
|
return (
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
mb: 4,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 2
|
||||||
|
}}>
|
||||||
|
<Box sx={{display: 'flex', alignItems: 'center', gap: 1}}>
|
||||||
|
<CalendarIcon color="primary"/>
|
||||||
|
<Typography variant="h4">Календарь чемпионатов</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Фильтры */}
|
||||||
|
{/*<Box sx={{display: 'flex', gap: 2, flexWrap: 'wrap'}}>*/}
|
||||||
|
{/* <Button*/}
|
||||||
|
{/* variant="outlined"*/}
|
||||||
|
{/* // startIcon={<FilterIcon/>}*/}
|
||||||
|
{/* onClick={() => setShowFilters(!showFilters)}*/}
|
||||||
|
{/* size="small"*/}
|
||||||
|
{/* >*/}
|
||||||
|
{/* Фильтры*/}
|
||||||
|
{/* </Button>*/}
|
||||||
|
{/* {showFilters && (*/}
|
||||||
|
{/* <FormControl size="small" sx={{minWidth: 120}}>*/}
|
||||||
|
{/* <InputLabel>Класс</InputLabel>*/}
|
||||||
|
{/* <Select value={selectedClass} defaultValue={'Все'} onChange={(e) => setSelectedClass(e.target.value)}>*/}
|
||||||
|
{/* {['Все', 'Юниоры', 'Взрослые', 'Богатыри', '35+', 'Pro', 'Amateur', 'Симулятор A', 'Симулятор B'].map(*/}
|
||||||
|
{/* (cls) => (*/}
|
||||||
|
{/* <MenuItem key={cls} value={cls}>*/}
|
||||||
|
{/* {cls}*/}
|
||||||
|
{/* </MenuItem>*/}
|
||||||
|
{/* )*/}
|
||||||
|
{/* )}*/}
|
||||||
|
{/* </Select>*/}
|
||||||
|
{/* </FormControl>*/}
|
||||||
|
{/* )}*/}
|
||||||
|
{/*</Box>*/}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarHeader;
|
||||||
106
front/src/components/calendar/DesktopCalendarStageList.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import {Box, Chip, Paper, Typography} from "@mui/material";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const DesktopCalendarStageList = ({filteredStages, viewDate}) => {
|
||||||
|
return (
|
||||||
|
<Box sx={{height: '100%', display: 'flex', flexDirection: 'column'}}>
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{p: 3}}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Этапы на{' '}
|
||||||
|
{viewDate.format('MMMM YYYY')}
|
||||||
|
</Typography>
|
||||||
|
{filteredStages.length === 0 ? (
|
||||||
|
<Typography color="text.secondary" sx={{my: 2}}>
|
||||||
|
Нет этапов в выбранном периоде
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
filteredStages.map((stage) => (
|
||||||
|
<Link
|
||||||
|
to={`/stages/${stage.id}`}
|
||||||
|
style={{textDecoration: 'none'}}
|
||||||
|
key={stage.id}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 2.5,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: 'action.hover',
|
||||||
|
mb: 2,
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'action.selected',
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="text.primary"
|
||||||
|
noWrap
|
||||||
|
sx={{mb: 0.5}}
|
||||||
|
>
|
||||||
|
{stage.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{mb: 1}}>
|
||||||
|
{stage.stage} · {stage.class}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{display: 'flex', gap: 1, flexWrap: 'wrap'}}>
|
||||||
|
<Chip
|
||||||
|
label={stage.status}
|
||||||
|
size="small"
|
||||||
|
color={
|
||||||
|
stage.status === 'Идёт'
|
||||||
|
? 'warning'
|
||||||
|
: stage.status === 'Регистрация открыта'
|
||||||
|
? 'success'
|
||||||
|
: 'info'
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
backgroundColor:
|
||||||
|
stage.status === 'Идёт'
|
||||||
|
? 'warning.dark'
|
||||||
|
: stage.status === 'Регистрация открыта'
|
||||||
|
? 'success.dark'
|
||||||
|
: 'info.dark',
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{mt: 1.5}}
|
||||||
|
>
|
||||||
|
{new Date(stage.date).toLocaleDateString('ru-RU', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
weekday: 'short',
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Link>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DesktopCalendarStageList;
|
||||||
|
|
||||||
43
front/src/components/calendar/MobileCalendarStageList.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {Box, Button, Drawer} from "@mui/material";
|
||||||
|
import {CalendarToday as CalendarIcon} from "@mui/icons-material";
|
||||||
|
import React from "react";
|
||||||
|
import CalendarDrawerContent from "./CalendarDrawerContent";
|
||||||
|
|
||||||
|
const MobileCalendarStageList = ({isMobile, openDrawer, setOpenDrawer, filteredStages, viewDate}) => {
|
||||||
|
if (!isMobile || filteredStages.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Drawer для мобильных */}
|
||||||
|
<Drawer
|
||||||
|
anchor="bottom"
|
||||||
|
open={openDrawer}
|
||||||
|
onClose={() => setOpenDrawer(false)}
|
||||||
|
PaperProps={{sx: {borderRadius: '16px 16px 0 0'}}}
|
||||||
|
>
|
||||||
|
<CalendarDrawerContent filteredStages={filteredStages} viewDate={viewDate}/>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
{/* Кнопка для открытия drawer на мобильных */}
|
||||||
|
<Box sx={{position: 'fixed', bottom: 80, right: 24}}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={() => setOpenDrawer(true)}
|
||||||
|
startIcon={<CalendarIcon/>}
|
||||||
|
sx={{
|
||||||
|
boxShadow: 3,
|
||||||
|
'&:hover': {boxShadow: 4},
|
||||||
|
borderRadius: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Посмотреть этапы
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MobileCalendarStageList;
|
||||||
26
front/src/components/championship/ChampionshipCard.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import {Card, Grid} from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import ChampionshipCardContent from "./ChampionshipCardContent";
|
||||||
|
import ChampionshipCardAction from "./ChampionshipCardAction";
|
||||||
|
|
||||||
|
const ChampionshipCard = ({champ}) => {
|
||||||
|
return (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={champ.id}>
|
||||||
|
<Card variant="outlined" sx={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between', // Растягивает содержимое, прижимает actions вниз
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.02)'
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<ChampionshipCardContent champ={champ}/>
|
||||||
|
<ChampionshipCardAction champId={champ.id}/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChampionshipCard;
|
||||||
35
front/src/components/championship/ChampionshipCardAction.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {Box, CardActions, IconButton} from "@mui/material";
|
||||||
|
import {Edit as EditIcon} from "@mui/icons-material";
|
||||||
|
import React from "react";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
|
||||||
|
const ChampionshipCardAction = ({champId}) => {
|
||||||
|
return (
|
||||||
|
<CardActions sx={{justifyContent: 'flex-start', p: 2}}>
|
||||||
|
{/* Действия */}
|
||||||
|
<Box sx={{display: 'flex', gap: 1}}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to={`/championships/${champId}`}
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Подробнее
|
||||||
|
</Button>
|
||||||
|
<IconButton
|
||||||
|
component={Link}
|
||||||
|
to={`/championships/${champId}/edit`}
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
title="Редактировать"
|
||||||
|
>
|
||||||
|
<EditIcon fontSize="small"/>
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</CardActions>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChampionshipCardAction;
|
||||||
62
front/src/components/championship/ChampionshipCardContent.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {Box, CardContent, Table, TableBody, TableCell, TableRow, Tooltip, Typography} from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const ChampionshipCardContent = ({champ}) => {
|
||||||
|
return (
|
||||||
|
<CardContent>
|
||||||
|
<Box
|
||||||
|
sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'start', mb: 2}}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6">
|
||||||
|
{champ.title}
|
||||||
|
</Typography>
|
||||||
|
<Tooltip title={champ.status}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: champ.status === 'Идёт' ? 'warning.main' :
|
||||||
|
champ.status === 'Регистрация открыта' ? 'success.main' : 'info.main',
|
||||||
|
color: 'white',
|
||||||
|
px: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{champ.status}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary">
|
||||||
|
{champ.season}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Таблица с основными данными */}
|
||||||
|
<Table size="small" sx={{mt: 2}}>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell variant="head">Этапы</TableCell>
|
||||||
|
<TableCell>{champ.stages}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell variant="head">Классы</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{champ.classes.join(', ')}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell variant="head">Начало</TableCell>
|
||||||
|
<TableCell>{champ.startDate}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell variant="head">Конец</TableCell>
|
||||||
|
<TableCell>{champ.endDate}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChampionshipCardContent;
|
||||||
25
front/src/components/championship/ChampionshipHeader.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {Box, Button, Typography} from "@mui/material";
|
||||||
|
import {Add as AddIcon} from "@mui/icons-material";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const ChampionshipHeader = () => {
|
||||||
|
return (
|
||||||
|
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4}}>
|
||||||
|
<Typography variant="h4">
|
||||||
|
Все чемпионаты
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size={'small'}
|
||||||
|
color="primary"
|
||||||
|
startIcon={<AddIcon/>}
|
||||||
|
href="/championships/create"
|
||||||
|
sx={{px: 3}}
|
||||||
|
>
|
||||||
|
Создать чемпионат
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChampionshipHeader;
|
||||||
32
front/src/components/championship/ChampionshipsList.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {Button, Grid, Paper, Typography} from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import ChampionshipCard from "./ChampionshipCard";
|
||||||
|
import {CHAMPIONSHIP_STAGES} from "../../data/constants";
|
||||||
|
|
||||||
|
const ChampionshipsList = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid container spacing={4}>
|
||||||
|
{CHAMPIONSHIP_STAGES.map((champ) => <ChampionshipCard champ={champ}/>)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Сообщение, если чемпионатов нет */}
|
||||||
|
{CHAMPIONSHIP_STAGES.length === 0 && (
|
||||||
|
<Paper sx={{textAlign: 'center', py: 8}}>
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
Пока нет ни одного чемпионата. Создайте первый!
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
sx={{mt: 2}}
|
||||||
|
href="/championships/create"
|
||||||
|
>
|
||||||
|
Создать чемпионат
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChampionshipsList;
|
||||||
14
front/src/components/core/Footer.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import {Divider, Typography} from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import {FOOTER_MESSAGE} from "../../data/constants";
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Divider sx={{my: 4}}/>
|
||||||
|
<Typography variant="body2" color="text.secondary" align="center" sx={{my:2}}>
|
||||||
|
{FOOTER_MESSAGE}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,49 +1,41 @@
|
|||||||
import {styled, useColorScheme} from "@mui/material/styles";
|
import {styled, useColorScheme} from "@mui/material/styles";
|
||||||
import Switch from "@mui/material/Switch";
|
import Switch from "@mui/material/Switch";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Box from "@mui/material/Box";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
import Typography from "@mui/material/Typography";
|
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||||
|
|
||||||
export function ThemeSwitch() {
|
export function ThemeSwitch() {
|
||||||
const {mode, setMode} = useColorScheme();
|
const {mode, setMode} = useColorScheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<>
|
||||||
sx={{
|
{/*Box*/}
|
||||||
alignItems: 'center',
|
{/*// sx={{*/}
|
||||||
// backgroundColor: 'var(--mui-palette-neutral-950)',
|
{/*// // alignItems: 'center',*/}
|
||||||
border: '1px solid var(--mui-palette-neutral-700)',
|
{/*// // backgroundColor: 'var(--mui-palette-neutral-950)',*/}
|
||||||
borderRadius: '12px',
|
{/*// // border: '1px solid var(--mui-palette-neutral-700)',*/}
|
||||||
cursor: 'pointer',
|
{/*// // borderRadius: '12px',*/}
|
||||||
display: 'flex',
|
{/*// // cursor: 'pointer',*/}
|
||||||
p: '4px 12px',
|
{/*// // display: 'flex',*/}
|
||||||
}}
|
{/*// // p: '4px 12px',*/}
|
||||||
>
|
{/*// }}*/}
|
||||||
|
{/*// >*/}
|
||||||
|
<ListItemIcon>
|
||||||
|
<LightModeIcon fontSize="small"/>
|
||||||
|
</ListItemIcon>
|
||||||
<StyledSwitch
|
<StyledSwitch
|
||||||
checked={mode === 'dark'}
|
checked={mode === 'dark'}
|
||||||
onChange={(e) => setMode(e.target.checked ? 'dark' : 'light')}
|
onChange={(e) => setMode(e.target.checked ? 'dark' : 'light')}
|
||||||
inputProps={{'aria-label': 'controlled'}}
|
inputProps={{'aria-label': 'controlled'}}
|
||||||
/>
|
/>
|
||||||
<Box sx={{flex: '1 1 auto'}}>
|
</>
|
||||||
<Typography color="inherit" variant="subtitle1">
|
|
||||||
{(mode === 'dark' ? "Темная " : "Светлая ") + "тема"}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledSwitch = styled(Switch)(({theme}) => ({
|
const StyledSwitch = styled(Switch)(({theme}) => ({
|
||||||
width: 62,
|
|
||||||
height: 34,
|
|
||||||
padding: 7,
|
|
||||||
'& .MuiSwitch-switchBase': {
|
'& .MuiSwitch-switchBase': {
|
||||||
margin: 1,
|
|
||||||
padding: 0,
|
|
||||||
transform: 'translateX(6px)',
|
|
||||||
'&.Mui-checked': {
|
'&.Mui-checked': {
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
transform: 'translateX(22px)',
|
|
||||||
'& .MuiSwitch-thumb:before': {
|
'& .MuiSwitch-thumb:before': {
|
||||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
||||||
'#fff',
|
'#fff',
|
||||||
@@ -51,24 +43,22 @@ const StyledSwitch = styled(Switch)(({theme}) => ({
|
|||||||
},
|
},
|
||||||
'& + .MuiSwitch-track': {
|
'& + .MuiSwitch-track': {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
backgroundColor: theme.palette.mode === 'dark' ? '#c5bebe' : '#aab4be',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'& .MuiSwitch-thumb': {
|
'& .MuiSwitch-thumb': {
|
||||||
backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
|
backgroundColor: theme.palette.mode === 'dark' ? '#8f8f8f' : '#424242',
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
'&::before': {
|
'&::before': {
|
||||||
content: "''",
|
content: "''",
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
width: '100%',
|
width: '95%',
|
||||||
height: '100%',
|
height: '95%',
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: 'no-repeat',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 18 18"><path fill="${encodeURIComponent(
|
||||||
'#fff',
|
'#fff',
|
||||||
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
|
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
|
||||||
},
|
},
|
||||||
|
|||||||
24
front/src/components/core/UpButton.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import {Fab} from "@mui/material";
|
||||||
|
import UpIcon from '@mui/icons-material';
|
||||||
|
import {blue} from "@mui/material/colors";
|
||||||
|
|
||||||
|
const UpButton = () => {
|
||||||
|
return (
|
||||||
|
<Fab sx={{
|
||||||
|
alpha: '30%',
|
||||||
|
position: 'sticky',
|
||||||
|
left: 'calc(100% - 16px)',
|
||||||
|
bottom: '16px',
|
||||||
|
color: 'common.white',
|
||||||
|
bgcolor: blue[600],
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: blue[600],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => window.window.scrollTo(0, 0)}
|
||||||
|
aria-label='Expand'
|
||||||
|
color='inherit'>
|
||||||
|
<UpIcon/>
|
||||||
|
</Fab>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import * as React from 'react';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import {User as UserIcon} from '@phosphor-icons/react/dist/ssr/User';
|
||||||
|
import {Link} from 'react-router-dom';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import MenuList from '@mui/material/MenuList';
|
import MenuList from '@mui/material/MenuList';
|
||||||
import Popover from '@mui/material/Popover';
|
import Popover from '@mui/material/Popover';
|
||||||
@@ -14,6 +16,8 @@ import {authClient} from "../../lib/clients/AuthClient";
|
|||||||
import {useLocation} from "react-router-dom";
|
import {useLocation} from "react-router-dom";
|
||||||
import {useUser} from "../../hooks/useUser";
|
import {useUser} from "../../hooks/useUser";
|
||||||
import {paths} from "../../path";
|
import {paths} from "../../path";
|
||||||
|
import {ThemeSwitch} from "./ThemeSwitch";
|
||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
|
||||||
export function UserPopover({anchorEl, onClose, open}) {
|
export function UserPopover({anchorEl, onClose, open}) {
|
||||||
const {checkSession} = useAuth();
|
const {checkSession} = useAuth();
|
||||||
@@ -55,18 +59,19 @@ export function UserPopover({anchorEl, onClose, open}) {
|
|||||||
</Box>
|
</Box>
|
||||||
<Divider/>
|
<Divider/>
|
||||||
<MenuList disablePadding sx={{p: '8px', '& .MuiMenuItem-root': {borderRadius: 1}}}>
|
<MenuList disablePadding sx={{p: '8px', '& .MuiMenuItem-root': {borderRadius: 1}}}>
|
||||||
{/*<MenuItem component={'a'} href={paths.dashboard.settings} onClick={onClose}>*/}
|
{/*<MenuItem component={Link} href={paths.home} onClick={onClose}>*/}
|
||||||
{/* <ListItemIcon>*/}
|
{/* <ListItemIcon>*/}
|
||||||
{/* <GearSixIcon fontSize="var(--icon-fontSize-md)"/>*/}
|
{/* <SignInIcon fontSize="var(--icon-fontSize-md)"/>*/}
|
||||||
{/* </ListItemIcon>*/}
|
{/* </ListItemIcon>*/}
|
||||||
{/* Настройки*/}
|
{/* Настройки*/}
|
||||||
{/*</MenuItem>*/}
|
{/*</MenuItem>*/}
|
||||||
{/*<MenuItem component={'a'} href={paths.dashboard.account} onClick={onClose}>*/}
|
|
||||||
{/* <ListItemIcon>*/}
|
<MenuItem component={Link} to={`/users/${user.id}`} onClick={onClose}>
|
||||||
{/* <UserIcon fontSize="var(--icon-fontSize-md)"/>*/}
|
<ListItemIcon>
|
||||||
{/* </ListItemIcon>*/}
|
<UserIcon fontSize="var(--icon-fontSize-md)"/>
|
||||||
{/* Профиль*/}
|
</ListItemIcon>
|
||||||
{/*</MenuItem>*/}
|
Профиль
|
||||||
|
</MenuItem>
|
||||||
{!user.name ? <MenuItem onClick={() => window.location.replace(paths.auth.signIn)}>
|
{!user.name ? <MenuItem onClick={() => window.location.replace(paths.auth.signIn)}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SignInIcon fontSize="var(--icon-fontSize-md)"/>
|
<SignInIcon fontSize="var(--icon-fontSize-md)"/>
|
||||||
@@ -80,6 +85,9 @@ export function UserPopover({anchorEl, onClose, open}) {
|
|||||||
Выход
|
Выход
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
}
|
}
|
||||||
|
<MenuItem sx={{'--mui-palette-action-hover': 'rgba(0, 0, 0, 0)'}}>
|
||||||
|
<ThemeSwitch/>
|
||||||
|
</MenuItem>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,28 +5,30 @@ import {User as UserIcon} from '@phosphor-icons/react/dist/ssr/User';
|
|||||||
import {Users as UsersIcon} from '@phosphor-icons/react/dist/ssr/Users';
|
import {Users as UsersIcon} from '@phosphor-icons/react/dist/ssr/Users';
|
||||||
import {XSquare} from '@phosphor-icons/react/dist/ssr/XSquare';
|
import {XSquare} from '@phosphor-icons/react/dist/ssr/XSquare';
|
||||||
import {
|
import {
|
||||||
Basket,
|
Buildings,
|
||||||
BookOpen,
|
Calculator,
|
||||||
Books,
|
Calendar,
|
||||||
|
CardsThree,
|
||||||
|
SteeringWheel,
|
||||||
Cheers,
|
Cheers,
|
||||||
CoffeeBean,
|
CoffeeBean,
|
||||||
|
Car,
|
||||||
Coins,
|
Coins,
|
||||||
Martini,
|
House,
|
||||||
Storefront,
|
Trophy,
|
||||||
Users,
|
Users
|
||||||
Wallet,
|
|
||||||
Calculator
|
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
|
|
||||||
export const navIcons = {
|
export const navIcons = {
|
||||||
'menu': BookOpen,
|
'home': House,
|
||||||
'list': Books,
|
'championships': Trophy,
|
||||||
'storefront': Storefront,
|
'calendar': Calendar,
|
||||||
'wallet': Wallet,
|
'stages': SteeringWheel,
|
||||||
'cocktail': Martini,
|
'places': Buildings,
|
||||||
|
'rent': Car,
|
||||||
|
|
||||||
'visitors': Users,
|
'visitors': Users,
|
||||||
'orders': Cheers,
|
'orders': Cheers,
|
||||||
'basket': Basket,
|
|
||||||
'coins': Coins,
|
'coins': Coins,
|
||||||
'ingredients': CoffeeBean,
|
'ingredients': CoffeeBean,
|
||||||
'chart-pie': ChartPieIcon,
|
'chart-pie': ChartPieIcon,
|
||||||
|
|||||||
19
front/src/components/home/AnnounceBlock.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import * as React from "react";
|
||||||
|
import AnnounceCard from "./AnnounceCard";
|
||||||
|
import {MOCK_STAGES} from "../../data/constants";
|
||||||
|
|
||||||
|
const AnnounceBlock = () => {
|
||||||
|
const stages = MOCK_STAGES.slice(0, 3);
|
||||||
|
return (
|
||||||
|
<Grid container spacing={4} sx={{mb: 6}}>
|
||||||
|
{stages.map((champ, index) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||||
|
<AnnounceCard champ={champ}/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnnounceBlock;
|
||||||
46
front/src/components/home/AnnounceCard.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {Card, CardActions, CardContent} from "@mui/material";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
import * as React from "react";
|
||||||
|
import {Trophy} from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
|
||||||
|
const AnnounceCard = ({champ}) => {
|
||||||
|
return (
|
||||||
|
<Card variant="outlined" sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between', // Растягивает содержимое, прижимает actions вниз
|
||||||
|
height: '100%',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
'&:hover': {transform: 'scale(1.02)'}
|
||||||
|
}}>
|
||||||
|
<CardContent sx={{p:2, pb: 1}}>
|
||||||
|
<Box sx={{display: 'flex', alignItems: 'center', mb: 2}}>
|
||||||
|
<Trophy/>
|
||||||
|
<Typography variant="h6" sx={{ml: 1}}>
|
||||||
|
{champ.title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography color="text.secondary" sx={{mb: 1}}>
|
||||||
|
{champ.stage.name} — {champ.date}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
<CardActions sx={{ justifyContent: 'flex-start', px: 2, pb: 2 }}>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
to={`/stages/${champ.stage.id}`}
|
||||||
|
sx={{mt: 2}}
|
||||||
|
>
|
||||||
|
Подробнее
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnnounceCard;
|
||||||
34
front/src/components/home/CountdownTimer.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
|
||||||
|
// Компонент таймера (упрощённый)
|
||||||
|
const CountdownTimer = ({targetDate}) => {
|
||||||
|
const [timeLeft, setTimeLeft] = React.useState({});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const updateTimer = () => {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const distance = new Date(targetDate).getTime() - now;
|
||||||
|
|
||||||
|
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||||
|
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
|
||||||
|
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
|
setTimeLeft({days, hours, minutes, seconds});
|
||||||
|
};
|
||||||
|
|
||||||
|
const interval = setInterval(updateTimer, 1000);
|
||||||
|
updateTimer();
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [targetDate]);
|
||||||
|
return (
|
||||||
|
<Typography variant="h6" color="error" sx={{fontWeight: 'bold'}}>
|
||||||
|
До старта: {timeLeft.days}д {timeLeft.hours}ч {timeLeft.minutes}м {timeLeft.seconds}с
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CountdownTimer;
|
||||||
44
front/src/components/home/LastResult.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
import {paths} from "../../path";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const LastResult = () => {
|
||||||
|
// Результаты последнего этапа
|
||||||
|
const results = {
|
||||||
|
Юниоры: ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
|
||||||
|
Взрослые: ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
|
||||||
|
Богатыри: ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
|
||||||
|
'35+': ['Пупкин Петя', 'Иванов Ваня', 'Кот Кирилл'],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{mb: 6}}>
|
||||||
|
<Typography variant="h5" sx={{mb: 3}}>
|
||||||
|
Итоги 1‑го этапа SWC (18.01.2026)
|
||||||
|
</Typography>
|
||||||
|
{Object.entries(results).map(([category, winners]) => (
|
||||||
|
<Box key={category} sx={{mb: 2}}>
|
||||||
|
<Typography variant="subtitle1" color="primary">
|
||||||
|
{category}:
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
1 место: {winners[0]}, 2 место: {winners[1]}, 3 место: {winners[2]}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
variant="outlined"
|
||||||
|
to={paths.stg.stages}
|
||||||
|
sx={{mt: 3}}
|
||||||
|
>
|
||||||
|
Все результаты
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LastResult;
|
||||||
22
front/src/components/home/NextStageTimer.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import * as React from "react";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import CountdownTimer from "./CountdownTimer";
|
||||||
|
|
||||||
|
const NextStageTimer = () => {
|
||||||
|
const title = 'SWC Зимний чемпионат 2025–2026';
|
||||||
|
return (
|
||||||
|
<Box sx={{textAlign: 'center', mb: 6}}>
|
||||||
|
<Typography variant="h5" sx={{mb: 2}}>
|
||||||
|
Следующий этап "{title}":
|
||||||
|
</Typography>
|
||||||
|
<CountdownTimer targetDate="2026-02-08T09:00:00"/>
|
||||||
|
<Button variant="contained" color="secondary" sx={{mt:2}}>
|
||||||
|
Регистрация
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NextStageTimer;
|
||||||
20
front/src/components/home/QuickActions.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
|
||||||
|
const QuickActions = () => {
|
||||||
|
return (
|
||||||
|
<Box sx={{display: 'flex', flexWrap: 'wrap', gap: 2, mb: 6}}>
|
||||||
|
<Button variant="contained" color="secondary" href="/register">
|
||||||
|
Зарегистрироваться на этап
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" href="/calendar">
|
||||||
|
Посмотреть календарь всех этапов
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" href="/rules">
|
||||||
|
Правила гонок
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QuickActions;
|
||||||
23
front/src/components/home/RentBlock.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import * as React from "react";
|
||||||
|
import {RENT_BUTTON, RENT_MESSAGE, RENT_QUEST} from "../../data/constants";
|
||||||
|
|
||||||
|
const RentBlock = () => {
|
||||||
|
return (
|
||||||
|
<Box sx={{textAlign: 'center', my: 6}}>
|
||||||
|
<Typography variant="h5" sx={{mb: 2}}>
|
||||||
|
{RENT_QUEST}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary" sx={{mb: 3}}>
|
||||||
|
{RENT_MESSAGE}
|
||||||
|
</Typography>
|
||||||
|
<Button variant="contained" color="success" href="/rent">
|
||||||
|
{RENT_BUTTON}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RentBlock;
|
||||||
15
front/src/components/home/SponsorsBlock.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import SponsorsCarousel from "./SponsorsCarousel";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const SponsorsBlock = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SponsorsCarousel/>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{mb: 4}}>
|
||||||
|
Наши партнёры: поддержка чемпионатов и призов
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default SponsorsBlock;
|
||||||
48
front/src/components/home/SponsorsCarousel.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import {Box, Avatar} from '@mui/material';
|
||||||
|
import Carousel from "react-material-ui-carousel";
|
||||||
|
import {sponsors} from "../../data/constants";
|
||||||
|
|
||||||
|
const SponsorsCarousel = () => {
|
||||||
|
// Дублируем логотипы для бесшовной прокрутки
|
||||||
|
const items = [...sponsors];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{mb: 4, overflow: 'hidden'}}>
|
||||||
|
<Carousel
|
||||||
|
autoPlay
|
||||||
|
interval={3000} // смена каждые 3 сек
|
||||||
|
animation="slide" // плавный слайд
|
||||||
|
duration={800} // длительность анимации (мс)
|
||||||
|
stopOnHover={true} // не останавливать при наведении
|
||||||
|
indicators={false}
|
||||||
|
cycleNavigation // зациклить
|
||||||
|
infiniteLoop // бесконечная прокрутка
|
||||||
|
swipe // отключить свайп (если не нужен)
|
||||||
|
sx={{
|
||||||
|
'& .MuiBox-root': {p: 0, display: 'flex', alignItems: 'center'},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Генерируем слайды: каждый слайд — строка из 3–4 логотипов */}
|
||||||
|
{Array.from({length: Math.ceil(items.length / 3)}).map((_, slideIdx) => (
|
||||||
|
<Box key={slideIdx} sx={{display: 'flex', justifyContent: 'space-around', px: 2}}>
|
||||||
|
{items.slice(slideIdx * 3, slideIdx * 3 + 3).map((sponsor, idx) => (
|
||||||
|
<Avatar
|
||||||
|
key={idx}
|
||||||
|
src={sponsor.logo}
|
||||||
|
alt={sponsor.name}
|
||||||
|
sx={{
|
||||||
|
width: 300,
|
||||||
|
height: 150,
|
||||||
|
borderRadius: '6px',
|
||||||
|
mx: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SponsorsCarousel;
|
||||||
18
front/src/components/home/WelcomeBlock.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import * as React from "react";
|
||||||
|
import {WELCOME_MESSAGE_PRIME, WELCOME_MESSAGE_SECOND} from "../../data/constants";
|
||||||
|
|
||||||
|
const WelcomeBlock = () => {
|
||||||
|
return(
|
||||||
|
<>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
{WELCOME_MESSAGE_PRIME}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="text.secondary" gutterBottom sx={{mb: 4}}>
|
||||||
|
{WELCOME_MESSAGE_SECOND}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WelcomeBlock
|
||||||
@@ -27,6 +27,12 @@ export function MainNav() {
|
|||||||
<Box
|
<Box
|
||||||
component="header"
|
component="header"
|
||||||
sx={{
|
sx={{
|
||||||
|
'--MobileNav-background': 'var(--mui-palette-neutral-950)',
|
||||||
|
'--NavItem-color': 'var(--mui-palette-text-secondary)',
|
||||||
|
'--NavItem-active-color': 'var(--mui-palette-text-primary)',
|
||||||
|
'--NavItem-disabled-color': 'var(--mui-palette-neutral-200)',
|
||||||
|
'--NavItem-icon-color': 'var(--mui-palette-text-secondary)',
|
||||||
|
'--NavItem-icon-active-color': 'var(--mui-palette-text-primary)',
|
||||||
borderBottom: '1px solid var(--mui-palette-divider)',
|
borderBottom: '1px solid var(--mui-palette-divider)',
|
||||||
backgroundColor: 'var(--mui-palette-background-paper)',
|
backgroundColor: 'var(--mui-palette-background-paper)',
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
@@ -38,10 +44,10 @@ export function MainNav() {
|
|||||||
<Stack direction="row" spacing={3}
|
<Stack direction="row" spacing={3}
|
||||||
sx={{alignItems: 'center', justifyContent: 'space-between', minHeight: '64px', px: 2}}>
|
sx={{alignItems: 'center', justifyContent: 'space-between', minHeight: '64px', px: 2}}>
|
||||||
<Stack sx={{alignItems: 'center'}} direction="row" spacing={3}>
|
<Stack sx={{alignItems: 'center'}} direction="row" spacing={3}>
|
||||||
<IconButton onClick={() => setOpenNav(true)} sx={{display: {sm: 'none'}}}>
|
<IconButton onClick={() => setOpenNav(true)} sx={{display: {md: 'none'}}}>
|
||||||
<ListIcon/>
|
<ListIcon/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Box component="nav" sx={{flex: '1 1 auto', p: 1}}>
|
<Box component="nav" sx={{flex: '1 1 auto', p: 1, display: {xs: 'none', md: 'flex'}}}>
|
||||||
{renderNavItems({items: navItems, pathname: pathname, direction: 'row'})}
|
{renderNavItems({items: navItems, pathname: pathname, direction: 'row'})}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {Link} from "react-router-dom";
|
|||||||
|
|
||||||
export function renderNavItems({items = [], pathname, direction}) {
|
export function renderNavItems({items = [], pathname, direction}) {
|
||||||
const children = items.reduce((acc, curr) => {
|
const children = items.reduce((acc, curr) => {
|
||||||
const {key, ...item} = curr;
|
acc.push(<NavItem key={curr.name} pathname={pathname} {...curr} />);
|
||||||
acc.push(<NavItem key={key} pathname={pathname} {...item} />);
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -19,9 +18,9 @@ export function renderNavItems({items = [], pathname, direction}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavItem({disabled, external, href, icon, matcher, pathname, title}) {
|
function NavItem({disabled, name, external, href, matcher, pathname, title}) {
|
||||||
const active = isNavItemActive({disabled, external, href, matcher, pathname});
|
const active = isNavItemActive({disabled, external, href, matcher, pathname});
|
||||||
const Icon = icon ? navIcons[icon] : null;
|
const Icon = name ? navIcons[name] ? navIcons[name] : null : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
@@ -35,7 +34,7 @@ function NavItem({disabled, external, href, icon, matcher, pathname, title}) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flex: '0 0 auto',
|
flex: '0 0 auto',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
p: '6px 16px',
|
p: 0.5,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ export function NavigationMenu() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*верхняя стопка*/}
|
{/*верхняя стопка*/}
|
||||||
<Stack spacing={2} sx={{p: 2, height: '63px'}}>
|
|
||||||
<ThemeSwitch/>
|
|
||||||
</Stack>
|
|
||||||
<Divider sx={{borderColor: 'var(--mui-palette-neutral-700)'}}/>
|
<Divider sx={{borderColor: 'var(--mui-palette-neutral-700)'}}/>
|
||||||
{/*меню навигации*/}
|
{/*меню навигации*/}
|
||||||
{items}
|
{items}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import CalendarPage from "../../app/pages/calendar/CalendarPage";
|
|||||||
import StagesPage from "../../app/pages/stages/StagesPage";
|
import StagesPage from "../../app/pages/stages/StagesPage";
|
||||||
import ChampionshipPage from "../../app/pages/championship/ChampionshipPage";
|
import ChampionshipPage from "../../app/pages/championship/ChampionshipPage";
|
||||||
import StagePage from "../../app/pages/stages/StagePage";
|
import StagePage from "../../app/pages/stages/StagePage";
|
||||||
|
import PlacesPage from "../../app/pages/places/PlacesPage";
|
||||||
|
import PlacePage from "../../app/pages/places/PlacePage";
|
||||||
|
import UserPage from "../../app/pages/users/UserPage";
|
||||||
|
|
||||||
export function NavigationRoutes() {
|
export function NavigationRoutes() {
|
||||||
const {auth} = useAuth();
|
const {auth} = useAuth();
|
||||||
@@ -85,6 +88,21 @@ const authPages = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
children: (<StagePage/>)
|
children: (<StagePage/>)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: paths.places.places,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<PlacesPage/>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.places.place,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<PlacePage/>)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: paths.users.user,
|
||||||
|
isPrivate: true,
|
||||||
|
children: (<UserPage/>)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: paths.notFound,
|
path: paths.notFound,
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
|
|||||||
72
front/src/components/stages/StagesHeader.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import {Box, Chip, Paper, Typography} from "@mui/material";
|
||||||
|
import {STAGE_STATUSES} from "../../data/constants";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const StagesHeader = ({stage}) => {
|
||||||
|
return(
|
||||||
|
<Paper sx={{p: 4, mb: 4}} variant="outlined">
|
||||||
|
<Typography variant="h4" gutterBottom>{stage.title}</Typography>
|
||||||
|
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||||
|
{stage.stage.name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 3, mt: 2}}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary">Дата</Typography>
|
||||||
|
<Typography>
|
||||||
|
{new Date(stage.date).toLocaleDateString('ru-RU', {
|
||||||
|
weekday: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary">Место</Typography>
|
||||||
|
<Typography>{stage.location}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary">Класс</Typography>
|
||||||
|
<Typography>{stage.class}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" color="text.secondary">Статус</Typography>
|
||||||
|
<Chip
|
||||||
|
label={stage.status}
|
||||||
|
color={
|
||||||
|
stage.status === STAGE_STATUSES.GOING
|
||||||
|
? 'warning'
|
||||||
|
: stage.status === STAGE_STATUSES.REGISTRATION_OPEN
|
||||||
|
? 'success'
|
||||||
|
: stage.status === STAGE_STATUSES.PRE_REGISTRATION
|
||||||
|
? 'info'
|
||||||
|
: 'default'
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
backgroundColor:
|
||||||
|
stage.status === STAGE_STATUSES.GOING
|
||||||
|
? 'warning.dark'
|
||||||
|
: stage.status === STAGE_STATUSES.REGISTRATION_OPEN
|
||||||
|
? 'success.dark'
|
||||||
|
: stage.status === STAGE_STATUSES.PRE_REGISTRATION
|
||||||
|
? 'info.dark'
|
||||||
|
: 'grey.300',
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '0.8rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{stage.description && (
|
||||||
|
<Typography variant="body1" paragraph sx={{mt: 3}}>
|
||||||
|
{stage.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StagesHeader;
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
// src/data/constants.js
|
// src/data/constants.js
|
||||||
|
|
||||||
|
import {Trophy} from "@phosphor-icons/react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Константы для приложения «КартХолл»
|
* Константы для приложения «КартХолл»
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Классы участников
|
// export const FOOTER_MESSAGE = '© 2026 КартХолл. Все права защищены.';
|
||||||
export const CLASSES = [
|
export const FOOTER_MESSAGE = '2026 Race Invoice';
|
||||||
'Юниоры',
|
// export const WELCOME_MESSAGE_PRIME = "Добро пожаловать на платформу «КартХолл»!"
|
||||||
'Взрослые',
|
export const WELCOME_MESSAGE_PRIME = "Добро пожаловать на платформу «Race Invoice»!"
|
||||||
'Pro',
|
export const WELCOME_MESSAGE_SECOND = "Ваш гид по гоночным чемпионатам, этапам и онлайн‑соревнованиям."
|
||||||
'Amateur',
|
|
||||||
'Симулятор A',
|
|
||||||
'Симулятор B'
|
|
||||||
];
|
|
||||||
|
|
||||||
|
export const RENT_QUEST = "Не гоняешь? Попробуй прокат!";
|
||||||
|
// export const RENT_MESSAGE = "Ощутите адреналин за рулём прокатного карта. Доступно ежедневно с 10:00 до 22:00."
|
||||||
|
export const RENT_MESSAGE = "Ощутите адреналин за рулём прокатного карта"
|
||||||
|
export const RENT_BUTTON = "Забронировать!"
|
||||||
|
|
||||||
|
// Классы участников
|
||||||
// Статусы этапов
|
// Статусы этапов
|
||||||
export const STAGE_STATUSES = {
|
export const STAGE_STATUSES = {
|
||||||
GOING: 'Идёт',
|
GOING: 'Идёт',
|
||||||
@@ -26,19 +31,28 @@ export const STAGE_STATUSES = {
|
|||||||
export const MOCK_STAGES = [
|
export const MOCK_STAGES = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'SWC Зимний чемпионат',
|
title: 'SWC Зимний чемпионат 2025–2026',
|
||||||
stage: '2‑й этап',
|
stage: {
|
||||||
|
name: '2‑й этап',
|
||||||
|
id: 1
|
||||||
|
},
|
||||||
date: '2026-02-08',
|
date: '2026-02-08',
|
||||||
class: 'Юниоры',
|
class: 'Юниоры',
|
||||||
status: STAGE_STATUSES.GOING,
|
status: STAGE_STATUSES.GOING,
|
||||||
description: 'Главный зимний турнир года. Призовой фонд — 500 000 ₽.',
|
description: 'Главный зимний турнир года. Призовой фонд — 500 000 ₽.',
|
||||||
location: 'Москва, стадион "Лужники"',
|
location: 'Москва, стадион "Лужники"',
|
||||||
registrationLink: 'https://example.com/register/1'
|
registrationLink: 'https://example.com/register/1',
|
||||||
|
|
||||||
|
stageId: 1,
|
||||||
|
icon: <Trophy fontSize="large"/>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: 'Honda Winter Cup',
|
title: 'Honda Winter Cup',
|
||||||
stage: '1‑й этап',
|
stage: {
|
||||||
|
name: '1‑й этап',
|
||||||
|
id: 2
|
||||||
|
},
|
||||||
date: '2026-01-31',
|
date: '2026-01-31',
|
||||||
class: 'Pro',
|
class: 'Pro',
|
||||||
status: STAGE_STATUSES.REGISTRATION_OPEN,
|
status: STAGE_STATUSES.REGISTRATION_OPEN,
|
||||||
@@ -49,7 +63,10 @@ export const MOCK_STAGES = [
|
|||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: 'Кубок Покровска (онлайн)',
|
title: 'Кубок Покровска (онлайн)',
|
||||||
stage: '1‑й этап',
|
stage: {
|
||||||
|
name: '1‑й этап',
|
||||||
|
id: 3
|
||||||
|
},
|
||||||
date: '2026-02-01',
|
date: '2026-02-01',
|
||||||
class: 'Симулятор A',
|
class: 'Симулятор A',
|
||||||
status: STAGE_STATUSES.PRE_REGISTRATION,
|
status: STAGE_STATUSES.PRE_REGISTRATION,
|
||||||
@@ -114,6 +131,59 @@ export const MOCK_STAGES = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const CHAMPIONSHIP_STAGES = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'SWS Зимний чемпионат 2025–2026',
|
||||||
|
stage: '2‑й этап',
|
||||||
|
date: '2026-02-08',
|
||||||
|
class: 'Юниоры',
|
||||||
|
status: 'Идёт',
|
||||||
|
season: 'Зима 2025–2026',
|
||||||
|
stages: 5,
|
||||||
|
classes: ['Юниоры', 'Взрослые', 'Богатыри', '35+'],
|
||||||
|
startDate: '18.01.2026',
|
||||||
|
endDate: '08.03.2026',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Honda Winter Cup 2026',
|
||||||
|
stage: '1‑й этап',
|
||||||
|
date: '2026-01-31',
|
||||||
|
class: 'Pro',
|
||||||
|
status: 'Регистрация открыта',
|
||||||
|
season: 'Зима 2026',
|
||||||
|
stages: 3,
|
||||||
|
classes: ['Pro', 'Amateur'],
|
||||||
|
startDate: '31.01.2026',
|
||||||
|
endDate: '28.02.2026',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Кубок Покровска 2026 (онлайн)',
|
||||||
|
stage: '1‑й этап',
|
||||||
|
date: '2026-02-01',
|
||||||
|
class: 'Симулятор A',
|
||||||
|
season: '2026',
|
||||||
|
stages: 4,
|
||||||
|
status: 'Предрегистрация',
|
||||||
|
classes: ['Симулятор A', 'Симулятор B'],
|
||||||
|
startDate: '01.02.2026',
|
||||||
|
endDate: '25.03.2026',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Спонсоры (макеты URL)
|
||||||
|
export const sponsors = [
|
||||||
|
{name: 'Минеральная вода Ульянка', logo: '/assets/sponsors/ulyanka.svg'},
|
||||||
|
{name: 'Канистра', logo: '/assets/sponsors/kanistra.png'},
|
||||||
|
{name: 'NewDiffer', logo: '/assets/sponsors/newDiffer.svg'},
|
||||||
|
{name: 'WoodGrand', logo: '/assets/sponsors/woodGrand.svg'},
|
||||||
|
{name: 'Сервис Газ', logo: '/assets/sponsors/sgaz.png'},
|
||||||
|
{name: 'Саратов Союз Лифт Монтаж', logo: '/assets/sponsors/sslm.png'},
|
||||||
|
{name: 'Риострой', logo: '/assets/sponsors/riostroy.svg'},
|
||||||
|
];
|
||||||
|
|
||||||
// Дополнительные константы (если понадобятся позже)
|
// Дополнительные константы (если понадобятся позже)
|
||||||
export const DEFAULT_PAGINATION = {
|
export const DEFAULT_PAGINATION = {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -124,3 +194,31 @@ export const SORT_ORDERS = {
|
|||||||
ASC: 'asc',
|
ASC: 'asc',
|
||||||
DESC: 'desc'
|
DESC: 'desc'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Пример данных площадок (в реальном проекте — из API)
|
||||||
|
export const places = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Картодром KartHall',
|
||||||
|
images: [
|
||||||
|
'https://media73.ru/upload/iblock/542/5426aad66514849efe1febf0c36126bf.jpg',
|
||||||
|
'https://avatars.mds.yandex.net/i?id=1b63c0406714628f94080ff0a2bc0c58_l-5334917-images-thumbs&n=13'
|
||||||
|
],
|
||||||
|
address: 'г. Ульяновск, ул. Спортивная, д. 5',
|
||||||
|
type: 'Реальная трасса',
|
||||||
|
capacity: 20,
|
||||||
|
contact: '+7 (900) 123-45-67',
|
||||||
|
bookingNumber: 'Б-001',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Виртуальная трасса «Формула У»',
|
||||||
|
images: [
|
||||||
|
'/images/virtual-track.jpg'
|
||||||
|
],
|
||||||
|
type: 'Виртуальная площадка',
|
||||||
|
contact: 'support@formula-u.com',
|
||||||
|
bookingNumber: 'В-002',
|
||||||
|
},
|
||||||
|
// ... другие трассы
|
||||||
|
];
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import {paths} from "./path";
|
import {paths} from "./path";
|
||||||
|
|
||||||
export const navItems = [
|
export const navItems = [
|
||||||
{key: 'home', title: 'Домашняя страница', href: paths.home, icon: 'menu'},
|
{name: 'home', title: 'Домашняя страница', href: paths.home},
|
||||||
{key: 'championships', title: 'Чемпионаты', href: paths.chp.championships, icon: 'basket'},
|
{name: 'championships', title: 'Чемпионаты', href: paths.chp.championships},
|
||||||
{key: 'calendar', title: 'Календарь', href: paths.calendar, icon: 'basket'},
|
{name: 'calendar', title: 'Календарь', href: paths.calendar},
|
||||||
{key: 'stages', title: 'Этапы', href: paths.stg.stages, icon: 'ingredients'},
|
{name: 'stages', title: 'Этапы', href: paths.stg.stages},
|
||||||
|
{name: 'places', title: 'Площадки', href: paths.places.places}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ export const paths = {
|
|||||||
stages: '/stages',
|
stages: '/stages',
|
||||||
stage: '/stages/:id',
|
stage: '/stages/:id',
|
||||||
},
|
},
|
||||||
|
places: {
|
||||||
|
places: '/places',
|
||||||
|
place: '/places/:id',
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
user: '/users/:id'
|
||||||
|
},
|
||||||
calendar: '/calendar',
|
calendar: '/calendar',
|
||||||
auth: {signIn: '/auth/sign-in', bot: 'https://t.me/kayashovBarClientBot', tg: '/tg'},
|
auth: {signIn: '/auth/sign-in', bot: 'https://t.me/kayashovBarClientBot', tg: '/tg'},
|
||||||
errors: {notFound: '/errors/not-found'},
|
errors: {notFound: '/errors/not-found'},
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {experimental_extendTheme as extendTheme} from '@mui/material/styles';
|
import {experimental_extendTheme as extendTheme} from '@mui/material/styles';
|
||||||
import {components} from "./components/components";
|
import {components} from "./components/components";
|
||||||
import shadows from "@mui/material/styles/shadows";
|
// import shadows from "@mui/material/styles/shadows";
|
||||||
import {typography} from "./typography";
|
import {typography} from "./typography";
|
||||||
import {colorSchemes} from "./color-schemes";
|
import {colorSchemes} from "./color-schemes";
|
||||||
|
import {shadows} from "./shadows";
|
||||||
|
|
||||||
export function createTTheme() {
|
export function createTTheme() {
|
||||||
return extendTheme({
|
return extendTheme({
|
||||||
|
|||||||