refactor: Move Sidepeek param logic to SidePeekProvider

This commit is contained in:
Arvid Norlin
2024-09-03 15:35:06 +02:00
parent 5cfbd8e70d
commit 2849c69c52
11 changed files with 227 additions and 231 deletions

View File

@@ -73,6 +73,7 @@ export default async function IntroSection({
color="burgundy"
variant="icon"
href={`?s=${about[lang]}`}
scroll={false}
>
{intl.formatMessage({ id: "Read more about the hotel" })}
<ArrowRight color="burgundy" />

View File

@@ -1,99 +0,0 @@
"use client"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import { useIntl } from "react-intl"
import {
about,
activities,
amenities,
meetingsAndConferences,
restaurantAndBar,
wellnessAndExercise,
} from "@/constants/routes/hotelPageParams"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import SidePeekItem from "@/components/TempDesignSystem/SidePeek/Item"
import { SidePeekContentKey } from "@/components/TempDesignSystem/SidePeek/types"
import useLang from "@/hooks/useLang"
function SidePeekContainer() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [activeSidePeek, setActiveSidePeek] =
useState<SidePeekContentKey | null>(() => {
const sidePeekParam = searchParams.get("s") as SidePeekContentKey | null
return sidePeekParam || null
})
const lang = useLang()
const intl = useIntl()
useEffect(() => {
const sidePeekParam = searchParams.get("s") 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("s")
router.push(`${pathname}?${nextSearchParams}`, { scroll: false })
}
}
return (
<SidePeek handleClose={handleClose} activeSidePeek={activeSidePeek}>
<SidePeekItem
contentKey={amenities[lang]}
title={intl.formatMessage({ id: "Amenities" })}
>
{/* TODO: Render amenities as per the design. */}
Read more about the amenities here
</SidePeekItem>
<SidePeekItem
contentKey={about[lang]}
title={intl.formatMessage({ id: "Read more about the hotel" })}
>
Some additional information about the hotel
</SidePeekItem>
<SidePeekItem
contentKey={restaurantAndBar[lang]}
title={intl.formatMessage({ id: "Restaurant & Bar" })}
>
{/* TODO */}
Restaurant & Bar
</SidePeekItem>
<SidePeekItem
contentKey={wellnessAndExercise[lang]}
title={intl.formatMessage({ id: "Wellness & Exercise" })}
>
{/* TODO */}
Wellness & Exercise
</SidePeekItem>
<SidePeekItem
contentKey={activities[lang]}
title={intl.formatMessage({ id: "Activities" })}
>
{/* TODO */}
Activities
</SidePeekItem>
<SidePeekItem
contentKey={meetingsAndConferences[lang]}
title={intl.formatMessage({ id: "Meetings & Conferences" })}
>
{/* TODO */}
Meetings & Conferences
</SidePeekItem>
</SidePeek>
)
}
export default SidePeekContainer

View File

@@ -1,6 +1,12 @@
import hotelPageParams from "@/constants/routes/hotelPageParams"
import { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server"
import SidePeekProvider from "@/components/SidePeekProvider"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import { MOCK_FACILITIES } from "./Facilities/mockData"
import { setActivityCard } from "./Facilities/utils"
import DynamicMap from "./Map/DynamicMap"
@@ -12,13 +18,14 @@ import Facilities from "./Facilities"
import IntroSection from "./IntroSection"
import PreviewImages from "./PreviewImages"
import { Rooms } from "./Rooms"
import SidePeeks from "./SidePeeks"
import TabNavigation from "./TabNavigation"
import styles from "./hotelPage.module.css"
export default async function HotelPage() {
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const intl = await getIntl()
const lang = getLang()
const hotelData = await serverClient().hotel.get({
include: ["RoomCategories"],
})
@@ -61,6 +68,51 @@ export default async function HotelPage() {
address={hotelAddress}
tripAdvisor={hotelRatings?.tripAdvisor}
/>
<SidePeekProvider>
{/* eslint-disable import/no-named-as-default-member */}
<SidePeek
contentKey={hotelPageParams.amenities[lang]}
title={intl.formatMessage({ id: "Amenities" })}
>
{/* TODO: Render amenities as per the design. */}
Read more about the amenities here
</SidePeek>
<SidePeek
contentKey={hotelPageParams.about[lang]}
title={intl.formatMessage({ id: "Read more about the hotel" })}
>
Some additional information about the hotel
</SidePeek>
<SidePeek
contentKey={hotelPageParams.restaurantAndBar[lang]}
title={intl.formatMessage({ id: "Restaurant & Bar" })}
>
{/* TODO */}
Restaurant & Bar
</SidePeek>
<SidePeek
contentKey={hotelPageParams.wellnessAndExercise[lang]}
title={intl.formatMessage({ id: "Wellness & Exercise" })}
>
{/* TODO */}
Wellness & Exercise
</SidePeek>
<SidePeek
contentKey={hotelPageParams.activities[lang]}
title={intl.formatMessage({ id: "Activities" })}
>
{/* TODO */}
Activities
</SidePeek>
<SidePeek
contentKey={hotelPageParams.meetingsAndConferences[lang]}
title={intl.formatMessage({ id: "Meetings & Conferences" })}
>
{/* TODO */}
Meetings & Conferences
</SidePeek>
{/* eslint-enable import/no-named-as-default-member */}
</SidePeekProvider>
<AmenitiesList detailedFacilities={hotelDetailedFacilities} />
</div>
<Rooms rooms={roomCategories} />
@@ -80,7 +132,6 @@ export default async function HotelPage() {
/>
</>
) : null}
<SidePeeks />
</div>
)
}

View File

@@ -0,0 +1,45 @@
"use client"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { createContext, useEffect, useState } from "react"
interface ISidePeekContext {
handleClose: (isOpen: boolean) => void
activeSidePeek: string | null
}
export const SidePeekContext = createContext<ISidePeekContext | null>(null)
function SidePeekProvider({ children }: React.PropsWithChildren) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const [activeSidePeek, setActiveSidePeek] = useState<string | null>(() => {
const sidePeekParam = searchParams.get("s")
return sidePeekParam || null
})
useEffect(() => {
const sidePeekParam = searchParams.get("s")
if (sidePeekParam !== activeSidePeek) {
setActiveSidePeek(sidePeekParam)
}
}, [searchParams, activeSidePeek])
function handleClose(isOpen: boolean) {
if (!isOpen) {
const nextSearchParams = new URLSearchParams(searchParams.toString())
nextSearchParams.delete("s")
router.push(`${pathname}?${nextSearchParams}`, { scroll: false })
setActiveSidePeek(null)
}
}
return (
<SidePeekContext.Provider value={{ handleClose, activeSidePeek }}>
{children}
</SidePeekContext.Provider>
)
}
export default SidePeekProvider

