Merged in feat/SW-1276-implement-design (pull request #1348)
Feat/SW-1276 implement design * feat(SW-1276) UI implementation Desktop part 1 for MyStay * feat(SW-1276) UI implementation Desktop part 2 for MyStay * feat(SW-1276) UI implementation Mobile part 1 for MyStay * refactor: move files from MyStay/MyStay to MyStay * feat(SW-1276) Sidepeek implementation * feat(SW-1276): Refactoring * feat(SW-1276) UI implementation Mobile part 2 for MyStay * feat(SW-1276): translations * feat(SW-1276) fixed skeleton * feat(SW-1276): Added missing translations * feat(SW-1276): Removed console log * feat(SW-1276) fixed translations * feat(SW-1276): Added translations * feat(SW-1276) fix dynamic ID:s * feat(SW-1276) removed createElement * feat(SW-1276): Fixed build errors * feat(SW-1276): Updated label * feat(SW-1276): Rewrite SummaryCard Approved-by: Niclas Edenvin
This commit is contained in:
@@ -11,7 +11,10 @@ import styles from "./header.module.css"
|
||||
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
||||
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
|
||||
|
||||
export default function ToggleSidePeek({ hotelId }: ToggleSidePeekProps) {
|
||||
export default function ToggleSidePeek({
|
||||
hotelId,
|
||||
intent = "textInverted",
|
||||
}: ToggleSidePeekProps) {
|
||||
const intl = useIntl()
|
||||
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
|
||||
|
||||
@@ -21,7 +24,7 @@ export default function ToggleSidePeek({ hotelId }: ToggleSidePeekProps) {
|
||||
theme="base"
|
||||
size="small"
|
||||
variant="icon"
|
||||
intent="textInverted"
|
||||
intent={intent}
|
||||
wrapping
|
||||
className={styles.toggle}
|
||||
>
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/to
|
||||
export default function ToggleSidePeek({
|
||||
hotelId,
|
||||
roomTypeCode,
|
||||
intent = "textInverted",
|
||||
}: ToggleSidePeekProps) {
|
||||
const intl = useIntl()
|
||||
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
|
||||
@@ -25,7 +26,7 @@ export default function ToggleSidePeek({
|
||||
theme="base"
|
||||
size="small"
|
||||
variant="icon"
|
||||
intent="text"
|
||||
intent={intent}
|
||||
wrapping
|
||||
>
|
||||
{intl.formatMessage({ id: "See room details" })}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import Image from "@/components/Image"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./summaryCard.module.css"
|
||||
|
||||
interface SummaryCardProps {
|
||||
title: string
|
||||
image: {
|
||||
src: string
|
||||
alt: string
|
||||
}
|
||||
texts: string[]
|
||||
supportingText?: string
|
||||
links?: {
|
||||
href: string
|
||||
text: string
|
||||
icon: React.ReactNode
|
||||
}[]
|
||||
chip?: React.ReactNode
|
||||
}
|
||||
|
||||
export default function SummaryCard({
|
||||
title,
|
||||
texts,
|
||||
image,
|
||||
supportingText,
|
||||
links,
|
||||
chip,
|
||||
}: SummaryCardProps) {
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<div className={styles.image}>
|
||||
<Image src={image.src} alt={image.alt} width={152} height={152} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.topContent}>
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{title}
|
||||
</Body>
|
||||
{texts.map((text) => (
|
||||
<Body color="uiTextHighContrast" key={text}>
|
||||
{text}
|
||||
</Body>
|
||||
))}
|
||||
</div>
|
||||
{supportingText && (
|
||||
<Caption color="uiTextPlaceholder">{supportingText}</Caption>
|
||||
)}
|
||||
<div className={styles.bottomContent}>
|
||||
{chip}
|
||||
{links && (
|
||||
<div className={styles.links}>
|
||||
{links.map((link) => (
|
||||
<Caption asChild type="bold" color="burgundy" key={link.href}>
|
||||
<Link
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
color="burgundy"
|
||||
className={styles.link}
|
||||
>
|
||||
{link.icon}
|
||||
{link.text}
|
||||
</Link>
|
||||
</Caption>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.card {
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 152px;
|
||||
height: 152px;
|
||||
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.image {
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.topContent {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bottomContent {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
.bookingSummary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x5);
|
||||
}
|
||||
|
||||
.bookingSummaryContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bookingSummaryContent {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
width: var(--max-width-content);
|
||||
margin: 0 auto;
|
||||
}
|
||||
130
components/HotelReservation/MyStay/BookingSummary/index.tsx
Normal file
130
components/HotelReservation/MyStay/BookingSummary/index.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
DirectionsIcon,
|
||||
EmailIcon,
|
||||
LinkIcon,
|
||||
} from "@/components/Icons"
|
||||
import CrossCircleIcon from "@/components/Icons/CrossCircle"
|
||||
import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { Toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import SummaryCard from "./SummaryCard"
|
||||
|
||||
import styles from "./bookingSummary.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
interface BookingSummaryProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
}
|
||||
|
||||
export default async function BookingSummary({
|
||||
booking,
|
||||
hotel,
|
||||
}: BookingSummaryProps) {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
|
||||
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
|
||||
const isPaid =
|
||||
booking.rateDefinition.cancellationRule !== "CancellableBefore6PM"
|
||||
const bookingDate = dt(booking.createDateTime)
|
||||
.locale(lang)
|
||||
.format("D MMMM YYYY")
|
||||
|
||||
return (
|
||||
<div className={styles.bookingSummary}>
|
||||
<Subtitle textTransform="uppercase" color="burgundy">
|
||||
{intl.formatMessage({ id: "Booking summary" })}
|
||||
</Subtitle>
|
||||
<div className={styles.bookingSummaryContent}>
|
||||
<SummaryCard
|
||||
title={formatPrice(intl, booking.totalPrice, booking.currencyCode)}
|
||||
image={{
|
||||
src: "/_static/img/scandic-coin.svg",
|
||||
alt: "Scandic coin",
|
||||
}}
|
||||
texts={[`${intl.formatMessage({ id: "Payment" })}: N/A`]}
|
||||
supportingText={bookingDate}
|
||||
chip={
|
||||
<IconChip
|
||||
color={isPaid ? "green" : "red"}
|
||||
icon={
|
||||
isPaid ? (
|
||||
<CheckCircleIcon width={20} height={20} color="green" />
|
||||
) : (
|
||||
<CrossCircleIcon width={20} height={20} color="red" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<Caption color={isPaid ? "green" : "red"}>
|
||||
<strong>{intl.formatMessage({ id: "Status" })}:</strong>{" "}
|
||||
{isPaid
|
||||
? intl.formatMessage({ id: "Paid" })
|
||||
: intl.formatMessage({ id: "Unpaid" })}
|
||||
</Caption>
|
||||
</IconChip>
|
||||
}
|
||||
/>
|
||||
<SummaryCard
|
||||
title={hotel.name}
|
||||
image={{
|
||||
src: "/_static/img/scandic-service.svg",
|
||||
alt: "Scandic service",
|
||||
}}
|
||||
texts={[
|
||||
hotel.address.streetAddress,
|
||||
`${hotel.address.zipCode} ${hotel.address.city}`,
|
||||
]}
|
||||
supportingText={intl.formatMessage(
|
||||
{ id: "Long {long} ∙ Lat {lat}" },
|
||||
{
|
||||
lat: hotel.location.latitude,
|
||||
long: hotel.location.longitude,
|
||||
}
|
||||
)}
|
||||
links={[
|
||||
{
|
||||
href: directionsUrl,
|
||||
text: intl.formatMessage({ id: "Directions" }),
|
||||
icon: <DirectionsIcon width={20} height={20} color="burgundy" />,
|
||||
},
|
||||
{
|
||||
href: `mailto:${hotel.contactInformation.email}`,
|
||||
text: intl.formatMessage({ id: "Email" }),
|
||||
icon: <EmailIcon width={20} height={20} color="burgundy" />,
|
||||
},
|
||||
{
|
||||
href: hotel.contactInformation.websiteUrl,
|
||||
text: intl.formatMessage({ id: "Homepage" }),
|
||||
icon: <LinkIcon width={20} height={20} color="burgundy" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{hotel.specialAlerts.length > 0 && (
|
||||
<div className={styles.toast}>
|
||||
<Toast variant="info">
|
||||
<ul className={styles.list}>
|
||||
{hotel.specialAlerts.map((alert) => (
|
||||
<li key={alert.id}>
|
||||
<Body color="uiTextHighContrast">{alert.text}</Body>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Toast>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
28
components/HotelReservation/MyStay/Header/header.module.css
Normal file
28
components/HotelReservation/MyStay/Header/header.module.css
Normal file
@@ -0,0 +1,28 @@
|
||||
header .title {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
padding-top: var(--Spacing-x6);
|
||||
margin-top: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.title .hotelName {
|
||||
font-family: var(--typography-Title-3-fontFamily);
|
||||
font-size: var(--typography-Title-3-fontSize);
|
||||
font-weight: var(--typography-Title-3-fontWeight);
|
||||
letter-spacing: var(--typography-Title-3-letterSpacing);
|
||||
line-height: var(--typography-Title-3-lineHeight);
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.title .hotelName {
|
||||
font-family: var(--typography-Title-1-fontFamily);
|
||||
font-size: var(--typography-Title-1-fontSize);
|
||||
font-weight: var(--typography-Title-1-fontWeight);
|
||||
letter-spacing: var(--typography-Title-1-letterSpacing);
|
||||
line-height: var(--typography-Title-1-lineHeight);
|
||||
}
|
||||
}
|
||||
22
components/HotelReservation/MyStay/Header/index.tsx
Normal file
22
components/HotelReservation/MyStay/Header/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import styles from "./header.module.css"
|
||||
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export async function Header({ hotel }: Pick<BookingConfirmation, "hotel">) {
|
||||
const intl = await getIntl()
|
||||
return (
|
||||
<header>
|
||||
<Title as="h2" color="white" className={styles.title} textAlign="center">
|
||||
<BiroScript type="two" tilted="medium">
|
||||
{intl.formatMessage({ id: "My stay at" })}{" "}
|
||||
</BiroScript>
|
||||
<span className={styles.hotelName}>{hotel.name}</span>
|
||||
{hotel.cityName}
|
||||
</Title>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import styles from "./bookingActions.module.css"
|
||||
|
||||
export async function BookingActions() {
|
||||
const intl = await getIntl()
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Changes can be made until {time}, {date} pending availability.",
|
||||
},
|
||||
{ time: "15:00", date: "Mon 15 Aug" }
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<Button>{intl.formatMessage({ id: "Modify dates" })}</Button>
|
||||
<Button>{intl.formatMessage({ id: "Cancel booking" })}</Button>
|
||||
<Button>{intl.formatMessage({ id: "Customer service" })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export async function Header({
|
||||
booking,
|
||||
hotel,
|
||||
}: Pick<BookingConfirmation, "booking" | "hotel">) {
|
||||
const intl = await getIntl()
|
||||
return (
|
||||
<header>
|
||||
<Title as="h2" color="red">
|
||||
{intl.formatMessage(
|
||||
{ id: "My stay at {hotelName}" },
|
||||
{
|
||||
hotelName: hotel.name,
|
||||
}
|
||||
)}
|
||||
</Title>
|
||||
<Title as="h3" level="h2" textTransform="regular">
|
||||
{intl.formatMessage(
|
||||
{ id: "Reservation No. {reservationNumber}" },
|
||||
{
|
||||
reservationNumber: booking.confirmationNumber,
|
||||
}
|
||||
)}
|
||||
</Title>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
import {
|
||||
getAncillaryPackages,
|
||||
getBookingConfirmation,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
import HotelDetails from "../../BookingConfirmation/HotelDetails"
|
||||
import PaymentDetails from "../../BookingConfirmation/PaymentDetails"
|
||||
import Promos from "../../BookingConfirmation/Promos"
|
||||
import Rooms from "../../BookingConfirmation/Rooms"
|
||||
import { Ancillaries } from "./Ancillaries"
|
||||
import { BookingActions } from "./BookingActions"
|
||||
import { Header } from "./Header"
|
||||
|
||||
import styles from "./myStay.module.css"
|
||||
|
||||
export async function MyStay({ reservationId }: { reservationId: string }) {
|
||||
const { booking, hotel, room } = await getBookingConfirmation(reservationId)
|
||||
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
|
||||
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
|
||||
const hotelId = hotel.operaId
|
||||
const ancillaryInput = { fromDate, hotelId, toDate }
|
||||
void getAncillaryPackages(ancillaryInput)
|
||||
const ancillaryPackages = await getAncillaryPackages(ancillaryInput)
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<Header booking={booking} hotel={hotel} />
|
||||
<BookingActions />
|
||||
{room && <Rooms booking={booking} mainRoom={room} />}
|
||||
{booking.showAncillaries && (
|
||||
<Ancillaries ancillaries={ancillaryPackages} />
|
||||
)}
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<PaymentDetails booking={booking} />
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<HotelDetails hotel={hotel} />
|
||||
<Promos
|
||||
hotelId={hotel.operaId}
|
||||
lastName={booking.guest.lastName}
|
||||
confirmationNumber={booking.confirmationNumber}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
.main {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x5);
|
||||
margin: 0 auto;
|
||||
min-height: 100dvh;
|
||||
padding-top: var(--Spacing-x5);
|
||||
width: var(--max-width-page);
|
||||
}
|
||||
|
||||
.headerSkeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.bookingActionsSkeleton {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x2);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bookingActionsSkeletonButtons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.ancillariesSkeleton {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.paymentDetailsSkeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.hotelDetailsSkeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
import styles from "./myStay.module.css"
|
||||
|
||||
export async function MyStaySkeleton() {
|
||||
return (
|
||||
<div className={styles.main}>
|
||||
<div className={styles.headerSkeleton}>
|
||||
<SkeletonShimmer width={"100%"} height="40px" />
|
||||
<SkeletonShimmer width={"300px"} height="30px" />
|
||||
</div>
|
||||
<div className={styles.bookingActionsSkeleton}>
|
||||
<SkeletonShimmer width={"300px"} height="20px" />
|
||||
<div className={styles.bookingActionsSkeletonButtons}>
|
||||
<SkeletonShimmer width={"125px"} height="50px" />
|
||||
<SkeletonShimmer width={"125px"} height="50px" />
|
||||
<SkeletonShimmer width={"125px"} height="50px" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.roomSkeleton}>
|
||||
<SkeletonShimmer width={"100%"} height="290px" />
|
||||
</div>
|
||||
<div className={styles.ancillariesSkeleton}>
|
||||
<SkeletonShimmer width={"280px"} height="200px" />
|
||||
<SkeletonShimmer width={"280px"} height="200px" />
|
||||
<SkeletonShimmer width={"280px"} height="200px" />
|
||||
<SkeletonShimmer width={"280px"} height="200px" />
|
||||
<SkeletonShimmer width={"280px"} height="200px" />
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.paymentDetailsSkeleton}>
|
||||
<SkeletonShimmer width={"200px"} height="30px" />
|
||||
<SkeletonShimmer width={"180px"} height="20px" />
|
||||
<SkeletonShimmer width={"190px"} height="20px" />
|
||||
<SkeletonShimmer width={"170px"} height="20px" />
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" />
|
||||
<div className={styles.hotelDetailsSkeleton}>
|
||||
<SkeletonShimmer width={"200px"} height="30px" />
|
||||
<SkeletonShimmer width={"180px"} height="20px" />
|
||||
<SkeletonShimmer width={"190px"} height="20px" />
|
||||
<SkeletonShimmer width={"170px"} height="20px" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
26
components/HotelReservation/MyStay/Promo/index.tsx
Normal file
26
components/HotelReservation/MyStay/Promo/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./promo.module.css"
|
||||
|
||||
import type { PromoProps } from "@/types/components/hotelReservation/bookingConfirmation/promo"
|
||||
|
||||
export default function Promo({ buttonText, href, text, title }: PromoProps) {
|
||||
return (
|
||||
<Link className={styles.link} color="none" href={href}>
|
||||
<article className={styles.promo}>
|
||||
<Title color="white" level="h4">
|
||||
{title}
|
||||
</Title>
|
||||
<Body className={styles.text} color="white" textAlign="center">
|
||||
{text}
|
||||
</Body>
|
||||
<Button asChild intent="primary" size="small" theme="primaryStrong">
|
||||
<div>{buttonText}</div>
|
||||
</Button>
|
||||
</article>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
33
components/HotelReservation/MyStay/Promo/promo.module.css
Normal file
33
components/HotelReservation/MyStay/Promo/promo.module.css
Normal file
@@ -0,0 +1,33 @@
|
||||
.promo {
|
||||
align-items: center;
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
flex: 1 0 480px;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
height: 480px;
|
||||
justify-content: center;
|
||||
padding: var(--Spacing-x4) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.promo {
|
||||
border-radius: var(--Medium, 8px);
|
||||
}
|
||||
}
|
||||
|
||||
.link .promo {
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.36) 37.88%,
|
||||
rgba(0, 0, 0, 0.75) 100%
|
||||
),
|
||||
url("/_static/img/Scandic_Family_Breakfast.jpg");
|
||||
}
|
||||
|
||||
.text {
|
||||
max-width: 400px;
|
||||
}
|
||||
131
components/HotelReservation/MyStay/ReferenceCard/index.tsx
Normal file
131
components/HotelReservation/MyStay/ReferenceCard/index.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./referenceCard.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
|
||||
export async function ReferenceCard({
|
||||
booking,
|
||||
hotel,
|
||||
}: {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
}) {
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||
|
||||
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
|
||||
|
||||
return (
|
||||
<div className={styles.referenceCard}>
|
||||
<div className={styles.referenceRow}>
|
||||
<Subtitle color="uiTextHighContrast" className={styles.titleMobile}>
|
||||
{intl.formatMessage({ id: "Reference" })}
|
||||
</Subtitle>
|
||||
<Subtitle color="uiTextHighContrast" className={styles.titleDesktop}>
|
||||
{intl.formatMessage({ id: "Reference number" })}
|
||||
</Subtitle>
|
||||
<Subtitle color="uiTextHighContrast">
|
||||
{booking.confirmationNumber}
|
||||
</Subtitle>
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" className={styles.divider} />
|
||||
<div className={styles.referenceRow}>
|
||||
<Caption
|
||||
textTransform="uppercase"
|
||||
type="bold"
|
||||
color="uiTextHighContrast"
|
||||
>
|
||||
{intl.formatMessage({ id: "Guests" })}
|
||||
</Caption>
|
||||
<Caption type="bold" color="uiTextHighContrast">
|
||||
{booking.childrenAges.length > 0
|
||||
? intl.formatMessage(
|
||||
{ id: "{adults} adults, {children} children" },
|
||||
{
|
||||
adults: booking.adults,
|
||||
children: booking.childrenAges.length,
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{ id: "{adults} adults" },
|
||||
{
|
||||
adults: booking.adults,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.referenceRow}>
|
||||
<Caption
|
||||
textTransform="uppercase"
|
||||
type="bold"
|
||||
color="uiTextHighContrast"
|
||||
>
|
||||
{intl.formatMessage({ id: "Check-in" })}
|
||||
</Caption>
|
||||
<Caption type="bold" color="uiTextHighContrast">
|
||||
{`${fromDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.referenceRow}>
|
||||
<Caption
|
||||
textTransform="uppercase"
|
||||
type="bold"
|
||||
color="uiTextHighContrast"
|
||||
>
|
||||
{intl.formatMessage({ id: "Check-out" })}
|
||||
</Caption>
|
||||
<Caption type="bold" color="uiTextHighContrast">
|
||||
{`${toDate.format("dddd, D MMMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`}
|
||||
</Caption>
|
||||
</div>
|
||||
<Divider color="primaryLightSubtle" className={styles.divider} />
|
||||
<div className={styles.referenceRow}>
|
||||
<Caption
|
||||
textTransform="uppercase"
|
||||
type="bold"
|
||||
color="uiTextHighContrast"
|
||||
>
|
||||
{intl.formatMessage({ id: "Total paid" })}
|
||||
</Caption>
|
||||
<Caption type="bold" color="uiTextHighContrast">
|
||||
{formatPrice(intl, booking.totalPrice, booking.currencyCode)}
|
||||
</Caption>
|
||||
</div>
|
||||
<div className={styles.actionArea}>
|
||||
<Button fullWidth>{intl.formatMessage({ id: "Manage stay" })}</Button>
|
||||
<Button fullWidth intent="secondary" asChild>
|
||||
<Link href={directionsUrl} target="_blank">
|
||||
{intl.formatMessage({ id: "Get directions" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
{booking.rateDefinition.cancellationRule !== "NotCancellable" && (
|
||||
<Caption className={styles.note} color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Changes can be made until {time} on {date}, subject to availability. Room rates may vary.",
|
||||
},
|
||||
{
|
||||
date: fromDate.format("D MMMM"),
|
||||
time: "18:00",
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
.referenceCard {
|
||||
width: var(--max-width-content);
|
||||
max-width: 588px;
|
||||
margin: 0 auto;
|
||||
padding: var(--Spacing-x3);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.referenceRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin-bottom: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.actionArea {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x3);
|
||||
margin: var(--Spacing-x4) 0 var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.referenceCard .note {
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.titleDesktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.titleMobile {
|
||||
display: none;
|
||||
}
|
||||
.titleDesktop {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
97
components/HotelReservation/MyStay/Room/GuestDetails.tsx
Normal file
97
components/HotelReservation/MyStay/Room/GuestDetails.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { DiamondIcon, EditIcon } from "@/components/Icons"
|
||||
import MembershipLevelIcon from "@/components/Levels/Icon"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./room.module.css"
|
||||
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { User } from "@/types/user"
|
||||
|
||||
export default function GuestDetails({
|
||||
user,
|
||||
booking,
|
||||
isMobile = false,
|
||||
}: {
|
||||
user: User | null
|
||||
booking: BookingConfirmation["booking"]
|
||||
isMobile?: boolean
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
const containerClass = isMobile
|
||||
? styles.guestDetailsMobile
|
||||
: styles.guestDetailsDesktop
|
||||
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
{user?.membership && (
|
||||
<div className={styles.userDetails}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.rowTitle}>
|
||||
<Caption
|
||||
type="bold"
|
||||
color="burgundy"
|
||||
textTransform="uppercase"
|
||||
textAlign="center"
|
||||
>
|
||||
{intl.formatMessage({ id: "Your member tier" })}
|
||||
</Caption>
|
||||
</div>
|
||||
<MembershipLevelIcon
|
||||
level={user.membership.membershipLevel}
|
||||
color="red"
|
||||
height={isMobile ? "40" : "20"}
|
||||
width={isMobile ? "80" : "40"}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.totalPoints}>
|
||||
{isMobile && (
|
||||
<div className={styles.totalPointsIcon}>
|
||||
<DiamondIcon color="uiTextHighContrast" />
|
||||
</div>
|
||||
)}
|
||||
<Caption
|
||||
type="bold"
|
||||
color="uiTextHighContrast"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{intl.formatMessage({ id: "Total points" })}
|
||||
</Caption>
|
||||
|
||||
<Body color="uiTextHighContrast" className={styles.totalPointsText}>
|
||||
{user.membership.currentPoints}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.guest}>
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{booking.guest.firstName} {booking.guest.lastName}
|
||||
</Body>
|
||||
{user?.membership && (
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Member no." })}{" "}
|
||||
{user.membership.membershipNumber}
|
||||
</Body>
|
||||
)}
|
||||
<Caption color="uiTextHighContrast">{booking.guest.email}</Caption>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{booking.guest.phoneNumber}
|
||||
</Caption>
|
||||
</div>
|
||||
<Button
|
||||
variant="icon"
|
||||
color="burgundy"
|
||||
intent={isMobile ? "secondary" : "text"}
|
||||
>
|
||||
<EditIcon color="burgundy" width={20} height={20} />
|
||||
<Caption color="burgundy">
|
||||
{intl.formatMessage({ id: "Modify guest details" })}
|
||||
</Caption>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
298
components/HotelReservation/MyStay/Room/index.tsx
Normal file
298
components/HotelReservation/MyStay/Room/index.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { getIconForFeatureCode } from "@/components/HotelReservation/utils"
|
||||
import {
|
||||
BedDoubleIcon,
|
||||
CoffeeIcon,
|
||||
ContractIcon,
|
||||
DoorOpenIcon,
|
||||
PersonIcon,
|
||||
} from "@/components/Icons"
|
||||
import RocketLaunch from "@/components/Icons/Refresh"
|
||||
import Image from "@/components/Image"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
import ToggleSidePeek from "../../EnterDetails/SelectedRoom/ToggleSidePeek"
|
||||
import PriceDetailsModal from "../../PriceDetailsModal"
|
||||
import GuestDetails from "./GuestDetails"
|
||||
|
||||
import styles from "./room.module.css"
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import type { Hotel, Room } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { User } from "@/types/user"
|
||||
|
||||
interface RoomProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
room:
|
||||
| (Room & {
|
||||
bedType: Room["roomTypes"][number]
|
||||
})
|
||||
| null
|
||||
hotel: Hotel
|
||||
user: User | null
|
||||
}
|
||||
|
||||
function hasBreakfastPackage(
|
||||
packages: BookingConfirmation["booking"]["packages"]
|
||||
) {
|
||||
return packages.some(
|
||||
(p) =>
|
||||
p.code === BreakfastPackageEnum.REGULAR_BREAKFAST ||
|
||||
p.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST ||
|
||||
p.code === BreakfastPackageEnum.SPECIAL_PACKAGE_BREAKFAST
|
||||
)
|
||||
}
|
||||
|
||||
function RoomHeader({
|
||||
room,
|
||||
hotel,
|
||||
}: {
|
||||
room: RoomProps["room"]
|
||||
hotel: Hotel
|
||||
}) {
|
||||
if (!room) return null
|
||||
|
||||
return (
|
||||
<div className={styles.roomHeader}>
|
||||
<Subtitle
|
||||
textTransform="uppercase"
|
||||
color="burgundy"
|
||||
className={styles.roomName}
|
||||
>
|
||||
{room.name}
|
||||
</Subtitle>
|
||||
<ToggleSidePeek
|
||||
hotelId={hotel.operaId}
|
||||
roomTypeCode={room.roomTypes[0].code}
|
||||
intent="text"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Room({ booking, room, hotel, user }: RoomProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
|
||||
if (!room) return null
|
||||
|
||||
const fromDate = dt(booking.checkInDate).locale(lang)
|
||||
|
||||
return (
|
||||
<div className={styles.roomContainer}>
|
||||
<article className={styles.room}>
|
||||
<RoomHeader room={room} hotel={hotel} />
|
||||
<div className={styles.booking}>
|
||||
<div className={styles.chipContainer}>
|
||||
{booking.packages
|
||||
.filter((item) =>
|
||||
Object.values(RoomPackageCodeEnum).includes(
|
||||
item.code as RoomPackageCodeEnum
|
||||
)
|
||||
)
|
||||
.map((item) => {
|
||||
const Icon = getIconForFeatureCode(
|
||||
item.code as RoomPackageCodeEnum
|
||||
)
|
||||
return (
|
||||
<span className={styles.chip} key={item.code}>
|
||||
<Icon width={16} height={16} color="burgundy" />
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.images}>
|
||||
{room.images.slice(0, 2).map((image) => (
|
||||
<Image
|
||||
key={image.imageSizes.large}
|
||||
src={image.imageSizes.large}
|
||||
className={styles.image}
|
||||
alt={room?.name ?? hotel.name}
|
||||
width={700}
|
||||
height={450}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.roomDetails}>
|
||||
<div className={styles.bookingDetails}>
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<ContractIcon color="grey80" width={20} height={20} />
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Booking policy" })}
|
||||
</Body>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.rateDefinition.title}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<RocketLaunch color="grey80" width={20} height={20} />
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Rebooking" })}
|
||||
</Body>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "Until {time}, {date}" },
|
||||
{ time: "18:00", date: fromDate.format("dddd D MMM") }
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
{booking.packages.some((item) =>
|
||||
Object.values(RoomPackageCodeEnum).includes(
|
||||
item.code as RoomPackageCodeEnum
|
||||
)
|
||||
) && (
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<DoorOpenIcon color="grey80" width={20} height={20} />
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Room type" })}
|
||||
</Body>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.packages
|
||||
.filter((item) =>
|
||||
Object.values(RoomPackageCodeEnum).includes(
|
||||
item.code as RoomPackageCodeEnum
|
||||
)
|
||||
)
|
||||
.map((item) => item.description)
|
||||
.join(", ")}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<PersonIcon color="grey80" width={20} height={20} />
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Guests" })}
|
||||
</Body>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{booking.childrenAges.length > 0
|
||||
? intl.formatMessage(
|
||||
{ id: "{adults} adults, {children} children" },
|
||||
{
|
||||
adults: booking.adults,
|
||||
children: booking.childrenAges.length,
|
||||
}
|
||||
)
|
||||
: intl.formatMessage(
|
||||
{ id: "{adults} adults" },
|
||||
{
|
||||
adults: booking.adults,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<CoffeeIcon color="grey80" width={20} height={20} />
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Breakfast" })}
|
||||
</Body>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{hasBreakfastPackage(booking.packages)
|
||||
? intl.formatMessage({ id: "Included" })
|
||||
: intl.formatMessage({ id: "Not included" })}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<span className={styles.rowTitle}>
|
||||
<BedDoubleIcon color="grey80" width={20} height={20} />
|
||||
<Body textTransform="bold" color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Bed preference" })}
|
||||
</Body>
|
||||
</span>
|
||||
<div className={styles.rowContent}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{room.bedType.mainBed.description}
|
||||
{room.bedType.mainBed.widthRange.min ===
|
||||
room.bedType.mainBed.widthRange.max
|
||||
? ` (${room.bedType.mainBed.widthRange.min} ${intl.formatMessage({ id: "cm" })})`
|
||||
: ` (${room.bedType.mainBed.widthRange.min} - ${room.bedType.mainBed.widthRange.max} ${intl.formatMessage({ id: "cm" })})`}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GuestDetails user={user} booking={booking} isMobile={false} />
|
||||
</div>
|
||||
<div className={styles.bookingInformation}>
|
||||
<div className={styles.bookingCode}></div>
|
||||
<div className={styles.priceDetails}>
|
||||
<div className={styles.price}>
|
||||
<Body color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Room total" })}
|
||||
</Body>
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{formatPrice(intl, booking.totalPrice, booking.currencyCode)}
|
||||
</Body>
|
||||
</div>
|
||||
|
||||
<PriceDetailsModal
|
||||
fromDate={dt(booking.checkInDate).format("YYYY-MM-DD")}
|
||||
toDate={dt(booking.checkOutDate).format("YYYY-MM-DD")}
|
||||
rooms={[
|
||||
{
|
||||
adults: booking.adults,
|
||||
childrenInRoom: undefined,
|
||||
roomPrice: {
|
||||
perNight: {
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: booking.currencyCode,
|
||||
price: booking.totalPrice,
|
||||
},
|
||||
},
|
||||
perStay: {
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: booking.currencyCode,
|
||||
price: booking.totalPrice,
|
||||
},
|
||||
},
|
||||
},
|
||||
roomType: room.name,
|
||||
},
|
||||
]}
|
||||
totalPrice={{
|
||||
requested: undefined,
|
||||
local: {
|
||||
currency: booking.currencyCode,
|
||||
price: booking.totalPrice,
|
||||
},
|
||||
}}
|
||||
vat={booking.vatPercentage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<GuestDetails user={user} booking={booking} isMobile={true} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
284
components/HotelReservation/MyStay/Room/room.module.css
Normal file
284
components/HotelReservation/MyStay/Room/room.module.css
Normal file
@@ -0,0 +1,284 @@
|
||||
.room {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
padding: var(--Spacing-x3) 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.room {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.roomHeader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--max-width-content);
|
||||
margin: 0 auto;
|
||||
align-items: flex-start;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.roomHeader {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.booking {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
position: relative;
|
||||
width: var(--max-width-content);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.booking {
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
|
||||
.chipContainer {
|
||||
position: absolute;
|
||||
top: 300px;
|
||||
left: 25px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.chip {
|
||||
background-color: var(--Main-Grey-White);
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
}
|
||||
|
||||
.images {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
grid-template-columns: 1fr;
|
||||
height: 210px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.images {
|
||||
height: 320px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
width: 100%;
|
||||
height: 210px;
|
||||
aspect-ratio: 16/9;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.image:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.image {
|
||||
height: 100%;
|
||||
}
|
||||
.image:last-child {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.roomDetails {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x5);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.roomDetails {
|
||||
grid-template-columns: minmax(0, 700px) 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.bookingDetails {
|
||||
max-width: 100%;
|
||||
padding: 0 var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bookingDetails {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--Spacing-x-one-and-half) 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.row {
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.rowTitle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.rowTitle svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.rowTitle svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.rowContent {
|
||||
padding-left: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.guestDetailsDesktop {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.guestDetailsDesktop {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.guestDetailsMobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: var(--Spacing-x2);
|
||||
background-color: var(--Main-Brand-PalePeach);
|
||||
padding: var(--Spacing-x3) 0;
|
||||
}
|
||||
|
||||
.guestDetailsMobile .row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.guestDetailsMobile .rowTitle {
|
||||
margin-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.guestDetailsMobile .userDetails {
|
||||
width: calc(100% - var(--Spacing-x4) - var(--Spacing-x4));
|
||||
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider);
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
margin-bottom: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.guestDetailsMobile .totalPoints {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--Spacing-x1);
|
||||
padding-top: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.guestDetailsMobile .totalPointsText {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.guestDetailsMobile .guest {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.guestDetailsMobile {
|
||||
display: none;
|
||||
}
|
||||
.totalPoints {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--Spacing-x-one-and-half) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.guest {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
margin-bottom: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.bookingInformation {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bookingInformation {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.priceDetails {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
border-top: 1px solid var(--Base-Border-Subtle);
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
padding: var(--Spacing-x-one-and-half) 0;
|
||||
width: calc(100% - var(--Spacing-x4));
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.priceDetails {
|
||||
border: none;
|
||||
margin: 0;
|
||||
width: auto;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.price {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
}
|
||||
|
||||
.userDetails {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
margin-bottom: var(--Spacing-x1);
|
||||
}
|
||||
71
components/HotelReservation/MyStay/index.tsx
Normal file
71
components/HotelReservation/MyStay/index.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { homeHrefs } from "@/constants/homeHrefs"
|
||||
import { env } from "@/env/server"
|
||||
import { dt } from "@/lib/dt"
|
||||
import {
|
||||
getAncillaryPackages,
|
||||
getBookingConfirmation,
|
||||
getProfileSafely,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Image from "@/components/Image"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { Ancillaries } from "./Ancillaries"
|
||||
import BookingSummary from "./BookingSummary"
|
||||
import { Header } from "./Header"
|
||||
import Promo from "./Promo"
|
||||
import { ReferenceCard } from "./ReferenceCard"
|
||||
import { Room } from "./Room"
|
||||
|
||||
import styles from "./myStay.module.css"
|
||||
|
||||
export async function MyStay({ reservationId }: { reservationId: string }) {
|
||||
const { booking, hotel, room } = await getBookingConfirmation(reservationId)
|
||||
const userResponse = await getProfileSafely()
|
||||
const user = userResponse && !("error" in userResponse) ? userResponse : null
|
||||
const intl = await getIntl()
|
||||
const lang = getLang()
|
||||
const homeUrl = homeHrefs[env.NODE_ENV][lang]
|
||||
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
|
||||
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
|
||||
const hotelId = hotel.operaId
|
||||
const ancillaryInput = { fromDate, hotelId, toDate }
|
||||
void getAncillaryPackages(ancillaryInput)
|
||||
const ancillaryPackages = await getAncillaryPackages(ancillaryInput)
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.imageContainer}>
|
||||
<div className={styles.blurOverlay} />
|
||||
{hotel.gallery?.heroImages[0].imageSizes.large && (
|
||||
<Image
|
||||
className={styles.image}
|
||||
src={hotel.gallery.heroImages[0].imageSizes.large}
|
||||
alt={hotel.name}
|
||||
fill
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.headerContainer}>
|
||||
<Header hotel={hotel} />
|
||||
<ReferenceCard booking={booking} hotel={hotel} />
|
||||
</div>
|
||||
{booking.showAncillaries && (
|
||||
<Ancillaries ancillaries={ancillaryPackages} />
|
||||
)}
|
||||
<Room booking={booking} room={room} hotel={hotel} user={user} />
|
||||
<BookingSummary booking={booking} hotel={hotel} />
|
||||
<Promo
|
||||
buttonText={intl.formatMessage({ id: "Book another stay" })}
|
||||
href={`${homeUrl}?hotel=${hotel.operaId}`}
|
||||
text={intl.formatMessage({
|
||||
id: "Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||
})}
|
||||
title={intl.formatMessage({ id: "Book your next stay" })}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
98
components/HotelReservation/MyStay/myStay.module.css
Normal file
98
components/HotelReservation/MyStay/myStay.module.css
Normal file
@@ -0,0 +1,98 @@
|
||||
.main {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
.blurOverlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
backdrop-filter: blur(12px);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, transparent 100%);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0.5) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 80px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.content {
|
||||
width: var(--max-width-content);
|
||||
padding-bottom: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.headerSkeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
align-items: center;
|
||||
padding: var(--Spacing-x6) var(--Spacing-x2) 0;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: 0 var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.cardSkeleton {
|
||||
max-width: 100%;
|
||||
margin: -30px auto 0;
|
||||
padding: 0 var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.ancillariesSkeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.ancillariesSkeleton {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.paymentDetailsSkeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.hotelDetailsSkeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
34
components/HotelReservation/MyStay/myStaySkeleton.tsx
Normal file
34
components/HotelReservation/MyStay/myStaySkeleton.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import styles from "./myStay.module.css"
|
||||
|
||||
export async function MyStaySkeleton() {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.headerSkeleton}>
|
||||
<SkeletonShimmer width={"100px"} height="20px" />
|
||||
<SkeletonShimmer width={"450px"} height="50px" />
|
||||
<SkeletonShimmer width={"200px"} height="30px" />
|
||||
</div>
|
||||
<div className={styles.cardSkeleton}>
|
||||
<SkeletonShimmer width="590px" height="380px" />
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<SkeletonShimmer width={"200px"} height="30px" />
|
||||
<div className={styles.ancillariesSkeleton}>
|
||||
<SkeletonShimmer width="280px" height="200px" />
|
||||
<SkeletonShimmer width="280px" height="200px" />
|
||||
<SkeletonShimmer width="280px" height="200px" />
|
||||
<SkeletonShimmer width="280px" height="200px" />
|
||||
<SkeletonShimmer width="280px" height="200px" />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<SkeletonShimmer width={"200px"} height="30px" />
|
||||
<div className={styles.roomSkeleton}>
|
||||
<SkeletonShimmer width="100%" height="700px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall"
|
||||
|
||||
23
components/Icons/Diamond.tsx
Normal file
23
components/Icons/Diamond.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function DiamondIcon({ className, color, ...props }: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
fill="none"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11.9993 20.17C11.7331 20.17 11.475 20.1117 11.225 19.995C10.975 19.8783 10.7542 19.7075 10.5625 19.4825L2.85 10.245C2.70834 10.0724 2.60209 9.88404 2.53125 9.68001C2.46042 9.47597 2.425 9.2641 2.425 9.04438C2.425 8.90313 2.4375 8.76167 2.4625 8.62001C2.4875 8.47834 2.5375 8.34084 2.6125 8.20751L4.525 4.42001C4.69167 4.10334 4.92018 3.85126 5.21053 3.66376C5.50088 3.47626 5.82654 3.38251 6.1875 3.38251H17.8125C18.1735 3.38251 18.4991 3.47626 18.7895 3.66376C19.0798 3.85126 19.3083 4.10334 19.475 4.42001L21.3875 8.20751C21.4625 8.34084 21.5125 8.47834 21.5375 8.62001C21.5625 8.76167 21.575 8.90313 21.575 9.04438C21.575 9.2641 21.5396 9.47597 21.4688 9.68001C21.3979 9.88404 21.2917 10.0724 21.15 10.245L13.4375 19.4825C13.2458 19.7075 13.0248 19.8783 12.7743 19.995C12.5237 20.1117 12.2654 20.17 11.9993 20.17ZM9.525 8.38251H14.475L12.9116 5.25751H11.0875L9.525 8.38251ZM11.0625 17.17V10.2575H5.3125L11.0625 17.17ZM12.9375 17.17L18.6875 10.2575H12.9375V17.17ZM16.5625 8.38251H19.35L17.7875 5.25751H15L16.5625 8.38251ZM4.65 8.38251H7.4375L9 5.25751H6.2125L4.65 8.38251Z"
|
||||
fill="#26201E"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
29
components/Icons/Directions.tsx
Normal file
29
components/Icons/Directions.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function DirectionsIcon({
|
||||
className,
|
||||
color,
|
||||
width = "20",
|
||||
height = "20",
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
fill="none"
|
||||
height={height}
|
||||
viewBox="0 0 20 20"
|
||||
width={width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M8.28158 9.94792H11.3024V11.0104C11.3024 11.191 11.3823 11.3142 11.542 11.3802C11.7017 11.4462 11.8441 11.4167 11.9691 11.2917L13.542 9.70834C13.7017 9.54862 13.7816 9.36632 13.7816 9.16146C13.7816 8.9566 13.7017 8.77431 13.542 8.61459L11.9691 7.04167C11.8441 6.91667 11.7017 6.88542 11.542 6.94792C11.3823 7.01042 11.3024 7.13195 11.3024 7.31251V8.38542H7.50033C7.28505 8.38542 7.10102 8.46181 6.94824 8.61459C6.79546 8.76737 6.71908 8.95139 6.71908 9.16667V11.6667C6.71908 11.882 6.79546 12.066 6.94824 12.2188C7.10102 12.3715 7.28505 12.4479 7.50033 12.4479C7.7156 12.4479 7.89963 12.3715 8.05241 12.2188C8.20519 12.066 8.28158 11.882 8.28158 11.6667V9.94792ZM10.0003 18.1146C9.79894 18.1146 9.60623 18.0764 9.4222 18C9.23817 17.9236 9.0663 17.809 8.90658 17.6563L2.34408 11.0938C2.1913 10.934 2.07671 10.7622 2.00033 10.5781C1.92394 10.3941 1.88574 10.2014 1.88574 10C1.88574 9.79862 1.92394 9.60417 2.00033 9.41667C2.07671 9.22917 2.1913 9.05903 2.34408 8.90626L8.90658 2.34376C9.0663 2.18403 9.23817 2.06771 9.4222 1.9948C9.60623 1.92188 9.79894 1.88542 10.0003 1.88542C10.2017 1.88542 10.3962 1.92188 10.5837 1.9948C10.7712 2.06771 10.9413 2.18403 11.0941 2.34376L17.6566 8.90626C17.8163 9.05903 17.9326 9.22917 18.0055 9.41667C18.0785 9.60417 18.1149 9.79862 18.1149 10C18.1149 10.2014 18.0785 10.3941 18.0055 10.5781C17.9326 10.7622 17.8163 10.934 17.6566 11.0938L11.0941 17.6563C10.9413 17.809 10.7712 17.9236 10.5837 18C10.3962 18.0764 10.2017 18.1146 10.0003 18.1146ZM10.0003 16.5625L16.5628 10L10.0003 3.43751L3.43783 10L10.0003 16.5625Z"
|
||||
fill="#4D001B"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
29
components/Icons/Link.tsx
Normal file
29
components/Icons/Link.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function LinkIcon({
|
||||
className,
|
||||
color,
|
||||
width = "20",
|
||||
height = "20",
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
fill="none"
|
||||
height={height}
|
||||
viewBox="0 0 20 20"
|
||||
width={width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M5.89583 14.0208C4.77778 14.0208 3.82812 13.6302 3.04688 12.8489C2.26562 12.0677 1.875 11.118 1.875 9.99999C1.875 8.88193 2.26562 7.93228 3.04688 7.15103C3.82812 6.36978 4.77778 5.97916 5.89583 5.97916H8.36458C8.57986 5.97916 8.76389 6.05555 8.91667 6.20832C9.06944 6.3611 9.14583 6.54513 9.14583 6.76041C9.14583 6.97568 9.06944 7.15971 8.91667 7.31249C8.76389 7.46527 8.57986 7.54166 8.36458 7.54166H5.89583C5.21528 7.54166 4.63542 7.78124 4.15625 8.26041C3.67708 8.73957 3.4375 9.31943 3.4375 9.99999C3.4375 10.6805 3.67708 11.2604 4.15625 11.7396C4.63542 12.2187 5.21528 12.4583 5.89583 12.4583H8.36458C8.57986 12.4583 8.76389 12.5347 8.91667 12.6875C9.06944 12.8403 9.14583 13.0243 9.14583 13.2396C9.14583 13.4549 9.06944 13.6389 8.91667 13.7917C8.76389 13.9444 8.57986 14.0208 8.36458 14.0208H5.89583ZM7.53125 10.7708C7.31597 10.7708 7.13194 10.6944 6.97917 10.5417C6.82639 10.3889 6.75 10.2049 6.75 9.98957C6.75 9.7743 6.82639 9.59027 6.97917 9.43749C7.13194 9.28471 7.31597 9.20832 7.53125 9.20832H12.4688C12.684 9.20832 12.8681 9.28471 13.0208 9.43749C13.1736 9.59027 13.25 9.7743 13.25 9.98957C13.25 10.2049 13.1736 10.3889 13.0208 10.5417C12.8681 10.6944 12.684 10.7708 12.4688 10.7708H7.53125ZM11.6354 14.0208C11.4201 14.0208 11.2361 13.9444 11.0833 13.7917C10.9306 13.6389 10.8542 13.4549 10.8542 13.2396C10.8542 13.0243 10.9306 12.8403 11.0833 12.6875C11.2361 12.5347 11.4201 12.4583 11.6354 12.4583H14.1042C14.7847 12.4583 15.3646 12.2187 15.8438 11.7396C16.3229 11.2604 16.5625 10.6805 16.5625 9.99999C16.5625 9.31943 16.3229 8.73957 15.8438 8.26041C15.3646 7.78124 14.7847 7.54166 14.1042 7.54166H11.6354C11.4201 7.54166 11.2361 7.46527 11.0833 7.31249C10.9306 7.15971 10.8542 6.97568 10.8542 6.76041C10.8542 6.54513 10.9306 6.3611 11.0833 6.20832C11.2361 6.05555 11.4201 5.97916 11.6354 5.97916H14.1042C15.2222 5.97916 16.1719 6.36978 16.9531 7.15103C17.7344 7.93228 18.125 8.88193 18.125 9.99999C18.125 11.118 17.7344 12.0677 16.9531 12.8489C16.1719 13.6302 15.2222 14.0208 14.1042 14.0208H11.6354Z"
|
||||
fill="#4D001B"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
CulturalIcon,
|
||||
CutleryOneIcon,
|
||||
CutleryTwoIcon,
|
||||
DiamondIcon,
|
||||
DirectionsIcon,
|
||||
DoorOpenIcon,
|
||||
DresserIcon,
|
||||
ElectricBikeIcon,
|
||||
@@ -67,6 +69,7 @@ import {
|
||||
KidsMocktailIcon,
|
||||
LampIcon,
|
||||
LaundryMachineIcon,
|
||||
LinkIcon,
|
||||
LocalBarIcon,
|
||||
LocationIcon,
|
||||
LockIcon,
|
||||
@@ -191,6 +194,10 @@ export function getIconByIconName(
|
||||
return CutleryOneIcon
|
||||
case IconName.CutleryTwo:
|
||||
return CutleryTwoIcon
|
||||
case IconName.Diamond:
|
||||
return DiamondIcon
|
||||
case IconName.Directions:
|
||||
return DirectionsIcon
|
||||
case IconName.DoorOpen:
|
||||
return DoorOpenIcon
|
||||
case IconName.Dresser:
|
||||
@@ -257,6 +264,8 @@ export function getIconByIconName(
|
||||
return LampIcon
|
||||
case IconName.LaundryMachine:
|
||||
return LaundryMachineIcon
|
||||
case IconName.Link:
|
||||
return LinkIcon
|
||||
case IconName.LocalBar:
|
||||
return LocalBarIcon
|
||||
case IconName.Location:
|
||||
|
||||
@@ -65,7 +65,9 @@ export { default as CutleryOneIcon } from "./CutleryOne"
|
||||
export { default as CutleryTwoIcon } from "./CutleryTwo"
|
||||
export { default as DeleteIcon } from "./Delete"
|
||||
export { default as DeskIcon } from "./Desk"
|
||||
export { default as DiamondIcon } from "./Diamond"
|
||||
export { default as DiningIcon } from "./Dining"
|
||||
export { default as DirectionsIcon } from "./Directions"
|
||||
export { default as DiscountIcon } from "./Discount"
|
||||
export { default as DoorClosedIcon } from "./DoorClosed"
|
||||
export { default as DoorOpenIcon } from "./DoorOpen"
|
||||
@@ -112,6 +114,7 @@ export { default as KidsMocktailIcon } from "./KidsMocktail"
|
||||
export { default as LampIcon } from "./Lamp"
|
||||
export { default as LaptopIcon } from "./Laptop"
|
||||
export { default as LaundryMachineIcon } from "./LaundryMachine"
|
||||
export { default as LinkIcon } from "./Link"
|
||||
export { default as LocalBarIcon } from "./LocalBar"
|
||||
export { default as LocationIcon } from "./Location"
|
||||
export { default as LockIcon } from "./Lock"
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function SkeletonShimmer({
|
||||
style={{
|
||||
height: height,
|
||||
width: width,
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
22
components/TempDesignSystem/IconChip/iconChip.module.css
Normal file
22
components/TempDesignSystem/IconChip/iconChip.module.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
}
|
||||
|
||||
.blue {
|
||||
background-color: var(--Scandic-Blue-00);
|
||||
color: var(--UI-Semantic-Information);
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: var(--Scandic-Green-00);
|
||||
color: var(--UI-Semantic-Success);
|
||||
}
|
||||
|
||||
.red {
|
||||
background-color: var(--Scandic-Red-00);
|
||||
color: var(--UI-Semantic-Error);
|
||||
}
|
||||
19
components/TempDesignSystem/IconChip/index.tsx
Normal file
19
components/TempDesignSystem/IconChip/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { iconChipVariants } from "./variants"
|
||||
|
||||
interface IconChipProps {
|
||||
color: "blue" | "green" | "red" | null | undefined
|
||||
icon: React.ReactNode
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default function IconChip({ color, icon, children }: IconChipProps) {
|
||||
const classNames = iconChipVariants({
|
||||
color: color,
|
||||
})
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{icon}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
components/TempDesignSystem/IconChip/variants.ts
Normal file
13
components/TempDesignSystem/IconChip/variants.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./iconChip.module.css"
|
||||
|
||||
export const iconChipVariants = cva(styles.chip, {
|
||||
variants: {
|
||||
color: {
|
||||
blue: styles.blue,
|
||||
green: styles.green,
|
||||
red: styles.red,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -143,3 +143,7 @@
|
||||
.baseText {
|
||||
color: var(--Base-Text-Inverted);
|
||||
}
|
||||
|
||||
.success {
|
||||
color: var(--UI-Semantic-Success);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ const config = {
|
||||
primaryDim: styles.primaryDim,
|
||||
primaryStrong: styles.primaryStrong,
|
||||
baseText: styles.baseText,
|
||||
success: styles.success,
|
||||
},
|
||||
textAlign: {
|
||||
center: styles.textAlignCenter,
|
||||
|
||||
@@ -110,3 +110,10 @@ p.caption {
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: var(--UI-Semantic-Success);
|
||||
}
|
||||
.blue {
|
||||
color: var(--UI-Semantic-Information);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ const config = {
|
||||
textMediumContrast: styles.textMediumContrast,
|
||||
red: styles.red,
|
||||
white: styles.white,
|
||||
green: styles.green,
|
||||
blue: styles.blue,
|
||||
uiTextHighContrast: styles.uiTextHighContrast,
|
||||
uiTextActive: styles.uiTextActive,
|
||||
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x3)
|
||||
var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
Reference in New Issue
Block a user