Compare commits
8
Commits
70e483bc45
...
899d27f468
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
899d27f468 | ||
|
|
90fa82234d | ||
|
|
b8ec6f7bdd | ||
|
|
94312de6e2 | ||
|
|
d4094d09c6 | ||
|
|
42437881c4 | ||
|
|
5d2eb2e635 | ||
|
|
2b3d1eafd3 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"endOfLine": "lf",
|
"endOfLine": "auto",
|
||||||
"plugins": ["prettier-plugin-tailwindcss"],
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
"tailwindStylesheet": "./src/index.css"
|
"tailwindStylesheet": "./src/index.css"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
User app for SomniaRooms.
|
User app for SomniaRooms.
|
||||||
|
|
||||||
## Docker Usage:
|
## Podman Usage:
|
||||||
|
|
||||||
### Dev
|
### Dev
|
||||||
|
|
||||||
**Build Image:**
|
**Build Image:**
|
||||||
|
|
||||||
```
|
```
|
||||||
docker build -t somniarooms-app:dev --target final-dev .
|
podman build -t somniarooms-app:dev --target final-dev .
|
||||||
```
|
```
|
||||||
|
|
||||||
**Docker Run Image:**
|
**Podman Run Image:**
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -d -it --rm -v ${PWD}:/app -v /app/node_modules -p 3000:3000 --name somniarooms-app somniarooms-app:dev
|
podman run -d -it --rm -v ${PWD}:/app -v /app/node_modules -p 3000:3000 --name somniarooms-app somniarooms-app:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Notas:
|
Notas:
|
||||||
@@ -28,24 +28,24 @@ Notas:
|
|||||||
**Build Image**
|
**Build Image**
|
||||||
|
|
||||||
```
|
```
|
||||||
docker build -t somniarooms-app:prod --target final-prod .
|
podman build -t somniarooms-app:prod --target final-prod .
|
||||||
```
|
```
|
||||||
|
|
||||||
**Docker Run**
|
**Podman Run**
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -d -it -p 8080:80 --name somniarooms-app somniarooms-app:prod
|
podman run -d -it -p 8080:80 --name somniarooms-app somniarooms-app:prod
|
||||||
```
|
```
|
||||||
|
|
||||||
**Docker run with env vars:**
|
**Podman 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
|
podman 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.
|
## Env Vars.
|
||||||
|
|
||||||
As this is distributed as a docker image ready to use, you must set some env variables to configure the app:
|
As this is distributed as a image ready to use, you must set some env variables to configure the app:
|
||||||
|
|
||||||
| Var | Default Value | Description |
|
| Var | Default Value | Description |
|
||||||
| :------------------------: | :----------------------------: | :------------------------------------------------------------------------------------------: |
|
| :------------------------: | :----------------------------: | :------------------------------------------------------------------------------------------: |
|
||||||
|
|||||||
Generated
+2230
-1020
File diff suppressed because it is too large
Load Diff
+31
-38
@@ -11,55 +11,48 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
|
||||||
"@radix-ui/react-popover": "^1.1.14",
|
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
|
||||||
"@radix-ui/react-toast": "^1.2.14",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
|
||||||
"@tailwindcss/vite": "^4.1.8",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"lucide-react": "^0.513.0",
|
"lucide-react": "^0.562.0",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-day-picker": "^9.7.0",
|
"react-day-picker": "^9.13.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.57.0",
|
"react-hook-form": "^7.70.0",
|
||||||
"react-router-dom": "^6.23.0",
|
"react-router-dom": "^6.30.2",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.8",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"zod": "^3.25.57",
|
"zod": "^3.25.76",
|
||||||
"zustand": "^5.0.5"
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.9",
|
"@eslint/compat": "^2.0.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@eslint/js": "^9.28.0",
|
"@eslint/js": "^9.39.2",
|
||||||
"@iconify-icon/react": "^3.0.0",
|
"@iconify-icon/react": "^3.0.3",
|
||||||
"@tailwindcss/postcss": "^4.1.8",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^22.15.30",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
||||||
"@typescript-eslint/parser": "^8.34.0",
|
"@typescript-eslint/parser": "^8.52.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.1",
|
"@vitejs/plugin-react-swc": "^4.2.2",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.26",
|
||||||
"globals": "^16.2.0",
|
"globals": "^17.0.0",
|
||||||
"postcss": "^8.5.4",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.12",
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
"tailwindcss": "^4.1.8",
|
"tailwindcss": "^4.1.18",
|
||||||
"tw-animate-css": "^1.3.4",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
'@tailwindcss/postcss': {},
|
"@tailwindcss/postcss": {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
import { fetcherUnwraperWithFilter } from "./backend";
|
import { fetcherUnwraperWithFilter } from "./backend";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import {
|
import {
|
||||||
AvailabilityFilter,
|
SearchPeriodRequest,
|
||||||
AvailabilityRequest,
|
|
||||||
FareFilter,
|
|
||||||
FareRequest,
|
|
||||||
RoomAvailabity,
|
RoomAvailabity,
|
||||||
RoomPrice,
|
RoomPriceSearchResponse,
|
||||||
} from "@/model/RoomSearch";
|
} from "@/model/RoomSearch";
|
||||||
|
|
||||||
export function useAPIRoomAvailability(filter: AvailabilityFilter | null) {
|
export function useAPIRoomAvailability(filter: SearchPeriodRequest | null) {
|
||||||
const request: AvailabilityRequest | null = filter
|
|
||||||
? {
|
|
||||||
startDate: filter.startDate,
|
|
||||||
endDate: filter.endDate,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const { data, isLoading, error } = useSWR(
|
const { data, isLoading, error } = useSWR(
|
||||||
request ? ["/api/search/availability", request] : null,
|
filter ? ["/api/search/availability", filter] : null,
|
||||||
([url, request]) =>
|
([url, request]) =>
|
||||||
fetcherUnwraperWithFilter<RoomAvailabity[]>(url, request),
|
fetcherUnwraperWithFilter<RoomAvailabity[]>(url, request),
|
||||||
);
|
);
|
||||||
@@ -30,18 +20,11 @@ export function useAPIRoomAvailability(filter: AvailabilityFilter | null) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAPIRoomPrices(filter: FareFilter | null) {
|
export function useAPIRoomPrices(filter: SearchPeriodRequest | null) {
|
||||||
const request: FareRequest | null = filter
|
|
||||||
? {
|
|
||||||
roomCategory: filter.roomCategory,
|
|
||||||
startDate: filter.startDate,
|
|
||||||
endDate: filter.endDate,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const { data, isLoading, error } = useSWR(
|
const { data, isLoading, error } = useSWR(
|
||||||
request ? ["/api/search/pricesCategory", request] : null,
|
filter ? ["/api/search/prices", filter] : null,
|
||||||
([url, request]) => fetcherUnwraperWithFilter<RoomPrice[]>(url, request),
|
([url, request]) =>
|
||||||
|
fetcherUnwraperWithFilter<RoomPriceSearchResponse[]>(url, request),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "../ui/alert-dialog";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ConstsNav, ConstsTexts } from "@/utils/Consts";
|
||||||
|
|
||||||
|
type OnCancelClickFunction = () => void;
|
||||||
|
|
||||||
|
export function ContactUsDialog({
|
||||||
|
onCancelClick,
|
||||||
|
isOpen,
|
||||||
|
cancelButtonText = "Cancel",
|
||||||
|
}: {
|
||||||
|
cancelButtonText: string;
|
||||||
|
onCancelClick: OnCancelClickFunction;
|
||||||
|
isOpen: boolean;
|
||||||
|
}) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const navToContactPage = (text: string) => {
|
||||||
|
navigate(ConstsNav.page_Contact, {
|
||||||
|
state: text,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog open={isOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{ConstsTexts.contactUsAlertDialog_title}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{ConstsTexts.contactUsAlertDialog_text}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel asChild>
|
||||||
|
<Button variant={"secondary"} onClick={onCancelClick}>
|
||||||
|
{cancelButtonText}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction asChild>
|
||||||
|
<Button
|
||||||
|
variant={"link"}
|
||||||
|
onClick={() =>
|
||||||
|
navToContactPage("Contact us for a personalized quote!")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Go to contact page
|
||||||
|
</Button>
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useAPIRoomCategory } from "@/api/api-roomCategories";
|
|
||||||
import { Card, CardContent } from "../ui/card";
|
import { Card, CardContent } from "../ui/card";
|
||||||
import { Typography } from "../ui/typography";
|
import { Typography } from "../ui/typography";
|
||||||
import {
|
import {
|
||||||
@@ -11,14 +10,14 @@ import {
|
|||||||
} from "../ui/table";
|
} from "../ui/table";
|
||||||
import { Skeleton } from "../ui/skeleton";
|
import { Skeleton } from "../ui/skeleton";
|
||||||
import { useAppStore } from "@/store";
|
import { useAppStore } from "@/store";
|
||||||
import { useAPIRoomPrices } from "@/api/api-roomSearch";
|
|
||||||
import { NumberSelector } from "../ui/number-selector";
|
import { NumberSelector } from "../ui/number-selector";
|
||||||
import { NewBooking, NewBookingRoom } from "@/model/Bookings";
|
import { NewBooking, NewBookingRoom } from "@/model/Bookings";
|
||||||
import { RoomItemCard } from "./room-item-card";
|
import { RoomItemCard } from "./room-item-card";
|
||||||
import { IconWithTooltip } from "../ui/icon-number-tooltip";
|
import { IconWithTooltip } from "../ui/icon-number-tooltip";
|
||||||
import { UserIcon } from "lucide-react";
|
import { UserIcon } from "lucide-react";
|
||||||
import { makeFareFilterFromSearchFilter } from "@/utils/utils";
|
import { makeSearchPeriodRequestFromSearchFilter } from "@/utils/utils";
|
||||||
import { SearchFilter } from "@/model/InternalTypes";
|
import { SearchFilter } from "@/model/InternalTypes";
|
||||||
|
import { useRoomCategory, useRoomPrice } from "@/utils/apiHooks";
|
||||||
|
|
||||||
function RoomBooking({
|
function RoomBooking({
|
||||||
roomCategoryId,
|
roomCategoryId,
|
||||||
@@ -77,24 +76,17 @@ function RoomBooking({
|
|||||||
const canAddMoreRooms: boolean = availableAmount > totalSelectAmount;
|
const canAddMoreRooms: boolean = availableAmount > totalSelectAmount;
|
||||||
|
|
||||||
//This seems a little bit overkill, but IDK.
|
//This seems a little bit overkill, but IDK.
|
||||||
const fareFilter = makeFareFilterFromSearchFilter(
|
const fareFilter = makeSearchPeriodRequestFromSearchFilter(appSearchFilter);
|
||||||
appSearchFilter,
|
|
||||||
|
const { value: roomCategory, isLoading: isLoadingCategory } =
|
||||||
|
useRoomCategory(roomCategoryId);
|
||||||
|
const { value: roomPriceList, isLoading: isLoadingPrices } = useRoomPrice(
|
||||||
|
fareFilter,
|
||||||
roomCategoryId,
|
roomCategoryId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
|
||||||
roomCategory,
|
|
||||||
isLoading: isLoadingCategory,
|
|
||||||
isError: isErrorCategory,
|
|
||||||
} = useAPIRoomCategory(roomCategoryId);
|
|
||||||
const {
|
|
||||||
roomPriceList,
|
|
||||||
isLoading: isLoadingPrices,
|
|
||||||
isError: isErrorPrices,
|
|
||||||
} = useAPIRoomPrices(fareFilter);
|
|
||||||
|
|
||||||
const isLoading = isLoadingCategory || isLoadingPrices;
|
const isLoading = isLoadingCategory || isLoadingPrices;
|
||||||
const isError = isErrorCategory || isErrorPrices;
|
const isError = roomCategory == null;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Skeleton className="mx-auto h-[150px] w-[600px]" />;
|
return <Skeleton className="mx-auto h-[150px] w-[600px]" />;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { IconWithTooltip } from "../ui/icon-number-tooltip";
|
|||||||
import defaultRoomImg from "@/res/SomniaRoomImage.svg";
|
import defaultRoomImg from "@/res/SomniaRoomImage.svg";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Icon } from "@iconify-icon/react";
|
import { Icon } from "@iconify-icon/react";
|
||||||
import { useConfigVariable } from "@/utils/hooks";
|
import { useConfigVariable } from "@/utils/apiHooks";
|
||||||
import { eConfigVarKeys } from "@/model/ConfigVar";
|
import { eConfigVarKeys } from "@/model/ConfigVar";
|
||||||
|
|
||||||
interface RoomItemCardProps {
|
interface RoomItemCardProps {
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import { SearchIcon } from "lucide-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SearchFilter } from "@/model/InternalTypes";
|
import { SearchFilter } from "@/model/InternalTypes";
|
||||||
import { DateRange } from "react-day-picker";
|
import { DateRange } from "react-day-picker";
|
||||||
import { useToast } from "../ui/use-toast";
|
import { isValidSearchFilter } from "@/utils/utils";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ConstsNav, ConstsTexts } from "@/utils/Consts";
|
||||||
|
import { ContactUsDialog } from "../Dialogs/ContactUsDialog";
|
||||||
|
import { useCheckForNightCount } from "@/utils/hooks";
|
||||||
|
|
||||||
type funOnSearchClick = (newFilter: SearchFilter) => void;
|
type funOnSearchClick = (newFilter: SearchFilter) => void;
|
||||||
|
|
||||||
@@ -23,42 +27,74 @@ export function SearchFilterCard({
|
|||||||
const [searchFilter, setSearchFilter] = useState<SearchFilter | null>(
|
const [searchFilter, setSearchFilter] = useState<SearchFilter | null>(
|
||||||
initialsearchFilter ?? null,
|
initialsearchFilter ?? null,
|
||||||
);
|
);
|
||||||
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||||
|
const [isContactDialogOpen, setIsContactDialogOpen] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
const { toast } = useToast();
|
const {
|
||||||
|
isLoading: isNightCountLoading,
|
||||||
|
isOverApiMax: nightCountCheckIsOverApi,
|
||||||
|
shouldShowContactDialog: nightCountShowDialog,
|
||||||
|
} = useCheckForNightCount(searchFilter);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Validate and call callback on btSearch click.
|
// Validate and call callback on btSearch click.
|
||||||
//TODOME: Add data validations and show messages as form instead of toasts.
|
|
||||||
const btSearchClick = () => {
|
const btSearchClick = () => {
|
||||||
if (!searchFilter || !searchFilter.dateRange) {
|
if (!isValidSearchFilter(searchFilter)) {
|
||||||
toast({
|
setErrorMsg("Select a valid 'from' and 'to' dates!");
|
||||||
variant: "destructive",
|
|
||||||
title: "Uh oh! Invalid Search.",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!searchFilter.dateRange.from || !searchFilter.dateRange.to) {
|
//Check for max night count.
|
||||||
toast({
|
//API Config vars values must be loaded before this! Unexpected behaviours may happen otherwise.
|
||||||
variant: "destructive",
|
if (nightCountCheckIsOverApi) {
|
||||||
title: "Uh oh! Invalid Search.",
|
//Navigate to contact page and show info.
|
||||||
description: "You should select a 'from' and 'to' dates",
|
navigate(ConstsNav.page_Contact, {
|
||||||
|
state: ConstsTexts.navToContact_maxAllowedNightCount,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (nightCountShowDialog) {
|
||||||
|
//Show Alert dialog.
|
||||||
|
setIsContactDialogOpen(true);
|
||||||
|
//Page onSearch action will be executed based on the dialog result.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Execute page onSearch action.
|
||||||
if (onSearchClick) {
|
if (onSearchClick) {
|
||||||
onSearchClick(searchFilter);
|
onSearchClick(searchFilter);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle Element updates.
|
// Handle internal Searchfilter updates.
|
||||||
const updateDateRange = (newRange: DateRange) => {
|
const updateDateRange = (newRange: DateRange) => {
|
||||||
setSearchFilter({
|
const newSearchFilter = {
|
||||||
...searchFilter,
|
...searchFilter,
|
||||||
dateRange: newRange,
|
dateRange: newRange,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
setSearchFilter(newSearchFilter);
|
||||||
|
|
||||||
|
if (isValidSearchFilter(newSearchFilter)) {
|
||||||
|
setErrorMsg(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onContactDialogClose = () => {
|
||||||
|
setIsContactDialogOpen(false);
|
||||||
|
|
||||||
|
if (!isValidSearchFilter(searchFilter)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onSearchClick) {
|
||||||
|
onSearchClick(searchFilter);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSearchButtonDisabled = isNightCountLoading;
|
||||||
|
|
||||||
const datePickerElement =
|
const datePickerElement =
|
||||||
searchFilter == null ? (
|
searchFilter == null ? (
|
||||||
<DatePickerWithRange
|
<DatePickerWithRange
|
||||||
@@ -74,23 +110,43 @@ export function SearchFilterCard({
|
|||||||
></DatePickerWithRange>
|
></DatePickerWithRange>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const errorMsgLabel = (
|
||||||
|
<Typography variant={"errorText"} className="mt-2 text-lg">
|
||||||
|
{errorMsg}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
const searchHasErrors = errorMsg != null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<>
|
||||||
<CardHeader className="m-0 mb-5 p-0">
|
<ContactUsDialog
|
||||||
<Typography variant={"h2"} as="h1" className="mx-5 py-2">
|
isOpen={isContactDialogOpen}
|
||||||
Reserva ahora
|
onCancelClick={onContactDialogClose}
|
||||||
</Typography>
|
cancelButtonText="Continue Search"
|
||||||
</CardHeader>
|
/>
|
||||||
<CardContent>
|
<Card>
|
||||||
{datePickerElement}
|
<CardHeader className="m-0 mb-5 p-0">
|
||||||
<Separator className="my-4" />
|
<Typography variant={"h2"} as="h1" className="mx-5 py-2">
|
||||||
<Button size={"lg"} onClick={btSearchClick}>
|
Reserva ahora
|
||||||
<SearchIcon className="me-2 text-primary-foreground" />
|
|
||||||
<Typography variant={"h3"} className="text-primary-foreground">
|
|
||||||
Buscar
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Button>
|
</CardHeader>
|
||||||
</CardContent>
|
<CardContent>
|
||||||
</Card>
|
{datePickerElement}
|
||||||
|
{searchHasErrors && errorMsgLabel}
|
||||||
|
<Separator className="my-4" />
|
||||||
|
<Button
|
||||||
|
size={"lg"}
|
||||||
|
onClick={btSearchClick}
|
||||||
|
disabled={isSearchButtonDisabled}
|
||||||
|
variant={searchHasErrors ? "destructive" : "default"}
|
||||||
|
>
|
||||||
|
<SearchIcon className="me-2 text-primary-foreground" />
|
||||||
|
<Typography variant={"h3"} className="text-primary-foreground">
|
||||||
|
Buscar
|
||||||
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useConfigVariable } from "@/utils/hooks";
|
|
||||||
import { Separator } from "./ui/separator";
|
import { Separator } from "./ui/separator";
|
||||||
import { Typography } from "./ui/typography";
|
import { Typography } from "./ui/typography";
|
||||||
import { eConfigVarKeys } from "@/model/ConfigVar";
|
import { eConfigVarKeys } from "@/model/ConfigVar";
|
||||||
@@ -9,6 +8,7 @@ import {
|
|||||||
import { Skeleton } from "./ui/skeleton";
|
import { Skeleton } from "./ui/skeleton";
|
||||||
import { Mail, Phone } from "lucide-react";
|
import { Mail, Phone } from "lucide-react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { useConfigVariable } from "@/utils/apiHooks";
|
||||||
|
|
||||||
function AppFooter() {
|
function AppFooter() {
|
||||||
const { value: varCompanyLogo, isLoading: isLogoLoading } = useConfigVariable(
|
const { value: varCompanyLogo, isLoading: isLogoLoading } = useConfigVariable(
|
||||||
@@ -41,7 +41,7 @@ function AppFooter() {
|
|||||||
isMailLoading;
|
isMailLoading;
|
||||||
const getCompanyElement = () => {
|
const getCompanyElement = () => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Skeleton className="h-[64px]" />;
|
return <Skeleton className="h-16" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -63,7 +63,7 @@ function AppFooter() {
|
|||||||
|
|
||||||
const getContactElement = () => {
|
const getContactElement = () => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Skeleton className="w-[64px]" />;
|
return <Skeleton className="w-16" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Link } from "react-router-dom";
|
|||||||
import { Bed, BookAIcon, HomeIcon } from "lucide-react";
|
import { Bed, BookAIcon, HomeIcon } from "lucide-react";
|
||||||
import { eConfigVarKeys } from "@/model/ConfigVar";
|
import { eConfigVarKeys } from "@/model/ConfigVar";
|
||||||
import { Skeleton } from "./ui/skeleton";
|
import { Skeleton } from "./ui/skeleton";
|
||||||
import { useConfigVariable } from "@/utils/hooks";
|
import { useConfigVariable } from "@/utils/apiHooks";
|
||||||
|
|
||||||
function AppHeader() {
|
function AppHeader() {
|
||||||
const { value: varCompanyLogo, isLoading: isLogoLoading } = useConfigVariable(
|
const { value: varCompanyLogo, isLoading: isLogoLoading } = useConfigVariable(
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { Button } from "../ui/button";
|
|||||||
import { useAPIRoomCategoryList } from "@/api/api-roomCategories";
|
import { useAPIRoomCategoryList } from "@/api/api-roomCategories";
|
||||||
import { IconWithTooltip } from "../ui/icon-number-tooltip";
|
import { IconWithTooltip } from "../ui/icon-number-tooltip";
|
||||||
import { UserIcon } from "lucide-react";
|
import { UserIcon } from "lucide-react";
|
||||||
|
import { useCheckForGuestNumber } from "@/utils/hooks";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ConstsNav, ConstsTexts } from "@/utils/Consts";
|
||||||
|
import { ContactUsDialog } from "../Dialogs/ContactUsDialog";
|
||||||
|
|
||||||
type OnBookingClickFunction = () => void;
|
type OnBookingClickFunction = () => void;
|
||||||
type BookingDetailsModes = "Book" | "View";
|
type BookingDetailsModes = "Book" | "View";
|
||||||
@@ -18,10 +23,42 @@ export function BookingDetails({
|
|||||||
onBookingConfirm?: OnBookingClickFunction;
|
onBookingConfirm?: OnBookingClickFunction;
|
||||||
mode: BookingDetailsModes;
|
mode: BookingDetailsModes;
|
||||||
}) {
|
}) {
|
||||||
|
const [isContactDialogOpen, setIsContactDialogOpen] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
const bookingData = useAppStore((state) => state.currentBooking);
|
const bookingData = useAppStore((state) => state.currentBooking);
|
||||||
const roomCategoryList = useAPIRoomCategoryList();
|
const { roomCategoryList } = useAPIRoomCategoryList();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading: isGuestNumberCheckLoading,
|
||||||
|
isOverApiMax: guestNumberCheckIsOverApi,
|
||||||
|
shouldShowContactDialog: guestNumberShowDialog,
|
||||||
|
} = useCheckForGuestNumber(bookingData);
|
||||||
|
|
||||||
const onBtBookingClick = () => {
|
const onBtBookingClick = () => {
|
||||||
|
//Check for Guest Number limits.
|
||||||
|
//API Config vars values must be loaded before this! Unexpected behaviours may happen otherwise.
|
||||||
|
if (guestNumberCheckIsOverApi) {
|
||||||
|
//Navigate to contact page and show info.
|
||||||
|
navigate(ConstsNav.page_Contact, {
|
||||||
|
state: ConstsTexts.navToContact_maxAllowedGuestNumber,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (guestNumberShowDialog) {
|
||||||
|
//Show Alert dialog.
|
||||||
|
setIsContactDialogOpen(true);
|
||||||
|
//Page onSearch action will be executed based on the dialog result.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onBookingConfirm) {
|
||||||
|
onBookingConfirm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onContactDialogClose = () => {
|
||||||
if (onBookingConfirm) {
|
if (onBookingConfirm) {
|
||||||
onBookingConfirm();
|
onBookingConfirm();
|
||||||
}
|
}
|
||||||
@@ -43,9 +80,7 @@ export function BookingDetails({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const roomList = bookingData.rooms.map((room) => {
|
const roomList = bookingData.rooms.map((room) => {
|
||||||
const category = roomCategoryList.roomCategoryList
|
const category = roomCategoryList?.find((c) => c.id == room.roomCategory);
|
||||||
?.filter((c) => c.id == room.roomCategory)
|
|
||||||
.at(0);
|
|
||||||
const photo = category?.roomCategoryPhotos.at(0);
|
const photo = category?.roomCategoryPhotos.at(0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,40 +124,51 @@ export function BookingDetails({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<>
|
||||||
<CardHeader>
|
<ContactUsDialog
|
||||||
<Typography variant={"h2"} as="h2">
|
isOpen={isContactDialogOpen}
|
||||||
Your Booking
|
cancelButtonText="Continue booking"
|
||||||
</Typography>
|
onCancelClick={onContactDialogClose}
|
||||||
</CardHeader>
|
/>
|
||||||
<CardContent>
|
<Card>
|
||||||
<Typography variant={"h3"}>Your Dates</Typography>
|
<CardHeader>
|
||||||
<Typography variant={"largeText"}>
|
<Typography variant={"h2"} as="h2">
|
||||||
Arrival: {bookingData.arrivalDate.toLocaleDateString()} - Departure:{" "}
|
Your Booking
|
||||||
{bookingData.departureDate.toLocaleDateString()} <br></br>
|
</Typography>
|
||||||
Number of nights:{" "}
|
</CardHeader>
|
||||||
{differenceInDays(
|
<CardContent>
|
||||||
bookingData.departureDate,
|
<Typography variant={"h3"}>Your Dates</Typography>
|
||||||
bookingData.arrivalDate,
|
<Typography variant={"largeText"}>
|
||||||
) + 1}
|
Arrival: {bookingData.arrivalDate.toLocaleDateString()} - Departure:{" "}
|
||||||
</Typography>
|
{bookingData.departureDate.toLocaleDateString()} <br></br>
|
||||||
<Separator className="my-2"></Separator>
|
Number of nights:{" "}
|
||||||
<Typography variant={"h3"}>Your Rooms</Typography>
|
{differenceInDays(
|
||||||
<div className="">{roomList}</div>
|
bookingData.departureDate,
|
||||||
<Separator className="my-2"></Separator>
|
bookingData.arrivalDate,
|
||||||
<Typography variant={"h3"}>Total Price</Typography>
|
)}
|
||||||
<Typography variant={"h3"}>{totalPrice / 100} €</Typography>
|
</Typography>
|
||||||
{mode == "Book" && (
|
<Separator className="my-2"></Separator>
|
||||||
<Button className="my-2" onClick={onBtBookingClick}>
|
<Typography variant={"h3"}>Your Rooms</Typography>
|
||||||
<Typography
|
<div className="">{roomList}</div>
|
||||||
variant={"largeText"}
|
<Separator className="my-2"></Separator>
|
||||||
className="text-primary-foreground"
|
<Typography variant={"h3"}>Total Price</Typography>
|
||||||
|
<Typography variant={"h3"}>{totalPrice / 100} €</Typography>
|
||||||
|
{mode == "Book" && (
|
||||||
|
<Button
|
||||||
|
className="my-2"
|
||||||
|
onClick={onBtBookingClick}
|
||||||
|
disabled={isGuestNumberCheckLoading}
|
||||||
>
|
>
|
||||||
Book it!
|
<Typography
|
||||||
</Typography>
|
variant={"largeText"}
|
||||||
</Button>
|
className="text-primary-foreground"
|
||||||
)}
|
>
|
||||||
</CardContent>
|
Book it!
|
||||||
</Card>
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export function ModeToggle() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<DropdownMenuTrigger>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon">
|
||||||
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
<SunIcon className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||||
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
function AlertDialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||||
|
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
data-slot="alert-dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
data-slot="alert-dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
data-slot="alert-dialog-title"
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
data-slot="alert-dialog-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogAction({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDialogCancel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||||
|
return (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogPortal,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
};
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
|
||||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
||||||
secondary:
|
|
||||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
||||||
destructive:
|
|
||||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function Badge({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span"> &
|
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : "span";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="badge"
|
|
||||||
className={cn(badgeVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
|
||||||
export { Badge, badgeVariants };
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot as SlotPrimitive } from "radix-ui";
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -9,14 +9,13 @@ const buttonVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
outline:
|
outline:
|
||||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost:
|
ghost:
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
@@ -26,6 +25,8 @@ const buttonVariants = cva(
|
|||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
icon: "size-9",
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -37,19 +38,21 @@ const buttonVariants = cva(
|
|||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant = "default",
|
||||||
size,
|
size = "default",
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: React.ComponentProps<"button"> &
|
||||||
VariantProps<typeof buttonVariants> & {
|
VariantProps<typeof buttonVariants> & {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const Comp = asChild ? Slot : "button";
|
const Comp = asChild ? SlotPrimitive.Slot : "button";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="button"
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import {
|
|||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
|
import {
|
||||||
|
DayPicker,
|
||||||
|
getDefaultClassNames,
|
||||||
|
type DayButton,
|
||||||
|
} from "react-day-picker";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
@@ -71,7 +75,10 @@ function Calendar({
|
|||||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||||
defaultClassNames.dropdown_root,
|
defaultClassNames.dropdown_root,
|
||||||
),
|
),
|
||||||
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
|
dropdown: cn(
|
||||||
|
"absolute bg-popover inset-0 opacity-0",
|
||||||
|
defaultClassNames.dropdown,
|
||||||
|
),
|
||||||
caption_label: cn(
|
caption_label: cn(
|
||||||
"select-none font-medium",
|
"select-none font-medium",
|
||||||
captionLayout === "label"
|
captionLayout === "label"
|
||||||
@@ -95,7 +102,10 @@ function Calendar({
|
|||||||
defaultClassNames.week_number,
|
defaultClassNames.week_number,
|
||||||
),
|
),
|
||||||
day: cn(
|
day: cn(
|
||||||
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||||
|
props.showWeekNumber
|
||||||
|
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||||
|
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||||
defaultClassNames.day,
|
defaultClassNames.day,
|
||||||
),
|
),
|
||||||
range_start: cn(
|
range_start: cn(
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
<div
|
<div
|
||||||
data-slot="card-header"
|
data-slot="card-header"
|
||||||
className={cn(
|
className={cn(
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
import { Checkbox as CheckboxPrimitive } from "radix-ui";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -19,7 +19,7 @@ function Checkbox({
|
|||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxPrimitive.Indicator
|
||||||
data-slot="checkbox-indicator"
|
data-slot="checkbox-indicator"
|
||||||
className="flex items-center justify-center text-current transition-none"
|
className="grid place-content-center text-current transition-none"
|
||||||
>
|
>
|
||||||
<CheckIcon className="size-3.5" />
|
<CheckIcon className="size-3.5" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function DatePickerWithRange({
|
|||||||
id="dateFrom"
|
id="dateFrom"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
data-empty={!dateRange}
|
data-empty={!dateRange}
|
||||||
className="w-[100%] justify-start text-left font-normal data-[empty=true]:text-muted-foreground"
|
className="w-full justify-start text-left font-normal data-[empty=true]:text-muted-foreground"
|
||||||
>
|
>
|
||||||
<CalendarIcon />
|
<CalendarIcon />
|
||||||
{dateRange?.from ? (
|
{dateRange?.from ? (
|
||||||
@@ -117,11 +117,11 @@ export function DatePickerWithRange({
|
|||||||
id="dateTo"
|
id="dateTo"
|
||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
data-empty={!dateRange}
|
data-empty={!dateRange}
|
||||||
className="w-[100%] justify-start text-left font-normal data-[empty=true]:text-muted-foreground"
|
className="w-full justify-start text-left font-normal data-[empty=true]:text-muted-foreground"
|
||||||
>
|
>
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
{dateRange?.to ? (
|
{dateRange?.to ? (
|
||||||
format(dateRange.to, "LLL dd, y")
|
format(dateRange.to, "PPP")
|
||||||
) : (
|
) : (
|
||||||
<span>Pick a date</span>
|
<span>Pick a date</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -40,7 +40,7 @@ function DropdownMenuContent({
|
|||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -209,7 +209,7 @@ function DropdownMenuSubTrigger({
|
|||||||
data-slot="dropdown-menu-sub-trigger"
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -228,7 +228,7 @@ function DropdownMenuSubContent({
|
|||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot="dropdown-menu-sub-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
|
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
@@ -102,12 +102,14 @@ function FormLabel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
function FormControl({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SlotPrimitive.Slot>) {
|
||||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
useFormField();
|
useFormField();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slot
|
<SlotPrimitive.Slot
|
||||||
data-slot="form-control"
|
data-slot="form-control"
|
||||||
id={formItemId}
|
id={formItemId}
|
||||||
aria-describedby={
|
aria-describedby={
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
||||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||||
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
import { Label as LabelPrimitive } from "radix-ui";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import { Popover as PopoverPrimitive } from "radix-ui";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function Popover({
|
function Popover({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverTrigger({
|
function PopoverTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverContent({
|
function PopoverContent({
|
||||||
@@ -28,19 +28,19 @@ function PopoverContent({
|
|||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</PopoverPrimitive.Portal>
|
</PopoverPrimitive.Portal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverAnchor({
|
function PopoverAnchor({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
import { Separator as SeparatorPrimitive } from "radix-ui";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { cva, VariantProps } from "class-variance-authority";
|
import { cva, VariantProps } from "class-variance-authority";
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { XIcon } from "lucide-react";
|
|
||||||
|
|
||||||
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-(--radix-toast-swipe-end-x) data-[swipe=move]:translate-x-(--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 sm:data-[state=open]: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-hidden focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground focus:group-[.destructive]: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-hidden focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 hover:group-[.destructive]:text-red-50 focus:group-[.destructive]:ring-red-400 focus:group-[.destructive]:ring-offset-red-600",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
toast-close=""
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<XIcon 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,
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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,5 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -44,13 +44,13 @@ function TooltipContent({
|
|||||||
data-slot="tooltip-content"
|
data-slot="tooltip-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md bg-primary px-3 py-1.5 text-xs text-balance text-primary-foreground",
|
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-primary fill-primary" />
|
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
||||||
</TooltipPrimitive.Content>
|
</TooltipPrimitive.Content>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { VariantProps, cva } from "class-variance-authority";
|
import { VariantProps, cva } from "class-variance-authority";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot as SlotPrimitive } from "radix-ui";
|
||||||
|
|
||||||
const typographyVariants = cva("text-foreground", {
|
const typographyVariants = cva("text-foreground", {
|
||||||
variants: {
|
variants: {
|
||||||
@@ -24,6 +24,7 @@ const typographyVariants = cva("text-foreground", {
|
|||||||
largeText: "text-lg font-semibold",
|
largeText: "text-lg font-semibold",
|
||||||
smallText: "text-sm leading-none font-medium",
|
smallText: "text-sm leading-none font-medium",
|
||||||
mutedText: "text-muted-foreground text-sm",
|
mutedText: "text-muted-foreground text-sm",
|
||||||
|
errorText: "text-destructive text-sm font-normal",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -51,11 +52,13 @@ const variantElementMap: Record<
|
|||||||
smallText: "small",
|
smallText: "small",
|
||||||
lead: "p",
|
lead: "p",
|
||||||
mutedText: "p",
|
mutedText: "p",
|
||||||
|
errorText: "p",
|
||||||
ul: "ul",
|
ul: "ul",
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TypographyProps
|
export interface TypographyProps
|
||||||
extends React.HTMLAttributes<HTMLElement>,
|
extends
|
||||||
|
React.HTMLAttributes<HTMLElement>,
|
||||||
VariantProps<typeof typographyVariants> {
|
VariantProps<typeof typographyVariants> {
|
||||||
asChild?: boolean;
|
asChild?: boolean;
|
||||||
as?: string;
|
as?: string;
|
||||||
@@ -64,7 +67,7 @@ export interface TypographyProps
|
|||||||
const Typography = React.forwardRef<HTMLElement, TypographyProps>(
|
const Typography = React.forwardRef<HTMLElement, TypographyProps>(
|
||||||
({ className, variant, as, asChild, ...props }, ref) => {
|
({ className, variant, as, asChild, ...props }, ref) => {
|
||||||
const Comp = asChild
|
const Comp = asChild
|
||||||
? Slot
|
? SlotPrimitive.Slot
|
||||||
: (as ?? (variant ? variantElementMap[variant] : undefined) ?? "div");
|
: (as ?? (variant ? variantElementMap[variant] : undefined) ?? "div");
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
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 };
|
|
||||||
+3
-7
@@ -1,11 +1,9 @@
|
|||||||
@import 'tailwindcss';
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
@@ -78,7 +76,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
|
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
@@ -145,7 +142,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|
||||||
--background: oklch(0.147 0.004 49.25);
|
--background: oklch(0.147 0.004 49.25);
|
||||||
|
|
||||||
--foreground: oklch(0.985 0.001 106.423);
|
--foreground: oklch(0.985 0.001 106.423);
|
||||||
@@ -221,8 +217,8 @@
|
|||||||
/* SomniaRooms Extended CSS */
|
/* SomniaRooms Extended CSS */
|
||||||
|
|
||||||
@utility grid-cols-40/60 {
|
@utility grid-cols-40/60 {
|
||||||
grid-template-columns: 40% 60%;
|
grid-template-columns: 40% 60%;
|
||||||
}
|
}
|
||||||
@utility grid-cols-60/40 {
|
@utility grid-cols-60/40 {
|
||||||
grid-template-columns: 60% 40%;
|
grid-template-columns: 60% 40%;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Homepage from "./routes/home-page";
|
|||||||
import SearchPage from "./routes/search-page";
|
import SearchPage from "./routes/search-page";
|
||||||
import BookPage from "./routes/book-page";
|
import BookPage from "./routes/book-page";
|
||||||
import SuccessPage from "./routes/success-page";
|
import SuccessPage from "./routes/success-page";
|
||||||
|
import ContactPage from "./routes/contact-page";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -35,6 +36,10 @@ const router = createBrowserRouter([
|
|||||||
path: "/success",
|
path: "/success",
|
||||||
element: <SuccessPage />,
|
element: <SuccessPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/contact",
|
||||||
|
element: <ContactPage />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ export type ConfigVar = {
|
|||||||
export enum eConfigVarKeys {
|
export enum eConfigVarKeys {
|
||||||
varCompanyName = "CompanyName",
|
varCompanyName = "CompanyName",
|
||||||
varCompanyWebsite = "CompanyWebsite",
|
varCompanyWebsite = "CompanyWebsite",
|
||||||
|
varCompanyContactFormUrl = "CompanyContactFormUrl",
|
||||||
varCompanyLogo = "CompanyLogo",
|
varCompanyLogo = "CompanyLogo",
|
||||||
varCompanyLogoSize = "CompanyLogoSize",
|
varCompanyLogoSize = "CompanyLogoSize",
|
||||||
varCompanyEmail = "CompanyEmail",
|
varCompanyEmail = "CompanyEmail",
|
||||||
varCompanyPhone = "CompanyPhone",
|
varCompanyPhone = "CompanyPhone",
|
||||||
varCompanyAddress = "CompanyAddress",
|
varCompanyAddress = "CompanyAddress",
|
||||||
varNightAmountForContactDialog = "NightAmountForContactDialog",
|
varNightCountForContactDialog = "NightCountForContactDialog",
|
||||||
varGuestNumberForContactDialog = "GuestNumberForContactDialog",
|
varGuestNumberForContactDialog = "GuestNumberForContactDialog",
|
||||||
varContactDialogShowBehaviour = "ContactDialogShowBehaviour",
|
|
||||||
varShowNonAvailableRooms = "ShowNonAvailableRooms",
|
varShowNonAvailableRooms = "ShowNonAvailableRooms",
|
||||||
varSomniaAttributionNotice = "SomniaAttributionNotice",
|
varSomniaAttributionNotice = "SomniaAttributionNotice",
|
||||||
varPhotoAspectRatio = "PhotoAspectRatio",
|
varPhotoAspectRatio = "PhotoAspectRatio",
|
||||||
|
varAPIMaxAllowedNightAmount = "MaxAllowedNightAmount",
|
||||||
|
varAPIMaxAllowedGuestNumber = "MaxAllowedGuestNumber",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,4 @@ export type RoomCategoryBedType = {
|
|||||||
amount: number;
|
amount: number;
|
||||||
bedTypeName: string;
|
bedTypeName: string;
|
||||||
bedTypeIcon: string;
|
bedTypeIcon: string;
|
||||||
}
|
};
|
||||||
|
|||||||
+4
-16
@@ -8,24 +8,12 @@ export type RoomPrice = {
|
|||||||
price: number;
|
price: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AvailabilityRequest = {
|
export type SearchPeriodRequest = {
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AvailabilityFilter = {
|
export type RoomPriceSearchResponse = {
|
||||||
startDate: string;
|
roomCategoryId: number;
|
||||||
endDate: string;
|
prices: RoomPrice[];
|
||||||
};
|
|
||||||
|
|
||||||
export type FareRequest = {
|
|
||||||
roomCategory: number;
|
|
||||||
startDate: string;
|
|
||||||
endDate: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type FareFilter = {
|
|
||||||
roomCategory: number;
|
|
||||||
startDate: string;
|
|
||||||
endDate: string;
|
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-3
@@ -3,17 +3,15 @@ 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() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
<div className="mx-auto w-full min-h-[100vh] bg-background p-8 text-center text-foreground sm:w-[90%] md:w-[80%]">
|
<div className="mx-auto min-h-screen w-full bg-background p-8 text-center text-foreground sm:w-[90%] md:w-[80%]">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
<Toaster />
|
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+17
-12
@@ -20,6 +20,9 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { eBookingState } from "@/utils/AppStateManager";
|
import { eBookingState } from "@/utils/AppStateManager";
|
||||||
|
import { useBookingStateDispacher } from "@/utils/hooks";
|
||||||
|
import { ConstsNav } from "@/utils/Consts";
|
||||||
|
import { NewBookingCustomerDetails } from "@/model/Bookings";
|
||||||
|
|
||||||
enum PageState {
|
enum PageState {
|
||||||
showForm,
|
showForm,
|
||||||
@@ -31,11 +34,11 @@ function BookPage() {
|
|||||||
const [pageState, setPageState] = useState<PageState>(PageState.showForm);
|
const [pageState, setPageState] = useState<PageState>(PageState.showForm);
|
||||||
const bookingData = useAppStore((state) => state.currentBooking);
|
const bookingData = useAppStore((state) => state.currentBooking);
|
||||||
const appBookingState = useAppStore((state) => state.bookingState);
|
const appBookingState = useAppStore((state) => state.bookingState);
|
||||||
const setAppBookingState = useAppStore((state) => state.setBookingState);
|
|
||||||
const setCustomerData = useAppStore(
|
const setCustomerData = useAppStore(
|
||||||
(state) => state.currentBookingSetCustomer,
|
(state) => state.currentBookingSetCustomer,
|
||||||
);
|
);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { setBookingStateBooked } = useBookingStateDispacher();
|
||||||
|
|
||||||
const bookingForm = useForm<z.infer<typeof BookingFormSchema>>({
|
const bookingForm = useForm<z.infer<typeof BookingFormSchema>>({
|
||||||
resolver: zodResolver(BookingFormSchema),
|
resolver: zodResolver(BookingFormSchema),
|
||||||
@@ -51,12 +54,13 @@ function BookPage() {
|
|||||||
if (bookingData == null) {
|
if (bookingData == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newCustomerData = bookingData.customerDetails;
|
const newCustomerData: NewBookingCustomerDetails = {
|
||||||
newCustomerData.name = values.name;
|
...bookingData.customerDetails,
|
||||||
newCustomerData.surname = values.surname;
|
name: values.name,
|
||||||
newCustomerData.email = values.email;
|
surname: values.surname,
|
||||||
newCustomerData.phoneNumber = values.phone;
|
email: values.email,
|
||||||
|
phoneNumber: values.phone,
|
||||||
|
};
|
||||||
setCustomerData(newCustomerData);
|
setCustomerData(newCustomerData);
|
||||||
|
|
||||||
setPageState(PageState.sending);
|
setPageState(PageState.sending);
|
||||||
@@ -70,18 +74,19 @@ function BookPage() {
|
|||||||
if (rs.isError) {
|
if (rs.isError) {
|
||||||
setPageState(PageState.error);
|
setPageState(PageState.error);
|
||||||
} else {
|
} else {
|
||||||
setAppBookingState(eBookingState.Booked);
|
setBookingStateBooked();
|
||||||
navigate("/success", { state: rs.bookingResponse });
|
navigate(ConstsNav.page_Success, { state: rs.bookingResponse });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//Redirect to home if no booking is active.
|
//Redirect to home if no booking is active.
|
||||||
if (
|
if (
|
||||||
!appBookingState ||
|
!appBookingState ||
|
||||||
appBookingState.state != eBookingState.Booking ||
|
!bookingData ||
|
||||||
!bookingData
|
(appBookingState.state != eBookingState.Booking &&
|
||||||
|
appBookingState.state != eBookingState.Booked)
|
||||||
) {
|
) {
|
||||||
navigate("/home");
|
navigate(ConstsNav.page_Home);
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMsg = (
|
const errorMsg = (
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Typography } from "@/components/ui/typography";
|
||||||
|
import { eConfigVarKeys } from "@/model/ConfigVar";
|
||||||
|
import { useConfigVariable } from "@/utils/apiHooks";
|
||||||
|
import { Mail, Phone } from "lucide-react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
function ContactPage() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const { value: varCompanyPhone, isLoading: isPhoneLoading } =
|
||||||
|
useConfigVariable(eConfigVarKeys.varCompanyPhone, "");
|
||||||
|
const { value: varCompanyMail, isLoading: isMailLoading } = useConfigVariable(
|
||||||
|
eConfigVarKeys.varCompanyEmail,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
const { value: varCompanyContactPage, isLoading: isContactPageLoading } =
|
||||||
|
useConfigVariable(eConfigVarKeys.varCompanyContactFormUrl, "");
|
||||||
|
|
||||||
|
const isDataLoading = isPhoneLoading || isMailLoading || isContactPageLoading;
|
||||||
|
|
||||||
|
const mainText = location.state ?? "Contact us for a personalized quote!";
|
||||||
|
|
||||||
|
const LoadingContent = <Skeleton className="h-[100px] w-full" />;
|
||||||
|
|
||||||
|
const MainContent = (
|
||||||
|
<div className="mx-auto">
|
||||||
|
<div className="mx-auto">
|
||||||
|
<Typography variant={"largeText"}>{mainText}</Typography>
|
||||||
|
<div className="my-4 flex justify-center gap-4">
|
||||||
|
<Card className="flex flex-col items-center gap-2 px-5 hover:bg-secondary">
|
||||||
|
<Phone size={"1.2em"} />
|
||||||
|
<Typography variant={"p_inline"}>{varCompanyPhone}</Typography>
|
||||||
|
</Card>
|
||||||
|
<Card className="flex flex-col items-center gap-2 px-5 hover:bg-secondary">
|
||||||
|
<Mail size={"1.2em"} />
|
||||||
|
<Typography variant={"p_inline"}>{varCompanyMail}</Typography>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{varCompanyContactPage !== "" && (
|
||||||
|
<Button className="my-4" variant={"link"}>
|
||||||
|
<a href={varCompanyContactPage}>
|
||||||
|
<Typography variant={"largeText"}>Go to contact form</Typography>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Typography variant={"h2"}>Contact us!</Typography>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>{isDataLoading ? LoadingContent : MainContent}</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContactPage;
|
||||||
+20
-23
@@ -7,40 +7,34 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { Typography } from "@/components/ui/typography";
|
import { Typography } from "@/components/ui/typography";
|
||||||
import { RoomCategory } from "@/model/RoomCategory";
|
import { RoomCategory } from "@/model/RoomCategory";
|
||||||
import { SearchFilter } from "@/model/InternalTypes";
|
import { SearchFilter } from "@/model/InternalTypes";
|
||||||
import { eBookingState } from "@/utils/AppStateManager";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { LoaderIcon } from "lucide-react";
|
import { LoaderIcon } from "lucide-react";
|
||||||
import "./home-page.css";
|
import "./home-page.css";
|
||||||
import { useAppStore } from "@/store";
|
import { useAppStore } from "@/store";
|
||||||
import { isValidSearchFilter, makeDayUTCDate } from "@/utils/utils";
|
import { useBookingStateDispacher } from "@/utils/hooks";
|
||||||
import { useConfigVariable } from "@/utils/hooks";
|
|
||||||
import { eConfigVarKeys } from "@/model/ConfigVar";
|
import { eConfigVarKeys } from "@/model/ConfigVar";
|
||||||
|
import { useConfigVariable } from "@/utils/apiHooks";
|
||||||
|
import { ConstsNav } from "@/utils/Consts";
|
||||||
|
|
||||||
function Homepage() {
|
function Homepage() {
|
||||||
|
|
||||||
const appSearchFilter = useAppStore((state) => state.searchFilter);
|
const appSearchFilter = useAppStore((state) => state.searchFilter);
|
||||||
const setAppSearchFilter = useAppStore((state) => state.setSearchFilter);
|
|
||||||
const setAppBookingState = useAppStore((state) => state.setBookingState);
|
|
||||||
const initAppBooking = useAppStore((state) => state.initCurrentBooking);
|
|
||||||
|
|
||||||
const { value: varCompanyName } = useConfigVariable(eConfigVarKeys.varCompanyName, "");
|
|
||||||
const { roomCategoryList, isLoading: isLoadingCategories, isError } = useAPIRoomCategoryList();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { setBookingStateSearched } = useBookingStateDispacher();
|
||||||
|
|
||||||
|
const { value: varCompanyName } = useConfigVariable(
|
||||||
|
eConfigVarKeys.varCompanyName,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
roomCategoryList,
|
||||||
|
isLoading: isLoadingCategories,
|
||||||
|
isError,
|
||||||
|
} = useAPIRoomCategoryList();
|
||||||
|
|
||||||
const onBtSearchClick = (filter: SearchFilter) => {
|
const onBtSearchClick = (filter: SearchFilter) => {
|
||||||
if (!isValidSearchFilter(filter)) {
|
setBookingStateSearched(filter);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAppSearchFilter(filter);
|
navigate(ConstsNav.page_Search, { state: filter });
|
||||||
setAppBookingState(eBookingState.Searched);
|
|
||||||
|
|
||||||
initAppBooking(
|
|
||||||
makeDayUTCDate(filter.dateRange.from),
|
|
||||||
makeDayUTCDate(filter.dateRange.to),
|
|
||||||
);
|
|
||||||
|
|
||||||
navigate("/search", { state: filter });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const roomList = roomCategoryList?.map((element: RoomCategory) => {
|
const roomList = roomCategoryList?.map((element: RoomCategory) => {
|
||||||
@@ -67,7 +61,10 @@ function Homepage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Separator />
|
<Separator />
|
||||||
<BackendConnectionStatus isLoading={isLoadingCategories} isError={isError} />
|
<BackendConnectionStatus
|
||||||
|
isLoading={isLoadingCategories}
|
||||||
|
isError={isError}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-23
@@ -9,29 +9,27 @@ import { Typography } from "@/components/ui/typography";
|
|||||||
import { SearchFilter } from "@/model/InternalTypes";
|
import { SearchFilter } from "@/model/InternalTypes";
|
||||||
import { eBookingState } from "@/utils/AppStateManager";
|
import { eBookingState } from "@/utils/AppStateManager";
|
||||||
import { useAppStore } from "@/store";
|
import { useAppStore } from "@/store";
|
||||||
import {
|
import { makeSearchPeriodRequestFromSearchFilter } from "@/utils/utils";
|
||||||
isValidSearchFilter,
|
|
||||||
makeAvailabilityFilterFromSearchFilter,
|
|
||||||
makeDayUTCDate,
|
|
||||||
} from "@/utils/utils";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useBookingStateDispacher } from "@/utils/hooks";
|
||||||
|
import { ConstsNav } from "@/utils/Consts";
|
||||||
|
|
||||||
//TODOME: Al pulsar el botón "buscar", si ya existen datos en la reserva (se han añadido habitaciones) mostrar un cuadro de aviso de que si se busca se borrará la selección actual.
|
//TODOME: Al pulsar el botón "buscar", si ya existen datos en la reserva (se han añadido habitaciones) mostrar un cuadro de aviso de que si se busca se borrará la selección actual.
|
||||||
//TODOME: Mediante los estados globales de la reserva, si ya está en estado "booking" añadiendo los datos, mostrar un mensaje y permitir redirigir directamente para que continue donde lo dejó.
|
//TODOME: Mediante los estados globales de la reserva, si ya está en estado "booking" añadiendo los datos, mostrar un mensaje y permitir redirigir directamente para que continue donde lo dejó.
|
||||||
function SearchPage() {
|
function SearchPage() {
|
||||||
const appBookingState = useAppStore((state) => state.bookingState);
|
const appBookingState = useAppStore((state) => state.bookingState);
|
||||||
const setAppBookingState = useAppStore((state) => state.setBookingState);
|
|
||||||
const appSearchFilter = useAppStore((state) => state.searchFilter);
|
const appSearchFilter = useAppStore((state) => state.searchFilter);
|
||||||
const setAppSearchFilter = useAppStore((state) => state.setSearchFilter);
|
|
||||||
const initAppBooking = useAppStore((state) => state.initCurrentBooking);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { setBookingStateSearched, setBookingStateBooking } =
|
||||||
|
useBookingStateDispacher();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
roomAvailabilityList,
|
roomAvailabilityList,
|
||||||
isLoading: isLoadingAvailability,
|
isLoading: isLoadingAvailability,
|
||||||
isError: isErrorAvailability,
|
isError: isErrorAvailability,
|
||||||
} = useAPIRoomAvailability(
|
} = useAPIRoomAvailability(
|
||||||
makeAvailabilityFilterFromSearchFilter(appSearchFilter) ?? null,
|
makeSearchPeriodRequestFromSearchFilter(appSearchFilter) ?? null,
|
||||||
);
|
);
|
||||||
const isLoading = isLoadingAvailability;
|
const isLoading = isLoadingAvailability;
|
||||||
const isError = isErrorAvailability;
|
const isError = isErrorAvailability;
|
||||||
@@ -39,24 +37,13 @@ function SearchPage() {
|
|||||||
const hasRooms =
|
const hasRooms =
|
||||||
roomAvailabilityList?.length && roomAvailabilityList?.length > 0;
|
roomAvailabilityList?.length && roomAvailabilityList?.length > 0;
|
||||||
|
|
||||||
//TODOME: Hay que ver para mover esto a un método común.
|
|
||||||
const onBtSearchClick = (newFilter: SearchFilter) => {
|
const onBtSearchClick = (newFilter: SearchFilter) => {
|
||||||
if (!isValidSearchFilter(newFilter)) {
|
setBookingStateSearched(newFilter);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAppSearchFilter(newFilter);
|
|
||||||
setAppBookingState(eBookingState.Searched);
|
|
||||||
|
|
||||||
initAppBooking(
|
|
||||||
makeDayUTCDate(newFilter.dateRange.from),
|
|
||||||
makeDayUTCDate(newFilter.dateRange.to),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBookingContinue = () => {
|
const onBookingContinue = () => {
|
||||||
setAppBookingState(eBookingState.Booking);
|
setBookingStateBooking();
|
||||||
navigate("/book");
|
navigate(ConstsNav.page_Book);
|
||||||
};
|
};
|
||||||
|
|
||||||
const roomList =
|
const roomList =
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|||||||
import { Typography } from "@/components/ui/typography";
|
import { Typography } from "@/components/ui/typography";
|
||||||
import { NewBookingResponse } from "@/model/Bookings";
|
import { NewBookingResponse } from "@/model/Bookings";
|
||||||
import { useAppStore } from "@/store";
|
import { useAppStore } from "@/store";
|
||||||
|
import { ConstsNav } from "@/utils/Consts";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@@ -13,16 +14,19 @@ function SuccessPage() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (pageState == null && location.state != null) {
|
||||||
|
setPageState(location.state);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.state == null) {
|
if (location.state == null) {
|
||||||
navigate("/home");
|
navigate(ConstsNav.page_Home);
|
||||||
}
|
}
|
||||||
setPageState(location.state);
|
|
||||||
}, [location, navigate]);
|
}, [location, navigate]);
|
||||||
|
|
||||||
const handleBtBackClick = () => {
|
const handleBtBackClick = () => {
|
||||||
resetBookingState();
|
resetBookingState();
|
||||||
navigate("/home");
|
navigate(ConstsNav.page_Home);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+21
-2
@@ -1,3 +1,22 @@
|
|||||||
|
export const c_somniaRooms_notice_text =
|
||||||
export const c_somniaRooms_notice_text = "Powered by SomniaRooms, an Open-Source hospitality booking platform!";
|
"Powered by SomniaRooms, an Open-Source hospitality booking platform!";
|
||||||
export const c_sominaRooms_webpage = "https://somnia.dev";
|
export const c_sominaRooms_webpage = "https://somnia.dev";
|
||||||
|
|
||||||
|
export const ConstsNav = {
|
||||||
|
page_Home: "/home",
|
||||||
|
page_Contact: "/contact",
|
||||||
|
page_Search: "/search",
|
||||||
|
page_Book: "/book",
|
||||||
|
page_Success: "/success",
|
||||||
|
};
|
||||||
|
|
||||||
|
//TMP - The following texts should be replaced for a proper intl system.
|
||||||
|
export const ConstsTexts = {
|
||||||
|
navToContact_maxAllowedNightCount:
|
||||||
|
"We are sorry, but the selected date range is too large. Search for a smaller range or contact us for a personalized quote.",
|
||||||
|
navToContact_maxAllowedGuestNumber:
|
||||||
|
"We are sorry, group bookings are not allowed. Contact us for a personalized quote.",
|
||||||
|
contactUsAlertDialog_title: "Contact us!",
|
||||||
|
contactUsAlertDialog_text:
|
||||||
|
"This booking seems like a big deal... Contact us for a personalized quote!",
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { useAPIConfigVarList } from "@/api/api-configVars";
|
||||||
|
import { getConfigVarFromList } from "./utils";
|
||||||
|
import { eConfigVarKeys } from "@/model/ConfigVar";
|
||||||
|
|
||||||
|
import { useAPIRoomCategoryList } from "@/api/api-roomCategories";
|
||||||
|
import { useAPIRoomPrices } from "@/api/api-roomSearch";
|
||||||
|
import { RoomCategory } from "@/model/RoomCategory";
|
||||||
|
import { RoomPrice, SearchPeriodRequest } from "@/model/RoomSearch";
|
||||||
|
|
||||||
|
export function useConfigVariable(
|
||||||
|
key: eConfigVarKeys,
|
||||||
|
defaultValue: string,
|
||||||
|
): {
|
||||||
|
value: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
} {
|
||||||
|
const { configVarList, isLoading } = useAPIConfigVarList();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return {
|
||||||
|
value: defaultValue,
|
||||||
|
isLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const configVar = getConfigVarFromList(configVarList, key);
|
||||||
|
|
||||||
|
return { value: configVar?.value ?? defaultValue, isLoading: isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfigVariableAsNumber(
|
||||||
|
key: eConfigVarKeys,
|
||||||
|
defaultValue: number,
|
||||||
|
): {
|
||||||
|
value: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
} {
|
||||||
|
const rs = useConfigVariable(key, defaultValue.toString());
|
||||||
|
return {
|
||||||
|
value: rs.isLoading ? defaultValue : parseFloat(rs.value),
|
||||||
|
isLoading: rs.isLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRoomCategory(roomCategoryId: number): {
|
||||||
|
value: RoomCategory | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
} {
|
||||||
|
const { roomCategoryList, isLoading, isError } = useAPIRoomCategoryList();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return {
|
||||||
|
value: null,
|
||||||
|
isLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isError) {
|
||||||
|
return {
|
||||||
|
value: null,
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomCategoryItem = roomCategoryList?.find(
|
||||||
|
(i) => i.id == roomCategoryId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { value: roomCategoryItem ?? null, isLoading: isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRoomPrice(
|
||||||
|
filter: SearchPeriodRequest | null,
|
||||||
|
roomCategoryId: number,
|
||||||
|
): {
|
||||||
|
value: RoomPrice[];
|
||||||
|
isLoading: boolean;
|
||||||
|
} {
|
||||||
|
const { roomPriceList, isLoading, isError } = useAPIRoomPrices(filter);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return {
|
||||||
|
value: [],
|
||||||
|
isLoading: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isError) {
|
||||||
|
return {
|
||||||
|
value: [],
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomPrice = roomPriceList?.find(
|
||||||
|
(i) => i.roomCategoryId == roomCategoryId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { value: roomPrice?.prices ?? [], isLoading: isLoading };
|
||||||
|
}
|
||||||
+123
-9
@@ -1,17 +1,131 @@
|
|||||||
import { useAPIConfigVarList } from "@/api/api-configVars";
|
import { SearchFilter } from "@/model/InternalTypes";
|
||||||
import { getConfigVarFromList } from "./utils";
|
import { useAppStore } from "@/store";
|
||||||
|
import { getNightCountFromSearchFilter, makeDayUTCDate } from "./utils";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { eBookingState } from "./AppStateManager";
|
||||||
|
import { useConfigVariableAsNumber } from "./apiHooks";
|
||||||
import { eConfigVarKeys } from "@/model/ConfigVar";
|
import { eConfigVarKeys } from "@/model/ConfigVar";
|
||||||
|
import { NewBooking } from "@/model/Bookings";
|
||||||
|
|
||||||
export function useConfigVariable(key: eConfigVarKeys, defaultValue: string) {
|
export function useBookingStateDispacher(): {
|
||||||
const { configVarList, isLoading } = useAPIConfigVarList();
|
setBookingStateSearched: (sf: SearchFilter) => void;
|
||||||
|
setBookingStateBooking: () => void;
|
||||||
|
setBookingStateBooked: () => void;
|
||||||
|
} {
|
||||||
|
const setAppBookingState = useAppStore((state) => state.setBookingState);
|
||||||
|
const setAppSearchFilter = useAppStore((state) => state.setSearchFilter);
|
||||||
|
const initAppBooking = useAppStore((state) => state.initCurrentBooking);
|
||||||
|
|
||||||
if (isLoading) {
|
const fnSearched = useCallback(
|
||||||
|
(sf: SearchFilter) => {
|
||||||
|
setAppSearchFilter(sf);
|
||||||
|
setAppBookingState(eBookingState.Searched);
|
||||||
|
|
||||||
|
initAppBooking(
|
||||||
|
makeDayUTCDate(sf.dateRange.from),
|
||||||
|
makeDayUTCDate(sf.dateRange.to),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[setAppBookingState, setAppSearchFilter, initAppBooking],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fnBooking = useCallback(() => {
|
||||||
|
setAppBookingState(eBookingState.Booking);
|
||||||
|
}, [setAppBookingState]);
|
||||||
|
const fnBooked = useCallback(() => {
|
||||||
|
setAppBookingState(eBookingState.Booked);
|
||||||
|
}, [setAppBookingState]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
setBookingStateSearched: fnSearched,
|
||||||
|
setBookingStateBooking: fnBooking,
|
||||||
|
setBookingStateBooked: fnBooked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//This checks may be moved into "useBookingStateDispacher".
|
||||||
|
// That way, the logic may be more general and separated from the components and buttons
|
||||||
|
// and the dialog may use global state.
|
||||||
|
export function useCheckForNightCount(sf: SearchFilter | null): {
|
||||||
|
isOverApiMax: boolean;
|
||||||
|
shouldShowContactDialog: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
} {
|
||||||
|
const {
|
||||||
|
value: varApiMaxAllowedNights,
|
||||||
|
isLoading: isLoadingVarMaxAllowedNights,
|
||||||
|
} = useConfigVariableAsNumber(eConfigVarKeys.varAPIMaxAllowedNightAmount, -1);
|
||||||
|
const {
|
||||||
|
value: varNightCountForContactDialog,
|
||||||
|
isLoading: isLoadingVarNightCountForContact,
|
||||||
|
} = useConfigVariableAsNumber(
|
||||||
|
eConfigVarKeys.varNightCountForContactDialog,
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
const areVariablesLoading =
|
||||||
|
isLoadingVarMaxAllowedNights || isLoadingVarNightCountForContact;
|
||||||
|
|
||||||
|
const filterNightCount = getNightCountFromSearchFilter(sf);
|
||||||
|
if (filterNightCount == null || areVariablesLoading) {
|
||||||
return {
|
return {
|
||||||
value: defaultValue,
|
isOverApiMax: false,
|
||||||
isLoading: true,
|
shouldShowContactDialog: false,
|
||||||
|
isLoading: areVariablesLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const configVar = getConfigVarFromList(configVarList, key);
|
|
||||||
|
|
||||||
return { value: configVar?.value ?? defaultValue, isLoading: isLoading };
|
const doCheckForMaxAllowedNights = varApiMaxAllowedNights > -1;
|
||||||
|
const doCheckForContactAlert = varNightCountForContactDialog > -1;
|
||||||
|
return {
|
||||||
|
isOverApiMax:
|
||||||
|
doCheckForMaxAllowedNights && filterNightCount > varApiMaxAllowedNights,
|
||||||
|
shouldShowContactDialog:
|
||||||
|
doCheckForContactAlert &&
|
||||||
|
filterNightCount >= varNightCountForContactDialog,
|
||||||
|
isLoading: areVariablesLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCheckForGuestNumber(bookingData: NewBooking | null): {
|
||||||
|
isOverApiMax: boolean;
|
||||||
|
shouldShowContactDialog: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
} {
|
||||||
|
const {
|
||||||
|
value: varApiMaxAllowedGuests,
|
||||||
|
isLoading: isLoadingVarMaxAllowedGuests,
|
||||||
|
} = useConfigVariableAsNumber(eConfigVarKeys.varAPIMaxAllowedGuestNumber, -1);
|
||||||
|
const {
|
||||||
|
value: varGuestNumberForContactDialog,
|
||||||
|
isLoading: isLoadingVarGuestNumberForContact,
|
||||||
|
} = useConfigVariableAsNumber(
|
||||||
|
eConfigVarKeys.varGuestNumberForContactDialog,
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
const areVariablesLoading =
|
||||||
|
isLoadingVarMaxAllowedGuests || isLoadingVarGuestNumberForContact;
|
||||||
|
|
||||||
|
if (bookingData == null || areVariablesLoading) {
|
||||||
|
return {
|
||||||
|
isOverApiMax: false,
|
||||||
|
shouldShowContactDialog: false,
|
||||||
|
isLoading: areVariablesLoading,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//Calculate guest number in current booking data.
|
||||||
|
const totalGuestNumber = bookingData.rooms.reduce((acc, r) => {
|
||||||
|
return acc + r.guestNumber * r.amount;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const doCheckForMaxAllowedGuests = varApiMaxAllowedGuests > -1;
|
||||||
|
const doCheckForContactAlert = varGuestNumberForContactDialog > -1;
|
||||||
|
return {
|
||||||
|
isOverApiMax:
|
||||||
|
doCheckForMaxAllowedGuests && totalGuestNumber > varApiMaxAllowedGuests,
|
||||||
|
shouldShowContactDialog:
|
||||||
|
doCheckForContactAlert &&
|
||||||
|
totalGuestNumber >= varGuestNumberForContactDialog,
|
||||||
|
isLoading: areVariablesLoading,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-19
@@ -1,6 +1,7 @@
|
|||||||
import { AvailabilityFilter, FareFilter } from "@/model/RoomSearch";
|
import { SearchPeriodRequest } from "@/model/RoomSearch";
|
||||||
import { SearchFilter } from "@/model/InternalTypes";
|
import { SearchFilter } from "@/model/InternalTypes";
|
||||||
import { ConfigVar } from "@/model/ConfigVar";
|
import { ConfigVar } from "@/model/ConfigVar";
|
||||||
|
import { differenceInDays } from "date-fns";
|
||||||
|
|
||||||
export function makeDayUTCDate(date: Date | undefined): Date {
|
export function makeDayUTCDate(date: Date | undefined): Date {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
@@ -18,16 +19,27 @@ export function makeDayUTCDate(date: Date | undefined): Date {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidSearchFilter(sf: SearchFilter): boolean {
|
export function isValidSearchFilter(
|
||||||
|
sf: SearchFilter | null,
|
||||||
|
): sf is SearchFilter {
|
||||||
if (sf?.dateRange?.from && sf?.dateRange?.to) {
|
if (sf?.dateRange?.from && sf?.dateRange?.to) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeAvailabilityFilterFromSearchFilter(
|
export function getNightCountFromSearchFilter(
|
||||||
|
sf: SearchFilter | null,
|
||||||
|
): number | null {
|
||||||
|
if (!isValidSearchFilter(sf)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return differenceInDays(sf.dateRange.to!, sf.dateRange.from!);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeSearchPeriodRequestFromSearchFilter(
|
||||||
sf: SearchFilter,
|
sf: SearchFilter,
|
||||||
): AvailabilityFilter | null {
|
): SearchPeriodRequest | null {
|
||||||
if (!isValidSearchFilter(sf)) {
|
if (!isValidSearchFilter(sf)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -37,21 +49,6 @@ export function makeAvailabilityFilterFromSearchFilter(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeFareFilterFromSearchFilter(
|
|
||||||
sf: SearchFilter,
|
|
||||||
roomCategoryId: number,
|
|
||||||
): FareFilter | null {
|
|
||||||
if (!isValidSearchFilter(sf) || roomCategoryId < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
startDate: sf.dateRange.from!.toISOString(),
|
|
||||||
endDate: sf.dateRange.to!.toISOString(),
|
|
||||||
roomCategory: roomCategoryId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getConfigVarFromList(
|
export function getConfigVarFromList(
|
||||||
varList: ConfigVar[] | undefined,
|
varList: ConfigVar[] | undefined,
|
||||||
key: string,
|
key: string,
|
||||||
|
|||||||
+1
-3
@@ -3,9 +3,7 @@
|
|||||||
// ...
|
// ...
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./src/*"]
|
||||||
"./src/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import tailwindcss from "@tailwindcss/vite"
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
Reference in New Issue
Block a user