3 Commits
Author SHA1 Message Date
Daniel-Garmig f476f4307a Added Prettier for TailwindCSS.
CI / Docker Lint (push) Successful in 21s
2024-09-03 19:05:45 +02:00
Daniel-Garmig dedfc1cdd8 Redesign some more pages and other improvements. 2024-09-03 18:58:47 +02:00
Daniel-Garmig 2d2992593a Refactor HomePage and more! 2024-08-30 21:20:40 +02:00
19 changed files with 4295 additions and 236 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"endOfLine": "lf" "endOfLine": "lf",
"plugins": ["prettier-plugin-tailwindcss"]
} }
+97 -12
View File
@@ -44,7 +44,8 @@
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "3.3.3", "prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.2.2", "typescript": "^5.2.2",
@@ -2397,12 +2398,13 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -3068,10 +3070,11 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@@ -3414,6 +3417,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@@ -3594,12 +3598,13 @@
} }
}, },
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.5", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"braces": "^3.0.2", "braces": "^3.0.3",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
}, },
"engines": { "engines": {
@@ -4058,6 +4063,85 @@
"url": "https://github.com/prettier/prettier?sponsor=1" "url": "https://github.com/prettier/prettier?sponsor=1"
} }
}, },
"node_modules/prettier-plugin-tailwindcss": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.6.tgz",
"integrity": "sha512-OPva5S7WAsPLEsOuOWXATi13QrCKACCiIonFgIR6V4lYv4QLp++UXVhZSzRbZxXGimkQtQT86CC6fQqTOybGng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.21.3"
},
"peerDependencies": {
"@ianvs/prettier-plugin-sort-imports": "*",
"@prettier/plugin-pug": "*",
"@shopify/prettier-plugin-liquid": "*",
"@trivago/prettier-plugin-sort-imports": "*",
"@zackad/prettier-plugin-twig-melody": "*",
"prettier": "^3.0",
"prettier-plugin-astro": "*",
"prettier-plugin-css-order": "*",
"prettier-plugin-import-sort": "*",
"prettier-plugin-jsdoc": "*",
"prettier-plugin-marko": "*",
"prettier-plugin-multiline-arrays": "*",
"prettier-plugin-organize-attributes": "*",
"prettier-plugin-organize-imports": "*",
"prettier-plugin-sort-imports": "*",
"prettier-plugin-style-order": "*",
"prettier-plugin-svelte": "*"
},
"peerDependenciesMeta": {
"@ianvs/prettier-plugin-sort-imports": {
"optional": true
},
"@prettier/plugin-pug": {
"optional": true
},
"@shopify/prettier-plugin-liquid": {
"optional": true
},
"@trivago/prettier-plugin-sort-imports": {
"optional": true
},
"@zackad/prettier-plugin-twig-melody": {
"optional": true
},
"prettier-plugin-astro": {
"optional": true
},
"prettier-plugin-css-order": {
"optional": true
},
"prettier-plugin-import-sort": {
"optional": true
},
"prettier-plugin-jsdoc": {
"optional": true
},
"prettier-plugin-marko": {
"optional": true
},
"prettier-plugin-multiline-arrays": {
"optional": true
},
"prettier-plugin-organize-attributes": {
"optional": true
},
"prettier-plugin-organize-imports": {
"optional": true
},
"prettier-plugin-sort-imports": {
"optional": true
},
"prettier-plugin-style-order": {
"optional": true
},
"prettier-plugin-svelte": {
"optional": true
}
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4701,6 +4785,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
+4 -2
View File
@@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 && npx prettier . --check",
"pretty": "npx prettier . --write",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
@@ -46,7 +47,8 @@
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "3.3.3", "prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.2.2", "typescript": "^5.2.2",
+59 -67
View File
@@ -1,8 +1,7 @@
import { useAPIRoomCategoryId } from "@/api/api-roomCategories"; import { useAPIRoomCategoryId } from "@/api/api-roomCategories";
import { Card, CardContent, CardHeader } from "../ui/card"; import { Card, CardContent } from "../ui/card";
import { useMemo, useState } from "react"; import { useMemo } from "react";
import { Typography } from "../ui/typography"; import { Typography } from "../ui/typography";
import { RoomCategoryPhoto } from "@/model/RoomCategory";
import { import {
Table, Table,
TableBody, TableBody,
@@ -17,7 +16,10 @@ import { DateRange } from "react-day-picker";
import { FareFilter } from "@/model/RoomSearch"; import { FareFilter } from "@/model/RoomSearch";
import { useAPIRoomPrices } from "@/api/api-roomSearch"; import { useAPIRoomPrices } from "@/api/api-roomSearch";
import { NumberSelector } from "../ui/number-selector"; 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({ function RoomBooking({
roomCategoryId, roomCategoryId,
@@ -26,14 +28,13 @@ function RoomBooking({
roomCategoryId: number; roomCategoryId: number;
availableAmount: number; availableAmount: number;
}) { }) {
const [selectedAmount, setSelectedAmount] = useState<Map<number, number>>(
new Map(),
);
const currentDates: DateRange = useAppStore( const currentDates: DateRange = useAppStore(
(state) => state.searchFilter.dateRange, (state) => state.searchFilter.dateRange,
); );
const addNewRoom = useAppStore((state) => state.addNewBookingRoom); const addNewRoom = useAppStore((state) => state.addNewBookingRoom);
const bookingRooms: NewBooking | null = useAppStore(
(state) => state.newBookingData,
);
function onSelectedAmountUpdate( function onSelectedAmountUpdate(
guestNumber: number, guestNumber: number,
@@ -49,10 +50,6 @@ function RoomBooking({
newCount, newCount,
); );
const newAmountMap = new Map(selectedAmount);
newAmountMap.set(guestNumber, newCount);
setSelectedAmount(newAmountMap);
const newRoom: NewBookingRoom = { const newRoom: NewBookingRoom = {
roomCategory: roomCategoryId, roomCategory: roomCategoryId,
guestNumber: guestNumber, guestNumber: guestNumber,
@@ -64,7 +61,10 @@ function RoomBooking({
const totalSelected = () => { const totalSelected = () => {
let sum = 0; let sum = 0;
selectedAmount.forEach((v) => (sum += v)); bookingRooms &&
bookingRooms.rooms
.filter((v) => v.roomCategory == roomCategoryId)
.forEach((v) => (sum += v.amount));
return sum; return sum;
}; };
const canAddMore: boolean = availableAmount > totalSelected(); const canAddMore: boolean = availableAmount > totalSelected();
@@ -93,12 +93,11 @@ function RoomBooking({
isError: isErrorPrices, isError: isErrorPrices,
} = useAPIRoomPrices(fareFilter); } = useAPIRoomPrices(fareFilter);
//TODOME: Gestionar también los estados al cargar los precios.
const isLoading = isLoadingCategory || isLoadingPrices; const isLoading = isLoadingCategory || isLoadingPrices;
const isError = isErrorCategory || isErrorPrices; const isError = isErrorCategory || isErrorPrices;
if (isLoading) { if (isLoading) {
return <Skeleton className="w-[600px] h-[150px] mx-auto" />; return <Skeleton className="mx-auto h-[150px] w-[600px]" />;
} }
if (isError) { if (isError) {
@@ -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; const hasPrices = roomPriceList != null && roomPriceList?.length > 0;
let priceTable = null; let priceTable = null;
if (hasPrices) { if (hasPrices) {
const tableRows = roomPriceList?.map((el) => { const tableRows = roomPriceList?.map((el) => {
return ( return (
<TableRow key={el.guestNumber}> <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>{el.price / 100} €</TableCell>
<TableCell> <TableCell>
<NumberSelector <NumberSelector
@@ -140,51 +137,46 @@ function RoomBooking({
}); });
priceTable = ( priceTable = (
<> <Table className="overflow-hidden">
<Table> <TableHeader>
<TableHeader> <TableRow>
<TableRow> <TableHead>Guest number</TableHead>
<TableHead>Guest number</TableHead> <TableHead>Price</TableHead>
<TableHead>Price</TableHead> <TableHead>Amount</TableHead>
<TableHead>Amount</TableHead> </TableRow>
</TableRow> </TableHeader>
</TableHeader> <TableBody>
<TableBody> {isLoadingPrices ? <Skeleton className="h-full w-full" /> : tableRows}
{isLoadingPrices ? ( </TableBody>
<Skeleton className="w-[80%] h-[100%]" /> </Table>
) : ( );
tableRows }
)}
</TableBody> let roomCard = null;
</Table> if (roomCategory) {
<p>Rooms Available: {availableAmount}</p> roomCard = (
</> <RoomItemCard
room={roomCategory}
key={roomCategoryId}
className="z-20 border-4"
></RoomItemCard>
); );
} }
return ( return (
<Card> <div className="relative sm:grid sm:grid-cols-40/60">
<CardHeader> {roomCard}
<div className="flex flex-row gap-5 w-full"> <Card className="relative mx-auto w-[95%] -translate-y-2 self-center rounded-none rounded-b-xl sm:mx-0 sm:h-4/5 sm:w-full sm:-translate-x-2 sm:translate-y-0 sm:rounded-e-xl">
<div className="w-[40%] flex-none">{photoElement}</div> <CardContent className="flex h-full w-full flex-col justify-center gap-4 p-5">
<div> {hasPrices ? (
<Typography variant={"h3"} as="h3"> priceTable
{roomCategory?.name} ) : (
</Typography> <Typography variant={"largeText"}>No Prices available</Typography>
<Typography>{roomCategory?.description}</Typography> )}
</div> <p>Rooms Available: {availableAmount}</p>
</div> </CardContent>
</CardHeader> </Card>
<CardContent> </div>
{hasPrices ? (
priceTable
) : (
<Typography variant={"largeText"}>No Prices available</Typography>
)}
<div></div>
<p>Selected: {totalSelected()}</p>
</CardContent>
</Card>
); );
} }
+61 -46
View File
@@ -1,12 +1,15 @@
import { Separator } from "../ui/separator"; import { Separator } from "../ui/separator";
import { Card, CardContent, CardHeader } from "../ui/card"; import { Card, CardContent } from "../ui/card";
import { Typography } from "../ui/typography"; import { Typography } from "../ui/typography";
import { BedDoubleIcon, BedSingleIcon, UserIcon } from "lucide-react"; import { BedDoubleIcon, BedSingleIcon, UserIcon } from "lucide-react";
import { RoomCategory, RoomCategoryPhoto } from "@/model/RoomCategory"; import { RoomCategory, RoomCategoryPhoto } from "@/model/RoomCategory";
import { IconWithTooltipNumber } from "../ui/icon-number-tooltip"; import { IconWithTooltip } from "../ui/icon-number-tooltip";
import roomImg from "@/res/SomniaRoomImage.svg";
import { cn } from "@/lib/utils";
interface RoomItemCardProps { interface RoomItemCardProps {
room: RoomCategory; room: RoomCategory;
className?: string;
} }
const defaultRoom: RoomCategory = { const defaultRoom: RoomCategory = {
@@ -19,7 +22,10 @@ const defaultRoom: RoomCategory = {
roomCategoryPhotos: [], roomCategoryPhotos: [],
}; };
export function RoomItemCard({ room = defaultRoom }: RoomItemCardProps) { export function RoomItemCard({
room = defaultRoom,
className,
}: RoomItemCardProps) {
const mainPhoto: RoomCategoryPhoto | undefined = room.roomCategoryPhotos.find( const mainPhoto: RoomCategoryPhoto | undefined = room.roomCategoryPhotos.find(
(element) => { (element) => {
return (element.kind = "main"); return (element.kind = "main");
@@ -34,50 +40,59 @@ export function RoomItemCard({ room = defaultRoom }: RoomItemCardProps) {
bedTypeList.at(1) !== undefined && bedTypeList[1] > 0; bedTypeList.at(1) !== undefined && bedTypeList[1] > 0;
return ( return (
<Card id={room.id.toString()}> <Card id={room.id.toString()} className={cn(className, "relative")}>
<CardHeader className="p-0 m-0 mb-5"> {/* <CardHeader className="p-0 m-0 mb-5">
<img </CardHeader> */}
src={ <CardContent className="flex h-full w-full flex-col p-0">
mainPhoto?.path ?? <div className="">
"https://images.unsplash.com/photo-1618773928121-c32242e63f39" <img
} src={mainPhoto?.path ?? roomImg}
className="rounded-md" className="aspect-[4/3] w-full rounded-t-md object-cover"
></img> ></img>
<Typography variant={"h2"} as="h1" className="mx-5 py-2"> <Separator className="my-0 border-2" />
{room.name} </div>
</Typography> <div>
</CardHeader> <Typography variant={"h2"} as="h2" className="border-none px-4 pt-4">
<CardContent> {room.name}
<Typography variant={"p"} as="p"> </Typography>
{room.description} </div>
</Typography> <div>
<Separator className="my-4" /> <Typography variant={"p"} as="h2" className="px-5">
<div className="flex justify-around"> {room.description}
<div className="flex flex-row gap-2"> </Typography>
{hasSingleBed && ( </div>
<IconWithTooltipNumber <div className="mt-auto pb-4">
altText="Single bed" <Separator className="my-4" />
amount={bedTypeList[0]} <div className="flex justify-around">
> <div className="flex flex-row gap-2">
<BedSingleIcon /> {hasSingleBed && (
</IconWithTooltipNumber> <IconWithTooltip
)} tooltipText="Single bed"
{hasDoubleBed && ( label={bedTypeList[0]?.toString()}
<>
<IconWithTooltipNumber
altText="Double bed"
amount={bedTypeList[1]}
> >
<BedDoubleIcon /> <BedSingleIcon />
</IconWithTooltipNumber> </IconWithTooltip>
</> )}
)} {hasDoubleBed && (
</div> <>
<div className="flex flex-row"> <IconWithTooltip
<UserIcon /> tooltipText="Double bed"
<Typography variant={"p_inline"} className="ml-3"> label={bedTypeList[1]?.toString()}
{room.maxGuestNum} >
</Typography> <BedDoubleIcon />
</IconWithTooltip>
</>
)}
</div>
<div className="flex flex-row">
<IconWithTooltip
tooltipText="Max. gests"
label={room.maxGuestNum?.toString()}
labelPosition={"right"}
>
<UserIcon />
</IconWithTooltip>
</div>
</div> </div>
</div> </div>
</CardContent> </CardContent>
+9 -11
View File
@@ -5,12 +5,11 @@ 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 { useEffect, useState } from "react"; import { useState } from "react";
import { SearchFilter } from "@/model/SearchFilter"; import { SearchFilter } from "@/model/SearchFilter";
import { DateRange } from "react-day-picker"; import { DateRange } from "react-day-picker";
import { useToast } from "../ui/use-toast"; import { useToast } from "../ui/use-toast";
import { useAppStore } from "@/store"; import { useAppStore } from "@/store";
import { useLocation } from "react-router-dom";
type funOnSearchClick = (newFilter: SearchFilter) => void; type funOnSearchClick = (newFilter: SearchFilter) => void;
@@ -29,10 +28,10 @@ export function SearchFilterCard({
//On location change, try to load searchFilter from state. //On location change, try to load searchFilter from state.
//FIXME: Si llegamos sin state, hay que borrar el state global... //FIXME: Si llegamos sin state, hay que borrar el state global...
const location = useLocation(); // const location = useLocation();
useEffect(() => { // useEffect(() => {
setSearchFilter(location.state); // setSearchFilter(location.state);
}, [location]); // }, [location]);
const { toast } = useToast(); const { toast } = useToast();
@@ -56,8 +55,9 @@ export function SearchFilterCard({
return; return;
} }
setAppSearchFilter(searchFilter);
if (onSearchClick) { if (onSearchClick) {
setAppSearchFilter(searchFilter);
onSearchClick(searchFilter); onSearchClick(searchFilter);
} }
}; };
@@ -68,8 +68,6 @@ export function SearchFilterCard({
...searchFilter, ...searchFilter,
dateRange: newRange, dateRange: newRange,
}); });
console.log("SearchFilterCard filter UPDATED");
}; };
const datePickerElement = const datePickerElement =
@@ -89,7 +87,7 @@ export function SearchFilterCard({
return ( return (
<Card> <Card>
<CardHeader className="p-0 m-0 mb-5"> <CardHeader className="m-0 mb-5 p-0">
<Typography variant={"h2"} as="h1" className="mx-5 py-2"> <Typography variant={"h2"} as="h1" className="mx-5 py-2">
Reserva ahora Reserva ahora
</Typography> </Typography>
@@ -98,7 +96,7 @@ export function SearchFilterCard({
{datePickerElement} {datePickerElement}
<Separator className="my-4" /> <Separator className="my-4" />
<Button size={"lg"} onClick={btSearchClick}> <Button size={"lg"} onClick={btSearchClick}>
<SearchIcon className="text-primary-foreground me-2" /> <SearchIcon className="me-2 text-primary-foreground" />
<Typography variant={"h3"} className="text-primary-foreground"> <Typography variant={"h3"} className="text-primary-foreground">
Buscar Buscar
</Typography> </Typography>
+1 -1
View File
@@ -1,6 +1,6 @@
function AppFooter() { function AppFooter() {
return ( return (
<div className="bg-secondary flex-col hidden"> <div className="hidden flex-col bg-secondary">
<div className="">Created with Love by me :)</div> <div className="">Created with Love by me :)</div>
<div className=""> <div className="">
<p>Más información</p> <p>Más información</p>
+4 -4
View File
@@ -7,15 +7,15 @@ import { BookAIcon, HomeIcon } from "lucide-react";
function AppHeader() { function AppHeader() {
return ( return (
<Card className="flex items-center mx-10 my-4"> <Card className="mx-10 my-4 flex items-center">
<CardContent className="w-full py-2"> <CardContent className="w-full py-2">
<div className="flex justify-around items-center"> <div className="flex items-center justify-around">
<div className="flex flex-row items-center gap-2 relative"> <div className="relative flex flex-row items-center gap-2">
<img src="public/SR2.svg" className="w-[64px]"></img> <img src="public/SR2.svg" className="w-[64px]"></img>
<Typography variant={"largeText"}>Somnia Rooms</Typography> <Typography variant={"largeText"}>Somnia Rooms</Typography>
<a <a
href="https://duck.com" href="https://duck.com"
className="absolute w-[100%] h-[100%]" className="absolute h-[100%] w-[100%]"
></a> ></a>
</div> </div>
<Button variant="ghost" className="h-15"> <Button variant="ghost" className="h-15">
+45 -32
View File
@@ -3,15 +3,10 @@ import { Card, CardContent, CardHeader } from "../ui/card";
import { Typography } from "../ui/typography"; import { Typography } from "../ui/typography";
import { differenceInDays } from "date-fns"; import { differenceInDays } from "date-fns";
import { Separator } from "../ui/separator"; import { Separator } from "../ui/separator";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../ui/table";
import { Button } from "../ui/button"; 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 OnBookingClickFunction = () => void;
type BookingDetailsModes = "Book" | "View"; type BookingDetailsModes = "Book" | "View";
@@ -24,6 +19,7 @@ export function BookingDetails({
mode: BookingDetailsModes; mode: BookingDetailsModes;
}) { }) {
const bookingData = useAppStore((state) => state.newBookingData); const bookingData = useAppStore((state) => state.newBookingData);
const roomCategoryList = useAPIRoomCategories();
const onBtBookingClick = () => { const onBtBookingClick = () => {
if (onBookingConfirm) { if (onBookingConfirm) {
@@ -47,32 +43,46 @@ export function BookingDetails({
} }
const roomList = bookingData.rooms.map((room) => { 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 ( return (
<TableRow key={room.roomCategory + "_" + room.guestNumber}> <Card
<TableCell>{room.roomCategory}</TableCell> key={room.roomCategory.toString() + room.guestNumber.toString()}
<TableCell>{room.guestNumber}</TableCell> className="my-3 flex flex-row bg-accent"
<TableCell>{room.amount}</TableCell> >
<TableCell>{room.price / 100} €</TableCell> {photo && (
<TableCell>{(room.price * room.amount) / 100} €</TableCell> <img
</TableRow> src={photo.path}
className="max-w-[30%] rounded-s-xl object-cover"
/>
)}
<div className="w-full p-3">
<Typography variant={"h3"} as="h3" className="m-2 text-center">
{category?.name}
</Typography>
<div className="flex w-full flex-row 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( const totalPrice = bookingData.rooms.reduce(
(acc, room) => acc + room.price * room.amount, (acc, room) => acc + room.price * room.amount,
0, 0,
@@ -91,11 +101,14 @@ export function BookingDetails({
Arrival: {bookingData.arrivalDate.toLocaleDateString()} - Departure:{" "} Arrival: {bookingData.arrivalDate.toLocaleDateString()} - Departure:{" "}
{bookingData.departureDate.toLocaleDateString()} <br></br> {bookingData.departureDate.toLocaleDateString()} <br></br>
Number of nights:{" "} Number of nights:{" "}
{differenceInDays(bookingData.departureDate, bookingData.arrivalDate)} {differenceInDays(
bookingData.departureDate,
bookingData.arrivalDate,
) + 1}
</Typography> </Typography>
<Separator className="my-2"></Separator> <Separator className="my-2"></Separator>
<Typography variant={"h3"}>Your Rooms</Typography> <Typography variant={"h3"}>Your Rooms</Typography>
{roomTable} <div className="">{roomList}</div>
<Separator className="my-2"></Separator> <Separator className="my-2"></Separator>
<Typography variant={"h3"}>Total Price</Typography> <Typography variant={"h3"}>Total Price</Typography>
<Typography variant={"h3"}>{totalPrice / 100} €</Typography> <Typography variant={"h3"}>{totalPrice / 100} €</Typography>
+1 -1
View File
@@ -204,7 +204,7 @@ const CarouselPrevious = React.forwardRef<
variant={variant} variant={variant}
size={size} size={size}
className={cn( className={cn(
"absolute h-8 w-8 rounded-full", "absolute h-8 w-8 rounded-full",
orientation === "horizontal" orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2" ? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
+11 -11
View File
@@ -46,13 +46,10 @@ export function DatePickerWithRange({
const handleDayClickFrom = (day: Date, mods: ActiveModifiers) => { const handleDayClickFrom = (day: Date, mods: ActiveModifiers) => {
const newDateRange: DateRange = { const newDateRange: DateRange = {
from: mods.selected ? undefined : day, from: mods.selected ? undefined : day,
to: dateRange?.to, to: undefined,
}; };
setDateRange(newDateRange); fnUpdateRange(newDateRange);
if (updateDateRange !== undefined) {
updateDateRange(newDateRange);
}
}; };
const handleDayClickTo = (day: Date, mods: ActiveModifiers) => { const handleDayClickTo = (day: Date, mods: ActiveModifiers) => {
@@ -61,10 +58,13 @@ export function DatePickerWithRange({
to: mods.selected ? undefined : day, to: mods.selected ? undefined : day,
}; };
setDateRange(newDateRange); fnUpdateRange(newDateRange);
};
const fnUpdateRange = (range: DateRange) => {
setDateRange(range);
if (updateDateRange !== undefined) { if (updateDateRange !== undefined) {
updateDateRange(newDateRange); updateDateRange(range);
} }
}; };
@@ -82,7 +82,7 @@ export function DatePickerWithRange({
} }
return ( return (
<div className={cn("grid gap-x-2 grid-cols-2 grid-rows-2", className)}> <div className={cn("grid grid-cols-2 grid-rows-2 gap-x-2", className)}>
<Typography variant={"largeText"} as="p"> <Typography variant={"largeText"} as="p">
Desde Desde
</Typography> </Typography>
@@ -107,7 +107,7 @@ export function DatePickerWithRange({
)} )}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start"> <PopoverContent className="w-auto p-0" align="center">
<Calendar <Calendar
initialFocus initialFocus
mode="single" mode="single"
@@ -137,11 +137,11 @@ export function DatePickerWithRange({
)} )}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start"> <PopoverContent className="w-auto p-0" align="center">
<Calendar <Calendar
initialFocus initialFocus
mode="single" mode="single"
defaultMonth={dateRange?.to} defaultMonth={dateRange?.to ?? dateRange?.from}
selected={dateRange?.to} selected={dateRange?.to}
onDayClick={handleDayClickTo} onDayClick={handleDayClickTo}
numberOfMonths={1} numberOfMonths={1}
+18 -10
View File
@@ -8,21 +8,23 @@ import {
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
interface IconWithTooltipNumberProps { interface IconWithTooltipNumberProps {
amount?: number | null | undefined; label?: string | null | undefined;
altText?: string | null | undefined; labelPosition?: "left" | "right" | null | undefined;
tooltipText?: string | null | undefined;
} }
export function IconWithTooltipNumber({ export function IconWithTooltip({
amount = undefined, label = undefined,
altText = undefined, labelPosition = "left",
tooltipText = undefined,
children, children,
}: PropsWithChildren<IconWithTooltipNumberProps>) { }: PropsWithChildren<IconWithTooltipNumberProps>) {
const iconElement = altText ? ( const iconElement = tooltipText ? (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger> <TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{altText}</p> <p className="inline">{tooltipText}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -30,10 +32,16 @@ export function IconWithTooltipNumber({
<>{children}</> <>{children}</>
); );
const labelElement = (
<Typography variant={"p_inline"} className="px-1">
{label}
</Typography>
);
return ( return (
<> <div className="flex flex-row items-center gap-1">
{amount && <Typography variant={"p_inline"}>{amount}</Typography>} {label && labelPosition == "left" && labelElement}
{iconElement} {iconElement}
</> {label && labelPosition == "right" && labelElement}
</div>
); );
} }
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 133 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 96 KiB

+1 -1
View File
@@ -10,7 +10,7 @@ function Root() {
<> <>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<AppHeader /> <AppHeader />
<div className="appPage bg-background text-foreground"> <div className="mx-auto w-full bg-background p-8 text-center text-foreground sm:w-[90%] md:w-[80%]">
<Outlet /> <Outlet />
</div> </div>
<Toaster /> <Toaster />
+3
View File
@@ -0,0 +1,3 @@
.homePageRoomGrid {
grid-template-columns: repeat(auto-fit, minmax(300px, 3fr));
}
+9 -5
View File
@@ -8,6 +8,8 @@ import { Typography } from "@/components/ui/typography";
import { RoomCategory } from "@/model/RoomCategory"; import { RoomCategory } from "@/model/RoomCategory";
import { SearchFilter } from "@/model/SearchFilter"; import { SearchFilter } from "@/model/SearchFilter";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { LoaderIcon } from "lucide-react";
import "./home-page.css";
function Homepage() { function Homepage() {
const { roomCategoryList, isLoading, isError } = useAPIRoomCategories(); const { roomCategoryList, isLoading, isError } = useAPIRoomCategories();
@@ -27,13 +29,15 @@ function Homepage() {
<Separator className="my-10" /> <Separator className="my-10" />
<SearchFilterCard onSearchClick={onBtSearchClick} /> <SearchFilterCard onSearchClick={onBtSearchClick} />
<Separator className="my-10" /> <Separator className="my-10" />
<div className="grid grid-cols-2 gap-4">{roomList}</div> <div className="homePageRoomGrid grid gap-4">{roomList}</div>
{isLoading && ( {isLoading && (
<> <>
<Typography variant={"h3"} as="h2"> <Skeleton className="mx-auto flex h-[200px] w-full flex-col items-center justify-center">
Loading... <LoaderIcon />
</Typography> <Typography variant={"h3"} as="h2">
<Skeleton className="w-[400px] h-[200px] mx-auto" /> Loading...
</Typography>
</Skeleton>
</> </>
)} )}
<Separator className="my-10" /> <Separator className="my-10" />
+43 -32
View File
@@ -21,25 +21,6 @@ function SearchPage() {
const [isSearched, setIsSearched] = useState<boolean>(false); 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. //This seems a little bit overkill, but IDK.
const availabilityFilter: AvailabilityFilter | null = useMemo(() => { const availabilityFilter: AvailabilityFilter | null = useMemo(() => {
if ( if (
@@ -66,6 +47,25 @@ function SearchPage() {
const isLoading = isLoadingAvailability; const isLoading = isLoadingAvailability;
const isError = isErrorAvailability; 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) => { const roomList = roomAvailabilityList?.map((element) => {
return ( return (
<RoomBooking <RoomBooking
@@ -87,26 +87,37 @@ function SearchPage() {
roomAvailabilityList?.length && roomAvailabilityList?.length > 0; roomAvailabilityList?.length && roomAvailabilityList?.length > 0;
console.log(hasRooms); console.log(hasRooms);
return ( const pageContent = isLoading ? (
<div className="bg-background text-foreground"> <>
<SearchFilterCard onSearchClick={onBtSearchClick} /> <Typography variant={"h3"} as="h2">
<Separator className="my-10" /> Loading...
{isLoading && ( </Typography>
<> <Skeleton className="mx-auto h-[200px] w-full" />
<Typography variant={"h3"} as="h2"> </>
Loading... ) : (
</Typography> <div className="2xl:grid 2xl:grid-cols-60/40 2xl:gap-6">
<Skeleton className="w-[400px] h-[200px] mx-auto" />
</>
)}
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
{hasRooms ? roomList : isSearched && noRoomsAvailable} {hasRooms ? roomList : isSearched && noRoomsAvailable}
</div> </div>
<Separator className="my-10" /> <Separator className="my-10 2xl:hidden" />
<BookingDetails <BookingDetails
onBookingConfirm={onBookingContinue} onBookingConfirm={onBookingContinue}
mode={"Book"} mode={"Book"}
></BookingDetails> ></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" /> <Separator className="my-10" />
<BackendConnectionStatus isLoading={isLoading} isError={isError} /> <BackendConnectionStatus isLoading={isLoading} isError={isError} />
</div> </div>
+6
View File
@@ -73,6 +73,12 @@ export default {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 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], plugins: [plugin],