Merged in feature/select-room-ux-one-page (pull request #523)

This updates the select room page according to the new UX. It has different sections on the same page, but with specific URLs per section. Since neither UX, UI nor API is completely done both design and data structures are a bit temporary.

Approved-by: Simon.Emanuelsson
This commit is contained in:
Niclas Edenvin
2024-08-29 13:38:14 +00:00
parent 00fc2af3dd
commit f178f7fde0
35 changed files with 794 additions and 372 deletions

View File

@@ -1,70 +1,54 @@
import Header from "@/components/Section/Header"
import { getIntl } from "@/i18n"
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import SelectionCard from "../SelectionCard"
import styles from "./bedSelection.module.css"
const choices = [
{
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",
},
]
import { BedSelectionProps } from "@/types/components/hotelReservation/selectRate/section"
export default async function BedSelection() {
const { formatMessage } = await getIntl()
export default function BedSelection({
alternatives,
nextPath,
}: BedSelectionProps) {
const router = useRouter()
const searchParams = useSearchParams()
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const queryParams = new URLSearchParams(searchParams)
queryParams.set("bed", e.currentTarget.bed?.value)
router.push(`${nextPath}?${queryParams}`)
}
return (
<div className={styles.wrapper}>
<div className={styles.header}>
<Header
title={formatMessage({ id: "Choose type of bed" })}
subtitle={formatMessage({ id: "How do you want to sleep?" })}
/>
<p>
{formatMessage({
id: "All our beds are from Bliss, allowing you to adjust the firmness for your perfect comfort.",
})}
</p>
</div>
<form
method="GET"
action={`${nextPath}?${searchParams}`}
onSubmit={handleSubmit}
>
<ul className={styles.list}>
{alternatives.map((alternative) => (
<li key={alternative.value}>
<label>
<input type="radio" name="bed" value={alternative.value} />
<SelectionCard
title={alternative.name}
subtext={`(${alternative.payment})`}
price={alternative.pricePerNight}
membersPrice={alternative.membersPricePerNight}
currency={alternative.currency}
/>
</label>
</li>
))}
</ul>
<ul className={styles.list}>
{choices.map((choice) => (
<li key={choice.value}>
<label>
<input type="radio" name="bed" value={choice.value} />
<SelectionCard
title={choice.name}
subtext={`(${choice.payment})`}
price={choice.pricePerNight}
membersPrice={choice.membersPricePerNight}
currency={choice.currency}
/>
</label>
</li>
))}
</ul>
<button type="submit" hidden>
Submit
</button>
</form>
</div>
)
}

View File

@@ -1,56 +1,57 @@
import Header from "@/components/Section/Header"
import { getIntl } from "@/i18n"
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import SelectionCard from "../SelectionCard"
import styles from "./breakfastSelection.module.css"
const choices = [
{
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",
},
]
import { BreakfastSelectionProps } from "@/types/components/hotelReservation/selectRate/section"
export default async function BreakfastSelection() {
const { formatMessage } = await getIntl()
export default function BreakfastSelection({
alternatives,
nextPath,
}: BreakfastSelectionProps) {
const router = useRouter()
const searchParams = useSearchParams()
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const queryParams = new URLSearchParams(searchParams)
queryParams.set("breakfast", e.currentTarget.breakfast?.value)
router.push(`${nextPath}?${queryParams}`)
}
return (
<div className={styles.wrapper}>
<div className={styles.header}>
<Header
title={formatMessage({ id: "Breakfast" })}
subtitle={formatMessage({
id: "Do you want to start the day with Scandics famous breakfast buffé?",
})}
/>
</div>
<form
method="GET"
action={`${nextPath}?${searchParams}`}
onSubmit={handleSubmit}
>
<ul className={styles.list}>
{alternatives.map((alternative) => (
<li key={alternative.value}>
<label>
<input
type="radio"
name="breakfast"
value={alternative.value}
/>
<SelectionCard
title={alternative.name}
subtext={alternative.payment}
price={alternative.pricePerNight}
currency={alternative.currency}
/>
</label>
</li>
))}
</ul>
<ul className={styles.list}>
{choices.map((choice) => (
<li key={choice.value}>
<label>
<input type="radio" name="breakfast" value={choice.value} />
<SelectionCard
title={choice.name}
subtext={choice.payment}
price={choice.pricePerNight}
currency={choice.currency}
/>
</label>
</li>
))}
</ul>
<button type="submit" hidden>
Submit
</button>
</form>
</div>
)
}

View File

@@ -0,0 +1,2 @@
.wrapper {
}

View File

@@ -0,0 +1,6 @@
"use client"
import styles from "./details.module.css"
export default function Details() {
return <div className={styles.wrapper}>Details TBI</div>
}

View File

@@ -1,28 +0,0 @@
.wrapper {
border-bottom: 1px solid rgba(17, 17, 17, 0.2);
padding-bottom: var(--Spacing-x3);
}
.header {
margin-top: var(--Spacing-x2);
margin-bottom: var(--Spacing-x2);
}
.list {
margin-top: var(--Spacing-x4);
list-style: none;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
column-gap: var(--Spacing-x2);
row-gap: var(--Spacing-x4);
}
.list > li {
width: 100%;
}
.list input[type="radio"] {
opacity: 0;
position: fixed;
width: 0;
}

View File

@@ -1,62 +0,0 @@
import Header from "@/components/Section/Header"
import { getIntl } from "@/i18n"
import SelectionCard from "../SelectionCard"
import styles from "./flexibilitySelection.module.css"
const choices = [
{
value: "non-refundable",
name: "Non refundable",
payment: "Pay now",
pricePerNight: 0,
membersPricePerNight: 0,
currency: "SEK",
},
{
value: "rebook",
name: "Free rebooking",
payment: "Pay now",
pricePerNight: 77,
membersPricePerNight: 20,
currency: "SEK",
},
{
value: "cancellation",
name: "Free cancellation",
payment: "Pay later",
pricePerNight: 132,
membersPricePerNight: 80,
currency: "SEK",
},
]
export default async function FlexibilitySelection() {
const { formatMessage } = await getIntl()
return (
<div className={styles.wrapper}>
<div className={styles.header}>
<Header title={formatMessage({ id: "Flexibility" })} subtitle={null} />
</div>
<ul className={styles.list}>
{choices.map((choice) => (
<li key={choice.value}>
<label>
<input type="radio" name="flexibility" value={choice.value} />
<SelectionCard
title={choice.name}
subtext={choice.payment}
price={choice.pricePerNight}
membersPrice={choice.membersPricePerNight}
currency={choice.currency}
/>
</label>
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,6 @@
"use client"
import styles from "./payment.module.css"
export default function Payment() {
return <div className={styles.wrapper}>Payment TBI</div>
}

View File

@@ -0,0 +1,2 @@
.wrapper {
}

View File

@@ -0,0 +1,15 @@
.card {
font-size: 14px;
border-radius: var(--Corner-radius-Medium);
border: 1px solid var(--Base-Border-Normal);
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
}
input[type="radio"]:checked + .card {
background-color: var(--Base-Surface-Primary-light-Hover-alt);
}
.header {
display: flex;
justify-content: space-between;
}

View File

@@ -0,0 +1,45 @@
"use client"
import { useIntl } from "react-intl"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import styles from "./flexibilityOption.module.css"
import { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
export default function FlexibilityOption({
currency,
standardPrice,
memberPrice,
name,
value,
paymentTerm,
}: FlexibilityOptionProps) {
const intl = useIntl()
return (
<label>
<input type="radio" name="flexibility" value={value} />
<div className={styles.card}>
<div className={styles.header}>
<Body>{name}</Body>
<Caption>{paymentTerm}</Caption>
</div>
<dl>
<div>
<dt>{intl.formatMessage({ id: "Standard price" })}</dt>
<dd>
{standardPrice} {currency}
</dd>
</div>
<div>
<dt>{intl.formatMessage({ id: "Member price" })}</dt>
<dd>
{memberPrice} {currency}
</dd>
</div>
</dl>
</div>
</label>
)
}

View File

@@ -1,49 +1,92 @@
"use client"
import { useIntl } from "react-intl"
import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import styles from "./roomCard.module.css"
import { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
export default async function RoomCard({ room }: RoomCardProps) {
const { formatMessage } = await getIntl()
export default function RoomCard({
room,
nrOfAdults,
nrOfNights,
breakfastIncluded,
}: RoomCardProps) {
const intl = useIntl()
return (
<div className={styles.card}>
<div className={styles.cardBody}>
<div>
<Title className={styles.name} as="h5" level="h3">
<div className={styles.specification}>
<Subtitle className={styles.name} type="two">
{room.name}
</Title>
<div className={styles.nameInfo}>i</div>
</Subtitle>
<Caption>{room.size}</Caption>
<Button intent="text" type="button" size="small" theme="base">
{intl.formatMessage({ id: "See room details" })}
</Button>
<Caption>
{/*TODO: Handle pluralisation*/}
{intl.formatMessage(
{
id: "Nr night, nr adult",
defaultMessage:
"{nights, number} night, {adults, number} adult",
},
{ nights: nrOfNights, adults: nrOfAdults }
)}
{" | "}
{breakfastIncluded
? intl.formatMessage({
id: "Breakfast included",
})
: intl.formatMessage({
id: "Breakfast excluded",
})}
</Caption>
</div>
<Caption color="burgundy">{room.size}</Caption>
<Caption color="burgundy">{room.description}</Caption>
<Caption color="burgundy">
{/* TODO: Handle currency and this whole line of text in a better way through intl */}
{formatMessage({ id: "From" })}{" "}
<span className={styles.price}>{room.pricePerNight}</span>{" "}
{room.currency}/{formatMessage({ id: "night" })}
</Caption>
<FlexibilityOption
name={intl.formatMessage({ id: "Non-refundable" })}
value="non-refundable"
paymentTerm={intl.formatMessage({ id: "Pay now" })}
standardPrice={room.prices.nonRefundable.standard}
memberPrice={room.prices.nonRefundable.member}
currency={room.prices.currency}
/>
<FlexibilityOption
name={intl.formatMessage({ id: "Free rebooking" })}
value="free-rebooking"
paymentTerm={intl.formatMessage({ id: "Pay now" })}
standardPrice={room.prices.freeRebooking.standard}
memberPrice={room.prices.freeRebooking.member}
currency={room.prices.currency}
/>
<FlexibilityOption
name={intl.formatMessage({ id: "Free cancellation" })}
value="free-cancellation"
paymentTerm={intl.formatMessage({ id: "Pay later" })}
standardPrice={room.prices.freeCancellation.standard}
memberPrice={room.prices.freeCancellation.member}
currency={room.prices.currency}
/>
<Button
asChild
type="button"
type="submit"
size="small"
theme="primaryDark"
className={styles.button}
>
<label htmlFor={`room-${room.id}`}>
{formatMessage({ id: "Choose room" })}
</label>
{intl.formatMessage({ id: "Choose room" })}
</Button>
</div>
{/* TODO: maybe use the `Image` component instead of the `img` tag. Waiting until we know how to get the image */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt={formatMessage({ id: "A photo of the room" })}
alt={intl.formatMessage({ id: "A photo of the room" })}
src={room.imageSrc}
/>
</div>

View File

@@ -1,6 +1,5 @@
.card {
font-size: 14px;
text-align: center;
display: flex;
flex-direction: column-reverse;
background-color: #fff;
@@ -8,12 +7,15 @@
border: 1px solid rgba(77, 0, 27, 0.1);
}
input[type="radio"]:checked + .card {
border: 3px solid var(--Scandic-Brand-Scandic-Red);
.cardBody {
padding: var(--Spacing-x1);
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
}
.cardBody {
padding: var(--Spacing-x2);
.specification {
padding: var(--Spacing-x1);
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
@@ -22,15 +24,6 @@ input[type="radio"]:checked + .card {
.name {
display: inline-block;
}
.nameInfo {
float: right;
}
.price {
font-size: 24px;
font-weight: 600;
text-align: center;
}
.card .button {
display: inline;
@@ -38,6 +31,6 @@ input[type="radio"]:checked + .card {
.card img {
max-width: 100%;
aspect-ratio: 2.45;
aspect-ratio: 1.5;
object-fit: cover;
}

View File

@@ -1,42 +1,52 @@
import Header from "@/components/Section/Header"
import { getIntl } from "@/i18n"
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import RoomCard from "./RoomCard"
import styles from "./roomSelection.module.css"
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/section"
export default async function RoomSelection({ rooms }: RoomSelectionProps) {
const { formatMessage } = await getIntl()
export default function RoomSelection({
alternatives,
nextPath,
nrOfNights,
nrOfAdults,
}: RoomSelectionProps) {
const router = useRouter()
const searchParams = useSearchParams()
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const queryParams = new URLSearchParams(searchParams)
queryParams.set("roomClass", e.currentTarget.roomClass?.value)
queryParams.set("flexibility", e.currentTarget.flexibility?.value)
router.push(`${nextPath}?${queryParams}`)
}
return (
<div className={styles.wrapper}>
<div className={styles.header}>
<Header
title={formatMessage({ id: "Choose room" })}
subtitle={formatMessage({
id: "Which room class suits you the best?",
})}
link={{
href: "#",
text: formatMessage({
id: "All rooms comes with standard amenities",
}),
}}
/>
</div>
<ul className={styles.roomList}>
{rooms.map((room) => (
{alternatives.map((room) => (
<li key={room.id}>
<input
type="radio"
name="room"
value={room.id}
id={`room-${room.id}`}
/>
<RoomCard room={room} />
<form
method="GET"
action={`${nextPath}?${searchParams}`}
onSubmit={handleSubmit}
>
<input
type="hidden"
name="roomClass"
value={room.id}
id={`room-${room.id}`}
/>
<RoomCard
room={room}
nrOfAdults={nrOfAdults}
nrOfNights={nrOfNights}
breakfastIncluded={room.breakfastIncluded}
/>
</form>
</li>
))}
</ul>

View File

@@ -2,16 +2,12 @@
border-bottom: 1px solid rgba(17, 17, 17, 0.2);
padding-bottom: var(--Spacing-x3);
}
.header {
margin-top: var(--Spacing-x2);
margin-bottom: var(--Spacing-x2);
}
.roomList {
margin-top: var(--Spacing-x4);
list-style: none;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-columns: 1fr 1fr 1fr 1fr;
column-gap: var(--Spacing-x2);
row-gap: var(--Spacing-x4);
}

View File

@@ -0,0 +1,48 @@
import { CheckCircleIcon, ChevronDownIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
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 styles from "./sectionAccordion.module.css"
import { SectionAccordionProps } from "@/types/components/hotelReservation/selectRate/sectionAccordion"
export default async function SectionAccordion({
header,
selection,
path,
children,
}: React.PropsWithChildren<SectionAccordionProps>) {
const intl = await getIntl()
return (
<div className={styles.wrapper}>
<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 />
</div>
</div>
{children}
</div>
)
}

View File

@@ -0,0 +1,21 @@
.wrapper {
border-bottom: 1px solid var(--Base-Border-Normal);
}
.top {
padding-bottom: var(--Spacing-x3);
padding-top: var(--Spacing-x3);
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);
}

View File

@@ -1,19 +1,21 @@
"use client"
import { useIntl } from "react-intl"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import styles from "./selectionCard.module.css"
import { SelectionCardProps } from "@/types/components/hotelReservation/selectRate/selectionCard"
export default async function SelectionCard({
export default function SelectionCard({
price,
membersPrice,
currency,
title,
subtext,
}: SelectionCardProps) {
const { formatMessage } = await getIntl()
const intl = useIntl()
return (
<div className={styles.card}>
@@ -28,13 +30,13 @@ export default async function SelectionCard({
<div>
<Caption color="burgundy" className={styles.price}>
{/* TODO: Handle currency and this whole line of text in a better way through intl */}
{price} {currency}/{formatMessage({ id: "night" })}
{price} {currency}/{intl.formatMessage({ id: "night" })}
</Caption>
<Caption color="burgundy" className={styles.membersPrice}>
{/* TODO: Handle currency and this whole line of text in a better way through intl */}
{formatMessage({ id: "Members" })} {membersPrice} {currency}/
{formatMessage({ id: "night" })}
{intl.formatMessage({ id: "Members" })} {membersPrice} {currency}/
{intl.formatMessage({ id: "night" })}
</Caption>
</div>
</div>

View File

@@ -0,0 +1,6 @@
"use client"
import styles from "./summary.module.css"
export default function Summary() {
return <div className={styles.wrapper}>Summary TBI</div>
}

View File

@@ -0,0 +1,2 @@
.wrapper {
}