fix: toggle section accordion open

This commit is contained in:
Christel Westerberg
2024-10-03 17:00:04 +02:00
parent 1bb2d3f687
commit 668eedd837
12 changed files with 245 additions and 289 deletions

View File

@@ -1,176 +0,0 @@
import { notFound } from "next/navigation"
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
import Details from "@/components/HotelReservation/EnterDetails/Details"
import HotelSelectionHeader from "@/components/HotelReservation/HotelSelectionHeader"
import Payment from "@/components/HotelReservation/SelectRate/Payment"
import SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion"
import Summary from "@/components/HotelReservation/SelectRate/Summary"
import { getIntl } from "@/i18n"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { SectionPageProps } from "@/types/components/hotelReservation/selectRate/section"
import { LangParams, PageArgs } from "@/types/params"
const bedAlternatives = [
{
value: "queen",
name: "Queen bed",
payment: "160 cm",
pricePerNight: 0,
membersPricePerNight: 0,
currency: "SEK",
},
{
value: "king",
name: "King bed",
payment: "160 cm",
pricePerNight: 0,
membersPricePerNight: 0,
currency: "SEK",
},
{
value: "twin",
name: "Twin bed",
payment: "90 cm + 90 cm",
pricePerNight: 82,
membersPricePerNight: 67,
currency: "SEK",
},
]
const breakfastAlternatives = [
{
value: "no",
name: "No breakfast",
payment: "Always cheeper to get it online",
pricePerNight: 0,
currency: "SEK",
},
{
value: "buffe",
name: "Breakfast buffé",
payment: "Always cheeper to get it online",
pricePerNight: 150,
currency: "SEK",
},
]
const getFlexibilityMessage = (value: string) => {
switch (value) {
case "non-refundable":
return "Non refundable"
case "free-rebooking":
return "Free rebooking"
case "free-cancellation":
return "Free cancellation"
}
return undefined
}
export default async function SectionsPage({
params,
searchParams,
}: PageArgs<LangParams & { section: string }, SectionPageProps>) {
setLang(params.lang)
const profile = await getProfileSafely()
const hotel = await serverClient().hotel.hotelData.get({
hotelId: "811",
language: params.lang,
})
if (!hotel) {
// TODO: handle case with hotel missing
return notFound()
}
const rooms = await serverClient().hotel.rates.get({
// TODO: pass the correct hotel ID and all other parameters that should be included in the search
hotelId: hotel.data.id,
})
const intl = await getIntl()
const selectedBed = searchParams.bed
? bedAlternatives.find((a) => a.value === searchParams.bed)?.name
: undefined
const selectedBreakfast = searchParams.breakfast
? breakfastAlternatives.find((a) => a.value === searchParams.breakfast)
?.name
: undefined
const selectedRoom = searchParams.roomClass
? rooms.find((room) => room.id.toString() === searchParams.roomClass)?.name
: undefined
const selectedFlexibility = searchParams.flexibility
? getFlexibilityMessage(searchParams.flexibility)
: undefined
const currentSearchParams = new URLSearchParams(searchParams).toString()
let user = null
if (profile && !("error" in profile)) {
user = profile
}
return (
<div>
<HotelSelectionHeader hotel={hotel.data.attributes} />
<div className={styles.content}>
<div className={styles.main}>
<SectionAccordion
header={intl.formatMessage({ id: "Room & Terms" })}
selection={
selectedRoom
? [
selectedRoom,
intl.formatMessage({ id: selectedFlexibility }),
]
: undefined
}
path={`select-rate?${currentSearchParams}`}
></SectionAccordion>
<SectionAccordion
header={intl.formatMessage({ id: "Bed type" })}
selection={selectedBed}
path={`select-bed?${currentSearchParams}`}
>
{params.section === "select-bed" ? <BedType /> : null}
</SectionAccordion>
<SectionAccordion
header={intl.formatMessage({ id: "Breakfast" })}
selection={selectedBreakfast}
path={`breakfast?${currentSearchParams}`}
>
{params.section === "breakfast" ? <Breakfast /> : null}
</SectionAccordion>
<SectionAccordion
header={intl.formatMessage({ id: "Your details" })}
path={`details?${currentSearchParams}`}
>
{params.section === "details" ? <Details user={user} /> : null}
</SectionAccordion>
<SectionAccordion
header={intl.formatMessage({ id: "Payment info" })}
path={`payment?${currentSearchParams}`}
>
{params.section === "payment" && (
<Payment hotel={hotel.data.attributes} />
)}
</SectionAccordion>
</div>
<div className={styles.summary}>
<Summary />
</div>
</div>
</div>
)
}

