Author SHA1 Message Date
Daniel-Garmig 5676262692 Generate Nightly Build - 202405131816
Publish_on_Tag / Build and Push (push) Successful in 3m24s
2024-05-13 18:16:20 +02:00
Daniel-Garmig d3358c7c58 Fix Actions - Change image repository for push.
CI / Docker Lint (push) Successful in 31s
2024-05-13 01:18:26 +02:00
Daniel-Garmig 4e825c7ca7 Small fix pass.
CI / Docker Lint (push) Successful in 28s
2024-05-13 00:49:54 +02:00
Daniel-Garmig b83af1b872 Copy "/public" to image on prod build. 2024-05-13 00:37:57 +02:00
Daniel-Garmig f8365e05af Env Vars - Support for container env vars.
CI / Docker Lint (push) Successful in 29s
Now you can configure your container using Environmet vars.
2024-05-12 13:21:28 +02:00
Daniel-Garmig 0039b09d98 Gitea Actions support (#1)
Add Gitea Actions support.

Reviewed-on: #1
Co-authored-by: Daniel García <[email protected]>
Co-committed-by: Daniel García <[email protected]>
2024-05-12 02:33:16 +02:00
Daniel-Garmig e9b67efd46 Fix build error. 2024-05-11 23:05:45 +02:00
Daniel-Garmig 0047099742 Booking lifecycle:
- Create bookings :)
2024-05-10 20:04:37 +02:00
Daniel-Garmig c753d1e9fa Add routes & navigation. 2024-05-09 22:45:34 +02:00
Daniel-Garmig fe7122b107 Booking process - Add and remove rooms from booking. 2024-05-09 21:47:12 +02:00
Daniel-Garmig 630d5cb708 State Management & RoomPrices
- Added zustand for global state management.
- New Component - NumberSelector.
- Implement Prices API.
2024-05-09 18:32:18 +02:00
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
43 changed files with 4129 additions and 204 deletions
+3 -1
View File
@@ -1,4 +1,6 @@
node_modules
build
.dockerignore
Dockerfile
Dockerfile
.env
.env.local
+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
+3
View File
@@ -0,0 +1,3 @@
# App config Variables (Will be visible by client):
VITE_BACKEND_HOST=SOMNIAROOMS_BACKEND_HOST
VITE_BACKEND_PORT=SOMNIAROOMS_BACKEND_PORT
+23
View File
@@ -0,0 +1,23 @@
name: CI
on:
push:
branches:
- main
pull_request: ~
workflow_dispatch: ~
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
lint:
name: Docker Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Lint Dockerfile
uses: hadolint/[email protected]
+45
View File
@@ -0,0 +1,45 @@
name: Publish_on_Tag
on:
push:
tags:
- devbuild_*
workflow_dispatch: ~
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
name: Build and Push
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to local registry
uses: docker/login-action@v3
with:
registry: ${{vars.REGISTRY_INSTANCE}}
username: ${{gitea.repository_owner}}
password: ${{secrets.REGISTRY_TOKEN}} # It should be a user/org token with package perms.
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{vars.REGISTRY_INSTANCE}}/somnia/somniaroomsapp:devbuild
target: final-prod
platforms: linux/amd64,linux/arm64
secrets: |
GIT_AUTH_TOKEN=${{secrets.REGISTRY_TOKEN}}
+4 -2
View File
@@ -14,11 +14,13 @@ CMD ["npm", "run", "dev"]
# Build app for production.
FROM app-base as builder
RUN npm ci
RUN npm run build
RUN npm ci && npm run build
# production environment
FROM nginx:stable-alpine as final-prod
COPY --from=builder /app/dist /usr/share/nginx/html
COPY /public /usr/share/nginx/html/public
COPY scripts/env.sh /docker-entrypoint.d/env.sh
EXPOSE 80
RUN chmod +x /docker-entrypoint.d/env.sh
CMD ["nginx", "-g", "daemon off;"]
+28 -1
View File
@@ -30,4 +30,31 @@ docker build -t somniarooms-app:prod --target final-prod .
**Docker Run**
```
docker run -d -it -p 8080:80 --name somniarooms-app somniarooms-app:prod
```
```
**Docker run with env vars:**
```
docker run -d -it -p 8080:80 --name somniarooms-app -e SOMNIAROOMS_BACKEND_HOST=https://somniaapi.example.org/ -e SOMNIAROOMS_BACKEND_PORT=443 somniarooms-app:prod
```
## Env Vars.
As this is distributed as a docker image ready to use, you must set some env variables to configure the app:
| Var | Default Value | Description |
| :------------------------: | :----------------------------: | :------------------------------------------------------------------------------------------: |
| `SOMNIAROOMS_BACKEND_HOST` | https://somniaapi.example.org/ | Host where uses can connect to the SomniaRoom API. API endpoint should be reachble for uses. |
| `SOMNIAROOMS_BACKEND_PORT` | 443 | Port to SomniarRooms API. |
## .env files
This project uses .env files to configure the app. As Vite only supports compile-time vars, we use some tricks to allow setting variables from container env variables.
### .env
Contains default values. It is just an example configuration.
For development is recomended to create a `.env.local` file based on `.env`.
### .env.production
Set varibles to certains placeholder, that are replace on container startup.
(We are using this solution: https://stackoverflow.com/a/77454537)
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/SR2.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
+250 -153
View File
@@ -15,6 +15,7 @@
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -25,7 +26,9 @@
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.23.0",
"tailwind-merge": "^2.3.0"
"swr": "^2.2.5",
"tailwind-merge": "^2.3.0",
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/node": "^20.12.8",
@@ -46,15 +49,6 @@
"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": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -1286,6 +1280,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": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
@@ -1483,9 +1511,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.3.tgz",
"integrity": "sha512-1ACInKIT0pXmTYuPoJAL8sOT0lV3PEACFSVxnD03hGIojJ1CmbzZmLJyk2xew+yxqTlmx7xydkiJcBzdp0V+AQ==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz",
"integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==",
"cpu": [
"arm"
],
@@ -1496,9 +1524,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.3.tgz",
"integrity": "sha512-vGl+Bny8cawCM7ExugzqEB8ke3t7Pm9/mo+ciA9kJh6pMuNyM+31qhewMwHwseDZ/LtdW0SCocW1CsMxcq1Lsg==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz",
"integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==",
"cpu": [
"arm64"
],
@@ -1509,9 +1537,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.3.tgz",
"integrity": "sha512-Lj8J9WzQRvfWO4GfI+bBkIThUFV1PtI+es/YH/3cwUQ+edXu8Mre0JRJfRrAeRjPiHDPFFZaX51zfgHHEhgRAg==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz",
"integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==",
"cpu": [
"arm64"
],
@@ -1522,9 +1550,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.3.tgz",
"integrity": "sha512-NPPOXMTIWJk50lgZmRReEYJFvLG5rgMDzaVauWNB2MgFQYm9HuNXQdVVg3iEZ3A5StIzxhMlPjVyS5fsv4PJmg==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz",
"integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==",
"cpu": [
"x64"
],
@@ -1535,9 +1563,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.3.tgz",
"integrity": "sha512-ij4tv1XtWcDScaTgoMnvDEYZ2Wjl2ZhDFEyftjBKu6sNNLHIkKuXBol/bVSh+md5zSJ6em9hUXyPO3cVPCsl4Q==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz",
"integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==",
"cpu": [
"arm"
],
@@ -1548,9 +1576,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.3.tgz",
"integrity": "sha512-MTMAl30dzcfYB+smHe1sJuS2P1/hB8pqylkCe0/8/Lo8CADjy/eM8x43nBoR5eqcYgpOtCh7IgHpvqSMAE38xw==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz",
"integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==",
"cpu": [
"arm"
],
@@ -1561,9 +1589,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.3.tgz",
"integrity": "sha512-vY3fAg6JLDoNh781HHHMPvt8K6RWG3OmEj3xI9BOFSQTD5PNaGKvCB815MyGlDnFYUw7lH+WvvQqoBwLtRDR1A==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz",
"integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==",
"cpu": [
"arm64"
],
@@ -1574,9 +1602,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.3.tgz",
"integrity": "sha512-61SpQGBSb8QkfV/hUYWezlEig4ro55t8NcE5wWmy1bqRsRVHCEDkF534d+Lln/YeLUoSWtJHvvG3bx9lH/S6uA==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz",
"integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==",
"cpu": [
"arm64"
],
@@ -1587,9 +1615,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.3.tgz",
"integrity": "sha512-4XGexJthsNhEEgv/zK4/NnAOjYKoeCsIoT+GkqTY2u3rse0lbJ8ft1bpDCdlkvifsLDL2uwe4fn8PLR4IMTKQQ==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz",
"integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==",
"cpu": [
"ppc64"
],
@@ -1600,9 +1628,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.3.tgz",
"integrity": "sha512-/pArXjqnEdhbQ1qe4CTTlJ6/GjWGdWNRucKAp4fqKnKf7QC0BES3QEV34ACumHHQ4uEGt4GctF2ISCMRhkli0A==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz",
"integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==",
"cpu": [
"riscv64"
],
@@ -1613,9 +1641,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.3.tgz",
"integrity": "sha512-vu4f3Y8iwjtRfSZdmtP8nC1jmRx1IrRVo2cLQlQfpFZ0e2AE9YbPgfIzpuK+i3C4zFETaLLNGezbBns2NuS/uA==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz",
"integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==",
"cpu": [
"s390x"
],
@@ -1626,9 +1654,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.3.tgz",
"integrity": "sha512-n4HEgIJulNSmAKT3SYF/1wuzf9od14woSBseNkzur7a+KJIbh2Jb+J9KIsdGt3jJnsLW0BT1Sj6MiwL4Zzku6Q==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz",
"integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==",
"cpu": [
"x64"
],
@@ -1639,9 +1667,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.3.tgz",
"integrity": "sha512-guO/4N1884ig2AzTKPc6qA7OTnFMUEg/X2wiesywRO1eRD7FzHiaiTQQOLFmnUXWj2pgQXIT1g5g3e2RpezXcQ==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz",
"integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==",
"cpu": [
"x64"
],
@@ -1652,9 +1680,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.3.tgz",
"integrity": "sha512-+rxD3memdkhGz0NhNqbYHXBoA33MoHBK4uubZjF1IeQv1Psi6tqgsCcC6vwQjxBM1qoCqOQQBy0cgNbbZKnGUg==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz",
"integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==",
"cpu": [
"arm64"
],
@@ -1665,9 +1693,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.3.tgz",
"integrity": "sha512-0NxVbLhBXmwANWWbgZY/RdSkeuHEgF+u8Dc0qBowUVBYsR2y2vwVGjKgUcj1wtu3jpjs057io5g9HAPr3Icqjg==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz",
"integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==",
"cpu": [
"ia32"
],
@@ -1678,9 +1706,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.3.tgz",
"integrity": "sha512-hutnZavtOx/G4uVdgoZz5279By9NVbgmxOmGGgnzUjZYuwp2+NzGq6KXQmHXBWz7W/vottXn38QmKYAdQLa/vQ==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz",
"integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==",
"cpu": [
"x64"
],
@@ -1937,9 +1965,9 @@
"devOptional": true
},
"node_modules/@types/react": {
"version": "18.2.79",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz",
"integrity": "sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz",
"integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
@@ -1947,9 +1975,9 @@
}
},
"node_modules/@types/react-dom": {
"version": "18.2.25",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.25.tgz",
"integrity": "sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==",
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"devOptional": true,
"dependencies": {
"@types/react": "*"
@@ -1983,16 +2011,16 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz",
"integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz",
"integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/type-utils": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"@typescript-eslint/scope-manager": "7.8.0",
"@typescript-eslint/type-utils": "7.8.0",
"@typescript-eslint/utils": "7.8.0",
"@typescript-eslint/visitor-keys": "7.8.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
@@ -2018,15 +2046,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz",
"integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz",
"integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"@typescript-eslint/scope-manager": "7.8.0",
"@typescript-eslint/types": "7.8.0",
"@typescript-eslint/typescript-estree": "7.8.0",
"@typescript-eslint/visitor-keys": "7.8.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2046,13 +2074,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz",
"integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz",
"integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1"
"@typescript-eslint/types": "7.8.0",
"@typescript-eslint/visitor-keys": "7.8.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -2063,13 +2091,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz",
"integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz",
"integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/utils": "7.7.1",
"@typescript-eslint/typescript-estree": "7.8.0",
"@typescript-eslint/utils": "7.8.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -2090,9 +2118,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz",
"integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz",
"integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==",
"dev": true,
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -2103,13 +2131,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz",
"integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz",
"integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/visitor-keys": "7.7.1",
"@typescript-eslint/types": "7.8.0",
"@typescript-eslint/visitor-keys": "7.8.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -2131,17 +2159,17 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz",
"integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz",
"integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.15",
"@types/semver": "^7.5.8",
"@typescript-eslint/scope-manager": "7.7.1",
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/typescript-estree": "7.7.1",
"@typescript-eslint/scope-manager": "7.8.0",
"@typescript-eslint/types": "7.8.0",
"@typescript-eslint/typescript-estree": "7.8.0",
"semver": "^7.6.0"
},
"engines": {
@@ -2156,12 +2184,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz",
"integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==",
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz",
"integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.7.1",
"@typescript-eslint/types": "7.8.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -2519,6 +2547,11 @@
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -2672,9 +2705,9 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.4.754",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.754.tgz",
"integrity": "sha512-7Kr5jUdns5rL/M9wFFmMZAgFDuL2YOnanFH4OI4iFzUqyh3XOL7nAGbSlSMZdzKMIyyTpNSbqZsWG9odwLeKvA==",
"version": "1.4.756",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.756.tgz",
"integrity": "sha512-RJKZ9+vEBMeiPAvKNWyZjuYyUqMndcP1f335oHqn3BEQbs2NFtVrnK5+6Xg5wSM9TknNNpWghGDUCKGYF+xWXw==",
"dev": true
},
"node_modules/embla-carousel": {
@@ -2823,9 +2856,9 @@
}
},
"node_modules/eslint-plugin-react-hooks": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
"integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
"dev": true,
"engines": {
"node": ">=10"
@@ -3675,17 +3708,17 @@
}
},
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
"integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"dependencies": {
"@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
"type-check": "^0.4.0"
"type-check": "^0.4.0",
"word-wrap": "^1.2.5"
},
"engines": {
"node": ">= 0.8.0"
@@ -4024,9 +4057,9 @@
]
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -4048,15 +4081,15 @@
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.2.0"
"react": "^18.3.1"
}
},
"node_modules/react-remove-scroll": {
@@ -4234,9 +4267,9 @@
}
},
"node_modules/rollup": {
"version": "4.16.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.3.tgz",
"integrity": "sha512-Ygm4fFO4usWcAG3Ud36Lmif5nudoi0X6QPLC+kRgrRjulAbmFkaTawP7fTIkRDnCNSf/4IAQzXM1T8e691kRtw==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz",
"integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.5"
@@ -4249,22 +4282,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.16.3",
"@rollup/rollup-android-arm64": "4.16.3",
"@rollup/rollup-darwin-arm64": "4.16.3",
"@rollup/rollup-darwin-x64": "4.16.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.16.3",
"@rollup/rollup-linux-arm-musleabihf": "4.16.3",
"@rollup/rollup-linux-arm64-gnu": "4.16.3",
"@rollup/rollup-linux-arm64-musl": "4.16.3",
"@rollup/rollup-linux-powerpc64le-gnu": "4.16.3",
"@rollup/rollup-linux-riscv64-gnu": "4.16.3",
"@rollup/rollup-linux-s390x-gnu": "4.16.3",
"@rollup/rollup-linux-x64-gnu": "4.16.3",
"@rollup/rollup-linux-x64-musl": "4.16.3",
"@rollup/rollup-win32-arm64-msvc": "4.16.3",
"@rollup/rollup-win32-ia32-msvc": "4.16.3",
"@rollup/rollup-win32-x64-msvc": "4.16.3",
"@rollup/rollup-android-arm-eabi": "4.17.2",
"@rollup/rollup-android-arm64": "4.17.2",
"@rollup/rollup-darwin-arm64": "4.17.2",
"@rollup/rollup-darwin-x64": "4.17.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.17.2",
"@rollup/rollup-linux-arm-musleabihf": "4.17.2",
"@rollup/rollup-linux-arm64-gnu": "4.17.2",
"@rollup/rollup-linux-arm64-musl": "4.17.2",
"@rollup/rollup-linux-powerpc64le-gnu": "4.17.2",
"@rollup/rollup-linux-riscv64-gnu": "4.17.2",
"@rollup/rollup-linux-s390x-gnu": "4.17.2",
"@rollup/rollup-linux-x64-gnu": "4.17.2",
"@rollup/rollup-linux-x64-musl": "4.17.2",
"@rollup/rollup-win32-arm64-msvc": "4.17.2",
"@rollup/rollup-win32-ia32-msvc": "4.17.2",
"@rollup/rollup-win32-x64-msvc": "4.17.2",
"fsevents": "~2.3.2"
}
},
@@ -4292,9 +4325,9 @@
}
},
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": {
"loose-envify": "^1.1.0"
}
@@ -4535,6 +4568,18 @@
"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": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz",
@@ -4699,9 +4744,9 @@
"dev": true
},
"node_modules/update-browserslist-db": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.14.tgz",
"integrity": "sha512-JixKH8GR2pWYshIPUg/NujK3JO7JiqEEUiNArE86NQyrgUuZeTlZQN3xuS/yiV5Kb48ev9K6RqNkaJjXsdg7Jw==",
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.15.tgz",
"integrity": "sha512-K9HWH62x3/EalU1U6sjSZiylm9C8tgq2mSvshZpqc7QE69RaA2qjhkW2HlNA0tFpEbtyFz7HTqbSdN4MSwUodA==",
"dev": true,
"funding": [
{
@@ -4778,6 +4823,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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -4785,9 +4838,9 @@
"dev": true
},
"node_modules/vite": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz",
"integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==",
"version": "5.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
"dev": true,
"dependencies": {
"esbuild": "^0.20.1",
@@ -4854,6 +4907,15 @@
"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": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
@@ -4983,6 +5045,41 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz",
"integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/zustand/node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
}
}
}
+4 -1
View File
@@ -17,6 +17,7 @@
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -27,7 +28,9 @@
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.23.0",
"tailwind-merge": "^2.3.0"
"swr": "^2.2.5",
"tailwind-merge": "^2.3.0",
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/node": "^20.12.8",
+2121
View File
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 96 KiB

+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
for i in $(env | grep SOMNIAROOMS_)
do
key=$(echo $i | cut -d '=' -f 1)
value=$(echo $i | cut -d '=' -f 2-)
echo $key=$value
# sed All files
# find /usr/share/nginx/html -type f -exec sed -i "s|${key}|${value}|g" '{}' +
# sed JS and CSS only
find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' +
done
+19
View File
@@ -0,0 +1,19 @@
import { NewBooking, NewBookingResponse } from "@/model/Bookings";
import { fetcherWithFilter } from "./backend";
export async function APISendCreateBooking(bookingData: NewBooking | null) {
if (!bookingData) {
return {
bookingResponse: null,
isError: true
}
}
const rs = await fetcherWithFilter<NewBookingResponse>("/api/booking/create", bookingData);
return {
bookingResponse: rs,
isError: false
}
}
+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
}
}
+33
View File
@@ -0,0 +1,33 @@
import { fetcherUnwraperWithFilter } from "./backend";
import useSWR from 'swr';
import { AvailabilityFilter, AvailabilityRequest, FareFilter, FareRequest, RoomAvailabity, RoomPrice } 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
}
}
export function useAPIRoomPrices(filter: FareFilter | null) {
const request: FareRequest | null = filter ? {
filter: filter
} : null;
const { data, isLoading, error } = useSWR(request ? ['/api/search/fares', request] : null, ([url, request]) => fetcherUnwraperWithFilter<RoomPrice[]>(url, request))
return {
roomPriceList: 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
})
}
+144
View File
@@ -0,0 +1,144 @@
import { useAPIRoomCategoryId } from "@/api/api-roomCategories"
import { Card, CardContent, CardHeader } from "../ui/card"
import { useMemo, useState } from "react";
import { Typography } from "../ui/typography";
import { RoomCategoryPhoto } from "@/model/RoomCategory";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
import { Skeleton } from "../ui/skeleton";
import { useAppStore } from "@/store";
import { DateRange } from "react-day-picker";
import { FareFilter } from "@/model/RoomSearch";
import { useAPIRoomPrices } from "@/api/api-roomSearch";
import { NumberSelector } from "../ui/number-selector";
import { NewBookingRoom } from "@/model/Bookings";
function RoomBooking({ roomCategoryId, availableAmount }: { roomCategoryId: number, availableAmount: number }) {
const [selectedAmount, setSelectedAmount] = useState<Map<number, number>>(new Map());
const currentDates: DateRange = useAppStore((state) => (state.searchFilter.dateRange));
const addNewRoom = useAppStore((state) => (state.addNewBookingRoom));
function onSelectedAmountUpdate(guestNumber: number, newCount: number, price: number) {
console.log("Room " + roomCategoryId + " Guestnumber: " + guestNumber + " Count: " + newCount);
const newAmountMap = new Map(selectedAmount);
newAmountMap.set(guestNumber, newCount);
setSelectedAmount(newAmountMap)
const newRoom: NewBookingRoom = {
roomCategory: roomCategoryId,
guestNumber: guestNumber,
amount: newCount,
price: price
};
addNewRoom(newRoom);
}
const totalSelected = () => {
let sum = 0;
selectedAmount.forEach((v) => sum += v)
return sum;
};
const canAddMore: boolean = availableAmount > totalSelected();
//This seems a little bit overkill, but IDK.
const fareFilter: FareFilter | null = useMemo(
() => {
if (currentDates && currentDates.from && currentDates.to) {
const filter: FareFilter = {
startDate: currentDates.from?.toDateString(),
endDate: currentDates.to?.toDateString(),
roomCategory: roomCategoryId
}
return filter;
}
return null;
},
[currentDates, roomCategoryId]
);
const { roomCategory, isLoading: isLoadingCategory, isError: isErrorCategory } = useAPIRoomCategoryId(roomCategoryId);
const { roomPriceList, isLoading: isLoadingPrices, isError: isErrorPrices } = useAPIRoomPrices(fareFilter);
//TODOME: Gestionar también los estados al cargar los precios.
const isLoading = isLoadingCategory || isLoadingPrices;
const isError = isErrorCategory || isErrorPrices;
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>
);
const hasPrices = roomPriceList != null && roomPriceList?.length > 0;
let priceTable = null;
if (hasPrices) {
const tableRows = roomPriceList?.map((el) => {
return (
<TableRow key={el.guestNumber}>
<TableCell>{el.guestNumber}</TableCell>
<TableCell>{el.price / 100} €</TableCell>
<TableCell>
<NumberSelector updateCount={(count) => onSelectedAmountUpdate(el.guestNumber, count, el.price)} addEnabled={canAddMore}></NumberSelector>
</TableCell>
</TableRow>
);
});
priceTable = (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Guest number</TableHead>
<TableHead>Price</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoadingPrices ? <Skeleton className="w-[80%] h-[100%]" /> : tableRows}
</TableBody>
</Table>
<p>Rooms Available: {availableAmount}</p>
</>
);
}
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>
{hasPrices ? priceTable : <Typography variant={"largeText"}>No Prices available</Typography>}
<div>
</div>
<p>Selected: {totalSelected()}</p>
</CardContent>
</Card>
);
}
export default RoomBooking
+41 -10
View File
@@ -2,30 +2,61 @@ import { Separator } from "../ui/separator"
import { Card, CardContent, CardHeader } from "../ui/card"
import { Typography } from "../ui/typography"
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 (
<Card>
<Card id={room.id.toString()}>
<CardHeader className="p-0 m-0 mb-5">
<img src="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>
<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">{room.name}</Typography>
</CardHeader>
<CardContent>
<Typography variant={"p"} as="p">
Habitación doble con <strong>baño privado</strong>.
{room.description}
</Typography>
<Separator className="my-4" />
<div className="flex justify-around">
<div className="flex flex-row gap-2">
<Typography variant={"p_inline"}>2</Typography>
<BedSingleIcon />
<Typography variant={"p_inline"}>1</Typography>
<BedDoubleIcon />
{hasSingleBed && (
<IconWithTooltipNumber altText="Single bed" amount={bedTypeList[0]}>
<BedSingleIcon />
</IconWithTooltipNumber>
)}
{hasDoubleBed && (
<>
<IconWithTooltipNumber altText="Double bed" amount={bedTypeList[1]}>
<BedDoubleIcon />
</IconWithTooltipNumber>
</>
)}
</div>
<div className="flex flex-row">
<UserIcon />
<Typography variant={"p_inline"} className="ml-3">3</Typography>
<Typography variant={"p_inline"} className="ml-3">{room.maxGuestNum}</Typography>
</div>
</div>
</CardContent>
+81 -6
View File
@@ -2,11 +2,86 @@ import { Separator } from "../ui/separator"
import { Card, CardContent, CardHeader } from "../ui/card"
import { Typography } from "../ui/typography"
import 'react-day-picker/dist/style.css';
import { DatePickerWithRange } from "../ui/DateRangePicker";
import { DatePickerWithRange } from "../ui/dateRange-picker";
import { Button } from "../ui/button";
import { SearchIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { SearchFilter } from "@/model/SearchFilter";
import { DateRange } from "react-day-picker";
import { useToast } from "../ui/use-toast";
import { useAppStore } from "@/store";
import { useLocation } from "react-router-dom";
export function SearchFilterCard() {
type funOnSearchClick = (newFilter: SearchFilter) => void;
export function SearchFilterCard({ onSearchClick }: { onSearchClick?: funOnSearchClick }) {
//App filter. Updated on btSearchClick.
const appSearchFilter = useAppStore((state) => (state.searchFilter));
const setAppSearchFilter = useAppStore((state) => (state.setSearchFilter));
//Local Filter state.
const [searchFilter, setSearchFilter] = useState<SearchFilter>(appSearchFilter)
//On location change, try to load searchFilter from state.
//FIXME: Si llegamos sin state, hay que borrar el state global...
const location = useLocation();
useEffect(() => {
setSearchFilter(location.state);
}, [location]);
const { toast } = useToast();
// 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) {
setAppSearchFilter(searchFilter);
onSearchClick(searchFilter);
}
}
// Handle Element updates.
const updateDateRange = (newRange: DateRange) => {
setSearchFilter({
...searchFilter,
dateRange: newRange,
});
console.log("SearchFilterCard filter UPDATED");
}
const datePickerElement = searchFilter == null ? (
<DatePickerWithRange
updateDateRange={updateDateRange}
allowPast={false}>
</DatePickerWithRange>
) : (
<DatePickerWithRange
updateDateRange={updateDateRange}
allowPast={false}
startDate={searchFilter.dateRange.from}
endDate={searchFilter.dateRange.to}>
</DatePickerWithRange>
);
return (
<Card>
@@ -14,11 +89,11 @@ export function SearchFilterCard() {
<Typography variant={"h2"} as="h1" className="mx-5 py-2">Reserva ahora</Typography>
</CardHeader>
<CardContent>
<DatePickerWithRange></DatePickerWithRange>
{datePickerElement}
<Separator className="my-4" />
<Button size={"lg"}>
<SearchIcon className="text-primary-foreground me-2"/>
<Typography variant={"h3"} className="text-primary-foreground">Buscar</Typography>
<Button size={"lg"} onClick={btSearchClick}>
<SearchIcon className="text-primary-foreground me-2" />
<Typography variant={"h3"} className="text-primary-foreground">Buscar</Typography>
</Button>
</CardContent>
</Card>
+14 -7
View File
@@ -1,19 +1,26 @@
import { BookmarkIcon } from "@radix-ui/react-icons"
import { ModeToggle } from "./mode-toggle"
import { Button } from "./ui/button"
import { Card, CardContent } from "./ui/card"
import { Typography } from "./ui/typography"
import { Link } from "react-router-dom"
import { BookAIcon, HomeIcon } from "lucide-react"
function AppHeader() {
return (
<Card className="flex items-center mx-10 my-4">
<CardContent className="w-full">
<div className="flex justify-around">
<Typography variant={"largeText"}>Hostal del Val</Typography>
<Button variant="ghost">
<BookmarkIcon/>
<Typography variant={"largeText"}>Reservar</Typography>
<CardContent className="w-full py-2">
<div className="flex justify-around items-center">
<div className="flex flex-row items-center gap-2 relative">
<img src="public/SR2.svg" className="w-[64px]"></img>
<Typography variant={"largeText"}>Somnia Rooms</Typography>
<a href="https://duck.com" className="absolute w-[100%] h-[100%]"></a>
</div>
<Button variant="ghost" className="h-15">
<Link to={`home`}><HomeIcon className="mx-auto" />Home</Link>
</Button>
<Button variant="ghost" className="h-15">
<Link to={`search`}><BookAIcon className="mx-auto" />Reservar</Link>
</Button>
<ModeToggle />
</div>
@@ -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
@@ -0,0 +1,95 @@
import { useAppStore } from "@/store"
import { Card, CardContent, CardHeader } from "../ui/card"
import { Typography } from "../ui/typography"
import { differenceInDays } from "date-fns";
import { Separator } from "../ui/separator";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
import { Button } from "../ui/button";
type OnBookingClickFunction = () => void;
type BookingDetailsModes = "Book" | "View"
export function BookingDetails({ onBookingConfirm, mode }: { onBookingConfirm?: OnBookingClickFunction, mode: BookingDetailsModes }) {
const bookingData = useAppStore((state) => state.newBookingData);
const onBtBookingClick = () => {
if (onBookingConfirm) {
onBookingConfirm();
}
}
if (!bookingData || bookingData.rooms.length <= 0) {
return (
<Card>
<CardHeader>
<Typography variant={"h2"} as="h2">Your Booking</Typography>
</CardHeader>
<CardContent>
<Typography variant={"h3"}>No rooms added yet</Typography>
</CardContent>
</Card>
);
}
const roomList = bookingData.rooms.map((room) => {
return (
<TableRow key={room.roomCategory + "_" + room.guestNumber}>
<TableCell>{room.roomCategory}</TableCell>
<TableCell>{room.guestNumber}</TableCell>
<TableCell>{room.amount}</TableCell>
<TableCell>{room.price / 100} €</TableCell>
<TableCell>{(room.price * room.amount) / 100} €</TableCell>
</TableRow>
);
})
const roomTable = (
<Table>
<TableHeader>
<TableRow>
<TableHead>Room</TableHead>
<TableHead>Guests</TableHead>
<TableHead>Quantity</TableHead>
<TableHead>Unit price</TableHead>
<TableHead>Total price</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{roomList}
</TableBody>
</Table>
);
const totalPrice = bookingData.rooms.reduce((acc, room) => (acc + (room.price * room.amount)), 0);
return (
<Card>
<CardHeader>
<Typography variant={"h2"} as="h2">Your Booking</Typography>
</CardHeader>
<CardContent>
<Typography variant={"h3"}>Your Dates</Typography>
<Typography variant={"largeText"}>
Arrival: {bookingData.arrivalDate.toLocaleDateString()} -
Departure: {bookingData.departureDate.toLocaleDateString()} <br></br>
Number of nights: {differenceInDays(bookingData.departureDate, bookingData.arrivalDate)}
</Typography>
<Separator className="my-2"></Separator>
<Typography variant={"h3"}>Your Rooms</Typography>
{roomTable}
<Separator className="my-2"></Separator>
<Typography variant={"h3"}>Total Price</Typography>
<Typography variant={"h3"}>{totalPrice / 100} €</Typography>
{
mode == "Book" &&
<Button className="my-2" onClick={onBtBookingClick}>
<Typography variant={"largeText"} className="text-primary-foreground">Book it!</Typography>
</Button>
}
</CardContent>
</Card>
)
}
+1
View File
@@ -63,6 +63,7 @@ export function ThemeProvider({
)
}
// eslint-disable-next-line react-refresh/only-export-components
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
+2 -3
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
import { DayPicker } from "react-day-picker"
@@ -59,8 +58,8 @@ function Calendar({
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
IconLeft: () => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: () => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>
@@ -3,7 +3,7 @@
import * as React from "react"
import { CalendarIcon } from "@radix-ui/react-icons"
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 { Button } from "@/components/ui/button"
@@ -15,26 +15,58 @@ import {
} from "@/components/ui/popover"
import { Typography } from "./typography"
export type UpdateRangeFunction = (newRange: DateRange) => void;
export function DatePickerWithRange({
className,
}: React.HTMLAttributes<HTMLDivElement>) {
className, updateDateRange, startDate, endDate, allowPast = true
}: {
className?: React.HTMLAttributes<HTMLDivElement> | undefined, updateDateRange: UpdateRangeFunction | undefined,
startDate?: Date, endDate?: Date, allowPast?: boolean
}) {
// Date Range State.
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
from: undefined,
to: undefined,
})
from: startDate,
to: endDate,
});
//TODOME: Si el día es mods.outside, navegar al mes correspondiente.
// Handle Day change events.
const handleDayClickFrom = (day: Date, mods: ActiveModifiers) => {
setDateRange({
const newDateRange: DateRange = {
from: mods.selected ? undefined : day,
to: dateRange?.to
})
}
setDateRange(newDateRange)
if (updateDateRange !== undefined) {
updateDateRange(newDateRange)
}
}
const handleDayClickTo = (day: Date, mods: ActiveModifiers) => {
setDateRange({
const newDateRange: DateRange = {
from: dateRange?.from,
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];
}
let pastMatcher: DateBefore | undefined = undefined;
if (!allowPast) {
pastMatcher = { before: new Date() };
}
return (
@@ -67,6 +99,7 @@ export function DatePickerWithRange({
selected={dateRange?.from}
onDayClick={handleDayClickFrom}
numberOfMonths={1}
disabled={pastMatcher}
/>
</PopoverContent>
</Popover>
@@ -96,6 +129,7 @@ export function DatePickerWithRange({
selected={dateRange?.to}
onDayClick={handleDayClickTo}
numberOfMonths={1}
disabled={beforeAndSelected}
/>
</PopoverContent>
</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}
</>
)
}
+33
View File
@@ -0,0 +1,33 @@
import { useState } from "react"
import { Button } from "./button";
import { Typography } from "./typography";
export type UpdateSelectedNumberFunction = (newCount: number) => void;
export function NumberSelector(
{ updateCount, addEnabled = true, allowNegative = false }:
{ updateCount: UpdateSelectedNumberFunction, addEnabled?: boolean, allowNegative?: boolean }) {
const [count, setCount] = useState<number>(0);
const counterUpdated = (newAmount: number) => {
setCount(newAmount);
if (updateCount != undefined) {
updateCount(newAmount);
}
}
const minusDisabled = (!allowNegative && count <= 0)
return (
<div className="flex flex-row items-center gap-2">
<Button disabled={minusDisabled}
onClick={() => counterUpdated(count - 1)}>-</Button>
<Typography variant={"largeText"} className="mx-2">{count}</Typography>
<Button disabled={!addEnabled}
onClick={() => counterUpdated(count + 1)}>+</Button>
</div>
)
}
+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';
// eslint-disable-next-line react-refresh/only-export-components
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 }
+21
View File
@@ -5,6 +5,9 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import Root from './routes/Root';
import ErrorPage from './error-page';
import Homepage from './routes/home-page';
import SearchPage from './routes/search-page';
import BookPage from './routes/book-page';
import SuccessPage from './routes/success-page';
const router = createBrowserRouter([
{
@@ -12,14 +15,32 @@ const router = createBrowserRouter([
element: <Root />,
errorElement: <ErrorPage />,
children: [
{
path: "/",
element: <Homepage />
},
{
path: "/home",
element: <Homepage />
},
{
path: "/search",
element: <SearchPage />
},
{
path: "/book",
element: <BookPage />
},
{
path: "/success",
element: <SuccessPage />
}
]
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
+25
View File
@@ -0,0 +1,25 @@
export type NewBooking = {
arrivalDate: Date,
departureDate: Date,
customerDetails: NewBookingCustomerDetails,
rooms: NewBookingRoom[]
};
export type NewBookingCustomerDetails = {
name: string | null,
surname: string | null,
email: string | null,
phoneNumber: string | null
};
export type NewBookingRoom = {
roomCategory: number,
guestNumber: number,
amount: number,
price: number
};
export type NewBookingResponse = {
refNumber: number,
}
+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,
}
+30
View File
@@ -0,0 +1,30 @@
export type RoomAvailabity = {
roomCategory: number,
availability: number
}
export type RoomPrice = {
guestNumber: number,
price: 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 AppFooter from '../components/app-footer'
import { ThemeProvider } from '@/components/theme-provider'
import { Toaster } from '@/components/ui/toaster'
function Root() {
@@ -10,9 +11,10 @@ function Root() {
<>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<AppHeader />
<div className='appPage'>
<div className='appPage bg-background text-foreground'>
<Outlet />
</div>
<Toaster />
<AppFooter />
</ThemeProvider>
</>
+125
View File
@@ -0,0 +1,125 @@
import { APISendCreateBooking } from "@/api/api-booking"
import { BookingDetails } from "@/components/booking/booking-details"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
import { Typography } from "@/components/ui/typography"
import { useToast } from "@/components/ui/use-toast"
import { useAppStore } from "@/store"
import { ChangeEvent, useState } from "react"
import { useNavigate } from "react-router-dom"
enum PageState {
noData, showForm, sending, created, error
}
function BookPage() {
const [pageState, setPageState] = useState<PageState>(PageState.showForm);
const bookingData = useAppStore((state) => state.newBookingData)
const setCustomerData = useAppStore((state) => state.setBookingCustomer);
const { toast } = useToast();
const navigate = useNavigate();
if (!bookingData || !bookingData.customerDetails) {
return (
<Typography variant={"h2"}>You must select some rooms...</Typography>
)
}
function handleOnChange(e: ChangeEvent<HTMLInputElement>) {
if (bookingData == null) {
return;
}
const newCustomerData = bookingData.customerDetails;
switch (e.target.name) {
case "name":
newCustomerData.name = e.target.value;
break;
case "surname":
newCustomerData.surname = e.target.value;
break;
case "email":
newCustomerData.email = e.target.value;
break;
case "phoneNumber":
newCustomerData.phoneNumber = e.target.value;
break;
default:
break;
}
setCustomerData(newCustomerData);
}
function handleOnBookingSubmit() {
const customerDetails = bookingData?.customerDetails;
if (!customerDetails ||
customerDetails.name == "" || customerDetails.surname == "" || customerDetails.phoneNumber == "" || customerDetails.email == "") {
toast({
variant: "destructive",
title: "Uh oh! You must fill all fields.",
});
return;
}
setPageState(PageState.sending);
console.log(bookingData);
sendCreateRequest();
}
const sendCreateRequest = async () => {
const rs = await APISendCreateBooking(bookingData);
if (rs.isError) {
setPageState(PageState.error);
} else {
navigate("/success", { state: rs.bookingResponse })
}
}
return (
<>
<Typography variant={"h1"}>Booking Details</Typography>
<Separator className="my-10" />
<div>
<Typography variant={"h3"}>Personal Details</Typography>
<div>
<div>
<Label htmlFor="name">Name</Label>
<Input type="text" name="name" id="name" placeholder="Name" value={bookingData.customerDetails.name ?? ""}
onChange={handleOnChange} />
</div>
<div>
<Label htmlFor="surname">Surname</Label>
<Input type="text" name="surname" id="surname" placeholder="Surname" value={bookingData.customerDetails.surname ?? ""}
onChange={handleOnChange} />
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input type="email" name="email" id="email" placeholder="Email" value={bookingData.customerDetails.email ?? ""}
onChange={handleOnChange} />
</div>
<div>
<Label htmlFor="phone">Phone Number</Label>
<Input type="tel" name="phoneNumber" id="phone" placeholder="Number" value={bookingData.customerDetails.phoneNumber ?? ""}
onChange={handleOnChange} />
</div>
</div>
</div>
<Separator className="my-10" />
<BookingDetails mode="View"></BookingDetails>
<Separator className="my-10" />
<Button onClick={handleOnBookingSubmit} disabled={pageState == PageState.sending}>
<Typography variant={"h3"} className="text-primary-foreground">Book it now!</Typography>
</Button>
</>
)
}
export default BookPage
+37 -8
View File
@@ -1,18 +1,47 @@
import { useAPIRoomCategories } from "@/api/api-roomCategories"
import { RoomItemCard } from "@/components/Rooms/room-item-card"
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 { Skeleton } from "@/components/ui/skeleton"
import { Typography } from "@/components/ui/typography"
import { RoomCategory } from "@/model/RoomCategory"
import { SearchFilter } from "@/model/SearchFilter"
import { useNavigate } from "react-router-dom"
function Homepage() {
const { roomCategoryList, isLoading, isError } = useAPIRoomCategories();
const navigate = useNavigate();
const onBtSearchClick = (filter: SearchFilter) => {
navigate("/search", {state: filter})
}
const roomList = roomCategoryList?.map((element: RoomCategory) => {
return (
<RoomItemCard key={element.id} room={element} />
);
});
return (
<div className="bg-background text-foreground">
<Button>Test</Button>
<Separator className="my-10"/>
<SearchFilterCard />
<Separator className="my-10"/>
<RoomItemCard />
</div>
<>
<Typography variant={"h1"}>Somnia Rooms</Typography>
<Separator className="my-10" />
<SearchFilterCard onSearchClick={onBtSearchClick}/>
<Separator className="my-10" />
<div className="grid grid-cols-2 gap-4">
{roomList}
</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} />
</>
)
}
+95
View File
@@ -0,0 +1,95 @@
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 { BookingDetails } from "@/components/booking/booking-details"
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 { useAppStore } from "@/store"
import { useMemo, useState } from "react"
import { useNavigate } from "react-router-dom"
//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 searchFilter = useAppStore((state) => (state.searchFilter));
const resetBooking = useAppStore((state) => (state.resetBookingState));
const navigate = useNavigate();
const [isSearched, setIsSearched] = useState<boolean>(false);
const onBtSearchClick = (newFilter: SearchFilter) => {
console.log("onBtSearchClick: ");
console.log(newFilter);
if (newFilter.dateRange && newFilter.dateRange.from && newFilter.dateRange.to) {
setIsSearched(true);
resetBooking(newFilter.dateRange.from, newFilter.dateRange.to);
}
}
const onBookingContinue = () => {
navigate("/book");
}
//This seems a little bit overkill, but IDK.
const availabilityFilter: AvailabilityFilter | null = useMemo(
() => {
if (searchFilter && searchFilter.dateRange && searchFilter.dateRange.from && searchFilter.dateRange.to) {
const newAvailabilityFilter: AvailabilityFilter = {
startDate: searchFilter.dateRange.from.toDateString(),
endDate: searchFilter.dateRange.to.toDateString()
};
return newAvailabilityFilter;
}
return null;
},
[searchFilter]
);
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" />
<BookingDetails onBookingConfirm={onBookingContinue} mode={"Book"}></BookingDetails>
<Separator className="my-10" />
<BackendConnectionStatus isLoading={isLoading} isError={isError} />
</div>
)
}
export default SearchPage
+47
View File
@@ -0,0 +1,47 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { Typography } from "@/components/ui/typography"
import { NewBookingResponse } from "@/model/Bookings"
import { useAppStore } from "@/store"
import { addDays } from "date-fns"
import { useEffect, useState } from "react"
import { useLocation, useNavigate } from "react-router-dom"
function SuccessPage() {
const [pageState, setPageState] = useState<NewBookingResponse|null>(null)
const resetBookingState = useAppStore((state) => state.resetBookingState);
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
if(location.state == null) {
navigate("/home");
}
setPageState(location.state);
}, [location, navigate])
const handleBtBackClick = () => {
resetBookingState(new Date(), addDays(new Date(), 2));
navigate("/home");
}
return (
<Card>
<CardHeader>
<Typography variant={"h2"}>Booking Completed</Typography>
</CardHeader>
<CardContent>
<Typography variant={"largeText"}>Booking ref: {pageState?.refNumber}</Typography>
<Button className="my-4" onClick={handleBtBackClick}>
<Typography variant={"largeText"} className="text-primary-foreground">Go Back</Typography>
</Button>
</CardContent>
</Card>
)
}
export default SuccessPage
+95
View File
@@ -0,0 +1,95 @@
import { create } from 'zustand'
import { SearchFilter } from './model/SearchFilter'
import { NewBooking, NewBookingCustomerDetails, NewBookingRoom } from './model/Bookings'
interface AppState {
searchFilter: SearchFilter
setSearchFilter: (newFilter: SearchFilter) => void
newBookingData: NewBooking | null
setNewBooking: (newBooking: NewBooking) => void
resetBookingState: (arrival: Date, departure: Date) => void
addNewBookingRoom: (newRoom: NewBookingRoom) => void
setBookingCustomer: (newCustomer: NewBookingCustomerDetails) => void
}
const defaultSearchFilter: SearchFilter = {
dateRange: { from: new Date() }
}
const defaultNewBooking: NewBooking = {
arrivalDate: new Date(),
departureDate: new Date(),
customerDetails: {
name: null,
surname: null,
email: null,
phoneNumber: null
},
rooms: []
}
function getDefaultBookingData(arrivalDate: Date, departureDate: Date) {
return {
...defaultNewBooking,
arrivalDate: arrivalDate,
departureDate: departureDate
}
}
function addNewBookingRoomToCurrentState(newRoom: NewBookingRoom, currentState: NewBooking | null): NewBooking | null {
if (currentState == null) {
return currentState;
}
//Remove rooms with "same key".
const filteredRooms = currentState.rooms.filter((room) => {
const diffRoom = room.roomCategory != newRoom.roomCategory;
const diffAmount = room.guestNumber != newRoom.guestNumber;
if(diffRoom || (!diffRoom && diffAmount)) {
return true;
}
return false;
})
console.log(filteredRooms);
if (newRoom.amount > 0) {
filteredRooms.push(newRoom);
}
const newState: NewBooking = {
...currentState,
rooms: filteredRooms ?? []
}
console.log("AddRoomNewState:");
console.log(newState);
return newState;
}
function setBookingCustomer(newCustomer: NewBookingCustomerDetails, currentState: NewBooking | null) {
if(currentState == null) {
return currentState;
}
const newState: NewBooking = {
...currentState,
customerDetails: newCustomer
};
return newState;
}
export const useAppStore = create<AppState>()((set) => ({
searchFilter: defaultSearchFilter,
setSearchFilter: (newFilter) => set(() => ({ searchFilter: newFilter })),
newBookingData: null,
setNewBooking: (newBooking) => set(() => ({ newBookingData: newBooking })),
resetBookingState: (arrival, departure) =>
set(() => ({ newBookingData: getDefaultBookingData(arrival, departure) })),
addNewBookingRoom: (newRoom: NewBookingRoom) =>
set((state) => ({ newBookingData: addNewBookingRoomToCurrentState(newRoom, state.newBookingData) })),
setBookingCustomer: (newCustomer: NewBookingCustomerDetails) =>
set((state) => ({newBookingData: setBookingCustomer(newCustomer, state.newBookingData)}))
}))