forked from Somnia/SomniaRoomsApp
Search Page - Initial Implementation.
- Added components. - Query API to get availability for selected period.
This commit is contained in:
Generated
+35
@@ -15,6 +15,7 @@
|
|||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -1278,6 +1279,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-toast": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-collection": "1.0.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.0.5",
|
||||||
|
"@radix-ui/react-portal": "1.0.4",
|
||||||
|
"@radix-ui/react-presence": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.0.1",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-tooltip": {
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
import { RoomCategory } from "@/model/RoomCategory";
|
import { RoomCategory } from "@/model/RoomCategory";
|
||||||
import { fetcherUnwraper } from "./backend";
|
import { fetcher, fetcherUnwraper } from "./backend";
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
export function useAPIRoomCategories () {
|
export function useAPIRoomCategories() {
|
||||||
const { data, error, isLoading } = useSWR<RoomCategory[]>(`/api/room/category`, fetcherUnwraper)
|
const { data, error, isLoading } = useSWR<RoomCategory[]>('/api/room/category', fetcherUnwraper)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
roomCategoryList: data,
|
roomCategoryList: data,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError: error
|
isError: error
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAPIRoomCategoryId(categoryId: number) {
|
||||||
|
const url = categoryId ? '/api/room/category/' + categoryId : null;
|
||||||
|
|
||||||
|
const { data, error, isLoading } = useSWR<RoomCategory>(url, fetcher)
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomCategory: data,
|
||||||
|
isLoading,
|
||||||
|
isError: error
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { fetcherUnwraperWithFilter } from "./backend";
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { AvailabilityFilter, AvailabilityRequest, RoomAvailabity } from "@/model/RoomSearch";
|
||||||
|
|
||||||
|
export function useAPIRoomAvailability(filter: AvailabilityFilter | null) {
|
||||||
|
|
||||||
|
const request: AvailabilityRequest | null = filter ? {
|
||||||
|
filter: filter
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
const { data, isLoading, error } = useSWR(request ? ['/api/search/availability', request] : null, ([url, request]) => fetcherUnwraperWithFilter<RoomAvailabity[]>(url, request))
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomAvailabilityList: data,
|
||||||
|
isLoading,
|
||||||
|
isError: error
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,3 +29,38 @@ export function fetcherUnwraper<T>(endpoint: string): Promise<T> {
|
|||||||
return data.results
|
return data.results
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetcherWithFilter<T>(endpoint: string, body: NonNullable<unknown>): Promise<T> {
|
||||||
|
return fetch(url + endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText)
|
||||||
|
}
|
||||||
|
return response.json() as Promise<T>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetcherUnwraperWithFilter<T>(endpoint: string, body: NonNullable<unknown>): Promise<T> {
|
||||||
|
return fetch(url + endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText)
|
||||||
|
}
|
||||||
|
return response.json() as Promise<{ results: T }>
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
return data.results
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { useAPIRoomCategoryId } from "@/api/api-roomCategories"
|
||||||
|
import { Card, CardContent, CardHeader } from "../ui/card"
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Typography } from "../ui/typography";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { RoomCategoryPhoto } from "@/model/RoomCategory";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
|
||||||
|
import { Skeleton } from "../ui/skeleton";
|
||||||
|
|
||||||
|
function RoomBooking({ roomCategoryId, availableAmount }: { roomCategoryId: number, availableAmount: number }) {
|
||||||
|
|
||||||
|
const [selectedAmount, setSelectedAmount] = useState<number>(0);
|
||||||
|
|
||||||
|
const { roomCategory, isLoading: isLoadingCategory, isError: isErrorCategory } = useAPIRoomCategoryId(roomCategoryId);
|
||||||
|
|
||||||
|
//TODOME: Gestionar también los estados al cargar los precios.
|
||||||
|
const isLoading = isLoadingCategory;
|
||||||
|
const isError = isErrorCategory;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Skeleton className="w-[600px] h-[150px] mx-auto" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<Typography variant={"h3"} as="h3">Sorry, there was an error loading data.</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainPhoto: RoomCategoryPhoto | undefined = roomCategory?.roomCategoryPhotos.filter((p) => p.kind == "main")[0];
|
||||||
|
const photoElement = mainPhoto && (
|
||||||
|
<img src={mainPhoto.path} alt={mainPhoto.altText} className="rounded-md mx-0 px-0"></img>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-row gap-5 w-full">
|
||||||
|
<div className="w-[40%] flex-none">
|
||||||
|
{photoElement}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography variant={"h3"} as="h3">{roomCategory?.name}</Typography>
|
||||||
|
<Typography >{roomCategory?.description}</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Guest number</TableHead>
|
||||||
|
<TableHead>Price</TableHead>
|
||||||
|
<TableHead>Amount</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>1</TableCell>
|
||||||
|
<TableCell>25€</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<Button
|
||||||
|
onClick={() => setSelectedAmount(selectedAmount - 1)}>-</Button>
|
||||||
|
<p className="mx-2">{selectedAmount}</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => setSelectedAmount(selectedAmount + 1)}>+</Button>
|
||||||
|
</div></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<div>
|
||||||
|
<p>Available: {availableAmount}</p>
|
||||||
|
<p>Selected: {selectedAmount}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RoomBooking
|
||||||
@@ -5,8 +5,55 @@ import 'react-day-picker/dist/style.css';
|
|||||||
import { DatePickerWithRange } from "../ui/dateRange-picker";
|
import { DatePickerWithRange } from "../ui/dateRange-picker";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { SearchIcon } from "lucide-react";
|
import { SearchIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { SearchFilter } from "@/model/SearchFilter";
|
||||||
|
import { DateRange } from "react-day-picker";
|
||||||
|
import { useToast } from "../ui/use-toast";
|
||||||
|
|
||||||
export function SearchFilterCard() {
|
type funOnSearchClick = (newFilter: SearchFilter) => void;
|
||||||
|
|
||||||
|
export function SearchFilterCard({ onSearchClick }: { onSearchClick?: funOnSearchClick }) {
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [searchFilter, setSearchFilter] = useState<SearchFilter>({
|
||||||
|
dateRange: { from: new Date() }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Validate and call callback on btSearch click.
|
||||||
|
//TODOME: Add data validations and show messages.
|
||||||
|
const btSearchClick = () => {
|
||||||
|
if (!searchFilter || !searchFilter.dateRange) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Uh oh! Invalid Search.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!searchFilter.dateRange.from || !searchFilter.dateRange.to) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Uh oh! Invalid Search.",
|
||||||
|
description: "You should select a 'from' and 'to' dates"
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onSearchClick) {
|
||||||
|
onSearchClick(searchFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Element updates.
|
||||||
|
const updateDateRange = (newRange: DateRange) => {
|
||||||
|
setSearchFilter({
|
||||||
|
...searchFilter,
|
||||||
|
dateRange: newRange,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("SearchFilterCard filter UPDATED");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -14,10 +61,10 @@ export function SearchFilterCard() {
|
|||||||
<Typography variant={"h2"} as="h1" className="mx-5 py-2">Reserva ahora</Typography>
|
<Typography variant={"h2"} as="h1" className="mx-5 py-2">Reserva ahora</Typography>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<DatePickerWithRange></DatePickerWithRange>
|
<DatePickerWithRange updateDateRange={updateDateRange}></DatePickerWithRange>
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
<Button size={"lg"}>
|
<Button size={"lg"} onClick={btSearchClick}>
|
||||||
<SearchIcon className="text-primary-foreground me-2"/>
|
<SearchIcon className="text-primary-foreground me-2" />
|
||||||
<Typography variant={"h3"} className="text-primary-foreground">Buscar</Typography>
|
<Typography variant={"h3"} className="text-primary-foreground">Buscar</Typography>
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
function BackendConnectionStatus({ isLoading, isError }: { isLoading?: boolean, isError?: boolean }) {
|
||||||
|
|
||||||
|
const BACKEND_HOST = import.meta.env.VITE_BACKEND_HOST;
|
||||||
|
const BACKEND_PORT = import.meta.env.VITE_BACKEND_PORT;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Conected to Back-End at: {BACKEND_HOST} - {BACKEND_PORT}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Conection Status: {isError ? "ERROR" : (isLoading ? "LOADING" : "LOADED")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BackendConnectionStatus
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { CalendarIcon } from "@radix-ui/react-icons"
|
import { CalendarIcon } from "@radix-ui/react-icons"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
import { ActiveModifiers, DateRange } from "react-day-picker"
|
import { ActiveModifiers, DateBefore, DateRange, Matcher } from "react-day-picker"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -15,26 +15,50 @@ import {
|
|||||||
} from "@/components/ui/popover"
|
} from "@/components/ui/popover"
|
||||||
import { Typography } from "./typography"
|
import { Typography } from "./typography"
|
||||||
|
|
||||||
|
export type UpdateRangeFunction = (newRange: DateRange) => void;
|
||||||
|
|
||||||
export function DatePickerWithRange({
|
export function DatePickerWithRange({
|
||||||
className,
|
className, updateDateRange
|
||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
}: { className?: React.HTMLAttributes<HTMLDivElement> | undefined, updateDateRange: UpdateRangeFunction | undefined }) {
|
||||||
|
// Date Range State.
|
||||||
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
|
||||||
from: undefined,
|
from: undefined,
|
||||||
to: undefined,
|
to: undefined,
|
||||||
})
|
});
|
||||||
|
|
||||||
//TODOME: Si el día es mods.outside, navegar al mes correspondiente.
|
//TODOME: Si el día es mods.outside, navegar al mes correspondiente.
|
||||||
|
// Handle Day change events.
|
||||||
const handleDayClickFrom = (day: Date, mods: ActiveModifiers) => {
|
const handleDayClickFrom = (day: Date, mods: ActiveModifiers) => {
|
||||||
setDateRange({
|
const newDateRange: DateRange = {
|
||||||
from: mods.selected ? undefined : day,
|
from: mods.selected ? undefined : day,
|
||||||
to: dateRange?.to
|
to: dateRange?.to
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDateRange(newDateRange)
|
||||||
|
if (updateDateRange !== undefined) {
|
||||||
|
updateDateRange(newDateRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDayClickTo = (day: Date, mods: ActiveModifiers) => {
|
const handleDayClickTo = (day: Date, mods: ActiveModifiers) => {
|
||||||
setDateRange({
|
const newDateRange: DateRange = {
|
||||||
from: dateRange?.from,
|
from: dateRange?.from,
|
||||||
to: mods.selected ? undefined : day
|
to: mods.selected ? undefined : day
|
||||||
})
|
}
|
||||||
|
|
||||||
|
setDateRange(newDateRange)
|
||||||
|
|
||||||
|
if (updateDateRange !== undefined) {
|
||||||
|
updateDateRange(newDateRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable days from "To" before "From"
|
||||||
|
let beforeAndSelected: Matcher[] = [];
|
||||||
|
if (dateRange?.from) {
|
||||||
|
const beforeMatcher: DateBefore = { before: dateRange?.from };
|
||||||
|
const dateMatcher: Matcher = dateRange?.from;
|
||||||
|
beforeAndSelected = [beforeMatcher, dateMatcher];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -96,6 +120,7 @@ export function DatePickerWithRange({
|
|||||||
selected={dateRange?.to}
|
selected={dateRange?.to}
|
||||||
onDayClick={handleDayClickTo}
|
onDayClick={handleDayClickTo}
|
||||||
numberOfMonths={1}
|
numberOfMonths={1}
|
||||||
|
disabled={beforeAndSelected}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tool
|
|||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
|
|
||||||
interface IconWithTooltipNumberProps {
|
interface IconWithTooltipNumberProps {
|
||||||
amount: number | null |undefined,
|
amount?: number | null |undefined,
|
||||||
altText: string | null | undefined
|
altText?: string | null | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IconWithTooltipNumber({ amount = undefined, altText = undefined, children }: PropsWithChildren<IconWithTooltipNumberProps>) {
|
export function IconWithTooltipNumber({ amount = undefined, altText = undefined, children }: PropsWithChildren<IconWithTooltipNumberProps>) {
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Table = React.forwardRef<
|
||||||
|
HTMLTableElement,
|
||||||
|
React.HTMLAttributes<HTMLTableElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table
|
||||||
|
ref={ref}
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
Table.displayName = "Table"
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
|
))
|
||||||
|
TableHeader.displayName = "TableHeader"
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableBody.displayName = "TableBody"
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableFooter.displayName = "TableFooter"
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<
|
||||||
|
HTMLTableRowElement,
|
||||||
|
React.HTMLAttributes<HTMLTableRowElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableRow.displayName = "TableRow"
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableHead.displayName = "TableHead"
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCell.displayName = "TableCell"
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TableCaption.displayName = "TableCaption"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Cross2Icon } from "@radix-ui/react-icons"
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
|
VariantProps<typeof toastVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toastVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Cross2Icon className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
))
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm opacity-90", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider>
|
||||||
|
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription>{description}</ToastDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "src/components/ui/toast"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string
|
||||||
|
title?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
action?: ToastActionElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"]
|
||||||
|
toast: ToasterToast
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"]
|
||||||
|
toast: Partial<ToasterToast>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId)
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = []
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState<State>(memoryState)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState)
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState)
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast }
|
||||||
@@ -5,6 +5,7 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom';
|
|||||||
import Root from './routes/Root';
|
import Root from './routes/Root';
|
||||||
import ErrorPage from './error-page';
|
import ErrorPage from './error-page';
|
||||||
import Homepage from './routes/home-page';
|
import Homepage from './routes/home-page';
|
||||||
|
import SearchPage from './routes/search-page';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -15,6 +16,10 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: "/home",
|
path: "/home",
|
||||||
element: <Homepage />
|
element: <Homepage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/search",
|
||||||
|
element: <SearchPage />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export type RoomAvailabity = {
|
||||||
|
roomCategory: number,
|
||||||
|
availability: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AvailabilityRequest = {
|
||||||
|
filter: AvailabilityFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AvailabilityFilter = {
|
||||||
|
startDate: string,
|
||||||
|
endDate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FareRequest = {
|
||||||
|
filter: FareFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FareFilter = {
|
||||||
|
roomCategory: number,
|
||||||
|
startDate: string,
|
||||||
|
endDate: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { DateRange } from "react-day-picker"
|
||||||
|
|
||||||
|
export type SearchFilter = {
|
||||||
|
dateRange: DateRange
|
||||||
|
}
|
||||||
+3
-1
@@ -3,6 +3,7 @@ import './Root.css'
|
|||||||
import AppHeader from '../components/app-header'
|
import AppHeader from '../components/app-header'
|
||||||
import AppFooter from '../components/app-footer'
|
import AppFooter from '../components/app-footer'
|
||||||
import { ThemeProvider } from '@/components/theme-provider'
|
import { ThemeProvider } from '@/components/theme-provider'
|
||||||
|
import { Toaster } from '@/components/ui/toaster'
|
||||||
|
|
||||||
function Root() {
|
function Root() {
|
||||||
|
|
||||||
@@ -10,9 +11,10 @@ function Root() {
|
|||||||
<>
|
<>
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
<div className='appPage'>
|
<div className='appPage bg-background text-foreground'>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
<Toaster />
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useAPIRoomCategories } from "@/api/api-roomCategories"
|
import { useAPIRoomCategories } from "@/api/api-roomCategories"
|
||||||
import { RoomItemCard } from "@/components/Rooms/room-item-card"
|
import { RoomItemCard } from "@/components/Rooms/room-item-card"
|
||||||
import { SearchFilterCard } from "@/components/Search/SearchFilterCard"
|
import { SearchFilterCard } from "@/components/Search/SearchFilterCard"
|
||||||
|
import BackendConnectionStatus from "@/components/backend-conection-status"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
import { Typography } from "@/components/ui/typography"
|
import { Typography } from "@/components/ui/typography"
|
||||||
@@ -10,17 +11,14 @@ function Homepage() {
|
|||||||
|
|
||||||
const { roomCategoryList, isLoading, isError } = useAPIRoomCategories();
|
const { roomCategoryList, isLoading, isError } = useAPIRoomCategories();
|
||||||
|
|
||||||
const BACKEND_HOST = import.meta.env.VITE_BACKEND_HOST;
|
|
||||||
const BACKEND_PORT = import.meta.env.VITE_BACKEND_PORT;
|
|
||||||
|
|
||||||
const roomList = roomCategoryList?.map((element: RoomCategory) => {
|
const roomList = roomCategoryList?.map((element: RoomCategory) => {
|
||||||
return (
|
return (
|
||||||
<RoomItemCard key={element.id} room={element}/>
|
<RoomItemCard key={element.id} room={element} />
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background text-foreground">
|
<>
|
||||||
<Typography variant={"h1"}>Hostal del Val</Typography>
|
<Typography variant={"h1"}>Hostal del Val</Typography>
|
||||||
<Separator className="my-10" />
|
<Separator className="my-10" />
|
||||||
<SearchFilterCard />
|
<SearchFilterCard />
|
||||||
@@ -31,17 +29,12 @@ function Homepage() {
|
|||||||
{isLoading && (
|
{isLoading && (
|
||||||
<>
|
<>
|
||||||
<Typography variant={"h3"} as="h2">Loading...</Typography>
|
<Typography variant={"h3"} as="h2">Loading...</Typography>
|
||||||
<Skeleton className="w-[400px] h-[200px] mx-auto"/>
|
<Skeleton className="w-[400px] h-[200px] mx-auto" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Separator className="my-10" />
|
<Separator className="my-10" />
|
||||||
<p>
|
<BackendConnectionStatus isLoading={isLoading} isError={isError} />
|
||||||
Conected to Back-End at: {BACKEND_HOST} - {BACKEND_PORT}
|
</>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Conection Status: {isError ? "ERROR" : (isLoading ? "LOADING" : "LOADED")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useAPIRoomAvailability } from "@/api/api-roomSearch"
|
||||||
|
import RoomBooking from "@/components/Rooms/room-booking"
|
||||||
|
import { SearchFilterCard } from "@/components/Search/SearchFilterCard"
|
||||||
|
import BackendConnectionStatus from "@/components/backend-conection-status"
|
||||||
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import { Typography } from "@/components/ui/typography"
|
||||||
|
import { AvailabilityFilter } from "@/model/RoomSearch"
|
||||||
|
import { SearchFilter } from "@/model/SearchFilter"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
//TODOME: Igual podemos crear un StateMachine conjunta para los siguiente estados de la página: Sin Buscar, Sin disponibilidad, error, cargando.
|
||||||
|
function SearchPage() {
|
||||||
|
|
||||||
|
const [availabilityFilter, setAvailabilityFilter] = useState<AvailabilityFilter>();
|
||||||
|
const [isSearched, setIsSearched] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const onBtSearchClick = (newFilter: SearchFilter) => {
|
||||||
|
setAvailabilityFilter({
|
||||||
|
startDate: newFilter.dateRange.from?.toDateString() ?? "",
|
||||||
|
endDate: newFilter.dateRange.to?.toDateString() ?? ""
|
||||||
|
});
|
||||||
|
setIsSearched(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { roomAvailabilityList, isLoading: isLoadingAvailability, isError: isErrorAvailability } = useAPIRoomAvailability(availabilityFilter ?? null);
|
||||||
|
|
||||||
|
const isLoading = isLoadingAvailability;
|
||||||
|
const isError = isErrorAvailability;
|
||||||
|
|
||||||
|
const roomList = roomAvailabilityList?.map((element) => {
|
||||||
|
return (
|
||||||
|
<RoomBooking key={element.roomCategory} availableAmount={element.availability} roomCategoryId={element.roomCategory} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const noRoomsAvailable = (
|
||||||
|
<>
|
||||||
|
<Typography variant={"h3"} as="h3">Sorry 😔, there aren't rooms available for selected period.
|
||||||
|
You can contact us for more info.</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
const hasRooms = (roomAvailabilityList?.length && roomAvailabilityList?.length > 0);
|
||||||
|
console.log(hasRooms);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-background text-foreground">
|
||||||
|
<SearchFilterCard onSearchClick={onBtSearchClick} />
|
||||||
|
<Separator className="my-10" />
|
||||||
|
{isLoading && (
|
||||||
|
<>
|
||||||
|
<Typography variant={"h3"} as="h2">Loading...</Typography>
|
||||||
|
<Skeleton className="w-[400px] h-[200px] mx-auto" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{hasRooms ? roomList : (isSearched && noRoomsAvailable)}
|
||||||
|
</div>
|
||||||
|
<Separator className="my-10" />
|
||||||
|
<RoomBooking key={99} availableAmount={5} roomCategoryId={6} />
|
||||||
|
<Separator className="my-10" />
|
||||||
|
<BackendConnectionStatus isLoading={isLoading} isError={isError} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchPage
|
||||||
Reference in New Issue
Block a user