Merged in monorepo-step-1 (pull request #1080)
Migrate to a monorepo setup - step 1 * Move web to subfolder /apps/scandic-web * Yarn + transitive deps - Move to yarn - design-system package removed for now since yarn doesn't support the parameter for token (ie project currently broken) - Add missing transitive dependencies as Yarn otherwise prevents these imports - VS Code doesn't pick up TS path aliases unless you open /apps/scandic-web instead of root (will be fixed with monorepo) * Pin framer-motion to temporarily fix typing issue https://github.com/adobe/react-spectrum/issues/7494 * Pin zod to avoid typ error There seems to have been a breaking change in the types returned by zod where error is now returned as undefined instead of missing in the type. We should just handle this but to avoid merge conflicts just pin the dependency for now. * Pin react-intl version Pin version of react-intl to avoid tiny type issue where formatMessage does not accept a generic any more. This will be fixed in a future commit, but to avoid merge conflicts just pin for now. * Pin typescript version Temporarily pin version as newer versions as stricter and results in a type error. Will be fixed in future commit after merge. * Setup workspaces * Add design-system as a monorepo package * Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN * Fix husky for monorepo setup * Update netlify.toml * Add lint script to root package.json * Add stub readme * Fix react-intl formatMessage types * Test netlify.toml in root * Remove root toml * Update netlify.toml publish path * Remove package-lock.json * Update build for branch/preview builds Approved-by: Linus Flood
This commit is contained in:
committed by
Linus Flood
parent
667cab6fb6
commit
80100e7631
@@ -0,0 +1,45 @@
|
||||
.imagePlaceholder {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #000000 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
||||
background-size: 120px 120px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 60px,
|
||||
60px -60px,
|
||||
-60px 0;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
position: relative;
|
||||
min-width: 177px;
|
||||
border-radius: var(--Corner-radius-Medium) 0 0 var(--Corner-radius-Medium);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.imageContainer.top {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
height: 90px;
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
|
||||
.imageContainer img {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.imageContainer .tripAdvisor {
|
||||
position: absolute;
|
||||
left: 7px;
|
||||
top: 7px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.imageContainer.top .tripAdvisor {
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TripAdvisorIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Chip from "@/components/TempDesignSystem/Chip"
|
||||
|
||||
import { hotelCardDialogImageVariants } from "./variants"
|
||||
|
||||
import styles from "./hotelCardDialogImage.module.css"
|
||||
|
||||
import type { HotelCardDialogImageProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
export default function HotelCardDialogImage({
|
||||
firstImage,
|
||||
altText,
|
||||
ratings,
|
||||
imageError,
|
||||
setImageError,
|
||||
position,
|
||||
}: HotelCardDialogImageProps) {
|
||||
const classNames = hotelCardDialogImageVariants({ position })
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{!firstImage || imageError ? (
|
||||
<div className={styles.imagePlaceholder} />
|
||||
) : (
|
||||
<Image
|
||||
src={firstImage}
|
||||
alt={altText || ""}
|
||||
fill
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.tripAdvisor}>
|
||||
<Chip className={styles.tripAdvisor}>
|
||||
<TripAdvisorIcon color="burgundy" />
|
||||
{ratings}
|
||||
</Chip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./hotelCardDialogImage.module.css"
|
||||
|
||||
export const hotelCardDialogImageVariants = cva(styles.imageContainer, {
|
||||
variants: {
|
||||
position: {
|
||||
top: styles.top,
|
||||
left: styles.left,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
position: "top",
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,128 @@
|
||||
"use client"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { isValidClientSession } from "@/utils/clientSession"
|
||||
|
||||
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
|
||||
import HotelCardDialogImage from "../HotelCardDialogImage"
|
||||
|
||||
import styles from "../hotelCardDialog.module.css"
|
||||
|
||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
interface ListingHotelCardProps {
|
||||
data: HotelPin
|
||||
lang: Lang
|
||||
imageError: boolean
|
||||
setImageError: (error: boolean) => void
|
||||
}
|
||||
|
||||
export default function ListingHotelCardDialog({
|
||||
data,
|
||||
lang,
|
||||
imageError,
|
||||
setImageError,
|
||||
}: ListingHotelCardProps) {
|
||||
const intl = useIntl()
|
||||
const { data: session } = useSession()
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
const {
|
||||
name,
|
||||
publicPrice,
|
||||
memberPrice,
|
||||
currency,
|
||||
amenities,
|
||||
images,
|
||||
ratings,
|
||||
operaId,
|
||||
} = data
|
||||
|
||||
const firstImage = images[0]?.imageSizes?.small
|
||||
const altText = images[0]?.metaData?.altText
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
<HotelCardDialogImage
|
||||
firstImage={firstImage}
|
||||
altText={altText}
|
||||
ratings={ratings || 0}
|
||||
imageError={imageError}
|
||||
setImageError={setImageError}
|
||||
position="top"
|
||||
/>
|
||||
<div className={styles.detailsContainer}>
|
||||
<div className={styles.name}>
|
||||
<Subtitle type="two">{name}</Subtitle>
|
||||
</div>
|
||||
<div className={styles.facilities}>
|
||||
{amenities.map((facility) => {
|
||||
const IconComponent = mapFacilityToIcon(facility.id)
|
||||
return (
|
||||
<div className={styles.facilitiesItem} key={facility.id}>
|
||||
{IconComponent && (
|
||||
<IconComponent width={20} height={20} color="grey80" />
|
||||
)}
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{facility.name}
|
||||
</Caption>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{publicPrice || memberPrice ? (
|
||||
<div className={styles.bottomContainer}>
|
||||
<div className={styles.pricesContainer}>
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage({ id: "Per night from" })}
|
||||
</Caption>
|
||||
<div className={styles.listingPrices}>
|
||||
{publicPrice && !isUserLoggedIn && (
|
||||
<>
|
||||
<Subtitle type="two">
|
||||
{publicPrice} {currency}
|
||||
</Subtitle>
|
||||
{memberPrice && <Caption>/</Caption>}
|
||||
</>
|
||||
)}
|
||||
{memberPrice && (
|
||||
<Subtitle type="two" color="red" className={styles.memberPrice}>
|
||||
{intl.formatMessage(
|
||||
{ id: "{price} {currency}" },
|
||||
{
|
||||
price: memberPrice,
|
||||
currency,
|
||||
}
|
||||
)}
|
||||
</Subtitle>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button asChild theme="base" size="small" className={styles.button}>
|
||||
<Link
|
||||
href={`${selectRate(lang)}?hotel=${operaId}`}
|
||||
color="none"
|
||||
keepSearchParams
|
||||
>
|
||||
{intl.formatMessage({ id: "See rooms" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<NoPriceAvailableCard />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
"use client"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { isValidClientSession } from "@/utils/clientSession"
|
||||
|
||||
import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard"
|
||||
import HotelCardDialogImage from "../HotelCardDialogImage"
|
||||
|
||||
import styles from "../hotelCardDialog.module.css"
|
||||
|
||||
import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
interface StandaloneHotelCardProps {
|
||||
data: HotelPin
|
||||
lang: Lang
|
||||
imageError: boolean
|
||||
setImageError: (error: boolean) => void
|
||||
}
|
||||
|
||||
export default function StandaloneHotelCardDialog({
|
||||
data,
|
||||
lang,
|
||||
imageError,
|
||||
setImageError,
|
||||
}: StandaloneHotelCardProps) {
|
||||
const intl = useIntl()
|
||||
const { data: session } = useSession()
|
||||
const isUserLoggedIn = isValidClientSession(session)
|
||||
const {
|
||||
name,
|
||||
publicPrice,
|
||||
memberPrice,
|
||||
currency,
|
||||
amenities,
|
||||
images,
|
||||
ratings,
|
||||
operaId,
|
||||
} = data
|
||||
|
||||
const firstImage = images[0]?.imageSizes?.small
|
||||
const altText = images[0]?.metaData?.altText
|
||||
|
||||
return (
|
||||
<>
|
||||
<HotelCardDialogImage
|
||||
firstImage={firstImage}
|
||||
altText={altText}
|
||||
ratings={ratings || 0}
|
||||
imageError={imageError}
|
||||
setImageError={setImageError}
|
||||
position="left"
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.detailsContainer}>
|
||||
<div className={styles.name}>
|
||||
<Body textTransform="bold">{name}</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.facilities}>
|
||||
{amenities.slice(0, 3).map((facility) => {
|
||||
const IconComponent = mapFacilityToIcon(facility.id)
|
||||
return (
|
||||
<div className={styles.facilitiesItem} key={facility.id}>
|
||||
{IconComponent && (
|
||||
<IconComponent width={16} height={16} color="grey80" />
|
||||
)}
|
||||
<Footnote color="uiTextMediumContrast">
|
||||
{facility.name}
|
||||
</Footnote>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.pricesContainer}>
|
||||
{publicPrice || memberPrice ? (
|
||||
<>
|
||||
<div className={styles.priceCard}>
|
||||
<Caption type="bold">
|
||||
{intl.formatMessage({ id: "From" })}
|
||||
</Caption>
|
||||
{publicPrice && !isUserLoggedIn && (
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{ id: "{price} {currency}" },
|
||||
{
|
||||
price: publicPrice,
|
||||
currency,
|
||||
}
|
||||
)}
|
||||
<Body asChild>
|
||||
<span>/{intl.formatMessage({ id: "night" })}</span>
|
||||
</Body>
|
||||
</Subtitle>
|
||||
)}
|
||||
{memberPrice && (
|
||||
<Subtitle
|
||||
type="two"
|
||||
color="red"
|
||||
className={styles.memberPrice}
|
||||
>
|
||||
{intl.formatMessage(
|
||||
{ id: "{price} {currency}" },
|
||||
{
|
||||
price: memberPrice,
|
||||
currency,
|
||||
}
|
||||
)}
|
||||
<Body asChild color="red">
|
||||
<span>/{intl.formatMessage({ id: "night" })}</span>
|
||||
</Body>
|
||||
</Subtitle>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
theme="base"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
>
|
||||
<Link
|
||||
href={`${selectRate(lang)}?hotel=${operaId}`}
|
||||
color="none"
|
||||
keepSearchParams
|
||||
>
|
||||
{intl.formatMessage({ id: "See rooms" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<NoPriceAvailableCard />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
.dialog {
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dialogContainer {
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
min-width: 402px;
|
||||
background: var(--Base-Surface-Primary-light-Normal);
|
||||
box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1);
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dialogContainer[data-type="listing"] {
|
||||
min-width: 358px;
|
||||
}
|
||||
|
||||
.dialogContainer[data-type="listing"] .header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.name {
|
||||
height: 48px;
|
||||
max-width: 180px;
|
||||
margin-bottom: var(--Spacing-x-half);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
.content {
|
||||
width: 100%;
|
||||
min-width: 220px;
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dialogContainer[data-type="listing"] .content {
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.facilities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.dialogContainer[data-type="listing"] .facilities {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
padding-right: 20px;
|
||||
gap: 0 var(--Spacing-x1);
|
||||
max-width: 242px;
|
||||
}
|
||||
.dialogContainer[data-type="listing"] .facilities::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
rgba(255, 255, 255, 1),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.facilitiesItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.dialogContainer[data-type="listing"] .facilitiesItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.priceCard {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
background: var(--Base-Surface-Secondary-light-Normal);
|
||||
margin-top: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.prices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bottomContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
||||
padding-top: var(--Spacing-x2);
|
||||
padding-bottom: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.pricesContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dialogContainer[data-type="listing"] .pricesContainer {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
gap: 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.listingPrices {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.perNight {
|
||||
color: var(--Base-Text-Subtle-light-Normal);
|
||||
}
|
||||
|
||||
.content .button {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.dialog {
|
||||
bottom: 32px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client"
|
||||
|
||||
import { useParams } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
|
||||
import { CloseLargeIcon } from "@/components/Icons"
|
||||
|
||||
import ListingHotelCardDialog from "./ListingHotelCardDialog"
|
||||
import StandaloneHotelCardDialog from "./StandaloneHotelCardDialog"
|
||||
|
||||
import styles from "./hotelCardDialog.module.css"
|
||||
|
||||
import type { HotelCardDialogProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
export default function HotelCardDialog({
|
||||
data,
|
||||
isOpen,
|
||||
type = "standalone",
|
||||
handleClose,
|
||||
}: HotelCardDialogProps) {
|
||||
const params = useParams()
|
||||
const lang = params.lang as Lang
|
||||
const [imageError, setImageError] = useState(false)
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<dialog open={isOpen} className={styles.dialog}>
|
||||
<div className={styles.dialogContainer} data-type={type}>
|
||||
<CloseLargeIcon
|
||||
onClick={handleClose}
|
||||
className={styles.closeIcon}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
{type === "standalone" ? (
|
||||
<StandaloneHotelCardDialog
|
||||
data={data}
|
||||
lang={lang}
|
||||
imageError={imageError}
|
||||
setImageError={setImageError}
|
||||
/>
|
||||
) : (
|
||||
<ListingHotelCardDialog
|
||||
data={data}
|
||||
lang={lang}
|
||||
imageError={imageError}
|
||||
setImageError={setImageError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user