feat(SW-1012): Started implementing restaurant data
This commit is contained in:
committed by
Fredrik Thorsson
parent
05006506f0
commit
63a77b215d
@@ -0,0 +1,40 @@
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import { RestaurantBarOpeningHoursProps } from "@/types/components/hotelPage/sidepeek/restaurantBar"
|
||||
import { RestaurantOpeningHoursDay } from "@/types/hotel"
|
||||
|
||||
export default function OpeningHours({
|
||||
openingHours,
|
||||
alternateOpeningHours,
|
||||
}: RestaurantBarOpeningHoursProps) {
|
||||
const days: (keyof typeof openingHours)[] = [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Body textTransform="bold" asChild>
|
||||
<h5>{openingHours.name}</h5>
|
||||
</Body>
|
||||
{days.map((day) => {
|
||||
const today = openingHours[day] as RestaurantOpeningHoursDay
|
||||
return today ? (
|
||||
<Body color="uiTextHighContrast">
|
||||
{day}:{" "}
|
||||
{today.isClosed
|
||||
? "Closed"
|
||||
: today.alwaysOpen
|
||||
? "Always open"
|
||||
: `${today.openingTime}-${today.closingTime}`}
|
||||
</Body>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export default function RestaurantSidepeek() {}
|
||||
@@ -0,0 +1,101 @@
|
||||
import NextLink from "next/link"
|
||||
|
||||
import { OpenInNewSmallIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import OpeningHours from "../OpeningHours"
|
||||
|
||||
import styles from "./restaurantBarItem.module.css"
|
||||
|
||||
import { RestaurantBarItemProps } from "@/types/components/hotelPage/sidepeek/restaurantBar"
|
||||
|
||||
export default function RestaurantBarItem({
|
||||
restaurant,
|
||||
}: RestaurantBarItemProps) {
|
||||
const { name, openingDetails, menus } = restaurant
|
||||
const { images, bookTableUrl } = restaurant.content
|
||||
const visibleImages = restaurant.content.images.slice(0, 2)
|
||||
const imageWidth = images.length === 2 ? 240 : 496
|
||||
// TODO: Not defined where this should lead.
|
||||
const ctaUrl = ""
|
||||
|
||||
console.log({ bookTableUrl: bookTableUrl })
|
||||
return (
|
||||
<div className={styles.restaurantBarItem}>
|
||||
<div className={styles.stickyHeading}>
|
||||
<Subtitle type="one" color="burgundy" asChild>
|
||||
<h3>{name}</h3>
|
||||
</Subtitle>
|
||||
</div>
|
||||
<div className={styles.imageWrapper}>
|
||||
{visibleImages.map(({ metaData, imageSizes }) => (
|
||||
<Image
|
||||
key={imageSizes.tiny}
|
||||
src={imageSizes.tiny}
|
||||
alt={metaData.altText}
|
||||
width={imageWidth}
|
||||
height={240}
|
||||
className={styles.image}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{openingDetails.length ? (
|
||||
<div className={styles.content}>
|
||||
<Subtitle type="two" asChild>
|
||||
<h4>Opening Hours</h4>
|
||||
</Subtitle>
|
||||
{openingDetails.map((details) => (
|
||||
<OpeningHours
|
||||
key={details.openingHours.name}
|
||||
openingHours={details.openingHours}
|
||||
alternateOpeningHours={details.alternateOpeningHours}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{menus.length ? (
|
||||
<div className={styles.content}>
|
||||
<Subtitle type="two" asChild>
|
||||
<h4>Menus</h4>
|
||||
</Subtitle>
|
||||
<ul className={styles.menuList}>
|
||||
{menus.map(({ name, url }) => (
|
||||
<li key={name}>
|
||||
<Link
|
||||
href={url}
|
||||
target="_blank"
|
||||
textDecoration="underline"
|
||||
variant="icon"
|
||||
color="baseTextMediumContrast"
|
||||
className={styles.menuLink}
|
||||
>
|
||||
{name}
|
||||
<OpenInNewSmallIcon />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
{bookTableUrl || ctaUrl ? (
|
||||
<div className={styles.ctaWrapper}>
|
||||
{bookTableUrl ? (
|
||||
<Button fullWidth theme="base" intent="primary" asChild>
|
||||
<a href={bookTableUrl} target="_blank">
|
||||
Book a table online
|
||||
</a>
|
||||
</Button>
|
||||
) : null}
|
||||
{ctaUrl ? (
|
||||
<Button fullWidth theme="base" intent="secondary" asChild>
|
||||
<NextLink href={ctaUrl}>Discover {name}</NextLink>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
.restaurantBarItem {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.stickyHeading {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
/* Hack to make it look like the heading has a background which covers the top of the sidepeek */
|
||||
.stickyHeading::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
margin-top: calc(-1 * var(--Spacing-x4));
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
bottom: -16px;
|
||||
}
|
||||
|
||||
.imageWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.image {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.menuList {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.ctaWrapper {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import RestaurantBarItem from "./RestaurantBarItem"
|
||||
|
||||
import styles from "./restaurantBar.module.css"
|
||||
|
||||
import type { RestaurantBarSidePeekProps } from "@/types/components/hotelPage/sidepeek/restaurantBar"
|
||||
@@ -12,7 +14,6 @@ export default async function RestaurantBarSidePeek({
|
||||
restaurants,
|
||||
}: RestaurantBarSidePeekProps) {
|
||||
const lang = getLang()
|
||||
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
@@ -22,8 +23,8 @@ export default async function RestaurantBarSidePeek({
|
||||
>
|
||||
<div className={styles.content}>
|
||||
{restaurants.map((restaurant) => (
|
||||
<div key={restaurant.id}>
|
||||
<h3>{restaurant.name}</h3>
|
||||
<div key={restaurant.id} className={styles.item}>
|
||||
<RestaurantBarItem restaurant={restaurant} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.content {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
.item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--Base-Border-Subtle);
|
||||
padding-bottom: var(--Spacing-x4);
|
||||
}
|
||||
|
||||
27
components/Icons/OpenInNew.tsx
Normal file
27
components/Icons/OpenInNew.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function OpenInNewIcon({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M5.09998 20.775C4.58434 20.775 4.14293 20.5914 3.77575 20.2242C3.40857 19.857 3.22498 19.4156 3.22498 18.9V5.09998C3.22498 4.58434 3.40857 4.14293 3.77575 3.77575C4.14293 3.40857 4.58434 3.22498 5.09998 3.22498H10.9875C11.2458 3.22498 11.4666 3.31664 11.65 3.49998C11.8333 3.68331 11.925 3.90414 11.925 4.16248C11.925 4.42081 11.8333 4.64164 11.65 4.82498C11.4666 5.00831 11.2458 5.09998 10.9875 5.09998H5.09998V18.9H18.9V13.0125C18.9 12.7541 18.9916 12.5333 19.175 12.35C19.3583 12.1666 19.5791 12.075 19.8375 12.075C20.0958 12.075 20.3166 12.1666 20.5 12.35C20.6833 12.5333 20.775 12.7541 20.775 13.0125V18.9C20.775 19.4156 20.5914 19.857 20.2242 20.2242C19.857 20.5914 19.4156 20.775 18.9 20.775H5.09998ZM18.9 6.41248L10.3875 14.925C10.2125 15.1 9.99789 15.1875 9.74373 15.1875C9.48956 15.1875 9.27081 15.0958 9.08748 14.9125C8.90414 14.7291 8.81248 14.5104 8.81248 14.2562C8.81248 14.0021 8.90311 13.7843 9.08438 13.6031L17.5875 5.09998H14.7375C14.4791 5.09998 14.2583 5.00831 14.075 4.82498C13.8916 4.64164 13.8 4.42081 13.8 4.16248C13.8 3.90414 13.8916 3.68331 14.075 3.49998C14.2583 3.31664 14.4791 3.22498 14.7375 3.22498H20.775V9.26248C20.775 9.52081 20.6833 9.74164 20.5 9.92498C20.3166 10.1083 20.0958 10.2 19.8375 10.2C19.5791 10.2 19.3583 10.1083 19.175 9.92498C18.9916 9.74164 18.9 9.52081 18.9 9.26248V6.41248Z"
|
||||
fill="#26201E"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
27
components/Icons/OpenInNewSmall.tsx
Normal file
27
components/Icons/OpenInNewSmall.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function OpenInNewSmallIcon({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M4.58333 16.8125C4.19949 16.8125 3.87088 16.6758 3.59752 16.4025C3.32417 16.1291 3.1875 15.8005 3.1875 15.4167V4.58333C3.1875 4.19947 3.32417 3.87087 3.59752 3.59752C3.87088 3.32417 4.19949 3.1875 4.58333 3.1875H9.9375V4.58333H4.58333V15.4167H15.4167V10.0625H16.8125V15.4167C16.8125 15.8005 16.6758 16.1291 16.4025 16.4025C16.1291 16.6758 15.8005 16.8125 15.4167 16.8125H4.58333ZM8.09375 12.8958L7.10417 11.9062L14.4271 4.58333H11.8333V3.1875H16.8125V8.16667H15.4167V5.57292L8.09375 12.8958Z"
|
||||
fill="#26201E"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -79,6 +79,8 @@ import {
|
||||
NatureIcon,
|
||||
NightlifeIcon,
|
||||
NoSmokingIcon,
|
||||
OpenInNewIcon,
|
||||
OpenInNewSmallIcon,
|
||||
OutdoorFurnitureIcon,
|
||||
ParkingIcon,
|
||||
People2Icon,
|
||||
@@ -280,6 +282,10 @@ export function getIconByIconName(
|
||||
return NightlifeIcon
|
||||
case IconName.NoSmoking:
|
||||
return NoSmokingIcon
|
||||
case IconName.OpenInNew:
|
||||
return OpenInNewIcon
|
||||
case IconName.OpenInNewSmall:
|
||||
return OpenInNewSmallIcon
|
||||
case IconName.OutdoorFurniture:
|
||||
return OutdoorFurnitureIcon
|
||||
case IconName.Parking:
|
||||
|
||||
@@ -127,6 +127,8 @@ export { default as NatureIcon } from "./Nature"
|
||||
export { default as NightlifeIcon } from "./Nightlife"
|
||||
export { default as NoBreakfastIcon } from "./NoBreakfast"
|
||||
export { default as NoSmokingIcon } from "./NoSmoking"
|
||||
export { default as OpenInNewIcon } from "./OpenInNew"
|
||||
export { default as OpenInNewSmallIcon } from "./OpenInNewSmall"
|
||||
export { default as OutdoorFurnitureIcon } from "./OutdoorFurniture"
|
||||
export { default as ParkingIcon } from "./Parking"
|
||||
export { default as People2Icon } from "./People2"
|
||||
|
||||
@@ -174,12 +174,14 @@
|
||||
}
|
||||
|
||||
.peach80 *,
|
||||
.peach80 * {
|
||||
.baseTextMediumContrast * {
|
||||
fill: var(--Base-Text-Medium-contrast);
|
||||
}
|
||||
|
||||
.peach80:hover *,
|
||||
.peach80:active * {
|
||||
.peach80:active *,
|
||||
.baseTextMediumContrast:hover *,
|
||||
.baseTextMediumContrast:active * {
|
||||
fill: var(--Base-Text-High-contrast);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ const restaurantPriceSchema = z.object({
|
||||
currency: z.string(),
|
||||
amount: z.number(),
|
||||
})
|
||||
const restaurantDaySchema = z.object({
|
||||
export const restaurantDaySchema = z.object({
|
||||
sortOrder: z.number(),
|
||||
alwaysOpen: z.boolean(),
|
||||
isClosed: z.boolean(),
|
||||
openingTime: z.string(),
|
||||
closingTime: z.string(),
|
||||
})
|
||||
const restaurantOpeningHoursSchema = z.object({
|
||||
export const restaurantOpeningHoursSchema = z.object({
|
||||
isActive: z.boolean(),
|
||||
name: z.string().optional(),
|
||||
openingTime: z.string().optional(),
|
||||
@@ -52,7 +52,7 @@ export const restaurantSchema = z
|
||||
url: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
.default([]),
|
||||
openingDetails: z.array(restaurantOpeningDetailSchema),
|
||||
content: z.object({
|
||||
images: z.array(imageSchema),
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import type { RestaurantData } from "@/types/hotel"
|
||||
import type { RestaurantData, RestaurantOpeningHours } from "@/types/hotel"
|
||||
|
||||
export interface RestaurantBarSidePeekProps {
|
||||
restaurants: RestaurantData[]
|
||||
}
|
||||
|
||||
export interface RestaurantBarItemProps {
|
||||
restaurant: RestaurantData
|
||||
}
|
||||
|
||||
export interface RestaurantBarOpeningHoursProps {
|
||||
openingHours: RestaurantOpeningHours
|
||||
alternateOpeningHours?: RestaurantOpeningHours
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { RouterOutput } from "@/lib/trpc/client"
|
||||
|
||||
export interface RoomProps {
|
||||
booking: RouterOutput["booking"]["confirmation"]["booking"]
|
||||
img: NonNullable<
|
||||
RouterOutput["booking"]["confirmation"]["hotel"]["included"]
|
||||
>["rooms"][number]["images"][number]
|
||||
roomName: NonNullable<
|
||||
RouterOutput["booking"]["confirmation"]["hotel"]["included"]
|
||||
>["rooms"][number]["name"]
|
||||
}
|
||||
@@ -87,6 +87,8 @@ export enum IconName {
|
||||
Nature = "Nature",
|
||||
Nightlife = "Nightlife",
|
||||
NoSmoking = "NoSmoking",
|
||||
OpenInNew = "OpenInNew",
|
||||
OpenInNewSmall = "OpenInNewSmall",
|
||||
OutdoorFurniture = "OutdoorFurniture",
|
||||
Parking = "Parking",
|
||||
People2 = "People2",
|
||||
|
||||
@@ -8,10 +8,14 @@ import {
|
||||
pointOfInterestSchema,
|
||||
} from "@/server/routers/hotels/output"
|
||||
import { imageSchema } from "@/server/routers/hotels/schemas/image"
|
||||
import { restaurantSchema } from "@/server/routers/hotels/schemas/restaurants"
|
||||
import {
|
||||
restaurantDaySchema,
|
||||
restaurantOpeningHoursSchema,
|
||||
restaurantSchema,
|
||||
} from "@/server/routers/hotels/schemas/restaurants"
|
||||
import { roomSchema } from "@/server/routers/hotels/schemas/room"
|
||||
|
||||
export type HotelData = z.infer<typeof getHotelDataSchema>
|
||||
export type HotelData = z.output<typeof getHotelDataSchema>
|
||||
|
||||
export type Hotel = HotelData["data"]["attributes"]
|
||||
export type HotelAddress = HotelData["data"]["attributes"]["address"]
|
||||
@@ -24,7 +28,11 @@ export type HotelTripAdvisor =
|
||||
| undefined
|
||||
|
||||
export type RoomData = z.infer<typeof roomSchema>
|
||||
export type RestaurantData = z.infer<typeof restaurantSchema>
|
||||
export type RestaurantData = z.output<typeof restaurantSchema>
|
||||
export type RestaurantOpeningHours = z.output<
|
||||
typeof restaurantOpeningHoursSchema
|
||||
>
|
||||
export type RestaurantOpeningHoursDay = z.output<typeof restaurantDaySchema>
|
||||
export type GalleryImage = z.infer<typeof imageSchema>
|
||||
export type CheckInData = z.infer<typeof checkinSchema>
|
||||
|
||||
|
||||
23
utils/getBookedHotelRoom.ts
Normal file
23
utils/getBookedHotelRoom.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { RouterOutput } from "@/lib/trpc/client"
|
||||
|
||||
export function getBookedHotelRoom(
|
||||
hotel: RouterOutput["booking"]["confirmation"]["hotel"],
|
||||
roomTypeCode: string
|
||||
) {
|
||||
const room = hotel.included?.rooms?.find((include) => {
|
||||
return include.roomTypes.find((roomType) => roomType.code === roomTypeCode)
|
||||
})
|
||||
if (!room) {
|
||||
return null
|
||||
}
|
||||
const bedType = room.roomTypes.find(
|
||||
(roomType) => roomType.code === roomTypeCode
|
||||
)
|
||||
if (!bedType) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...room,
|
||||
bedType,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user