View File

@@ -75,7 +75,7 @@ export default function Link({
trackPageViewStart()
startTransition(() => {
startRouterTransition()
router.push(href)
router.push(href, { scroll })
})
}}
href={href}

View File

@@ -1,39 +0,0 @@
"use client"
import { PropsWithChildren } from "react"
import { CloseIcon } from "@/components/Icons"
import { SidePeekContentProps } from "@/components/TempDesignSystem/SidePeek/types"
import Title from "@/components/TempDesignSystem/Text/Title"
import Button from "../../Button"
import styles from "./sidePeekItem.module.css"
function SidePeekItem({
title,
children,
isActive = false,
onClose,
}: PropsWithChildren<SidePeekContentProps>) {
return isActive ? (
<aside className={styles.sidePeekItem}>
<header className={styles.header}>
<Title color="burgundy" textTransform="uppercase" level="h2" as="h3">
{title}
</Title>
<Button
intent="text"
onClick={() => {
onClose && onClose()
}}
>
<CloseIcon color="burgundy" height={32} width={32} />
</Button>
</header>
{children}
</aside>
) : null
}
export default SidePeekItem

View File

@@ -1,27 +0,0 @@
.sidePeekItem {
display: grid;
grid-template-rows: min-content auto;
gap: var(--Spacing-x4);
height: 100%;
}
.content>* {
padding: var(--Spacing-x3) var(--Spacing-x2);
}
.header {
display: flex;
justify-content: flex-end;
border-bottom: 1px solid var(--Base-Border-Subtle);
align-items: center;
}
.header:has(> h2) {
justify-content: space-between;
}
@media screen and (min-width: 1367px) {
.content>* {
padding: var(--Spacing-x4);
}
}

View File

