Merge branch 'develop' into feat/performance-improvements

This commit is contained in:
Linus Flood
2024-11-06 13:48:26 +01:00
70 changed files with 1149 additions and 519 deletions
@@ -23,7 +23,7 @@ export const joinDetailsSchema = baseDetailsSchema.merge(
z.object({
join: z.literal(true),
zipCode: z.string().min(1, { message: "Zip code is required" }),
dateOfBirth: z.string(),
dateOfBirth: z.string().min(1, { message: "Date of birth is required" }),
termsAccepted: z.literal(true, {
errorMap: (err, ctx) => {
switch (err.code) {
@@ -0,0 +1,33 @@
"use client"
import { useIntl } from "react-intl"
import useSidePeekStore from "@/stores/sidepeek"
import Button from "@/components/TempDesignSystem/Button"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
export default function ToggleSidePeek({
hotelId,
roomTypeCode,
}: ToggleSidePeekProps) {
const intl = useIntl()
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
return (
<Button
onClick={() =>
openSidePeek({ key: SidePeekEnum.roomDetails, hotelId, roomTypeCode })
}
theme="base"
size="small"
variant="icon"
intent="text"
wrapping
>
{intl.formatMessage({ id: "See room details" })}{" "}
</Button>
)
}
@@ -2,15 +2,25 @@
import { useIntl } from "react-intl"
import { RoomConfiguration } from "@/server/routers/hotels/output"
import { EditIcon, ImageIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./selectedRoom.module.css"
export default function SelectedRoom() {
export default function SelectedRoom({
hotelId,
room,
}: {
hotelId: string
room: RoomConfiguration
}) {
const intl = useIntl()
return (
<article className={styles.container}>
@@ -22,42 +32,50 @@ export default function SelectedRoom() {
/>
</div>
<div className={styles.content}>
<div className={styles.textContainer}>
<Footnote
className={styles.label}
color="uiTextPlaceholder"
textTransform="uppercase"
>
{intl.formatMessage({ id: "Your room" })}
</Footnote>
<div className={styles.text}>
{/**
* [TEMP]
* No translation on Subtitles as they will be derived
* from Room selection.
*/}
<Subtitle
className={styles.room}
color="uiTextHighContrast"
type="two"
<div>
<div className={styles.textContainer}>
<Footnote
className={styles.label}
color="uiTextPlaceholder"
textTransform="uppercase"
>
Cozy cabin
</Subtitle>
<Subtitle
className={styles.invertFontWeight}
color="uiTextMediumContrast"
type="two"
>
Free rebooking
</Subtitle>
<Subtitle
className={styles.invertFontWeight}
color="uiTextMediumContrast"
type="two"
>
Pay now
</Subtitle>
{intl.formatMessage({ id: "Your room" })}
</Footnote>
<div className={styles.text}>
{/**
* [TEMP]
* No translation on Subtitles as they will be derived
* from Room selection.
*/}
<Subtitle
className={styles.room}
color="uiTextHighContrast"
type="two"
>
{room.roomType}
</Subtitle>
<Subtitle
className={styles.invertFontWeight}
color="uiTextMediumContrast"
type="two"
>
Free rebooking
</Subtitle>
<Subtitle
className={styles.invertFontWeight}
color="uiTextMediumContrast"
type="two"
>
Pay now
</Subtitle>
</div>
</div>
{room?.roomTypeCode && (
<ToggleSidePeek
hotelId={hotelId}
roomTypeCode={room.roomTypeCode}
/>
)}
</div>
<Button
asChild
@@ -1,46 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import Contact from "@/components/HotelReservation/Contact"
import Divider from "@/components/TempDesignSystem/Divider"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import Body from "@/components/TempDesignSystem/Text/Body"
import styles from "./enterDetailsSidePeek.module.css"
import {
SidePeekEnum,
SidePeekProps,
} from "@/types/components/hotelReservation/enterDetails/sidePeek"
export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) {
const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek)
const close = useEnterDetailsStore((state) => state.closeSidePeek)
const intl = useIntl()
return (
<SidePeek
contentKey={SidePeekEnum.hotelDetails}
title={intl.formatMessage({ id: "About the hotel" })}
isOpen={activeSidePeek === SidePeekEnum.hotelDetails}
handleClose={close}
>
<article className={styles.spacing}>
<Contact hotel={hotel} />
<Divider />
<section className={styles.spacing}>
<Body>{hotel.hotelContent.texts.descriptions.medium}</Body>
{hotel.hotelContent.texts.facilityInformation
.split(/[\n\r]/g)
.filter((p) => p)
.map((paragraph, idx) => (
<Body key={`facilityInfo-${idx}`}>{paragraph}</Body>
))}
</section>
</article>
</SidePeek>
)
}
@@ -1,35 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { ChevronRightSmallIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
export default function ToggleSidePeek() {
const intl = useIntl()
const openSidePeek = useEnterDetailsStore((state) => state.openSidePeek)
return (
<Button
onClick={() => {
openSidePeek(SidePeekEnum.hotelDetails)
}}
theme="base"
size="small"
variant="icon"
intent="text"
wrapping
>
{intl.formatMessage({ id: "See room details" })}{" "}
<ChevronRightSmallIcon
color="baseButtonTextOnFillNormal"
height={20}
width={20}
/>
</Button>
)
}
+17 -99
View File
@@ -1,111 +1,29 @@
"use client"
import { useState } from "react"
import { useIntl } from "react-intl"
import useSidePeekStore from "@/stores/sidepeek"
import { ChevronRightIcon } from "@/components/Icons"
import Accordion from "@/components/TempDesignSystem/Accordion"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Button from "@/components/TempDesignSystem/Button"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import Contact from "../Contact"
import styles from "./readMore.module.css"
import {
ParkingProps,
ReadMoreProps,
} from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { Amenities, Hotel } from "@/types/hotel"
import { ReadMoreProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
function getAmenitiesList(hotel: Hotel) {
const detailedAmenities: Amenities = hotel.detailedFacilities.filter(
// Remove Parking facilities since parking accordion is based on hotel.parking
(facility) => !facility.name.startsWith("Parking") && facility.public
)
return detailedAmenities
}
export default function ReadMore({ label, hotel, hotelId }: ReadMoreProps) {
const intl = useIntl()
const [sidePeekOpen, setSidePeekOpen] = useState(false)
const amenitiesList = getAmenitiesList(hotel)
export default function ReadMore({ label, hotelId }: ReadMoreProps) {
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
return (
<>
<Button
onPress={() => {
setSidePeekOpen(true)
}}
intent="text"
theme="base"
wrapping
className={styles.detailsButton}
>
{label}
<ChevronRightIcon color="burgundy" />
</Button>
<SidePeek
title={hotel.name}
isOpen={sidePeekOpen}
contentKey={`${hotelId}`}
handleClose={() => {
setSidePeekOpen(false)
}}
>
<div className={styles.content}>
<Subtitle>
{intl.formatMessage({ id: "Practical information" })}
</Subtitle>
<Contact hotel={hotel} />
<Accordion>
{/* parking */}
{hotel.parking.length ? (
<AccordionItem title={intl.formatMessage({ id: "Parking" })}>
{hotel.parking.map((p) => (
<Parking key={p.name} parking={p} />
))}
</AccordionItem>
) : null}
<AccordionItem title={intl.formatMessage({ id: "Accessibility" })}>
TODO: What content should be in the accessibility section?
</AccordionItem>
{amenitiesList.map((amenity) => {
return (
<div key={amenity.id} className={styles.amenity}>
{amenity.name}
</div>
)
})}
</Accordion>
{/* TODO: handle linking to Hotel Page */}
<Button theme={"base"}>To the hotel</Button>
</div>
</SidePeek>
</>
)
}
function Parking({ parking }: ParkingProps) {
const intl = useIntl()
return (
<div>
<Body>{`${intl.formatMessage({ id: parking.type })} (${parking.name})`}</Body>
<ul className={styles.list}>
<li>
{`${intl.formatMessage({
id: "Number of charging points for electric cars",
})}: ${parking.numberOfChargingSpaces}`}
</li>
<li>{`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${parking.canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}</li>
<li>{`${intl.formatMessage({ id: "Number of parking spots" })}: ${parking.numberOfParkingSpots}`}</li>
<li>{`${intl.formatMessage({ id: "Distance to hotel" })}: ${parking.distanceToHotel} m`}</li>
<li>{`${intl.formatMessage({ id: "Address" })}: ${parking.address}`}</li>
</ul>
</div>
<Button
onPress={() => {
openSidePeek({ key: SidePeekEnum.hotelDetails, hotelId })
}}
intent="text"
theme="base"
wrapping
className={styles.detailsButton}
>
{label}
<ChevronRightIcon color="burgundy" />
</Button>
)
}
@@ -1,5 +1,6 @@
.container {
min-width: 272px;
display: none;
}
.facilities {
@@ -24,3 +25,9 @@
height: 1.25rem;
margin: 0;
}
@media (min-width: 768px) {
.container {
display: block;
}
}
@@ -0,0 +1,48 @@
"use client"
import { useIntl } from "react-intl"
import { selectHotelMap } from "@/constants/routes/hotelReservation"
import { FilterIcon, MapIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
import styles from "./mobileMapButtonContainer.module.css"
export default function MobileMapButtonContainer({ city }: { city: string }) {
const intl = useIntl()
const lang = useLang()
return (
<div className={styles.buttonContainer}>
<Button
asChild
variant="icon"
intent="secondary"
size="small"
className={styles.button}
>
<Link
href={`${selectHotelMap[lang]}`}
keepSearchParams
color="burgundy"
>
<MapIcon color="burgundy" />
{intl.formatMessage({ id: "See on map" })}
</Link>
</Button>
{/* TODO: Add filter toggle */}
<Button
variant="icon"
intent="secondary"
size="small"
className={styles.button}
>
<FilterIcon color="burgundy" />
{intl.formatMessage({ id: "Filter and sort" })}
</Button>
</div>
)
}
@@ -0,0 +1,15 @@
.buttonContainer {
display: flex;
gap: var(--Spacing-x2);
margin-bottom: var(--Spacing-x3);
}
.button {
flex: 1;
}
@media (min-width: 768px) {
.buttonContainer {
display: none;
}
}
@@ -0,0 +1,9 @@
.hotelListing {
display: none;
}
@media (min-width: 768px) {
.hotelListing {
display: block;
}
}
@@ -1,5 +1,7 @@
"use client"
import styles from "./hotelListing.module.css"
import { HotelListingProps } from "@/types/components/hotelReservation/selectHotel/map"
// TODO: This component is copied from
@@ -7,5 +9,5 @@ import { HotelListingProps } from "@/types/components/hotelReservation/selectHot
// Look at that for inspiration on how to do the interaction with the map.
export default function HotelListing({}: HotelListingProps) {
return <section>Hotel listing TBI</section>
return <section className={styles.hotelListing}>Hotel listing TBI</section>
}
@@ -1,14 +1,14 @@
"use client"
import { APIProvider } from "@vis.gl/react-google-maps"
import { useRouter, useSearchParams } from "next/navigation"
import { useState } from "react"
import { useIntl } from "react-intl"
import { selectHotel } from "@/constants/routes/hotelReservation"
import { CloseIcon } from "@/components/Icons"
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
import InteractiveMap from "@/components/Maps/InteractiveMap"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
import HotelListing from "./HotelListing"
@@ -22,36 +22,60 @@ export default function SelectHotelMap({
coordinates,
pointsOfInterest,
mapId,
isModal,
}: SelectHotelMapProps) {
const searchParams = useSearchParams()
const router = useRouter()
const lang = useLang()
const intl = useIntl()
const [activePoi, setActivePoi] = useState<string | null>(null)
function handleModalDismiss() {
router.back()
}
function handlePageRedirect() {
router.push(`${selectHotel[lang]}?${searchParams.toString()}`)
}
const closeButton = (
<Button
asChild
intent="inverted"
size="small"
theme="base"
className={styles.closeButton}
onClick={isModal ? handleModalDismiss : handlePageRedirect}
>
<Link href={selectHotel[lang]} keepSearchParams color="burgundy">
<CloseIcon color="burgundy" />
{intl.formatMessage({ id: "Close the map" })}
</Link>
<CloseIcon color="burgundy" />
{intl.formatMessage({ id: "Close the map" })}
</Button>
)
return (
<APIProvider apiKey={apiKey}>
<HotelListing />
<InteractiveMap
closeButton={closeButton}
coordinates={coordinates}
pointsOfInterest={pointsOfInterest}
activePoi={activePoi}
onActivePoiChange={setActivePoi}
mapId={mapId}
/>
<div className={styles.container}>
<div className={styles.filterContainer}>
<Button
intent="text"
size="small"
variant="icon"
wrapping
onClick={isModal ? handleModalDismiss : handlePageRedirect}
>
<CloseLargeIcon />
</Button>
<span>Filter and sort</span>
{/* TODO: Add filter and sort button */}
</div>
<HotelListing />
<InteractiveMap
closeButton={closeButton}
coordinates={coordinates}
pointsOfInterest={pointsOfInterest}
activePoi={activePoi}
onActivePoiChange={setActivePoi}
mapId={mapId}
/>
</div>
</APIProvider>
)
}
@@ -2,4 +2,39 @@
pointer-events: initial;
box-shadow: var(--button-box-shadow);
gap: var(--Spacing-x-half);
display: none !important;
}
.container {
height: 100%;
}
.filterContainer {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
top: 0;
left: 0;
right: 0;
z-index: 10;
background-color: var(--Base-Surface-Secondary-light-Normal);
padding: 0 var(--Spacing-x2);
height: 44px;
}
.filterContainer .closeButton {
color: var(--UI-Text-High-Contrast);
}
@media (min-width: 768px) {
.closeButton {
display: flex !important;
}
.filterContainer {
display: none;
}
.container {
display: flex;
}
}
@@ -5,13 +5,13 @@ import { useIntl } from "react-intl"
import { RateDefinition } from "@/server/routers/hotels/output"
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import RoomSidePeek from "../../../../SidePeeks/RoomSidePeek"
import ImageGallery from "../../ImageGallery"
import { getIconForFeatureCode } from "../../utils"
@@ -21,6 +21,7 @@ import type { RoomCardProps } from "@/types/components/hotelReservation/selectRa
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
export default function RoomCard({
hotelId,
rateDefinitions,
roomConfiguration,
roomCategories,
@@ -57,9 +58,10 @@ export default function RoomCard({
?.generalTerms
}
const petRoomPackage = packages.find(
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
)
const petRoomPackage =
(selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM) &&
packages.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) ||
undefined
const selectedRoom = roomCategories.find(
(room) => room.name === roomConfiguration.roomType
@@ -86,8 +88,11 @@ export default function RoomCard({
: `${roomSize?.min}-${roomSize?.max}`}
m²
</Caption>
{selectedRoom && (
<RoomSidePeek room={selectedRoom} buttonSize="small" />
{roomConfiguration.roomTypeCode && (
<ToggleSidePeek
hotelId={hotelId}
roomTypeCode={roomConfiguration.roomTypeCode}
/>
)}
</div>
<div className={styles.container}>
@@ -67,6 +67,7 @@ export default function RoomSelection({
{roomConfigurations.map((roomConfiguration) => (
<li key={roomConfiguration.roomTypeCode}>
<RoomCard
hotelId={roomsAvailability.hotelId.toString()}
rateDefinitions={rateDefinitions}
roomConfiguration={roomConfiguration}
roomCategories={roomCategories}
@@ -0,0 +1,60 @@
"use client"
import { trpc } from "@/lib/trpc/client"
import useSidePeekStore from "@/stores/sidepeek"
import HotelSidePeek from "@/components/SidePeeks/HotelSidePeek"
import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek"
import useLang from "@/hooks/useLang"
import { HotelData } from "@/types/hotel"
export default function HotelReservationSidePeek({
hotel,
}: {
hotel: HotelData | null
}) {
const activeSidePeek = useSidePeekStore((state) => state.activeSidePeek)
const hotelId = useSidePeekStore((state) => state.hotelId)
const roomTypeCode = useSidePeekStore((state) => state.roomTypeCode)
const close = useSidePeekStore((state) => state.closeSidePeek)
const lang = useLang()
const { data: hotelData } = trpc.hotel.hotelData.get.useQuery(
{
hotelId: hotelId ?? "",
language: lang,
},
{
enabled: !!hotelId,
initialData: hotel ?? undefined,
}
)
const selectedRoom = hotelData?.included?.find((room) =>
room.roomTypes.some((type) => type.code === roomTypeCode)
)
if (activeSidePeek) {
return (
<>
{hotelData && (
<HotelSidePeek
hotel={hotelData.data?.attributes}
activeSidePeek={activeSidePeek}
close={close}
/>
)}
{selectedRoom && (
<RoomSidePeek
room={selectedRoom}
activeSidePeek={activeSidePeek}
close={close}
/>
)}
</>
)
}
return null
}