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);
}
.main {
.section {
flex-grow: 1;
}
.summary {
max-width: 340px;
}
.form {
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -1,58 +1,18 @@
"use client"
import { notFound } from "next/navigation"
import { useState } from "react"
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 Button from "@/components/TempDesignSystem/Button"
import styles from "./page.module.css"
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 {
"select-bed" = "select-bed",
breakfast = "breakfast",
@@ -68,22 +28,100 @@ function isValidStep(step: string): step is Step {
export default function StepPage({
params,
}: PageArgs<LangParams & { step: string }>) {
const { step } = params
}: PageArgs<LangParams & { step: Step }>) {
const [activeStep, setActiveStep] = useState<Step>(params.step)
const intl = useIntl()
if (isValidStep(step)) {
if (!isValidStep(activeStep)) {
return notFound()
}
switch (step) {
switch (activeStep) {
case StepEnum.breakfast:
return <div>Select BREAKFAST</div>
//return <div>Select BREAKFAST</div>
case StepEnum.details:
return <div>Select DETAILS</div>
//return <div>Select DETAILS</div>
case StepEnum.payment:
return <div>Select PAYMENT</div>
//return <div>Select PAYMENT</div>
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 {
min-height: 100dvh;
background-color: var(--Base-Background-Primary-Normal);
}

View File

@@ -1,57 +1,91 @@
import { getHotelDataSchema } from "@/server/routers/hotels/output"
import tempHotelData from "@/server/routers/hotels/tempHotelData.json"
"use client"
import { useEffect, useRef } from "react"
import { useIntl } from "react-intl"
import { CheckCircleIcon, ChevronDownIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import { CheckIcon, ChevronDownIcon } from "@/components/Icons"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { getIntl } from "@/i18n"
import HotelSelectionHeader from "../../HotelSelectionHeader"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import styles from "./sectionAccordion.module.css"
import { SectionAccordionProps } from "@/types/components/hotelReservation/selectRate/sectionAccordion"
export default async function SectionAccordion({
export default function SectionAccordion({
header,
selection,
isOpen,
isCompleted,
label,
path,
children,
}: 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 (
<div className={styles.wrapper}>
<HotelSelectionHeader hotel={hotel.data.attributes} />
<div className={styles.top}>
<div>
<CheckCircleIcon color={selection ? "green" : "pale"} />
</div>
<div className={styles.header}>
<Caption color={"burgundy"} asChild>
<h2>{header}</h2>
</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 />
<section className={styles.wrapper} data-open={isOpen}>
<div className={styles.iconWrapper}>
<div
className={styles.circle}
data-checked={isCompleted}
ref={circleRef}
>
{isCompleted ? (
<CheckIcon color="white" height="16" width="16" />
) : null}
</div>
</div>
{children}
</div>
<main className={styles.main}>
<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 {
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-top: var(--Spacing-x3);
}
.headerContainer {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--Spacing-x2);
}
.header {
flex-grow: 1;
}
.selection {
font-weight: 450;
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 * {
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,
uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast,
blue: styles.blue,
},
},
defaultVariants: {

View File

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

View File

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

View File

@@ -14,19 +14,8 @@ import type { MiddlewareMatcher } from "@/types/middleware"
export const middleware: NextMiddleware = async (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)
if (uid) {
headers.set("x-uid", uid)
}
if (contentType) {
headers.set("x-contenttype", contentType)
}
return NextResponse.next({
request: {
headers,

View File

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