Merge branch 'develop' into feature/sw-561-design-fixes

This commit is contained in:
Linus Flood
2024-10-09 14:51:44 +02:00
72 changed files with 1215 additions and 298 deletions
+107
View File
@@ -0,0 +1,107 @@
"use client"
import {
flexRender,
getCoreRowModel,
getPaginationRowModel,
useReactTable,
} from "@tanstack/react-table"
import { useState } from "react"
import SectionContainer from "@/components/Section/Container"
import SectionHeader from "@/components/Section/Header"
import ScrollWrapper from "@/components/TempDesignSystem/ScrollWrapper"
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
import Table from "@/components/TempDesignSystem/Table"
import styles from "./table.module.css"
import type { TableBlockProps } from "@/types/components/blocks/table"
export default function TableBlock({ data }: TableBlockProps) {
const { columns, rows, totalWidth, heading, preamble } = data
const initialPageSize = 5
const [pageSize, setPageSize] = useState(initialPageSize)
const showMoreVisible = rows.length > initialPageSize
const showLessVisible = pageSize >= rows.length
const columnDefs = columns.map((col) => ({
accessorKey: col.id,
header: col.header,
size: col.width,
}))
const table = useReactTable({
columns: columnDefs,
data: rows,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
state: {
pagination: {
pageIndex: 0,
pageSize,
},
},
})
function handleShowMore() {
setPageSize(showLessVisible ? initialPageSize : rows.length)
}
return (
<SectionContainer>
{heading ? (
<SectionHeader preamble={data.preamble} title={heading} />
) : null}
<div className={styles.tableWrapper}>
<ScrollWrapper>
<Table
width={`${totalWidth}%`}
variant="content"
intent="striped"
layout="fixed"
borderRadius="none"
>
<Table.THead>
{table.getHeaderGroups().map((headerGroup) => (
<Table.TR key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<Table.TH
key={header.id}
width={`${header.column.columnDef.size}%`}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.TH>
))}
</Table.TR>
))}
</Table.THead>
<Table.TBody>
{table.getRowModel().rows.map((row) => (
<Table.TR key={row.id}>
{row.getVisibleCells().map((cell) => (
<Table.TD key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</Table.TD>
))}
</Table.TR>
))}
</Table.TBody>
</Table>
</ScrollWrapper>
{showMoreVisible ? (
<ShowMoreButton
loadMoreData={handleShowMore}
showLess={showLessVisible}
intent="table"
/>
) : null}
</div>
</SectionContainer>
)
}
+6
View File
@@ -0,0 +1,6 @@
.tableWrapper {
display: grid;
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Small);
overflow: hidden;
}
+4
View File
@@ -5,6 +5,8 @@ import TextCols from "@/components/Blocks/TextCols"
import UspGrid from "@/components/Blocks/UspGrid"
import JsonToHtml from "@/components/JsonToHtml"
import Table from "./Table"
import type { BlocksProps } from "@/types/components/blocks"
import { BlocksEnums } from "@/types/enums/blocks"
@@ -47,6 +49,8 @@ export default function Blocks({ blocks }: BlocksProps) {
title={block.shortcuts.title}
/>
)
case BlocksEnums.block.Table:
return <Table data={block.table} />
case BlocksEnums.block.TextCols:
return <TextCols text_cols={block.text_cols} />
case BlocksEnums.block.TextContent:
@@ -42,7 +42,7 @@
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
position: sticky;
top: 0;
z-index: 10000;
z-index: 9;
background-color: var(--Base-Surface-Primary-light-Normal);
}
@@ -40,6 +40,8 @@
}
.mainContent {
display: grid;
gap: var(--Spacing-x4);
width: 100%;
}
@@ -5,7 +5,7 @@ import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { KingBedIcon } from "@/components/Icons"
import RadioCard from "@/components/TempDesignSystem/Form/Card/Radio"
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
import { bedTypeSchema } from "./schema"
@@ -24,9 +24,7 @@ export default function BedType() {
reValidateMode: "onChange",
})
// @ts-expect-error - Types mismatch docs as this is
// a pattern that is allowed https://formatjs.io/docs/react-intl/api#usage
const text = intl.formatMessage(
const text = intl.formatMessage<React.ReactNode>(
{ id: "<b>Included</b> (based on availability)" },
{ b: (str) => <b>{str}</b> }
)
@@ -5,7 +5,7 @@ import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { BreakfastIcon, NoBreakfastIcon } from "@/components/Icons"
import RadioCard from "@/components/TempDesignSystem/Form/Card/Radio"
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
import { breakfastSchema } from "./schema"
@@ -31,9 +31,7 @@ export default function Breakfast() {
Icon={BreakfastIcon}
id={breakfastEnum.BREAKFAST}
name="breakfast"
// @ts-expect-error - Types mismatch docs as this is
// a pattern that is allowed https://formatjs.io/docs/react-intl/api#usage
subtitle={intl.formatMessage(
subtitle={intl.formatMessage<React.ReactNode>(
{ id: "<b>{amount} {currency}</b>/night per adult" },
{
amount: "150",
@@ -4,7 +4,7 @@ import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import Button from "@/components/TempDesignSystem/Button"
import CheckboxCard from "@/components/TempDesignSystem/Form/Card/Checkbox"
import CheckboxCard from "@/components/TempDesignSystem/Form/ChoiceCard/Checkbox"
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
import Input from "@/components/TempDesignSystem/Form/Input"
import Phone from "@/components/TempDesignSystem/Form/Phone"
@@ -88,8 +88,9 @@ export default function Details({ user }: DetailsProps) {
<footer className={styles.footer}>
{user ? null : (
<CheckboxCard
highlightSubtitle
list={list}
saving
name="join"
subtitle={intl.formatMessage(
{
id: "{difference}{amount} {currency}",
@@ -0,0 +1,77 @@
"use client"
import { useIntl } from "react-intl"
import { EditIcon, ImageIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import styles from "./selectedRoom.module.css"
export default function SelectedRoom() {
const intl = useIntl()
return (
<article className={styles.container}>
<div className={styles.tempImage}>
<ImageIcon
color="baseButtonTertiaryOnFillNormal"
height={60}
width={60}
/>
</div>
<div className={styles.content}>
<div className={styles.textContainer}>
<Footnote
className={styles.label}
color="uiTextPlaceholder"
textTransform="uppercase"
>
{intl.formatMessage({ id: "Your room" })}
</Footnote>
<div className={styles.text}>
{/**
* [TEMP]
* No translation on Subtitles as they will be derived
* from Room selection.
*/}
<Subtitle
className={styles.room}
color="uiTextHighContrast"
type="two"
>
Cozy cabin
</Subtitle>
<Subtitle
className={styles.invertFontWeight}
color="uiTextMediumContrast"
type="two"
>
Free rebooking
</Subtitle>
<Subtitle
className={styles.invertFontWeight}
color="uiTextMediumContrast"
type="two"
>
Pay now
</Subtitle>
</div>
</div>
<Button
asChild
intent="tertiary"
size="small"
theme="base"
variant="icon"
>
<Link href="#">
<EditIcon color="baseButtonTertiaryOnFillNormal" />
{intl.formatMessage({ id: "Modify" })}
</Link>
</Button>
</div>
</article>
)
}
@@ -0,0 +1,51 @@
.container {
background-color: var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Corner-radius-Large);
display: grid;
grid-template-columns: 144px 1fr;
gap: var(--Spacing-x3);
padding: var(--Spacing-x2) var(--Spacing-x4) var(--Spacing-x2)
var(--Spacing-x2);
}
.tempImage {
align-items: center;
background-color: lightgray;
border-radius: var(--Corner-radius-Medium);
display: flex;
height: auto;
justify-content: center;
min-height: 80px;
}
.content {
align-items: center;
display: grid;
gap: var(--Spacing-x3);
grid-template-columns: 1fr auto;
}
.textContainer {
display: grid;
}
.label {
grid-column: 1 / -1;
}
.text {
display: flex;
flex-wrap: wrap;
gap: var(--Spacing-x1);
}
p.invertFontWeight {
font-weight: 400;
}
.invertFontWeight:not(:last-of-type)::after,
.room::after {
color: var(--UI-Text-Medium-contrast);
content: "∙";
padding-left: var(--Spacing-x1);
}
@@ -1,10 +1,13 @@
.hotelSelectionHeader {
display: flex;
flex-direction: column;
background-color: var(--Base-Surface-Subtle-Normal);
padding: var(--Spacing-x3) var(--Spacing-x2);
justify-content: center;
}
.hotelSelectionHeaderWrapper {
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
justify-content: center;
}
.titleContainer {
@@ -33,9 +36,15 @@
@media (min-width: 768px) {
.hotelSelectionHeader {
flex-direction: row;
padding: var(--Spacing-x4) var(--Spacing-x5);
}
.hotelSelectionHeaderWrapper {
flex-direction: row;
gap: var(--Spacing-x6);
max-width: var(--max-width-navigation);
margin: 0 auto;
width: 100%;
}
.titleContainer > h1 {
@@ -19,35 +19,35 @@ export default function HotelSelectionHeader({
return (
<header className={styles.hotelSelectionHeader}>
<div className={styles.titleContainer}>
<Title as="h3" level="h1">
{hotel.name}
</Title>
<address className={styles.address}>
<Caption color="textMediumContrast">
{hotel.address.streetAddress}, {hotel.address.city}
</Caption>
<div>
<Divider variant="vertical" color="subtle" />
</div>
<Caption color="textMediumContrast">
{intl.formatMessage(
{
id: "Distance to city centre",
},
{ number: hotel.location.distanceToCentre }
)}
</Caption>
</address>
</div>
<div className={styles.dividerContainer}>
<Divider variant="vertical" color="subtle" />
</div>
<div className={styles.descriptionContainer}>
<Body color="textHighContrast">
{hotel.hotelContent.texts.descriptions.short}
</Body>
<HotelDetailSidePeek />
<div className={styles.hotelSelectionHeaderWrapper}>
<div className={styles.titleContainer}>
<Title as="h3" level="h1">
{hotel.name}
</Title>
<address className={styles.address}>
<Caption color="textMediumContrast">
{hotel.address.streetAddress}, {hotel.address.city}
</Caption>
<div>
<Divider variant="vertical" color="subtle" />
</div>
<Caption color="textMediumContrast">
{intl.formatMessage(
{ id: "Distance to city centre" },
{ number: hotel.location.distanceToCentre }
)}
</Caption>
</address>
</div>
<div className={styles.dividerContainer}>
<Divider variant="vertical" color="subtle" />
</div>
<div className={styles.descriptionContainer}>
<Body color="textHighContrast">
{hotel.hotelContent.texts.descriptions.short}
</Body>
<HotelDetailSidePeek />
</div>
</div>
</header>
)
@@ -129,7 +129,7 @@ export default function Payment({ hotel }: PaymentProps) {
name="payment-method"
id="card"
value="card"
checked={selectedPaymentMethod === "card"}
defaultChecked={selectedPaymentMethod === "card"}
/>
<label htmlFor="card">card</label>
</button>
@@ -145,7 +145,7 @@ export default function Payment({ hotel }: PaymentProps) {
name="payment-method"
id={paymentOption}
value={paymentOption}
checked={selectedPaymentMethod === paymentOption}
defaultChecked={selectedPaymentMethod === paymentOption}
/>
<label htmlFor={paymentOption}>{paymentOption}</label>
</button>
@@ -9,17 +9,31 @@ import styles from "./flexibilityOption.module.css"
import { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
export default function FlexibilityOption({
currency,
standardPrice,
memberPrice,
product,
name,
value,
paymentTerm,
}: FlexibilityOptionProps) {
const intl = useIntl()
if (!product) {
// TODO: Implement empty state when this rate can't be booked
return <div>TBI: Rate not available</div>
}
const { productType } = product
const { public: publicPrice, member: memberPrice } = productType
const { localPrice: publicLocalPrice, requestedPrice: publicRequestedPrice } =
publicPrice
const { localPrice: memberLocalPrice, requestedPrice: memberRequestedPrice } =
memberPrice
return (
<label>
<input type="radio" name="flexibility" value={value} />
<input
type="radio"
name="rateCode"
value={product.productType.public.rateCode}
/>
<div className={styles.card}>
<div className={styles.header}>
<Body>{name}</Body>
@@ -29,15 +43,27 @@ export default function FlexibilityOption({
<div>
<dt>{intl.formatMessage({ id: "Standard price" })}</dt>
<dd>
{standardPrice} {currency}
{publicLocalPrice.pricePerNight} {publicLocalPrice.currency}/
{intl.formatMessage({ id: "night" })}
</dd>
</div>
<div>
<dt>{intl.formatMessage({ id: "Member price" })}</dt>
<dd>
{memberPrice} {currency}
{memberLocalPrice.pricePerNight} {memberLocalPrice.currency}/
{intl.formatMessage({ id: "night" })}
</dd>
</div>
{publicRequestedPrice && memberRequestedPrice && (
<div>
<dt>{intl.formatMessage({ id: "Approx." })}</dt>
<dd>
{publicRequestedPrice.pricePerNight}/
{memberRequestedPrice.pricePerNight}{" "}
{publicRequestedPrice.currency}
</dd>
</div>
)}
</dl>
</div>
</label>
@@ -11,20 +11,54 @@ import styles from "./roomCard.module.css"
import { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
export default function RoomCard({
room,
nrOfAdults,
nrOfNights,
breakfastIncluded,
rateDefinitions,
roomConfiguration,
}: RoomCardProps) {
const intl = useIntl()
const saveRate = rateDefinitions.find(
// TODO: Update string when API has decided
(rate) => rate.cancellationRule === "NonCancellable"
)
const changeRate = rateDefinitions.find(
// TODO: Update string when API has decided
(rate) => rate.cancellationRule === "Modifiable"
)
const flexRate = rateDefinitions.find(
// TODO: Update string when API has decided
(rate) => rate.cancellationRule === "CancellableBefore6PM"
)
const saveProduct = saveRate
? roomConfiguration.products.find(
(product) =>
product.productType.public.rateCode === saveRate.rateCode ||
product.productType.member.rateCode === saveRate.rateCode
)
: undefined
const changeProduct = changeRate
? roomConfiguration.products.find(
(product) =>
product.productType.public.rateCode === changeRate.rateCode ||
product.productType.member.rateCode === changeRate.rateCode
)
: undefined
const flexProduct = flexRate
? roomConfiguration.products.find(
(product) =>
product.productType.public.rateCode === flexRate.rateCode ||
product.productType.member.rateCode === flexRate.rateCode
)
: undefined
return (
<div className={styles.card}>
<div className={styles.cardBody}>
<div className={styles.specification}>
<Subtitle className={styles.name} type="two">
{room.name}
{roomConfiguration.roomType}
</Subtitle>
<Caption>{room.size}</Caption>
<Caption>Room size TBI</Caption>
<Button intent="text" type="button" size="small" theme="base">
{intl.formatMessage({ id: "See room details" })}
</Button>
@@ -32,20 +66,15 @@ export default function RoomCard({
{/*TODO: Handle pluralisation*/}
{intl.formatMessage(
{
id: "Nr night, nr adult",
defaultMessage:
"{nights, number} night, {adults, number} adult",
id: "Max {nrOfGuests} guests",
defaultMessage: "Max {nrOfGuests} guests",
},
{ nights: nrOfNights, adults: nrOfAdults }
// TODO: Correct number
{ nrOfGuests: 2 }
)}
{" | "}
{breakfastIncluded
? intl.formatMessage({
id: "Breakfast included",
})
: intl.formatMessage({
id: "Breakfast excluded",
})}
{intl.formatMessage({
id: "Breakfast included",
})}
</Caption>
</div>
@@ -53,25 +82,19 @@ export default function RoomCard({
name={intl.formatMessage({ id: "Non-refundable" })}
value="non-refundable"
paymentTerm={intl.formatMessage({ id: "Pay now" })}
standardPrice={room.prices.nonRefundable.standard}
memberPrice={room.prices.nonRefundable.member}
currency={room.prices.currency}
product={saveProduct}
/>
<FlexibilityOption
name={intl.formatMessage({ id: "Free rebooking" })}
value="free-rebooking"
paymentTerm={intl.formatMessage({ id: "Pay now" })}
standardPrice={room.prices.freeRebooking.standard}
memberPrice={room.prices.freeRebooking.member}
currency={room.prices.currency}
product={changeProduct}
/>
<FlexibilityOption
name={intl.formatMessage({ id: "Free cancellation" })}
value="free-cancellation"
paymentTerm={intl.formatMessage({ id: "Pay later" })}
standardPrice={room.prices.freeCancellation.standard}
memberPrice={room.prices.freeCancellation.member}
currency={room.prices.currency}
product={flexProduct}
/>
<Button
@@ -87,7 +110,8 @@ export default function RoomCard({
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
alt={intl.formatMessage({ id: "A photo of the room" })}
src={room.imageSrc}
// TODO: Correct image URL
src="https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg"
/>
</div>
)
@@ -1,5 +1,8 @@
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import { useIntl } from "react-intl"
import Button from "@/components/TempDesignSystem/Button"
import RoomCard from "./RoomCard"
@@ -8,12 +11,11 @@ import styles from "./roomSelection.module.css"
import { RoomSelectionProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
export default function RoomSelection({
rates,
nrOfNights,
nrOfAdults,
roomConfigurations,
}: RoomSelectionProps) {
const router = useRouter()
const searchParams = useSearchParams()
const intl = useIntl()
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
@@ -25,31 +27,28 @@ export default function RoomSelection({
return (
<div className={styles.wrapper}>
<ul className={styles.roomList}>
{rates.map((room) => (
<li key={room.id}>
<form
method="GET"
action={`select-bed?${searchParams}`}
onSubmit={handleSubmit}
>
<input
type="hidden"
name="roomClass"
value={room.id}
id={`room-${room.id}`}
/>
<form
method="GET"
action={`select-bed?${searchParams}`}
onSubmit={handleSubmit}
>
<ul className={styles.roomList}>
{roomConfigurations.roomConfigurations.map((roomConfiguration) => (
<li key={roomConfiguration.roomType}>
<RoomCard
room={room}
nrOfAdults={nrOfAdults}
nrOfNights={nrOfNights}
breakfastIncluded={room.breakfastIncluded}
rateDefinitions={roomConfigurations.rateDefinitions}
roomConfiguration={roomConfiguration}
/>
</form>
</li>
))}
</ul>
<div className={styles.summary}>This is summary</div>
</li>
))}
</ul>
<div className={styles.summary}>
This is summary
<Button type="submit" size="small" theme="primaryDark">
{intl.formatMessage({ id: "Choose room" })}
</Button>
</div>
</form>
</div>
)
}
@@ -27,4 +27,6 @@
bottom: 0;
left: 0;
right: 0;
background-color: white;
padding: var(--Spacing-x3) var(--Spacing-x7) var(--Spacing-x5);
}
@@ -40,7 +40,7 @@
.iconWrapper {
position: relative;
top: var(--Spacing-x1);
z-index: 10;
z-index: 2;
}
.circle {
+5
View File
@@ -66,3 +66,8 @@
.blue * {
fill: var(--UI-Input-Controls-Fill-Selected);
}
.baseButtonTertiaryOnFillNormal,
.baseButtonTertiaryOnFillNormal * {
fill: var(--Base-Button-Tertiary-On-Fill-Normal);
}
+3 -2
View File
@@ -5,19 +5,20 @@ import styles from "./icon.module.css"
const config = {
variants: {
color: {
baseButtonTertiaryOnFillNormal: styles.baseButtonTertiaryOnFillNormal,
baseIconLowContrast: styles.baseIconLowContrast,
black: styles.black,
blue: styles.blue,
burgundy: styles.burgundy,
green: styles.green,
grey80: styles.grey80,
pale: styles.pale,
peach80: styles.peach80,
primaryLightOnSurfaceAccent: styles.plosa,
red: styles.red,
green: styles.green,
white: styles.white,
uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast,
blue: styles.blue,
},
},
defaultVariants: {
+3 -3
View File
@@ -49,10 +49,10 @@ export default function FullView({
className={styles.fullViewImage}
>
<Image
src={image.url}
alt={image.alt}
layout="fill"
objectFit="cover"
fill
src={image.url}
style={{ objectFit: "cover" }}
/>
<div className={styles.fullViewFooter}>
@@ -64,10 +64,10 @@ a.default {
justify-content: center;
}
.icon {
.btn.icon:is(.small, .medium, .large) {
align-items: center;
display: flex;
gap: var(--Spacing-x-half);
gap: var(--Spacing-x1);
justify-content: center;
}
@@ -76,7 +76,8 @@ a.default {
font-size: var(--typography-Caption-Bold-fontSize);
line-height: var(--typography-Caption-Bold-lineHeight);
gap: var(--Spacing-x-quarter);
padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2); /* Special case padding to adjust the missing border */
padding: calc(var(--Spacing-x1) + 2px) var(--Spacing-x2);
/* Special case padding to adjust the missing border */
}
.btn.small.secondary {
@@ -85,7 +86,8 @@ a.default {
.btn.medium {
gap: var(--Spacing-x-half);
padding: calc(var(--Spacing-x-one-and-half) + 2px) var(--Spacing-x2); /* Special case padding to adjust the missing border */
padding: calc(var(--Spacing-x-one-and-half) + 2px) var(--Spacing-x2);
/* Special case padding to adjust the missing border */
}
.medium.secondary {
@@ -94,7 +96,8 @@ a.default {
.btn.large {
gap: var(--Spacing-x-half);
padding: calc(var(--Spacing-x2) + 2px) var(--Spacing-x3); /* Special case padding to adjust the missing border */
padding: calc(var(--Spacing-x2) + 2px) var(--Spacing-x3);
/* Special case padding to adjust the missing border */
}
.large.secondary {
@@ -1,6 +1,6 @@
import Card from "."
import Card from "./_Card"
import type { CheckboxProps } from "./card"
import type { CheckboxProps } from "./_Card/card"
export default function CheckboxCard(props: CheckboxProps) {
return <Card {...props} type="checkbox" />
@@ -1,6 +1,6 @@
import Card from "."
import Card from "./_Card"
import type { RadioProps } from "./card"
import type { RadioProps } from "./_Card/card"
export default function RadioCard(props: RadioProps) {
return <Card {...props} type="radio" />
@@ -1,14 +1,15 @@
import type { IconProps } from "@/types/components/icon"
interface BaseCardProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
interface BaseCardProps
extends Omit<React.LabelHTMLAttributes<HTMLLabelElement>, "title"> {
Icon?: (props: IconProps) => JSX.Element
declined?: boolean
highlightSubtitle?: boolean
iconHeight?: number
iconWidth?: number
name?: string
saving?: boolean
subtitle?: string
title: string
name: string
subtitle?: React.ReactNode
title: React.ReactNode
type: "checkbox" | "radio"
value?: string
}
@@ -22,7 +23,7 @@ interface ListCardProps extends BaseCardProps {
interface TextCardProps extends BaseCardProps {
list?: never
text: string
text: React.ReactNode
}
export type CardProps = ListCardProps | TextCardProps
@@ -13,10 +13,10 @@ export default function Card({
iconHeight = 32,
iconWidth = 32,
declined = false,
highlightSubtitle = false,
id,
list,
name = "join",
saving = false,
name,
subtitle,
text,
title,
@@ -31,7 +31,7 @@ export default function Card({
{subtitle ? (
<Caption
className={styles.subtitle}
color={saving ? "baseTextAccent" : "uiTextHighContrast"}
color={highlightSubtitle ? "baseTextAccent" : "uiTextHighContrast"}
textTransform="bold"
>
{subtitle}
@@ -0,0 +1,36 @@
"use client"
import { useMemo } from "react"
import useScrollShadows from "@/hooks/useScrollShadows"
import styles from "./scrollWrapper.module.css"
import type { ScrollWrapperProps } from "./scrollWrapper"
export default function ScrollWrapper({
className,
children,
}: ScrollWrapperProps) {
const { containerRef, showLeftShadow, showRightShadow } =
useScrollShadows<HTMLDivElement>()
const classNames = useMemo(() => {
const cls = [styles.scrollWrapper, className]
if (showLeftShadow) {
cls.push(styles.leftShadow)
}
if (showRightShadow) {
cls.push(styles.rightShadow)
}
return cls.join(" ")
}, [showLeftShadow, showRightShadow, className])
return (
<div className={classNames}>
<div className={styles.content} ref={containerRef}>
{children}
</div>
</div>
)
}
@@ -0,0 +1,33 @@
.scrollWrapper {
position: relative;
overflow: hidden;
}
.scrollWrapper::before,
.scrollWrapper::after {
content: "";
position: absolute;
top: 0;
height: 100%;
pointer-events: none;
z-index: 1;
transition: opacity 0.2s ease;
opacity: 0;
width: 50px;
}
.scrollWrapper.leftShadow::before {
left: 0;
background: linear-gradient(to right, rgba(0, 0, 0, 0.3), transparent);
opacity: 1;
}
.scrollWrapper.rightShadow::after {
right: 0;
background: linear-gradient(to left, rgba(0, 0, 0, 0.3), transparent);
opacity: 1;
}
.content {
overflow-x: auto;
}
@@ -0,0 +1,2 @@
export interface ScrollWrapperProps
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>> {}
@@ -1,4 +0,0 @@
.container {
display: flex;
justify-content: center;
}
@@ -5,18 +5,29 @@ import { useIntl } from "react-intl"
import { ChevronDownIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import styles from "./button.module.css"
import { showMoreButtonVariants } from "./variants"
import type { ShowMoreButtonParams } from "@/types/components/myPages/stays/button"
import styles from "./showMoreButton.module.css"
import type { ShowMoreButtonProps } from "./showMoreButton"
export default function ShowMoreButton({
className,
intent,
disabled,
showLess,
loadMoreData,
}: ShowMoreButtonParams) {
const { formatMessage } = useIntl()
}: ShowMoreButtonProps) {
const intl = useIntl()
const classNames = showMoreButtonVariants({
className,
intent,
})
return (
<div className={styles.container}>
<div className={`${classNames} ${showLess ? styles.showLess : ""}`}>
<Button
className={styles.button}
disabled={disabled}
onClick={loadMoreData}
variant="icon"
@@ -24,8 +35,8 @@ export default function ShowMoreButton({
theme="base"
intent="text"
>
<ChevronDownIcon />
{formatMessage({ id: "Show more" })}
<ChevronDownIcon className={styles.icon} />
{intl.formatMessage({ id: showLess ? "Show less" : "Show more" })}
</Button>
</div>
)
@@ -0,0 +1,23 @@
.container {
display: flex;
justify-content: center;
}
.table {
display: grid;
justify-content: stretch;
border-top: 1px solid var(--Base-Border-Subtle);
background-color: var(--Base-Surface-Primary-light-Normal);
}
.table .button {
border-radius: 0;
}
.icon {
transition: transform 0.3s;
}
.showLess .icon {
transform: rotate(180deg);
}
@@ -0,0 +1,11 @@
import { showMoreButtonVariants } from "./variants"
import type { VariantProps } from "class-variance-authority"
export interface ShowMoreButtonProps
extends React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>,
VariantProps<typeof showMoreButtonVariants> {
disabled?: boolean
showLess?: boolean
loadMoreData: () => void
}
@@ -0,0 +1,11 @@
import { cva } from "class-variance-authority"
import styles from "./showMoreButton.module.css"
export const showMoreButtonVariants = cva(styles.container, {
variants: {
intent: {
table: styles.table,
},
},
})
+8 -2
View File
@@ -1,7 +1,13 @@
import styles from "./table.module.css"
function TH({ children }: React.PropsWithChildren) {
return <th className={styles.th}>{children}</th>
import type { THeadProps } from "./table"
function TH({ children, width = "auto", ...props }: THeadProps) {
return (
<th className={styles.th} style={{ width }} {...props}>
{children}
</th>
)
}
export default TH
+25 -3
View File
@@ -3,11 +3,33 @@ import TD from "./TD"
import TH from "./TH"
import THead from "./THead"
import TR from "./TR"
import { tableVariants } from "./variants"
import styles from "./table.module.css"
import type { TableProps } from "./table"
function Table({ children }: React.PropsWithChildren) {
return <table className={styles.table}>{children}</table>
function Table({
className,
intent,
borderRadius,
variant,
layout,
width = "100%",
children,
...props
}: TableProps) {
const classNames = tableVariants({
className,
borderRadius,
intent,
layout,
variant,
})
return (
<table className={classNames} style={{ width }} {...props}>
{children}
</table>
)
}
Table.THead = THead
@@ -1,20 +1,20 @@
.table {
border-radius: var(--Corner-radius-Medium);
border-collapse: collapse;
overflow: hidden;
width: 100%;
min-width: 100%;
}
.thead {
background-color: var(--Base-Background-Secondary-Normal, #f7e1d5);
color: var(--Base-Text-High-contrast);
background-color: var(--Base-Surface-Primary-dark-Normal);
}
.tbody {
background-color: var(--Base-Surface-Primary-light-Normal, #fff);
background-color: var(--Base-Surface-Primary-light-Normal);
}
.tr:not(:last-of-type) {
border-bottom: 1px solid var(--Primary-Light-On-Surface-Divider, #f0c1b6);
border-bottom: 1px solid var(--Base-Border-Subtle);
}
.th {
@@ -28,6 +28,35 @@
padding: var(--Spacing-x2);
}
.fixed {
table-layout: fixed;
}
.smallRadius {
border-radius: var(--Corner-radius-Small);
}
.mediumRadius {
border-radius: var(--Corner-radius-Medium);
}
.largeRadius {
border-radius: var(--Corner-radius-Large);
}
.content .thead {
background-color: var(--Base-Surface-Subtle-Hover);
}
.content .tbody {
background-color: var(--Base-Background-Primary-Normal);
}
.content.striped .tbody .tr:nth-child(odd) {
background-color: var(--Base-Surface-Subtle-Normal);
}
.content.striped .tbody .tr:nth-child(even) {
background-color: var(--Base-Background-Primary-Normal);
}
@media screen and (min-width: 768px) {
.th {
padding: var(--Spacing-x2) var(--Spacing-x3);
@@ -0,0 +1,14 @@
import { tableVariants } from "./variants"
import type { VariantProps } from "class-variance-authority"
export interface TableProps
extends React.PropsWithChildren<React.HTMLAttributes<HTMLTableElement>>,
VariantProps<typeof tableVariants> {
width?: string
}
export interface THeadProps
extends React.PropsWithChildren<React.HTMLAttributes<HTMLTableCellElement>> {
width?: string
}
@@ -0,0 +1,27 @@
import { cva } from "class-variance-authority"
import styles from "./table.module.css"
export const tableVariants = cva(styles.table, {
variants: {
intent: {
light: styles.light,
striped: styles.striped,
},
variant: {
content: styles.content,
},
borderRadius: {
none: "",
small: styles.smallRadius,
medium: styles.mediumRadius,
large: styles.largeRadius,
},
layout: {
fixed: styles.fixed,
},
},
defaultVariants: {
borderRadius: "medium",
},
})
@@ -10,7 +10,7 @@
0.3vw + 15px,
var(--typography-Subtitle-1-Desktop-fontSize)
);
font-weight: 600;
font-weight: 500;
letter-spacing: var(--typography-Subtitle-1-letterSpacing);
line-height: var(--typography-Subtitle-1-lineHeight);
}
@@ -22,7 +22,7 @@
0.3vw + 15px,
var(--typography-Subtitle-2-Desktop-fontSize)
);
font-weight: 600;
font-weight: 500;
letter-spacing: var(--typography-Subtitle-2-letterSpacing);
line-height: var(--typography-Subtitle-2-lineHeight);
}
@@ -62,3 +62,7 @@
.uiTextHighContrast {
color: var(--UI-Text-High-contrast);
}
.uiTextMediumContrast {
color: var(--UI-Text-Medium-contrast);
}
@@ -9,6 +9,7 @@ const config = {
burgundy: styles.burgundy,
pale: styles.pale,
uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast,
},
textAlign: {
center: styles.center,