Compare commits
1 Commits
master
...
feat/refre
| Author | SHA1 | Date | |
|---|---|---|---|
| 67758d91cf |
@ -80,7 +80,7 @@ COPY --from=builder /build/server/dist ./
|
||||
COPY --from=builder /build/client/dist ./public
|
||||
|
||||
# Zkopírování produkčních .env serveru
|
||||
COPY /server/.env.production ./server
|
||||
COPY /server/.env.production ./server/src
|
||||
|
||||
# Zkopírování konfigurace easter eggů
|
||||
RUN if [ -f /server/.easter-eggs.json ]; then cp /server/.easter-eggs.json ./server/; fi
|
||||
|
||||
@ -6,31 +6,31 @@
|
||||
"type": "module",
|
||||
"homepage": ".",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.7.1",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"bootstrap": "^5.3.8",
|
||||
"react": "^19.2.0",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-jwt": "^1.3.0",
|
||||
"react-modal": "^3.16.3",
|
||||
"react-router": "^7.9.4",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.11.20",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"bootstrap": "^5.2.3",
|
||||
"react": "^19.0.0",
|
||||
"react-bootstrap": "^2.7.2",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-jwt": "^1.2.0",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-router": "^7.2.0",
|
||||
"react-router-dom": "^7.2.0",
|
||||
"react-select-search": "^4.1.6",
|
||||
"react-snowfall": "^2.3.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"recharts": "^3.2.1",
|
||||
"sass": "^1.93.2",
|
||||
"react-snowfall": "^2.2.0",
|
||||
"react-toastify": "^10.0.4",
|
||||
"recharts": "^2.15.1",
|
||||
"sass": "^1.80.6",
|
||||
"socket.io-client": "^4.6.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.9",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^6.0.3",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
@ -56,6 +56,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"prettier": "^3.6.2"
|
||||
"prettier": "^3.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<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"
|
||||
width="100px" height="100px" viewBox="1 0 100 100" enable-background="new 1 0 100 100" xml:space="preserve">
|
||||
<path fill="#8B4513" d="M31.5,77.2c0,0,2.6-1.9,3.4-2.8c0.8-0.9,2-2.5,2-2.5s-4.7-0.7-7.9-1.6c-3.2-0.9-9.7-3.7-9.7-3.7
|
||||
s1.3-0.4,2.9-1.4c1.6-1.1,2-1.7,2-1.7s-4.7-1.3-7.5-2.4c-2.8-1-7.3-2.9-7.3-2.9s1.4-0.2,2.8-1.3c1.4-1.1,1.5-2.2,1.5-2.2
|
||||
S9.4,53.3,7,51.9c-2.4-1.5-5.3-3.7-5.3-3.7s5.2-4,8.7-5.5c3.5-1.5,9.1-2.6,9.1-2.6s-0.9-1.5-2.1-2.6c-1.1-1.1-4.2-2.9-4.2-2.9
|
||||
s5.6-1.9,8.3-2.5c2.7-0.6,8.2,0,8.2,0s-0.1-1.1-1.3-2.2c-1.2-1.1-2.7-2-2.7-2S33,25.4,37,24.4c4-1,10.8-0.6,10.8-0.6
|
||||
S46.1,22.1,45,21c-1.1-1.1-2.7-2.3-2.7-2.3s5.4-0.4,9.1-0.4c3.7,0.1,9.5,1.1,9.5,1.1s-0.7-1-1.4-2.3c-0.8-1.3-1.5-2.6-1.5-2.6
|
||||
s7.1,1.7,9.5,2.8c2.4,1,7.6,3.9,7.6,3.9s-0.1-1.8-1-3.5c-0.9-1.7-1.8-3-1.8-3s27.7,17.2,28.1,33.5c0.1,1.8-0.1,3.7-0.3,5.5
|
||||
c-1.5-0.7-3.1-1.3-4.7-1.8c-2.3-6.3-4.3-9.9-6.4-12.6c-2.4-2.9-6.9-8-9.6-9.1c2.3,1.9,6.8,6.2,9.7,11.8c2.3,4.6,3,5.6,3.8,9.2
|
||||
c-3.6-1.1-7.3-1.9-11-2.5c-0.7-0.1-1.4-0.2-2.1-0.3c-2-4.3-4.4-8.8-7.6-11.5c-3.7-3.2-8.3-6.7-11.1-7.5c2.6,1.5,8.7,6.8,11.8,10.9
|
||||
c2.2,3,2.9,5.1,4,7.7c-5-0.6-9.9-0.9-14.9-1.1c-2-3.7-4.5-7.5-7.6-9.6c-3.4-2.4-7.6-5-10-5.5c2.3,1.1,7.9,5.1,10.8,8.3
|
||||
c2.3,2.6,3,4.4,4.2,6.9c-11.1,0.4-22,1.7-32.8,3.6c9.4-0.8,18.9-1,28.3-0.6c-1.1,1.7-1.8,3-3.8,4.7c-2.6,2.2-7.5,4.5-9.5,4.9
|
||||
c2,0.1,5.6-1.3,8.6-2.6c2.7-1.3,5.1-4.1,7.1-6.9c1.8,0.1,3.5,0.2,5.3,0.3c2.9,0.1,5.8,0.3,8.7,0.5c-1.4,3-2,5-4.6,8.1
|
||||
c-3.1,3.6-9.1,8-11.6,9.2c2.7-0.4,7.2-3.3,10.8-5.9c3.3-2.4,6-6.8,8.1-11c2.7,0.3,5.4,0.6,8.1,1.1c2,0.3,3.9,0.7,5.9,1.2
|
||||
c-1.9,4.1-2,4.6-5,8c-3.4,3.9-9.1,8.9-11.6,10.4c2.9-0.7,8.5-4.7,11.1-6.7c2.4-1.8,5.8-5.5,8.2-11.1c3,0.8,5.9,1.9,8.7,3.2
|
||||
c-3.1,12.9-11.9,24.1-11.9,24.1s0.2-1.7-0.5-3.9c-0.8-2.2-1-2-1-2s-1.1,2.9-5,6.8c-3.9,3.9-8.9,5-8.9,5s0.7-1.1,0.8-2.4
|
||||
c0.1-1.3-0.3-2-0.3-2s-2.6,1.9-5.3,3c-2.7,1.2-8.6,2.5-8.6,2.5s1.6-1.9,1.9-3c0.3-1-0.3-1.9-0.3-1.9s-3.9,1.5-7.5,1.5
|
||||
c-3.6,0-7.7-1.5-7.7-1.5s1-0.5,1.7-1.8c0.8-1.3,0.2-1.9,0.2-1.9s-4.4,0.5-7.5,0.1C36.6,79.3,31.5,77.2,31.5,77.2z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<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"
|
||||
width="100px" height="100px" viewBox="1 0 100 100" enable-background="new 1 0 100 100" xml:space="preserve">
|
||||
<path fill="#228B22" d="M31.5,77.2c0,0,2.6-1.9,3.4-2.8c0.8-0.9,2-2.5,2-2.5s-4.7-0.7-7.9-1.6c-3.2-0.9-9.7-3.7-9.7-3.7
|
||||
s1.3-0.4,2.9-1.4c1.6-1.1,2-1.7,2-1.7s-4.7-1.3-7.5-2.4c-2.8-1-7.3-2.9-7.3-2.9s1.4-0.2,2.8-1.3c1.4-1.1,1.5-2.2,1.5-2.2
|
||||
S9.4,53.3,7,51.9c-2.4-1.5-5.3-3.7-5.3-3.7s5.2-4,8.7-5.5c3.5-1.5,9.1-2.6,9.1-2.6s-0.9-1.5-2.1-2.6c-1.1-1.1-4.2-2.9-4.2-2.9
|
||||
s5.6-1.9,8.3-2.5c2.7-0.6,8.2,0,8.2,0s-0.1-1.1-1.3-2.2c-1.2-1.1-2.7-2-2.7-2S33,25.4,37,24.4c4-1,10.8-0.6,10.8-0.6
|
||||
S46.1,22.1,45,21c-1.1-1.1-2.7-2.3-2.7-2.3s5.4-0.4,9.1-0.4c3.7,0.1,9.5,1.1,9.5,1.1s-0.7-1-1.4-2.3c-0.8-1.3-1.5-2.6-1.5-2.6
|
||||
s7.1,1.7,9.5,2.8c2.4,1,7.6,3.9,7.6,3.9s-0.1-1.8-1-3.5c-0.9-1.7-1.8-3-1.8-3s27.7,17.2,28.1,33.5c0.1,1.8-0.1,3.7-0.3,5.5
|
||||
c-1.5-0.7-3.1-1.3-4.7-1.8c-2.3-6.3-4.3-9.9-6.4-12.6c-2.4-2.9-6.9-8-9.6-9.1c2.3,1.9,6.8,6.2,9.7,11.8c2.3,4.6,3,5.6,3.8,9.2
|
||||
c-3.6-1.1-7.3-1.9-11-2.5c-0.7-0.1-1.4-0.2-2.1-0.3c-2-4.3-4.4-8.8-7.6-11.5c-3.7-3.2-8.3-6.7-11.1-7.5c2.6,1.5,8.7,6.8,11.8,10.9
|
||||
c2.2,3,2.9,5.1,4,7.7c-5-0.6-9.9-0.9-14.9-1.1c-2-3.7-4.5-7.5-7.6-9.6c-3.4-2.4-7.6-5-10-5.5c2.3,1.1,7.9,5.1,10.8,8.3
|
||||
c2.3,2.6,3,4.4,4.2,6.9c-11.1,0.4-22,1.7-32.8,3.6c9.4-0.8,18.9-1,28.3-0.6c-1.1,1.7-1.8,3-3.8,4.7c-2.6,2.2-7.5,4.5-9.5,4.9
|
||||
c2,0.1,5.6-1.3,8.6-2.6c2.7-1.3,5.1-4.1,7.1-6.9c1.8,0.1,3.5,0.2,5.3,0.3c2.9,0.1,5.8,0.3,8.7,0.5c-1.4,3-2,5-4.6,8.1
|
||||
c-3.1,3.6-9.1,8-11.6,9.2c2.7-0.4,7.2-3.3,10.8-5.9c3.3-2.4,6-6.8,8.1-11c2.7,0.3,5.4,0.6,8.1,1.1c2,0.3,3.9,0.7,5.9,1.2
|
||||
c-1.9,4.1-2,4.6-5,8c-3.4,3.9-9.1,8.9-11.6,10.4c2.9-0.7,8.5-4.7,11.1-6.7c2.4-1.8,5.8-5.5,8.2-11.1c3,0.8,5.9,1.9,8.7,3.2
|
||||
c-3.1,12.9-11.9,24.1-11.9,24.1s0.2-1.7-0.5-3.9c-0.8-2.2-1-2-1-2s-1.1,2.9-5,6.8c-3.9,3.9-8.9,5-8.9,5s0.7-1.1,0.8-2.4
|
||||
c0.1-1.3-0.3-2-0.3-2s-2.6,1.9-5.3,3c-2.7,1.2-8.6,2.5-8.6,2.5s1.6-1.9,1.9-3c0.3-1-0.3-1.9-0.3-1.9s-3.9,1.5-7.5,1.5
|
||||
c-3.6,0-7.7-1.5-7.7-1.5s1-0.5,1.7-1.8c0.8-1.3,0.2-1.9,0.2-1.9s-4.4,0.5-7.5,0.1C36.6,79.3,31.5,77.2,31.5,77.2z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<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"
|
||||
width="100px" height="100px" viewBox="1 0 100 100" enable-background="new 1 0 100 100" xml:space="preserve">
|
||||
<path fill="#D2691E" d="M31.5,77.2c0,0,2.6-1.9,3.4-2.8c0.8-0.9,2-2.5,2-2.5s-4.7-0.7-7.9-1.6c-3.2-0.9-9.7-3.7-9.7-3.7
|
||||
s1.3-0.4,2.9-1.4c1.6-1.1,2-1.7,2-1.7s-4.7-1.3-7.5-2.4c-2.8-1-7.3-2.9-7.3-2.9s1.4-0.2,2.8-1.3c1.4-1.1,1.5-2.2,1.5-2.2
|
||||
S9.4,53.3,7,51.9c-2.4-1.5-5.3-3.7-5.3-3.7s5.2-4,8.7-5.5c3.5-1.5,9.1-2.6,9.1-2.6s-0.9-1.5-2.1-2.6c-1.1-1.1-4.2-2.9-4.2-2.9
|
||||
s5.6-1.9,8.3-2.5c2.7-0.6,8.2,0,8.2,0s-0.1-1.1-1.3-2.2c-1.2-1.1-2.7-2-2.7-2S33,25.4,37,24.4c4-1,10.8-0.6,10.8-0.6
|
||||
S46.1,22.1,45,21c-1.1-1.1-2.7-2.3-2.7-2.3s5.4-0.4,9.1-0.4c3.7,0.1,9.5,1.1,9.5,1.1s-0.7-1-1.4-2.3c-0.8-1.3-1.5-2.6-1.5-2.6
|
||||
s7.1,1.7,9.5,2.8c2.4,1,7.6,3.9,7.6,3.9s-0.1-1.8-1-3.5c-0.9-1.7-1.8-3-1.8-3s27.7,17.2,28.1,33.5c0.1,1.8-0.1,3.7-0.3,5.5
|
||||
c-1.5-0.7-3.1-1.3-4.7-1.8c-2.3-6.3-4.3-9.9-6.4-12.6c-2.4-2.9-6.9-8-9.6-9.1c2.3,1.9,6.8,6.2,9.7,11.8c2.3,4.6,3,5.6,3.8,9.2
|
||||
c-3.6-1.1-7.3-1.9-11-2.5c-0.7-0.1-1.4-0.2-2.1-0.3c-2-4.3-4.4-8.8-7.6-11.5c-3.7-3.2-8.3-6.7-11.1-7.5c2.6,1.5,8.7,6.8,11.8,10.9
|
||||
c2.2,3,2.9,5.1,4,7.7c-5-0.6-9.9-0.9-14.9-1.1c-2-3.7-4.5-7.5-7.6-9.6c-3.4-2.4-7.6-5-10-5.5c2.3,1.1,7.9,5.1,10.8,8.3
|
||||
c2.3,2.6,3,4.4,4.2,6.9c-11.1,0.4-22,1.7-32.8,3.6c9.4-0.8,18.9-1,28.3-0.6c-1.1,1.7-1.8,3-3.8,4.7c-2.6,2.2-7.5,4.5-9.5,4.9
|
||||
c2,0.1,5.6-1.3,8.6-2.6c2.7-1.3,5.1-4.1,7.1-6.9c1.8,0.1,3.5,0.2,5.3,0.3c2.9,0.1,5.8,0.3,8.7,0.5c-1.4,3-2,5-4.6,8.1
|
||||
c-3.1,3.6-9.1,8-11.6,9.2c2.7-0.4,7.2-3.3,10.8-5.9c3.3-2.4,6-6.8,8.1-11c2.7,0.3,5.4,0.6,8.1,1.1c2,0.3,3.9,0.7,5.9,1.2
|
||||
c-1.9,4.1-2,4.6-5,8c-3.4,3.9-9.1,8.9-11.6,10.4c2.9-0.7,8.5-4.7,11.1-6.7c2.4-1.8,5.8-5.5,8.2-11.1c3,0.8,5.9,1.9,8.7,3.2
|
||||
c-3.1,12.9-11.9,24.1-11.9,24.1s0.2-1.7-0.5-3.9c-0.8-2.2-1-2-1-2s-1.1,2.9-5,6.8c-3.9,3.9-8.9,5-8.9,5s0.7-1.1,0.8-2.4
|
||||
c0.1-1.3-0.3-2-0.3-2s-2.6,1.9-5.3,3c-2.7,1.2-8.6,2.5-8.6,2.5s1.6-1.9,1.9-3c0.3-1-0.3-1.9-0.3-1.9s-3.9,1.5-7.5,1.5
|
||||
c-3.6,0-7.7-1.5-7.7-1.5s1-0.5,1.7-1.8c0.8-1.3,0.2-1.9,0.2-1.9s-4.4,0.5-7.5,0.1C36.6,79.3,31.5,77.2,31.5,77.2z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<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"
|
||||
width="100px" height="100px" viewBox="1 0 100 100" enable-background="new 1 0 100 100" xml:space="preserve">
|
||||
<path fill="#B22222" d="M31.5,77.2c0,0,2.6-1.9,3.4-2.8c0.8-0.9,2-2.5,2-2.5s-4.7-0.7-7.9-1.6c-3.2-0.9-9.7-3.7-9.7-3.7
|
||||
s1.3-0.4,2.9-1.4c1.6-1.1,2-1.7,2-1.7s-4.7-1.3-7.5-2.4c-2.8-1-7.3-2.9-7.3-2.9s1.4-0.2,2.8-1.3c1.4-1.1,1.5-2.2,1.5-2.2
|
||||
S9.4,53.3,7,51.9c-2.4-1.5-5.3-3.7-5.3-3.7s5.2-4,8.7-5.5c3.5-1.5,9.1-2.6,9.1-2.6s-0.9-1.5-2.1-2.6c-1.1-1.1-4.2-2.9-4.2-2.9
|
||||
s5.6-1.9,8.3-2.5c2.7-0.6,8.2,0,8.2,0s-0.1-1.1-1.3-2.2c-1.2-1.1-2.7-2-2.7-2S33,25.4,37,24.4c4-1,10.8-0.6,10.8-0.6
|
||||
S46.1,22.1,45,21c-1.1-1.1-2.7-2.3-2.7-2.3s5.4-0.4,9.1-0.4c3.7,0.1,9.5,1.1,9.5,1.1s-0.7-1-1.4-2.3c-0.8-1.3-1.5-2.6-1.5-2.6
|
||||
s7.1,1.7,9.5,2.8c2.4,1,7.6,3.9,7.6,3.9s-0.1-1.8-1-3.5c-0.9-1.7-1.8-3-1.8-3s27.7,17.2,28.1,33.5c0.1,1.8-0.1,3.7-0.3,5.5
|
||||
c-1.5-0.7-3.1-1.3-4.7-1.8c-2.3-6.3-4.3-9.9-6.4-12.6c-2.4-2.9-6.9-8-9.6-9.1c2.3,1.9,6.8,6.2,9.7,11.8c2.3,4.6,3,5.6,3.8,9.2
|
||||
c-3.6-1.1-7.3-1.9-11-2.5c-0.7-0.1-1.4-0.2-2.1-0.3c-2-4.3-4.4-8.8-7.6-11.5c-3.7-3.2-8.3-6.7-11.1-7.5c2.6,1.5,8.7,6.8,11.8,10.9
|
||||
c2.2,3,2.9,5.1,4,7.7c-5-0.6-9.9-0.9-14.9-1.1c-2-3.7-4.5-7.5-7.6-9.6c-3.4-2.4-7.6-5-10-5.5c2.3,1.1,7.9,5.1,10.8,8.3
|
||||
c2.3,2.6,3,4.4,4.2,6.9c-11.1,0.4-22,1.7-32.8,3.6c9.4-0.8,18.9-1,28.3-0.6c-1.1,1.7-1.8,3-3.8,4.7c-2.6,2.2-7.5,4.5-9.5,4.9
|
||||
c2,0.1,5.6-1.3,8.6-2.6c2.7-1.3,5.1-4.1,7.1-6.9c1.8,0.1,3.5,0.2,5.3,0.3c2.9,0.1,5.8,0.3,8.7,0.5c-1.4,3-2,5-4.6,8.1
|
||||
c-3.1,3.6-9.1,8-11.6,9.2c2.7-0.4,7.2-3.3,10.8-5.9c3.3-2.4,6-6.8,8.1-11c2.7,0.3,5.4,0.6,8.1,1.1c2,0.3,3.9,0.7,5.9,1.2
|
||||
c-1.9,4.1-2,4.6-5,8c-3.4,3.9-9.1,8.9-11.6,10.4c2.9-0.7,8.5-4.7,11.1-6.7c2.4-1.8,5.8-5.5,8.2-11.1c3,0.8,5.9,1.9,8.7,3.2
|
||||
c-3.1,12.9-11.9,24.1-11.9,24.1s0.2-1.7-0.5-3.9c-0.8-2.2-1-2-1-2s-1.1,2.9-5,6.8c-3.9,3.9-8.9,5-8.9,5s0.7-1.1,0.8-2.4
|
||||
c0.1-1.3-0.3-2-0.3-2s-2.6,1.9-5.3,3c-2.7,1.2-8.6,2.5-8.6,2.5s1.6-1.9,1.9-3c0.3-1-0.3-1.9-0.3-1.9s-3.9,1.5-7.5,1.5
|
||||
c-3.6,0-7.7-1.5-7.7-1.5s1-0.5,1.7-1.8c0.8-1.3,0.2-1.9,0.2-1.9s-4.4,0.5-7.5,0.1C36.6,79.3,31.5,77.2,31.5,77.2z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<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"
|
||||
width="100px" height="100px" viewBox="1 0 100 100" enable-background="new 1 0 100 100" xml:space="preserve">
|
||||
<path fill="#FFD700" d="M31.5,77.2c0,0,2.6-1.9,3.4-2.8c0.8-0.9,2-2.5,2-2.5s-4.7-0.7-7.9-1.6c-3.2-0.9-9.7-3.7-9.7-3.7
|
||||
s1.3-0.4,2.9-1.4c1.6-1.1,2-1.7,2-1.7s-4.7-1.3-7.5-2.4c-2.8-1-7.3-2.9-7.3-2.9s1.4-0.2,2.8-1.3c1.4-1.1,1.5-2.2,1.5-2.2
|
||||
S9.4,53.3,7,51.9c-2.4-1.5-5.3-3.7-5.3-3.7s5.2-4,8.7-5.5c3.5-1.5,9.1-2.6,9.1-2.6s-0.9-1.5-2.1-2.6c-1.1-1.1-4.2-2.9-4.2-2.9
|
||||
s5.6-1.9,8.3-2.5c2.7-0.6,8.2,0,8.2,0s-0.1-1.1-1.3-2.2c-1.2-1.1-2.7-2-2.7-2S33,25.4,37,24.4c4-1,10.8-0.6,10.8-0.6
|
||||
S46.1,22.1,45,21c-1.1-1.1-2.7-2.3-2.7-2.3s5.4-0.4,9.1-0.4c3.7,0.1,9.5,1.1,9.5,1.1s-0.7-1-1.4-2.3c-0.8-1.3-1.5-2.6-1.5-2.6
|
||||
s7.1,1.7,9.5,2.8c2.4,1,7.6,3.9,7.6,3.9s-0.1-1.8-1-3.5c-0.9-1.7-1.8-3-1.8-3s27.7,17.2,28.1,33.5c0.1,1.8-0.1,3.7-0.3,5.5
|
||||
c-1.5-0.7-3.1-1.3-4.7-1.8c-2.3-6.3-4.3-9.9-6.4-12.6c-2.4-2.9-6.9-8-9.6-9.1c2.3,1.9,6.8,6.2,9.7,11.8c2.3,4.6,3,5.6,3.8,9.2
|
||||
c-3.6-1.1-7.3-1.9-11-2.5c-0.7-0.1-1.4-0.2-2.1-0.3c-2-4.3-4.4-8.8-7.6-11.5c-3.7-3.2-8.3-6.7-11.1-7.5c2.6,1.5,8.7,6.8,11.8,10.9
|
||||
c2.2,3,2.9,5.1,4,7.7c-5-0.6-9.9-0.9-14.9-1.1c-2-3.7-4.5-7.5-7.6-9.6c-3.4-2.4-7.6-5-10-5.5c2.3,1.1,7.9,5.1,10.8,8.3
|
||||
c2.3,2.6,3,4.4,4.2,6.9c-11.1,0.4-22,1.7-32.8,3.6c9.4-0.8,18.9-1,28.3-0.6c-1.1,1.7-1.8,3-3.8,4.7c-2.6,2.2-7.5,4.5-9.5,4.9
|
||||
c2,0.1,5.6-1.3,8.6-2.6c2.7-1.3,5.1-4.1,7.1-6.9c1.8,0.1,3.5,0.2,5.3,0.3c2.9,0.1,5.8,0.3,8.7,0.5c-1.4,3-2,5-4.6,8.1
|
||||
c-3.1,3.6-9.1,8-11.6,9.2c2.7-0.4,7.2-3.3,10.8-5.9c3.3-2.4,6-6.8,8.1-11c2.7,0.3,5.4,0.6,8.1,1.1c2,0.3,3.9,0.7,5.9,1.2
|
||||
c-1.9,4.1-2,4.6-5,8c-3.4,3.9-9.1,8.9-11.6,10.4c2.9-0.7,8.5-4.7,11.1-6.7c2.4-1.8,5.8-5.5,8.2-11.1c3,0.8,5.9,1.9,8.7,3.2
|
||||
c-3.1,12.9-11.9,24.1-11.9,24.1s0.2-1.7-0.5-3.9c-0.8-2.2-1-2-1-2s-1.1,2.9-5,6.8c-3.9,3.9-8.9,5-8.9,5s0.7-1.1,0.8-2.4
|
||||
c0.1-1.3-0.3-2-0.3-2s-2.6,1.9-5.3,3c-2.7,1.2-8.6,2.5-8.6,2.5s1.6-1.9,1.9-3c0.3-1-0.3-1.9-0.3-1.9s-3.9,1.5-7.5,1.5
|
||||
c-3.6,0-7.7-1.5-7.7-1.5s1-0.5,1.7-1.8c0.8-1.3,0.2-1.9,0.2-1.9s-4.4,0.5-7.5,0.1C36.6,79.3,31.5,77.2,31.5,77.2z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<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"
|
||||
width="100px" height="100px" viewBox="1 0 100 100" enable-background="new 1 0 100 100" xml:space="preserve">
|
||||
<path fill="#471C37" d="M31.5,77.2c0,0,2.6-1.9,3.4-2.8c0.8-0.9,2-2.5,2-2.5s-4.7-0.7-7.9-1.6c-3.2-0.9-9.7-3.7-9.7-3.7
|
||||
s1.3-0.4,2.9-1.4c1.6-1.1,2-1.7,2-1.7s-4.7-1.3-7.5-2.4c-2.8-1-7.3-2.9-7.3-2.9s1.4-0.2,2.8-1.3c1.4-1.1,1.5-2.2,1.5-2.2
|
||||
S9.4,53.3,7,51.9c-2.4-1.5-5.3-3.7-5.3-3.7s5.2-4,8.7-5.5c3.5-1.5,9.1-2.6,9.1-2.6s-0.9-1.5-2.1-2.6c-1.1-1.1-4.2-2.9-4.2-2.9
|
||||
s5.6-1.9,8.3-2.5c2.7-0.6,8.2,0,8.2,0s-0.1-1.1-1.3-2.2c-1.2-1.1-2.7-2-2.7-2S33,25.4,37,24.4c4-1,10.8-0.6,10.8-0.6
|
||||
S46.1,22.1,45,21c-1.1-1.1-2.7-2.3-2.7-2.3s5.4-0.4,9.1-0.4c3.7,0.1,9.5,1.1,9.5,1.1s-0.7-1-1.4-2.3c-0.8-1.3-1.5-2.6-1.5-2.6
|
||||
s7.1,1.7,9.5,2.8c2.4,1,7.6,3.9,7.6,3.9s-0.1-1.8-1-3.5c-0.9-1.7-1.8-3-1.8-3s27.7,17.2,28.1,33.5c0.1,1.8-0.1,3.7-0.3,5.5
|
||||
c-1.5-0.7-3.1-1.3-4.7-1.8c-2.3-6.3-4.3-9.9-6.4-12.6c-2.4-2.9-6.9-8-9.6-9.1c2.3,1.9,6.8,6.2,9.7,11.8c2.3,4.6,3,5.6,3.8,9.2
|
||||
c-3.6-1.1-7.3-1.9-11-2.5c-0.7-0.1-1.4-0.2-2.1-0.3c-2-4.3-4.4-8.8-7.6-11.5c-3.7-3.2-8.3-6.7-11.1-7.5c2.6,1.5,8.7,6.8,11.8,10.9
|
||||
c2.2,3,2.9,5.1,4,7.7c-5-0.6-9.9-0.9-14.9-1.1c-2-3.7-4.5-7.5-7.6-9.6c-3.4-2.4-7.6-5-10-5.5c2.3,1.1,7.9,5.1,10.8,8.3
|
||||
c2.3,2.6,3,4.4,4.2,6.9c-11.1,0.4-22,1.7-32.8,3.6c9.4-0.8,18.9-1,28.3-0.6c-1.1,1.7-1.8,3-3.8,4.7c-2.6,2.2-7.5,4.5-9.5,4.9
|
||||
c2,0.1,5.6-1.3,8.6-2.6c2.7-1.3,5.1-4.1,7.1-6.9c1.8,0.1,3.5,0.2,5.3,0.3c2.9,0.1,5.8,0.3,8.7,0.5c-1.4,3-2,5-4.6,8.1
|
||||
c-3.1,3.6-9.1,8-11.6,9.2c2.7-0.4,7.2-3.3,10.8-5.9c3.3-2.4,6-6.8,8.1-11c2.7,0.3,5.4,0.6,8.1,1.1c2,0.3,3.9,0.7,5.9,1.2
|
||||
c-1.9,4.1-2,4.6-5,8c-3.4,3.9-9.1,8.9-11.6,10.4c2.9-0.7,8.5-4.7,11.1-6.7c2.4-1.8,5.8-5.5,8.2-11.1c3,0.8,5.9,1.9,8.7,3.2
|
||||
c-3.1,12.9-11.9,24.1-11.9,24.1s0.2-1.7-0.5-3.9c-0.8-2.2-1-2-1-2s-1.1,2.9-5,6.8c-3.9,3.9-8.9,5-8.9,5s0.7-1.1,0.8-2.4
|
||||
c0.1-1.3-0.3-2-0.3-2s-2.6,1.9-5.3,3c-2.7,1.2-8.6,2.5-8.6,2.5s1.6-1.9,1.9-3c0.3-1-0.3-1.9-0.3-1.9s-3.9,1.5-7.5,1.5
|
||||
c-3.6,0-7.7-1.5-7.7-1.5s1-0.5,1.7-1.8c0.8-1.3,0.2-1.9,0.2-1.9s-4.4,0.5-7.5,0.1C36.6,79.3,31.5,77.2,31.5,77.2z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@ -51,24 +51,8 @@
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
/* Sticky footer layout */
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
/* Zabere zbytek místa */
|
||||
}
|
||||
|
||||
.title {
|
||||
|
||||
@ -18,10 +18,10 @@ import Loader from './components/Loader';
|
||||
import { getHumanDateTime, isInTheFuture } from './Utils';
|
||||
import NoteModal from './components/modals/NoteModal';
|
||||
import { useEasterEgg } from './context/eggs';
|
||||
import { Link } from 'react-router';
|
||||
import { STATS_URL } from './AppRoutes';
|
||||
import { ClientData, Food, PizzaOrder, DepartureTime, PizzaDayState, Restaurant, RestaurantDayMenu, RestaurantDayMenuMap, LunchChoice, UserLunchChoice, PizzaVariant, getData, getEasterEggImage, addPizza, removePizza, updatePizzaDayNote, createPizzaDay, deletePizzaDay, lockPizzaDay, unlockPizzaDay, finishOrder, finishDelivery, addChoice, jdemeObed, removeChoices, removeChoice, updateNote, changeDepartureTime } from '../../types';
|
||||
import { getLunchChoiceName } from './enums';
|
||||
import FallingLeaves, { LEAF_PRESETS, LEAF_COLOR_THEMES } from './FallingLeaves';
|
||||
import './FallingLeaves.scss';
|
||||
|
||||
const EVENT_CONNECT = "connect"
|
||||
|
||||
@ -32,26 +32,6 @@ const EASTER_EGG_STYLE = {
|
||||
animationTimingFunction: "ease"
|
||||
}
|
||||
|
||||
// Mapování čísel alergenů na jejich názvy
|
||||
const ALLERGENS: { [key: number]: string } = {
|
||||
1: "Obiloviny obsahující lepek",
|
||||
2: "Korýši a výrobky z nich",
|
||||
3: "Vejce a výrobky z nich",
|
||||
4: "Ryby a výrobky z nich",
|
||||
5: "Arašidy a výrobky z nich",
|
||||
6: "Sója a výrobky z nich",
|
||||
7: "Mléko a výrobky z nich (včetně laktózy)",
|
||||
8: "Skořápkové plody",
|
||||
9: "Celer a výrobky z něj",
|
||||
10: "Hořčice a výrobky z ní",
|
||||
11: "Sezamová semena a výrobky z nich",
|
||||
12: "Oxid siřičitý a siřičitany",
|
||||
13: "Vlčí bob (Lupina) a výrobky z něj",
|
||||
14: "Měkkýši a výrobky z nich"
|
||||
}
|
||||
|
||||
const LINK_ALLERGENS = 'https://www.strava.cz/Strava/Napoveda/cz/Prilohy/alergeny.pdf';
|
||||
|
||||
// Výchozí doba trvání animace v sekundách, pokud není přetíženo v konfiguračním JSONu
|
||||
const EASTER_EGG_DEFAULT_DURATION = 0.75;
|
||||
|
||||
@ -374,20 +354,7 @@ function App() {
|
||||
(!hideSoups || !f.isSoup) &&
|
||||
<tr key={f.name} onClick={() => doAddClickFoodChoice(location, index)}>
|
||||
<td>{f.amount}</td>
|
||||
<td>
|
||||
{f.name}
|
||||
{f.allergens && f.allergens.length > 0 && (
|
||||
<> ({f.allergens.map((a, idx) => (
|
||||
<span key={a}>
|
||||
<span title={ALLERGENS[a]} style={{ cursor: 'help', textDecoration: 'underline' }} onClick={e => {
|
||||
e.stopPropagation();
|
||||
window.open(LINK_ALLERGENS, '_blank');
|
||||
}}>{a}</span>
|
||||
{idx < f.allergens!.length - 1 && ','}
|
||||
</span>
|
||||
))})</>
|
||||
)}
|
||||
</td>
|
||||
<td>{f.name}</td>
|
||||
<td>{f.price}</td>
|
||||
</tr>
|
||||
)}
|
||||
@ -437,7 +404,7 @@ function App() {
|
||||
const { path, url, startOffset, endOffset, duration, ...style } = easterEgg || {};
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<>
|
||||
{easterEgg && eggImage && <img ref={eggRef} alt='' src={URL.createObjectURL(eggImage)} style={{ position: 'absolute', ...EASTER_EGG_STYLE, ...style, animationDuration: `${duration ?? EASTER_EGG_DEFAULT_DURATION}s` }} />}
|
||||
<Header />
|
||||
<div className='wrapper'>
|
||||
@ -447,20 +414,15 @@ function App() {
|
||||
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> */}
|
||||
Poslední změny:
|
||||
<ul>
|
||||
<li>Zobrazení alergenu při najetí myší a proklik na seznam alergenů</li>
|
||||
<li>Přesun přenačtení menu do samostatného dialogu</li>
|
||||
<li>Podzimní atmosféra</li>
|
||||
<li>Migrace na generované <Link target='_blank' to="https://www.openapis.org">OpenAPI</Link></li>
|
||||
<li>Odebrání zimní atmosféry</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
{dayIndex != null &&
|
||||
<div className='day-navigator'>
|
||||
<span title='Předchozí den'>
|
||||
<FontAwesomeIcon icon={faChevronLeft} style={{ cursor: "pointer", visibility: dayIndex > 0 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex - 1)} />
|
||||
</span>
|
||||
<FontAwesomeIcon title="Předchozí den" icon={faChevronLeft} style={{ cursor: "pointer", visibility: dayIndex > 0 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex - 1)} />
|
||||
<h1 className='title' style={{ color: dayIndex === data.todayDayIndex ? 'black' : 'gray' }}>{data.date}</h1>
|
||||
<span title="Následující den">
|
||||
<FontAwesomeIcon icon={faChevronRight} style={{ cursor: "pointer", visibility: dayIndex < 4 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex + 1)} />
|
||||
</span>
|
||||
<FontAwesomeIcon title="Následující den" icon={faChevronRight} style={{ cursor: "pointer", visibility: dayIndex < 4 ? "initial" : "hidden" }} onClick={() => handleDayChange(dayIndex + 1)} />
|
||||
</div>
|
||||
}
|
||||
<Row className='food-tables'>
|
||||
@ -529,22 +491,18 @@ function App() {
|
||||
const trusted = userPayload?.trusted || false;
|
||||
return <tr key={entry[0]}>
|
||||
<td>
|
||||
{trusted && <span className='trusted-icon' title='Uživatel ověřený doménovým přihlášením'>
|
||||
<FontAwesomeIcon icon={faCircleCheck} style={{ cursor: "help" }} />
|
||||
{trusted && <span className='trusted-icon'>
|
||||
<FontAwesomeIcon title='Uživatel ověřený doménovým přihlášením' icon={faCircleCheck} style={{ cursor: "help" }} />
|
||||
</span>}
|
||||
{login}
|
||||
{userPayload.departureTime && <small> ({userPayload.departureTime})</small>}
|
||||
{userPayload.note && <small style={{ overflowWrap: 'anywhere' }}> ({userPayload.note})</small>}
|
||||
{login === auth.login && canChangeChoice && <span title='Upravit poznámku'>
|
||||
<FontAwesomeIcon onClick={() => {
|
||||
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
|
||||
setNoteModalOpen(true);
|
||||
}} className='action-icon' icon={faNoteSticky} />
|
||||
</span>}
|
||||
{login === auth.login && canChangeChoice && <span title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`}>
|
||||
<FontAwesomeIcon onClick={() => {
|
||||
}} title='Upravit poznámku' className='action-icon' icon={faNoteSticky} />}
|
||||
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
|
||||
doRemoveChoices(key as LunchChoice);
|
||||
}} className='action-icon' icon={faTrashCan} />
|
||||
</span>}
|
||||
}} title={`Odstranit volbu ${locationName}, včetně případných zvolených jídel`} className='action-icon' icon={faTrashCan} />}
|
||||
</td>
|
||||
{userChoices?.length && food ? <td>
|
||||
<ul>
|
||||
@ -553,12 +511,9 @@ function App() {
|
||||
const foodName = food[restaurantKey]?.food?.[foodIndex].name;
|
||||
return <li key={foodIndex}>
|
||||
{foodName}
|
||||
{login === auth.login && canChangeChoice &&
|
||||
<span title={`Odstranit ${foodName}`}>
|
||||
<FontAwesomeIcon onClick={() => {
|
||||
{login === auth.login && canChangeChoice && <FontAwesomeIcon onClick={() => {
|
||||
doRemoveFoodChoice(restaurantKey, foodIndex);
|
||||
}} className='action-icon' icon={faTrashCan} />
|
||||
</span>}
|
||||
}} title={`Odstranit ${foodName}`} className='action-icon' icon={faTrashCan} />}
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
@ -699,13 +654,9 @@ function App() {
|
||||
</div>
|
||||
</> || "Jejda, něco se nám nepovedlo :("}
|
||||
</div>
|
||||
<FallingLeaves
|
||||
numLeaves={LEAF_PRESETS.NORMAL}
|
||||
leafVariants={LEAF_COLOR_THEMES.AUTUMN}
|
||||
/>
|
||||
<Footer />
|
||||
<NoteModal isOpen={noteModalOpen} onClose={() => setNoteModalOpen(false)} onSave={saveNote} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
.falling-leaves {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leaf-scene {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
transform-style: preserve-3d;
|
||||
|
||||
div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
transform-style: preserve-3d;
|
||||
backface-visibility: visible;
|
||||
}
|
||||
}
|
||||
@ -1,317 +0,0 @@
|
||||
import React, { useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
// Různé barevné varianty listů
|
||||
const LEAF_VARIANTS = [
|
||||
'leaf.svg', // Původní tmavě hnědá
|
||||
'leaf-orange.svg', // Oranžová
|
||||
'leaf-yellow.svg', // Žlutá
|
||||
'leaf-red.svg', // Červená
|
||||
'leaf-brown.svg', // Světle hnědá
|
||||
'leaf-green.svg', // Zelená
|
||||
] as const;
|
||||
|
||||
interface LeafData {
|
||||
el: HTMLDivElement;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
rotation: {
|
||||
axis: 'X' | 'Y' | 'Z';
|
||||
value: number;
|
||||
speed: number;
|
||||
x: number;
|
||||
};
|
||||
xSpeedVariation: number;
|
||||
ySpeed: number;
|
||||
path: {
|
||||
type: number;
|
||||
start: number;
|
||||
};
|
||||
image: number;
|
||||
}
|
||||
|
||||
interface WindOptions {
|
||||
magnitude: number;
|
||||
maxSpeed: number;
|
||||
duration: number;
|
||||
start: number;
|
||||
speed: (t: number, y: number) => number;
|
||||
}
|
||||
|
||||
interface LeafSceneOptions {
|
||||
numLeaves: number;
|
||||
wind: WindOptions;
|
||||
}
|
||||
|
||||
interface FallingLeavesProps {
|
||||
/** Počet padających listů (výchozí: 20) */
|
||||
numLeaves?: number;
|
||||
/** CSS třída pro kontejner (výchozí: 'falling-leaves') */
|
||||
className?: string;
|
||||
/** Barevné varianty listů k použití (výchozí: všechny) */
|
||||
leafVariants?: readonly string[];
|
||||
}
|
||||
|
||||
class LeafScene {
|
||||
private viewport: HTMLElement;
|
||||
private world: HTMLDivElement;
|
||||
private leaves: LeafData[] = [];
|
||||
private options: LeafSceneOptions;
|
||||
private width: number;
|
||||
private height: number;
|
||||
private timer: number = 0;
|
||||
private animationId: number | null = null;
|
||||
private leafVariants: readonly string[];
|
||||
|
||||
constructor(el: HTMLElement, numLeaves: number = 20, leafVariants: readonly string[] = LEAF_VARIANTS) {
|
||||
this.viewport = el;
|
||||
this.world = document.createElement('div');
|
||||
this.leafVariants = leafVariants;
|
||||
|
||||
this.options = {
|
||||
numLeaves,
|
||||
wind: {
|
||||
magnitude: 1.2,
|
||||
maxSpeed: 12,
|
||||
duration: 300,
|
||||
start: 0,
|
||||
speed: () => 0
|
||||
},
|
||||
};
|
||||
|
||||
this.width = this.viewport.offsetWidth;
|
||||
this.height = this.viewport.offsetHeight;
|
||||
}
|
||||
|
||||
private resetLeaf = (leaf: LeafData): LeafData => {
|
||||
// place leaf towards the top left
|
||||
leaf.x = this.width * 2 - Math.random() * this.width * 1.75;
|
||||
leaf.y = -10;
|
||||
leaf.z = Math.random() * 200;
|
||||
|
||||
if (leaf.x > this.width) {
|
||||
leaf.x = this.width + 10;
|
||||
leaf.y = Math.random() * this.height / 2;
|
||||
}
|
||||
|
||||
// at the start, the leaf can be anywhere
|
||||
if (this.timer === 0) {
|
||||
leaf.y = Math.random() * this.height;
|
||||
}
|
||||
|
||||
// Choose axis of rotation.
|
||||
// If axis is not X, chose a random static x-rotation for greater variability
|
||||
leaf.rotation.speed = Math.random() * 10;
|
||||
const randomAxis = Math.random();
|
||||
|
||||
if (randomAxis > 0.5) {
|
||||
leaf.rotation.axis = 'X';
|
||||
} else if (randomAxis > 0.25) {
|
||||
leaf.rotation.axis = 'Y';
|
||||
leaf.rotation.x = Math.random() * 180 + 90;
|
||||
} else {
|
||||
leaf.rotation.axis = 'Z';
|
||||
leaf.rotation.x = Math.random() * 360 - 180;
|
||||
// looks weird if the rotation is too fast around this axis
|
||||
leaf.rotation.speed = Math.random() * 3;
|
||||
}
|
||||
|
||||
// random speed
|
||||
leaf.xSpeedVariation = Math.random() * 0.8 - 0.4;
|
||||
leaf.ySpeed = Math.random() + 1.5;
|
||||
|
||||
// randomly select leaf color variant
|
||||
const randomVariantIndex = Math.floor(Math.random() * this.leafVariants.length);
|
||||
leaf.image = randomVariantIndex;
|
||||
|
||||
// apply the background image to the leaf element
|
||||
const leafVariant = this.leafVariants[randomVariantIndex];
|
||||
leaf.el.style.backgroundImage = `url(${leafVariant})`;
|
||||
|
||||
return leaf;
|
||||
};
|
||||
|
||||
private updateLeaf = (leaf: LeafData): void => {
|
||||
const leafWindSpeed = this.options.wind.speed(this.timer - this.options.wind.start, leaf.y);
|
||||
|
||||
const xSpeed = leafWindSpeed + leaf.xSpeedVariation;
|
||||
leaf.x -= xSpeed;
|
||||
leaf.y += leaf.ySpeed;
|
||||
leaf.rotation.value += leaf.rotation.speed;
|
||||
|
||||
const transform = `translateX(${leaf.x}px) translateY(${leaf.y}px) translateZ(${leaf.z}px) rotate${leaf.rotation.axis}(${leaf.rotation.value}deg)${leaf.rotation.axis !== 'X' ? ` rotateX(${leaf.rotation.x}deg)` : ''
|
||||
}`;
|
||||
|
||||
leaf.el.style.transform = transform;
|
||||
|
||||
// reset if out of view
|
||||
if (leaf.x < -10 || leaf.y > this.height + 10) {
|
||||
this.resetLeaf(leaf);
|
||||
}
|
||||
};
|
||||
|
||||
private updateWind = (): void => {
|
||||
// wind follows a sine curve: asin(b*time + c) + a
|
||||
// where a = wind magnitude as a function of leaf position, b = wind.duration, c = offset
|
||||
// wind duration should be related to wind magnitude, e.g. higher windspeed means longer gust duration
|
||||
|
||||
if (this.timer === 0 || this.timer > (this.options.wind.start + this.options.wind.duration)) {
|
||||
this.options.wind.magnitude = Math.random() * this.options.wind.maxSpeed;
|
||||
this.options.wind.duration = this.options.wind.magnitude * 50 + (Math.random() * 20 - 10);
|
||||
this.options.wind.start = this.timer;
|
||||
|
||||
const screenHeight = this.height;
|
||||
|
||||
this.options.wind.speed = function (t: number, y: number) {
|
||||
// should go from full wind speed at the top, to 1/2 speed at the bottom, using leaf Y
|
||||
const a = this.magnitude / 2 * (screenHeight - 2 * y / 3) / screenHeight;
|
||||
return a * Math.sin(2 * Math.PI / this.duration * t + (3 * Math.PI / 2)) + a;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
public init = (): void => {
|
||||
// Clear existing leaves
|
||||
this.leaves = [];
|
||||
this.world.innerHTML = '';
|
||||
|
||||
for (let i = 0; i < this.options.numLeaves; i++) {
|
||||
const leaf: LeafData = {
|
||||
el: document.createElement('div'),
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
rotation: {
|
||||
axis: 'X',
|
||||
value: 0,
|
||||
speed: 0,
|
||||
x: 0
|
||||
},
|
||||
xSpeedVariation: 0,
|
||||
ySpeed: 0,
|
||||
path: {
|
||||
type: 1,
|
||||
start: 0,
|
||||
},
|
||||
image: 1
|
||||
};
|
||||
|
||||
this.resetLeaf(leaf);
|
||||
this.leaves.push(leaf);
|
||||
this.world.appendChild(leaf.el);
|
||||
}
|
||||
|
||||
this.world.className = 'leaf-scene';
|
||||
this.viewport.appendChild(this.world);
|
||||
|
||||
// set perspective
|
||||
this.world.style.perspective = "400px";
|
||||
|
||||
// reset window height/width on resize
|
||||
const handleResize = (): void => {
|
||||
this.width = this.viewport.offsetWidth;
|
||||
this.height = this.viewport.offsetHeight;
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
};
|
||||
|
||||
public render = (): void => {
|
||||
this.updateWind();
|
||||
|
||||
for (let i = 0; i < this.leaves.length; i++) {
|
||||
this.updateLeaf(this.leaves[i]);
|
||||
}
|
||||
|
||||
this.timer++;
|
||||
this.animationId = requestAnimationFrame(this.render);
|
||||
};
|
||||
|
||||
public destroy = (): void => {
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
}
|
||||
|
||||
if (this.world && this.world.parentNode) {
|
||||
this.world.parentNode.removeChild(this.world);
|
||||
}
|
||||
|
||||
window.removeEventListener('resize', () => {
|
||||
this.width = this.viewport.offsetWidth;
|
||||
this.height = this.viewport.offsetHeight;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Komponenta pro zobrazení padajících listů na pozadí stránky
|
||||
*
|
||||
* @param numLeaves - Počet padajících listů (výchozí: 20)
|
||||
* @param className - CSS třída pro kontejner (výchozí: 'falling-leaves')
|
||||
* @param leafVariants - Barevné varianty listů k použití (výchozí: všechny)
|
||||
*
|
||||
* @example
|
||||
* // Základní použití s výchozím počtem listů
|
||||
* <FallingLeaves />
|
||||
*
|
||||
* @example
|
||||
* // Použití s vlastním počtem listů
|
||||
* <FallingLeaves numLeaves={50} />
|
||||
*
|
||||
* @example
|
||||
* // Použití s vlastní CSS třídou a pouze podzimními barvami
|
||||
* <FallingLeaves
|
||||
* numLeaves={15}
|
||||
* className="autumn-leaves"
|
||||
* leafVariants={['leaf-orange.svg', 'leaf-red.svg', 'leaf-yellow.svg']}
|
||||
* />
|
||||
*/
|
||||
const FallingLeaves: React.FC<FallingLeavesProps> = ({
|
||||
numLeaves = 20,
|
||||
className = 'falling-leaves',
|
||||
leafVariants = LEAF_VARIANTS
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const leafSceneRef = useRef<LeafScene | null>(null);
|
||||
|
||||
const initializeLeafScene = useCallback(() => {
|
||||
if (containerRef.current) {
|
||||
leafSceneRef.current = new LeafScene(containerRef.current, numLeaves, leafVariants);
|
||||
leafSceneRef.current.init();
|
||||
leafSceneRef.current.render();
|
||||
}
|
||||
}, [numLeaves, leafVariants]);
|
||||
|
||||
useEffect(() => {
|
||||
initializeLeafScene();
|
||||
|
||||
return () => {
|
||||
if (leafSceneRef.current) {
|
||||
leafSceneRef.current.destroy();
|
||||
leafSceneRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [initializeLeafScene]);
|
||||
|
||||
return <div ref={containerRef} className={className} />;
|
||||
};
|
||||
|
||||
// Přednastavení pro různé účely
|
||||
export const LEAF_PRESETS = {
|
||||
LIGHT: 10, // Lehký podzimní efekt
|
||||
NORMAL: 20, // Standardní množství
|
||||
HEAVY: 40, // Silný podzimní vítr
|
||||
BLIZZARD: 80 // Hustý pád listí
|
||||
} as const;
|
||||
|
||||
// Přednastavené barevné kombinace
|
||||
export const LEAF_COLOR_THEMES = {
|
||||
ALL: LEAF_VARIANTS, // Všechny barvy
|
||||
AUTUMN: ['leaf-orange.svg', 'leaf-red.svg', 'leaf-yellow.svg', 'leaf-brown.svg'] as const, // Podzimní barvy
|
||||
WARM: ['leaf-orange.svg', 'leaf-red.svg', 'leaf-brown.svg'] as const, // Teplé barvy
|
||||
CLASSIC: ['leaf.svg', 'leaf-brown.svg'] as const, // Klasické hnědé odstíny
|
||||
BRIGHT: ['leaf-yellow.svg', 'leaf-orange.svg'] as const, // Světlé barvy
|
||||
} as const;
|
||||
|
||||
export default FallingLeaves;
|
||||
@ -1,12 +1,7 @@
|
||||
import { Navbar } from "react-bootstrap";
|
||||
|
||||
export default function Footer() {
|
||||
return <Navbar className="text-light" variant='dark' expand="lg" style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: "auto", // Pushne footer na spodek
|
||||
flexShrink: 0 // Zabrání zmenšování při malém obsahu
|
||||
}}>
|
||||
return <Navbar className="text-light" variant='dark' expand="lg" style={{ display: "flex", justifyContent: "center" }}>
|
||||
<span>🄯 Žádná práva nevyhrazena. TODO a zdrojové kódy dostupné <a href="https://gitea.melancholik.eu/mates/Luncher">zde</a>.</span>
|
||||
</Navbar >
|
||||
}
|
||||
@ -5,10 +5,10 @@ import SettingsModal from "./modals/SettingsModal";
|
||||
import { useSettings } from "../context/settings";
|
||||
import FeaturesVotingModal from "./modals/FeaturesVotingModal";
|
||||
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
|
||||
import RefreshMenuModal from "./modals/RefreshMenuModal";
|
||||
import { useNavigate } from "react-router";
|
||||
import { STATS_URL } from "../AppRoutes";
|
||||
import { FeatureRequest, getVotes, updateVote } from "../../../types";
|
||||
import { FeatureRequest, getVotes, refreshMenu, Restaurant, updateVote } from "../../../types";
|
||||
import RefreshMenuModal from "./modals/RefreshMenuModal";
|
||||
|
||||
export default function Header() {
|
||||
const auth = useAuth();
|
||||
@ -17,7 +17,7 @@ export default function Header() {
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState<boolean>(false);
|
||||
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
|
||||
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
||||
const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState<boolean>(false);
|
||||
const [refreshModalOpen, setRefreshModalOpen] = useState<boolean>(false);
|
||||
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -40,8 +40,8 @@ export default function Header() {
|
||||
setPizzaModalOpen(false);
|
||||
}
|
||||
|
||||
const closeRefreshMenuModal = () => {
|
||||
setRefreshMenuModalOpen(false);
|
||||
const closeRefreshModal = () => {
|
||||
setRefreshModalOpen(false);
|
||||
}
|
||||
|
||||
const isValidInteger = (str: string) => {
|
||||
@ -113,6 +113,12 @@ export default function Header() {
|
||||
setFeatureVotes(votes);
|
||||
}
|
||||
|
||||
const handleRefreshMenu = async (restaurants: Restaurant[]) => {
|
||||
if (restaurants.length > 0) {
|
||||
await refreshMenu({ body: restaurants });
|
||||
}
|
||||
}
|
||||
|
||||
return <Navbar variant='dark' expand="lg">
|
||||
<Navbar.Brand href="/">Luncher</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||
@ -120,18 +126,18 @@ export default function Header() {
|
||||
<Nav className="nav">
|
||||
<NavDropdown align="end" title={auth?.login} id="basic-nav-dropdown">
|
||||
<NavDropdown.Item onClick={() => setSettingsModalOpen(true)}>Nastavení</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => setRefreshMenuModalOpen(true)}>Přenačtení menu</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => setVotingModalOpen(true)}>Hlasovat o nových funkcích</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
||||
<NavDropdown.Item onClick={() => setRefreshModalOpen(true)}>Přenačíst menu</NavDropdown.Item>
|
||||
<NavDropdown.Divider />
|
||||
<NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
<SettingsModal isOpen={settingsModalOpen} onClose={closeSettingsModal} onSave={saveSettings} />
|
||||
<RefreshMenuModal isOpen={refreshMenuModalOpen} onClose={closeRefreshMenuModal} />
|
||||
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
|
||||
<PizzaCalculatorModal isOpen={pizzaModalOpen} onClose={closePizzaModal} />
|
||||
<RefreshMenuModal isOpen={refreshModalOpen} onClose={closeRefreshModal} onSubmit={handleRefreshMenu} />
|
||||
</Navbar>
|
||||
}
|
||||
@ -28,11 +28,9 @@ export default function PizzaOrderRow({ creator, order, state, onDelete, onFeeMo
|
||||
<span key={pizzaOrder.name}>
|
||||
{`${pizzaOrder.name}, ${pizzaOrder.size} (${pizzaOrder.price} Kč)`}
|
||||
{auth?.login === order.customer && state === PizzaDayState.CREATED &&
|
||||
<span title='Odstranit'>
|
||||
<FontAwesomeIcon onClick={() => {
|
||||
onDelete(pizzaOrder);
|
||||
}} className='action-icon' icon={faTrashCan} />
|
||||
</span>
|
||||
<FontAwesomeIcon onClick={() => {
|
||||
onDelete(pizzaOrder);
|
||||
}} title='Odstranit' className='action-icon' icon={faTrashCan} />
|
||||
}
|
||||
</span>)
|
||||
.reduce((prev, curr, index) => [prev, <br key={`br-${index}`} />, curr])}
|
||||
@ -40,7 +38,7 @@ export default function PizzaOrderRow({ creator, order, state, onDelete, onFeeMo
|
||||
<td style={{ maxWidth: "200px" }}>{order.note ?? '-'}</td>
|
||||
<td style={{ maxWidth: "200px" }}>{order.fee?.price ? `${order.fee.price} Kč${order.fee.text ? ` (${order.fee.text})` : ''}` : '-'}</td>
|
||||
<td>
|
||||
{order.totalPrice} Kč{auth?.login === creator && state === PizzaDayState.CREATED && <span title='Nastavit příplatek'><FontAwesomeIcon onClick={() => { setIsFeeModalOpen(true) }} className='action-icon' icon={faMoneyBill1} /></span>}
|
||||
{order.totalPrice} Kč{auth?.login === creator && state === PizzaDayState.CREATED && <FontAwesomeIcon onClick={() => { setIsFeeModalOpen(true) }} title='Nastavit příplatek' className='action-icon' icon={faMoneyBill1} />}
|
||||
</td>
|
||||
<PizzaAdditionalFeeModal customerName={order.customer} isOpen={isFeeModalOpen} onClose={() => setIsFeeModalOpen(false)} onSave={saveFees} initialValues={{ text: order.fee?.text, price: order.fee?.price?.toString() }} />
|
||||
</>
|
||||
|
||||
@ -37,7 +37,9 @@ export default function PizzaCalculatorModal({ isOpen, onClose }: Readonly<Props
|
||||
// 1. pizza
|
||||
if (diameter1Ref.current?.value) {
|
||||
const diameter1 = parseInt(diameter1Ref.current?.value);
|
||||
r.pizza1 ??= {};
|
||||
if (!r.pizza1) {
|
||||
r.pizza1 = {};
|
||||
}
|
||||
if (diameter1 && diameter1 > 0) {
|
||||
r.pizza1.diameter = diameter1;
|
||||
r.pizza1.area = Math.PI * Math.pow(diameter1 / 2, 2);
|
||||
@ -57,7 +59,9 @@ export default function PizzaCalculatorModal({ isOpen, onClose }: Readonly<Props
|
||||
// 2. pizza
|
||||
if (diameter2Ref.current?.value) {
|
||||
const diameter2 = parseInt(diameter2Ref.current?.value);
|
||||
r.pizza2 ??= {};
|
||||
if (!r.pizza2) {
|
||||
r.pizza2 = {};
|
||||
}
|
||||
if (diameter2 && diameter2 > 0) {
|
||||
r.pizza2.diameter = diameter2;
|
||||
r.pizza2.area = Math.PI * Math.pow(diameter2 / 2, 2);
|
||||
|
||||
@ -1,105 +1,53 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { Modal, Button, Alert } from "react-bootstrap";
|
||||
import { Modal, Button, Form } from "react-bootstrap"
|
||||
import { Restaurant } from "../../../../types";
|
||||
import { getRestaurantName } from "../../enums";
|
||||
import { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
isOpen: boolean,
|
||||
onClose: () => void,
|
||||
onSubmit: (restaurants: Restaurant[]) => void,
|
||||
}
|
||||
|
||||
/** Modální dialog pro přenačtení menu z restaurací. */
|
||||
export default function RefreshMenuModal({ isOpen, onClose }: Readonly<Props>) {
|
||||
const refreshPassRef = useRef<HTMLInputElement>(null);
|
||||
const refreshTypeRef = useRef<HTMLSelectElement>(null);
|
||||
const [refreshLoading, setRefreshLoading] = useState(false);
|
||||
const [refreshMessage, setRefreshMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
||||
/** Modální dialog pro přenačtení menu jednotlivých podniků. */
|
||||
export default function RefreshMenuModal({ isOpen, onClose, onSubmit }: Readonly<Props>) {
|
||||
|
||||
const handleRefresh = async () => {
|
||||
const password = refreshPassRef.current?.value;
|
||||
const type = refreshTypeRef.current?.value;
|
||||
if (!password || !type) {
|
||||
setRefreshMessage({ type: 'error', text: 'Zadejte heslo a typ refresh.' });
|
||||
return;
|
||||
const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.currentTarget.checked) {
|
||||
setRestaurants([...restaurants, e.currentTarget.value as Restaurant]);
|
||||
} else {
|
||||
setRestaurants(restaurants.filter(restaurant => restaurant !== e.currentTarget.value as Restaurant));
|
||||
}
|
||||
}
|
||||
|
||||
setRefreshLoading(true);
|
||||
setRefreshMessage(null);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/food/refresh?type=${type}&heslo=${encodeURIComponent(password)}`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setRefreshMessage({ type: 'success', text: 'Uspesny fetch' });
|
||||
if (refreshPassRef.current) {
|
||||
// Clean hesla xd
|
||||
refreshPassRef.current.value = '';
|
||||
}
|
||||
} else {
|
||||
setRefreshMessage({ type: 'error', text: data.error || 'Chyba při obnovování jídelníčku.' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing menu:', error);
|
||||
setRefreshMessage({ type: 'error', text: 'Chyba při obnovování jídelníčku.' });
|
||||
} finally {
|
||||
setRefreshLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setRefreshMessage(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onHide={handleClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title><h2>Přenačtení menu</h2></Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Ruční refresh dat z restaurací.</p>
|
||||
|
||||
{refreshMessage && (
|
||||
<Alert variant={refreshMessage.type === 'success' ? 'success' : 'danger'}>
|
||||
{refreshMessage.text}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="mb-3">
|
||||
Heslo: <input
|
||||
ref={refreshPassRef}
|
||||
type="password"
|
||||
placeholder="Zadejte heslo"
|
||||
className="form-control d-inline-block"
|
||||
style={{ width: 'auto', marginLeft: '10px' }}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
Typ refreshe: <select
|
||||
ref={refreshTypeRef}
|
||||
className="form-select d-inline-block"
|
||||
style={{ width: 'auto', marginLeft: '10px' }}
|
||||
defaultValue="week"
|
||||
>
|
||||
<option value="week">Týden</option>
|
||||
<option value="day">Den</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="info"
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshLoading}
|
||||
className="mb-3"
|
||||
>
|
||||
{refreshLoading ? 'Refreshing...' : 'Refresh'}
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Zavřít
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
return <Modal show={isOpen} onHide={onClose} size="lg">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
Vyberte podniky k přenačtení menu
|
||||
<p style={{ fontSize: '12px' }}>Menu lze přenačíst nejdříve 15 minut od poslední aktualizace</p>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{(Object.keys(Restaurant) as Array<keyof typeof Restaurant>).map(key => {
|
||||
return <Form.Check
|
||||
key={key}
|
||||
type='checkbox'
|
||||
id={key}
|
||||
label={getRestaurantName(key as Restaurant)}
|
||||
onChange={handleChange}
|
||||
value={key}
|
||||
/>
|
||||
})}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="primary" onClick={() => onSubmit(restaurants)} disabled={restaurants.length === 0}>
|
||||
Přenačíst
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Zrušit
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
}
|
||||
@ -107,13 +107,9 @@ export default function StatsPage() {
|
||||
<div className="stats-page">
|
||||
<h1>Statistiky</h1>
|
||||
<div className="week-navigator">
|
||||
<span title="Předchozí týden">
|
||||
<FontAwesomeIcon icon={faChevronLeft} style={{ cursor: "pointer" }} onClick={handlePreviousWeek} />
|
||||
</span>
|
||||
<FontAwesomeIcon title="Předchozí týden" icon={faChevronLeft} style={{ cursor: "pointer" }} onClick={handlePreviousWeek} />
|
||||
<h2 className="date-range">{getHumanDate(dateRange[0])} - {getHumanDate(dateRange[1])}</h2>
|
||||
<span title="Následující týden">
|
||||
<FontAwesomeIcon icon={faChevronRight} style={{ cursor: "pointer" }} onClick={handleNextWeek} />
|
||||
</span>
|
||||
<FontAwesomeIcon title="Následující týden" icon={faChevronRight} style={{ cursor: "pointer" }} onClick={handleNextWeek} />
|
||||
</div>
|
||||
<LineChart width={CHART_WIDTH} height={CHART_HEIGHT} data={data}>
|
||||
{Object.values(LunchChoice).map(location => renderLine(location))}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
@ -15,12 +16,10 @@
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "bundler",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
|
||||
1466
client/yarn.lock
1466
client/yarn.lock
File diff suppressed because it is too large
Load Diff
23
run_dev.sh
23
run_dev.sh
@ -1,18 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Spustí server a klienta v samostatných panelech uvnitř stejného tmux okna.
|
||||
# Pokud už daná tmux session existuje, pouze se k ní připojí.
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
SESSION="luncher"
|
||||
|
||||
if ! tmux has-session -t $SESSION 2>/dev/null; then
|
||||
cd types && yarn openapi-ts && cd ..
|
||||
tmux new-session -d -s $SESSION
|
||||
tmux send-keys -t $SESSION:0 "cd $SCRIPT_DIR" Enter
|
||||
tmux split-window -v
|
||||
tmux send-keys -t $SESSION:0.0 "cd server && export NODE_ENV=development && yarn startReload" Enter
|
||||
tmux send-keys -t $SESSION:0.1 "cd client && yarn start" Enter
|
||||
fi
|
||||
|
||||
tmux attach-session -t $SESSION
|
||||
export NODE_ENV=development
|
||||
cd types && yarn install && yarn openapi-ts
|
||||
cd server && yarn install && yarn start &
|
||||
cd client && yarn install && yarn start &
|
||||
wait
|
||||
1
server/.gitignore
vendored
1
server/.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
/data
|
||||
/dist
|
||||
/resources/easterEggs
|
||||
/src/gen
|
||||
|
||||
@ -11,29 +11,29 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.4",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.7.1",
|
||||
"@babel/core": "^7.23.0",
|
||||
"@babel/preset-env": "^7.22.20",
|
||||
"@babel/preset-typescript": "^7.23.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/node": "^20.11.20",
|
||||
"@types/request-promise": "^4.1.48",
|
||||
"babel-jest": "^30.2.0",
|
||||
"jest": "^30.2.0",
|
||||
"nodemon": "^3.1.10",
|
||||
"babel-jest": "^29.7.0",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.12.2",
|
||||
"cheerio": "^1.1.2",
|
||||
"axios": "^1.4.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"redis": "^5.8.3",
|
||||
"redis": "^4.6.7",
|
||||
"simple-json-db": "^2.0.0",
|
||||
"socket.io": "^4.6.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,13 +9,13 @@ import { generateToken, verify } from "./auth";
|
||||
import { InsufficientPermissions } from "./utils";
|
||||
import { initWebsocket } from "./websocket";
|
||||
import pizzaDayRoutes from "./routes/pizzaDayRoutes";
|
||||
import foodRoutes, { refreshMetoda } from "./routes/foodRoutes";
|
||||
import foodRoutes from "./routes/foodRoutes";
|
||||
import votingRoutes from "./routes/votingRoutes";
|
||||
import easterEggRoutes from "./routes/easterEggRoutes";
|
||||
import statsRoutes from "./routes/statsRoutes";
|
||||
|
||||
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
||||
dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) });
|
||||
|
||||
// Validace nastavení JWT tokenu - nemá bez něj smysl vůbec povolit server spustit
|
||||
if (!process.env.JWT_SECRET) {
|
||||
@ -96,9 +96,6 @@ app.get("/api/qr", (req, res) => {
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
// Přeskočení auth pro refresh dat xd
|
||||
app.use("/api/food/refresh", refreshMetoda);
|
||||
|
||||
/** Middleware ověřující JWT token */
|
||||
app.use("/api/", (req, res, next) => {
|
||||
if (HTTP_REMOTE_USER_ENABLED) {
|
||||
|
||||
@ -6,31 +6,79 @@ const MOCK_DATA = {
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Česnečka s uzeným masem a krutony",
|
||||
name: "Kulajda",
|
||||
price: "35\xA0Kč",
|
||||
isSoup: true,
|
||||
allergens: [1, 3, 7, 9]
|
||||
},
|
||||
{
|
||||
amount: "250g",
|
||||
name: "Přírodní roštěná s jasmínovou rýží",
|
||||
name: "Kuřecí křidélka s vařeným bramborem",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 9, 10]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Noky s kuřecím masem a sýrovou omáčkou",
|
||||
name: "Hovězí hamburger s BBQ omáčkou a hranolky",
|
||||
price: "145\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Kuřecí stehno pečené na Moravance s feferony, bramborový knedlík",
|
||||
name: "Frankfurtská hovězí pečeně s jasmínovou rýží",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Hovězí vývar s kapáním",
|
||||
price: "35\xA0Kč",
|
||||
isSoup: true,
|
||||
},
|
||||
{
|
||||
amount: "200g",
|
||||
name: "Smažený karbanátek s bramborovou kaší",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Vepřová plec na smetaně s kynutým knedlíkem",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Trhané kachní maso se zeleninovým kuskusem",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Zelná polévka s klobásou",
|
||||
price: "35\xA0Kč",
|
||||
isSoup: true,
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Hovězí na česneku s bramborovým knedlíkem",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
},
|
||||
{
|
||||
amount: "250g",
|
||||
name: "Přírodní holandský řízek s bramborovou kaší, rajčatový salát",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
},
|
||||
{
|
||||
amount: "350g",
|
||||
name: "Bagel s vinnou klobásou, cibulový konfit, kysané zelí, slanina a hořčicová mayo, hranolky, curry omáčka",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7, 9]
|
||||
}
|
||||
],
|
||||
[
|
||||
@ -39,118 +87,50 @@ const MOCK_DATA = {
|
||||
name: "Kuřecí vývar s nudlemi",
|
||||
price: "35\xA0Kč",
|
||||
isSoup: true,
|
||||
allergens: [1, 3, 7, 9]
|
||||
},
|
||||
{
|
||||
amount: "200g",
|
||||
name: "Hovězí maso v rajské omáčce s kynutým knedlíkem",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Krůtí roláda se sušenými rajčaty , mozzarellou a bramborovou kaší",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 7]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Telecí játra na grilu, restované brambory, tatarská omáčka , polníčkový salát",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [3, 7]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Zeleninová polévka",
|
||||
price: "35\xA0Kč",
|
||||
isSoup: true,
|
||||
allergens: [3, 9]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Smažené rybí filé s vařeným bramborem, tatarka",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [3, 7]
|
||||
},
|
||||
{
|
||||
amount: "250g",
|
||||
name: "Vepřové výpečky se špenátem, bramborový knedlík 1,3,7",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "350g",
|
||||
name: "Kuřecí řízek \" Ondráš \" , kysané zelí",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [3, 7]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Hovězí vývar s játrovými knedlíčky",
|
||||
price: "35\xA0Kč",
|
||||
isSoup: true,
|
||||
allergens: [1, 3]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Merguez klobáska, bílé fazole na kyselo, sázené vejce a vídeňská cibulka",
|
||||
name: "Kovbojské fazole s klobásou a chlebem",
|
||||
price: "125\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Kuřecí steak s liškovou omáčkou a opečený brambor",
|
||||
name: "Kuřecí rarášci s vařeným bramborem",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 7]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Kaťák – vepřové kostky s feferonou, cibulí, kečupem ,česnekem, smažené krokety",
|
||||
name: "Hovězí pečeně na slanině s jasmínovou rýží",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [3, 7]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
amount: "0,25l",
|
||||
name: "Čočková polévka",
|
||||
name: "Dršťková polévka",
|
||||
price: "35\xA0Kč",
|
||||
isSoup: true,
|
||||
allergens: [9, 12]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Ovocné knedlíky s tvarohem",
|
||||
name: "Tortilla s kuřecím masem, čedarem, zeleninou a papričkami jalapeňos",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Smažený vepřový řízek s bramborovým salátem",
|
||||
name: "Segedínský guláš s kynutým knedlíkem",
|
||||
price: "135\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7, 9, 10]
|
||||
},
|
||||
{
|
||||
amount: "150g",
|
||||
name: "Znojemský hovězí guláš s jasmínovou rýží",
|
||||
name: "Filet z krůtích prsou, omáčka z modrého sýra, pečené brambory",
|
||||
price: "145\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 9]
|
||||
}
|
||||
]
|
||||
],
|
||||
@ -290,185 +270,161 @@ const MOCK_DATA = {
|
||||
[
|
||||
{
|
||||
amount: "-",
|
||||
name: "Batátový krém s chilli a kokosovým mlékem",
|
||||
name: "Uzený vývar s kapustou",
|
||||
price: "40\xA0Kč",
|
||||
isSoup: true,
|
||||
allergens: [1, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Kuřecí stehno na paprice, knedlík",
|
||||
name: "Čočka na kyselo, opečená klobása, okurka, chléb",
|
||||
price: "130\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Těstoviny se sušenými rajčaty a cuketou, parmezán",
|
||||
name: "Smažená brokolice, brambory, tatarská omáčka",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Quesadilla s trhaným vepřovým masem, salát coleslaw, hranolky",
|
||||
name: "Uzený vepřový bůček, bramborové pyré",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Smažený kuřecí řízek v sezamové strouhance, vařené brambory, wasabi majonéza",
|
||||
name: "Kuřecí medailonky v sýrové omáčce, hranolky",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
amount: "-",
|
||||
name: "Ovarová",
|
||||
name: "Slepičí s nudlemi",
|
||||
price: "40\xA0Kč",
|
||||
isSoup: true,
|
||||
allergens: [1]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Zapečené těstoviny s uzeným masem, okurka",
|
||||
name: "Zvěřinový guláš, knedlík",
|
||||
price: "130\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Cheddarové kuličky s jalapeños, máslové brambory, tatarská omáčka",
|
||||
name: "Čínské nudle se zeleninou a vejcem",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Steak z krkovice s miso omáčkou, jasmínová rýže",
|
||||
name: "Jitrnice/jelito, brambory, zelný salát s křenem, hořčice",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 6, 11]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Kuřecí supreme s bramborovo-mrkvovým pyré, restovaná cuketa",
|
||||
name: "Vídeňská roštěná se smaženou cibulkou, jasmínová rýže",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [7]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
amount: "-",
|
||||
name: "Hovězí s hráškem a rýží",
|
||||
name: "Dýňový krém se smetanou",
|
||||
price: "40\xA0Kč",
|
||||
isSoup: true,
|
||||
allergens: [9]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Rizoto s kuřecím masem a zeleninou, okurka, sýr",
|
||||
name: "Kuřecí směs se zeleninou, rýže",
|
||||
price: "130\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [7, 9]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Smažené rýžové nudle Pad thai s arašídy, zeleninou a vejcem",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 6, 8, 11]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Vykoštěné vepřové koleno s křenem a hořčicí, chléb",
|
||||
name: "Tvarohové knedlíky s meruňkami, strouhaný tvaroh, máslo, cukr",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Gordon bleu, hranolky, pikantní dip",
|
||||
name: "Ovar, křen, hořčice, pečivo",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Telecí holandský řízek s uzeným sýrem, bramborové pyré",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
amount: "-",
|
||||
name: "Dýňová",
|
||||
name: "Zeleninová s jáhly",
|
||||
price: "40\xA0Kč",
|
||||
isSoup: true,
|
||||
allergens: [1, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Uzená plec, křenová omáčka, knedlík",
|
||||
name: "Rizoto s vepřovým masem, okurka",
|
||||
price: "130\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Palačinky s marmeládou přelité čokoládou, sypané cukrem",
|
||||
name: "Tortellini s parmezánovou omáčkou",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Smažený holandský řízek s bramborovou kaší a nakládanou zeleninou",
|
||||
name: "Pečený prejt, brambory, zelný salát",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Kuřecí jatýrka na smetaně s čerstvou majoránkou, jasmínová rýže",
|
||||
name: "Chobotnice na grilu, grilovaná zelenina, bylinková bageta",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [7]
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
amount: "-",
|
||||
name: "Hovězí vývar s játrovými knedlíčky",
|
||||
name: "Fazolová s uzeninou",
|
||||
price: "40\xA0Kč",
|
||||
isSoup: true,
|
||||
allergens: [1, 3, 7, 9]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Kuřecí Kung-pao, jasmínová rýže",
|
||||
name: "Krůtí perkelt, těstoviny",
|
||||
price: "130\xA0Kč",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 5, 6]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Sýrové tortelliny s pažitkovou omáčkou",
|
||||
name: "Grilovaný hermelín, bulgurový salát se zeleninou",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Teriyaki losos burger s frisée salátem a citrusovou majonézou, bramborové lupínky",
|
||||
name: "Zabijačkový guláš, karlovarský knedlík",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 6, 7, 11]
|
||||
},
|
||||
{
|
||||
amount: "-",
|
||||
name: "Vepřové výpečky s červeným zelím, bramborové knedlíky se smaženou cibulkou",
|
||||
name: "Vepřový plátek na žampionech, jasmínová rýže",
|
||||
price: "na\xA0váhu",
|
||||
isSoup: false,
|
||||
allergens: [1, 3, 7]
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
@ -8,6 +8,49 @@ import { NotifikaceData, NotifikaceInput } from '../../types';
|
||||
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
||||
|
||||
// const gotifyDataRaw = process.env.GOTIFY_SERVERS_AND_KEYS || "{}";
|
||||
// const gotifyData: GotifyServer[] = JSON.parse(gotifyDataRaw);
|
||||
// export const gotifyCall = async (data: NotififaceInput, gotifyServers?: GotifyServer[]): Promise<any[]> => {
|
||||
// if (!Array.isArray(gotifyServers)) {
|
||||
// return []
|
||||
// }
|
||||
// const urls = gotifyServers.flatMap(gotifyServer =>
|
||||
// gotifyServer.api_keys.map(apiKey => `${gotifyServer.server}/message?token=${apiKey}`));
|
||||
//
|
||||
// const dataPayload = {
|
||||
// title: "Luncher",
|
||||
// message: `${data.udalost} - spustil:${data.user}`,
|
||||
// priority: 7,
|
||||
// };
|
||||
//
|
||||
// const headers = { "Content-Type": "application/json" };
|
||||
//
|
||||
// const promises = urls.map(url =>
|
||||
// axios.post(url, dataPayload, { headers }).then(response => {
|
||||
// response.data = {
|
||||
// success: true,
|
||||
// message: "Notifikace doručena",
|
||||
// };
|
||||
// return response;
|
||||
// }).catch(error => {
|
||||
// if (axios.isAxiosError(error)) {
|
||||
// const axiosError = error as AxiosError;
|
||||
// if (axiosError.response) {
|
||||
// axiosError.response.data = {
|
||||
// success: false,
|
||||
// message: "fail",
|
||||
// };
|
||||
// console.log(error)
|
||||
// return axiosError.response;
|
||||
// }
|
||||
// }
|
||||
// // Handle unknown error without a response
|
||||
// console.log(error, "unknown error");
|
||||
// })
|
||||
// );
|
||||
// return promises;
|
||||
// };
|
||||
|
||||
export const ntfyCall = async (data: NotifikaceInput) => {
|
||||
const url = process.env.NTFY_HOST
|
||||
const username = process.env.NTFY_USERNAME;
|
||||
|
||||
@ -96,7 +96,9 @@ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize
|
||||
totalPrice: 0,
|
||||
hasQr: false,
|
||||
}
|
||||
clientData.pizzaDay.orders ??= [];
|
||||
if (!clientData.pizzaDay.orders) {
|
||||
clientData.pizzaDay.orders = [];
|
||||
}
|
||||
clientData.pizzaDay.orders.push(order);
|
||||
}
|
||||
const pizzaOrder: PizzaVariant = {
|
||||
@ -105,7 +107,9 @@ export async function addPizzaOrder(login: string, pizza: Pizza, size: PizzaSize
|
||||
size: size.size,
|
||||
price: size.price,
|
||||
}
|
||||
order.pizzaList ??= [];
|
||||
if (!order.pizzaList) {
|
||||
order.pizzaList = [];
|
||||
}
|
||||
order.pizzaList.push(pizzaOrder);
|
||||
order.totalPrice += pizzaOrder.price;
|
||||
await storage.setData(today, clientData);
|
||||
|
||||
@ -23,7 +23,7 @@ const SOUP_NAMES = [
|
||||
const DAYS_IN_WEEK = ['pondělí', 'úterý', 'středa', 'čtvrtek', 'pátek', 'sobota', 'neděle'];
|
||||
|
||||
// URL na týdenní menu jednotlivých restaurací
|
||||
const SLADOVNICKA_URL = 'https://sladovnicka.unasplzenchutna.cz/cz/#denni-nabidka';
|
||||
const SLADOVNICKA_URL = 'https://sladovnicka.unasplzenchutna.cz/cz/denni-nabidka';
|
||||
const U_MOTLIKU_URL = 'https://www.umotliku.cz/menu';
|
||||
const TECHTOWER_URL = 'https://www.equifarm.cz/restaurace-techtower';
|
||||
const ZASTAVKAUMICHALA_URL = 'https://www.zastavkaumichala.cz';
|
||||
@ -53,28 +53,6 @@ const sanitizeText = (text: string): string => {
|
||||
return text.replace('\t', '').replace(' , ', ', ').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsuje čísla alergenů z názvu jídla a vrací vyčištěný název spolu s polem alergenů.
|
||||
* Alergeny jsou očekávány na konci názvu ve formátu číslic oddělených čárkami.
|
||||
*
|
||||
* @param name původní název jídla
|
||||
* @returns objekt obsahující vyčištěný název a pole alergenů
|
||||
*/
|
||||
const parseAllergens = (name: string): { cleanName: string, allergens: number[] } => {
|
||||
// Regex pro nalezení čísel na konci řetězce oddělených čárkami a případnými mezerami
|
||||
const regex = /\s+(\d+(?:\s*,\s*\d+)*)\s*$/;
|
||||
const match = regex.exec(name);
|
||||
|
||||
if (match) {
|
||||
const allergenString = match[1];
|
||||
const allergens = allergenString.split(',').map(num => parseInt(num.trim(), 10)).filter(num => !isNaN(num));
|
||||
const cleanName = name.replace(regex, '').trim();
|
||||
return { cleanName, allergens };
|
||||
}
|
||||
|
||||
return { cleanName: name, allergens: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Stáhne a vrátí aktuální HTML z dané URL.
|
||||
*
|
||||
@ -100,69 +78,81 @@ export const getMenuSladovnicka = async (firstDayOfWeek: Date, mock: boolean = f
|
||||
const html = await getHtml(SLADOVNICKA_URL);
|
||||
const $ = load(html);
|
||||
|
||||
const menuContentElements = $('#daily-menu-content-list').children('[id^="daily-menu-content-"]');
|
||||
// Prozatím předpokládáme, že budou mít vždy elementy pro všech 5 dní v týdnu, i pokud bude zavřeno
|
||||
if (menuContentElements.length < 5) {
|
||||
throw Error("Neočekávaný počet dní v menu Sladovnické: " + menuContentElements.length + ", očekáváno 5 (možná je některý den zavřeno?)");
|
||||
}
|
||||
|
||||
const list = $('ul.tab-links').children();
|
||||
const result: Food[][] = [];
|
||||
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
||||
const dayChildren = $(menuContentElements[dayIndex]).children();
|
||||
// Prozatím předpokládáme, že budou mít vždy polévku a hlavní jídla
|
||||
if (dayChildren.length < 2) {
|
||||
throw Error("Neočekávaný počet children v menu Sladovnické pro den " + dayIndex + ": " + dayChildren.length + ", očekávány alespoň 2 (polévka a hlavní jídlo)");
|
||||
const currentDate = new Date(firstDayOfWeek);
|
||||
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
|
||||
const searchedDayText = `${currentDate.getDate()}.${currentDate.getMonth() + 1}.${capitalize(DAYS_IN_WEEK[dayIndex])}`;
|
||||
// Najdeme index pro vstupní datum (např. při svátcích bude posunutý)
|
||||
// TODO validovat, že vstupní datum je v aktuálním týdnu
|
||||
// TODO tenhle způsob je zbytečně komplikovaný - stačilo by hledat rovnou v div.tab-content, protože každý den tam má datum taky (akorát je print-only)
|
||||
let index = undefined;
|
||||
list.each((i, dayRow) => {
|
||||
const rowText = $(dayRow).first().text().trim();
|
||||
if (rowText === searchedDayText) {
|
||||
index = i;
|
||||
}
|
||||
})
|
||||
if (index === undefined) {
|
||||
// Pravděpodobně svátek, nebo je zavřeno
|
||||
result[dayIndex] = [{
|
||||
amount: undefined,
|
||||
name: "Pro daný den nebyla nalezena denní nabídka",
|
||||
price: "",
|
||||
isSoup: false,
|
||||
}];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parsování polévky
|
||||
const soupElement = dayChildren.get(0);
|
||||
const soupTable = $(soupElement).find('table tbody tr');
|
||||
const soupCells = soupTable.children('td');
|
||||
// Dle dohledaného indexu najdeme správný tabpanel
|
||||
const rows = $('div.tab-content').children();
|
||||
if (index >= rows.length) {
|
||||
throw Error("V HTML nebyl nalezen řádek menu pro index " + index);
|
||||
}
|
||||
const tabPanel = $(rows.get(index));
|
||||
|
||||
// Opětovná validace, že daný tabpanel je pro vstupní datum
|
||||
const headers = tabPanel.find('h2');
|
||||
if (headers.length !== 3) {
|
||||
throw Error("Neočekávaný počet elementů h2 v menu pro datum " + searchedDayText + ", očekávány 3, ale nalezeno bylo " + headers.length);
|
||||
}
|
||||
const dayText = $(headers.get(0)).text().trim();
|
||||
if (dayText !== searchedDayText) {
|
||||
throw Error("Neočekávaný datum na řádce nalezeného dne: '" + dayText + "', ale očekáváno bylo '" + searchedDayText + "'");
|
||||
}
|
||||
|
||||
// V tabpanelu očekáváme dvě tabulky - pro polévku a pro hlavní jídlo
|
||||
const tables = tabPanel.find('table');
|
||||
if (tables.length !== 2) {
|
||||
throw Error("Neočekávaný počet tabulek na řádce nalezeného dne: " + tables.length + ", ale očekávány byly 2");
|
||||
}
|
||||
const currentDayFood: Food[] = [];
|
||||
// Polévka - div -> table -> tbody -> tr -> 3x td
|
||||
const soupCells = $(tables.get(0)).children().first().children().first().children();
|
||||
if (soupCells.length !== 3) {
|
||||
throw Error("Neočekávaný počet buněk v tabulce polévky: " + soupCells.length + ", ale očekávány byly 3");
|
||||
}
|
||||
|
||||
const soupAmount = sanitizeText($(soupCells.get(0)).text());
|
||||
const soupNameRaw = sanitizeText($(soupCells.get(1)).text());
|
||||
const soupPrice = sanitizeText($(soupCells.get(2)).text().replace(' ', '\xA0'));
|
||||
const soupParsed = parseAllergens(soupNameRaw);
|
||||
|
||||
// Parsování hlavních jídel
|
||||
const mainCourseElement = dayChildren.get(1);
|
||||
const mainCourseTable = $(mainCourseElement).find('table tbody');
|
||||
const mainCourseRows = mainCourseTable.children('tr');
|
||||
|
||||
const currentDayFood: Food[] = [];
|
||||
|
||||
// Přidáme polévku do seznamu jídel
|
||||
currentDayFood.push({
|
||||
amount: soupAmount,
|
||||
name: soupParsed.cleanName,
|
||||
price: soupPrice,
|
||||
amount: sanitizeText($(soupCells.get(0)).text()),
|
||||
name: sanitizeText($(soupCells.get(1)).text()),
|
||||
price: sanitizeText($(soupCells.get(2)).text().replace(' ', '\xA0')),
|
||||
isSoup: true,
|
||||
allergens: soupParsed.allergens.length > 0 ? soupParsed.allergens : undefined,
|
||||
});
|
||||
|
||||
// Projdeme všechny řádky hlavních jídel
|
||||
mainCourseRows.each((i, row) => {
|
||||
const cells = $(row).children('td');
|
||||
const amount = sanitizeText($(cells.get(0)).text());
|
||||
const nameRaw = sanitizeText($(cells.get(1)).text());
|
||||
const price = sanitizeText($(cells.get(2)).text().replace(' ', '\xA0'));
|
||||
const parsed = parseAllergens(nameRaw);
|
||||
|
||||
// Přeskočíme prázdné řádky (první řádek může být prázdný)
|
||||
if (parsed.cleanName.trim().length > 0) {
|
||||
currentDayFood.push({
|
||||
amount,
|
||||
name: parsed.cleanName,
|
||||
price,
|
||||
isSoup: false,
|
||||
allergens: parsed.allergens.length > 0 ? parsed.allergens : undefined,
|
||||
});
|
||||
// Hlavní jídla - div -> table -> tbody -> 3x tr
|
||||
const mainCourseRows = $(tables.get(1)).children().first().children();
|
||||
mainCourseRows.each((i, foodRow) => {
|
||||
const foodCells = $(foodRow).children();
|
||||
if (foodCells.length !== 3) {
|
||||
throw Error("Neočekávaný počet buněk v řádku jídla: " + foodCells.length + ", ale očekávány byly 3");
|
||||
}
|
||||
});
|
||||
|
||||
currentDayFood.push({
|
||||
amount: sanitizeText($(foodCells.get(0)).text()),
|
||||
name: sanitizeText($(foodCells.get(1)).text()),
|
||||
price: sanitizeText($(foodCells.get(2)).text().replace(' ', '\xA0')),
|
||||
isSoup: false,
|
||||
});
|
||||
})
|
||||
result[dayIndex] = currentDayFood;
|
||||
}
|
||||
return result;
|
||||
@ -301,7 +291,7 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
|
||||
|
||||
const result: Food[][] = [];
|
||||
// TODO validovat, že v textu nalezeného <font> je rozsah, do kterého spadá vstupní datum
|
||||
const siblings = secondTry ? $(font).parent().parent().parent().siblings('p') : $(font).parent().parent().siblings();
|
||||
const siblings = secondTry ? $(font).parent().siblings() : $(font).parent().parent().siblings();
|
||||
let parsing = false;
|
||||
let currentDayIndex = 0;
|
||||
for (let i = 0; i < siblings.length; i++) {
|
||||
@ -319,25 +309,21 @@ export const getMenuTechTower = async (firstDayOfWeek: Date, mock: boolean = fal
|
||||
continue;
|
||||
}
|
||||
let price = 'na\xA0váhu';
|
||||
let nameRaw = text.replace('•', '');
|
||||
let name = text.replace('•', '');
|
||||
if (text.toLowerCase().endsWith('kč')) {
|
||||
const tmp = text.replace('\xA0', ' ').split(' ');
|
||||
const split = [tmp.slice(0, -2).join(' ')].concat(tmp.slice(-2));
|
||||
price = `${split.slice(1)[0]}\xA0Kč`
|
||||
nameRaw = split[0].replace('•', '');
|
||||
name = split[0].replace('•', '');
|
||||
}
|
||||
if (nameRaw.endsWith('–')) {
|
||||
nameRaw = nameRaw.slice(0, -1).trim();
|
||||
if (result[currentDayIndex] == null) {
|
||||
result[currentDayIndex] = [];
|
||||
}
|
||||
|
||||
const parsed = parseAllergens(nameRaw);
|
||||
result[currentDayIndex] ??= [];
|
||||
result[currentDayIndex].push({
|
||||
amount: '-',
|
||||
name: parsed.cleanName,
|
||||
name,
|
||||
price,
|
||||
isSoup: isTextSoupName(parsed.cleanName),
|
||||
allergens: parsed.allergens.length > 0 ? parsed.allergens : undefined,
|
||||
isSoup: isTextSoupName(name),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -356,8 +342,7 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
||||
return getMenuZastavkaUmichalaMock();
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0,0,0,0);
|
||||
const nowDate = new Date().getDate();
|
||||
const headers = {
|
||||
"Cookie": "_nss=1; PHPSESSID=9e37de17e0326b0942613d6e67a30e69",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
|
||||
@ -366,8 +351,8 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
||||
for (let dayIndex = 0; dayIndex < 5; dayIndex++) {
|
||||
const currentDate = new Date(firstDayOfWeek);
|
||||
currentDate.setDate(firstDayOfWeek.getDate() + dayIndex);
|
||||
currentDate.setHours(0,0,0,0);
|
||||
if (currentDate < today || (currentDate.getTime() === today.getTime() && new Date().getHours() >= 14)) {
|
||||
|
||||
if (currentDate.getDate() < nowDate || (currentDate.getDate() === nowDate && new Date().getHours() >= 14)) {
|
||||
result[dayIndex] = [{
|
||||
amount: undefined,
|
||||
name: "Pro tento den není uveřejněna nabídka jídel",
|
||||
@ -375,9 +360,8 @@ export const getMenuZastavkaUmichala = async (firstDayOfWeek: Date, mock: boolea
|
||||
isSoup: false,
|
||||
}];
|
||||
} else {
|
||||
const url = (currentDate.getTime() === today.getTime())
|
||||
? ZASTAVKAUMICHALA_URL
|
||||
: ZASTAVKAUMICHALA_URL + '/?do=dailyMenu-changeDate&dailyMenu-dateString=' + formatDate(currentDate, 'DD.MM.YYYY');
|
||||
const url = (currentDate.getDate() === nowDate) ?
|
||||
ZASTAVKAUMICHALA_URL : ZASTAVKAUMICHALA_URL + '/?do=dailyMenu-changeDate&dailyMenu-dateString=' + formatDate(currentDate, 'DD.MM.YYYY');
|
||||
const html = await axios.get(url, {
|
||||
headers,
|
||||
}).then(res => res.data).then(content => content);
|
||||
@ -417,13 +401,11 @@ export const getMenuSenkSerikova = async (firstDayOfWeek: Date, mock: boolean =
|
||||
}).then(res => decoder.decode(new Uint8Array(res.data))).then(content => content);
|
||||
const $ = load(html);
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0,0,0,0);
|
||||
const nowDate = new Date().getDate();
|
||||
const currentDate = new Date(firstDayOfWeek);
|
||||
const result: Food[][] = [];
|
||||
let dayIndex = 0;
|
||||
currentDate.setHours(0,0,0,0);
|
||||
while (currentDate < today) {
|
||||
while (currentDate.getDate() < nowDate) {
|
||||
result[dayIndex] = [{
|
||||
amount: undefined,
|
||||
name: "Pro tento den není uveřejněna nabídka jídel",
|
||||
@ -437,11 +419,9 @@ export const getMenuSenkSerikova = async (firstDayOfWeek: Date, mock: boolean =
|
||||
$('.menicka').each((i, element) => {
|
||||
const currentDayFood: Food[] = [];
|
||||
$(element).find('.popup-gallery li').each((j, element) => {
|
||||
const rawName = $(element).children('div.polozka').text();
|
||||
const nameWithoutNumber = rawName.replace(/^\d+\.\s*/, '');
|
||||
currentDayFood.push({
|
||||
amount: '-',
|
||||
name: nameWithoutNumber,
|
||||
name: $(element).children('div.polozka').text(),
|
||||
price: $(element).children('div.cena').text().replace(/ /g, '\xA0'),
|
||||
isSoup: $(element).hasClass('polevka'),
|
||||
});
|
||||
|
||||
@ -5,7 +5,7 @@ import path from "path";
|
||||
import fs from "fs";
|
||||
import { EasterEgg } from "../../../types/gen/types.gen";
|
||||
|
||||
const EASTER_EGGS_JSON_PATH = path.join(__dirname, "../../resources/.easter-eggs.json");
|
||||
const EASTER_EGGS_JSON_PATH = path.join(__dirname, "../../.easter-eggs.json");
|
||||
const IMAGES_PATH = '../../resources/easterEggs';
|
||||
|
||||
type EasterEggsJson = {
|
||||
|
||||
@ -1,51 +1,13 @@
|
||||
import express, { Request, Response } from "express";
|
||||
import express, { Request } from "express";
|
||||
import { getLogin, getTrusted } from "../auth";
|
||||
import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote, fetchRestaurantWeekMenuData, saveRestaurantWeekMenu } from "../service";
|
||||
import { getDayOfWeekIndex, parseToken, getFirstWorkDayOfWeek } from "../utils";
|
||||
import { addChoice, getDateForWeekIndex, getRestaurantMenu, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service";
|
||||
import { getDayOfWeekIndex, parseToken } from "../utils";
|
||||
import { getWebsocket } from "../websocket";
|
||||
import { callNotifikace } from "../notifikace";
|
||||
import { AddChoiceData, ChangeDepartureTimeData, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen";
|
||||
|
||||
|
||||
// RateLimit na refresh endpoint
|
||||
interface RateLimitEntry {
|
||||
count: number;
|
||||
resetTime: number;
|
||||
}
|
||||
const rateLimits: Record<string, RateLimitEntry> = {};
|
||||
const RATE_LIMIT = 1; // maximální počet požadavků za minutu
|
||||
const RATE_LIMIT_WINDOW = 30 * 60 * 1000; // je to v ms (x * 1min)
|
||||
|
||||
// Kontrola ratelimitu
|
||||
function checkRateLimit(key: string, limit: number = RATE_LIMIT): boolean {
|
||||
const now = Date.now();
|
||||
|
||||
// Vyčištění starých záznamů
|
||||
Object.keys(rateLimits).forEach(k => {
|
||||
if (rateLimits[k].resetTime < now) {
|
||||
delete rateLimits[k];
|
||||
}
|
||||
});
|
||||
|
||||
// Kontrola, že záznam existuje a platí
|
||||
if (rateLimits[key] && rateLimits[key].resetTime > now) {
|
||||
// Záznam platí a kontroluje se limit
|
||||
if (rateLimits[key].count >= limit) {
|
||||
return false; // Překročen limit
|
||||
}
|
||||
|
||||
// ++ xd
|
||||
rateLimits[key].count++;
|
||||
return true;
|
||||
} else {
|
||||
// + klic
|
||||
rateLimits[key] = {
|
||||
count: 1,
|
||||
resetTime: now + RATE_LIMIT_WINDOW
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/** Po jak dlouhé době (v minutách) lze provést nové načtení menu. */
|
||||
const MENU_REFRESH_INTERVAL = 15;
|
||||
|
||||
/**
|
||||
* Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň
|
||||
@ -182,85 +144,25 @@ router.post("/jdemeObed", async (req, res, next) => {
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
// /api/food/refresh?type=week&heslo=docasnyheslo
|
||||
export const refreshMetoda = async (req: Request, res: Response) => {
|
||||
const { type, heslo } = req.query as { type?: string; heslo?: string };
|
||||
if (heslo !== "docasnyheslo" && heslo !== "tohleheslopavelnesmizjistit123") {
|
||||
return res.status(403).json({ error: "Neplatné heslo" });
|
||||
}
|
||||
if (!checkRateLimit("refresh") && heslo !== "tohleheslopavelnesmizjistit123") {
|
||||
return res.status(429).json({ error: "Refresh už se zavolal, chvíli počkej :))" });
|
||||
}
|
||||
if (type !== "week" && type !== "day") {
|
||||
return res.status(400).json({ error: "Neznámý typ refresh" });
|
||||
}
|
||||
if (type === "day") {
|
||||
return res.status(400).json({ error: "ještě neumim TODO..." });
|
||||
router.post("/refreshMenu", async (req, res, next) => {
|
||||
if (!req.body || !Array.isArray(req.body)) {
|
||||
return res.status(400).json({ error: "Neplatný požadavek" });
|
||||
}
|
||||
try {
|
||||
// Pro všechny restaurace refreshni menu na aktuální týden
|
||||
const restaurants = ["SLADOVNICKA", "TECHTOWER", "ZASTAVKAUMICHALA", "SENKSERIKOVA"] as const;
|
||||
const firstDay = getFirstWorkDayOfWeek(getToday());
|
||||
const results: Record<string, any> = {};
|
||||
const successfulRestaurants: string[] = [];
|
||||
const failedRestaurants: string[] = [];
|
||||
|
||||
// Nejdříve načíst všechna data bez ukládání
|
||||
for (const rest of restaurants) {
|
||||
try {
|
||||
const weekData = await fetchRestaurantWeekMenuData(rest, firstDay);
|
||||
results[rest] = weekData;
|
||||
|
||||
// Kontrola validity dat
|
||||
if (weekData && weekData.length > 0 &&
|
||||
weekData.some(dayMenu => dayMenu && dayMenu.length > 0)) {
|
||||
successfulRestaurants.push(rest);
|
||||
} else {
|
||||
failedRestaurants.push(rest);
|
||||
results[rest] = { error: "Žádná validní data" };
|
||||
const now = new Date();
|
||||
for (const restaurant of req.body) {
|
||||
// TODO tohle je technicky špatně, protože pokud aktuálně jídla načtená nejsou, tak je toto volání načte a následně je to načte znovu kvůli force!
|
||||
const menu = await getRestaurantMenu(restaurant);
|
||||
if (menu.lastUpdate != null) {
|
||||
const minutes = (now.getTime() - menu.lastUpdate) / 1000 / 60;
|
||||
if (minutes < MENU_REFRESH_INTERVAL) {
|
||||
throw Error(`Podnik ${restaurant} byl přenačtený před ${Math.round(minutes)} minutami. Nové přenačtení lze provést nejdříve za ${Math.round(MENU_REFRESH_INTERVAL - minutes)} minut.`);
|
||||
}
|
||||
} catch (error) {
|
||||
failedRestaurants.push(rest);
|
||||
results[rest] = { error: `Chyba při načítání: ${error}` };
|
||||
}
|
||||
await getRestaurantMenu(restaurant, undefined, true);
|
||||
}
|
||||
|
||||
// Pokud se nepodařilo načíst žádnou restauraci
|
||||
if (successfulRestaurants.length === 0) {
|
||||
return res.status(400).json({
|
||||
error: "Nepodařilo se získat validní data z žádné restaurace",
|
||||
failed: failedRestaurants,
|
||||
results: results
|
||||
});
|
||||
}
|
||||
|
||||
// Uložit pouze validní data
|
||||
for (const rest of successfulRestaurants) {
|
||||
try {
|
||||
await saveRestaurantWeekMenu(rest as any, firstDay, results[rest]);
|
||||
} catch (error) {
|
||||
console.error(`Chyba při ukládání dat pro ${rest}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Připravit odpověď
|
||||
const response: any = {
|
||||
ok: true,
|
||||
refreshed: results,
|
||||
successful: successfulRestaurants
|
||||
};
|
||||
|
||||
if (failedRestaurants.length > 0) {
|
||||
response.warning = `Nepodařilo se načíst: ${failedRestaurants.join(', ')}`;
|
||||
response.failed = failedRestaurants;
|
||||
}
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (e: any) {
|
||||
res.status(500).json({ error: e?.message || "Chyba při refreshi" });
|
||||
}
|
||||
}
|
||||
router.get("/refresh", refreshMetoda);
|
||||
|
||||
res.status(200).json({});
|
||||
} catch (e: any) { next(e) }
|
||||
});
|
||||
|
||||
export default router;
|
||||
@ -76,107 +76,15 @@ async function getMenu(date: Date): Promise<WeekMenu | undefined> {
|
||||
}
|
||||
|
||||
// TODO přesun do restaurants.ts
|
||||
/**
|
||||
* Načte menu dané restaurace pro celý týden bez ukládání do storage.
|
||||
* Používá se pro validaci dat před uložením.
|
||||
*
|
||||
* @param restaurant restaurace
|
||||
* @param firstDay první pracovní den týdne
|
||||
* @returns pole menu pro jednotlivé dny týdne
|
||||
*/
|
||||
export async function fetchRestaurantWeekMenuData(restaurant: Restaurant, firstDay: Date): Promise<any[]> {
|
||||
return await fetchRestaurantWeekMenu(restaurant, firstDay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uloží týdenní menu restaurace do storage.
|
||||
*
|
||||
* @param restaurant restaurace
|
||||
* @param date datum z týdne, pro který ukládat menu
|
||||
* @param weekData data týdenního menu
|
||||
*/
|
||||
export async function saveRestaurantWeekMenu(restaurant: Restaurant, date: Date, weekData: any[]): Promise<void> {
|
||||
const now = new Date().getTime();
|
||||
let weekMenu = await getMenu(date);
|
||||
weekMenu ??= [{}, {}, {}, {}, {}];
|
||||
|
||||
// Inicializace struktury pro restauraci
|
||||
for (let i = 0; i < 5; i++) {
|
||||
weekMenu[i] ??= {};
|
||||
weekMenu[i][restaurant] ??= {
|
||||
lastUpdate: now,
|
||||
closed: false,
|
||||
food: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Uložení dat pro všechny dny
|
||||
for (let i = 0; i < weekData.length && i < weekMenu.length; i++) {
|
||||
weekMenu[i][restaurant]!.food = weekData[i];
|
||||
weekMenu[i][restaurant]!.lastUpdate = now;
|
||||
|
||||
// Detekce uzavření pro každou restauraci
|
||||
switch (restaurant) {
|
||||
case 'SLADOVNICKA':
|
||||
if (weekData[i].length === 1 && weekData[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
break;
|
||||
case 'TECHTOWER':
|
||||
if (weekData[i]?.length === 1 && weekData[i][0].name.toLowerCase() === 'svátek') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
break;
|
||||
case 'ZASTAVKAUMICHALA':
|
||||
if (weekData[i]?.length === 1 && weekData[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
break;
|
||||
case 'SENKSERIKOVA':
|
||||
if (weekData[i]?.length === 1 && weekData[i][0].name === 'Pro tento den nebylo zadáno menu.') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Uložení do storage
|
||||
await storage.setData(getMenuKey(date), weekMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Načte menu dané restaurace pro celý týden bez ukládání do storage.
|
||||
*
|
||||
* @param restaurant restaurace
|
||||
* @param firstDay první pracovní den týdne
|
||||
* @returns pole menu pro jednotlivé dny týdne
|
||||
*/
|
||||
async function fetchRestaurantWeekMenu(restaurant: Restaurant, firstDay: Date): Promise<any[]> {
|
||||
const mock = process.env.MOCK_DATA === 'true';
|
||||
|
||||
switch (restaurant) {
|
||||
case 'SLADOVNICKA':
|
||||
return await getMenuSladovnicka(firstDay, mock);
|
||||
case 'TECHTOWER':
|
||||
return await getMenuTechTower(firstDay, mock);
|
||||
case 'ZASTAVKAUMICHALA':
|
||||
return await getMenuZastavkaUmichala(firstDay, mock);
|
||||
case 'SENKSERIKOVA':
|
||||
return await getMenuSenkSerikova(firstDay, mock);
|
||||
default:
|
||||
throw new Error(`Nepodporovaná restaurace: ${restaurant}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrátí menu dané restaurace pro předaný den.
|
||||
* Pokud neexistuje, provede stažení menu pro příslušný týden a uložení do DB.
|
||||
* Pokud neexistuje nebo je nastaven příznak force, provede stažení menu pro příslušný týden a uložení do DB.
|
||||
*
|
||||
* @param restaurant restaurace
|
||||
* @param date datum, ke kterému získat menu
|
||||
* @param forceRefresh příznak vynuceného obnovení
|
||||
* @param force Příznak, zda znovu získat aktuální menu i v případě, že je již načteno. Pokud není předán, provede se načtení pouze v případě, že menu aktuálně nemáme. Pokud je true, provede nové načtení. Pokud je false, neprovede se nové načtení ani v případě, že menu aktuálně nemáme.
|
||||
*/
|
||||
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, forceRefresh = false): Promise<RestaurantDayMenu> {
|
||||
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, force?: boolean): Promise<RestaurantDayMenu> {
|
||||
const usedDate = date ?? getToday();
|
||||
const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
|
||||
const now = new Date().getTime();
|
||||
@ -189,56 +97,94 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, for
|
||||
}
|
||||
|
||||
let weekMenu = await getMenu(usedDate);
|
||||
weekMenu ??= [{}, {}, {}, {}, {}];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
weekMenu[i] ??= {};
|
||||
weekMenu[i][restaurant] ??= {
|
||||
lastUpdate: now,
|
||||
closed: false,
|
||||
food: [],
|
||||
};
|
||||
if (weekMenu == null) {
|
||||
weekMenu = [{}, {}, {}, {}, {}];
|
||||
}
|
||||
if (forceRefresh || (!weekMenu[dayOfWeekIndex][restaurant]?.food?.length && !weekMenu[dayOfWeekIndex][restaurant]?.closed)) {
|
||||
const firstDay = getFirstWorkDayOfWeek(usedDate);
|
||||
|
||||
try {
|
||||
const restaurantWeekFood = await fetchRestaurantWeekMenu(restaurant, firstDay);
|
||||
|
||||
// Aktualizace menu pro všechny dny
|
||||
for (let i = 0; i < restaurantWeekFood.length && i < weekMenu.length; i++) {
|
||||
weekMenu[i][restaurant]!.food = restaurantWeekFood[i];
|
||||
weekMenu[i][restaurant]!.lastUpdate = now;
|
||||
|
||||
// Detekce uzavření pro každou restauraci
|
||||
switch (restaurant) {
|
||||
case 'SLADOVNICKA':
|
||||
if (restaurantWeekFood[i].length === 1 && restaurantWeekFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
break;
|
||||
case 'TECHTOWER':
|
||||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name.toLowerCase() === 'svátek') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
break;
|
||||
case 'ZASTAVKAUMICHALA':
|
||||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
break;
|
||||
case 'SENKSERIKOVA':
|
||||
if (restaurantWeekFood[i]?.length === 1 && restaurantWeekFood[i][0].name === 'Pro tento den nebylo zadáno menu.') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Uložení do storage
|
||||
await storage.setData(getMenuKey(usedDate), weekMenu);
|
||||
} catch (e: any) {
|
||||
console.error(`Selhalo načtení jídel pro podnik ${restaurant}`, e);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (weekMenu[i] == null) {
|
||||
weekMenu[i] = {};
|
||||
}
|
||||
if (weekMenu[i][restaurant] == null) {
|
||||
weekMenu[i][restaurant] = {
|
||||
lastUpdate: now,
|
||||
closed: false,
|
||||
food: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ((!weekMenu[dayOfWeekIndex][restaurant]?.food?.length && force === undefined) || force) {
|
||||
const firstDay = getFirstWorkDayOfWeek(usedDate);
|
||||
const mock = process.env.MOCK_DATA === 'true';
|
||||
switch (restaurant) {
|
||||
case 'SLADOVNICKA':
|
||||
try {
|
||||
const sladovnickaFood = await getMenuSladovnicka(firstDay, mock);
|
||||
for (let i = 0; i < sladovnickaFood.length; i++) {
|
||||
weekMenu[i][restaurant]!.food = sladovnickaFood[i];
|
||||
// Velice chatrný a nespolehlivý způsob detekce uzavření...
|
||||
if (sladovnickaFood[i].length === 1 && sladovnickaFood[i][0].name.toLowerCase() === 'pro daný den nebyla nalezena denní nabídka') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Selhalo načtení jídel pro podnik Sladovnická", e);
|
||||
}
|
||||
break;
|
||||
// case 'UMOTLIKU':
|
||||
// try {
|
||||
// const uMotlikuFood = await getMenuUMotliku(firstDay, mock);
|
||||
// for (let i = 0; i < uMotlikuFood.length; i++) {
|
||||
// menus[i][restaurant]!.food = uMotlikuFood[i];
|
||||
// if (uMotlikuFood[i].length === 1 && uMotlikuFood[i][0].name.toLowerCase() === 'zavřeno') {
|
||||
// menus[i][restaurant]!.closed = true;
|
||||
// }
|
||||
// }
|
||||
// } catch (e: any) {
|
||||
// console.error("Selhalo načtení jídel pro podnik U Motlíků", e);
|
||||
// }
|
||||
// break;
|
||||
case 'TECHTOWER':
|
||||
try {
|
||||
const techTowerFood = await getMenuTechTower(firstDay, mock);
|
||||
for (let i = 0; i < techTowerFood.length; i++) {
|
||||
weekMenu[i][restaurant]!.food = techTowerFood[i];
|
||||
if (techTowerFood[i]?.length === 1 && techTowerFood[i][0].name.toLowerCase() === 'svátek') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Selhalo načtení jídel pro podnik TechTower", e);
|
||||
}
|
||||
break;
|
||||
case 'ZASTAVKAUMICHALA':
|
||||
try {
|
||||
const zastavkaUmichalaFood = await getMenuZastavkaUmichala(firstDay, mock);
|
||||
for (let i = 0; i < zastavkaUmichalaFood.length; i++) {
|
||||
weekMenu[i][restaurant]!.food = zastavkaUmichalaFood[i];
|
||||
if (zastavkaUmichalaFood[i]?.length === 1 && zastavkaUmichalaFood[i][0].name === 'Pro tento den není uveřejněna nabídka jídel.') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Selhalo načtení jídel pro podnik Zastávka u Michala", e);
|
||||
}
|
||||
break;
|
||||
case 'SENKSERIKOVA':
|
||||
try {
|
||||
const senkSerikovaFood = await getMenuSenkSerikova(firstDay, mock);
|
||||
for (let i = 0; i < senkSerikovaFood.length; i++) {
|
||||
weekMenu[i][restaurant]!.food = senkSerikovaFood[i];
|
||||
if (senkSerikovaFood[i]?.length === 1 && senkSerikovaFood[i][0].name === 'Pro tento den nebylo zadáno menu.') {
|
||||
weekMenu[i][restaurant]!.closed = true;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Selhalo načtení jídel pro podnik Pivovarský šenk Šeříková", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
await storage.setData(getMenuKey(usedDate), weekMenu);
|
||||
}
|
||||
return weekMenu[dayOfWeekIndex][restaurant]!;
|
||||
}
|
||||
@ -381,7 +327,9 @@ export async function addChoice(login: string, trusted: boolean, locationKey: Lu
|
||||
removeChoiceIfPresent(login, usedDate, locationKey);
|
||||
}
|
||||
// TODO vytáhnout inicializaci "prázdné struktury" do vlastní funkce
|
||||
data.choices[locationKey] ??= {};
|
||||
if (!(data.choices[locationKey])) {
|
||||
data.choices[locationKey] = {}
|
||||
}
|
||||
if (!(login in data.choices[locationKey])) {
|
||||
if (!data.choices[locationKey]) {
|
||||
data.choices[locationKey] = {}
|
||||
|
||||
@ -5,12 +5,6 @@
|
||||
* Postupem času lze předělat pro efektivnější využití Redis.
|
||||
*/
|
||||
export interface StorageInterface {
|
||||
|
||||
/**
|
||||
* Inicializuje úložiště, pokud je potřeba (např. připojení k databázi).
|
||||
*/
|
||||
initialize?(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Vrátí příznak, zda existují data pro předaný klíč.
|
||||
* @param key klíč, pro který zjišťujeme data (typicky datum)
|
||||
|
||||
@ -5,7 +5,7 @@ import JsonStorage from "./json";
|
||||
import RedisStorage from "./redis";
|
||||
|
||||
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
||||
dotenv.config({ path: path.resolve(__dirname, `../../.env.${ENVIRONMENT}`) });
|
||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
||||
|
||||
const JSON_KEY = 'json';
|
||||
const REDIS_KEY = 'redis';
|
||||
@ -19,13 +19,6 @@ if (!process.env.STORAGE || process.env.STORAGE?.toLowerCase() === JSON_KEY) {
|
||||
throw Error("Nepodporovaná hodnota proměnné STORAGE: " + process.env.STORAGE + ", podporované jsou 'json' nebo 'redis'");
|
||||
}
|
||||
|
||||
(async () => {
|
||||
if (storage.initialize) {
|
||||
await storage.initialize();
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
export default function getStorage(): StorageInterface {
|
||||
return storage;
|
||||
}
|
||||
|
||||
@ -11,9 +11,6 @@ export default class RedisStorage implements StorageInterface {
|
||||
const HOST = process.env.REDIS_HOST ?? 'localhost';
|
||||
const PORT = process.env.REDIS_PORT ?? 6379;
|
||||
client = createClient({ url: `redis://${HOST}:${PORT}` });
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
client.connect();
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,9 @@ export async function getUserVotes(login: string) {
|
||||
*/
|
||||
export async function updateFeatureVote(login: string, option: FeatureRequest, active: boolean): Promise<VotingData> {
|
||||
let data = await storage.getData<VotingData>(STORAGE_KEY);
|
||||
data ??= {};
|
||||
if (data == null) {
|
||||
data = {};
|
||||
}
|
||||
if (!(login in data)) {
|
||||
data[login] = [];
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { DefaultEventsMap, Server } from "socket.io";
|
||||
import { Server } from "socket.io";
|
||||
import { DefaultEventsMap } from "socket.io/dist/typed-events";
|
||||
|
||||
let io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>;
|
||||
|
||||
|
||||
@ -4,11 +4,10 @@
|
||||
"../types/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "node16",
|
||||
"target": "ES2016",
|
||||
"module": "CommonJS",
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "../",
|
||||
|
||||
3572
server/yarn.lock
3572
server/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -26,6 +26,8 @@ paths:
|
||||
$ref: "./paths/food/changeDepartureTime.yml"
|
||||
/food/jdemeObed:
|
||||
$ref: "./paths/food/jdemeObed.yml"
|
||||
/food/refreshMenu:
|
||||
$ref: "./paths/food/refreshMenu.yml"
|
||||
|
||||
# Pizza day (/api/pizzaDay)
|
||||
/pizzaDay/create:
|
||||
|
||||
@ -6,6 +6,6 @@
|
||||
"devDependencies": {
|
||||
"@hey-api/client-fetch": "^0.8.2",
|
||||
"@hey-api/openapi-ts": "^0.64.7",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
15
types/paths/food/refreshMenu.yml
Normal file
15
types/paths/food/refreshMenu.yml
Normal file
@ -0,0 +1,15 @@
|
||||
post:
|
||||
operationId: refreshMenu
|
||||
summary: Přenačtení menu vybraných podniků
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "../../schemas/_index.yml#/Restaurant"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Menu bylo přenačteno
|
||||
@ -151,12 +151,6 @@ Food:
|
||||
isSoup:
|
||||
description: Příznak, zda se jedná o polévku
|
||||
type: boolean
|
||||
allergens:
|
||||
description: Seznam čísel alergenů obsažených v jídle
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
example: [1, 3, 7]
|
||||
RestaurantDayMenu:
|
||||
description: Menu restaurace na konkrétní den
|
||||
type: object
|
||||
|
||||
@ -283,10 +283,10 @@ tinyexec@^0.3.2:
|
||||
resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2"
|
||||
integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
|
||||
|
||||
typescript@^5.9.3:
|
||||
version "5.9.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
||||
typescript@^5.0.2:
|
||||
version "5.8.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
|
||||
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
|
||||
|
||||
ufo@^1.5.4:
|
||||
version "1.5.4"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user