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[] }>) {
|
}: PageArgs<LangParams & { path: string[] }>) {
|
||||||
const accountPage = await serverClient().contentstack.accountPage.get()
|
const accountPage = await serverClient().contentstack.accountPage.get()
|
||||||
const { formatMessage } = await getIntl()
|
const { formatMessage } = await getIntl()
|
||||||
|
|
||||||
|
if (!accountPage) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.blocks}>
|
<main className={styles.blocks}>
|
||||||
{accountPage.content.length ? (
|
{accountPage.content.length ? (
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
|
|||||||
|
|
||||||
export default async function LanguageSwitcherRoute() {
|
export default async function LanguageSwitcherRoute() {
|
||||||
const data = await serverClient().contentstack.languageSwitcher.get()
|
const data = await serverClient().contentstack.languageSwitcher.get()
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return <LanguageSwitcher urls={data.urls} lang={data.lang} />
|
return <LanguageSwitcher urls={data.urls} lang={data.lang} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { ReactNode } from "react"
|
|
||||||
|
|
||||||
import Header from "@/components/Current/Header"
|
import Header from "@/components/Current/Header"
|
||||||
|
|
||||||
import { LangParams, PageArgs } from "@/types/params"
|
import { LangParams, PageArgs } from "@/types/params"
|
||||||
@@ -7,15 +5,8 @@ import { LangParams, PageArgs } from "@/types/params"
|
|||||||
export default function HeaderLayout({
|
export default function HeaderLayout({
|
||||||
params,
|
params,
|
||||||
languageSwitcher,
|
languageSwitcher,
|
||||||
children,
|
|
||||||
}: PageArgs<LangParams> & {
|
}: PageArgs<LangParams> & {
|
||||||
languageSwitcher: ReactNode
|
languageSwitcher: React.ReactNode
|
||||||
children: ReactNode
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return <Header lang={params.lang} languageSwitcher={languageSwitcher} />
|
||||||
<>
|
|
||||||
<Header lang={params.lang} languageSwitcher={languageSwitcher} />
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
|
|||||||
|
|
||||||
export default async function LanguageSwitcherRoute() {
|
export default async function LanguageSwitcherRoute() {
|
||||||
const data = await serverClient().contentstack.languageSwitcher.get()
|
const data = await serverClient().contentstack.languageSwitcher.get()
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return <LanguageSwitcher urls={data.urls} lang={data.lang} />
|
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) {
|
export default async function LoyaltyPage({ lang }: LangParams) {
|
||||||
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get()
|
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get()
|
||||||
|
if (!loyaltyPage) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<section className={styles.content}>
|
<section className={styles.content}>
|
||||||
{loyaltyPage.sidebar.length ? (
|
{loyaltyPage.sidebar.length ? (
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import { LangParams } from "@/types/params"
|
|||||||
|
|
||||||
export default async function MyPages({ lang }: LangParams) {
|
export default async function MyPages({ lang }: LangParams) {
|
||||||
const accountPage = await serverClient().contentstack.accountPage.get()
|
const accountPage = await serverClient().contentstack.accountPage.get()
|
||||||
|
if (!accountPage) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const linkToOverview = `/${lang}/webview${accountPage.url}` !== overview[lang]
|
const linkToOverview = `/${lang}/webview${accountPage.url}` !== overview[lang]
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { LangParams } from "@/types/params"
|
|||||||
|
|
||||||
export default async function AboutScandicFriends({ lang }: LangParams) {
|
export default async function AboutScandicFriends({ lang }: LangParams) {
|
||||||
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get()
|
const loyaltyPage = await serverClient().contentstack.loyaltyPage.get()
|
||||||
|
if (!loyaltyPage) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<section className={styles.content}>
|
<section className={styles.content}>
|
||||||
<LinkToOverview lang={lang} />
|
<LinkToOverview lang={lang} />
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import { LangParams } from "@/types/params"
|
|||||||
|
|
||||||
export default async function Footer({ lang }: LangParams) {
|
export default async function Footer({ lang }: LangParams) {
|
||||||
const footerData = await serverClient().contentstack.base.footer({ lang })
|
const footerData = await serverClient().contentstack.base.footer({ lang })
|
||||||
|
if (!footerData) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<footer className={styles.container}>
|
<footer className={styles.container}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export default async function Header({
|
|||||||
})
|
})
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const homeHref = homeHrefs[env.NODE_ENV][lang]
|
const homeHref = homeHrefs[env.NODE_ENV][lang]
|
||||||
const { frontpage_link_text, logo, menu, top_menu } = data
|
const { frontpage_link_text, logo, menu, top_menu } = data
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ async function DynamicComponentBlock({ component }: DynamicComponentProps) {
|
|||||||
const session = await auth()
|
const session = await auth()
|
||||||
|
|
||||||
const user = session ? await serverClient().user.get() : null
|
const user = session ? await serverClient().user.get() : null
|
||||||
|
if (!user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
switch (component) {
|
switch (component) {
|
||||||
case LoyaltyComponentEnum.how_it_works:
|
case LoyaltyComponentEnum.how_it_works:
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import type { ContactRowProps } from "@/types/components/loyalty/sidebar"
|
|||||||
|
|
||||||
export default async function ContactRow({ contact }: ContactRowProps) {
|
export default async function ContactRow({ contact }: ContactRowProps) {
|
||||||
const data = await serverClient().contentstack.base.contact()
|
const data = await serverClient().contentstack.base.contact()
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const val = getValueFromContactConfig(contact.contact_field, data)
|
const val = getValueFromContactConfig(contact.contact_field, data)
|
||||||
|
|
||||||
|
|||||||
@@ -30,15 +30,19 @@ export default async function JoinLoyaltyContact({
|
|||||||
{block.preamble ? (
|
{block.preamble ? (
|
||||||
<Body textAlign="center">{block.preamble}</Body>
|
<Body textAlign="center">{block.preamble}</Body>
|
||||||
) : null}
|
) : null}
|
||||||
<Button asChild className={styles.link} intent="primary">
|
<Button asChild intent="primary">
|
||||||
<Link href="#">{formatMessage({ id: "Join Scandic Friends" })}</Link>
|
<Body asChild fontOnly textAlign="center" textTransform="bold">
|
||||||
</Button>
|
|
||||||
<Link href={login[lang]}>
|
<Link href={login[lang]}>
|
||||||
<Footnote textAlign="center" textTransform="bold">
|
{formatMessage({ id: "Join Scandic Friends" })}
|
||||||
|
</Link>
|
||||||
|
</Body>
|
||||||
|
</Button>
|
||||||
|
<Footnote asChild fontOnly textAlign="center" textTransform="bold">
|
||||||
|
<Link color="burgundy" href={`/${lang}/login`}>
|
||||||
{formatMessage({ id: "Already a friend?" })} <br />
|
{formatMessage({ id: "Already a friend?" })} <br />
|
||||||
{formatMessage({ id: "Click here to log in" })}
|
{formatMessage({ id: "Click here to log in" })}
|
||||||
</Footnote>
|
|
||||||
</Link>
|
</Link>
|
||||||
|
</Footnote>
|
||||||
</article>
|
</article>
|
||||||
{block.contact ? <Contact contactBlock={block.contact} /> : null}
|
{block.contact ? <Contact contactBlock={block.contact} /> : null}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -10,12 +10,3 @@
|
|||||||
gap: var(--Spacing-x5);
|
gap: var(--Spacing-x5);
|
||||||
padding: var(--Spacing-x4) var(--Spacing-x2) 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 (
|
return (
|
||||||
<JoinLoyaltyContact
|
<JoinLoyaltyContact
|
||||||
block={block.join_loyalty_contact}
|
block={block.join_loyalty_contact}
|
||||||
|
key={`${block.__typename}-${idx}`}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
key={`${block.join_loyalty_contact.title}-${idx}`}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -17,29 +17,6 @@
|
|||||||
grid-area: header;
|
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 {
|
.section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
@@ -121,17 +98,6 @@
|
|||||||
grid-template-columns: 1fr;
|
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 {
|
.victories {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: var(--card-height) 1fr 1fr;
|
grid-template-rows: var(--card-height) 1fr 1fr;
|
||||||
@@ -139,20 +105,11 @@
|
|||||||
|
|
||||||
.circle {
|
.circle {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--some-white-color, #fff);
|
background-color: var(--Main-Grey-White);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 2rem;
|
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 Image from "@/components/Image"
|
||||||
|
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
@@ -25,8 +26,10 @@ export default async function Challenges({
|
|||||||
<section className={styles.journeys}>
|
<section className={styles.journeys}>
|
||||||
{journeys.map((journey) => (
|
{journeys.map((journey) => (
|
||||||
<article className={styles.journey} key={journey.title}>
|
<article className={styles.journey} key={journey.title}>
|
||||||
<p className={styles.subtitle}>{journey.tag}</p>
|
<BiroScript color="black">{journey.tag}</BiroScript>
|
||||||
<h4 className={styles.title}>{journey.title}</h4>
|
<Title as="h5" level="h4">
|
||||||
|
{journey.title}
|
||||||
|
</Title>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
@@ -48,8 +51,10 @@ export default async function Challenges({
|
|||||||
width={12}
|
width={12}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.subtitle}>{victory.tag}</p>
|
<BiroScript color="black">{victory.tag}</BiroScript>
|
||||||
<h4 className={styles.title}>{victory.title}</h4>
|
<Title as="h5" level="h4">
|
||||||
|
{victory.title}
|
||||||
|
</Title>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ export default async function Overview({
|
|||||||
title,
|
title,
|
||||||
}: AccountPageComponentProps) {
|
}: AccountPageComponentProps) {
|
||||||
const user = await serverClient().user.get()
|
const user = await serverClient().user.get()
|
||||||
|
if (!user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<section className={styles.container}>
|
<section className={styles.container}>
|
||||||
<Header link={link} subtitle={subtitle} title={title} topTitle />
|
<Header link={link} subtitle={subtitle} title={title} topTitle />
|
||||||
|
|||||||
@@ -16,16 +16,19 @@ async function CurrentPointsBalance({
|
|||||||
}: AccountPageComponentProps) {
|
}: AccountPageComponentProps) {
|
||||||
const user = await serverClient().user.get()
|
const user = await serverClient().user.get()
|
||||||
const { formatMessage } = await getIntl()
|
const { formatMessage } = await getIntl()
|
||||||
|
if (!user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
const membership = getMembership(user.memberships)
|
const membership = getMembership(user.memberships)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header title={title} link={link} subtitle={subtitle} />
|
<Header title={title} link={link} subtitle={subtitle} />
|
||||||
|
|
||||||
<div className={styles.card}>
|
<div className={styles.card}>
|
||||||
<h2>{`${formatMessage({ id: "Total points" })}*`}</h2>
|
<h2>{`${formatMessage({ id: "Total Points" })}*`}</h2>
|
||||||
<p
|
<p className={styles.points}>
|
||||||
className={styles.points}
|
{`${formatMessage({ id: "Points" })}: ${membership?.currentPoints || "N/A"}`}
|
||||||
>{`${formatMessage({ id: "Points" })}: ${membership?.currentPoints || "N/A"}`}</p>
|
</p>
|
||||||
<p className={styles.disclaimer}>
|
<p className={styles.disclaimer}>
|
||||||
{`*${formatMessage({ id: "Points may take up to 10 days to be displayed." })}`}
|
{`*${formatMessage({ id: "Points may take up to 10 days to be displayed." })}`}
|
||||||
</p>
|
</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;
|
display: grid;
|
||||||
gap: var(--Spacing-x3);
|
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 { serverClient } from "@/lib/trpc/server"
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
|
||||||
|
|
||||||
import Header from "@/components/SectionHeader"
|
import Header from "@/components/SectionHeader"
|
||||||
|
|
||||||
|
import ClientEarnAndBurn from "./Client"
|
||||||
|
|
||||||
import styles from "./earnAndBurn.module.css"
|
import styles from "./earnAndBurn.module.css"
|
||||||
|
|
||||||
import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||||
import { Page, RowProps } from "@/types/components/myPages/myPage/earnAndBurn"
|
|
||||||
|
|
||||||
const tableHeadings = [
|
export default async function EarnAndBurn({
|
||||||
"Arrival date",
|
|
||||||
"Description",
|
|
||||||
"Booking number",
|
|
||||||
"Transaction date",
|
|
||||||
"Points",
|
|
||||||
]
|
|
||||||
|
|
||||||
function EarnAndBurn({
|
|
||||||
lang,
|
lang,
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
link,
|
link,
|
||||||
|
subtitle,
|
||||||
|
title,
|
||||||
}: AccountPageComponentProps) {
|
}: AccountPageComponentProps) {
|
||||||
const intl = useIntl()
|
const initialTransactions =
|
||||||
const { data, hasNextPage, fetchNextPage } =
|
await serverClient().user.transaction.friendTransactions({ limit: 5 })
|
||||||
trpc.user.transaction.friendTransactions.useInfiniteQuery(
|
if (!initialTransactions) {
|
||||||
{ limit: 5 },
|
return null
|
||||||
{
|
|
||||||
getNextPageParam: (lastPage: Page) => lastPage.nextCursor,
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
function loadMoreData() {
|
|
||||||
if (hasNextPage) {
|
|
||||||
fetchNextPage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const transactions = data?.pages.flatMap((page) => page.data) ?? []
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Header title={title} link={link} subtitle={subtitle} />
|
<Header title={title} link={link} subtitle={subtitle} />
|
||||||
|
<ClientEarnAndBurn initialData={initialTransactions} lang={lang} />
|
||||||
<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>
|
|
||||||
</div>
|
</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 SectionHeader from "@/components/SectionHeader"
|
||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
|
||||||
import Header from "@/components/SectionHeader"
|
|
||||||
import Grids from "@/components/TempDesignSystem/Grids"
|
|
||||||
|
|
||||||
import Container from "../Container"
|
import Container from "../Container"
|
||||||
import ListContainer from "../ListContainer"
|
import ClientPreviousStays from "./Client"
|
||||||
import ShowMoreButton from "../ShowMoreButton"
|
|
||||||
import StayCard from "../StayCard"
|
|
||||||
import EmptyPreviousStaysBlock from "./EmptyPreviousStays"
|
|
||||||
|
|
||||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||||
|
|
||||||
export default function PreviousStays({
|
export default async function PreviousStays({
|
||||||
lang,
|
lang,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
link,
|
link,
|
||||||
}: AccountPageComponentProps) {
|
}: AccountPageComponentProps) {
|
||||||
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
|
const initialPreviousStays = await serverClient().user.stays.previous()
|
||||||
trpc.user.stays.previous.useInfiniteQuery(
|
if (!initialPreviousStays?.data) {
|
||||||
{},
|
return null
|
||||||
{
|
|
||||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
function loadMoreData() {
|
|
||||||
if (hasNextPage) {
|
|
||||||
fetchNextPage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stays = data?.pages.flatMap((page) => page.data) ?? []
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Header title={title} subtitle={subtitle} link={link} />
|
<SectionHeader title={title} subtitle={subtitle} link={link} />
|
||||||
{isLoading ? (
|
<ClientPreviousStays
|
||||||
<LoadingSpinner />
|
initialPreviousStays={initialPreviousStays}
|
||||||
) : stays.length ? (
|
|
||||||
<ListContainer>
|
|
||||||
<Grids.Stackable>
|
|
||||||
{stays.map((stay) => (
|
|
||||||
<StayCard
|
|
||||||
key={stay.attributes.confirmationNumber}
|
|
||||||
lang={lang}
|
lang={lang}
|
||||||
stay={stay}
|
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</Grids.Stackable>
|
|
||||||
{hasNextPage ? (
|
|
||||||
<ShowMoreButton disabled={isFetching} loadMoreData={loadMoreData} />
|
|
||||||
) : null}
|
|
||||||
</ListContainer>
|
|
||||||
) : (
|
|
||||||
<EmptyPreviousStaysBlock />
|
|
||||||
)}
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,17 @@ export default async function SoonestStays({
|
|||||||
subtitle,
|
subtitle,
|
||||||
link,
|
link,
|
||||||
}: AccountPageComponentProps) {
|
}: 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 (
|
return (
|
||||||
<section className={styles.container}>
|
<section className={styles.container}>
|
||||||
<Header title={title} subtitle={subtitle} link={link} />
|
<Header title={title} subtitle={subtitle} link={link} />
|
||||||
{stays.length ? (
|
{response.data.length ? (
|
||||||
<Grids.Stackable>
|
<Grids.Stackable>
|
||||||
{stays.map((stay) => (
|
{response.data.map((stay) => (
|
||||||
<StayCard
|
<StayCard
|
||||||
key={stay.attributes.confirmationNumber}
|
key={stay.attributes.confirmationNumber}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Calendar } from "react-feather"
|
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
|
import { CalendarIcon } from "@/components/Icons"
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
|
||||||
import styles from "./stay.module.css"
|
import styles from "./stay.module.css"
|
||||||
@@ -33,14 +33,14 @@ export default function StayCard({ stay, lang }: StayCardProps) {
|
|||||||
{hotelInformation.hotelName}
|
{hotelInformation.hotelName}
|
||||||
</Title>
|
</Title>
|
||||||
<div className={styles.date}>
|
<div className={styles.date}>
|
||||||
<Calendar
|
<CalendarIcon color="burgundy" />
|
||||||
height={20}
|
<Caption asChild>
|
||||||
width={20}
|
|
||||||
color="var(--Scandic-Brand-Burgundy)"
|
|
||||||
/>
|
|
||||||
<time dateTime={arrivalDateTime}>{arrivalDate}</time>
|
<time dateTime={arrivalDateTime}>{arrivalDate}</time>
|
||||||
|
</Caption>
|
||||||
{" - "}
|
{" - "}
|
||||||
|
<Caption asChild>
|
||||||
<time dateTime={departDateTime}>{departDate}</time>
|
<time dateTime={departDateTime}>{departDate}</time>
|
||||||
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -36,8 +36,4 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x-half);
|
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 SectionHeader from "@/components/SectionHeader"
|
||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
|
||||||
import Header from "@/components/SectionHeader"
|
|
||||||
import Grids from "@/components/TempDesignSystem/Grids"
|
|
||||||
|
|
||||||
import Container from "../Container"
|
import Container from "../Container"
|
||||||
import ListContainer from "../ListContainer"
|
import ClientUpcomingStays from "./Client"
|
||||||
import ShowMoreButton from "../ShowMoreButton"
|
|
||||||
import StayCard from "../StayCard"
|
|
||||||
import EmptyUpcomingStaysBlock from "./EmptyUpcomingStays"
|
|
||||||
|
|
||||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||||
|
|
||||||
export default function UpcomingStays({
|
export default async function UpcomingStays({
|
||||||
lang,
|
lang,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
link,
|
link,
|
||||||
}: AccountPageComponentProps) {
|
}: AccountPageComponentProps) {
|
||||||
const { data, hasNextPage, isFetching, fetchNextPage, isLoading } =
|
const initialUpcomingStays = await serverClient().user.stays.upcoming()
|
||||||
trpc.user.stays.upcoming.useInfiniteQuery(
|
if (!initialUpcomingStays?.data) {
|
||||||
{ limit: 6 },
|
return null
|
||||||
{
|
|
||||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
function loadMoreData() {
|
|
||||||
if (hasNextPage) {
|
|
||||||
fetchNextPage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stays = data?.pages.flatMap((page) => page.data) ?? []
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Header title={title} subtitle={subtitle} link={link} />
|
<SectionHeader title={title} subtitle={subtitle} link={link} />
|
||||||
{isLoading ? (
|
<ClientUpcomingStays
|
||||||
<LoadingSpinner />
|
initialUpcomingStays={initialUpcomingStays}
|
||||||
) : stays.length ? (
|
|
||||||
<ListContainer>
|
|
||||||
<Grids.Stackable>
|
|
||||||
{stays.map((stay) => (
|
|
||||||
<StayCard
|
|
||||||
key={stay.attributes.confirmationNumber}
|
|
||||||
lang={lang}
|
lang={lang}
|
||||||
stay={stay}
|
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</Grids.Stackable>
|
|
||||||
{hasNextPage ? (
|
|
||||||
<ShowMoreButton disabled={isFetching} loadMoreData={loadMoreData} />
|
|
||||||
) : null}
|
|
||||||
</ListContainer>
|
|
||||||
) : (
|
|
||||||
<EmptyUpcomingStaysBlock />
|
|
||||||
)}
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { serverClient } from "@/lib/trpc/server"
|
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 Link from "@/components/TempDesignSystem/Link"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
|
|
||||||
@@ -8,6 +8,9 @@ import styles from "./breadcrumbs.module.css"
|
|||||||
|
|
||||||
export default async function Breadcrumbs() {
|
export default async function Breadcrumbs() {
|
||||||
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get()
|
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get()
|
||||||
|
if (!breadcrumbs) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
const homeBreadcrumb = breadcrumbs.shift()
|
const homeBreadcrumb = breadcrumbs.shift()
|
||||||
return (
|
return (
|
||||||
<nav className={styles.breadcrumbs}>
|
<nav className={styles.breadcrumbs}>
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import type { LangParams } from "@/types/params"
|
|||||||
export default async function Sidebar({ lang }: LangParams) {
|
export default async function Sidebar({ lang }: LangParams) {
|
||||||
const navigation = await serverClient().contentstack.myPages.navigation.get()
|
const navigation = await serverClient().contentstack.myPages.navigation.get()
|
||||||
const { formatMessage } = await getIntl()
|
const { formatMessage } = await getIntl()
|
||||||
|
if (!navigation) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<aside className={styles.sidebar}>
|
<aside className={styles.sidebar}>
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import Form from "./Form"
|
|||||||
|
|
||||||
export default async function EditProfile() {
|
export default async function EditProfile() {
|
||||||
const user = await serverClient().user.get()
|
const user = await serverClient().user.get()
|
||||||
|
if (!user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Container user={user}>
|
<Container user={user}>
|
||||||
<Form user={user} />
|
<Form user={user} />
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import styles from "./profile.module.css"
|
|||||||
export default async function Profile() {
|
export default async function Profile() {
|
||||||
// const { formatMessage } = await getIntl()
|
// const { formatMessage } = await getIntl()
|
||||||
const user = await serverClient().user.get()
|
const user = await serverClient().user.get()
|
||||||
|
if (!user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
// const countryName = countries.find(
|
// const countryName = countries.find(
|
||||||
// (country) => country.code === user.address.country
|
// (country) => country.code === user.address.country
|
||||||
// )
|
// )
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
/* TODO: Waiting for variables for buttons from Design team */
|
||||||
font-family: var(--typography-Body-Regular-fontFamily);
|
font-family: var(--typography-Body-Regular-fontFamily);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 150%;
|
line-height: 150%;
|
||||||
|
|||||||
@@ -41,13 +41,8 @@
|
|||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scriptedTitle {
|
span.scriptedTitle {
|
||||||
color: var(--script-color);
|
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);
|
padding: var(--Spacing-x1);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
transform: rotate(-3deg);
|
transform: rotate(-3deg);
|
||||||
@@ -57,13 +52,8 @@
|
|||||||
color: var(--font-color);
|
color: var(--font-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bodyText {
|
p.bodyText {
|
||||||
color: var(--font-color);
|
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 {
|
.buttonContainer {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
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 Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
|
||||||
import { cardVariants } from "./variants"
|
import { cardVariants } from "./variants"
|
||||||
@@ -42,13 +44,20 @@ export default function Card({
|
|||||||
>
|
>
|
||||||
{scriptedTopTitle ? (
|
{scriptedTopTitle ? (
|
||||||
<section className={styles.scriptContainer}>
|
<section className={styles.scriptContainer}>
|
||||||
<h3 className={styles.scriptedTitle}>{scriptedTopTitle}</h3>
|
<BiroScript className={styles.scriptedTitle} type="two">
|
||||||
|
{scriptedTopTitle}
|
||||||
|
</BiroScript>
|
||||||
|
<Divider />
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
<Title as="h5" className={styles.heading} level="h3">
|
<Title as="h5" className={styles.heading} level="h3">
|
||||||
{heading}
|
{heading}
|
||||||
</Title>
|
</Title>
|
||||||
{bodyText ? <p className={styles.bodyText}>{bodyText}</p> : null}
|
{bodyText ? (
|
||||||
|
<Body className={styles.bodyText} textAlign="center">
|
||||||
|
{bodyText}
|
||||||
|
</Body>
|
||||||
|
) : null}
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
{primaryButton ? (
|
{primaryButton ? (
|
||||||
<Button asChild theme={buttonTheme} size="small">
|
<Button asChild theme={buttonTheme} size="small">
|
||||||
|
|||||||
@@ -23,12 +23,6 @@
|
|||||||
.input,
|
.input,
|
||||||
.listBoxItem {
|
.listBoxItem {
|
||||||
color: var(--UI-Grey-60);
|
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 {
|
.button {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
import { useController, useFormContext } from "react-hook-form"
|
import { useController, useFormContext } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
import SelectChevron from "../SelectChevron"
|
import SelectChevron from "../SelectChevron"
|
||||||
import { countries } from "./countries"
|
import { countries } from "./countries"
|
||||||
|
|
||||||
@@ -53,11 +55,13 @@ export default function CountrySelect({
|
|||||||
selectedKey={field.value}
|
selectedKey={field.value}
|
||||||
>
|
>
|
||||||
<div className={styles.comboBoxContainer}>
|
<div className={styles.comboBoxContainer}>
|
||||||
|
<Body asChild fontOnly>
|
||||||
<Input
|
<Input
|
||||||
aria-label={selectCountryLabel}
|
aria-label={selectCountryLabel}
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
placeholder={selectCountryLabel}
|
placeholder={selectCountryLabel}
|
||||||
/>
|
/>
|
||||||
|
</Body>
|
||||||
<Button className={styles.button}>
|
<Button className={styles.button}>
|
||||||
<SelectChevron />
|
<SelectChevron />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -79,14 +83,15 @@ export default function CountrySelect({
|
|||||||
>
|
>
|
||||||
<ListBox>
|
<ListBox>
|
||||||
{countries.map((country, idx) => (
|
{countries.map((country, idx) => (
|
||||||
|
<Body asChild fontOnly key={`${country.code}-${idx}`}>
|
||||||
<ListBoxItem
|
<ListBoxItem
|
||||||
aria-label={country.name}
|
aria-label={country.name}
|
||||||
className={styles.listBoxItem}
|
className={styles.listBoxItem}
|
||||||
id={country.code}
|
id={country.code}
|
||||||
key={`${country.code}-${idx}`}
|
|
||||||
>
|
>
|
||||||
{country.name}
|
{country.name}
|
||||||
</ListBoxItem>
|
</ListBoxItem>
|
||||||
|
</Body>
|
||||||
))}
|
))}
|
||||||
</ListBox>
|
</ListBox>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
.message {
|
.message {
|
||||||
color: var(--Scandic-Red-60);
|
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;
|
margin: var(--Spacing-x-half) 0 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
|
import { ErrorMessage as RHFErrorMessage } from "@hookform/error-message"
|
||||||
|
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
import styles from "./error.module.css"
|
import styles from "./error.module.css"
|
||||||
|
|
||||||
import type { ErrorMessageProps } from "./errorMessage"
|
import type { ErrorMessageProps } from "./errorMessage"
|
||||||
@@ -12,7 +14,11 @@ export default function ErrorMessage<T>({
|
|||||||
<RHFErrorMessage
|
<RHFErrorMessage
|
||||||
errors={errors}
|
errors={errors}
|
||||||
name={name}
|
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 { useController } from "react-hook-form"
|
||||||
|
|
||||||
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
import ErrorMessage from "@/components/TempDesignSystem/Form/ErrorMessage"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
import styles from "./input.module.css"
|
import styles from "./input.module.css"
|
||||||
|
|
||||||
@@ -35,11 +36,13 @@ export default function Input({
|
|||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
type={type}
|
type={type}
|
||||||
>
|
>
|
||||||
|
<Body asChild fontOnly>
|
||||||
<AriaInput
|
<AriaInput
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
ref={field.ref}
|
ref={field.ref}
|
||||||
/>
|
/>
|
||||||
|
</Body>
|
||||||
<ErrorMessage errors={formState.errors} name={name} />
|
<ErrorMessage errors={formState.errors} name={name} />
|
||||||
</TextField>
|
</TextField>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,12 +2,7 @@
|
|||||||
border: 2px solid var(--UI-Grey-60);
|
border: 2px solid var(--UI-Grey-60);
|
||||||
border-radius: var(--Corner-radius-Small);
|
border-radius: var(--Corner-radius-Small);
|
||||||
color: var(--UI-Grey-60);
|
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;
|
height: 40px;
|
||||||
letter-spacing: var(--typography-Body-Regular-letterSpacing);
|
|
||||||
line-height: var(--typography-Body-Regular-lineHeight);
|
|
||||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||||
width: min(280px, 100%);
|
width: min(280px, 100%);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "react-aria-components"
|
} from "react-aria-components"
|
||||||
|
|
||||||
|
import Body from "../../Text/Body"
|
||||||
|
import Footnote from "../../Text/Footnote"
|
||||||
import SelectChevron from "../SelectChevron"
|
import SelectChevron from "../SelectChevron"
|
||||||
|
|
||||||
import styles from "./select.module.css"
|
import styles from "./select.module.css"
|
||||||
@@ -48,13 +50,18 @@ export default function Select({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
selectedKey={value as Key}
|
selectedKey={value as Key}
|
||||||
>
|
>
|
||||||
|
<Body asChild fontOnly>
|
||||||
<Button className={styles.input}>
|
<Button className={styles.input}>
|
||||||
<div className={styles.inputContentWrapper}>
|
<div className={styles.inputContentWrapper}>
|
||||||
|
<Footnote asChild fontOnly>
|
||||||
<Label className={styles.label}>{label}</Label>
|
<Label className={styles.label}>{label}</Label>
|
||||||
|
</Footnote>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</div>
|
</div>
|
||||||
<SelectChevron />
|
<SelectChevron />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Body>
|
||||||
|
<Body asChild fontOnly>
|
||||||
<Popover
|
<Popover
|
||||||
className={styles.popover}
|
className={styles.popover}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
@@ -80,6 +87,7 @@ export default function Select({
|
|||||||
))}
|
))}
|
||||||
</ListBox>
|
</ListBox>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
</Body>
|
||||||
</ReactAriaSelect>
|
</ReactAriaSelect>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,10 +4,6 @@
|
|||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--Base-Text-UI-Placeholder);
|
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 {
|
.select {
|
||||||
@@ -32,11 +28,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Spacing-x-half);
|
gap: var(--Spacing-x-half);
|
||||||
height: 56px;
|
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;
|
outline: none;
|
||||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -56,7 +47,6 @@
|
|||||||
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.08);
|
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.08);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-size: var(--typography-Body-Regular-fontSize);
|
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -34,6 +34,10 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.black {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
.burgundy {
|
.burgundy {
|
||||||
color: var(--Scandic-Brand-Burgundy);
|
color: var(--Scandic-Brand-Burgundy);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import styles from "./biroScript.module.css"
|
|||||||
const config = {
|
const config = {
|
||||||
variants: {
|
variants: {
|
||||||
color: {
|
color: {
|
||||||
|
black: styles.black,
|
||||||
burgundy: styles.burgundy,
|
burgundy: styles.burgundy,
|
||||||
pale: styles.pale,
|
pale: styles.pale,
|
||||||
primaryLightOnSurfaceAccent: styles.plosa,
|
primaryLightOnSurfaceAccent: styles.plosa,
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bodyFontOnly {
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.bold {
|
.bold {
|
||||||
font-family: var(--typography-Body-Bold-fontFamily);
|
font-family: var(--typography-Body-Bold-fontFamily);
|
||||||
font-size: var(--typography-Body-Bold-fontSize);
|
font-size: var(--typography-Body-Bold-fontSize);
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export interface BodyProps
|
|||||||
extends Omit<React.HTMLAttributes<HTMLHeadingElement>, "color">,
|
extends Omit<React.HTMLAttributes<HTMLHeadingElement>, "color">,
|
||||||
VariantProps<typeof bodyVariants> {
|
VariantProps<typeof bodyVariants> {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
|
fontOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
|
||||||
import { bodyVariants } from "./variants"
|
import { bodyFontOnlyVariants, bodyVariants } from "./variants"
|
||||||
|
|
||||||
import type { BodyProps } from "./body"
|
import type { BodyProps } from "./body"
|
||||||
|
|
||||||
@@ -8,12 +8,19 @@ export default function Body({
|
|||||||
asChild = false,
|
asChild = false,
|
||||||
className = "",
|
className = "",
|
||||||
color,
|
color,
|
||||||
|
fontOnly = false,
|
||||||
textAlign,
|
textAlign,
|
||||||
textTransform,
|
textTransform,
|
||||||
...props
|
...props
|
||||||
}: BodyProps) {
|
}: BodyProps) {
|
||||||
const Comp = asChild ? Slot : "p"
|
const Comp = asChild ? Slot : "p"
|
||||||
const classNames = bodyVariants({
|
const classNames = fontOnly
|
||||||
|
? bodyFontOnlyVariants({
|
||||||
|
className,
|
||||||
|
textAlign,
|
||||||
|
textTransform,
|
||||||
|
})
|
||||||
|
: bodyVariants({
|
||||||
className,
|
className,
|
||||||
color,
|
color,
|
||||||
textAlign,
|
textAlign,
|
||||||
|
|||||||
@@ -27,3 +27,22 @@ const config = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const bodyVariants = cva(styles.body, config)
|
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;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.captionFontOnly {
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.bold {
|
.bold {
|
||||||
font-family: var(--typography-Caption-Bold-fontFamily);
|
font-family: var(--typography-Caption-Bold-fontFamily);
|
||||||
font-size: var(--typography-Caption-Bold-fontSize);
|
font-size: var(--typography-Caption-Bold-fontSize);
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export interface CaptionProps
|
|||||||
extends Omit<React.HTMLAttributes<HTMLHeadingElement>, "color">,
|
extends Omit<React.HTMLAttributes<HTMLHeadingElement>, "color">,
|
||||||
VariantProps<typeof captionVariants> {
|
VariantProps<typeof captionVariants> {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
|
fontOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
|
||||||
import { captionVariants } from "./variants"
|
import { captionVariants, fontOnlycaptionVariants } from "./variants"
|
||||||
|
|
||||||
import type { CaptionProps } from "./caption"
|
import type { CaptionProps } from "./caption"
|
||||||
|
|
||||||
@@ -8,11 +8,17 @@ export default function Caption({
|
|||||||
asChild = false,
|
asChild = false,
|
||||||
className = "",
|
className = "",
|
||||||
color,
|
color,
|
||||||
|
fontOnly = false,
|
||||||
textTransform,
|
textTransform,
|
||||||
...props
|
...props
|
||||||
}: CaptionProps) {
|
}: CaptionProps) {
|
||||||
const Comp = asChild ? Slot : "p"
|
const Comp = asChild ? Slot : "p"
|
||||||
const classNames = captionVariants({
|
const classNames = fontOnly
|
||||||
|
? fontOnlycaptionVariants({
|
||||||
|
className,
|
||||||
|
textTransform,
|
||||||
|
})
|
||||||
|
: captionVariants({
|
||||||
className,
|
className,
|
||||||
color,
|
color,
|
||||||
textTransform,
|
textTransform,
|
||||||
|
|||||||
@@ -21,3 +21,20 @@ const config = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const captionVariants = cva(styles.caption, config)
|
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;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footnoteFontOnly {
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.bold {
|
.bold {
|
||||||
font-family: var(--typography-Footnote-Bold-fontFamily);
|
font-family: var(--typography-Footnote-Bold-fontFamily);
|
||||||
font-size: var(--typography-Footnote-Bold-fontSize);
|
font-size: var(--typography-Footnote-Bold-fontSize);
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export interface FootnoteProps
|
|||||||
extends Omit<React.HTMLAttributes<HTMLParagraphElement>, "color">,
|
extends Omit<React.HTMLAttributes<HTMLParagraphElement>, "color">,
|
||||||
VariantProps<typeof footnoteVariants> {
|
VariantProps<typeof footnoteVariants> {
|
||||||
asChild?: boolean
|
asChild?: boolean
|
||||||
|
fontOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
|
||||||
import { footnoteVariants } from "./variants"
|
import { footnoteFontOnlyVariants, footnoteVariants } from "./variants"
|
||||||
|
|
||||||
import type { FootnoteProps } from "./footnote"
|
import type { FootnoteProps } from "./footnote"
|
||||||
|
|
||||||
@@ -8,12 +8,19 @@ export default function Footnote({
|
|||||||
asChild = false,
|
asChild = false,
|
||||||
className = "",
|
className = "",
|
||||||
color,
|
color,
|
||||||
|
fontOnly = false,
|
||||||
textAlign,
|
textAlign,
|
||||||
textTransform,
|
textTransform,
|
||||||
...props
|
...props
|
||||||
}: FootnoteProps) {
|
}: FootnoteProps) {
|
||||||
const Comp = asChild ? Slot : "p"
|
const Comp = asChild ? Slot : "p"
|
||||||
const classNames = footnoteVariants({
|
const classNames = fontOnly
|
||||||
|
? footnoteFontOnlyVariants({
|
||||||
|
className,
|
||||||
|
textAlign,
|
||||||
|
textTransform,
|
||||||
|
})
|
||||||
|
: footnoteVariants({
|
||||||
className,
|
className,
|
||||||
color,
|
color,
|
||||||
textAlign,
|
textAlign,
|
||||||
|
|||||||
@@ -24,3 +24,24 @@ const config = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const footnoteVariants = cva(styles.footnote, config)
|
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 { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { Children } from "react"
|
||||||
|
|
||||||
import { subtitleVariants } from "./variants"
|
import { subtitleVariants } from "./variants"
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export default function Subtitle({
|
|||||||
textTransform,
|
textTransform,
|
||||||
...props
|
...props
|
||||||
}: SubtitleProps) {
|
}: SubtitleProps) {
|
||||||
if (hideEmpty && !props.children) {
|
if (hideEmpty && Children.count(props.children) === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const Comp = asChild ? Slot : "p"
|
const Comp = asChild ? Slot : "p"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Children } from "react"
|
||||||
|
|
||||||
import { headingVariants } from "./variants"
|
import { headingVariants } from "./variants"
|
||||||
|
|
||||||
import type { HeadingProps } from "./title"
|
import type { HeadingProps } from "./title"
|
||||||
@@ -12,7 +14,7 @@ export default function Title({
|
|||||||
textAlign,
|
textAlign,
|
||||||
textTransform,
|
textTransform,
|
||||||
}: HeadingProps) {
|
}: HeadingProps) {
|
||||||
if (hideEmpty && !children) {
|
if (hideEmpty && Children.count(children) === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const Hx = level
|
const Hx = level
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { headingVariants } from "./variants"
|
|||||||
|
|
||||||
import type { VariantProps } from "class-variance-authority"
|
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
|
export interface HeadingProps
|
||||||
extends Omit<React.HTMLAttributes<HTMLHeadingElement>, "color">,
|
extends Omit<React.HTMLAttributes<HTMLHeadingElement>, "color">,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const config = {
|
|||||||
h3: styles.h3,
|
h3: styles.h3,
|
||||||
h4: styles.h4,
|
h4: styles.h4,
|
||||||
h5: styles.h5,
|
h5: styles.h5,
|
||||||
h6: styles.h6,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"Description": "Beskrivelse",
|
"Description": "Beskrivelse",
|
||||||
"Edit": "Redigere",
|
"Edit": "Redigere",
|
||||||
"Email": "E-mail",
|
"Email": "E-mail",
|
||||||
|
"Empty": "Empty",
|
||||||
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
||||||
"Find booking": "Find booking",
|
"Find booking": "Find booking",
|
||||||
"Get inspired": "Blive inspireret",
|
"Get inspired": "Blive inspireret",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"Description": "Beschreibung",
|
"Description": "Beschreibung",
|
||||||
"Edit": "Bearbeiten",
|
"Edit": "Bearbeiten",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
|
"Empty": "Empty",
|
||||||
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
||||||
"Find booking": "Buchung finden",
|
"Find booking": "Buchung finden",
|
||||||
"Get inspired": "Lass dich inspirieren",
|
"Get inspired": "Lass dich inspirieren",
|
||||||
|
|||||||
@@ -23,8 +23,10 @@
|
|||||||
"Description": "Description",
|
"Description": "Description",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
|
"Empty": "Empty",
|
||||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||||
"Find booking": "Find booking",
|
"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",
|
"Get inspired": "Get inspired",
|
||||||
"Go back to overview": "Go back to overview",
|
"Go back to overview": "Go back to overview",
|
||||||
"How it works": "How it works",
|
"How it works": "How it works",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"Description": "Kuvaus",
|
"Description": "Kuvaus",
|
||||||
"Edit": "Muokata",
|
"Edit": "Muokata",
|
||||||
"Email": "Sähköposti",
|
"Email": "Sähköposti",
|
||||||
|
"Empty": "Empty",
|
||||||
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
||||||
"Find booking": "Etsi varaus",
|
"Find booking": "Etsi varaus",
|
||||||
"Get inspired": "Inspiroidu",
|
"Get inspired": "Inspiroidu",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"Description": "Beskrivelse",
|
"Description": "Beskrivelse",
|
||||||
"Edit": "Redigere",
|
"Edit": "Redigere",
|
||||||
"Email": "E-post",
|
"Email": "E-post",
|
||||||
|
"Empty": "Empty",
|
||||||
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
||||||
"Find booking": "Finn booking",
|
"Find booking": "Finn booking",
|
||||||
"Get inspired": "Bli inspirert",
|
"Get inspired": "Bli inspirert",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"Description": "Beskrivning",
|
"Description": "Beskrivning",
|
||||||
"Edit": "Redigera",
|
"Edit": "Redigera",
|
||||||
"Email": "E-post",
|
"Email": "E-post",
|
||||||
|
"Empty": "Tom",
|
||||||
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
||||||
"Find booking": "Hitta bokning",
|
"Find booking": "Hitta bokning",
|
||||||
"Get inspired": "Bli inspirerad",
|
"Get inspired": "Bli inspirerad",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default function TrpcProvider({
|
|||||||
}),
|
}),
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
staleTime: 3000,
|
staleTime: 60 * 1000,
|
||||||
retry(failureCount, error) {
|
retry(failureCount, error) {
|
||||||
if (error instanceof TRPCClientError) {
|
if (error instanceof TRPCClientError) {
|
||||||
const appError: TRPCClientError<AnyTRPCRouter> = error
|
const appError: TRPCClientError<AnyTRPCRouter> = error
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
GetAccountPageRefs,
|
GetAccountPageRefs,
|
||||||
} from "@/lib/graphql/Query/AccountPage.graphql"
|
} from "@/lib/graphql/Query/AccountPage.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
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 { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -45,15 +45,14 @@ export const accountPageQueryRouter = router({
|
|||||||
throw notFound(refsResponse)
|
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 cleanedData = removeEmptyObjects(refsResponse.data)
|
||||||
|
|
||||||
const validatedAccountPageRefs =
|
const validatedAccountPageRefs =
|
||||||
validateAccountPageRefsSchema.safeParse(cleanedData)
|
validateAccountPageRefsSchema.safeParse(cleanedData)
|
||||||
if (!validatedAccountPageRefs.success) {
|
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)
|
const connections = getConnections(validatedAccountPageRefs.data)
|
||||||
@@ -81,7 +80,9 @@ export const accountPageQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!validatedAccountPage.success) {
|
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
|
// TODO: Make returned data nicer
|
||||||
|
|||||||
@@ -8,12 +8,8 @@ import {
|
|||||||
GetCurrentHeaderRef,
|
GetCurrentHeaderRef,
|
||||||
} from "@/lib/graphql/Query/CurrentHeader.graphql"
|
} from "@/lib/graphql/Query/CurrentHeader.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import { internalServerError, notFound } from "@/server/errors/trpc"
|
import { notFound } from "@/server/errors/trpc"
|
||||||
import {
|
import { contentstackBaseProcedure, router } from "@/server/trpc"
|
||||||
contentstackBaseProcedure,
|
|
||||||
publicProcedure,
|
|
||||||
router,
|
|
||||||
} from "@/server/trpc"
|
|
||||||
|
|
||||||
import { generateTag } from "@/utils/generateTag"
|
import { generateTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
@@ -47,12 +43,16 @@ export const baseQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!validatedContactConfigConfig.success) {
|
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]
|
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, {
|
const responseRef = await request<HeaderRefDataRaw>(GetCurrentHeaderRef, {
|
||||||
locale: input.lang,
|
locale: input.lang,
|
||||||
})
|
})
|
||||||
@@ -79,7 +79,9 @@ export const baseQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!validatedHeaderConfig.success) {
|
if (!validatedHeaderConfig.success) {
|
||||||
throw internalServerError(validatedHeaderConfig.error)
|
console.info(`Failed to validate Header - (lang: ${input.lang})`)
|
||||||
|
console.error(validatedHeaderConfig.error)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const logo =
|
const logo =
|
||||||
@@ -91,7 +93,7 @@ export const baseQueryRouter = router({
|
|||||||
logo,
|
logo,
|
||||||
} as HeaderData
|
} as HeaderData
|
||||||
}),
|
}),
|
||||||
footer: publicProcedure.input(langInput).query(async ({ input }) => {
|
footer: contentstackBaseProcedure.input(langInput).query(async ({ input }) => {
|
||||||
const responseRef = await request<FooterRefDataRaw>(GetCurrentFooterRef, {
|
const responseRef = await request<FooterRefDataRaw>(GetCurrentFooterRef, {
|
||||||
locale: input.lang,
|
locale: input.lang,
|
||||||
})
|
})
|
||||||
@@ -116,7 +118,9 @@ export const baseQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!validatedFooterConfig.success) {
|
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]
|
return validatedFooterConfig.data.all_current_footer.items[0]
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
GetMyPagesBreadcrumbs,
|
GetMyPagesBreadcrumbs,
|
||||||
GetMyPagesBreadcrumbsRefs,
|
GetMyPagesBreadcrumbsRefs,
|
||||||
} from "@/lib/graphql/Query/BreadcrumbsMyPages.graphql"
|
} from "@/lib/graphql/Query/BreadcrumbsMyPages.graphql"
|
||||||
import { internalServerError } from "@/server/errors/trpc"
|
|
||||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -41,7 +40,11 @@ async function getLoyaltyPageBreadcrumbs(variables: Variables) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!validatedRefsData.success) {
|
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)
|
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) {
|
if (!response.data.all_loyalty_page.items[0].web?.breadcrumbs?.title) {
|
||||||
return []
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedBreadcrumbsData =
|
const validatedBreadcrumbsData =
|
||||||
validateLoyaltyPageBreadcrumbsContentstackSchema.safeParse(response.data)
|
validateLoyaltyPageBreadcrumbsContentstackSchema.safeParse(response.data)
|
||||||
|
|
||||||
if (!validatedBreadcrumbsData.success) {
|
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(
|
return getBreadcrumbs(
|
||||||
@@ -80,7 +87,11 @@ async function getMyPagesBreadcrumbs(variables: Variables) {
|
|||||||
refsResponse.data
|
refsResponse.data
|
||||||
)
|
)
|
||||||
if (!validatedRefsData.success) {
|
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)
|
const tags = getTags(validatedRefsData.data.all_account_page, variables)
|
||||||
@@ -99,7 +110,11 @@ async function getMyPagesBreadcrumbs(variables: Variables) {
|
|||||||
validateMyPagesBreadcrumbsContentstackSchema.safeParse(response.data)
|
validateMyPagesBreadcrumbsContentstackSchema.safeParse(response.data)
|
||||||
|
|
||||||
if (!validatedBreadcrumbsData.success) {
|
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(
|
return getBreadcrumbs(
|
||||||
|
|||||||
@@ -119,7 +119,11 @@ export const languageSwitcherQueryRouter = router({
|
|||||||
validateLanguageSwitcherData.safeParse(urls)
|
validateLanguageSwitcherData.safeParse(urls)
|
||||||
|
|
||||||
if (!validatedLanguageSwitcherData.success) {
|
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 {
|
return {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
GetLoyaltyPageRefs,
|
GetLoyaltyPageRefs,
|
||||||
} from "@/lib/graphql/Query/LoyaltyPage.graphql"
|
} from "@/lib/graphql/Query/LoyaltyPage.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
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 { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -62,17 +62,16 @@ export const loyaltyPageQueryRouter = router({
|
|||||||
throw notFound(refsResponse)
|
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 cleanedData = removeEmptyObjects(refsResponse.data)
|
||||||
|
|
||||||
const validatedLoyaltyPageRefs =
|
const validatedLoyaltyPageRefs =
|
||||||
validateLoyaltyPageRefsSchema.safeParse(cleanedData)
|
validateLoyaltyPageRefsSchema.safeParse(cleanedData)
|
||||||
if (!validatedLoyaltyPageRefs.success) {
|
if (!validatedLoyaltyPageRefs.success) {
|
||||||
console.error("Bad validation for `GetLoyaltyPageRefs`")
|
console.info(
|
||||||
|
`Failed to validate Loyaltypage Refs - (lang: ${lang}, uid: ${uid})`
|
||||||
|
)
|
||||||
console.error(validatedLoyaltyPageRefs.error)
|
console.error(validatedLoyaltyPageRefs.error)
|
||||||
throw internalServerError(validatedLoyaltyPageRefs.error)
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const connections = getConnections(validatedLoyaltyPageRefs.data)
|
const connections = getConnections(validatedLoyaltyPageRefs.data)
|
||||||
@@ -170,7 +169,11 @@ export const loyaltyPageQueryRouter = router({
|
|||||||
validateLoyaltyPageSchema.safeParse(loyaltyPage)
|
validateLoyaltyPageSchema.safeParse(loyaltyPage)
|
||||||
|
|
||||||
if (!validatedLoyaltyPage.success) {
|
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
|
// Assert LoyaltyPage type to get correct typings for RTE fields
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
GetNavigationMyPagesRefs,
|
GetNavigationMyPagesRefs,
|
||||||
} from "@/lib/graphql/Query/NavigationMyPages.graphql"
|
} from "@/lib/graphql/Query/NavigationMyPages.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
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 { contentstackBaseProcedure, router } from "@/server/trpc"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -71,7 +71,11 @@ export const navigationQueryRouter = router({
|
|||||||
const validatedMyPagesNavigationRefs =
|
const validatedMyPagesNavigationRefs =
|
||||||
navigationRefsPayloadSchema.safeParse(refsResponse.data)
|
navigationRefsPayloadSchema.safeParse(refsResponse.data)
|
||||||
if (!validatedMyPagesNavigationRefs.success) {
|
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)
|
const connections = getConnections(validatedMyPagesNavigationRefs.data)
|
||||||
@@ -99,7 +103,11 @@ export const navigationQueryRouter = router({
|
|||||||
response.data
|
response.data
|
||||||
)
|
)
|
||||||
if (!validatedMyPagesNavigation.success) {
|
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 =
|
const menuItem =
|
||||||
@@ -112,7 +120,11 @@ export const navigationQueryRouter = router({
|
|||||||
|
|
||||||
const validatedNav = getNavigationSchema.safeParse(nav)
|
const validatedNav = getNavigationSchema.safeParse(nav)
|
||||||
if (!validatedNav.success) {
|
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
|
return validatedNav.data
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
/**
|
|
||||||
* Add User mutations
|
|
||||||
*/
|
|
||||||
@@ -104,17 +104,17 @@ export const getFriendTransactionsSchema = z.object({
|
|||||||
data: z.array(
|
data: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
attributes: z.object({
|
attributes: z.object({
|
||||||
hotelOperaId: z.string(),
|
awardPoints: z.number().default(0),
|
||||||
confirmationNumber: z.string(),
|
checkinDate: z.string().default(""),
|
||||||
checkinDate: z.string(),
|
checkoutDate: z.string().default(""),
|
||||||
checkoutDate: z.string(),
|
confirmationNumber: z.string().default(""),
|
||||||
nights: z.number(),
|
hotelOperaId: z.string().default(""),
|
||||||
awardPoints: z.number(),
|
nights: z.number().default(1),
|
||||||
pointsCalculated: z.boolean(),
|
pointsCalculated: z.boolean().default(true),
|
||||||
hotelInformation: z
|
hotelInformation: z
|
||||||
.object({
|
.object({
|
||||||
hotelName: z.string(),
|
city: z.string().default(""),
|
||||||
city: z.string(),
|
name: z.string().default(""),
|
||||||
hotelContent: z.object({
|
hotelContent: z.object({
|
||||||
images: z.object({
|
images: z.object({
|
||||||
metaData: z.object({
|
metaData: z.object({
|
||||||
@@ -135,29 +135,28 @@ export const getFriendTransactionsSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
relationships: z.object({
|
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
|
hotel: z
|
||||||
.object({
|
.object({
|
||||||
links: z.object({
|
|
||||||
related: z.string(),
|
|
||||||
}),
|
|
||||||
|
|
||||||
data: z.object({
|
data: z.object({
|
||||||
id: z.string(),
|
id: z.string().default(""),
|
||||||
type: z.string(),
|
type: z.string().default(""),
|
||||||
|
}),
|
||||||
|
links: z.object({
|
||||||
|
related: z.string().default(""),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
booking: z.object({
|
|
||||||
links: z.object({
|
|
||||||
related: z.string(),
|
|
||||||
}),
|
}),
|
||||||
data: z.object({
|
type: z.string().default(""),
|
||||||
id: z.string(),
|
|
||||||
type: z.string(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
type: z.string(),
|
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
links: z
|
links: z
|
||||||
@@ -169,7 +168,3 @@ export const getFriendTransactionsSchema = z.object({
|
|||||||
})
|
})
|
||||||
.nullable(),
|
.nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
type GetFriendTransactionsData = z.infer<typeof getFriendTransactionsSchema>
|
|
||||||
|
|
||||||
export type Transaction = GetFriendTransactionsData["data"][number]
|
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
import {
|
|
||||||
badRequestError,
|
|
||||||
forbiddenError,
|
|
||||||
internalServerError,
|
|
||||||
notFound,
|
|
||||||
unauthorizedError,
|
|
||||||
} from "@/server/errors/trpc"
|
|
||||||
import { protectedProcedure, router } from "@/server/trpc"
|
import { protectedProcedure, router } from "@/server/trpc"
|
||||||
|
|
||||||
import { friendTransactionsInput, staysInput } from "./input"
|
import { friendTransactionsInput, staysInput } from "./input"
|
||||||
@@ -34,26 +27,36 @@ export const userQueryRouter = router({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
switch (apiResponse.status) {
|
// switch (apiResponse.status) {
|
||||||
case 400:
|
// case 400:
|
||||||
throw badRequestError(apiResponse)
|
// throw badRequestError(apiResponse)
|
||||||
case 401:
|
// case 401:
|
||||||
throw unauthorizedError(apiResponse)
|
// throw unauthorizedError(apiResponse)
|
||||||
case 403:
|
// case 403:
|
||||||
throw forbiddenError(apiResponse)
|
// throw forbiddenError(apiResponse)
|
||||||
default:
|
// default:
|
||||||
throw internalServerError(apiResponse)
|
// 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()
|
const apiJson = await apiResponse.json()
|
||||||
if (!apiJson.data?.attributes) {
|
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)
|
const verifiedData = getUserSchema.safeParse(apiJson.data.attributes)
|
||||||
if (!verifiedData.success) {
|
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 {
|
return {
|
||||||
@@ -98,23 +101,30 @@ export const userQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
switch (apiResponse.status) {
|
// switch (apiResponse.status) {
|
||||||
case 400:
|
// case 400:
|
||||||
throw badRequestError(apiResponse)
|
// throw badRequestError(apiResponse)
|
||||||
case 401:
|
// case 401:
|
||||||
throw unauthorizedError(apiResponse)
|
// throw unauthorizedError(apiResponse)
|
||||||
case 403:
|
// case 403:
|
||||||
throw forbiddenError(apiResponse)
|
// throw forbiddenError(apiResponse)
|
||||||
default:
|
// default:
|
||||||
throw internalServerError(apiResponse)
|
// 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 apiJson = await apiResponse.json()
|
||||||
|
|
||||||
const verifiedData = getStaysSchema.safeParse(apiJson)
|
const verifiedData = getStaysSchema.safeParse(apiJson)
|
||||||
if (!verifiedData.success) {
|
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 =
|
const nextCursor =
|
||||||
@@ -152,22 +162,29 @@ export const userQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
switch (apiResponse.status) {
|
// switch (apiResponse.status) {
|
||||||
case 400:
|
// case 400:
|
||||||
throw badRequestError(apiResponse)
|
// throw badRequestError(apiResponse)
|
||||||
case 401:
|
// case 401:
|
||||||
throw unauthorizedError(apiResponse)
|
// throw unauthorizedError(apiResponse)
|
||||||
case 403:
|
// case 403:
|
||||||
throw forbiddenError(apiResponse)
|
// throw forbiddenError(apiResponse)
|
||||||
default:
|
// default:
|
||||||
throw internalServerError(apiResponse)
|
// 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 apiJson = await apiResponse.json()
|
||||||
const verifiedData = getStaysSchema.safeParse(apiJson)
|
const verifiedData = getStaysSchema.safeParse(apiJson)
|
||||||
if (!verifiedData.success) {
|
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 =
|
const nextCursor =
|
||||||
@@ -186,7 +203,6 @@ export const userQueryRouter = router({
|
|||||||
friendTransactions: protectedProcedure
|
friendTransactions: protectedProcedure
|
||||||
.input(friendTransactionsInput)
|
.input(friendTransactionsInput)
|
||||||
.query(async (opts) => {
|
.query(async (opts) => {
|
||||||
try {
|
|
||||||
const { limit, cursor } = opts.input
|
const { limit, cursor } = opts.input
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
@@ -207,31 +223,29 @@ export const userQueryRouter = router({
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!apiResponse.ok) {
|
if (!apiResponse.ok) {
|
||||||
switch (apiResponse.status) {
|
// switch (apiResponse.status) {
|
||||||
case 400:
|
// case 400:
|
||||||
throw badRequestError()
|
// throw badRequestError()
|
||||||
case 401:
|
// case 401:
|
||||||
throw unauthorizedError()
|
// throw unauthorizedError()
|
||||||
case 403:
|
// case 403:
|
||||||
throw forbiddenError()
|
// throw forbiddenError()
|
||||||
default:
|
// default:
|
||||||
throw internalServerError()
|
// 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 = await apiResponse.json()
|
||||||
// const apiJson = friendTransactionsMockJson
|
|
||||||
|
|
||||||
if (!apiJson.data?.length) {
|
|
||||||
// throw internalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
const verifiedData = getFriendTransactionsSchema.safeParse(apiJson)
|
||||||
|
|
||||||
if (!verifiedData.success) {
|
if (!verifiedData.success) {
|
||||||
console.info(`Get Friend Transactions - Verified Data Error`)
|
console.info(`Failed to validate Friend Transactions Data`)
|
||||||
|
console.info(`User: (${JSON.stringify(opts.ctx.session.user)})`)
|
||||||
console.error(verifiedData.error)
|
console.error(verifiedData.error)
|
||||||
throw badRequestError()
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextCursor =
|
const nextCursor =
|
||||||
@@ -241,21 +255,18 @@ export const userQueryRouter = router({
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: verifiedData.data.data.map(({ attributes }) => ({
|
data: verifiedData.data.data.map(({ attributes }) => {
|
||||||
|
return {
|
||||||
|
awardPoints: attributes.awardPoints,
|
||||||
checkinDate: attributes.checkinDate,
|
checkinDate: attributes.checkinDate,
|
||||||
checkoutDate: attributes.checkoutDate,
|
checkoutDate: attributes.checkoutDate,
|
||||||
awardPoints: attributes.awardPoints,
|
|
||||||
hotelName: attributes.hotelInformation?.hotelName,
|
|
||||||
city: attributes.hotelInformation?.city,
|
city: attributes.hotelInformation?.city,
|
||||||
nights: attributes.nights,
|
|
||||||
confirmationNumber: attributes.confirmationNumber,
|
confirmationNumber: attributes.confirmationNumber,
|
||||||
})),
|
hotelName: attributes.hotelInformation?.name,
|
||||||
nextCursor,
|
nights: attributes.nights,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}),
|
||||||
console.info(`Get Friend Transactions Error`)
|
nextCursor,
|
||||||
console.error(error)
|
|
||||||
throw internalServerError()
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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 = {
|
export type EarnAndBurnProps = {
|
||||||
lang: Lang
|
lang: Lang
|
||||||
}
|
}
|
||||||
|
|
||||||
type Transaction = {
|
export interface TableProps {
|
||||||
checkinDate: string
|
lang: Lang
|
||||||
checkoutDate: string
|
transactions: Transactions
|
||||||
awardPoints: number
|
|
||||||
hotelName?: string
|
|
||||||
city?: string
|
|
||||||
nights: number
|
|
||||||
confirmationNumber: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Page = {
|
export interface RowProps {
|
||||||
data: Transaction[]
|
|
||||||
nextCursor?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RowProps = {
|
|
||||||
lang: Lang
|
lang: Lang
|
||||||
transaction: Transaction
|
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"
|
import type { User } from "@/types/user"
|
||||||
|
|
||||||
|
export type UserQueryRouter = typeof userQueryRouter
|
||||||
|
|
||||||
export interface UserProps {
|
export interface UserProps {
|
||||||
user: User
|
user: User
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user