Merged in fix/header-parallel-route (pull request #238)

Static pages sync

Approved-by: Michael Zetterberg
This commit is contained in:
Christel Westerberg
2024-06-18 09:43:01 +00:00
committed by Michael Zetterberg
67 changed files with 759 additions and 278 deletions

View File

@@ -0,0 +1,14 @@
"use client"
import { useParams } from "next/navigation"
import { baseUrls } from "@/constants/routes/baseUrls"
import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
import { LangParams } from "@/types/params"
export default function Error() {
const params = useParams<LangParams>()
return <LanguageSwitcher urls={baseUrls} lang={params.lang} />
}

View File

@@ -0,0 +1,9 @@
import { serverClient } from "@/lib/trpc/server"
import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
export default async function LanguageSwitcherRoute() {
const data = await serverClient().contentstack.languageSwitcher.get()
return <LanguageSwitcher urls={data.urls} lang={data.lang} />
}

View File

@@ -0,0 +1,21 @@
import { ReactNode } from "react"
import Header from "@/components/Current/Header"
import { LangParams, PageArgs } from "@/types/params"
export default function HeaderLayout({
params,
languageSwitcher,
children,
}: PageArgs<LangParams> & {
languageSwitcher: ReactNode
children: ReactNode
}) {
return (
<>
<Header lang={params.lang} languageSwitcher={languageSwitcher} />
{children}
</>
)
}

View File

@@ -0,0 +1,3 @@
export default function EmptyHeaderPage() {
return null
}

View File

@@ -0,0 +1,5 @@
"use client"
export default function Error() {
return null
}

View File

@@ -0,0 +1,15 @@
import { baseUrls } from "@/constants/routes/baseUrls"
import Header from "@/components/Current/Header"
import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
import { LangParams, PageArgs } from "@/types/params"
export default async function HeaderPage({ params }: PageArgs<LangParams>) {
return (
<Header
lang={params.lang}
languageSwitcher={<LanguageSwitcher urls={baseUrls} lang={params.lang} />}
/>
)
}

View File

@@ -1,14 +0,0 @@
import { serverClient } from "@/lib/trpc/server"
import Desktop from "@/components/Current/Header/LanguageSwitcher/Desktop"
import Mobile from "@/components/Current/Header/LanguageSwitcher/Mobile"
export default async function LanguageSwitcher() {
const data = await serverClient().contentstack.languageSwitcher.get()
return (
<>
<Desktop currentLanguage={data.lang} urls={data.urls} />
<Mobile currentLanguage={data.lang} urls={data.urls} />
</>
)
}

View File

@@ -1,3 +0,0 @@
export default async function DefaultLanguageSwitcher() {
return null
}

View File

@@ -7,7 +7,6 @@ import TrpcProvider from "@/lib/trpc/Provider"
import AdobeScript from "@/components/Current/AdobeScript"
import Footer from "@/components/Current/Footer"
import Header from "@/components/Current/Header"
import VwoScript from "@/components/Current/VwoScript"
import { getIntl } from "@/i18n"
import ServerIntlProvider from "@/i18n/Provider"
@@ -24,10 +23,10 @@ export const metadata: Metadata = {
export default async function RootLayout({
children,
params,
languageSwitcher,
header,
}: React.PropsWithChildren<
LayoutArgs<LangParams> & {
languageSwitcher: React.ReactNode
header: React.ReactNode
}
>) {
const { defaultLocale, locale, messages } = await getIntl()
@@ -55,12 +54,9 @@ export default async function RootLayout({
<body>
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<TrpcProvider lang={params.lang}>
<Header
lang={params.lang}
languageSwitcher={languageSwitcher}
/>
{header}
{children}
<Footer />
<Footer lang={params.lang} />
</TrpcProvider>
</ServerIntlProvider>
<Script id="page-tracking">{`

View File

@@ -0,0 +1,11 @@
import { Lang } from "@/constants/languages"
import NotFound from "@/components/Current/NotFound"
import { LangParams, PageArgs } from "@/types/params"
export default function NotFoundPage({ params }: PageArgs<LangParams>) {
const lang = params.lang || Lang.en
return <NotFound lang={lang} />
}

View File

@@ -1,14 +1,9 @@
import { serverClient } from "@/lib/trpc/server"
import Desktop from "@/components/Current/Header/LanguageSwitcher/Desktop"
import Mobile from "@/components/Current/Header/LanguageSwitcher/Mobile"
import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
export default async function LanguageSwitcher() {
export default async function LanguageSwitcherRoute() {
const data = await serverClient().contentstack.languageSwitcher.get()
return (
<>
<Desktop currentLanguage={data.lang} urls={data.urls} />
<Mobile currentLanguage={data.lang} urls={data.urls} />
</>
)
return <LanguageSwitcher urls={data.urls} lang={data.lang} />
}

View File

@@ -1,3 +0,0 @@
export default function DefaultLanguageSwitcher() {
return null
}

View File

@@ -0,0 +1,14 @@
"use client"
import { useParams } from "next/navigation"
import { baseUrls } from "@/constants/routes/baseUrls"
import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
import { LangParams } from "@/types/params"
export default function Error() {
const params = useParams<LangParams>()
return <LanguageSwitcher urls={baseUrls} lang={params.lang} />
}

View File

@@ -5,7 +5,6 @@ import { GetCurrentBlockPageTrackingData } from "@/lib/graphql/Query/CurrentBloc
import { request } from "@/lib/graphql/request"
import ContentPage from "@/components/Current/ContentPage"
import Header from "@/components/Current/Header"
import Tracking from "@/components/Current/Tracking"
import type { LangParams, PageArgs, UriParams } from "@/types/params"
@@ -27,9 +26,7 @@ export default async function CurrentContentPage({
locale: params.lang,
url: searchParams.uri,
},
{
next: { tags: [`${searchParams.uri}-${params.lang}`] },
}
{ tags: [`${searchParams.uri}-${params.lang}`] }
)
if (!response.data?.all_current_blocks_page?.total) {
@@ -43,9 +40,7 @@ export default async function CurrentContentPage({
const pageDataForTracking = await request<TrackingData>(
GetCurrentBlockPageTrackingData,
{ uid: response.data.all_current_blocks_page.items[0].system.uid },
{
next: { tags: [`${searchParams.uri}-en`] },
}
{ tags: [`${searchParams.uri}-en`] }
)
const pageData = response.data.all_current_blocks_page.items[0]

View File

@@ -17,8 +17,7 @@ import type { LangParams, LayoutArgs } from "@/types/params"
export const fetchCache = "default-no-store"
export const metadata: Metadata = {
description: "New web",
title: "Scandic Hotels New Web",
title: "Scandic Hotels",
}
export default async function RootLayout({
@@ -85,8 +84,8 @@ export default async function RootLayout({
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<Header lang={params.lang} languageSwitcher={languageSwitcher} />
{children}
<Footer lang={params.lang} />
</ServerIntlProvider>
<Footer />
<Script id="page-tracking">{`
typeof _satellite !== "undefined" && _satellite.pageBottom();
`}</Script>

View File

@@ -1,11 +1,13 @@
import { getIntl } from "@/i18n"
"use client"
export default async function NotFound() {
const { formatMessage } = await getIntl()
return (
<main>
<h1>{formatMessage({ id: "Not found" })}</h1>
<p>{formatMessage({ id: "Could not find requested resource" })}</p>
</main>
)
import { useParams } from "next/navigation"
import NotFound from "@/components/Current/NotFound"
import { LangParams } from "@/types/params"
export default function NotFoundPage() {
const { lang } = useParams<LangParams>()
return <NotFound lang={lang} />
}

View File

@@ -53,7 +53,7 @@ export default function RootLayout({
<LangPopup lang={params.lang} />
<SkipToMainContent />
{children}
<Footer />
<Footer lang={params.lang} />
</body>
</html>
)

View File

@@ -1,18 +0,0 @@
import type { Metadata } from "next"
import type { LangParams, LayoutArgs } from "@/types/params"
export const metadata: Metadata = {
title: "Test Site",
}
export default function RootLayout({
children,
params,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
return (
<html lang={params.lang}>
<body>{children}</body>
</html>
)
}

View File

@@ -1,15 +0,0 @@
import type { Metadata } from "next"
export const metadata: Metadata = {
title: "Hello World",
}
export default function HelloWorldTestPage() {
return (
<main>
<header>
<h1>Hello World!</h1>
</header>
</main>
)
}

View File

@@ -12,13 +12,13 @@ export default function Contact({ sections, system: { locale } }: ContactNode) {
const visitingAddressMessage = getVisitingAddressMessage(locale)
return (
<section>
{sections.map((section) => {
{sections.map((section, idx) => {
switch (section.__typename) {
case Section.ContactBlockSectionsExtraInfo:
return <p>{section.extra_info.text}</p>
case Section.ContactBlockSectionsMailingAddress:
return (
<p>
<p key={`section-mail-${idx}`}>
{section.mailing_address.name}
<br />
{section.mailing_address.street}
@@ -30,7 +30,10 @@ export default function Contact({ sections, system: { locale } }: ContactNode) {
)
case Section.ContactBlockSectionsPhone:
return (
<div className={styles.highlightBlock}>
<div
className={styles.highlightBlock}
key={`section-phone-${idx}`}
>
<h3>{section.phone.title}</h3>
<div className={styles.phoneContainer}>
<svg
@@ -55,10 +58,14 @@ export default function Contact({ sections, system: { locale } }: ContactNode) {
</div>
)
case Section.ContactBlockSectionsTitle:
return <h2 className={styles.heading}>{section.title.text}</h2>
return (
<h2 className={styles.heading} key={`section-heading-${idx}`}>
{section.title.text}
</h2>
)
case Section.ContactBlockSectionsVisitingAddress:
return (
<p>
<p key={`section-visiting-address-${idx}`}>
{visitingAddressMessage}: {section.visiting_address.street}{" "}
</p>
)

View File

@@ -14,15 +14,15 @@ export default function Blocks({ blocks }: BlocksProps) {
return (
<section className={styles.wrapper}>
{blocks.map((block) => {
{blocks.map((block, idx) => {
const type = block.__typename
switch (type) {
case BlocksTypenameEnum.CurrentBlocksPageBlocksList:
return <List key={block.__typename} {...block} />
return <List key={`${block.__typename}-${idx}`} {...block} />
case BlocksTypenameEnum.CurrentBlocksPageBlocksPuffs:
return <Puffs key={block.__typename} {...block} />
return <Puffs key={`${block.__typename}-${idx}`} {...block} />
case BlocksTypenameEnum.CurrentBlocksPageBlocksText:
return <Text key={block.__typename} {...block} />
return <Text key={`${block.__typename}-${idx}`} {...block} />
default:
console.log(`Unknown type: (${type})`)
return null

View File

@@ -6,8 +6,10 @@ import Navigation from "./Navigation"
import styles from "./footer.module.css"
export default async function Footer() {
const footerData = await serverClient().contentstack.base.footer()
import { LangParams } from "@/types/params"
export default async function Footer({ lang }: LangParams) {
const footerData = await serverClient().contentstack.base.footer({ lang })
return (
<footer className={styles.container}>
<div className={styles.content}>

View File

@@ -12,7 +12,7 @@
sans-serif;
}
.languageSwitcher {
.toggle {
display: flex;
color: #fff;
padding: 3px 15px;

View File

@@ -0,0 +1,19 @@
import Desktop from "./Desktop"
import Mobile from "./Mobile"
import { LangParams } from "@/types/params"
import { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
type LanguageSwitcherProps = LangParams & { urls: LanguageSwitcherData }
export default function LanguageSwitcher({
urls,
lang,
}: LanguageSwitcherProps) {
return (
<>
<Desktop currentLanguage={lang} urls={urls} />
<Mobile currentLanguage={lang} urls={urls} />
</>
)
}

View File

@@ -275,6 +275,7 @@
.logo {
width: 102.17px;
height: 100%;
padding-bottom: 4px;
}
.listWrapper {

View File

@@ -23,7 +23,9 @@ export default async function TopMenu({
</a>
<ul className={styles.list}>
<li className={styles.langSwitcher}>{languageSwitcher}</li>
{languageSwitcher ? (
<li className={styles.langSwitcher}>{languageSwitcher}</li>
) : null}
{links.map(({ link }, i) => (
<li key={link.href + i}>

View File

@@ -16,7 +16,9 @@ export default async function Header({
lang,
languageSwitcher,
}: LangParams & { languageSwitcher: React.ReactNode }) {
const data = await serverClient().contentstack.base.header()
const data = await serverClient().contentstack.base.header({
lang,
})
const session = await auth()
const homeHref = homeHrefs[env.NODE_ENV][lang]

View File

@@ -0,0 +1,131 @@
import { Lang } from "@/constants/languages"
type Texts = {
title: string
goToStartPage: {
question: string
link: string
linkText: string
}
goToDestinations: {
question: string
link: string
linkText: string
}
goToOffers: {
question: string
link: string
linkText: string
}
}
export const texts: Record<Lang, Texts> = {
en: {
title: "Sorry, page not found.",
goToStartPage: {
question: "Would you like to go back to the ",
link: "https://www.scandichotels.com/",
linkText: "startpage",
},
goToDestinations: {
question: "Or take a trip to our ",
link: "https://www.scandichotels.com/hotels",
linkText: "destinations",
},
goToOffers: {
question: " or latest ",
link: "https://www.scandichotels.com/weekend-packages-and-offers",
linkText: "offers",
},
},
sv: {
title: "Oj då, vi kunde inte hitta sidan du söker.",
goToStartPage: {
question: "Vill du gå tillbaka till ",
link: "https://www.scandichotels.se/",
linkText: "startsidan",
},
goToDestinations: {
question: "Eller resa till våra ",
link: "https://www.scandichotels.se/hotell",
linkText: "destinationer",
},
goToOffers: {
question: " eller se våra senaste ",
link: "https://www.scandichotels.se/erbjudanden-och-weekendpaket",
linkText: "erbjudanden",
},
},
de: {
title: "Tut uns leid, Seite nicht gefunden.",
goToStartPage: {
question: "Möchten Sie zurück zur ",
link: "https://www.scandichotels.de/",
linkText: "Startseite",
},
goToDestinations: {
question: "Oder machen Sie einen Ausflug zu unseren ",
link: "https://www.scandichotels.de/hotelsuche",
linkText: "Reisezielen",
},
goToOffers: {
question: " und aktuellen ",
link: "https://www.scandichotels.de/angebote-arrangements",
linkText: "Angeboten",
},
},
fi: {
title: "TValitettavasti sivua ei löydy.",
goToStartPage: {
question: "Haluaisitko mennä takaisin ",
link: "https://www.scandichotels.fi/",
linkText: "etusivulle",
},
goToDestinations: {
question: "Voit myös tutustu ",
link: "https://www.scandichotels.fi/hotellit",
linkText: "kohteisiimme",
},
goToOffers: {
question: " tai ajankohtaisiin ",
link: "https://www.scandichotels.fi/tarjoukset",
linkText: "tarjouksiimme",
},
},
no: {
title: "Oi da, vi fant ikke siden du lette etter...",
goToStartPage: {
question: "Vil du gå tilbake til ",
link: "https://www.scandichotels.no/",
linkText: "startsiden",
},
goToDestinations: {
question: "Eller ta en tur til våre ",
link: "https://www.scandichotels.no/hotell",
linkText: "destinasjoner",
},
goToOffers: {
question: " eller siste ",
link: "https://www.scandichotels.no/hotelltilbud",
linkText: "tilbud",
},
},
da: {
title: "Hov - siden kan ikke findes!",
goToStartPage: {
question: "Vil du gå tilbage til ",
link: "https://www.scandichotels.dk/",
linkText: "startsiden",
},
goToDestinations: {
question: "Eller tag en tur til vores ",
link: "https://www.scandichotels.dk/hoteller",
linkText: "destinationer",
},
goToOffers: {
question: " eller seneste ",
link: "https://www.scandichotels.dk/tilbud-og-hotelpakker",
linkText: "tilbud",
},
},
}

View File

@@ -0,0 +1,48 @@
import { texts } from "./Texts"
import styles from "./notFound.module.css"
import { LangParams } from "@/types/params"
export default function NotFound({ lang }: LangParams) {
const infoTexts = texts[lang]
return (
<div className={styles.container}>
<div className={styles.content}>
<h1 className={styles.header}>{infoTexts.title}</h1>
<div className={styles.pitch}>
<p className={styles.text}>
{infoTexts.goToStartPage.question}
<a
className={styles.link}
title={infoTexts.goToStartPage.linkText}
href={infoTexts.goToStartPage.link}
>
{infoTexts.goToStartPage.linkText}
</a>
?
</p>
<p className={styles.text}>
{infoTexts.goToDestinations.question}
<a
className={styles.link}
title={infoTexts.goToDestinations.linkText}
href={infoTexts.goToDestinations.link}
>
{infoTexts.goToDestinations.linkText}
</a>
{infoTexts.goToOffers.question}
<a
className={styles.link}
title={infoTexts.goToOffers.linkText}
href={infoTexts.goToOffers.link}
>
{infoTexts.goToOffers.linkText}
</a>
.
</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,70 @@
.container {
margin-top: 0;
background: var(--Main-Grey-White);
position: relative;
border-top: 50px solid var(--Main-Grey-White);
background-clip: content-box;
}
.content {
position: relative;
text-align: center;
box-sizing: content-box;
max-width: 1200px;
margin: 0 auto;
padding: 70px 30px;
}
.header {
font-family:
brandon text,
Arial,
Helvetica,
sans-serif;
font-size: 32px;
line-height: 1;
margin: 0;
text-transform: uppercase;
font-weight: 400;
color: #483729;
}
.pitch {
margin-top: 32px;
margin-bottom: 16px;
}
.text {
font-family:
Helvetica Neue,
Roboto,
Helvetica,
Arial,
sans-serif;
font-weight: 300;
line-height: normal;
text-transform: none;
font-size: 20px;
color: #333;
}
.link {
color: #00838e;
text-decoration: none;
}
@media screen and (min-width: 740px) {
.content {
text-align: left;
}
}
@media screen and (min-width: 950px) {
.header {
font-size: 46px;
}
.text {
font-size: 24px;
}
}

View File

@@ -37,7 +37,10 @@ export default function BenefitCard({
</div>
<div className={styles.benefitComparison}>
{comparedValues.map((benefit, idx) => (
<div key={idx} className={styles.comparisonItem}>
<div
key={`${benefit.valueDetails}-${idx}`}
className={styles.comparisonItem}
>
<BenefitValue benefit={benefit} />
</div>
))}

View File

@@ -29,8 +29,9 @@ export default function BenefitList({ levels }: BenefitListProps) {
<BenefitCard
title={benefit.name}
description={benefit.description}
comparedValues={levelBenefits.map((benefit) => {
comparedValues={levelBenefits.map((benefit, idx) => {
return {
key: `${benefit.name}-${idx}`,
value: benefit.value,
unlocked: benefit.unlocked,
valueDetails: benefit.valueDetails,

View File

@@ -14,10 +14,15 @@ export function Blocks({ lang, blocks }: BlocksProps & LangParams) {
const firstItem = idx === 0
switch (block.__typename) {
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
return <CardsGrid cards_grid={block.cards_grid} />
return (
<CardsGrid
key={`${block.cards_grid.title}-${idx}`}
cards_grid={block.cards_grid}
/>
)
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:
return (
<section>
<section key={`${block.__typename}-${idx}`}>
<JsonToHtml
nodes={block.content.content.json.children}
embeds={block.content.content.embedded_itemsConnection.edges}
@@ -39,6 +44,7 @@ export function Blocks({ lang, blocks }: BlocksProps & LangParams) {
<DynamicContentBlock
dynamicContent={dynamicContent}
firstItem={firstItem}
key={`${block.dynamic_content.title}-${idx}`}
/>
)
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts:
@@ -49,6 +55,7 @@ export function Blocks({ lang, blocks }: BlocksProps & LangParams) {
return (
<Shortcuts
firstItem={firstItem}
key={`${block.shortcuts.title}-${idx}`}
shortcuts={shortcuts}
title={block.shortcuts.title}
subtitle={block.shortcuts.preamble}

View File

@@ -13,7 +13,7 @@ export function Blocks({ blocks }: BlocksProps) {
switch (block.__typename) {
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksContent:
return (
<section>
<section key={`${block.__typename}-${idx}`}>
<JsonToHtml
nodes={block.content.content.json.children}
embeds={block.content.content.embedded_itemsConnection.edges}
@@ -25,19 +25,26 @@ export function Blocks({ blocks }: BlocksProps) {
<DynamicContentBlock
dynamicContent={block.dynamic_content}
firstItem={firstItem}
key={`${block.dynamic_content.title}-${idx}`}
/>
)
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksShortcuts:
return (
<Shortcuts
firstItem={firstItem}
key={`${block.shortcuts.title}-${idx}`}
shortcuts={block.shortcuts.shortcuts}
subtitle={block.shortcuts.preamble}
title={block.shortcuts.title}
/>
)
case LoyaltyBlocksTypenameEnum.LoyaltyPageBlocksCardsGrid:
return <CardsGrid cards_grid={block.cards_grid} />
return (
<CardsGrid
cards_grid={block.cards_grid}
key={`${block.cards_grid.title}-${idx}`}
/>
)
default:
return null
}

View File

@@ -1,3 +1,5 @@
import { login } from "@/constants/routes/handleAuth"
import { ScandicFriends } from "@/components/Levels"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
@@ -11,11 +13,12 @@ import Contact from "./Contact"
import styles from "./joinLoyalty.module.css"
import type { JoinLoyaltyContactProps } from "@/types/components/loyalty/sidebar"
import { LangParams } from "@/types/params"
export default async function JoinLoyaltyContact({
block,
lang,
}: JoinLoyaltyContactProps) {
}: JoinLoyaltyContactProps & LangParams) {
const { formatMessage } = await getIntl()
return (
<section className={styles.container}>
@@ -28,11 +31,9 @@ export default async function JoinLoyaltyContact({
<Body textAlign="center">{block.preamble}</Body>
) : null}
<Button asChild className={styles.link} intent="primary">
<Link href="#">
{formatMessage({ id: "Join Scandic Friends" })}
</Link>
<Link href="#">{formatMessage({ id: "Join Scandic Friends" })}</Link>
</Button>
<Link href={`/${lang}/login`}>
<Link href={login[lang]}>
<Footnote textAlign="center" textTransform="bold">
{formatMessage({ id: "Already a friend?" })} <br />
{formatMessage({ id: "Click here to log in" })}

View File

@@ -6,15 +6,22 @@ import styles from "./sidebar.module.css"
import { SidebarTypenameEnum } from "@/types/components/loyalty/enums"
import { SidebarProps } from "@/types/components/loyalty/sidebar"
import { LangParams } from "@/types/params"
export default function SidebarLoyalty({ blocks, lang }: SidebarProps) {
export default function SidebarLoyalty({
blocks,
lang,
}: SidebarProps & LangParams) {
return (
<aside className={styles.aside}>
{blocks.map((block) => {
{blocks.map((block, idx) => {
switch (block.__typename) {
case SidebarTypenameEnum.LoyaltyPageSidebarContent:
return (
<section className={styles.content}>
<section
className={styles.content}
key={`${block.__typename}-${idx}`}
>
<JsonToHtml
embeds={block.content.content.embedded_itemsConnection.edges}
nodes={block.content.content.json.children}
@@ -26,6 +33,7 @@ export default function SidebarLoyalty({ blocks, lang }: SidebarProps) {
<JoinLoyaltyContact
block={block.join_loyalty_contact}
lang={lang}
key={`${block.join_loyalty_contact.title}-${idx}`}
/>
)
default:

View File

@@ -47,7 +47,7 @@ function DynamicComponent({ component, props }: AccountPageContentProps) {
}
export default function Content({ lang, content }: ContentProps) {
return content.map((item) => {
return content.map((item, idx) => {
switch (item.__typename) {
case ContentEntries.AccountPageContentDynamicContent:
const link = item.dynamic_content.link.linkConnection.edges.length
@@ -71,6 +71,7 @@ export default function Content({ lang, content }: ContentProps) {
}
return (
<DynamicComponent
key={`${item.dynamic_content.title}-${idx}`}
component={item.dynamic_content.component}
props={componentProps}
/>
@@ -78,6 +79,7 @@ export default function Content({ lang, content }: ContentProps) {
case ContentEntries.AccountPageContentShortcuts:
return (
<Shortcuts
key={`${item.shortcuts.title}-${idx}`}
shortcuts={item.shortcuts.shortcuts}
subtitle={item.shortcuts.preamble}
title={item.shortcuts.title}
@@ -85,7 +87,7 @@ export default function Content({ lang, content }: ContentProps) {
)
case ContentEntries.AccountPageContentTextContent:
return (
<section>
<section key={`${item.__typename}-${idx}`}>
<JsonToHtml
embeds={item.text_content.content.embedded_itemsConnection.edges}
nodes={item.text_content.content.json.children}

View File

@@ -48,7 +48,7 @@ function DynamicComponent({ component, props }: AccountPageContentProps) {
export default function Content({ lang, content }: ContentProps) {
return (
<>
{content.map((item) => {
{content.map((item, idx) => {
switch (item.__typename) {
case ContentEntries.AccountPageContentDynamicContent:
const link = item.dynamic_content.link.linkConnection.edges.length
@@ -70,6 +70,7 @@ export default function Content({ lang, content }: ContentProps) {
}
return (
<DynamicComponent
key={`${item.dynamic_content.title}-${idx}`}
component={item.dynamic_content.component}
props={componentProps}
/>
@@ -83,6 +84,7 @@ export default function Content({ lang, content }: ContentProps) {
})
return (
<Shortcuts
key={`${item.shortcuts.title}-${idx}`}
shortcuts={shortcuts}
subtitle={item.shortcuts.preamble}
title={item.shortcuts.title}
@@ -90,7 +92,7 @@ export default function Content({ lang, content }: ContentProps) {
)
case ContentEntries.AccountPageContentTextContent:
return (
<section>
<section key={`${item.__typename}-${idx}`}>
<JsonToHtml
embeds={
item.text_content.content.embedded_itemsConnection.edges

View File

@@ -49,13 +49,10 @@
line-height: var(--typography-Script-2-lineHeight);
letter-spacing: 0.48px;
padding: var(--Spacing-x1);
margin: 0;
transform: rotate(-3deg);
}
.divider {
border-bottom-color: var(--divider-color);
}
.heading {
color: var(--font-color);
}

View File

@@ -42,10 +42,7 @@ export default function Card({
>
{scriptedTopTitle ? (
<section className={styles.scriptContainer}>
<Title className={styles.scriptedTitle} level="h3">
{scriptedTopTitle}
</Title>
<Divider className={styles.divider} />
<h3 className={styles.scriptedTitle}>{scriptedTopTitle}</h3>
</section>
) : null}
<Title as="h5" className={styles.heading} level="h3">

View File

@@ -0,0 +1,10 @@
import { LanguageSwitcherData } from "@/types/requests/languageSwitcher"
export const baseUrls: LanguageSwitcherData = {
da: { url: "/da/" },
de: { url: "/de/" },
en: { url: "/en/" },
fi: { url: "/fi/" },
no: { url: "/no/" },
sv: { url: "/sv/" },
}

View File

@@ -1,6 +0,0 @@
describe("Hello World", () => {
it("should have an h1 tag", () => {
cy.visit("/en/test")
cy.get("h1").contains("Hello World")
})
})

View File

@@ -0,0 +1,32 @@
describe("About page", () => {
it("should load in Danish", () => {
cy.visit("/da/kundeservice/sporgsmal-og-svar/om-scandics-website")
cy.get("h1").contains("SCANDICS WEBSITE SPØRGSMÅL OG SVAR")
})
it("should load in German", () => {
cy.visit(
"/de/kundenbetreuung/haufig-gestellte-fragen/nutzung-der-internetseite"
)
cy.get("h1").contains("SCANDICS WEBSEITE FRAGEN UND ANTWORTEN")
})
it("should load in English", () => {
cy.visit(
"/en/customer-service/frequently-asked-questions/using-the-website"
)
cy.get("h1").contains("SCANDIC WEBSITE QUESTIONS AND ANSWERS")
})
it("should load in Finnish", () => {
cy.visit(
"/fi/asiakaspalvelu/usein-kysytyt-kysymykset/tietoja-internetsivuista"
)
cy.get("h1").contains("SCANDIC-VERKKOSIVUT KYSYMYKSIÄ JA VASTAUKSIA")
})
it("should load in Norwegian", () => {
cy.visit("/no/kundeservice/sporsmal-og-svar/bruk-av-nettsiden")
cy.get("h1").contains("SCANDIC-NETTSTEDET SPØRSMÅL OG SVAR")
})
it("should load in Swedish", () => {
cy.visit("/sv/kundservice/fragor-och-svar/om-scandics-webbplats")
cy.get("h1").contains("SCANDICS WEBBPLATS - FRÅGOR OCH SVAR")
})
})

View File

@@ -0,0 +1,26 @@
describe("Sponsoring page", () => {
it("should load in Danish", () => {
cy.visit("/da/sponsorering")
cy.get("h1").contains("SCANDICS SYN PÅ SPONSORATER")
})
it("should load in German", () => {
cy.visit("/de/sponsoring")
cy.get("h1").contains("SPONSORING BEI SCANDIC")
})
it("should load in English", () => {
cy.visit("/en/sponsoring")
cy.get("h1").contains("SCANDIC'S TAKE ON SPONSORSHIP")
})
it("should load in Finnish", () => {
cy.visit("/fi/sponsorointi")
cy.get("h1").contains("SCANDIC JA SPONSOROINTI")
})
it("should load in Norwegian", () => {
cy.visit("/no/vi-sponser")
cy.get("h1").contains("SCANDICS SYN PÅ SPONSING")
})
it("should load in Swedish", () => {
cy.visit("/sv/vi-sponsrar")
cy.get("h1").contains("SÅ SER SCANDIC PÅ SPONSRING")
})
})

View File

@@ -0,0 +1,26 @@
describe("Wifi page", () => {
it("should load in Danish", () => {
cy.visit("/da/oplev-scandic/wifi")
cy.get("h1").contains("FRI WIFI ER EN SELVFØLGE")
})
it("should load in German", () => {
cy.visit("/de/scandic-entdecken/wlan")
cy.get("h1").contains("GRATIS WLAN IST EINE SELBSTVERSTÄNDLICHKEIT")
})
it("should load in English", () => {
cy.visit("/en/explore-scandic/wifi")
cy.get("h1").contains("FREE WI-FI GOES WITHOUT SAYING")
})
it("should load in Finnish", () => {
cy.visit("/fi/koe-scandic/maksuton-internetyhteys")
cy.get("h1").contains("MAKSUTON WI-FI")
})
it("should load in Norwegian", () => {
cy.visit("/no/utforsk-scandic/wifi")
cy.get("h1").contains("SELVFØLGELIG HAR VI GRATIS WIFI")
})
it("should load in Swedish", () => {
cy.visit("/sv/utforska-scandic/wi-fi")
cy.get("h1").contains("FRITT WIFI, SÅ KLART")
})
})

View File

@@ -18,3 +18,13 @@ import "./commands"
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.on("uncaught:exception", (err) => {
// https://github.com/cypress-io/cypress/issues/27204
// Cypress and React Hydrating the document don't get along
// for some unknown reason. Hopefully, we figure out why eventually
// so we can remove this.
// Maybe React 19 (that has changes to hydration logic) might solve this.
if (/hydration|hydrating/i.test(err.message)) {
return false
}
})

View File

@@ -1,23 +1,35 @@
query GetDaDeEnUrlsCurrentBlocksPage($uid: String!) {
de: current_blocks_page(uid: $uid, locale: "de") {
url: original_url
de: all_current_blocks_page(where: { uid: $uid }, locale: "de") {
items {
url
}
}
en: current_blocks_page(uid: $uid, locale: "en") {
url: original_url
en: all_current_blocks_page(where: { uid: $uid }, locale: "en") {
items {
url
}
}
da: current_blocks_page(uid: $uid, locale: "da") {
url: original_url
da: all_current_blocks_page(where: { uid: $uid }, locale: "da") {
items {
url
}
}
}
query GetFiNoSvUrlsCurrentBlocksPage($uid: String!) {
fi: current_blocks_page(uid: $uid, locale: "fi") {
url: original_url
fi: all_current_blocks_page(where: { uid: $uid }, locale: "fi") {
items {
url
}
}
no: current_blocks_page(uid: $uid, locale: "no") {
url: original_url
no: all_current_blocks_page(where: { uid: $uid }, locale: "no") {
items {
url
}
}
sv: current_blocks_page(uid: $uid, locale: "sv") {
url: original_url
sv: all_current_blocks_page(where: { uid: $uid }, locale: "sv") {
items {
url
}
}
}

74
lib/graphql/_request.ts Normal file
View File

@@ -0,0 +1,74 @@
import "server-only"
import { GraphQLClient } from "graphql-request"
import { env } from "@/env/server"
import type { DocumentNode } from "graphql"
import type { Data } from "@/types/request"
export async function request<T>(
client: GraphQLClient,
query: string | DocumentNode,
variables?: {},
next?: NextFetchRequestConfig
): Promise<Data<T>> {
try {
if (next) {
client.requestConfig.next = next
}
if (env.PRINT_QUERY) {
const print = (await import("graphql/language/printer")).print
const rawResponse = await client.rawRequest<T>(
print(query as DocumentNode),
variables,
{
access_token: env.CMS_ACCESS_TOKEN,
"Content-Type": "application/json",
}
)
/**
* TODO: Send to Monitoring (Logging and Metrics)
*/
console.log({
complexityLimit: rawResponse.headers.get("x-query-complexity"),
})
console.log({
referenceDepth: rawResponse.headers.get("x-reference-depth"),
})
console.log({ resolverCost: rawResponse.headers.get("x-resolver-cost") })
return {
data: rawResponse.data,
}
}
const print = (await import("graphql/language/printer")).print
const nr = Math.random()
console.log(`START REQUEST ${nr}`)
console.time(`OUTGOING REQUEST ${nr}`)
console.log(`Sending reqeust to ${env.CMS_URL}`)
console.log(`Query:`, print(query as DocumentNode))
console.log(`Variables:`, variables)
const response = await client.request<T>({
document: query,
requestHeaders: {
access_token: env.CMS_ACCESS_TOKEN,
"Content-Type": "application/json",
},
variables,
})
console.timeEnd(`OUTGOING REQUEST ${nr}`)
console.log({ response })
return { data: response }
} catch (error) {
console.error(error)
throw new Error("Something went wrong")
}
}

View File

@@ -12,9 +12,7 @@ export async function batchRequest<T>(
try {
const response = await Promise.allSettled(
queries.map((query) =>
request<T>(query.document, query.variables, {
next: { tags: query.tags },
})
request<T>(query.document, query.variables, { tags: query.tags })
)
)

View File

@@ -0,0 +1,20 @@
import { DocumentNode } from "graphql"
import { GraphQLClient } from "graphql-request"
import { env } from "@/env/server"
import { request as _request } from "./_request"
import { Data } from "@/types/request"
const client = new GraphQLClient(env.CMS_URL, {
fetch: fetch,
})
export async function edgeRequest<T>(
query: string | DocumentNode,
variables?: {},
next?: NextFetchRequestConfig
): Promise<Data<T>> {
return _request(client, query, variables, next)
}

View File

@@ -1,16 +1,14 @@
import "server-only"
import { DocumentNode } from "graphql"
import { GraphQLClient } from "graphql-request"
import { cache } from "react"
import { env } from "@/env/server"
import type { DocumentNode } from "graphql"
import { request as _request } from "./_request"
import type { Data } from "@/types/request"
import { Data } from "@/types/request"
const client = new GraphQLClient(env.CMS_URL, {
cache: "force-cache",
fetch: cache(async function (
url: URL | RequestInfo,
params: RequestInit | undefined
@@ -22,50 +20,7 @@ const client = new GraphQLClient(env.CMS_URL, {
export async function request<T>(
query: string | DocumentNode,
variables?: {},
options?: Pick<RequestInit, "cache" | "next">
next?: NextFetchRequestConfig
): Promise<Data<T>> {
if (options?.cache) {
client.requestConfig.cache = options.cache
}
if (options?.next) {
client.requestConfig.next = options.next
}
if (env.PRINT_QUERY) {
const { print } = await import("graphql")
const rawResponse = await client.rawRequest<T>(
print(query as DocumentNode),
variables,
{
access_token: env.CMS_ACCESS_TOKEN,
"Content-Type": "application/json",
}
)
/**
* TODO: Send to Monitoring (Logging and Metrics)
*/
console.log({
complexityLimit: rawResponse.headers.get("x-query-complexity"),
})
console.log({
referenceDepth: rawResponse.headers.get("x-reference-depth"),
})
console.log({ resolverCost: rawResponse.headers.get("x-resolver-cost") })
return {
data: rawResponse.data,
}
}
const response = await client.request<T>({
document: query,
requestHeaders: {
access_token: env.CMS_ACCESS_TOKEN,
"Content-Type": "application/json",
},
variables,
})
return { data: response }
return _request(client, query, variables, next)
}

View File

@@ -1,6 +1,6 @@
import { NextMiddleware, NextResponse } from "next/server"
import { findLang } from "./constants/languages"
import { findLang, Lang } from "./constants/languages"
import * as authRequired from "./middlewares/authRequired"
import * as cmsContent from "./middlewares/cmsContent"
import * as currentWebLogin from "./middlewares/currentWebLogin"
@@ -16,8 +16,10 @@ export const middleware: NextMiddleware = async (request, event) => {
// Without it we shortcircuit early.
// We use middleware-error route because notFound() requires a root layout
// which we do not want. We can move to that once all Current stuff is gone.
// Default to English if no lang is found.
return NextResponse.rewrite(
new URL(`/${lang}/middleware-error/404`, request.nextUrl),
new URL(`/${Lang.en}/middleware-error/404`, request.nextUrl),
{
status: 404,
statusText: "Not found",

View File

@@ -13,9 +13,9 @@ package = "netlify-plugin-cypress"
[plugins.inputs]
configFile = "cypress.config.ts"
[plugins.inputs.postBuild]
enable = true
start = "npm start"
wait-on = "http://127.0.0.1:3000/en/test"
enable = true
start = "npm start"
wait-on = "http://127.0.0.1:3000/en/sponsoring"
wait-on-timeout = "30" # seconds
[build.environment]

View File

@@ -16,8 +16,8 @@
"start": "node .next/standalone/server.js",
"test:component": "cypress open --component",
"test:component:headless": "cypress run --component",
"test:e2e": "start-server-and-test test:setup http://127.0.0.1:3000/en/test \"cypress open --e2e\"",
"test:e2e:headless": "start-server-and-test test:setup http://127.0.0.1:3000/en/test \"cypress run --e2e\"",
"test:e2e": "start-server-and-test test:setup http://127.0.0.1:3000/en/sponsoring \"cypress open --e2e\"",
"test:e2e:headless": "start-server-and-test test:setup http://127.0.0.1:3000/en/sponsoring \"cypress run --e2e\"",
"test:setup": "npm run build && npm run start",
"preinstall": "export $(cat .env.local | grep -v '^#' | xargs)",
"update-dotenv": "node update-dotenv.mjs"

File diff suppressed because one or more lines are too long

View File

@@ -37,9 +37,7 @@ export const accountPageQueryRouter = router({
uid,
},
{
next: {
tags: [generateRefsResponseTag(lang, uid)],
},
tags: [generateRefsResponseTag(lang, uid)],
}
)
@@ -71,7 +69,7 @@ export const accountPageQueryRouter = router({
locale: lang,
uid,
},
{ next: { tags } }
{ tags }
)
if (!response.data) {

View File

@@ -0,0 +1,5 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
export const langInput = z.object({ lang: z.nativeEnum(Lang) })

View File

@@ -9,10 +9,15 @@ import {
} from "@/lib/graphql/Query/CurrentHeader.graphql"
import { request } from "@/lib/graphql/request"
import { internalServerError, notFound } from "@/server/errors/trpc"
import { contentstackBaseProcedure, router } from "@/server/trpc"
import {
contentstackBaseProcedure,
publicProcedure,
router,
} from "@/server/trpc"
import { generateTag } from "@/utils/generateTag"
import { langInput } from "./input"
import {
type ContactConfigData,
FooterDataRaw,
@@ -47,23 +52,21 @@ export const baseQueryRouter = router({
return validatedContactConfigConfig.data.all_contact_config.items[0]
}),
header: contentstackBaseProcedure.query(async ({ ctx }) => {
header: publicProcedure.input(langInput).query(async ({ input }) => {
const responseRef = await request<HeaderRefDataRaw>(GetCurrentHeaderRef, {
locale: ctx.lang,
locale: input.lang,
})
const response = await request<HeaderDataRaw>(
GetCurrentHeader,
{ locale: ctx.lang },
{ locale: input.lang },
{
next: {
tags: [
generateTag(
ctx.lang,
responseRef.data.all_current_header.items[0].system.uid
),
],
},
tags: [
generateTag(
input.lang,
responseRef.data.all_current_header.items[0].system.uid
),
],
}
)
@@ -88,25 +91,23 @@ export const baseQueryRouter = router({
logo,
} as HeaderData
}),
footer: contentstackBaseProcedure.query(async ({ ctx }) => {
footer: publicProcedure.input(langInput).query(async ({ input }) => {
const responseRef = await request<FooterRefDataRaw>(GetCurrentFooterRef, {
locale: ctx.lang,
locale: input.lang,
})
const response = await request<FooterDataRaw>(
GetCurrentFooter,
{
locale: ctx.lang,
locale: input.lang,
},
{
next: {
tags: [
generateTag(
ctx.lang,
responseRef.data.all_current_footer.items[0].system.uid
),
],
},
tags: [
generateTag(
input.lang,
responseRef.data.all_current_footer.items[0].system.uid
),
],
}
)

View File

@@ -68,9 +68,7 @@ export type Variables = {
export async function getRefsResponse<T>(query: string, variables: Variables) {
const refsResponse = await request<T>(query, variables, {
next: {
tags: [generateRefsResponseTag(variables.locale, variables.url, affix)],
},
tags: [generateRefsResponseTag(variables.locale, variables.url, affix)],
})
if (!refsResponse.data) {
throw notFound(refsResponse)
@@ -91,7 +89,7 @@ export async function getResponse<T>(
variables: Variables,
tags: string[]
) {
const response = await request<T>(query, variables, { next: { tags } })
const response = await request<T>(query, variables, { tags })
if (!response.data) {
throw notFound(response)
}

View File

@@ -1,4 +1,5 @@
import { Lang } from "@/constants/languages"
import { baseUrls } from "@/constants/routes/baseUrls"
import { batchRequest } from "@/lib/graphql/batchRequest"
import {
GetDaDeEnUrlsAccountPage,
@@ -13,7 +14,7 @@ import {
GetFiNoSvUrlsLoyaltyPage,
} from "@/lib/graphql/Query/LoyaltyPage.graphql"
import { internalServerError } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import { publicProcedure, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag"
@@ -91,12 +92,14 @@ async function getLanguageSwitcher(options: LanguageSwitcherVariables) {
}
export const languageSwitcherQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
get: publicProcedure.query(async ({ ctx }) => {
if (!ctx.uid || !ctx.lang) {
return { lang: ctx.lang, urls: baseUrls }
}
const res = await getLanguageSwitcher({
contentType: ctx.contentType!,
uid: ctx.uid,
})
const urls = Object.keys(res.data).reduce<LanguageSwitcherData>(
(acc, key) => {
const item = res.data[key as Lang]?.items[0]

View File

@@ -54,9 +54,7 @@ export const loyaltyPageQueryRouter = router({
uid,
},
{
next: {
tags: [generateRefsResponseTag(lang, uid)],
},
tags: [generateRefsResponseTag(lang, uid)],
}
)
@@ -90,7 +88,7 @@ export const loyaltyPageQueryRouter = router({
locale: lang,
uid,
},
{ next: { tags } }
{ tags }
)
if (!response.data) {

View File

@@ -60,9 +60,7 @@ export const navigationQueryRouter = router({
GetNavigationMyPagesRefs,
{ locale: lang },
{
next: {
tags: [generateRefsResponseTag(lang, "navigation_my_pages")],
},
tags: [generateRefsResponseTag(lang, "navigation_my_pages")],
}
)
@@ -90,7 +88,7 @@ export const navigationQueryRouter = router({
const response = await request<GetNavigationMyPagesData>(
GetNavigationMyPages,
{ locale: lang },
{ next: { tags } }
{ tags }
)
if (!response.data) {

View File

@@ -2,11 +2,7 @@ import { initTRPC } from "@trpc/server"
import { env } from "@/env/server"
import {
badRequestError,
sessionExpiredError,
unauthorizedError,
} from "./errors/trpc"
import { badRequestError, sessionExpiredError } from "./errors/trpc"
import { transformer } from "./transformer"
import type { Meta } from "@/types/trpc/meta"

View File

@@ -12,7 +12,7 @@ export type MainMenuProps = {
links: CurrentHeaderLink[]
logo: Image
topMenuMobileLinks: TopMenuHeaderLink[]
languageSwitcher: React.ReactNode
languageSwitcher: React.ReactNode | null
bookingHref: string
isLoggedIn: boolean
lang: Lang

View File

@@ -6,6 +6,6 @@ export type TopMenuProps = {
frontpageLinkText: string
homeHref: string
links: TopMenuHeaderLink[]
languageSwitcher: React.ReactNode
languageSwitcher: React.ReactNode | null
lang: Lang
}

View File

@@ -13,7 +13,7 @@ export enum Section {
type ExtraInfo = Typename<
{
extra_info: {
text: string
text: string[]
}
},
Section.ContactBlockSectionsExtraInfo

View File

@@ -1,31 +1,23 @@
import { DocumentNode, print } from "graphql"
import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import { edgeRequest } from "@/lib/graphql/edgeRequest"
import { ResolveEntryByUrl } from "@/lib/graphql/Query/ResolveEntry.graphql"
import { internalServerError } from "@/server/errors/next"
import { validateEntryResolveSchema } from "@/types/requests/entry"
export async function resolve(url: string, lang = Lang.en) {
const result = await fetch(env.CMS_URL, {
method: "POST",
headers: {
access_token: env.CMS_ACCESS_TOKEN,
"Content-Type": "application/json",
const response = await edgeRequest(
ResolveEntryByUrl,
{
locale: lang,
url,
},
body: JSON.stringify({
query: print(ResolveEntryByUrl as DocumentNode),
variables: {
locale: lang,
url,
},
}),
})
{
revalidate: 3600,
}
)
const { data } = await result.json()
const validatedData = validateEntryResolveSchema.safeParse(data)
const validatedData = validateEntryResolveSchema.safeParse(response.data)
if (!validatedData.success) {
throw internalServerError(validatedData.error)