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:
@@ -1,3 +1,3 @@
|
|||||||
.layout {
|
.layout {
|
||||||
height: 100vh;
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,5 @@ import { LangParams, LayoutArgs } from "@/types/params"
|
|||||||
export default function HotelReservationLayout({
|
export default function HotelReservationLayout({
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
||||||
return (
|
return <div className={styles.layout}>{children}</div>
|
||||||
<div className={styles.layout}>
|
|
||||||
<Title as="h1" color="black">
|
|
||||||
Lorem, ipsum.
|
|
||||||
</Title>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
}
|
||||||
114
app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx
Normal file
114
app/[lang]/(live)/(public)/hotelreservation/select-rate/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
42
components/HotelReservation/SelectRate/RoomCard/index.tsx
Normal file
42
components/HotelReservation/SelectRate/RoomCard/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
11
components/HotelReservation/SelectRate/RoomCard/roomCard.ts
Normal file
11
components/HotelReservation/SelectRate/RoomCard/roomCard.ts
Normal 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 }
|
||||||
@@ -4,10 +4,17 @@ import { headingVariants } from "./variants"
|
|||||||
import type { HeadingProps } from "./title"
|
import type { HeadingProps } from "./title"
|
||||||
|
|
||||||
export default function Title({
|
export default function Title({
|
||||||
|
/**
|
||||||
|
* What styling to use, based on heading level. If not provided `level`
|
||||||
|
* will determine the styling.
|
||||||
|
*/
|
||||||
as,
|
as,
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
color,
|
color,
|
||||||
|
/**
|
||||||
|
* What HTML tag to use. Defaults to h1.
|
||||||
|
*/
|
||||||
level = "h1",
|
level = "h1",
|
||||||
textAlign,
|
textAlign,
|
||||||
textTransform,
|
textTransform,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from "next/server"
|
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"
|
import type { NextMiddleware } from "next/server"
|
||||||
|
|
||||||
@@ -11,5 +13,6 @@ export const middleware: NextMiddleware = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const matcher: MiddlewareMatcher = (request) => {
|
export const matcher: MiddlewareMatcher = (request) => {
|
||||||
return bookingFlow.includes(request.nextUrl.pathname)
|
const lang = findLang(request.nextUrl.pathname)!
|
||||||
|
return request.nextUrl.pathname.startsWith(hotelReservation[lang])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user