From d4e4c4a0d01e83291f631c639f26ba2e40f0405f Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Mon, 9 Dec 2024 20:21:16 +0100 Subject: [PATCH] fix(SW-1143): Added loading/skeleton to select hotel --- .../(standard)/select-hotel/page.tsx | 160 ++---------------- .../HotelCard/HotelCardSkeleton.module.css | 58 +++++++ .../HotelCard/HotelCardSkeleton.tsx | 33 ++++ .../SelectHotel/SelectHotelSkeleton.tsx | 42 +++++ .../HotelReservation/SelectHotel/index.tsx | 154 +++++++++++++++++ .../SelectHotel/selectHotel.module.css | 18 ++ .../selectHotel/selectHotel.ts | 19 ++- 7 files changed, 341 insertions(+), 143 deletions(-) create mode 100644 components/HotelReservation/HotelCard/HotelCardSkeleton.module.css create mode 100644 components/HotelReservation/HotelCard/HotelCardSkeleton.tsx create mode 100644 components/HotelReservation/SelectHotel/SelectHotelSkeleton.tsx create mode 100644 components/HotelReservation/SelectHotel/index.tsx rename app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css => components/HotelReservation/SelectHotel/selectHotel.module.css (88%) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 662d051d7..76f30ed5b 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -1,42 +1,18 @@ import { notFound } from "next/navigation" import { Suspense } from "react" -import { - selectHotel, - selectHotelMap, -} from "@/constants/routes/hotelReservation" import { getLocations } from "@/lib/trpc/memoizedRequests" -import { - fetchAvailableHotels, - getFiltersFromHotels, -} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" -import HotelCardListing from "@/components/HotelReservation/HotelCardListing" -import HotelCount from "@/components/HotelReservation/SelectHotel/HotelCount" -import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" -import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter" -import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer" +import SelectHotel from "@/components/HotelReservation/SelectHotel" +import { SelectHotelSkeleton } from "@/components/HotelReservation/SelectHotel/SelectHotelSkeleton" import { generateChildrenString, getHotelReservationQueryParams, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" -import { ChevronRightIcon } from "@/components/Icons" -import StaticMap from "@/components/Maps/StaticMap" -import Alert from "@/components/TempDesignSystem/Alert" -import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs" -import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton" -import Button from "@/components/TempDesignSystem/Button" -import Link from "@/components/TempDesignSystem/Link" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { getIntl } from "@/i18n" import { setLang } from "@/i18n/serverContext" -import styles from "./page.module.css" - -import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" -import { AlertTypeEnum } from "@/types/enums/alert" -import { LangParams, PageArgs } from "@/types/params" +import type { LangParams, PageArgs } from "@/types/params" export default async function SelectHotelPage({ params, @@ -55,10 +31,6 @@ export default async function SelectHotelPage({ if (!city) return notFound() - const isCityWithCountry = (city: any): city is { country: string } => - "country" in city - - const intl = await getIntl() const selectHotelParams = new URLSearchParams(searchParams) const selectHotelParamsObject = getHotelReservationQueryParams(selectHotelParams) @@ -70,121 +42,25 @@ export default async function SelectHotelPage({ return notFound() } - const adults = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms - const children = selectHotelParamsObject.room[0].child + const adultsParams = selectHotelParamsObject.room[0].adults // TODO: Handle multiple rooms + const childrenParams = selectHotelParamsObject.room[0].child ? generateChildrenString(selectHotelParamsObject.room[0].child) : undefined // TODO: Handle multiple rooms - const hotels = await fetchAvailableHotels({ - cityId: city.id, - roomStayStartDate: searchParams.fromDate, - roomStayEndDate: searchParams.toDate, - adults, - children, - }) - - const validHotels = hotels.filter( - (hotel): hotel is HotelData => hotel !== null - ) - - const filterList = getFiltersFromHotels(validHotels) - const breadcrumbs = [ - { - title: intl.formatMessage({ id: "Home" }), - href: `/${params.lang}`, - uid: "home-page", - }, - { - title: intl.formatMessage({ id: "Hotel reservation" }), - href: `/${params.lang}/hotelreservation`, - uid: "hotel-reservation", - }, - { - title: intl.formatMessage({ id: "Select hotel" }), - href: `${selectHotel(params.lang)}/?${selectHotelParams}`, - uid: "select-hotel", - }, - { - title: city.name, - uid: city.id, - }, - ] - - const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined) + const reservationParams = { + selectHotelParams, + searchParams, + adultsParams, + childrenParams, + } return ( - <> -
- }> - - -
-
- {city.name} - -
-
- -
-
- -
-
-
- {hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available - -
- - -
- - ) : ( -
- -
- )} - -
-
- {isAllUnavailable && ( - - )} - -
-
- + }> + + ) } diff --git a/components/HotelReservation/HotelCard/HotelCardSkeleton.module.css b/components/HotelReservation/HotelCard/HotelCardSkeleton.module.css new file mode 100644 index 000000000..2305130c7 --- /dev/null +++ b/components/HotelReservation/HotelCard/HotelCardSkeleton.module.css @@ -0,0 +1,58 @@ +.card { + font-size: 14px; + display: flex; + flex-direction: column; + background-color: #fff; + border-radius: var(--Corner-radius-Large); + border: 1px solid var(--Base-Border-Subtle); + position: relative; + height: 100%; + justify-content: space-between; + min-height: 200px; + flex: 1; + overflow: hidden; +} + +.imageContainer { + aspect-ratio: 16/9; + width: 100%; + height: 200px; +} + +.priceVariants { + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); + padding: var(--Spacing-x2); + flex: 1; +} + +.content { + display: flex; + flex-direction: column; + flex: 1; + gap: var(--Spacing-x1); + padding: var(--Spacing-x2); +} + +.text { + display: none; +} + +@media (min-width: 1367px) { + .content { + padding: var(--Spacing-x2) 0 var(--Spacing-x2) var(--Spacing-x2); + } + + .text { + display: block; + } + + .card { + flex-direction: row; + } + .imageContainer { + width: 315px; + height: 100%; + } +} diff --git a/components/HotelReservation/HotelCard/HotelCardSkeleton.tsx b/components/HotelReservation/HotelCard/HotelCardSkeleton.tsx new file mode 100644 index 000000000..9c62f413a --- /dev/null +++ b/components/HotelReservation/HotelCard/HotelCardSkeleton.tsx @@ -0,0 +1,33 @@ +import SkeletonShimmer from "@/components/SkeletonShimmer" + +import styles from "./HotelCardSkeleton.module.css" + +export function HotelCardSkeleton() { + return ( +
+ {/* image container */} +
+ +
+ +
+ +
+ + + + +
+ + +
+ +
+ {/* price variants */} + {Array.from({ length: 2 }).map((_, index) => ( + + ))} +
+
+ ) +} diff --git a/components/HotelReservation/SelectHotel/SelectHotelSkeleton.tsx b/components/HotelReservation/SelectHotel/SelectHotelSkeleton.tsx new file mode 100644 index 000000000..7b8fe5d72 --- /dev/null +++ b/components/HotelReservation/SelectHotel/SelectHotelSkeleton.tsx @@ -0,0 +1,42 @@ +import SkeletonShimmer from "@/components/SkeletonShimmer" + +import { HotelCardSkeleton } from "../HotelCard/HotelCardSkeleton" + +import styles from "./selectHotel.module.css" + +type Props = { + count?: number +} + +export async function SelectHotelSkeleton({ count = 4 }: Props) { + return ( +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+ {Array.from({ length: count }).map((_, index) => ( + + ))} +
+
+
+ ) +} diff --git a/components/HotelReservation/SelectHotel/index.tsx b/components/HotelReservation/SelectHotel/index.tsx new file mode 100644 index 000000000..9acb038d7 --- /dev/null +++ b/components/HotelReservation/SelectHotel/index.tsx @@ -0,0 +1,154 @@ +import { + selectHotel, + selectHotelMap, +} from "@/constants/routes/hotelReservation" + +import { + fetchAvailableHotels, + getFiltersFromHotels, +} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" +import { ChevronRightIcon } from "@/components/Icons" +import StaticMap from "@/components/Maps/StaticMap" +import Alert from "@/components/TempDesignSystem/Alert" +import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs" +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { getIntl } from "@/i18n" + +import HotelCardListing from "../HotelCardListing" +import HotelCount from "./HotelCount" +import HotelFilter from "./HotelFilter" +import HotelSorter from "./HotelSorter" +import MobileMapButtonContainer from "./MobileMapButtonContainer" + +import styles from "./selectHotel.module.css" + +import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" +import type { SelectHotelProps } from "@/types/components/hotelReservation/selectHotel/selectHotel" +import { AlertTypeEnum } from "@/types/enums/alert" + +export default async function SelectHotel({ + city, + params, + reservationParams, +}: SelectHotelProps) { + const { selectHotelParams, searchParams, adultsParams, childrenParams } = + reservationParams + + const intl = await getIntl() + + const hotels = await fetchAvailableHotels({ + cityId: city.id, + roomStayStartDate: searchParams.fromDate, + roomStayEndDate: searchParams.toDate, + adults: adultsParams, + children: childrenParams?.toString(), + }) + + const isCityWithCountry = (city: any): city is { country: string } => + "country" in city + + const validHotels = hotels.filter( + (hotel): hotel is HotelData => hotel !== null + ) + + const filterList = getFiltersFromHotels(validHotels) + const breadcrumbs = [ + { + title: intl.formatMessage({ id: "Home" }), + href: `/${params.lang}`, + uid: "home-page", + }, + { + title: intl.formatMessage({ id: "Hotel reservation" }), + href: `/${params.lang}/hotelreservation`, + uid: "hotel-reservation", + }, + { + title: intl.formatMessage({ id: "Select hotel" }), + href: `${selectHotel(params.lang)}/?${selectHotelParams}`, + uid: "select-hotel", + }, + { + title: city.name, + uid: city.id, + }, + ] + + const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined) + + return ( + <> +
+ +
+
+ {city.name} + +
+
+ +
+
+ +
+
+
+ {hotels.length > 0 ? ( // TODO: Temp fix until API returns hotels that are not available + +
+ + +
+ + ) : ( +
+ +
+ )} + +
+
+ {isAllUnavailable && ( + + )} + +
+
+ + ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css b/components/HotelReservation/SelectHotel/selectHotel.module.css similarity index 88% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css rename to components/HotelReservation/SelectHotel/selectHotel.module.css index 052d9ee6b..264b8b10b 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css +++ b/components/HotelReservation/SelectHotel/selectHotel.module.css @@ -7,6 +7,7 @@ max-width: var(--max-width); margin: 0 auto; } + .header { display: flex; flex-direction: column; @@ -34,6 +35,10 @@ flex-direction: column; } +.sideBarItem { + display: none; +} + .link { display: none; } @@ -59,6 +64,10 @@ display: none; } +.skeletonContainer .title { + margin-bottom: var(--Spacing-x3); +} + @media (min-width: 768px) { .main { padding: var(--Spacing-x5); @@ -92,6 +101,9 @@ .sideBar { max-width: 340px; } + .sideBarItem { + display: block; + } .filter { display: block; } @@ -114,4 +126,10 @@ .buttonContainer { display: none; } + .skeletonContainer .title { + margin-bottom: 0; + } + .skeletonContainer .sideBar { + gap: var(--Spacing-x3); + } } diff --git a/types/components/hotelReservation/selectHotel/selectHotel.ts b/types/components/hotelReservation/selectHotel/selectHotel.ts index d5f8807bd..016645813 100644 --- a/types/components/hotelReservation/selectHotel/selectHotel.ts +++ b/types/components/hotelReservation/selectHotel/selectHotel.ts @@ -1,4 +1,8 @@ -import { CheckInData, Hotel, ParkingData } from "@/types/hotel" +import { Lang } from "@/constants/languages" + +import type { CheckInData, Hotel, ParkingData } from "@/types/hotel" +import type { Location } from "@/types/trpc/routers/hotel/locations" +import type { SelectHotelSearchParams } from "./selectHotelSearchParams" export enum AvailabilityEnum { Available = "Available", @@ -35,3 +39,16 @@ export interface CheckInCheckOutProps { export interface MeetingsAndConferencesProps { meetingDescription: string } + +export interface SelectHotelProps { + city: Location + params: { + lang: Lang + } + reservationParams: { + selectHotelParams: URLSearchParams + searchParams: SelectHotelSearchParams + adultsParams: number + childrenParams: string | undefined + } +}