Redesign some more pages and other improvements.
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { useAPIRoomCategoryId } from "@/api/api-roomCategories";
|
||||
import { Card, CardContent, CardHeader } from "../ui/card";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
import { useMemo } from "react";
|
||||
import { Typography } from "../ui/typography";
|
||||
import { RoomCategoryPhoto } from "@/model/RoomCategory";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -17,7 +16,10 @@ import { DateRange } from "react-day-picker";
|
||||
import { FareFilter } from "@/model/RoomSearch";
|
||||
import { useAPIRoomPrices } from "@/api/api-roomSearch";
|
||||
import { NumberSelector } from "../ui/number-selector";
|
||||
import { NewBookingRoom } from "@/model/Bookings";
|
||||
import { NewBooking, NewBookingRoom } from "@/model/Bookings";
|
||||
import { RoomItemCard } from "./room-item-card";
|
||||
import { IconWithTooltip } from "../ui/icon-number-tooltip";
|
||||
import { UserIcon } from "lucide-react";
|
||||
|
||||
function RoomBooking({
|
||||
roomCategoryId,
|
||||
@@ -26,14 +28,13 @@ function RoomBooking({
|
||||
roomCategoryId: number;
|
||||
availableAmount: number;
|
||||
}) {
|
||||
const [selectedAmount, setSelectedAmount] = useState<Map<number, number>>(
|
||||
new Map(),
|
||||
);
|
||||
|
||||
const currentDates: DateRange = useAppStore(
|
||||
(state) => state.searchFilter.dateRange,
|
||||
);
|
||||
const addNewRoom = useAppStore((state) => state.addNewBookingRoom);
|
||||
const bookingRooms: NewBooking | null = useAppStore(
|
||||
(state) => state.newBookingData,
|
||||
);
|
||||
|
||||
function onSelectedAmountUpdate(
|
||||
guestNumber: number,
|
||||
@@ -49,10 +50,6 @@ function RoomBooking({
|
||||
newCount,
|
||||
);
|
||||
|
||||
const newAmountMap = new Map(selectedAmount);
|
||||
newAmountMap.set(guestNumber, newCount);
|
||||
setSelectedAmount(newAmountMap);
|
||||
|
||||
const newRoom: NewBookingRoom = {
|
||||
roomCategory: roomCategoryId,
|
||||
guestNumber: guestNumber,
|
||||
@@ -64,7 +61,10 @@ function RoomBooking({
|
||||
|
||||
const totalSelected = () => {
|
||||
let sum = 0;
|
||||
selectedAmount.forEach((v) => (sum += v));
|
||||
bookingRooms &&
|
||||
bookingRooms.rooms
|
||||
.filter((v) => v.roomCategory == roomCategoryId)
|
||||
.forEach((v) => (sum += v.amount));
|
||||
return sum;
|
||||
};
|
||||
const canAddMore: boolean = availableAmount > totalSelected();
|
||||
@@ -93,7 +93,6 @@ function RoomBooking({
|
||||
isError: isErrorPrices,
|
||||
} = useAPIRoomPrices(fareFilter);
|
||||
|
||||
//TODOME: Gestionar también los estados al cargar los precios.
|
||||
const isLoading = isLoadingCategory || isLoadingPrices;
|
||||
const isError = isErrorCategory || isErrorPrices;
|
||||
|
||||
@@ -109,23 +108,21 @@ function RoomBooking({
|
||||
);
|
||||
}
|
||||
|
||||
const mainPhoto: RoomCategoryPhoto | undefined =
|
||||
roomCategory?.roomCategoryPhotos.filter((p) => p.kind == "main")[0];
|
||||
const photoElement = mainPhoto && (
|
||||
<img
|
||||
src={mainPhoto.path}
|
||||
alt={mainPhoto.altText}
|
||||
className="rounded-md mx-0 px-0"
|
||||
></img>
|
||||
);
|
||||
|
||||
const hasPrices = roomPriceList != null && roomPriceList?.length > 0;
|
||||
let priceTable = null;
|
||||
if (hasPrices) {
|
||||
const tableRows = roomPriceList?.map((el) => {
|
||||
return (
|
||||
<TableRow key={el.guestNumber}>
|
||||
<TableCell>{el.guestNumber}</TableCell>
|
||||
<TableCell>
|
||||
<IconWithTooltip
|
||||
label={el.guestNumber?.toString()}
|
||||
tooltipText={"Guest Number"}
|
||||
labelPosition={"right"}
|
||||
>
|
||||
<UserIcon></UserIcon>
|
||||
</IconWithTooltip>
|
||||
</TableCell>
|
||||
<TableCell>{el.price / 100} €</TableCell>
|
||||
<TableCell>
|
||||
<NumberSelector
|
||||
@@ -140,51 +137,46 @@ function RoomBooking({
|
||||
});
|
||||
|
||||
priceTable = (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Guest number</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoadingPrices ? (
|
||||
<Skeleton className="w-[80%] h-[100%]" />
|
||||
) : (
|
||||
tableRows
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<p>Rooms Available: {availableAmount}</p>
|
||||
</>
|
||||
<Table className="overflow-hidden">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Guest number</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoadingPrices ? <Skeleton className="w-full h-full" /> : tableRows}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
let roomCard = null;
|
||||
if (roomCategory) {
|
||||
roomCard = (
|
||||
<RoomItemCard
|
||||
room={roomCategory}
|
||||
key={roomCategoryId}
|
||||
className="z-20 border-4"
|
||||
></RoomItemCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-row gap-5 w-full">
|
||||
<div className="w-[40%] flex-none">{photoElement}</div>
|
||||
<div>
|
||||
<Typography variant={"h3"} as="h3">
|
||||
{roomCategory?.name}
|
||||
</Typography>
|
||||
<Typography>{roomCategory?.description}</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{hasPrices ? (
|
||||
priceTable
|
||||
) : (
|
||||
<Typography variant={"largeText"}>No Prices available</Typography>
|
||||
)}
|
||||
<div></div>
|
||||
<p>Selected: {totalSelected()}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="relative sm:grid sm:grid-cols-40/60">
|
||||
{roomCard}
|
||||
<Card className="relative w-[95%] sm:w-full sm:h-4/5 self-center mx-auto sm:mx-0 rounded-b-xl rounded-none sm:rounded-e-xl -translate-y-2 sm:-translate-x-2 sm:translate-y-0">
|
||||
<CardContent className="p-5 w-full h-full flex flex-col justify-center gap-4">
|
||||
{hasPrices ? (
|
||||
priceTable
|
||||
) : (
|
||||
<Typography variant={"largeText"}>No Prices available</Typography>
|
||||
)}
|
||||
<p>Rooms Available: {availableAmount}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import { BedDoubleIcon, BedSingleIcon, UserIcon } from "lucide-react";
|
||||
import { RoomCategory, RoomCategoryPhoto } from "@/model/RoomCategory";
|
||||
import { IconWithTooltip } from "../ui/icon-number-tooltip";
|
||||
import roomImg from "@/res/SomniaRoomImage.svg";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface RoomItemCardProps {
|
||||
room: RoomCategory;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultRoom: RoomCategory = {
|
||||
@@ -20,7 +22,10 @@ const defaultRoom: RoomCategory = {
|
||||
roomCategoryPhotos: [],
|
||||
};
|
||||
|
||||
export function RoomItemCard({ room = defaultRoom }: RoomItemCardProps) {
|
||||
export function RoomItemCard({
|
||||
room = defaultRoom,
|
||||
className,
|
||||
}: RoomItemCardProps) {
|
||||
const mainPhoto: RoomCategoryPhoto | undefined = room.roomCategoryPhotos.find(
|
||||
(element) => {
|
||||
return (element.kind = "main");
|
||||
@@ -35,7 +40,7 @@ export function RoomItemCard({ room = defaultRoom }: RoomItemCardProps) {
|
||||
bedTypeList.at(1) !== undefined && bedTypeList[1] > 0;
|
||||
|
||||
return (
|
||||
<Card id={room.id.toString()} className="relative">
|
||||
<Card id={room.id.toString()} className={cn(className, "relative")}>
|
||||
{/* <CardHeader className="p-0 m-0 mb-5">
|
||||
</CardHeader> */}
|
||||
<CardContent className="flex flex-col h-full w-full p-0">
|
||||
|
||||
@@ -3,15 +3,10 @@ import { Card, CardContent, CardHeader } from "../ui/card";
|
||||
import { Typography } from "../ui/typography";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { Separator } from "../ui/separator";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../ui/table";
|
||||
import { Button } from "../ui/button";
|
||||
import { useAPIRoomCategories } from "@/api/api-roomCategories";
|
||||
import { IconWithTooltip } from "../ui/icon-number-tooltip";
|
||||
import { UserIcon } from "lucide-react";
|
||||
|
||||
type OnBookingClickFunction = () => void;
|
||||
type BookingDetailsModes = "Book" | "View";
|
||||
@@ -24,6 +19,7 @@ export function BookingDetails({
|
||||
mode: BookingDetailsModes;
|
||||
}) {
|
||||
const bookingData = useAppStore((state) => state.newBookingData);
|
||||
const roomCategoryList = useAPIRoomCategories();
|
||||
|
||||
const onBtBookingClick = () => {
|
||||
if (onBookingConfirm) {
|
||||
@@ -47,32 +43,46 @@ export function BookingDetails({
|
||||
}
|
||||
|
||||
const roomList = bookingData.rooms.map((room) => {
|
||||
const category = roomCategoryList.roomCategoryList
|
||||
?.filter((c) => c.id == room.roomCategory)
|
||||
.at(0);
|
||||
const photo = category?.roomCategoryPhotos.at(0);
|
||||
|
||||
return (
|
||||
<TableRow key={room.roomCategory + "_" + room.guestNumber}>
|
||||
<TableCell>{room.roomCategory}</TableCell>
|
||||
<TableCell>{room.guestNumber}</TableCell>
|
||||
<TableCell>{room.amount}</TableCell>
|
||||
<TableCell>{room.price / 100} €</TableCell>
|
||||
<TableCell>{(room.price * room.amount) / 100} €</TableCell>
|
||||
</TableRow>
|
||||
<Card
|
||||
key={room.roomCategory.toString() + room.guestNumber.toString()}
|
||||
className="bg-accent flex flex-row my-3 "
|
||||
>
|
||||
{photo && (
|
||||
<img
|
||||
src={photo.path}
|
||||
className="max-w-[30%] rounded-s-xl object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="w-full p-3">
|
||||
<Typography variant={"h3"} as="h3" className="text-center m-2">
|
||||
{category?.name}
|
||||
</Typography>
|
||||
<div className="flex flex-row w-full justify-around">
|
||||
<IconWithTooltip
|
||||
label={room.guestNumber.toString()}
|
||||
labelPosition={"right"}
|
||||
tooltipText={"Guest Number"}
|
||||
>
|
||||
<UserIcon />
|
||||
</IconWithTooltip>
|
||||
<Typography variant={"largeText"}>
|
||||
{room.amount} x {room.price / 100}€
|
||||
</Typography>
|
||||
<Typography variant={"largeText"}>
|
||||
{(room.amount * room.price) / 100}€
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
const roomTable = (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Room</TableHead>
|
||||
<TableHead>Guests</TableHead>
|
||||
<TableHead>Quantity</TableHead>
|
||||
<TableHead>Unit price</TableHead>
|
||||
<TableHead>Total price</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>{roomList}</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const totalPrice = bookingData.rooms.reduce(
|
||||
(acc, room) => acc + room.price * room.amount,
|
||||
0,
|
||||
@@ -91,11 +101,14 @@ export function BookingDetails({
|
||||
Arrival: {bookingData.arrivalDate.toLocaleDateString()} - Departure:{" "}
|
||||
{bookingData.departureDate.toLocaleDateString()} <br></br>
|
||||
Number of nights:{" "}
|
||||
{differenceInDays(bookingData.departureDate, bookingData.arrivalDate)}
|
||||
{differenceInDays(
|
||||
bookingData.departureDate,
|
||||
bookingData.arrivalDate,
|
||||
) + 1}
|
||||
</Typography>
|
||||
<Separator className="my-2"></Separator>
|
||||
<Typography variant={"h3"}>Your Rooms</Typography>
|
||||
{roomTable}
|
||||
<div className="">{roomList}</div>
|
||||
<Separator className="my-2"></Separator>
|
||||
<Typography variant={"h3"}>Total Price</Typography>
|
||||
<Typography variant={"h3"}>{totalPrice / 100} €</Typography>
|
||||
|
||||
@@ -24,7 +24,7 @@ export function IconWithTooltip({
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltipText}</p>
|
||||
<p className="inline">{tooltipText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -38,10 +38,10 @@ export function IconWithTooltip({
|
||||
</Typography>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{label && labelPosition == "left" && labelElement}
|
||||
{iconElement}
|
||||
{label && labelPosition == "right" && labelElement}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+43
-32
@@ -21,25 +21,6 @@ function SearchPage() {
|
||||
|
||||
const [isSearched, setIsSearched] = useState<boolean>(false);
|
||||
|
||||
const onBtSearchClick = (newFilter: SearchFilter) => {
|
||||
console.log("onBtSearchClick: ");
|
||||
console.log(newFilter);
|
||||
|
||||
if (
|
||||
newFilter.dateRange &&
|
||||
newFilter.dateRange.from &&
|
||||
newFilter.dateRange.to
|
||||
) {
|
||||
setIsSearched(true);
|
||||
|
||||
resetBooking(newFilter.dateRange.from, newFilter.dateRange.to);
|
||||
}
|
||||
};
|
||||
|
||||
const onBookingContinue = () => {
|
||||
navigate("/book");
|
||||
};
|
||||
|
||||
//This seems a little bit overkill, but IDK.
|
||||
const availabilityFilter: AvailabilityFilter | null = useMemo(() => {
|
||||
if (
|
||||
@@ -66,6 +47,25 @@ function SearchPage() {
|
||||
const isLoading = isLoadingAvailability;
|
||||
const isError = isErrorAvailability;
|
||||
|
||||
const onBtSearchClick = (newFilter: SearchFilter) => {
|
||||
console.log("onBtSearchClick: ");
|
||||
console.log(newFilter);
|
||||
|
||||
if (
|
||||
newFilter.dateRange &&
|
||||
newFilter.dateRange.from &&
|
||||
newFilter.dateRange.to
|
||||
) {
|
||||
setIsSearched(true);
|
||||
|
||||
resetBooking(newFilter.dateRange.from, newFilter.dateRange.to);
|
||||
}
|
||||
};
|
||||
|
||||
const onBookingContinue = () => {
|
||||
navigate("/book");
|
||||
};
|
||||
|
||||
const roomList = roomAvailabilityList?.map((element) => {
|
||||
return (
|
||||
<RoomBooking
|
||||
@@ -87,26 +87,37 @@ function SearchPage() {
|
||||
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" />
|
||||
</>
|
||||
)}
|
||||
const pageContent = isLoading ? (
|
||||
<>
|
||||
<Typography variant={"h3"} as="h2">
|
||||
Loading...
|
||||
</Typography>
|
||||
<Skeleton className="w-full h-[200px] mx-auto" />
|
||||
</>
|
||||
) : (
|
||||
<div className="2xl:grid 2xl:grid-cols-60/40 2xl:gap-6">
|
||||
<div className="flex flex-col gap-5">
|
||||
{hasRooms ? roomList : isSearched && noRoomsAvailable}
|
||||
</div>
|
||||
<Separator className="my-10" />
|
||||
<Separator className="my-10 2xl:hidden" />
|
||||
<BookingDetails
|
||||
onBookingConfirm={onBookingContinue}
|
||||
mode={"Book"}
|
||||
></BookingDetails>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-background text-foreground">
|
||||
<SearchFilterCard onSearchClick={onBtSearchClick} />
|
||||
<Separator className="my-10" />
|
||||
{hasRooms ? (
|
||||
pageContent
|
||||
) : (
|
||||
<Typography>
|
||||
Realiza una búsqueda para ver la disponibilidad y los precios.
|
||||
</Typography>
|
||||
)}
|
||||
<Separator className="my-10" />
|
||||
<BackendConnectionStatus isLoading={isLoading} isError={isError} />
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,12 @@ export default {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
|
||||
//SomniaRooms Extends
|
||||
gridTemplateColumns: {
|
||||
"60/40": "60% 40%",
|
||||
"40/60": "40% 60%",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [plugin],
|
||||
|
||||
Reference in New Issue
Block a user