feat: loosen up the zod validations and return null instead of throwing
This commit is contained in:
@@ -12,6 +12,11 @@ export default async function MyPages({
|
||||
}: PageArgs<LangParams & { path: string[] }>) {
|
||||
const accountPage = await serverClient().contentstack.accountPage.get()
|
||||
const { formatMessage } = await getIntl()
|
||||
|
||||
if (!accountPage) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={styles.blocks}>
|
||||
{accountPage.content.length ? (
|
||||
|
||||
@@ -4,6 +4,8 @@ import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
|
||||
|
||||
export default async function LanguageSwitcherRoute() {
|
||||
const data = await serverClient().contentstack.languageSwitcher.get()
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return <LanguageSwitcher urls={data.urls} lang={data.lang} />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ReactNode } from "react"
|
||||
|
||||
import Header from "@/components/Current/Header"
|
||||
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
@@ -7,15 +5,8 @@ import { LangParams, PageArgs } from "@/types/params"
|
||||
export default function HeaderLayout({
|
||||
params,
|
||||
languageSwitcher,
|
||||
children,
|
||||
}: PageArgs<LangParams> & {
|
||||
languageSwitcher: ReactNode
|
||||
children: ReactNode
|
||||
languageSwitcher: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Header lang={params.lang} languageSwitcher={languageSwitcher} />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
return <Header lang={params.lang} languageSwitcher={languageSwitcher} />
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
|
||||
|
||||
export default async function LanguageSwitcherRoute() {
|
||||
const data = await serverClient().contentstack.languageSwitcher.get()
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return <LanguageSwitcher urls={data.urls} lang={data.lang} />
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ import type { LangParams } from "@/types/params"
|
||||
|
||||
export default async function LoyaltyPage({ lang }: LangParams) {
|
||||
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get()
|
||||
if (!loyaltyPage) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<section className={styles.content}>
|
||||
{loyaltyPage.sidebar.length ? (
|
||||
|
||||
@@ -14,6 +14,9 @@ import { LangParams } from "@/types/params"
|
||||
|
||||
export default async function MyPages({ lang }: LangParams) {
|
||||
const accountPage = await serverClient().contentstack.accountPage.get()
|
||||
if (!accountPage) {
|
||||
return null
|
||||
}
|
||||
|
||||
const linkToOverview = `/${lang}/webview${accountPage.url}` !== overview[lang]
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ import { LangParams } from "@/types/params"
|
||||
|
||||
export default async function AboutScandicFriends({ lang }: LangParams) {
|
||||
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get()
|
||||
if (!loyaltyPage) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<section className={styles.content}>
|
||||
<LinkToOverview lang={lang} />
|
||||
|
||||
@@ -10,6 +10,9 @@ import { LangParams } from "@/types/params"
|
||||
|
||||
export default async function Footer({ lang }: LangParams) {
|
||||
const footerData = await serverClient().contentstack.base.footer({ lang })
|
||||
if (!footerData) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<footer className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ export default async function Header({
|
||||
})
|
||||
const session = await auth()
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
const homeHref = homeHrefs[env.NODE_ENV][lang]
|
||||
const { frontpage_link_text, logo, menu, top_menu } = data
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ async function DynamicComponentBlock({ component }: DynamicComponentProps) {
|
||||
const session = await auth()
|
||||
|
||||
const user = session ? await serverClient().user.get() : null
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (component) {
|
||||
case LoyaltyComponentEnum.how_it_works:
|
||||
|
||||
@@ -10,6 +10,9 @@ import type { ContactRowProps } from "@/types/components/loyalty/sidebar"
|
||||
|
||||
export default async function ContactRow({ contact }: ContactRowProps) {
|
||||
const data = await serverClient().contentstack.base.contact()
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
const val = getValueFromContactConfig(contact.contact_field, data)
|
||||
|
||||
|
||||
@@ -30,15 +30,19 @@ export default async function JoinLoyaltyContact({
|
||||
{block.preamble ? (
|
||||
<Body textAlign="center">{block.preamble}</Body>
|
||||
) : null}
|
||||
<Button asChild className={styles.link} intent="primary">
|
||||
<Link href="#">{formatMessage({ id: "Join Scandic Friends" })}</Link>
|
||||
<Button asChild intent="primary">
|
||||
<Body asChild fontOnly textAlign="center" textTransform="bold">
|
||||
<Link href={login[lang]}>
|
||||
{formatMessage({ id: "Join Scandic Friends" })}
|
||||
</Link>
|
||||
</Body>
|
||||
</Button>
|
||||
<Link href={login[lang]}>
|
||||
<Footnote textAlign="center" textTransform="bold">
|
||||
<Footnote asChild fontOnly textAlign="center" textTransform="bold">
|
||||
<Link color="burgundy" href={`/${lang}/login`}>
|
||||
{formatMessage({ id: "Already a friend?" })} <br />
|
||||
{formatMessage({ id: "Click here to log in" })}
|
||||
</Footnote>
|
||||
</Link>
|
||||
</Link>
|
||||
</Footnote>
|
||||
</article>
|
||||
{block.contact ? <Contact contactBlock={block.contact} /> : null}
|
||||
</section>
|
||||
|
||||
@@ -10,12 +10,3 @@
|
||||
gap: var(--Spacing-x5);
|
||||
padding: var(--Spacing-x4) var(--Spacing-x2) var(--Spacing-x5);
|
||||
}
|
||||
|
||||
/* TODO: Remove when we get proper button variables */
|
||||
.link {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||
letter-spacing: var(--typography-Body-Bold-letterSpacing);
|
||||
line-height: var(--typography-Body-Bold-lineHeight);
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ export default function SidebarLoyalty({
|
||||
return (
|
||||
<JoinLoyaltyContact
|
||||
block={block.join_loyalty_contact}
|
||||
key={`${block.__typename}-${idx}`}
|
||||
lang={lang}
|
||||
key={`${block.join_loyalty_contact.title}-${idx}`}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
|
||||
@@ -18,4 +18,4 @@
|
||||
justify-content: center;
|
||||
min-height: 280px;
|
||||
padding: var(--Spacing-x7) var(--Spacing-x3);
|
||||
}
|
||||
}
|
||||
@@ -17,29 +17,6 @@
|
||||
grid-area: header;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--some-black-color, #000);
|
||||
font-family: var(--typography-Script-1-fontFamily);
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.8rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--some-black-color, #000);
|
||||
font-family: var(--typography-Title-1-fontFamily);
|
||||
font-size: 1.6rem;
|
||||
font-weight: 900;
|
||||
inline-size: 18rem;
|
||||
line-height: 1.8rem;
|
||||
margin: 0;
|
||||
overflow-wrap: break-word;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
@@ -121,17 +98,6 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.journey .subtitle {
|
||||
font-size: 2.6rem;
|
||||
line-height: 3.2rem;
|
||||
}
|
||||
|
||||
.journey .title {
|
||||
font-size: 2.6rem;
|
||||
inline-size: 25rem;
|
||||
line-height: 3.2rem;
|
||||
}
|
||||
|
||||
.victories {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: var(--card-height) 1fr 1fr;
|
||||
@@ -139,20 +105,11 @@
|
||||
|
||||
.circle {
|
||||
align-items: center;
|
||||
background-color: var(--some-white-color, #fff);
|
||||
background-color: var(--Main-Grey-White);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.victory .subtitle {
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.6rem;
|
||||
}
|
||||
|
||||
.victory .title {
|
||||
inline-size: 13rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Image from "@/components/Image"
|
||||
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
@@ -25,8 +26,10 @@ export default async function Challenges({
|
||||
<section className={styles.journeys}>
|
||||
{journeys.map((journey) => (
|
||||
<article className={styles.journey} key={journey.title}>
|
||||
<p className={styles.subtitle}>{journey.tag}</p>
|
||||
<h4 className={styles.title}>{journey.title}</h4>
|
||||
<BiroScript color="black">{journey.tag}</BiroScript>
|
||||
<Title as="h5" level="h4">
|
||||
{journey.title}
|
||||
</Title>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
@@ -48,8 +51,10 @@ export default async function Challenges({
|
||||
width={12}
|
||||
/>
|
||||
</div>
|
||||
<p className={styles.subtitle}>{victory.tag}</p>
|
||||
<h4 className={styles.title}>{victory.title}</h4>
|
||||
<BiroScript color="black">{victory.tag}</BiroScript>
|
||||
<Title as="h5" level="h4">
|
||||
{victory.title}
|
||||
</Title>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
@@ -16,6 +16,9 @@ export default async function Overview({
|
||||
title,
|
||||
}: AccountPageComponentProps) {
|
||||
const user = await serverClient().user.get()
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<Header link={link} subtitle={subtitle} title={title} topTitle />
|
||||
|
||||
@@ -16,16 +16,19 @@ async function CurrentPointsBalance({
|
||||
}: AccountPageComponentProps) {
|
||||
const user = await serverClient().user.get()
|
||||
const { formatMessage } = await getIntl()
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
const membership = getMembership(user.memberships)
|
||||
return (
|
||||
<div>
|
||||
<Header title={title} link={link} subtitle={subtitle} />
|
||||
|
||||
<div className={styles.card}>
|
||||
<h2>{`${formatMessage({ id: "Total points" })}*`}</h2>
|
||||
<p
|
||||
className={styles.points}
|
||||
>{`${formatMessage({ id: "Points" })}: ${membership?.currentPoints || "N/A"}`}</p>
|
||||
<h2>{`${formatMessage({ id: "Total Points" })}*`}</h2>
|
||||
<p className={styles.points}>
|
||||
{`${formatMessage({ id: "Points" })}: ${membership?.currentPoints || "N/A"}`}
|
||||
</p>
|
||||
<p className={styles.disclaimer}>
|
||||
{`*${formatMessage({ id: "Points may take up to 10 days to be displayed." })}`}
|
||||
</p>
|
||||
|
||||
45
components/MyPages/Blocks/Points/EarnAndBurn/Client.tsx
Normal file
45
components/MyPages/Blocks/Points/EarnAndBurn/Client.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import DesktopTable from "./Desktop"
|
||||
import MobileTable from "./Mobile"
|
||||
|
||||
import type {
|
||||
ClientEarnAndBurnProps,
|
||||
TransactionsObject,
|
||||
} from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
export default function ClientEarnAndBurn({
|
||||
initialData,
|
||||
lang,
|
||||
}: ClientEarnAndBurnProps) {
|
||||
/**
|
||||
* desctruct fetchNextPage, hasNextPage once pagination is
|
||||
* possible through API
|
||||
*/
|
||||
const { data } = trpc.user.transaction.friendTransactions.useInfiniteQuery(
|
||||
{ limit: 5 },
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage?.nextCursor,
|
||||
initialData: {
|
||||
pageParams: [undefined, 1],
|
||||
pages: [initialData],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// TS having a hard time with the filtered type.
|
||||
// This is only temporary as we will not return null
|
||||
// later on when we handle errors appropriately.
|
||||
const filteredTransactions = (data?.pages.filter(
|
||||
(page) => page && page.data
|
||||
) ?? []) as unknown as TransactionsObject[]
|
||||
const transactions = filteredTransactions.flatMap((page) => page.data)
|
||||
return (
|
||||
<>
|
||||
<MobileTable lang={lang} transactions={transactions} />
|
||||
<DesktopTable lang={lang} transactions={transactions} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import styles from "./row.module.css"
|
||||
|
||||
import type { RowProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
export default function Row({ transaction, lang }: RowProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
const description =
|
||||
transaction.hotelName && transaction.city
|
||||
? `${transaction.hotelName}, ${transaction.city} ${transaction.nights} ${formatMessage({ id: "nights" })}`
|
||||
: `${transaction.nights} ${formatMessage({ id: "nights" })}`
|
||||
const arrival = dt(transaction.checkinDate).locale(lang).format("DD MMM YYYY")
|
||||
const departure = dt(transaction.checkoutDate)
|
||||
.locale(lang)
|
||||
.format("DD MMM YYYY")
|
||||
const values = [
|
||||
arrival,
|
||||
description,
|
||||
transaction.confirmationNumber,
|
||||
departure,
|
||||
transaction.awardPoints,
|
||||
]
|
||||
return (
|
||||
<tr className={styles.tr}>
|
||||
{values.map((value, idx) => (
|
||||
<td key={`value-${idx}`} className={styles.td}>
|
||||
{value}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.tr {
|
||||
border: 1px solid #e6e9ec;
|
||||
}
|
||||
|
||||
.td {
|
||||
text-align: left;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
.container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.thead {
|
||||
background-color: var(--Main-Grey-10);
|
||||
border-left: 1px solid var(--Main-Grey-10);
|
||||
border-right: 1px solid var(--Main-Grey-10);
|
||||
}
|
||||
|
||||
.th {
|
||||
text-align: left;
|
||||
padding: 20px 32px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--Main-Grey-10);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Row from "./Row"
|
||||
|
||||
import styles from "./desktop.module.css"
|
||||
|
||||
import type { TableProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
const tableHeadings = [
|
||||
"Arrival date",
|
||||
"Description",
|
||||
"Booking number",
|
||||
"Transaction date",
|
||||
"Points",
|
||||
]
|
||||
|
||||
export default function DesktopTable({ lang, transactions }: TableProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{transactions.length ? (
|
||||
<table className={styles.table}>
|
||||
<thead className={styles.thead}>
|
||||
<tr>
|
||||
{tableHeadings.map((heading) => (
|
||||
<th key={heading} className={styles.th}>
|
||||
{formatMessage({ id: heading })}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((transaction) => (
|
||||
<Row
|
||||
lang={lang}
|
||||
key={transaction.confirmationNumber}
|
||||
transaction={transaction}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
// TODO: add once pagination is available through API
|
||||
// <Button
|
||||
// disabled={isFetching}
|
||||
// intent="primary"
|
||||
// bgcolor="white"
|
||||
// type="button"
|
||||
// onClick={loadMoreData}
|
||||
// >
|
||||
// {formatMessage({id:"See more transactions"})}
|
||||
// </Button>
|
||||
<table className={styles.table}>
|
||||
<thead className={styles.thead}>
|
||||
<tr>
|
||||
{tableHeadings.map((heading) => (
|
||||
<th key={heading} className={styles.th}>
|
||||
{heading}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={tableHeadings.length} className={styles.placeholder}>
|
||||
{formatMessage({ id: "No transactions available" })}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import styles from "./mobile.module.css"
|
||||
|
||||
import type { TableProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
export default function MobileTable({ lang, transactions }: TableProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<table className={styles.table}>
|
||||
<thead className={styles.thead}>
|
||||
<tr>
|
||||
<Body asChild>
|
||||
<th className={styles.th}>
|
||||
{formatMessage({ id: "Transactions" })}
|
||||
</th>
|
||||
</Body>
|
||||
<Body asChild>
|
||||
<th className={styles.th}>{formatMessage({ id: "Points" })}</th>
|
||||
</Body>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.length ? (
|
||||
transactions.map((transaction) => (
|
||||
<tr className={styles.tr} key={transaction.confirmationNumber}>
|
||||
<td className={`${styles.td} ${styles.transactionDetails}`}>
|
||||
<span className={styles.transactionDate}>
|
||||
{dt(transaction.checkinDate)
|
||||
.locale(lang)
|
||||
.format("DD MMM YYYY")}
|
||||
</span>
|
||||
{transaction.hotelName && transaction.city ? (
|
||||
<span>{`${transaction.hotelName}, ${transaction.city}`}</span>
|
||||
) : null}
|
||||
<span>
|
||||
{`${transaction.nights} ${formatMessage({ id: transaction.nights === 1 ? "night" : "nights" })}`}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className={`${styles.mobileTd} ${styles.transactionPoints}`}
|
||||
>
|
||||
{`${transaction.awardPoints} P`}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td className={styles.placeholder} colSpan={2}>
|
||||
{formatMessage({ id: "Empty" })}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.thead {
|
||||
background-color: var(--Main-Grey-10);
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.tr {
|
||||
border-top: 1px solid var(--Main-Grey-10);
|
||||
}
|
||||
|
||||
.td {
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.transactionDetails {
|
||||
display: grid;
|
||||
font-size: var(--typography-Footnote-Regular-fontSize);
|
||||
}
|
||||
|
||||
.transactionDate {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.transactionPoints {
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
text-align: center;
|
||||
padding: var(--Spacing-x4);
|
||||
border: 1px solid var(--Main-Grey-10);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -2,97 +2,3 @@
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.mobileTable {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobileThead {
|
||||
background-color: var(--Main-Grey-10);
|
||||
}
|
||||
|
||||
.mobileTh {
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
font-weight: 500;
|
||||
padding: var(--Spacing-x2);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mobileTr {
|
||||
border-top: 1px solid var(--Main-Grey-10);
|
||||
}
|
||||
|
||||
.mobileTd {
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.mobileTransactionDetails {
|
||||
display: grid;
|
||||
font-size: var(--typography-Footnote-Regular-fontSize);
|
||||
}
|
||||
|
||||
.mobileTransactionDate {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mobileTransactionPoints {
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
}
|
||||
|
||||
.mobilePlaceholder {
|
||||
text-align: center;
|
||||
padding: var(--Spacing-x4);
|
||||
border: 1px solid var(--Main-Grey-10);
|
||||
}
|
||||
|
||||
.tableContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.thead {
|
||||
background-color: var(--Main-Grey-10);
|
||||
border-left: 1px solid var(--Main-Grey-10);
|
||||
border-right: 1px solid var(--Main-Grey-10);
|
||||
}
|
||||
|
||||
.tr {
|
||||
border: 1px solid #e6e9ec;
|
||||
}
|
||||
|
||||
.th {
|
||||
text-align: left;
|
||||
padding: 20px 32px;
|
||||
}
|
||||
|
||||
.td {
|
||||
text-align: left;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--Main-Grey-10);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.mobileTableContainer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tableContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,187 +1,28 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import Header from "@/components/SectionHeader"
|
||||
|
||||
import ClientEarnAndBurn from "./Client"
|
||||
|
||||
import styles from "./earnAndBurn.module.css"
|
||||
|
||||
import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
import { Page, RowProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
|
||||
const tableHeadings = [
|
||||
"Arrival date",
|
||||
"Description",
|
||||
"Booking number",
|
||||
"Transaction date",
|
||||
"Points",
|
||||
]
|
||||
|
||||
function EarnAndBurn({
|
||||
export default async function EarnAndBurn({
|
||||
lang,
|
||||
title,
|
||||
subtitle,
|
||||
link,
|
||||
subtitle,
|
||||
title,
|
||||
}: AccountPageComponentProps) {
|
||||
const intl = useIntl()
|
||||
const { data, hasNextPage, fetchNextPage } =
|
||||
trpc.user.transaction.friendTransactions.useInfiniteQuery(
|
||||
{ limit: 5 },
|
||||
{
|
||||
getNextPageParam: (lastPage: Page) => lastPage.nextCursor,
|
||||
}
|
||||
)
|
||||
|
||||
function loadMoreData() {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
const initialTransactions =
|
||||
await serverClient().user.transaction.friendTransactions({ limit: 5 })
|
||||
if (!initialTransactions) {
|
||||
return null
|
||||
}
|
||||
|
||||
const transactions = data?.pages.flatMap((page) => page.data) ?? []
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Header title={title} link={link} subtitle={subtitle} />
|
||||
|
||||
<div className={styles.mobileTableContainer}>
|
||||
<table className={styles.mobileTable}>
|
||||
<thead className={styles.mobileThead}>
|
||||
<tr>
|
||||
<th className={styles.mobileTh}>
|
||||
{intl.formatMessage({ id: "Transactions" })}
|
||||
</th>
|
||||
<th className={styles.mobileTh}>
|
||||
{intl.formatMessage({ id: "Points" })}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.length ? (
|
||||
transactions.map((transaction) => (
|
||||
<tr
|
||||
className={styles.mobileTr}
|
||||
key={transaction.confirmationNumber}
|
||||
>
|
||||
<td
|
||||
className={`${styles.mobileTd} ${styles.mobileTransactionDetails}`}
|
||||
>
|
||||
<span className={styles.mobileTransactionDate}>
|
||||
{dt(transaction.checkinDate)
|
||||
.locale(lang)
|
||||
.format("DD MMM YYYY")}
|
||||
</span>
|
||||
{transaction.hotelName && transaction.city ? (
|
||||
<span>{`${transaction.hotelName}, ${transaction.city}`}</span>
|
||||
) : null}
|
||||
<span>
|
||||
{`${transaction.nights} ${intl.formatMessage({ id: transaction.nights === 1 ? "night" : "nights" })}`}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className={`${styles.mobileTd} ${styles.mobileTransactionPoints}`}
|
||||
>
|
||||
{`${transaction.awardPoints} P`}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td className={styles.mobilePlaceholder} colSpan={2}>
|
||||
{intl.formatMessage({ id: "Empty" })}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={styles.tableContainer}>
|
||||
{transactions.length ? (
|
||||
<table className={styles.table}>
|
||||
<thead className={styles.thead}>
|
||||
<tr>
|
||||
{tableHeadings.map((heading) => (
|
||||
<th key={heading} className={styles.th}>
|
||||
{intl.formatMessage({ id: heading })}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((transaction) => (
|
||||
<Row
|
||||
lang={lang}
|
||||
key={transaction.confirmationNumber}
|
||||
transaction={transaction}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
// TODO: add once pagination is available through API
|
||||
// <Button
|
||||
// disabled={isFetching}
|
||||
// intent="primary"
|
||||
// bgcolor="white"
|
||||
// type="button"
|
||||
// onClick={loadMoreData}
|
||||
// >
|
||||
// {intl.formatMessage({id:"See more transactions"})}
|
||||
// </Button>
|
||||
<table className={styles.table}>
|
||||
<thead className={styles.thead}>
|
||||
<tr>
|
||||
{tableHeadings.map((heading) => (
|
||||
<th key={heading} className={styles.th}>
|
||||
{heading}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={tableHeadings.length}
|
||||
className={styles.placeholder}
|
||||
>
|
||||
{intl.formatMessage({ id: "No transactions available" })}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
<ClientEarnAndBurn initialData={initialTransactions} lang={lang} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({ transaction, lang }: RowProps) {
|
||||
const intl = useIntl()
|
||||
const description =
|
||||
transaction.hotelName && transaction.city
|
||||
? `${intl.formatMessage({ id: transaction.hotelName })}, ${transaction.city} ${transaction.nights} ${intl.formatMessage({ id: "nights" })}`
|
||||
: `${transaction.nights} ${intl.formatMessage({ id: "nights" })}`
|
||||
const arrival = dt(transaction.checkinDate).locale(lang).format("DD MMM YYYY")
|
||||
const departure = dt(transaction.checkoutDate)
|
||||
.locale(lang)
|
||||
.format("DD MMM YYYY")
|
||||
const values = [
|
||||
arrival,
|
||||
description,
|
||||
transaction.confirmationNumber,
|
||||
departure,
|
||||
transaction.awardPoints,
|
||||
]
|
||||
return (
|
||||
<tr className={styles.tr}>
|
||||
{values.map((value, idx) => (
|
||||
<td key={`value-${idx}`} className={styles.td}>
|
||||
{value}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
export default EarnAndBurn
|
||||
|
||||
67
components/MyPages/Blocks/Stays/Previous/Client.tsx
Normal file
67
components/MyPages/Blocks/Stays/Previous/Client.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Grids from "@/components/TempDesignSystem/Grids"
|
||||
|
||||
import ListContainer from "../ListContainer"
|
||||
import ShowMoreButton from "../ShowMoreButton"
|
||||
import StayCard from "../StayCard"
|
||||
import EmptyPreviousStaysBlock from "./EmptyPreviousStays"
|
||||
|
||||
import type {
|
||||
PreviousStaysClientProps,
|
||||
PreviousStaysNonNullResponseObject,
|
||||
} from "@/types/components/myPages/stays/previous"
|
||||
|
||||
export default function ClientPreviousStays({
|
||||
initialPreviousStays,
|
||||
lang,
|
||||
}: PreviousStaysClientProps) {
|
||||
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
|
||||
trpc.user.stays.previous.useInfiniteQuery(
|
||||
{},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage?.nextCursor,
|
||||
initialData: {
|
||||
pageParams: [undefined, 1],
|
||||
pages: [initialPreviousStays],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function loadMoreData() {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
// TS having a hard time with the filtered type.
|
||||
// This is only temporary as we will not return null
|
||||
// later on when we handle errors appropriately.
|
||||
const filteredStays = (data?.pages.filter((page) => page && page.data) ??
|
||||
[]) as unknown as PreviousStaysNonNullResponseObject[]
|
||||
const stays = filteredStays.flatMap((page) => page.data)
|
||||
|
||||
return isLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : stays.length ? (
|
||||
<ListContainer>
|
||||
<Grids.Stackable>
|
||||
{stays.map((stay) => (
|
||||
<StayCard
|
||||
key={stay.attributes.confirmationNumber}
|
||||
lang={lang}
|
||||
stay={stay}
|
||||
/>
|
||||
))}
|
||||
</Grids.Stackable>
|
||||
{hasNextPage ? (
|
||||
<ShowMoreButton disabled={isFetching} loadMoreData={loadMoreData} />
|
||||
) : null}
|
||||
</ListContainer>
|
||||
) : (
|
||||
<EmptyPreviousStaysBlock />
|
||||
)
|
||||
}
|
||||
@@ -1,64 +1,29 @@
|
||||
"use client"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Header from "@/components/SectionHeader"
|
||||
import Grids from "@/components/TempDesignSystem/Grids"
|
||||
import SectionHeader from "@/components/SectionHeader"
|
||||
|
||||
import Container from "../Container"
|
||||
import ListContainer from "../ListContainer"
|
||||
import ShowMoreButton from "../ShowMoreButton"
|
||||
import StayCard from "../StayCard"
|
||||
import EmptyPreviousStaysBlock from "./EmptyPreviousStays"
|
||||
import ClientPreviousStays from "./Client"
|
||||
|
||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
|
||||
export default function PreviousStays({
|
||||
export default async function PreviousStays({
|
||||
lang,
|
||||
title,
|
||||
subtitle,
|
||||
link,
|
||||
}: AccountPageComponentProps) {
|
||||
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
|
||||
trpc.user.stays.previous.useInfiniteQuery(
|
||||
{},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
)
|
||||
|
||||
function loadMoreData() {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
const initialPreviousStays = await serverClient().user.stays.previous()
|
||||
if (!initialPreviousStays?.data) {
|
||||
return null
|
||||
}
|
||||
|
||||
const stays = data?.pages.flatMap((page) => page.data) ?? []
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Header title={title} subtitle={subtitle} link={link} />
|
||||
{isLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : stays.length ? (
|
||||
<ListContainer>
|
||||
<Grids.Stackable>
|
||||
{stays.map((stay) => (
|
||||
<StayCard
|
||||
key={stay.attributes.confirmationNumber}
|
||||
lang={lang}
|
||||
stay={stay}
|
||||
/>
|
||||
))}
|
||||
</Grids.Stackable>
|
||||
{hasNextPage ? (
|
||||
<ShowMoreButton disabled={isFetching} loadMoreData={loadMoreData} />
|
||||
) : null}
|
||||
</ListContainer>
|
||||
) : (
|
||||
<EmptyPreviousStaysBlock />
|
||||
)}
|
||||
<SectionHeader title={title} subtitle={subtitle} link={link} />
|
||||
<ClientPreviousStays
|
||||
initialPreviousStays={initialPreviousStays}
|
||||
lang={lang}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,14 +16,17 @@ export default async function SoonestStays({
|
||||
subtitle,
|
||||
link,
|
||||
}: AccountPageComponentProps) {
|
||||
const { data: stays } = await serverClient().user.stays.upcoming({ limit: 3 })
|
||||
const response = await serverClient().user.stays.upcoming({ limit: 3 })
|
||||
if (!response?.data) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<Header title={title} subtitle={subtitle} link={link} />
|
||||
{stays.length ? (
|
||||
{response.data.length ? (
|
||||
<Grids.Stackable>
|
||||
{stays.map((stay) => (
|
||||
{response.data.map((stay) => (
|
||||
<StayCard
|
||||
key={stay.attributes.confirmationNumber}
|
||||
lang={lang}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Calendar } from "react-feather"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import { CalendarIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./stay.module.css"
|
||||
@@ -33,14 +33,14 @@ export default function StayCard({ stay, lang }: StayCardProps) {
|
||||
{hotelInformation.hotelName}
|
||||
</Title>
|
||||
<div className={styles.date}>
|
||||
<Calendar
|
||||
height={20}
|
||||
width={20}
|
||||
color="var(--Scandic-Brand-Burgundy)"
|
||||
/>
|
||||
<time dateTime={arrivalDateTime}>{arrivalDate}</time>
|
||||
<CalendarIcon color="burgundy" />
|
||||
<Caption asChild>
|
||||
<time dateTime={arrivalDateTime}>{arrivalDate}</time>
|
||||
</Caption>
|
||||
{" - "}
|
||||
<time dateTime={departDateTime}>{departDate}</time>
|
||||
<Caption asChild>
|
||||
<time dateTime={departDateTime}>{departDate}</time>
|
||||
</Caption>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
@@ -36,8 +36,4 @@
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
font-family: var(--typography-Caption-Regular-fontFamily);
|
||||
font-size: var(--typography-Caption-Regular-fontSize);
|
||||
font-weight: var(--typography-Caption-Regular-fontWeight);
|
||||
line-height: var(--typography-Caption-Regular-lineHeight);
|
||||
}
|
||||
|
||||
67
components/MyPages/Blocks/Stays/Upcoming/Client.tsx
Normal file
67
components/MyPages/Blocks/Stays/Upcoming/Client.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Grids from "@/components/TempDesignSystem/Grids"
|
||||
|
||||
import ListContainer from "../ListContainer"
|
||||
import ShowMoreButton from "../ShowMoreButton"
|
||||
import StayCard from "../StayCard"
|
||||
import EmptyUpcomingStaysBlock from "./EmptyUpcomingStays"
|
||||
|
||||
import type {
|
||||
UpcomingStaysClientProps,
|
||||
UpcomingStaysNonNullResponseObject,
|
||||
} from "@/types/components/myPages/stays/upcoming"
|
||||
|
||||
export default function ClientUpcomingStays({
|
||||
initialUpcomingStays,
|
||||
lang,
|
||||
}: UpcomingStaysClientProps) {
|
||||
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
|
||||
trpc.user.stays.upcoming.useInfiniteQuery(
|
||||
{},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage?.nextCursor,
|
||||
initialData: {
|
||||
pageParams: [undefined, 1],
|
||||
pages: [initialUpcomingStays],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function loadMoreData() {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
// TS having a hard time with the filtered type.
|
||||
// This is only temporary as we will not return null
|
||||
// later on when we handle errors appropriately.
|
||||
const filteredStays = (data?.pages.filter((page) => page && page.data) ??
|
||||
[]) as unknown as UpcomingStaysNonNullResponseObject[]
|
||||
const stays = filteredStays.flatMap((page) => page.data)
|
||||
|
||||
return isLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : stays.length ? (
|
||||
<ListContainer>
|
||||
<Grids.Stackable>
|
||||
{stays.map((stay) => (
|
||||
<StayCard
|
||||
key={stay.attributes.confirmationNumber}
|
||||
lang={lang}
|
||||
stay={stay}
|
||||
/>
|
||||
))}
|
||||
</Grids.Stackable>
|
||||
{hasNextPage ? (
|
||||
<ShowMoreButton disabled={isFetching} loadMoreData={loadMoreData} />
|
||||
) : null}
|
||||
</ListContainer>
|
||||
) : (
|
||||
<EmptyUpcomingStaysBlock />
|
||||
)
|
||||
}
|
||||
@@ -1,64 +1,29 @@
|
||||
"use client"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Header from "@/components/SectionHeader"
|
||||
import Grids from "@/components/TempDesignSystem/Grids"
|
||||
import SectionHeader from "@/components/SectionHeader"
|
||||
|
||||
import Container from "../Container"
|
||||
import ListContainer from "../ListContainer"
|
||||
import ShowMoreButton from "../ShowMoreButton"
|
||||
import StayCard from "../StayCard"
|
||||
import EmptyUpcomingStaysBlock from "./EmptyUpcomingStays"
|
||||
import ClientUpcomingStays from "./Client"
|
||||
|
||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
|
||||
export default function UpcomingStays({
|
||||
export default async function UpcomingStays({
|
||||
lang,
|
||||
title,
|
||||
subtitle,
|
||||
link,
|
||||
}: AccountPageComponentProps) {
|
||||
const { data, hasNextPage, isFetching, fetchNextPage, isLoading } =
|
||||
trpc.user.stays.upcoming.useInfiniteQuery(
|
||||
{ limit: 6 },
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
}
|
||||
)
|
||||
|
||||
function loadMoreData() {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
const initialUpcomingStays = await serverClient().user.stays.upcoming()
|
||||
if (!initialUpcomingStays?.data) {
|
||||
return null
|
||||
}
|
||||
|
||||
const stays = data?.pages.flatMap((page) => page.data) ?? []
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Header title={title} subtitle={subtitle} link={link} />
|
||||
{isLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : stays.length ? (
|
||||
<ListContainer>
|
||||
<Grids.Stackable>
|
||||
{stays.map((stay) => (
|
||||
<StayCard
|
||||
key={stay.attributes.confirmationNumber}
|
||||
lang={lang}
|
||||
stay={stay}
|
||||
/>
|
||||
))}
|
||||
</Grids.Stackable>
|
||||
{hasNextPage ? (
|
||||
<ShowMoreButton disabled={isFetching} loadMoreData={loadMoreData} />
|
||||
) : null}
|
||||
</ListContainer>
|
||||
) : (
|
||||
<EmptyUpcomingStaysBlock />
|
||||
)}
|
||||
<SectionHeader title={title} subtitle={subtitle} link={link} />
|
||||
<ClientUpcomingStays
|
||||
initialUpcomingStays={initialUpcomingStays}
|
||||
lang={lang}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { ChevronRightIcon , HouseIcon } from "@/components/Icons"
|
||||
import { ChevronRightIcon, HouseIcon } from "@/components/Icons"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
|
||||
@@ -8,6 +8,9 @@ import styles from "./breadcrumbs.module.css"
|
||||
|
||||
export default async function Breadcrumbs() {
|
||||
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get()
|
||||
if (!breadcrumbs) {
|
||||
return null
|
||||
}
|
||||
const homeBreadcrumb = breadcrumbs.shift()
|
||||
return (
|
||||
<nav className={styles.breadcrumbs}>
|
||||
|
||||
@@ -15,6 +15,9 @@ import type { LangParams } from "@/types/params"
|
||||
export default async function Sidebar({ lang }: LangParams) {
|
||||
const navigation = await serverClient().contentstack.myPages.navigation.get()
|
||||
const { formatMessage } = await getIntl()
|
||||
if (!navigation) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<aside className={styles.sidebar}>
|
||||
<nav className={styles.nav}>
|
||||
|
||||
@@ -5,6 +5,9 @@ import Form from "./Form"
|
||||
|
||||
export default async function EditProfile() {
|
||||
const user = await serverClient().user.get()
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Container user={user}>
|
||||
<Form user={user} />
|
||||
|
||||
@@ -17,6 +17,9 @@ import styles from "./profile.module.css"
|
||||
export default async function Profile() {
|
||||
// const { formatMessage } = await getIntl()
|
||||
const user = await serverClient().user.get()
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
// const countryName = countries.find(
|
||||
// (country) => country.code === user.address.country
|
||||
// )
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
/* TODO: Waiting for variables for buttons from Design team */
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-weight: 600;
|
||||
line-height: 150%;
|
||||
|
||||
@@ -41,13 +41,8 @@
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.scriptedTitle {
|
||||
span.scriptedTitle {
|
||||
color: var(--script-color);
|
||||
font-family: var(--typography-Script-2-fontFamily);
|
||||
font-size: var(--typography-Script-2-fontSize);
|
||||
font-weight: var(--typography-Script-2-fontWeight);
|
||||
line-height: var(--typography-Script-2-lineHeight);
|
||||
letter-spacing: 0.48px;
|
||||
padding: var(--Spacing-x1);
|
||||
margin: 0;
|
||||
transform: rotate(-3deg);
|
||||
@@ -57,13 +52,8 @@
|
||||
color: var(--font-color);
|
||||
}
|
||||
|
||||
.bodyText {
|
||||
p.bodyText {
|
||||
color: var(--font-color);
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
font-weight: var(--typography-Body-Regular-fontWeight);
|
||||
line-height: var(--typography-Body-Regular-lineHeight);
|
||||
letter-spacing: 0.096px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import { cardVariants } from "./variants"
|
||||
@@ -42,13 +44,20 @@ export default function Card({
|
||||
>
|
||||
{scriptedTopTitle ? (
|
||||
<section className={styles.scriptContainer}>
|
||||
<h3 className={styles.scriptedTitle}>{scriptedTopTitle}</h3>
|
||||
<BiroScript className={styles.scriptedTitle} type="two">
|
||||
{scriptedTopTitle}
|
||||
</BiroScript>
|
||||
<Divider />
|
||||
</section>
|
||||
) : null}
|
||||
<Title as="h5" className={styles.heading} level="h3">
|
||||
{heading}
|
||||
</Title>
|
||||
{bodyText ? <p className={styles.bodyText}>{bodyText}</p> : null}
|
||||
{bodyText ? (
|
||||
<Body className={styles.bodyText} textAlign="center">
|
||||
{bodyText}
|
||||
</Body>
|
||||
) : null}
|
||||
<div className={styles.buttonContainer}>
|
||||
{primaryButton ? (
|
||||
<Button asChild theme={buttonTheme} size="small">
|
||||
|
||||
@@ -23,12 +23,6 @@
|
||||
.input,
|
||||
.listBoxItem {
|
||||
color: var(--UI-Grey-60);
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
font-weight: var(--typography-Body-Regular-fontWeight);
|
||||
font-weight: var(--typography-Body-Regular-fontWeight);
|
||||
letter-spacing: var--typography-Body-Regular-letterSpacing;
|
||||
line-height: var(--typography-Body-Regular-lineHeight);
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
import { useController, useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import SelectChevron from "../SelectChevron"
|
||||
import { countries } from "./countries"
|
||||
|
||||
@@ -53,11 +55,13 @@ export default function CountrySelect({
|
||||
selectedKey={field.value}
|
||||
>
|
||||
<div className={styles.comboBoxContainer}>
|
||||
<Input
|
||||
aria-label={selectCountryLabel}
|
||||
className={styles.input}
|
||||
placeholder={selectCountryLabel}
|
||||
/>
|
||||
<Body asChild fontOnly>
|
||||
<Input
|
||||
aria-label={selectCountryLabel}
|
||||
className={styles.input}
|
||||
placeholder={selectCountryLabel}
|
||||
/>
|
||||
</Body>
|
||||
<Button className={styles.button}>
|
||||
<SelectChevron />
|
||||
</Button>
|
||||
@@ -79,14 +83,15 @@ export default function CountrySelect({
|
||||
>
|
||||
<ListBox>
|
||||
{countries.map((country, idx) => (
|
||||
<ListBoxItem
|
||||
aria-label={country.name}
|
||||
className={styles.listBoxItem}
|
||||
id={country.code}
|
||||
key={`${country.code}-${idx}`}
|
||||
>
|
||||
{country.name}
|
||||
</ListBoxItem>
|
||||
<Body asChild fontOnly key={`${country.code}-${idx}`}>
|
||||
<ListBoxItem
|
||||
aria-label={country.name}
|
||||
className={styles.listBoxItem}
|
||||
id={country.code}
|
||||
>
|
||||
{country.name}
|
||||
</ListBoxItem>
|
||||
</Body>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
.message {
|
||||
color: var(--Scandic-Red-60);
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
font-weight: var(--typography-Body-Regular-fontWeight);
|
||||
line-height: var(--typography-Body-Regular-lineHeight);
|
||||
margin: var(--Spacing-x-half) 0 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import styles from "./error.module.css"
|
||||
|
||||
import type { ErrorMessageProps } from "./errorMessage"
|
||||
@@ -12,7 +14,11 @@ export default function ErrorMessage<T>({
|
||||
<RHFErrorMessage
|
||||
errors={errors}
|
||||
name={name}
|
||||
render={({ message }) => <p className={styles.message}>{message}</p>}
|
||||
render={({ message }) => (
|
||||
<Body className={styles.message} fontOnly>
|
||||
{message}
|
||||
</Body>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Input as AriaInput, TextField } from "react-aria-components"
|
||||
import { useController } from "react-hook-form"
|
||||
|
||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import styles from "./input.module.css"
|
||||
|
||||
@@ -35,11 +36,13 @@ export default function Input({
|
||||
onChange={field.onChange}
|
||||
type={type}
|
||||
>
|
||||
<AriaInput
|
||||
className={styles.input}
|
||||
placeholder={placeholder}
|
||||
ref={field.ref}
|
||||
/>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput
|
||||
className={styles.input}
|
||||
placeholder={placeholder}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</Body>
|
||||
<ErrorMessage errors={formState.errors} name={name} />
|
||||
</TextField>
|
||||
)
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
border: 2px solid var(--UI-Grey-60);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
color: var(--UI-Grey-60);
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
font-weight: var(--typography-Body-Regular-fontWeight);
|
||||
height: 40px;
|
||||
letter-spacing: var(--typography-Body-Regular-letterSpacing);
|
||||
line-height: var(--typography-Body-Regular-lineHeight);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
width: min(280px, 100%);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
SelectValue,
|
||||
} from "react-aria-components"
|
||||
|
||||
import Body from "../../Text/Body"
|
||||
import Footnote from "../../Text/Footnote"
|
||||
import SelectChevron from "../SelectChevron"
|
||||
|
||||
import styles from "./select.module.css"
|
||||
@@ -48,38 +50,44 @@ export default function Select({
|
||||
placeholder={placeholder}
|
||||
selectedKey={value as Key}
|
||||
>
|
||||
<Button className={styles.input}>
|
||||
<div className={styles.inputContentWrapper}>
|
||||
<Label className={styles.label}>{label}</Label>
|
||||
<SelectValue />
|
||||
</div>
|
||||
<SelectChevron />
|
||||
</Button>
|
||||
<Popover
|
||||
className={styles.popover}
|
||||
placement="bottom"
|
||||
shouldFlip={false}
|
||||
/**
|
||||
* react-aria uses portals to render Popover in body
|
||||
* unless otherwise specified. We need it to be contained
|
||||
* by this component to both access css variables assigned
|
||||
* on the container as well as to not overflow it at any time.
|
||||
*/
|
||||
UNSTABLE_portalContainer={rootDiv ?? undefined}
|
||||
>
|
||||
<ListBox className={styles.listBox}>
|
||||
{items.map((item) => (
|
||||
<ListBoxItem
|
||||
aria-label={String(item)}
|
||||
className={styles.listBoxItem}
|
||||
id={item.value}
|
||||
key={item.label}
|
||||
>
|
||||
{item.label}
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
<Body asChild fontOnly>
|
||||
<Button className={styles.input}>
|
||||
<div className={styles.inputContentWrapper}>
|
||||
<Footnote asChild fontOnly>
|
||||
<Label className={styles.label}>{label}</Label>
|
||||
</Footnote>
|
||||
<SelectValue />
|
||||
</div>
|
||||
<SelectChevron />
|
||||
</Button>
|
||||
</Body>
|
||||
<Body asChild fontOnly>
|
||||
<Popover
|
||||
className={styles.popover}
|
||||
placement="bottom"
|
||||
shouldFlip={false}
|
||||
/**
|
||||
* react-aria uses portals to render Popover in body
|
||||
* unless otherwise specified. We need it to be contained
|
||||
* by this component to both access css variables assigned
|
||||
* on the container as well as to not overflow it at any time.
|
||||
*/
|
||||
UNSTABLE_portalContainer={rootDiv ?? undefined}
|
||||
>
|
||||
<ListBox className={styles.listBox}>
|
||||
{items.map((item) => (
|
||||
<ListBoxItem
|
||||
aria-label={String(item)}
|
||||
className={styles.listBoxItem}
|
||||
id={item.value}
|
||||
key={item.label}
|
||||
>
|
||||
{item.label}
|
||||
</ListBoxItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Body>
|
||||
</ReactAriaSelect>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
|
||||
.label {
|
||||
color: var(--Base-Text-UI-Placeholder);
|
||||
font-family: var(--typography-Footnote-Regular-fontFamily);
|
||||
font-size: var(--typography-Footnote-Regular-fontSize);
|
||||
font-weight: var(--typography-Footnote-Regular-fontWeight);
|
||||
line-height: var(--typography-Footnote-Regular-lineHeight);
|
||||
}
|
||||
|
||||
.select {
|
||||
@@ -32,11 +28,6 @@
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
height: 56px;
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
font-weight: var(--typography-Body-Regular-fontWeight);
|
||||
letter-spacing: var(--typography-Body-Regular-letterSpacing);
|
||||
line-height: var(--typography-Body-Regular-lineHeight);
|
||||
outline: none;
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
text-align: left;
|
||||
@@ -56,7 +47,6 @@
|
||||
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.08);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
font-size: var(--typography-Body-Regular-fontSize);
|
||||
gap: var(--Spacing-x1);
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
@@ -79,4 +69,4 @@
|
||||
|
||||
.listBoxItem[data-selected="true"] {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,10 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.burgundy {
|
||||
color: var(--Scandic-Brand-Burgundy);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import styles from "./biroScript.module.css"
|
||||
const config = {
|
||||
variants: {
|
||||
color: {
|
||||
black: styles.black,
|
||||
burgundy: styles.burgundy,
|
||||
pale: styles.pale,
|
||||
primaryLightOnSurfaceAccent: styles.plosa,
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bodyFontOnly {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-family: var(--typography-Body-Bold-fontFamily);
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface BodyProps
|
||||
extends Omit<React.HTMLAttributes<HTMLHeadingElement>, "color">,
|
||||
VariantProps<typeof bodyVariants> {
|
||||
asChild?: boolean
|
||||
fontOnly?: boolean
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
|
||||
import { bodyVariants } from "./variants"
|
||||
import { bodyFontOnlyVariants, bodyVariants } from "./variants"
|
||||
|
||||
import type { BodyProps } from "./body"
|
||||
|
||||
@@ -8,16 +8,23 @@ export default function Body({
|
||||
asChild = false,
|
||||
className = "",
|
||||
color,
|
||||
fontOnly = false,
|
||||
textAlign,
|
||||
textTransform,
|
||||
...props
|
||||
}: BodyProps) {
|
||||
const Comp = asChild ? Slot : "p"
|
||||
const classNames = bodyVariants({
|
||||
className,
|
||||
color,
|
||||
textAlign,
|
||||
textTransform,
|
||||
})
|
||||
const classNames = fontOnly
|
||||
? bodyFontOnlyVariants({
|
||||
className,
|
||||
textAlign,
|
||||
textTransform,
|
||||
})
|
||||
: bodyVariants({
|
||||
className,
|
||||
color,
|
||||
textAlign,
|
||||
textTransform,
|
||||
})
|
||||
return <Comp className={classNames} {...props} />
|
||||
}
|
||||
|
||||
@@ -27,3 +27,22 @@ const config = {
|
||||
} as const
|
||||
|
||||
export const bodyVariants = cva(styles.body, config)
|
||||
|
||||
const fontOnlyconfig = {
|
||||
variants: {
|
||||
textAlign: {
|
||||
center: styles.textAlignCenter,
|
||||
left: styles.textAlignLeft,
|
||||
},
|
||||
textTransform: {
|
||||
bold: styles.bold,
|
||||
regular: styles.regular,
|
||||
underlined: styles.underlined,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
textAlign: "left",
|
||||
textTransform: "regular",
|
||||
},
|
||||
} as const
|
||||
export const bodyFontOnlyVariants = cva(styles.bodyFontOnly, fontOnlyconfig)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
.body {
|
||||
.caption {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.captionFontOnly {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-family: var(--typography-Caption-Bold-fontFamily);
|
||||
font-size: var(--typography-Caption-Bold-fontSize);
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface CaptionProps
|
||||
extends Omit<React.HTMLAttributes<HTMLHeadingElement>, "color">,
|
||||
VariantProps<typeof captionVariants> {
|
||||
asChild?: boolean
|
||||
fontOnly?: boolean
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
|
||||
import { captionVariants } from "./variants"
|
||||
import { captionVariants, fontOnlycaptionVariants } from "./variants"
|
||||
|
||||
import type { CaptionProps } from "./caption"
|
||||
|
||||
@@ -8,14 +8,20 @@ export default function Caption({
|
||||
asChild = false,
|
||||
className = "",
|
||||
color,
|
||||
fontOnly = false,
|
||||
textTransform,
|
||||
...props
|
||||
}: CaptionProps) {
|
||||
const Comp = asChild ? Slot : "p"
|
||||
const classNames = captionVariants({
|
||||
className,
|
||||
color,
|
||||
textTransform,
|
||||
})
|
||||
const classNames = fontOnly
|
||||
? fontOnlycaptionVariants({
|
||||
className,
|
||||
textTransform,
|
||||
})
|
||||
: captionVariants({
|
||||
className,
|
||||
color,
|
||||
textTransform,
|
||||
})
|
||||
return <Comp className={classNames} {...props} />
|
||||
}
|
||||
|
||||
@@ -21,3 +21,20 @@ const config = {
|
||||
} as const
|
||||
|
||||
export const captionVariants = cva(styles.caption, config)
|
||||
|
||||
const fontOnlyConfig = {
|
||||
variants: {
|
||||
textTransform: {
|
||||
bold: styles.bold,
|
||||
regular: styles.regular,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
textTransform: "regular",
|
||||
},
|
||||
} as const
|
||||
|
||||
export const fontOnlycaptionVariants = cva(
|
||||
styles.captionFontOnly,
|
||||
fontOnlyConfig
|
||||
)
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footnoteFontOnly {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-family: var(--typography-Footnote-Bold-fontFamily);
|
||||
font-size: var(--typography-Footnote-Bold-fontSize);
|
||||
|
||||
@@ -6,4 +6,5 @@ export interface FootnoteProps
|
||||
extends Omit<React.HTMLAttributes<HTMLParagraphElement>, "color">,
|
||||
VariantProps<typeof footnoteVariants> {
|
||||
asChild?: boolean
|
||||
fontOnly?: boolean
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
|
||||
import { footnoteVariants } from "./variants"
|
||||
import { footnoteFontOnlyVariants, footnoteVariants } from "./variants"
|
||||
|
||||
import type { FootnoteProps } from "./footnote"
|
||||
|
||||
@@ -8,16 +8,23 @@ export default function Footnote({
|
||||
asChild = false,
|
||||
className = "",
|
||||
color,
|
||||
fontOnly = false,
|
||||
textAlign,
|
||||
textTransform,
|
||||
...props
|
||||
}: FootnoteProps) {
|
||||
const Comp = asChild ? Slot : "p"
|
||||
const classNames = footnoteVariants({
|
||||
className,
|
||||
color,
|
||||
textAlign,
|
||||
textTransform,
|
||||
})
|
||||
const classNames = fontOnly
|
||||
? footnoteFontOnlyVariants({
|
||||
className,
|
||||
textAlign,
|
||||
textTransform,
|
||||
})
|
||||
: footnoteVariants({
|
||||
className,
|
||||
color,
|
||||
textAlign,
|
||||
textTransform,
|
||||
})
|
||||
return <Comp className={classNames} {...props} />
|
||||
}
|
||||
|
||||
@@ -24,3 +24,24 @@ const config = {
|
||||
} as const
|
||||
|
||||
export const footnoteVariants = cva(styles.footnote, config)
|
||||
|
||||
const fontOnlyConfig = {
|
||||
variants: {
|
||||
textAlign: {
|
||||
center: styles.center,
|
||||
left: styles.left,
|
||||
},
|
||||
textTransform: {
|
||||
bold: styles.bold,
|
||||
regular: styles.regular,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
textTransform: "regular",
|
||||
},
|
||||
} as const
|
||||
|
||||
export const footnoteFontOnlyVariants = cva(
|
||||
styles.footnoteFontOnly,
|
||||
fontOnlyConfig
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { Children } from "react"
|
||||
|
||||
import { subtitleVariants } from "./variants"
|
||||
|
||||
@@ -13,7 +14,7 @@ export default function Subtitle({
|
||||
textTransform,
|
||||
...props
|
||||
}: SubtitleProps) {
|
||||
if (hideEmpty && !props.children) {
|
||||
if (hideEmpty && Children.count(props.children) === 0) {
|
||||
return null
|
||||
}
|
||||
const Comp = asChild ? Slot : "p"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Children } from "react"
|
||||
|
||||
import { headingVariants } from "./variants"
|
||||
|
||||
import type { HeadingProps } from "./title"
|
||||
@@ -12,7 +14,7 @@ export default function Title({
|
||||
textAlign,
|
||||
textTransform,
|
||||
}: HeadingProps) {
|
||||
if (hideEmpty && !children) {
|
||||
if (hideEmpty && Children.count(children) === 0) {
|
||||
return null
|
||||
}
|
||||
const Hx = level
|
||||
|
||||
@@ -2,7 +2,7 @@ import { headingVariants } from "./variants"
|
||||
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
|
||||
type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5"
|
||||
|
||||
export interface HeadingProps
|
||||
extends Omit<React.HTMLAttributes<HTMLHeadingElement>, "color">,
|
||||
|
||||
@@ -22,7 +22,6 @@ const config = {
|
||||
h3: styles.h3,
|
||||
h4: styles.h4,
|
||||
h5: styles.h5,
|
||||
h6: styles.h6,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"Description": "Beskrivelse",
|
||||
"Edit": "Redigere",
|
||||
"Email": "E-mail",
|
||||
"Empty": "Empty",
|
||||
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
||||
"Find booking": "Find booking",
|
||||
"Get inspired": "Blive inspireret",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"Description": "Beschreibung",
|
||||
"Edit": "Bearbeiten",
|
||||
"Email": "Email",
|
||||
"Empty": "Empty",
|
||||
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
||||
"Find booking": "Buchung finden",
|
||||
"Get inspired": "Lass dich inspirieren",
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
"Description": "Description",
|
||||
"Edit": "Edit",
|
||||
"Email": "Email",
|
||||
"Empty": "Empty",
|
||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||
"Find booking": "Find booking",
|
||||
"Free soft drink voucher for the kids when staying": "Free soft drink voucher for the kids when staying",
|
||||
"Get inspired": "Get inspired",
|
||||
"Go back to overview": "Go back to overview",
|
||||
"How it works": "How it works",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"Description": "Kuvaus",
|
||||
"Edit": "Muokata",
|
||||
"Email": "Sähköposti",
|
||||
"Empty": "Empty",
|
||||
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
||||
"Find booking": "Etsi varaus",
|
||||
"Get inspired": "Inspiroidu",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"Description": "Beskrivelse",
|
||||
"Edit": "Redigere",
|
||||
"Email": "E-post",
|
||||
"Empty": "Empty",
|
||||
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
||||
"Find booking": "Finn booking",
|
||||
"Get inspired": "Bli inspirert",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"Description": "Beskrivning",
|
||||
"Edit": "Redigera",
|
||||
"Email": "E-post",
|
||||
"Empty": "Tom",
|
||||
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
||||
"Find booking": "Hitta bokning",
|
||||
"Get inspired": "Bli inspirerad",
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function TrpcProvider({
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 3000,
|
||||
staleTime: 60 * 1000,
|
||||
retry(failureCount, error) {
|
||||
if (error instanceof TRPCClientError) {
|
||||
const appError: TRPCClientError<AnyTRPCRouter> = error
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
GetAccountPageRefs,
|
||||
} from "@/lib/graphql/Query/AccountPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { internalServerError, notFound } from "@/server/errors/trpc"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
|
||||
import {
|
||||
@@ -45,15 +45,14 @@ export const accountPageQueryRouter = router({
|
||||
throw notFound(refsResponse)
|
||||
}
|
||||
|
||||
// Remove empty objects from a fetched content type. Needed since
|
||||
// Contentstack returns empty objects for all non queried blocks in modular blocks.
|
||||
// This is an ongoing support case in Contentstack, ticker number #00031579
|
||||
const cleanedData = removeEmptyObjects(refsResponse.data)
|
||||
|
||||
const validatedAccountPageRefs =
|
||||
validateAccountPageRefsSchema.safeParse(cleanedData)
|
||||
if (!validatedAccountPageRefs.success) {
|
||||
throw internalServerError(validatedAccountPageRefs.error)
|
||||
console.info(`Failed to validate My Page Refs - (uid: ${uid})`)
|
||||
console.error(validatedAccountPageRefs.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const connections = getConnections(validatedAccountPageRefs.data)
|
||||
@@ -81,7 +80,9 @@ export const accountPageQueryRouter = router({
|
||||
)
|
||||
|
||||
if (!validatedAccountPage.success) {
|
||||
throw internalServerError(validatedAccountPage.error)
|
||||
console.info(`Failed to validate Account Page - (uid: ${uid})`)
|
||||
console.error(validatedAccountPage.error)
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: Make returned data nicer
|
||||
|
||||
@@ -8,12 +8,8 @@ import {
|
||||
GetCurrentHeaderRef,
|
||||
} from "@/lib/graphql/Query/CurrentHeader.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { internalServerError, notFound } from "@/server/errors/trpc"
|
||||
import {
|
||||
contentstackBaseProcedure,
|
||||
publicProcedure,
|
||||
router,
|
||||
} from "@/server/trpc"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import { contentstackBaseProcedure, router } from "@/server/trpc"
|
||||
|
||||
import { generateTag } from "@/utils/generateTag"
|
||||
|
||||
@@ -47,12 +43,16 @@ export const baseQueryRouter = router({
|
||||
)
|
||||
|
||||
if (!validatedContactConfigConfig.success) {
|
||||
throw internalServerError(validatedContactConfigConfig.error)
|
||||
console.info(
|
||||
`Failed to validate Contact Config Data - (lang: ${ctx.lang})`
|
||||
)
|
||||
console.error(validatedContactConfigConfig.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return validatedContactConfigConfig.data.all_contact_config.items[0]
|
||||
}),
|
||||
header: publicProcedure.input(langInput).query(async ({ input }) => {
|
||||
header: contentstackBaseProcedure.input(langInput).query(async ({ input }) => {
|
||||
const responseRef = await request<HeaderRefDataRaw>(GetCurrentHeaderRef, {
|
||||
locale: input.lang,
|
||||
})
|
||||
@@ -79,7 +79,9 @@ export const baseQueryRouter = router({
|
||||
)
|
||||
|
||||
if (!validatedHeaderConfig.success) {
|
||||
throw internalServerError(validatedHeaderConfig.error)
|
||||
console.info(`Failed to validate Header - (lang: ${input.lang})`)
|
||||
console.error(validatedHeaderConfig.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const logo =
|
||||
@@ -91,7 +93,7 @@ export const baseQueryRouter = router({
|
||||
logo,
|
||||
} as HeaderData
|
||||
}),
|
||||
footer: publicProcedure.input(langInput).query(async ({ input }) => {
|
||||
footer: contentstackBaseProcedure.input(langInput).query(async ({ input }) => {
|
||||
const responseRef = await request<FooterRefDataRaw>(GetCurrentFooterRef, {
|
||||
locale: input.lang,
|
||||
})
|
||||
@@ -116,7 +118,9 @@ export const baseQueryRouter = router({
|
||||
)
|
||||
|
||||
if (!validatedFooterConfig.success) {
|
||||
throw internalServerError(validatedFooterConfig.error)
|
||||
console.info(`Failed to validate Footer - (lang: ${input.lang})`)
|
||||
console.error(validatedFooterConfig.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return validatedFooterConfig.data.all_current_footer.items[0]
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
GetMyPagesBreadcrumbs,
|
||||
GetMyPagesBreadcrumbsRefs,
|
||||
} from "@/lib/graphql/Query/BreadcrumbsMyPages.graphql"
|
||||
import { internalServerError } from "@/server/errors/trpc"
|
||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
|
||||
import {
|
||||
@@ -41,7 +40,11 @@ async function getLoyaltyPageBreadcrumbs(variables: Variables) {
|
||||
)
|
||||
|
||||
if (!validatedRefsData.success) {
|
||||
throw internalServerError(validatedRefsData.error)
|
||||
console.info(
|
||||
`Failed to validate Loyaltypage Breadcrumbs Refs - (url: ${variables.url})`
|
||||
)
|
||||
console.error(validatedRefsData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const tags = getTags(validatedRefsData.data.all_loyalty_page, variables)
|
||||
@@ -53,14 +56,18 @@ async function getLoyaltyPageBreadcrumbs(variables: Variables) {
|
||||
)
|
||||
|
||||
if (!response.data.all_loyalty_page.items[0].web?.breadcrumbs?.title) {
|
||||
return []
|
||||
return null
|
||||
}
|
||||
|
||||
const validatedBreadcrumbsData =
|
||||
validateLoyaltyPageBreadcrumbsContentstackSchema.safeParse(response.data)
|
||||
|
||||
if (!validatedBreadcrumbsData.success) {
|
||||
throw internalServerError(validatedBreadcrumbsData.error)
|
||||
console.info(
|
||||
`Failed to validate Loyaltypage Breadcrumbs Data - (url: ${variables.url})`
|
||||
)
|
||||
console.error(validatedBreadcrumbsData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return getBreadcrumbs(
|
||||
@@ -80,7 +87,11 @@ async function getMyPagesBreadcrumbs(variables: Variables) {
|
||||
refsResponse.data
|
||||
)
|
||||
if (!validatedRefsData.success) {
|
||||
throw internalServerError(validatedRefsData.error)
|
||||
console.info(
|
||||
`Failed to validate My Page Breadcrumbs Refs - (url: ${variables.url})`
|
||||
)
|
||||
console.error(validatedRefsData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const tags = getTags(validatedRefsData.data.all_account_page, variables)
|
||||
@@ -99,7 +110,11 @@ async function getMyPagesBreadcrumbs(variables: Variables) {
|
||||
validateMyPagesBreadcrumbsContentstackSchema.safeParse(response.data)
|
||||
|
||||
if (!validatedBreadcrumbsData.success) {
|
||||
throw internalServerError(validatedBreadcrumbsData.error)
|
||||
console.info(
|
||||
`Failed to validate My Page Breadcrumbs Data - (url: ${variables.url})`
|
||||
)
|
||||
console.error(validatedBreadcrumbsData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return getBreadcrumbs(
|
||||
|
||||
@@ -119,7 +119,11 @@ export const languageSwitcherQueryRouter = router({
|
||||
validateLanguageSwitcherData.safeParse(urls)
|
||||
|
||||
if (!validatedLanguageSwitcherData.success) {
|
||||
throw internalServerError(validatedLanguageSwitcherData.error)
|
||||
console.info(
|
||||
`Failed to validate Language Switcher Data - (contentType: ${ctx.contentType}, lang: ${ctx.lang}, uid: ${ctx.uid})`
|
||||
)
|
||||
console.error(validatedLanguageSwitcherData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
GetLoyaltyPageRefs,
|
||||
} from "@/lib/graphql/Query/LoyaltyPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { internalServerError, notFound } from "@/server/errors/trpc"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
|
||||
import {
|
||||
@@ -35,9 +35,9 @@ function makeButtonObject(button: any) {
|
||||
href:
|
||||
button.is_contentstack_link && button.linkConnection.edges.length
|
||||
? button.linkConnection.edges[0].node.web?.original_url ||
|
||||
removeMultipleSlashes(
|
||||
`/${button.linkConnection.edges[0].node.system.locale}/${button.linkConnection.edges[0].node.url}`
|
||||
)
|
||||
removeMultipleSlashes(
|
||||
`/${button.linkConnection.edges[0].node.system.locale}/${button.linkConnection.edges[0].node.url}`
|
||||
)
|
||||
: button.external_link.href,
|
||||
isExternal: !button.is_contentstack_link,
|
||||
}
|
||||
@@ -62,17 +62,16 @@ export const loyaltyPageQueryRouter = router({
|
||||
throw notFound(refsResponse)
|
||||
}
|
||||
|
||||
// Remove empty objects from a fetched content type. Needed since
|
||||
// Contentstack returns empty objects for all non queried blocks in modular blocks.
|
||||
// This is an ongoing support case in Contentstack, ticker number #00031579
|
||||
const cleanedData = removeEmptyObjects(refsResponse.data)
|
||||
|
||||
const validatedLoyaltyPageRefs =
|
||||
validateLoyaltyPageRefsSchema.safeParse(cleanedData)
|
||||
if (!validatedLoyaltyPageRefs.success) {
|
||||
console.error("Bad validation for `GetLoyaltyPageRefs`")
|
||||
console.info(
|
||||
`Failed to validate Loyaltypage Refs - (lang: ${lang}, uid: ${uid})`
|
||||
)
|
||||
console.error(validatedLoyaltyPageRefs.error)
|
||||
throw internalServerError(validatedLoyaltyPageRefs.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const connections = getConnections(validatedLoyaltyPageRefs.data)
|
||||
@@ -97,66 +96,66 @@ export const loyaltyPageQueryRouter = router({
|
||||
|
||||
const blocks = response.data.loyalty_page.blocks
|
||||
? response.data.loyalty_page.blocks.map((block: any) => {
|
||||
switch (block.__typename) {
|
||||
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
|
||||
return {
|
||||
...block,
|
||||
dynamic_content: {
|
||||
...block.dynamic_content,
|
||||
link: block.dynamic_content.link.pageConnection.edges.length
|
||||
? {
|
||||
text: block.dynamic_content.link.text,
|
||||
href: removeMultipleSlashes(
|
||||
`/${block.dynamic_content.link.pageConnection.edges[0].node.system.locale}/${block.dynamic_content.link.pageConnection.edges[0].node.url}`
|
||||
),
|
||||
title:
|
||||
block.dynamic_content.link.pageConnection.edges[0]
|
||||
.node.title,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts:
|
||||
return {
|
||||
...block,
|
||||
shortcuts: {
|
||||
...block.shortcuts,
|
||||
shortcuts: block.shortcuts.shortcuts.map((shortcut: any) => ({
|
||||
text: shortcut.text,
|
||||
openInNewTab: shortcut.open_in_new_tab,
|
||||
...shortcut.linkConnection.edges[0].node,
|
||||
url:
|
||||
shortcut.linkConnection.edges[0].node.web?.original_url ||
|
||||
removeMultipleSlashes(
|
||||
`/${shortcut.linkConnection.edges[0].node.system.locale}/${shortcut.linkConnection.edges[0].node.url}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
}
|
||||
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
|
||||
return {
|
||||
...block,
|
||||
cards_grid: {
|
||||
...block.cards_grid,
|
||||
cards: block.cards_grid.cardConnection.edges.map(
|
||||
({ node: card }: { node: any }) => {
|
||||
return {
|
||||
...card,
|
||||
primaryButton: card.has_primary_button
|
||||
? makeButtonObject(card.primary_button)
|
||||
: undefined,
|
||||
secondaryButton: card.has_secondary_button
|
||||
? makeButtonObject(card.secondary_button)
|
||||
: undefined,
|
||||
}
|
||||
switch (block.__typename) {
|
||||
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksDynamicContent:
|
||||
return {
|
||||
...block,
|
||||
dynamic_content: {
|
||||
...block.dynamic_content,
|
||||
link: block.dynamic_content.link.pageConnection.edges.length
|
||||
? {
|
||||
text: block.dynamic_content.link.text,
|
||||
href: removeMultipleSlashes(
|
||||
`/${block.dynamic_content.link.pageConnection.edges[0].node.system.locale}/${block.dynamic_content.link.pageConnection.edges[0].node.url}`
|
||||
),
|
||||
title:
|
||||
block.dynamic_content.link.pageConnection.edges[0]
|
||||
.node.title,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts:
|
||||
return {
|
||||
...block,
|
||||
shortcuts: {
|
||||
...block.shortcuts,
|
||||
shortcuts: block.shortcuts.shortcuts.map((shortcut: any) => ({
|
||||
text: shortcut.text,
|
||||
openInNewTab: shortcut.open_in_new_tab,
|
||||
...shortcut.linkConnection.edges[0].node,
|
||||
url:
|
||||
shortcut.linkConnection.edges[0].node.web?.original_url ||
|
||||
removeMultipleSlashes(
|
||||
`/${shortcut.linkConnection.edges[0].node.system.locale}/${shortcut.linkConnection.edges[0].node.url}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
}
|
||||
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
|
||||
return {
|
||||
...block,
|
||||
cards_grid: {
|
||||
...block.cards_grid,
|
||||
cards: block.cards_grid.cardConnection.edges.map(
|
||||
({ node: card }: { node: any }) => {
|
||||
return {
|
||||
...card,
|
||||
primaryButton: card.has_primary_button
|
||||
? makeButtonObject(card.primary_button)
|
||||
: undefined,
|
||||
secondaryButton: card.has_secondary_button
|
||||
? makeButtonObject(card.secondary_button)
|
||||
: undefined,
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
default:
|
||||
return block
|
||||
}
|
||||
})
|
||||
}
|
||||
),
|
||||
},
|
||||
}
|
||||
default:
|
||||
return block
|
||||
}
|
||||
})
|
||||
: null
|
||||
|
||||
const loyaltyPage = {
|
||||
@@ -170,7 +169,11 @@ export const loyaltyPageQueryRouter = router({
|
||||
validateLoyaltyPageSchema.safeParse(loyaltyPage)
|
||||
|
||||
if (!validatedLoyaltyPage.success) {
|
||||
throw internalServerError(validatedLoyaltyPage.error)
|
||||
console.info(
|
||||
`Failed to validate Loyaltypage Data - (lang: ${lang}, uid: ${uid})`
|
||||
)
|
||||
console.error(validatedLoyaltyPage.error)
|
||||
return null
|
||||
}
|
||||
|
||||
// Assert LoyaltyPage type to get correct typings for RTE fields
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
GetNavigationMyPagesRefs,
|
||||
} from "@/lib/graphql/Query/NavigationMyPages.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { internalServerError, notFound } from "@/server/errors/trpc"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import { contentstackBaseProcedure, router } from "@/server/trpc"
|
||||
|
||||
import {
|
||||
@@ -71,7 +71,11 @@ export const navigationQueryRouter = router({
|
||||
const validatedMyPagesNavigationRefs =
|
||||
navigationRefsPayloadSchema.safeParse(refsResponse.data)
|
||||
if (!validatedMyPagesNavigationRefs.success) {
|
||||
throw internalServerError(validatedMyPagesNavigationRefs.error)
|
||||
console.info(
|
||||
`Failed to validate My Pages Navigation Refs - (lang: ${lang}`
|
||||
)
|
||||
console.error(validatedMyPagesNavigationRefs.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const connections = getConnections(validatedMyPagesNavigationRefs.data)
|
||||
@@ -99,7 +103,11 @@ export const navigationQueryRouter = router({
|
||||
response.data
|
||||
)
|
||||
if (!validatedMyPagesNavigation.success) {
|
||||
throw internalServerError(validatedMyPagesNavigation.error)
|
||||
console.info(
|
||||
`Failed to validate My Pages Navigation Data - (lang: ${lang}`
|
||||
)
|
||||
console.error(validatedMyPagesNavigation.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const menuItem =
|
||||
@@ -112,7 +120,11 @@ export const navigationQueryRouter = router({
|
||||
|
||||
const validatedNav = getNavigationSchema.safeParse(nav)
|
||||
if (!validatedNav.success) {
|
||||
throw internalServerError(validatedNav.error)
|
||||
console.info(
|
||||
`Failed to validate My Pages Navigation Return Data - (lang: ${lang}`
|
||||
)
|
||||
console.error(validatedNav.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return validatedNav.data
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
/**
|
||||
* Add User mutations
|
||||
*/
|
||||
@@ -104,17 +104,17 @@ export const getFriendTransactionsSchema = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
attributes: z.object({
|
||||
hotelOperaId: z.string(),
|
||||
confirmationNumber: z.string(),
|
||||
checkinDate: z.string(),
|
||||
checkoutDate: z.string(),
|
||||
nights: z.number(),
|
||||
awardPoints: z.number(),
|
||||
pointsCalculated: z.boolean(),
|
||||
awardPoints: z.number().default(0),
|
||||
checkinDate: z.string().default(""),
|
||||
checkoutDate: z.string().default(""),
|
||||
confirmationNumber: z.string().default(""),
|
||||
hotelOperaId: z.string().default(""),
|
||||
nights: z.number().default(1),
|
||||
pointsCalculated: z.boolean().default(true),
|
||||
hotelInformation: z
|
||||
.object({
|
||||
hotelName: z.string(),
|
||||
city: z.string(),
|
||||
city: z.string().default(""),
|
||||
name: z.string().default(""),
|
||||
hotelContent: z.object({
|
||||
images: z.object({
|
||||
metaData: z.object({
|
||||
@@ -135,29 +135,28 @@ export const getFriendTransactionsSchema = z.object({
|
||||
.optional(),
|
||||
}),
|
||||
relationships: z.object({
|
||||
booking: z.object({
|
||||
data: z.object({
|
||||
id: z.string().default(""),
|
||||
type: z.string().default(""),
|
||||
}),
|
||||
links: z.object({
|
||||
related: z.string().default(""),
|
||||
}),
|
||||
}),
|
||||
hotel: z
|
||||
.object({
|
||||
links: z.object({
|
||||
related: z.string(),
|
||||
}),
|
||||
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
id: z.string().default(""),
|
||||
type: z.string().default(""),
|
||||
}),
|
||||
links: z.object({
|
||||
related: z.string().default(""),
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
booking: z.object({
|
||||
links: z.object({
|
||||
related: z.string(),
|
||||
}),
|
||||
data: z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
type: z.string(),
|
||||
type: z.string().default(""),
|
||||
})
|
||||
),
|
||||
links: z
|
||||
@@ -169,7 +168,3 @@ export const getFriendTransactionsSchema = z.object({
|
||||
})
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
type GetFriendTransactionsData = z.infer<typeof getFriendTransactionsSchema>
|
||||
|
||||
export type Transaction = GetFriendTransactionsData["data"][number]
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import * as api from "@/lib/api"
|
||||
import {
|
||||
badRequestError,
|
||||
forbiddenError,
|
||||
internalServerError,
|
||||
notFound,
|
||||
unauthorizedError,
|
||||
} from "@/server/errors/trpc"
|
||||
import { protectedProcedure, router } from "@/server/trpc"
|
||||
|
||||
import { friendTransactionsInput, staysInput } from "./input"
|
||||
@@ -34,26 +27,36 @@ export const userQueryRouter = router({
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
switch (apiResponse.status) {
|
||||
case 400:
|
||||
throw badRequestError(apiResponse)
|
||||
case 401:
|
||||
throw unauthorizedError(apiResponse)
|
||||
case 403:
|
||||
throw forbiddenError(apiResponse)
|
||||
default:
|
||||
throw internalServerError(apiResponse)
|
||||
}
|
||||
// switch (apiResponse.status) {
|
||||
// case 400:
|
||||
// throw badRequestError(apiResponse)
|
||||
// case 401:
|
||||
// throw unauthorizedError(apiResponse)
|
||||
// case 403:
|
||||
// throw forbiddenError(apiResponse)
|
||||
// default:
|
||||
// throw internalServerError(apiResponse)
|
||||
// }
|
||||
console.info(`API Response Failed - Getting User`)
|
||||
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
if (!apiJson.data?.attributes) {
|
||||
throw notFound(apiJson)
|
||||
// throw notFound(apiJson)
|
||||
console.error(
|
||||
`User has no data - (user: ${JSON.stringify(ctx.session.user)})`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const verifiedData = getUserSchema.safeParse(apiJson.data.attributes)
|
||||
if (!verifiedData.success) {
|
||||
throw internalServerError(verifiedData.error)
|
||||
console.info(`Failed to validate User - (name: ${ctx.session.user?.name}`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -98,28 +101,35 @@ export const userQueryRouter = router({
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
switch (apiResponse.status) {
|
||||
case 400:
|
||||
throw badRequestError(apiResponse)
|
||||
case 401:
|
||||
throw unauthorizedError(apiResponse)
|
||||
case 403:
|
||||
throw forbiddenError(apiResponse)
|
||||
default:
|
||||
throw internalServerError(apiResponse)
|
||||
}
|
||||
// switch (apiResponse.status) {
|
||||
// case 400:
|
||||
// throw badRequestError(apiResponse)
|
||||
// case 401:
|
||||
// throw unauthorizedError(apiResponse)
|
||||
// case 403:
|
||||
// throw forbiddenError(apiResponse)
|
||||
// default:
|
||||
// throw internalServerError(apiResponse)
|
||||
// }
|
||||
console.info(`API Response Failed - Getting Previous Stays`)
|
||||
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
|
||||
const verifiedData = getStaysSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
throw internalServerError(verifiedData.error)
|
||||
console.info(`Failed to validate Previous Stays Data`)
|
||||
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const nextCursor =
|
||||
verifiedData.data.links &&
|
||||
verifiedData.data.links.offset < verifiedData.data.links.totalCount
|
||||
verifiedData.data.links.offset < verifiedData.data.links.totalCount
|
||||
? verifiedData.data.links.offset
|
||||
: undefined
|
||||
|
||||
@@ -152,27 +162,34 @@ export const userQueryRouter = router({
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
switch (apiResponse.status) {
|
||||
case 400:
|
||||
throw badRequestError(apiResponse)
|
||||
case 401:
|
||||
throw unauthorizedError(apiResponse)
|
||||
case 403:
|
||||
throw forbiddenError(apiResponse)
|
||||
default:
|
||||
throw internalServerError(apiResponse)
|
||||
}
|
||||
// switch (apiResponse.status) {
|
||||
// case 400:
|
||||
// throw badRequestError(apiResponse)
|
||||
// case 401:
|
||||
// throw unauthorizedError(apiResponse)
|
||||
// case 403:
|
||||
// throw forbiddenError(apiResponse)
|
||||
// default:
|
||||
// throw internalServerError(apiResponse)
|
||||
// }
|
||||
console.info(`API Response Failed - Getting Upcoming Stays`)
|
||||
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = getStaysSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
throw internalServerError(verifiedData.error)
|
||||
console.info(`Failed to validate Upcoming Stays Data`)
|
||||
console.info(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
const nextCursor =
|
||||
verifiedData.data.links &&
|
||||
verifiedData.data.links.offset < verifiedData.data.links.totalCount
|
||||
verifiedData.data.links.offset < verifiedData.data.links.totalCount
|
||||
? verifiedData.data.links.offset
|
||||
: undefined
|
||||
|
||||
@@ -186,76 +203,70 @@ export const userQueryRouter = router({
|
||||
friendTransactions: protectedProcedure
|
||||
.input(friendTransactionsInput)
|
||||
.query(async (opts) => {
|
||||
try {
|
||||
const { limit, cursor } = opts.input
|
||||
const { limit, cursor } = opts.input
|
||||
|
||||
const params = new URLSearchParams()
|
||||
params.set("limit", limit.toString())
|
||||
const params = new URLSearchParams()
|
||||
params.set("limit", limit.toString())
|
||||
|
||||
if (cursor) {
|
||||
params.set("offset", cursor.toString())
|
||||
}
|
||||
if (cursor) {
|
||||
params.set("offset", cursor.toString())
|
||||
}
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.friendTransactions,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${opts.ctx.session.token.access_token}`,
|
||||
},
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.friendTransactions,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${opts.ctx.session.token.access_token}`,
|
||||
},
|
||||
params
|
||||
)
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
switch (apiResponse.status) {
|
||||
case 400:
|
||||
throw badRequestError()
|
||||
case 401:
|
||||
throw unauthorizedError()
|
||||
case 403:
|
||||
throw forbiddenError()
|
||||
default:
|
||||
throw internalServerError()
|
||||
}
|
||||
}
|
||||
if (!apiResponse.ok) {
|
||||
// switch (apiResponse.status) {
|
||||
// case 400:
|
||||
// throw badRequestError()
|
||||
// case 401:
|
||||
// throw unauthorizedError()
|
||||
// case 403:
|
||||
// throw forbiddenError()
|
||||
// default:
|
||||
// throw internalServerError()
|
||||
// }
|
||||
console.info(`API Response Failed - Getting Friend Transactions`)
|
||||
console.info(`User: (${JSON.stringify(opts.ctx.session.user)})`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
// const apiJson = friendTransactionsMockJson
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
console.info(`Failed to validate Friend Transactions Data`)
|
||||
console.info(`User: (${JSON.stringify(opts.ctx.session.user)})`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!apiJson.data?.length) {
|
||||
// throw internalServerError()
|
||||
}
|
||||
const nextCursor =
|
||||
verifiedData.data.links &&
|
||||
verifiedData.data.links.offset < verifiedData.data.links.totalCount
|
||||
? verifiedData.data.links.offset
|
||||
: undefined
|
||||
|
||||
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
||||
|
||||
if (!verifiedData.success) {
|
||||
console.info(`Get Friend Transactions - Verified Data Error`)
|
||||
console.error(verifiedData.error)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
const nextCursor =
|
||||
verifiedData.data.links &&
|
||||
verifiedData.data.links.offset < verifiedData.data.links.totalCount
|
||||
? verifiedData.data.links.offset
|
||||
: undefined
|
||||
|
||||
return {
|
||||
data: verifiedData.data.data.map(({ attributes }) => ({
|
||||
return {
|
||||
data: verifiedData.data.data.map(({ attributes }) => {
|
||||
return {
|
||||
awardPoints: attributes.awardPoints,
|
||||
checkinDate: attributes.checkinDate,
|
||||
checkoutDate: attributes.checkoutDate,
|
||||
awardPoints: attributes.awardPoints,
|
||||
hotelName: attributes.hotelInformation?.hotelName,
|
||||
city: attributes.hotelInformation?.city,
|
||||
nights: attributes.nights,
|
||||
confirmationNumber: attributes.confirmationNumber,
|
||||
})),
|
||||
nextCursor,
|
||||
}
|
||||
} catch (error) {
|
||||
console.info(`Get Friend Transactions Error`)
|
||||
console.error(error)
|
||||
throw internalServerError()
|
||||
hotelName: attributes.hotelInformation?.name,
|
||||
nights: attributes.nights,
|
||||
}
|
||||
}),
|
||||
nextCursor,
|
||||
}
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { UserQueryRouter } from "../user"
|
||||
|
||||
export type TransactionResponse = Awaited<
|
||||
ReturnType<UserQueryRouter["transaction"]["friendTransactions"]>
|
||||
>
|
||||
export type TransactionsObject = NonNullable<TransactionResponse>
|
||||
export type Transactions = NonNullable<TransactionResponse>["data"]
|
||||
export type Transaction = NonNullable<TransactionResponse>["data"][number]
|
||||
|
||||
export type ClientEarnAndBurnProps = {
|
||||
initialData: TransactionsObject
|
||||
lang: Lang
|
||||
}
|
||||
|
||||
export type EarnAndBurnProps = {
|
||||
lang: Lang
|
||||
}
|
||||
|
||||
type Transaction = {
|
||||
checkinDate: string
|
||||
checkoutDate: string
|
||||
awardPoints: number
|
||||
hotelName?: string
|
||||
city?: string
|
||||
nights: number
|
||||
confirmationNumber: string
|
||||
export interface TableProps {
|
||||
lang: Lang
|
||||
transactions: Transactions
|
||||
}
|
||||
|
||||
export type Page = {
|
||||
data: Transaction[]
|
||||
nextCursor?: number
|
||||
}
|
||||
|
||||
export type RowProps = {
|
||||
export interface RowProps {
|
||||
lang: Lang
|
||||
transaction: Transaction
|
||||
}
|
||||
|
||||
15
types/components/myPages/stays/previous.ts
Normal file
15
types/components/myPages/stays/previous.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { UserQueryRouter } from "../user"
|
||||
|
||||
export type PreviousStaysResponse = Awaited<
|
||||
ReturnType<UserQueryRouter["stays"]["previous"]>
|
||||
>
|
||||
export type PreviousStaysNonNullResponseObject =
|
||||
NonNullable<PreviousStaysResponse>
|
||||
export type PreviousStays = NonNullable<PreviousStaysResponse>["data"]
|
||||
export type PreviousStay = NonNullable<PreviousStaysResponse>["data"][number]
|
||||
|
||||
export interface PreviousStaysClientProps {
|
||||
lang: Lang
|
||||
initialPreviousStays: PreviousStaysNonNullResponseObject
|
||||
}
|
||||
15
types/components/myPages/stays/upcoming.ts
Normal file
15
types/components/myPages/stays/upcoming.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { UserQueryRouter } from "../user"
|
||||
|
||||
export type UpcomingStaysResponse = Awaited<
|
||||
ReturnType<UserQueryRouter["stays"]["upcoming"]>
|
||||
>
|
||||
export type UpcomingStaysNonNullResponseObject =
|
||||
NonNullable<UpcomingStaysResponse>
|
||||
export type UpcomingStays = NonNullable<UpcomingStaysResponse>["data"]
|
||||
export type UpcomingStay = NonNullable<UpcomingStaysResponse>["data"][number]
|
||||
|
||||
export interface UpcomingStaysClientProps {
|
||||
lang: Lang
|
||||
initialUpcomingStays: UpcomingStaysNonNullResponseObject
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { userQueryRouter } from "@/server/routers/user/query"
|
||||
|
||||
import type { User } from "@/types/user"
|
||||
|
||||
export type UserQueryRouter = typeof userQueryRouter
|
||||
|
||||
export interface UserProps {
|
||||
user: User
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user