2 Commits
Author SHA1 Message Date
Daniel-Garmig d6a9ac8d7c Search Page - Initial Implementation.
- Added components.
- Query API to get availability for selected period.
2024-05-05 22:45:15 +02:00
Daniel-Garmig e81660914f Homepage - Initial implementation. 2024-05-05 12:45:59 +02:00
25 changed files with 1222 additions and 184 deletions
+3
View File
@@ -0,0 +1,3 @@
# App config Variables (Will be visible by client):
VITE_BACKEND_HOST=https://somniaapi.example.org/
VITE_BACKEND_PORT=443
+213 -152
View File
@@ -15,6 +15,7 @@
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -25,6 +26,7 @@
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.23.0", "react-router-dom": "^6.23.0",
"swr": "^2.2.5",
"tailwind-merge": "^2.3.0" "tailwind-merge": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
@@ -46,15 +48,6 @@
"vite": "^5.2.0" "vite": "^5.2.0"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
"integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -1286,6 +1279,40 @@
} }
} }
}, },
"node_modules/@radix-ui/react-toast": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz",
"integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.5",
"@radix-ui/react-portal": "1.0.4",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1",
"@radix-ui/react-visually-hidden": "1.0.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": { "node_modules/@radix-ui/react-tooltip": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
@@ -1483,9 +1510,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz",
"integrity": "sha512-1ACInKIT0pXmTYuPoJAL8sOT0lV3PEACFSVxnD03hGIojJ1CmbzZmLJyk2xew+yxqTlmx7xydkiJcBzdp0V+AQ==", "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1496,9 +1523,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz",
"integrity": "sha512-vGl+Bny8cawCM7ExugzqEB8ke3t7Pm9/mo+ciA9kJh6pMuNyM+31qhewMwHwseDZ/LtdW0SCocW1CsMxcq1Lsg==", "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1509,9 +1536,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz",
"integrity": "sha512-Lj8J9WzQRvfWO4GfI+bBkIThUFV1PtI+es/YH/3cwUQ+edXu8Mre0JRJfRrAeRjPiHDPFFZaX51zfgHHEhgRAg==", "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1522,9 +1549,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz",
"integrity": "sha512-NPPOXMTIWJk50lgZmRReEYJFvLG5rgMDzaVauWNB2MgFQYm9HuNXQdVVg3iEZ3A5StIzxhMlPjVyS5fsv4PJmg==", "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1535,9 +1562,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz",
"integrity": "sha512-ij4tv1XtWcDScaTgoMnvDEYZ2Wjl2ZhDFEyftjBKu6sNNLHIkKuXBol/bVSh+md5zSJ6em9hUXyPO3cVPCsl4Q==", "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1548,9 +1575,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz",
"integrity": "sha512-MTMAl30dzcfYB+smHe1sJuS2P1/hB8pqylkCe0/8/Lo8CADjy/eM8x43nBoR5eqcYgpOtCh7IgHpvqSMAE38xw==", "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1561,9 +1588,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz",
"integrity": "sha512-vY3fAg6JLDoNh781HHHMPvt8K6RWG3OmEj3xI9BOFSQTD5PNaGKvCB815MyGlDnFYUw7lH+WvvQqoBwLtRDR1A==", "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1574,9 +1601,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz",
"integrity": "sha512-61SpQGBSb8QkfV/hUYWezlEig4ro55t8NcE5wWmy1bqRsRVHCEDkF534d+Lln/YeLUoSWtJHvvG3bx9lH/S6uA==", "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1587,9 +1614,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz",
"integrity": "sha512-4XGexJthsNhEEgv/zK4/NnAOjYKoeCsIoT+GkqTY2u3rse0lbJ8ft1bpDCdlkvifsLDL2uwe4fn8PLR4IMTKQQ==", "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -1600,9 +1627,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz",
"integrity": "sha512-/pArXjqnEdhbQ1qe4CTTlJ6/GjWGdWNRucKAp4fqKnKf7QC0BES3QEV34ACumHHQ4uEGt4GctF2ISCMRhkli0A==", "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -1613,9 +1640,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz",
"integrity": "sha512-vu4f3Y8iwjtRfSZdmtP8nC1jmRx1IrRVo2cLQlQfpFZ0e2AE9YbPgfIzpuK+i3C4zFETaLLNGezbBns2NuS/uA==", "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -1626,9 +1653,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz",
"integrity": "sha512-n4HEgIJulNSmAKT3SYF/1wuzf9od14woSBseNkzur7a+KJIbh2Jb+J9KIsdGt3jJnsLW0BT1Sj6MiwL4Zzku6Q==", "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1639,9 +1666,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz",
"integrity": "sha512-guO/4N1884ig2AzTKPc6qA7OTnFMUEg/X2wiesywRO1eRD7FzHiaiTQQOLFmnUXWj2pgQXIT1g5g3e2RpezXcQ==", "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1652,9 +1679,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz",
"integrity": "sha512-+rxD3memdkhGz0NhNqbYHXBoA33MoHBK4uubZjF1IeQv1Psi6tqgsCcC6vwQjxBM1qoCqOQQBy0cgNbbZKnGUg==", "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1665,9 +1692,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz",
"integrity": "sha512-0NxVbLhBXmwANWWbgZY/RdSkeuHEgF+u8Dc0qBowUVBYsR2y2vwVGjKgUcj1wtu3jpjs057io5g9HAPr3Icqjg==", "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -1678,9 +1705,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz",
"integrity": "sha512-hutnZavtOx/G4uVdgoZz5279By9NVbgmxOmGGgnzUjZYuwp2+NzGq6KXQmHXBWz7W/vottXn38QmKYAdQLa/vQ==", "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1937,9 +1964,9 @@
"devOptional": true "devOptional": true
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.2.79", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz",
"integrity": "sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==", "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
@@ -1947,9 +1974,9 @@
} }
}, },
"node_modules/@types/react-dom": { "node_modules/@types/react-dom": {
"version": "18.2.25", "version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.25.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/react": "*" "@types/react": "*"
@@ -1983,16 +2010,16 @@
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.7.1", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz",
"integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.7.1", "@typescript-eslint/scope-manager": "7.8.0",
"@typescript-eslint/type-utils": "7.7.1", "@typescript-eslint/type-utils": "7.8.0",
"@typescript-eslint/utils": "7.7.1", "@typescript-eslint/utils": "7.8.0",
"@typescript-eslint/visitor-keys": "7.7.1", "@typescript-eslint/visitor-keys": "7.8.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
@@ -2018,15 +2045,15 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.7.1", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz",
"integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.7.1", "@typescript-eslint/scope-manager": "7.8.0",
"@typescript-eslint/types": "7.7.1", "@typescript-eslint/types": "7.8.0",
"@typescript-eslint/typescript-estree": "7.7.1", "@typescript-eslint/typescript-estree": "7.8.0",
"@typescript-eslint/visitor-keys": "7.7.1", "@typescript-eslint/visitor-keys": "7.8.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -2046,13 +2073,13 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.7.1", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz",
"integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.7.1", "@typescript-eslint/types": "7.8.0",
"@typescript-eslint/visitor-keys": "7.7.1" "@typescript-eslint/visitor-keys": "7.8.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -2063,13 +2090,13 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.7.1", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz",
"integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.7.1", "@typescript-eslint/typescript-estree": "7.8.0",
"@typescript-eslint/utils": "7.7.1", "@typescript-eslint/utils": "7.8.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -2090,9 +2117,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.7.1", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz",
"integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -2103,13 +2130,13 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.7.1", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz",
"integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.7.1", "@typescript-eslint/types": "7.8.0",
"@typescript-eslint/visitor-keys": "7.7.1", "@typescript-eslint/visitor-keys": "7.8.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -2131,17 +2158,17 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.7.1", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz",
"integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@typescript-eslint/scope-manager": "7.7.1", "@typescript-eslint/scope-manager": "7.8.0",
"@typescript-eslint/types": "7.7.1", "@typescript-eslint/types": "7.8.0",
"@typescript-eslint/typescript-estree": "7.7.1", "@typescript-eslint/typescript-estree": "7.8.0",
"semver": "^7.6.0" "semver": "^7.6.0"
}, },
"engines": { "engines": {
@@ -2156,12 +2183,12 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.7.1", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz",
"integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.7.1", "@typescript-eslint/types": "7.8.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -2519,6 +2546,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -2672,9 +2704,9 @@
"dev": true "dev": true
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.754", "version": "1.4.756",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.754.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.756.tgz",
"integrity": "sha512-7Kr5jUdns5rL/M9wFFmMZAgFDuL2YOnanFH4OI4iFzUqyh3XOL7nAGbSlSMZdzKMIyyTpNSbqZsWG9odwLeKvA==", "integrity": "sha512-RJKZ9+vEBMeiPAvKNWyZjuYyUqMndcP1f335oHqn3BEQbs2NFtVrnK5+6Xg5wSM9TknNNpWghGDUCKGYF+xWXw==",
"dev": true "dev": true
}, },
"node_modules/embla-carousel": { "node_modules/embla-carousel": {
@@ -2823,9 +2855,9 @@
} }
}, },
"node_modules/eslint-plugin-react-hooks": { "node_modules/eslint-plugin-react-hooks": {
"version": "4.6.0", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@@ -3675,17 +3707,17 @@
} }
}, },
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.3", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3", "deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6", "fast-levenshtein": "^2.0.6",
"levn": "^0.4.1", "levn": "^0.4.1",
"prelude-ls": "^1.2.1", "prelude-ls": "^1.2.1",
"type-check": "^0.4.0" "type-check": "^0.4.0",
"word-wrap": "^1.2.5"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
@@ -4024,9 +4056,9 @@
] ]
}, },
"node_modules/react": { "node_modules/react": {
"version": "18.2.0", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
}, },
@@ -4048,15 +4080,15 @@
} }
}, },
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "18.2.0", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.0" "scheduler": "^0.23.2"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^18.2.0" "react": "^18.3.1"
} }
}, },
"node_modules/react-remove-scroll": { "node_modules/react-remove-scroll": {
@@ -4234,9 +4266,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.16.3", "version": "4.17.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.3.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz",
"integrity": "sha512-Ygm4fFO4usWcAG3Ud36Lmif5nudoi0X6QPLC+kRgrRjulAbmFkaTawP7fTIkRDnCNSf/4IAQzXM1T8e691kRtw==", "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/estree": "1.0.5" "@types/estree": "1.0.5"
@@ -4249,22 +4281,22 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.16.3", "@rollup/rollup-android-arm-eabi": "4.17.2",
"@rollup/rollup-android-arm64": "4.16.3", "@rollup/rollup-android-arm64": "4.17.2",
"@rollup/rollup-darwin-arm64": "4.16.3", "@rollup/rollup-darwin-arm64": "4.17.2",
"@rollup/rollup-darwin-x64": "4.16.3", "@rollup/rollup-darwin-x64": "4.17.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.16.3", "@rollup/rollup-linux-arm-gnueabihf": "4.17.2",
"@rollup/rollup-linux-arm-musleabihf": "4.16.3", "@rollup/rollup-linux-arm-musleabihf": "4.17.2",
"@rollup/rollup-linux-arm64-gnu": "4.16.3", "@rollup/rollup-linux-arm64-gnu": "4.17.2",
"@rollup/rollup-linux-arm64-musl": "4.16.3", "@rollup/rollup-linux-arm64-musl": "4.17.2",
"@rollup/rollup-linux-powerpc64le-gnu": "4.16.3", "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2",
"@rollup/rollup-linux-riscv64-gnu": "4.16.3", "@rollup/rollup-linux-riscv64-gnu": "4.17.2",
"@rollup/rollup-linux-s390x-gnu": "4.16.3", "@rollup/rollup-linux-s390x-gnu": "4.17.2",
"@rollup/rollup-linux-x64-gnu": "4.16.3", "@rollup/rollup-linux-x64-gnu": "4.17.2",
"@rollup/rollup-linux-x64-musl": "4.16.3", "@rollup/rollup-linux-x64-musl": "4.17.2",
"@rollup/rollup-win32-arm64-msvc": "4.16.3", "@rollup/rollup-win32-arm64-msvc": "4.17.2",
"@rollup/rollup-win32-ia32-msvc": "4.16.3", "@rollup/rollup-win32-ia32-msvc": "4.17.2",
"@rollup/rollup-win32-x64-msvc": "4.16.3", "@rollup/rollup-win32-x64-msvc": "4.17.2",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -4292,9 +4324,9 @@
} }
}, },
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.23.0", "version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
} }
@@ -4535,6 +4567,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/swr": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz",
"integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
"dependencies": {
"client-only": "^0.0.1",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/tailwind-merge": { "node_modules/tailwind-merge": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz",
@@ -4699,9 +4743,9 @@
"dev": true "dev": true
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.0.14", "version": "1.0.15",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz",
"integrity": "sha512-JixKH8GR2pWYshIPUg/NujK3JO7JiqEEUiNArE86NQyrgUuZeTlZQN3xuS/yiV5Kb48ev9K6RqNkaJjXsdg7Jw==", "integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -4778,6 +4822,14 @@
} }
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -4785,9 +4837,9 @@
"dev": true "dev": true
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.2.10", "version": "5.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
"integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==", "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.20.1", "esbuild": "^0.20.1",
@@ -4854,6 +4906,15 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+2
View File
@@ -17,6 +17,7 @@
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -27,6 +28,7 @@
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.23.0", "react-router-dom": "^6.23.0",
"swr": "^2.2.5",
"tailwind-merge": "^2.3.0" "tailwind-merge": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
+25
View File
@@ -0,0 +1,25 @@
import { RoomCategory } from "@/model/RoomCategory";
import { fetcher, fetcherUnwraper } from "./backend";
import useSWR from 'swr';
export function useAPIRoomCategories() {
const { data, error, isLoading } = useSWR<RoomCategory[]>('/api/room/category', fetcherUnwraper)
return {
roomCategoryList: data,
isLoading,
isError: error
}
}
export function useAPIRoomCategoryId(categoryId: number) {
const url = categoryId ? '/api/room/category/' + categoryId : null;
const { data, error, isLoading } = useSWR<RoomCategory>(url, fetcher)
return {
roomCategory: data,
isLoading,
isError: error
}
}
+18
View File
@@ -0,0 +1,18 @@
import { fetcherUnwraperWithFilter } from "./backend";
import useSWR from 'swr';
import { AvailabilityFilter, AvailabilityRequest, RoomAvailabity } from "@/model/RoomSearch";
export function useAPIRoomAvailability(filter: AvailabilityFilter | null) {
const request: AvailabilityRequest | null = filter ? {
filter: filter
} : null;
const { data, isLoading, error } = useSWR(request ? ['/api/search/availability', request] : null, ([url, request]) => fetcherUnwraperWithFilter<RoomAvailabity[]>(url, request))
return {
roomAvailabilityList: data,
isLoading,
isError: error
}
}
+66
View File
@@ -0,0 +1,66 @@
const BACKEND_HOST = import.meta.env.VITE_BACKEND_HOST;
const BACKEND_PORT = import.meta.env.VITE_BACKEND_PORT;
const url = BACKEND_HOST + (BACKEND_PORT == 443 || BACKEND_PORT == 80 ? "" : ":" + BACKEND_PORT)
export function fetcher<T>(endpoint: string): Promise<T> {
return fetch(url + endpoint, {
method: "GET"
})
.then(r => {
if (!r.ok) {
throw new Error(r.statusText)
}
const data = r.json();
return data as Promise<T>;
})
}
export function fetcherUnwraper<T>(endpoint: string): Promise<T> {
return fetch(url + endpoint)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText)
}
return response.json() as Promise<{ results: T }>
})
.then(data => {
return data.results
})
}
export function fetcherWithFilter<T>(endpoint: string, body: NonNullable<unknown>): Promise<T> {
return fetch(url + endpoint, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
})
.then(response => {
if (!response.ok) {
throw new Error(response.statusText)
}
return response.json() as Promise<T>
})
}
export function fetcherUnwraperWithFilter<T>(endpoint: string, body: NonNullable<unknown>): Promise<T> {
return fetch(url + endpoint, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
})
.then(response => {
if (!response.ok) {
throw new Error(response.statusText)
}
return response.json() as Promise<{ results: T }>
})
.then(data => {
return data.results
})
}
+83
View File
@@ -0,0 +1,83 @@
import { useAPIRoomCategoryId } from "@/api/api-roomCategories"
import { Card, CardContent, CardHeader } from "../ui/card"
import { useState } from "react";
import { Typography } from "../ui/typography";
import { Button } from "../ui/button";
import { RoomCategoryPhoto } from "@/model/RoomCategory";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
import { Skeleton } from "../ui/skeleton";
function RoomBooking({ roomCategoryId, availableAmount }: { roomCategoryId: number, availableAmount: number }) {
const [selectedAmount, setSelectedAmount] = useState<number>(0);
const { roomCategory, isLoading: isLoadingCategory, isError: isErrorCategory } = useAPIRoomCategoryId(roomCategoryId);
//TODOME: Gestionar también los estados al cargar los precios.
const isLoading = isLoadingCategory;
const isError = isErrorCategory;
if (isLoading) {
return (
<Skeleton className="w-[600px] h-[150px] mx-auto" />
);
}
if (isError) {
return (
<Typography variant={"h3"} as="h3">Sorry, there was an error loading data.</Typography>
);
}
const mainPhoto: RoomCategoryPhoto | undefined = roomCategory?.roomCategoryPhotos.filter((p) => p.kind == "main")[0];
const photoElement = mainPhoto && (
<img src={mainPhoto.path} alt={mainPhoto.altText} className="rounded-md mx-0 px-0"></img>
);
return (
<Card>
<CardHeader>
<div className="flex flex-row gap-5 w-full">
<div className="w-[40%] flex-none">
{photoElement}
</div>
<div>
<Typography variant={"h3"} as="h3">{roomCategory?.name}</Typography>
<Typography >{roomCategory?.description}</Typography>
</div>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Guest number</TableHead>
<TableHead>Price</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>1</TableCell>
<TableCell>25€</TableCell>
<TableCell>
<div className="flex flex-row">
<Button
onClick={() => setSelectedAmount(selectedAmount - 1)}>-</Button>
<p className="mx-2">{selectedAmount}</p>
<Button
onClick={() => setSelectedAmount(selectedAmount + 1)}>+</Button>
</div></TableCell>
</TableRow>
</TableBody>
</Table>
<div>
<p>Available: {availableAmount}</p>
<p>Selected: {selectedAmount}</p>
</div>
</CardContent>
</Card>
);
}
export default RoomBooking
+39 -8
View File
@@ -2,30 +2,61 @@ import { Separator } from "../ui/separator"
import { Card, CardContent, CardHeader } from "../ui/card" import { Card, CardContent, CardHeader } from "../ui/card"
import { Typography } from "../ui/typography" import { Typography } from "../ui/typography"
import { BedDoubleIcon, BedSingleIcon, UserIcon } from "lucide-react" import { BedDoubleIcon, BedSingleIcon, UserIcon } from "lucide-react"
import { RoomCategory, RoomCategoryPhoto } from "@/model/RoomCategory"
import { IconWithTooltipNumber } from "../ui/icon-number-tooltip"
export function RoomItemCard() { interface RoomItemCardProps {
room: RoomCategory
}
const defaultRoom: RoomCategory = {
id: -1,
name: "Sample Room",
description: "Lorem Ipsum",
bedType: "0,1",
maxGuestNum: 2,
roomCategoryDetails: [],
roomCategoryPhotos: []
}
export function RoomItemCard({ room = defaultRoom }: RoomItemCardProps) {
const mainPhoto: RoomCategoryPhoto | undefined = room.roomCategoryPhotos.find((element) => {
return element.kind = "main"
});
const bedTypeList: number[] = room.bedType.split(",").map((e) => Number.parseInt(e));
const hasSingleBed: boolean = (bedTypeList.at(0) !== undefined && bedTypeList[0] > 0);
const hasDoubleBed: boolean = (bedTypeList.at(1) !== undefined && bedTypeList[1] > 0);
return ( return (
<Card> <Card id={room.id.toString()}>
<CardHeader className="p-0 m-0 mb-5"> <CardHeader className="p-0 m-0 mb-5">
<img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39" className="rounded-md"></img> <img src={mainPhoto?.path ?? "https://images.unsplash.com/photo-1618773928121-c32242e63f39"} className="rounded-md"></img>
<Typography variant={"h2"} as="h1" className="mx-5 py-2">Doble con baño</Typography> <Typography variant={"h2"} as="h1" className="mx-5 py-2">{room.name}</Typography>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Typography variant={"p"} as="p"> <Typography variant={"p"} as="p">
Habitación doble con <strong>baño privado</strong>. {room.description}
</Typography> </Typography>
<Separator className="my-4" /> <Separator className="my-4" />
<div className="flex justify-around"> <div className="flex justify-around">
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<Typography variant={"p_inline"}>2</Typography> {hasSingleBed && (
<IconWithTooltipNumber altText="Single bed" amount={bedTypeList[0]}>
<BedSingleIcon /> <BedSingleIcon />
<Typography variant={"p_inline"}>1</Typography> </IconWithTooltipNumber>
)}
{hasDoubleBed && (
<>
<IconWithTooltipNumber altText="Double bed" amount={bedTypeList[1]}>
<BedDoubleIcon /> <BedDoubleIcon />
</IconWithTooltipNumber>
</>
)}
</div> </div>
<div className="flex flex-row"> <div className="flex flex-row">
<UserIcon /> <UserIcon />
<Typography variant={"p_inline"} className="ml-3">3</Typography> <Typography variant={"p_inline"} className="ml-3">{room.maxGuestNum}</Typography>
</div> </div>
</div> </div>
</CardContent> </CardContent>
+52 -5
View File
@@ -2,11 +2,58 @@ import { Separator } from "../ui/separator"
import { Card, CardContent, CardHeader } from "../ui/card" import { Card, CardContent, CardHeader } from "../ui/card"
import { Typography } from "../ui/typography" import { Typography } from "../ui/typography"
import 'react-day-picker/dist/style.css'; import 'react-day-picker/dist/style.css';
import { DatePickerWithRange } from "../ui/DateRangePicker"; import { DatePickerWithRange } from "../ui/dateRange-picker";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { SearchIcon } from "lucide-react"; import { SearchIcon } from "lucide-react";
import { useState } from "react";
import { SearchFilter } from "@/model/SearchFilter";
import { DateRange } from "react-day-picker";
import { useToast } from "../ui/use-toast";
export function SearchFilterCard() { type funOnSearchClick = (newFilter: SearchFilter) => void;
export function SearchFilterCard({ onSearchClick }: { onSearchClick?: funOnSearchClick }) {
const { toast } = useToast();
const [searchFilter, setSearchFilter] = useState<SearchFilter>({
dateRange: { from: new Date() }
})
// Validate and call callback on btSearch click.
//TODOME: Add data validations and show messages.
const btSearchClick = () => {
if (!searchFilter || !searchFilter.dateRange) {
toast({
variant: "destructive",
title: "Uh oh! Invalid Search.",
});
return;
}
if (!searchFilter.dateRange.from || !searchFilter.dateRange.to) {
toast({
variant: "destructive",
title: "Uh oh! Invalid Search.",
description: "You should select a 'from' and 'to' dates"
})
return;
}
if (onSearchClick) {
onSearchClick(searchFilter);
}
}
// Handle Element updates.
const updateDateRange = (newRange: DateRange) => {
setSearchFilter({
...searchFilter,
dateRange: newRange,
});
console.log("SearchFilterCard filter UPDATED");
}
return ( return (
<Card> <Card>
@@ -14,10 +61,10 @@ export function SearchFilterCard() {
<Typography variant={"h2"} as="h1" className="mx-5 py-2">Reserva ahora</Typography> <Typography variant={"h2"} as="h1" className="mx-5 py-2">Reserva ahora</Typography>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<DatePickerWithRange></DatePickerWithRange> <DatePickerWithRange updateDateRange={updateDateRange}></DatePickerWithRange>
<Separator className="my-4" /> <Separator className="my-4" />
<Button size={"lg"}> <Button size={"lg"} onClick={btSearchClick}>
<SearchIcon className="text-primary-foreground me-2"/> <SearchIcon className="text-primary-foreground me-2" />
<Typography variant={"h3"} className="text-primary-foreground">Buscar</Typography> <Typography variant={"h3"} className="text-primary-foreground">Buscar</Typography>
</Button> </Button>
</CardContent> </CardContent>
@@ -0,0 +1,19 @@
function BackendConnectionStatus({ isLoading, isError }: { isLoading?: boolean, isError?: boolean }) {
const BACKEND_HOST = import.meta.env.VITE_BACKEND_HOST;
const BACKEND_PORT = import.meta.env.VITE_BACKEND_PORT;
return (
<div>
<p>
Conected to Back-End at: {BACKEND_HOST} - {BACKEND_PORT}
</p>
<p>
Conection Status: {isError ? "ERROR" : (isLoading ? "LOADING" : "LOADED")}
</p>
</div>
)
}
export default BackendConnectionStatus
+1
View File
@@ -63,6 +63,7 @@ export function ThemeProvider({
) )
} }
// eslint-disable-next-line react-refresh/only-export-components
export const useTheme = () => { export const useTheme = () => {
const context = useContext(ThemeProviderContext) const context = useContext(ThemeProviderContext)
@@ -3,7 +3,7 @@
import * as React from "react" import * as React from "react"
import { CalendarIcon } from "@radix-ui/react-icons" import { CalendarIcon } from "@radix-ui/react-icons"
import { format } from "date-fns" import { format } from "date-fns"
import { ActiveModifiers, DateRange } from "react-day-picker" import { ActiveModifiers, DateBefore, DateRange, Matcher } from "react-day-picker"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@@ -15,26 +15,50 @@ import {
} from "@/components/ui/popover" } from "@/components/ui/popover"
import { Typography } from "./typography" import { Typography } from "./typography"
export type UpdateRangeFunction = (newRange: DateRange) => void;
export function DatePickerWithRange({ export function DatePickerWithRange({
className, className, updateDateRange
}: React.HTMLAttributes<HTMLDivElement>) { }: { className?: React.HTMLAttributes<HTMLDivElement> | undefined, updateDateRange: UpdateRangeFunction | undefined }) {
// Date Range State.
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({ const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
from: undefined, from: undefined,
to: undefined, to: undefined,
}) });
//TODOME: Si el día es mods.outside, navegar al mes correspondiente. //TODOME: Si el día es mods.outside, navegar al mes correspondiente.
// Handle Day change events.
const handleDayClickFrom = (day: Date, mods: ActiveModifiers) => { const handleDayClickFrom = (day: Date, mods: ActiveModifiers) => {
setDateRange({ const newDateRange: DateRange = {
from: mods.selected ? undefined : day, from: mods.selected ? undefined : day,
to: dateRange?.to to: dateRange?.to
})
} }
setDateRange(newDateRange)
if (updateDateRange !== undefined) {
updateDateRange(newDateRange)
}
}
const handleDayClickTo = (day: Date, mods: ActiveModifiers) => { const handleDayClickTo = (day: Date, mods: ActiveModifiers) => {
setDateRange({ const newDateRange: DateRange = {
from: dateRange?.from, from: dateRange?.from,
to: mods.selected ? undefined : day to: mods.selected ? undefined : day
}) }
setDateRange(newDateRange)
if (updateDateRange !== undefined) {
updateDateRange(newDateRange)
}
}
// Disable days from "To" before "From"
let beforeAndSelected: Matcher[] = [];
if (dateRange?.from) {
const beforeMatcher: DateBefore = { before: dateRange?.from };
const dateMatcher: Matcher = dateRange?.from;
beforeAndSelected = [beforeMatcher, dateMatcher];
} }
return ( return (
@@ -96,6 +120,7 @@ export function DatePickerWithRange({
selected={dateRange?.to} selected={dateRange?.to}
onDayClick={handleDayClickTo} onDayClick={handleDayClickTo}
numberOfMonths={1} numberOfMonths={1}
disabled={beforeAndSelected}
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
+35
View File
@@ -0,0 +1,35 @@
import { Typography } from "./typography";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
import { PropsWithChildren } from "react";
interface IconWithTooltipNumberProps {
amount?: number | null |undefined,
altText?: string | null | undefined
}
export function IconWithTooltipNumber({ amount = undefined, altText = undefined, children }: PropsWithChildren<IconWithTooltipNumberProps>) {
const iconElement = altText ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
{children}
</TooltipTrigger>
<TooltipContent>
<p>{altText}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<>
{children}
</>
);
return (
<>
{amount && <Typography variant={"p_inline"}>{amount}</Typography>}
{iconElement}
</>
)
}
+120
View File
@@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
+127
View File
@@ -0,0 +1,127 @@
import * as React from "react"
import { Cross2Icon } from "@radix-ui/react-icons"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<Cross2Icon className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
+33
View File
@@ -0,0 +1,33 @@
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}
+1
View File
@@ -78,4 +78,5 @@ const Typography = React.forwardRef<HTMLElement, TypographyProps>(
Typography.displayName = 'Typography'; Typography.displayName = 'Typography';
// eslint-disable-next-line react-refresh/only-export-components
export { Typography, typographyVariants }; export { Typography, typographyVariants };
+192
View File
@@ -0,0 +1,192 @@
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "src/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }
+5
View File
@@ -5,6 +5,7 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import Root from './routes/Root'; import Root from './routes/Root';
import ErrorPage from './error-page'; import ErrorPage from './error-page';
import Homepage from './routes/home-page'; import Homepage from './routes/home-page';
import SearchPage from './routes/search-page';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -15,6 +16,10 @@ const router = createBrowserRouter([
{ {
path: "/home", path: "/home",
element: <Homepage /> element: <Homepage />
},
{
path: "/search",
element: <SearchPage />
} }
] ]
}, },
+24
View File
@@ -0,0 +1,24 @@
export type RoomCategory = {
id: number,
name: string,
description: string,
bedType: string,
maxGuestNum: number,
roomCategoryPhotos: RoomCategoryPhoto[],
roomCategoryDetails: RoomCategoryDetails[],
}
export type RoomCategoryDetails = {
id: number,
detailsSection: string,
detailValue: string,
}
export type RoomCategoryPhoto = {
id: number,
path: string,
altText: string | undefined,
kind: string,
}
+25
View File
@@ -0,0 +1,25 @@
export type RoomAvailabity = {
roomCategory: number,
availability: number
}
export type AvailabilityRequest = {
filter: AvailabilityFilter
}
export type AvailabilityFilter = {
startDate: string,
endDate: string
}
export type FareRequest = {
filter: FareFilter
}
export type FareFilter = {
roomCategory: number,
startDate: string,
endDate: string
}
+5
View File
@@ -0,0 +1,5 @@
import { DateRange } from "react-day-picker"
export type SearchFilter = {
dateRange: DateRange
}
+3 -1
View File
@@ -3,6 +3,7 @@ import './Root.css'
import AppHeader from '../components/app-header' import AppHeader from '../components/app-header'
import AppFooter from '../components/app-footer' import AppFooter from '../components/app-footer'
import { ThemeProvider } from '@/components/theme-provider' import { ThemeProvider } from '@/components/theme-provider'
import { Toaster } from '@/components/ui/toaster'
function Root() { function Root() {
@@ -10,9 +11,10 @@ function Root() {
<> <>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<AppHeader /> <AppHeader />
<div className='appPage'> <div className='appPage bg-background text-foreground'>
<Outlet /> <Outlet />
</div> </div>
<Toaster />
<AppFooter /> <AppFooter />
</ThemeProvider> </ThemeProvider>
</> </>
+28 -6
View File
@@ -1,18 +1,40 @@
import { useAPIRoomCategories } from "@/api/api-roomCategories"
import { RoomItemCard } from "@/components/Rooms/room-item-card" import { RoomItemCard } from "@/components/Rooms/room-item-card"
import { SearchFilterCard } from "@/components/Search/SearchFilterCard" import { SearchFilterCard } from "@/components/Search/SearchFilterCard"
import { Button } from "@/components/ui/button" import BackendConnectionStatus from "@/components/backend-conection-status"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"
import { Typography } from "@/components/ui/typography"
import { RoomCategory } from "@/model/RoomCategory"
function Homepage() { function Homepage() {
const { roomCategoryList, isLoading, isError } = useAPIRoomCategories();
const roomList = roomCategoryList?.map((element: RoomCategory) => {
return ( return (
<div className="bg-background text-foreground"> <RoomItemCard key={element.id} room={element} />
<Button>Test</Button> );
<Separator className="my-10"/> });
return (
<>
<Typography variant={"h1"}>Hostal del Val</Typography>
<Separator className="my-10" />
<SearchFilterCard /> <SearchFilterCard />
<Separator className="my-10"/> <Separator className="my-10" />
<RoomItemCard /> <div className="grid grid-cols-2 gap-4">
{roomList}
</div> </div>
{isLoading && (
<>
<Typography variant={"h3"} as="h2">Loading...</Typography>
<Skeleton className="w-[400px] h-[200px] mx-auto" />
</>
)}
<Separator className="my-10" />
<BackendConnectionStatus isLoading={isLoading} isError={isError} />
</>
) )
} }
+66
View File
@@ -0,0 +1,66 @@
import { useAPIRoomAvailability } from "@/api/api-roomSearch"
import RoomBooking from "@/components/Rooms/room-booking"
import { SearchFilterCard } from "@/components/Search/SearchFilterCard"
import BackendConnectionStatus from "@/components/backend-conection-status"
import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"
import { Typography } from "@/components/ui/typography"
import { AvailabilityFilter } from "@/model/RoomSearch"
import { SearchFilter } from "@/model/SearchFilter"
import { useState } from "react"
//TODOME: Igual podemos crear un StateMachine conjunta para los siguiente estados de la página: Sin Buscar, Sin disponibilidad, error, cargando.
function SearchPage() {
const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>();
const [isSearched, setIsSearched] = useState<boolean>(false);
const onBtSearchClick = (newFilter: SearchFilter) => {
setAvailabilityFilter({
startDate: newFilter.dateRange.from?.toDateString() ?? "",
endDate: newFilter.dateRange.to?.toDateString() ?? ""
});
setIsSearched(true);
}
const { roomAvailabilityList, isLoading: isLoadingAvailability, isError: isErrorAvailability } = useAPIRoomAvailability(availabilityFilter ?? null);
const isLoading = isLoadingAvailability;
const isError = isErrorAvailability;
const roomList = roomAvailabilityList?.map((element) => {
return (
<RoomBooking key={element.roomCategory} availableAmount={element.availability} roomCategoryId={element.roomCategory} />
);
});
const noRoomsAvailable = (
<>
<Typography variant={"h3"} as="h3">Sorry 😔, there aren't rooms available for selected period.
You can contact us for more info.</Typography>
</>
);
const hasRooms = (roomAvailabilityList?.length && roomAvailabilityList?.length > 0);
console.log(hasRooms);
return (
<div className="bg-background text-foreground">
<SearchFilterCard onSearchClick={onBtSearchClick} />
<Separator className="my-10" />
{isLoading && (
<>
<Typography variant={"h3"} as="h2">Loading...</Typography>
<Skeleton className="w-[400px] h-[200px] mx-auto" />
</>
)}
<div className="flex flex-col gap-5">
{hasRooms ? roomList : (isSearched && noRoomsAvailable)}
</div>
<Separator className="my-10" />
<RoomBooking key={99} availableAmount={5} roomCategoryId={6} />
<Separator className="my-10" />
<BackendConnectionStatus isLoading={isLoading} isError={isError} />
</div>
)
}
export default SearchPage