View File

@@ -16,10 +16,15 @@
gap: var(--Spacing-x7); gap: var(--Spacing-x7);
} }
.main { .section {
flex-grow: 1; flex-grow: 1;
} }
.summary { .summary {
max-width: 340px; max-width: 340px;
} }
.form {
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -1,58 +1,18 @@
"use client" "use client"
import { notFound } from "next/navigation" import { useState } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { notFound } from "@/server/errors/next"
import SectionAccordion from "@/components/HotelReservation/SelectRate/SectionAccordion"
import Summary from "@/components/HotelReservation/SelectRate/Summary" import Summary from "@/components/HotelReservation/SelectRate/Summary"
import Button from "@/components/TempDesignSystem/Button"
import styles from "./page.module.css" import styles from "./page.module.css"
import { LangParams, PageArgs } from "@/types/params" import { LangParams, PageArgs } from "@/types/params"
// const bedAlternatives = [
// {
// value: "queen",
// name: "Queen bed",
// payment: "160 cm",
// pricePerNight: 0,
// membersPricePerNight: 0,
// currency: "SEK",
// },
// {
// value: "king",
// name: "King bed",
// payment: "160 cm",
// pricePerNight: 0,
// membersPricePerNight: 0,
// currency: "SEK",
// },
// {
// value: "twin",
// name: "Twin bed",
// payment: "90 cm + 90 cm",
// pricePerNight: 82,
// membersPricePerNight: 67,
// currency: "SEK",
// },
// ]
// const breakfastAlternatives = [
// {
// value: "no",
// name: "No breakfast",
// payment: "Always cheeper to get it online",
// pricePerNight: 0,
// currency: "SEK",
// },
// {
// value: "buffe",
// name: "Breakfast buffé",
// payment: "Always cheeper to get it online",
// pricePerNight: 150,
// currency: "SEK",
// },
// ]
enum StepEnum { enum StepEnum {
"select-bed" = "select-bed", "select-bed" = "select-bed",
breakfast = "breakfast", breakfast = "breakfast",
@@ -68,22 +28,100 @@ function isValidStep(step: string): step is Step {
export default function StepPage({ export default function StepPage({
params, params,
}: PageArgs<LangParams & { step: string }>) { }: PageArgs<LangParams & { step: Step }>) {
const { step } = params const [activeStep, setActiveStep] = useState<Step>(params.step)
const intl = useIntl() const intl = useIntl()
if (isValidStep(step)) { if (!isValidStep(activeStep)) {
return notFound() return notFound()
} }
switch (step) { switch (activeStep) {
case StepEnum.breakfast: case StepEnum.breakfast:
return <div>Select BREAKFAST</div> //return <div>Select BREAKFAST</div>
case StepEnum.details: case StepEnum.details:
return <div>Select DETAILS</div> //return <div>Select DETAILS</div>
case StepEnum.payment: case StepEnum.payment:
return <div>Select PAYMENT</div> //return <div>Select PAYMENT</div>
case StepEnum["select-bed"]: case StepEnum["select-bed"]:
return <div>Select BED</div> // return <div>Select BED</div>
} }
function onNav(step: Step) {
setActiveStep(step)
if (typeof window !== "undefined") {
window.history.pushState({}, "", step)
}
}
return (
<main className={styles.page}>
<div className={styles.content}>
<section className={styles.section}>
<SectionAccordion
header="Select bed"
isCompleted={true}
isOpen={activeStep === StepEnum["select-bed"]}
label={intl.formatMessage({ id: "Request bedtype" })}
path="/select-bed"
>
<div className={styles.form}>
Hejhej lorem ipsim heheheheh andi fpok veoi cdfionaw aoiwube
cskdfaen
<Button onClick={() => onNav("breakfast")}>
Go to breakfast!
</Button>
</div>
</SectionAccordion>
<SectionAccordion
header="Food options"
isCompleted={true}
isOpen={activeStep === StepEnum.breakfast}
label={intl.formatMessage({ id: "Select breakfast options" })}
path="/breakfast"
>
<div className={styles.form}>
Hejhej lorem ipsim heheheheh andi fpok veoi cdfionaw aoiwube
cskdfaen
<Button onClick={() => onNav("details")}>Go to details!</Button>
</div>
</SectionAccordion>
<SectionAccordion
header="Details"
isCompleted={false}
isOpen={activeStep === StepEnum.details}
label={intl.formatMessage({ id: "Enter your details" })}
path="/details"
>
<div className={styles.form}>
Hejhej lorem ipsim heheheheh andi fpok veoi cdfionaw aoiwube
cskdfaen
<Button onClick={() => onNav("payment")}>Go to payment!</Button>
</div>
</SectionAccordion>
<SectionAccordion
header="Payment"
isCompleted={false}
isOpen={activeStep === StepEnum.payment}
label={intl.formatMessage({ id: "Select payment method" })}
path="/hotelreservation/select-bed"
>
<div className={styles.form}>
Hejhej lorem ipsim heheheheh andi fpok veoi cdfionaw aoiwube
cskdfaen Hejhej lorem ipsim heheheheh andi fpok veoi cdfionaw
aoiwube cskdfaen Hejhej lorem ipsim heheheheh andi fpok veoi
cdfionaw aoiwube cskdfaen Hejhej lorem ipsim heheheheh andi fpok
veoi cdfionaw aoiwube cskdfaen v Hejhej lorem ipsim heheheheh andi
fpok veoi cdfionaw aoiwube cskdfaen Hejhej lorem ipsim heheheheh
andi fpok veoi cdfionaw aoiwube cskdfaen
<Button onClick={() => onNav("select-bed")}>Go to beds!</Button>
</div>
</SectionAccordion>
</section>
<aside className={styles.summary}>
<Summary />
</aside>
</div>
</main>
)
} }

View File

@@ -1,3 +1,4 @@
.layout { .layout {
min-height: 100dvh; min-height: 100dvh;
background-color: var(--Base-Background-Primary-Normal);
} }

View File

@@ -1,57 +1,91 @@
import { getHotelDataSchema } from "@/server/routers/hotels/output" "use client"
import tempHotelData from "@/server/routers/hotels/tempHotelData.json" import { useEffect, useRef } from "react"
import { useIntl } from "react-intl"
import { CheckCircleIcon, ChevronDownIcon } from "@/components/Icons" import { CheckIcon, ChevronDownIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link" import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import HotelSelectionHeader from "../../HotelSelectionHeader"
import styles from "./sectionAccordion.module.css" import styles from "./sectionAccordion.module.css"
import { SectionAccordionProps } from "@/types/components/hotelReservation/selectRate/sectionAccordion" import { SectionAccordionProps } from "@/types/components/hotelReservation/selectRate/sectionAccordion"
export default async function SectionAccordion({ export default function SectionAccordion({
header, header,
selection, isOpen,
isCompleted,
label,
path, path,
children, children,
}: React.PropsWithChildren<SectionAccordionProps>) { }: React.PropsWithChildren<SectionAccordionProps>) {
const hotel = getHotelDataSchema.parse(tempHotelData) const intl = useIntl()
const intl = await getIntl() const contentRef = useRef<HTMLDivElement>(null)
const circleRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const content = contentRef.current
const circle = circleRef.current
if (content) {
if (isOpen) {
content.style.maxHeight = `${content.scrollHeight}px`
} else {
content.style.maxHeight = "0"
}
}
if (circle) {
if (isOpen) {
circle.style.backgroundColor = `var(--UI-Text-Placeholder);`
} else {
circle.style.backgroundColor = `var(--Base-Surface-Subtle-Hover);`
}
}
}, [isOpen])
return ( return (
<div className={styles.wrapper}> <section className={styles.wrapper} data-open={isOpen}>
<HotelSelectionHeader hotel={hotel.data.attributes} /> <div className={styles.iconWrapper}>
<div
<div className={styles.top}> className={styles.circle}
<div> data-checked={isCompleted}
<CheckCircleIcon color={selection ? "green" : "pale"} /> ref={circleRef}
</div> >
<div className={styles.header}> {isCompleted ? (
<Caption color={"burgundy"} asChild> <CheckIcon color="white" height="16" width="16" />
<h2>{header}</h2> ) : null}
</Caption>
{(Array.isArray(selection) ? selection : [selection]).map((s) => (
<Body key={s} className={styles.selection} color={"burgundy"}>
{s}
</Body>
))}
</div>
{selection && (
<Button intent="secondary" size="small" asChild>
<Link href={path}>{intl.formatMessage({ id: "Modify" })}</Link>
</Button>
)}
<div>
<ChevronDownIcon />
</div> </div>
</div> </div>
{children} <main className={styles.main}>
</div> <header className={styles.headerContainer}>
<div>
<Footnote
asChild
textTransform="uppercase"
color="uiTextPlaceholder"
>
<h2>{header}</h2>
</Footnote>
<Subtitle
type="two"
className={styles.selection}
color="highContrast"
>
{label}
</Subtitle>
</div>
{isCompleted && !isOpen && (
<Link href={path} color="burgundy" variant="icon">
{intl.formatMessage({ id: "Modify" })}{" "}
<ChevronDownIcon color="burgundy" />
</Link>
)}
</header>
<div className={styles.content} ref={contentRef}>
{children}
</div>
</main>
</section>
) )
} }

View File

@@ -1,21 +1,73 @@
.wrapper { .wrapper {
border-bottom: 1px solid var(--Base-Border-Normal); position: relative;
display: flex;
flex-direction: row;
gap: var(--Spacing-x3);
padding-top: var(--Spacing-x3);
} }
.top { .wrapper:not(:last-child)::after {
position: absolute;
left: 12px;
bottom: 0;
top: var(--Spacing-x5);
height: 100%;
content: "";
border-left: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
}
.main {
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
width: 100%;
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
padding-bottom: var(--Spacing-x3); padding-bottom: var(--Spacing-x3);
padding-top: var(--Spacing-x3); }
.headerContainer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: var(--Spacing-x2);
}
.header {
flex-grow: 1;
} }
.selection { .selection {
font-weight: 450; font-weight: 450;
font-size: var(--typography-Title-4-fontSize); font-size: var(--typography-Title-4-fontSize);
} }
.iconWrapper {
position: relative;
top: var(--Spacing-x1);
z-index: 10;
}
.circle {
width: 24px;
height: 24px;
border-radius: 100px;
transition: background-color 0.4s;
border: 2px solid var(--Base-Border-Inverted);
display: flex;
justify-content: center;
align-items: center;
}
.circle[data-checked="true"] {
background-color: var(--UI-Input-Controls-Fill-Selected);
}
.wrapper[data-open="true"] .circle[data-checked="false"] {
background-color: var(--UI-Text-Placeholder);
}
.wrapper[data-open="false"] .circle[data-checked="false"] {
background-color: var(--Base-Surface-Subtle-Hover);
}
.content {
overflow: hidden;
transition: max-height 0.4s ease-out;
max-height: 0;
}

View File

@@ -61,3 +61,8 @@
.uiTextMediumContrast * { .uiTextMediumContrast * {
fill: var(--UI-Text-Medium-contrast); fill: var(--UI-Text-Medium-contrast);
} }
.blue,
.blue * {
fill: var(--UI-Input-Controls-Fill-Selected);
}

View File

@@ -17,6 +17,7 @@ const config = {
white: styles.white, white: styles.white,
uiTextHighContrast: styles.uiTextHighContrast, uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast, uiTextMediumContrast: styles.uiTextMediumContrast,
blue: styles.blue,
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@@ -58,3 +58,7 @@
.pale { .pale {
color: var(--Scandic-Brand-Pale-Peach); color: var(--Scandic-Brand-Pale-Peach);
} }
.highContrast {
color: var(--UI-Text-High-contrast);
}

View File

@@ -8,6 +8,7 @@ const config = {
black: styles.black, black: styles.black,
burgundy: styles.burgundy, burgundy: styles.burgundy,
pale: styles.pale, pale: styles.pale,
highContrast: styles.highContrast,
}, },
textAlign: { textAlign: {
center: styles.center, center: styles.center,

View File

@@ -14,19 +14,8 @@ import type { MiddlewareMatcher } from "@/types/middleware"
export const middleware: NextMiddleware = async (request) => { export const middleware: NextMiddleware = async (request) => {
const { nextUrl } = request const { nextUrl } = request
const lang = findLang(nextUrl.pathname)!
const pathWithoutTrailingSlash = removeTrailingSlash(nextUrl.pathname)
const pathNameWithoutLang = pathWithoutTrailingSlash.replace(`/${lang}`, "")
const { contentType, uid } = await resolveEntry(pathNameWithoutLang, lang)
const headers = getDefaultRequestHeaders(request) const headers = getDefaultRequestHeaders(request)
if (uid) {
headers.set("x-uid", uid)
}
if (contentType) {
headers.set("x-contenttype", contentType)
}
return NextResponse.next({ return NextResponse.next({
request: { request: {
headers, headers,

View File

@@ -1,5 +1,7 @@
export interface SectionAccordionProps { export interface SectionAccordionProps {
header: string header: string
selection?: string | string[] isOpen: boolean
isCompleted: boolean
label: string
path: string path: string
} }