feat(SW-70): create base for rate selection page

This is the foundation for the rate selection. Since we don't have UX
and UI ready yet this is on a best effort basis. Things that will be
changed later includes proper API fetching, correct design,
internationalization of text and form handling.
This commit is contained in:
Niclas Edenvin
2024-07-08 11:06:58 +02:00
parent bb422f804d
commit d6fe6a33b4
9 changed files with 264 additions and 11 deletions

View File

@@ -1,3 +1,3 @@
.layout {
height: 100vh;
min-height: 100dvh;
}

View File

@@ -7,12 +7,5 @@ import { LangParams, LayoutArgs } from "@/types/params"
export default function HotelReservationLayout({
children,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
return (
<div className={styles.layout}>
<Title as="h1" color="black">
Lorem, ipsum.
</Title>
{children}
</div>
)
return <div className={styles.layout}>{children}</div>
}

View File

@@ -0,0 +1,41 @@
.header {
margin-top: var(--Spacing-x2);
margin-bottom: var(--Spacing-x2);
}
.hotelInfo {
margin-bottom: 64px;
}
.page {
min-height: 100dvh;
padding-top: var(--Spacing-x6);
padding-left: var(--Spacing-x2);
padding-right: var(--Spacing-x2);
background-color: var(--Scandic-Brand-Warm-White);
}
.content {
max-width: 1134px;
margin-left: auto;
margin-right: auto;
}
.page input[type="radio"] {
opacity: 0;
position: fixed;
width: 0;
}
.roomList {
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);
}
.roomList > li {
width: 100%;
}

View File

@@ -0,0 +1,114 @@
import RoomCard from "@/components/HotelReservation/SelectRate/RoomCard"
import { Room } from "@/components/HotelReservation/SelectRate/RoomCard/roomCard"
import Header from "@/components/Section/Header"
import styles from "./page.module.css"
const getRooms: () => Promise<Room[]> = () => {
return new Promise((resolve) =>
resolve([
{
id: 1,
name: "Cabin",
description:
"Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
size: "17 - 24 m² (1 - 2 persons)",
pricePerNight: 1348,
currency: "SEK",
imageSrc:
"https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
},
{
id: 2,
name: "Standard",
description:
"Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
size: "19 - 30 m² (1 - 2 persons)",
pricePerNight: 1548,
currency: "SEK",
imageSrc:
"https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
},
{
id: 3,
name: "Superior",
description:
"Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
size: "22 - 40 m² (1 - 3 persons)",
pricePerNight: 1744,
currency: "SEK",
imageSrc:
"https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
},
{
id: 4,
name: "Superior Family",
description:
"Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
size: "29 - 49 m² (3 - 4 persons)",
pricePerNight: 2032,
currency: "SEK",
imageSrc:
"https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
},
{
id: 5,
name: "Superior PLUS",
description:
"Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
size: "21 - 28 m² (2 - 3 persons)",
pricePerNight: 2065,
currency: "SEK",
imageSrc:
"https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
},
{
id: 6,
name: "Junior Suite",
description:
"Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
size: "35 - 43 m² (2 - 4 persons)",
pricePerNight: 3012,
currency: "SEK",
imageSrc:
"https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
},
])
)
}
export default async function SelectRate() {
const rooms = await getRooms()
return (
<div className={styles.page}>
<main className={styles.content}>
<div className={styles.hotelInfo}>Hotel info TBI</div>
<div className={styles.header}>
<Header
title="Choose room"
subtitle={"Which room class suits you the best?"}
link={{
href: "#",
text: "All rooms comes with standard amenities",
}}
/>
</div>
<ul className={styles.roomList}>
{rooms.map((room) => (
<li key={room.id}>
<input
type="radio"
name="room"
value={room.id}
id={`room-${room.id}`}
/>
<RoomCard room={room} />
</li>
))}
</ul>
</main>
</div>
)
}

View File

@@ -0,0 +1,42 @@
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Title from "@/components/TempDesignSystem/Text/Title"
import { RoomProps } from "./roomCard"
import styles from "./roomCard.module.css"
export default function RoomCard({ room }: RoomProps) {
return (
<div className={styles.card}>
<div className={styles.cardBody}>
<div className={styles.nameContainer}>
<Title className={styles.name} as="h5" level="h3">
{room.name}
</Title>
<div className={styles.nameInfo}>i</div>
</div>
<Caption color="burgundy">17 - 24 m² (1 - 2 persons)</Caption>
<Caption color="burgundy">{room.description}</Caption>
<Caption color="burgundy">
From <span className={styles.price}>{room.pricePerNight}</span>{" "}
{room.currency}/night
</Caption>
<Button
asChild
type="button"
size="small"
theme="primaryDark"
className={styles.button}
>
<label htmlFor={`room-${room.id}`}>Choose room</label>
</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="A photo of the room" src={room.imageSrc} />
</div>
)
}

View File

@@ -0,0 +1,42 @@
.card {
font-size: 14px;
text-align: center;
display: flex;
flex-direction: column-reverse;
background-color: #fff;
border-radius: 4px;
border: 1px solid rgba(77, 0, 27, 0.1);
}
.cardBody {
padding: var(--Spacing-x2);
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
}
.nameContainer {
}
.name {
display: inline-block;
}
.nameInfo {
float: right;
}
.price {
font-size: 24px;
font-weight: 600;
text-align: center;
}
.card .button {
display: inline;
}
.card img {
max-width: 100%;
aspect-ratio: 2.45;
object-fit: cover;
}

View File

@@ -0,0 +1,11 @@
export type Room = {
id: number
name: string
description: string
size: string
pricePerNight: number
currency: string
imageSrc: string
}
export type RoomProps = { room: Room }

View File

@@ -4,10 +4,17 @@ import { headingVariants } from "./variants"
import type { HeadingProps } from "./title"
export default function Title({
/**
* What styling to use, based on heading level. If not provided `level`
* will determine the styling.
*/
as,
children,
className = "",
color,
/**
* What HTML tag to use. Defaults to h1.
*/
level = "h1",
textAlign,
textTransform,

View File

@@ -1,6 +1,8 @@
import { NextResponse } from "next/server"
import { bookingFlow } from "@/constants/routes/hotelReservation"
import { hotelReservation } from "@/constants/routes/hotelReservation"
import { findLang } from "@/utils/languages"
import type { NextMiddleware } from "next/server"
@@ -11,5 +13,6 @@ export const middleware: NextMiddleware = () => {
}
export const matcher: MiddlewareMatcher = (request) => {
return bookingFlow.includes(request.nextUrl.pathname)
const lang = findLang(request.nextUrl.pathname)!
return request.nextUrl.pathname.startsWith(hotelReservation[lang])
}