@@ -1,15 +1,20 @@
"use client"
import { useIsSSR } from "@react-aria/ssr"
import React, { Children, cloneElement } from "react"
import { useContext } from "react"
import {
Dialog,
DialogTrigger,
Modal,
ModalOverlay,
} from "react-aria-components"
import { useIntl } from "react-intl"
import { SidePeekContentKey } from "@/components/TempDesignSystem/SidePeek/types"
import { CloseIcon } from "@/components/Icons"
import { SidePeekContext } from "@/components/SidePeekProvider"
import Button from "../Button"
import Title from "../Text/Title"
import styles from "./sidePeek.module.css"
@@ -17,33 +22,61 @@ import type { SidePeekProps } from "./sidePeek"
function SidePeek({
children,
title,
contentKey,
handleClose,
activeSidePeek,
isOpen,
}: React.PropsWithChildren<SidePeekProps>) {
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()
return isSSR ? (
<div>{children}</div>
) : (
const intl = useIntl()
const context = useContext(SidePeekContext)
function onClose() {
const closeHandler = handleClose || context?.handleClose
closeHandler && closeHandler(false)
}
if (isSSR) {
return (
<div>
<h2>{title}</h2>
{children}
</div>
)
}
return (
<DialogTrigger>
<ModalOverlay
className={styles.overlay}
isOpen={!!activeSidePeek}
onOpenChange={handleClose}
isOpen={isOpen || contentKey === context?.activeSidePeek}
onOpenChange={onClose}
isDismissable
>
<Modal className={styles.sidePeek}>
<Dialog className={styles.dialog}>{sidePeekChildren}</Dialog>
<Modal className={styles.modal}>
<Dialog className={styles.dialog}>
<aside className={styles.sidePeek}>
<header className={styles.header}>
{title ? (
<Title
color="burgundy"
textTransform="uppercase"
level="h2"
as="h3"
>
{title}
</Title>
) : null}
<Button
aria-label={intl.formatMessage({ id: "Close" })}
className={styles.closeButton}
intent="text"
onPress={onClose}
>
<CloseIcon color="burgundy" height={32} width={32} />
</Button>
</header>
<div className={styles.sidePeekContent}>{children}</div>
</aside>
</Dialog>
</Modal>
</ModalOverlay>
</DialogTrigger>

View File

@@ -1,38 +1,9 @@
.sidePeek {
position: fixed;
top: var(--current-mobile-site-header-height);
right: auto;
bottom: 0;
width: 100%;
height: calc(100vh - var(--current-mobile-site-header-height));
background-color: var(--Base-Background-Primary-Normal);
z-index: 100;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.85);
}
.sidePeek[data-entering] {
animation: slide-up 300ms;
}
.sidePeek[data-exiting] {
animation: slide-up 300ms reverse;
}
.dialog {
height: 100%;
}
.overlay {
position: absolute;
top: var(--current-mobile-site-header-height);
bottom: 0;
left: 0;
right: 0;
z-index: 99;
.modal {
--sidepeek-desktop-width: 600px;
}
@keyframes slide-in {
from {
right: -600px;
right: calc(-1 * var(--sidepeek-desktop-width));
}
to {
@@ -46,24 +17,84 @@
}
to {
top: var(--current-mobile-site-header-height);
top: 0;
}
}
.overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 99;
}
.modal {
position: fixed;
top: 0;
right: auto;
bottom: 0;
width: 100%;
height: 100vh;
background-color: var(--Base-Background-Primary-Normal);
z-index: 100;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.85);
}
.modal[data-entering] {
animation: slide-up 300ms;
}
.modal[data-exiting] {
animation: slide-up 300ms reverse;
}
.dialog {
height: 100%;
}
.sidePeek {
display: grid;
grid-template-rows: min-content auto;
height: 100%;
}
.header {
display: flex;
justify-content: flex-end;
border-bottom: 1px solid var(--Base-Border-Subtle);
align-items: center;
padding: var(--Spacing-x4);
}
.header:has(> h2) {
justify-content: space-between;
}
.closeButton {
padding: 0;
}
.sidePeekContent {
padding: var(--Spacing-x4);
}
@media screen and (min-width: 1367px) {
.sidePeek {
.modal {
top: 0;
right: 0px;
width: 600px;
width: var(--sidepeek-desktop-width);
height: 100vh;
}
.sidePeek[data-entering] {
.modal[data-entering] {
animation: slide-in 250ms;
}
.sidePeek[data-exiting] {
.modal[data-exiting] {
animation: slide-in 250ms reverse;
}
.overlay {
top: 0;
}

View File

@@ -1,4 +1,6 @@
export interface SidePeekProps {
handleClose: (isOpen: boolean) => void
activeSidePeek: string | null
contentKey: string
title: string
isOpen?: boolean
handleClose?: (isOpen: boolean) => void
}

View File

@@ -1,5 +1,3 @@
export type SidePeekContentKey = string
export type SidePeekProps = {
activeContent: string | null
onClose: (isOpen: boolean) => void
@@ -7,7 +5,7 @@ export type SidePeekProps = {
export type SidePeekContentProps = {
title?: string
contentKey: SidePeekContentKey
contentKey: string
isActive?: boolean
onClose?: () => void
}