refactor: update SidePeek composition

This commit is contained in:
Arvid Norlin
2024-08-12 13:27:03 +02:00
parent 04eb3c6d94
commit 68694ef914
7 changed files with 77 additions and 105 deletions

View File

@@ -2,8 +2,8 @@ import { about, amenities } from "@/constants/routes/hotelPageParams"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import SidePeekContainer from "@/components/TempDesignSystem/SidePeek/Container" import SidePeek from "@/components/TempDesignSystem/SidePeek"
import SidePeekContent from "@/components/TempDesignSystem/SidePeek/Content" import SidePeekItem from "@/components/TempDesignSystem/SidePeek/Item"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import AmenitiesList from "./AmenitiesList" import AmenitiesList from "./AmenitiesList"
@@ -20,10 +20,10 @@ export default async function HotelPage() {
if (!hotelPageIdentifierData) { if (!hotelPageIdentifierData) {
return null return null
} }
const lang = getLang()
const { attributes, roomCategories } = await serverClient().hotel.getHotel({ const { attributes, roomCategories } = await serverClient().hotel.getHotel({
hotelId: hotelPageIdentifierData.hotel_page_id, hotelId: hotelPageIdentifierData.hotel_page_id,
language: getLang(), language: lang,
include: ["RoomCategories"], include: ["RoomCategories"],
}) })
@@ -35,29 +35,27 @@ export default async function HotelPage() {
<main className={styles.mainSection}> <main className={styles.mainSection}>
<div className={styles.introContainer}> <div className={styles.introContainer}>
<IntroSection <IntroSection
// TODO: remove prop drilling once getLang() is merged
lang={lang}
hotelName={attributes.name} hotelName={attributes.name}
hotelDescription={attributes.hotelContent.texts.descriptions.short} hotelDescription={attributes.hotelContent.texts.descriptions.short}
location={attributes.location} location={attributes.location}
address={attributes.address} address={attributes.address}
tripAdvisor={attributes.ratings.tripAdvisor} tripAdvisor={attributes.ratings.tripAdvisor}
/> />
<SidePeekContainer> <SidePeek>
<SidePeekContent <SidePeekItem
contentKey={"amenities"} contentKey={amenities[lang]}
title={formatMessage({ id: "Amenities" })} title={formatMessage({ id: "Amenities" })}
> >
{/* TODO: Render amenities as per the design. */} {/* TODO: Render amenities as per the design. */}
Read more about the amenities here Read more about the amenities here
</SidePeekContent> </SidePeekItem>
<SidePeekContent <SidePeekItem
contentKey={"about"} contentKey={about[lang]}
title={formatMessage({ id: "About the hotel" })} title={formatMessage({ id: "About the hotel" })}
> >
Some additional information about the hotel Some additional information about the hotel
</SidePeekContent> </SidePeekItem>
</SidePeekContainer> </SidePeek>
<AmenitiesList <AmenitiesList
detailedFacilities={attributes.detailedFacilities} detailedFacilities={attributes.detailedFacilities}
lang={lang} lang={lang}

View File

@@ -9,7 +9,7 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import Preamble from "@/components/TempDesignSystem/Text/Preamble" import Preamble from "@/components/TempDesignSystem/Text/Preamble"
import Title from "@/components/TempDesignSystem/Text/Title" import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import { IntroSectionProps } from "./types" import { IntroSectionProps } from "./types"
import styles from "./introSection.module.css" import styles from "./introSection.module.css"
@@ -20,8 +20,6 @@ export default async function IntroSection({
location, location,
address, address,
tripAdvisor, tripAdvisor,
// TODO: remove prop drilling once getLang() is merged
lang,
}: IntroSectionProps) { }: IntroSectionProps) {
const intl = await getIntl() const intl = await getIntl()
const { formatMessage } = intl const { formatMessage } = intl
@@ -31,6 +29,7 @@ export default async function IntroSection({
{ id: "Distance to city centre" }, { id: "Distance to city centre" },
{ number: distanceToCentre } { number: distanceToCentre }
) )
const lang = getLang()
const formattedLocationText = `${streetAddress}, ${city} (${formattedDistanceText})` const formattedLocationText = `${streetAddress}, ${city} (${formattedDistanceText})`
const formattedTripAdvisorText = formatMessage( const formattedTripAdvisorText = formatMessage(
{ id: "Tripadvisor reviews" }, { id: "Tripadvisor reviews" },

View File

@@ -1,5 +1,3 @@
import { Lang } from "@/constants/languages"
import { import {
HotelAddress, HotelAddress,
HotelData, HotelData,
@@ -13,6 +11,4 @@ export type IntroSectionProps = {
location: HotelLocation location: HotelLocation
address: HotelAddress address: HotelAddress
tripAdvisor: HotelTripAdvisor tripAdvisor: HotelTripAdvisor
// TODO: remove prop drilling once getLang() is merged
lang: Lang
} }

View File

@@ -1,55 +0,0 @@
"use client"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import React, { Children, cloneElement, useEffect, useState } from "react"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { SidePeekContentKey } from "@/components/TempDesignSystem/SidePeek/types"
export default function SidePeekContainer({
children,
}: React.PropsWithChildren) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [activeSidePeek, setActiveSidePeek] =
useState<SidePeekContentKey | null>(() => {
const sidePeekParam = searchParams.get(
"open"
) as SidePeekContentKey | null
return sidePeekParam || null
})
useEffect(() => {
const sidePeekParam = searchParams.get("open") as SidePeekContentKey | null
if (sidePeekParam !== activeSidePeek) {
setActiveSidePeek(sidePeekParam)
}
}, [searchParams, activeSidePeek])
function handleClose(isOpen: boolean) {
if (!isOpen) {
setActiveSidePeek(null)
const nextSearchParams = new URLSearchParams(searchParams.toString())
nextSearchParams.delete("open")
router.push(`${pathname}?${nextSearchParams}`, { scroll: false })
}
}
const childrenWithIsActive = Children.map(children, (child) => {
if (!React.isValidElement(child)) {
return child
}
return cloneElement(child as React.ReactElement, {
isActive:
(child.props.contentKey as SidePeekContentKey) === activeSidePeek,
})
})
return (
<SidePeek activeContent={activeSidePeek} onClose={handleClose}>
{childrenWithIsActive}
</SidePeek>
)
}

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { Children, PropsWithChildren } from "react" import { PropsWithChildren } from "react"
import { CloseIcon } from "@/components/Icons" import { CloseIcon } from "@/components/Icons"
import { SidePeekContentProps } from "@/components/TempDesignSystem/SidePeek/types" import { SidePeekContentProps } from "@/components/TempDesignSystem/SidePeek/types"
@@ -8,16 +8,16 @@ import Title from "@/components/TempDesignSystem/Text/Title"
import Button from "../../Button" import Button from "../../Button"
import styles from "./content.module.css" import styles from "./sidePeekItem.module.css"
export default function Content({ function SidePeekItem({
title, title,
children, children,
isActive = false, isActive = false,
onClose, onClose,
}: PropsWithChildren<SidePeekContentProps>) { }: PropsWithChildren<SidePeekContentProps>) {
return isActive ? ( return isActive ? (
<aside className={styles.content}> <aside className={styles.sidePeekItem}>
<header className={styles.header}> <header className={styles.header}>
<Title color="burgundy" textTransform="uppercase" level="h2" as="h3"> <Title color="burgundy" textTransform="uppercase" level="h2" as="h3">
{title} {title}
@@ -35,3 +35,5 @@ export default function Content({
</aside> </aside>
) : null ) : null
} }
export default SidePeekItem

View File

@@ -1,4 +1,4 @@
.content { .sidePeekItem {
display: grid; display: grid;
grid-template-rows: min-content auto; grid-template-rows: min-content auto;
gap: var(--Spacing-x4); gap: var(--Spacing-x4);
@@ -20,7 +20,7 @@
justify-content: space-between; justify-content: space-between;
} }
@media and screen (min-width: 1367px) { @media screen and (min-width: 1367px) {
.content>* { .content>* {
padding: var(--Spacing-x4); padding: var(--Spacing-x4);
} }

View File

@@ -1,7 +1,8 @@
"use client" "use client"
import { useIsSSR } from "@react-aria/ssr" import { useIsSSR } from "@react-aria/ssr"
import { Children, cloneElement, PropsWithChildren } from "react" import { usePathname, useRouter, useSearchParams } from "next/navigation"
import React, { Children, cloneElement, useEffect, useState } from "react"
import { import {
Dialog, Dialog,
DialogTrigger, DialogTrigger,
@@ -9,15 +10,54 @@ import {
ModalOverlay, ModalOverlay,
} from "react-aria-components" } from "react-aria-components"
import { SidePeekProps } from "./types" import { SidePeekContentKey } from "@/components/TempDesignSystem/SidePeek/types"
import styles from "./sidePeek.module.css" import styles from "./sidePeek.module.css"
export default function SidePeek({ function SidePeek({ children }: React.PropsWithChildren) {
children, // TODO: to make this more reusable,
onClose, // should the param logic be moved up/out so this component can be
activeContent, // controlled by e.g. URL segments
}: PropsWithChildren<SidePeekProps>) { const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [activeSidePeek, setActiveSidePeek] =
useState<SidePeekContentKey | null>(() => {
const sidePeekParam = searchParams.get(
"open"
) as SidePeekContentKey | null
return sidePeekParam || null
})
useEffect(() => {
const sidePeekParam = searchParams.get("open") as SidePeekContentKey | null
if (sidePeekParam !== activeSidePeek) {
setActiveSidePeek(sidePeekParam)
}
}, [searchParams, activeSidePeek])
function handleClose(isOpen: boolean) {
if (!isOpen) {
setActiveSidePeek(null)
const nextSearchParams = new URLSearchParams(searchParams.toString())
nextSearchParams.delete("open")
router.push(`${pathname}?${nextSearchParams}`, { scroll: false })
}
}
const sidePeekChildren = Children.map(children, (child) => {
if (!React.isValidElement(child)) {
return child
}
return cloneElement(child as React.ReactElement, {
isActive:
(child.props.contentKey as SidePeekContentKey) === activeSidePeek,
onClose: handleClose,
})
})
const isSSR = useIsSSR() const isSSR = useIsSSR()
return isSSR ? ( return isSSR ? (
<div>{children}</div> <div>{children}</div>
@@ -25,24 +65,16 @@ export default function SidePeek({
<DialogTrigger> <DialogTrigger>
<ModalOverlay <ModalOverlay
className={styles.overlay} className={styles.overlay}
isOpen={!!activeContent} isOpen={!!activeSidePeek}
onOpenChange={onClose} onOpenChange={handleClose}
isDismissable isDismissable
> >
<Modal className={styles.sidePeek}> <Modal className={styles.sidePeek}>
<Dialog className={styles.dialog}> <Dialog className={styles.dialog}>{sidePeekChildren}</Dialog>
{({ close }) => (
<>
{Children.map(children, (child) => {
return cloneElement(child as React.ReactElement, {
onClose: close,
})
})}
</>
)}
</Dialog>
</Modal> </Modal>
</ModalOverlay> </ModalOverlay>
</DialogTrigger> </DialogTrigger>
) )
} }
export default SidePeek