Merge develop

This commit is contained in:
Linus Flood
2024-09-20 12:57:40 +02:00
28 changed files with 314 additions and 103 deletions

View File

@@ -48,3 +48,5 @@ GOOGLE_STATIC_MAP_KEY=""
GOOGLE_STATIC_MAP_SIGNATURE_SECRET=""
GOOGLE_STATIC_MAP_ID=""
GOOGLE_DYNAMIC_MAP_ID=""
HIDE_FOR_NEXT_RELEASE="true"

View File

@@ -41,3 +41,4 @@ GOOGLE_STATIC_MAP_KEY="test"
GOOGLE_STATIC_MAP_SIGNATURE_SECRET="test"
GOOGLE_STATIC_MAP_ID="test"
GOOGLE_DYNAMIC_MAP_ID="test"
HIDE_FOR_NEXT_RELEASE="true"

View File

@@ -1,5 +1,7 @@
import { notFound } from "next/navigation"
import { env } from "@/env/server"
import ContentPage from "@/components/ContentType/ContentPage"
import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
@@ -21,10 +23,16 @@ export default async function ContentTypePage({
switch (params.contentType) {
case "content-page":
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <ContentPage />
case "loyalty-page":
return <LoyaltyPage />
case "hotel-page":
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <HotelPage />
default:
const type: never = params.contentType

View File

@@ -1,3 +1,7 @@
import { notFound } from "next/navigation"
import { env } from "@/env/server"
import styles from "./layout.module.css"
import { LangParams, LayoutArgs } from "@/types/params"
@@ -5,5 +9,8 @@ import { LangParams, LayoutArgs } from "@/types/params"
export default function HotelReservationLayout({
children,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <div className={styles.layout}>{children}</div>
}

View File

@@ -1,3 +1,6 @@
import { env } from "@/env/server"
import CurrentHeader from "@/components/Current/Header"
import Header from "@/components/Header"
import { setLang } from "@/i18n/serverContext"
@@ -6,5 +9,9 @@ import { LangParams, PageArgs } from "@/types/params"
export default function HeaderPage({ params }: PageArgs<LangParams>) {
setLang(params.lang)
if (env.HIDE_FOR_NEXT_RELEASE) {
return <CurrentHeader />
}
return <Header />
}

View File

@@ -4,10 +4,12 @@ import "@scandic-hotels/design-system/style.css"
import Script from "next/script"
import { Suspense } from "react"
import { env } from "@/env/server"
import TrpcProvider from "@/lib/trpc/Provider"
import TokenRefresher from "@/components/Auth/TokenRefresher"
import AdobeSDKScript from "@/components/Current/AdobeSDKScript"
import CurrentFooter from "@/components/Current/Footer"
import VwoScript from "@/components/Current/VwoScript"
import Footer from "@/components/Footer"
import LoadingSpinner from "@/components/LoadingSpinner"
@@ -56,11 +58,11 @@ export default async function RootLayout({
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<TrpcProvider>
{header}
{bookingwidget}
{!env.HIDE_FOR_NEXT_RELEASE && <>{bookingwidget}</>}
{children}
<ToastHandler />
<Suspense fallback={<LoadingSpinner />}>
<Footer />
{env.HIDE_FOR_NEXT_RELEASE ? <CurrentFooter /> : <Footer />}
</Suspense>
<TokenRefresher />
</TrpcProvider>

View File

@@ -0,0 +1,10 @@
import Header from "@/components/Current/Header"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
export default async function HeaderPage({ params }: PageArgs<LangParams>) {
setLang(params.lang)
return <Header />
}

View File

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

View File

@@ -1,18 +0,0 @@
import { serverClient } from "@/lib/trpc/server"
import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
export default async function LanguageSwitcherRoute({
params,
}: PageArgs<LangParams>) {
setLang(params.lang)
const data = await serverClient().contentstack.languageSwitcher.get()
if (!data) {
return null
}
return <LanguageSwitcher urls={data.urls} />
}

View File

@@ -1,9 +0,0 @@
"use client"
import { baseUrls } from "@/constants/routes/baseUrls"
import LanguageSwitcher from "@/components/Current/Header/LanguageSwitcher"
export default function Error() {
return <LanguageSwitcher urls={baseUrls} />
}

View File

@@ -1,18 +0,0 @@
import { serverClient } from "@/lib/trpc/server"
import MyPagesMobileDropdown from "@/components/Current/Header/MyPagesMobileDropdown"
import { setLang } from "@/i18n/serverContext"
import { LangParams, PageArgs } from "@/types/params"
export default async function MyPagesMobileDropdownPage({
params,
}: PageArgs<LangParams>) {
setLang(params.lang)
const navigation = await serverClient().contentstack.myPages.navigation.get()
if (!navigation) {
return null
}
return <MyPagesMobileDropdown navigation={navigation} />
}

View File

@@ -1,7 +0,0 @@
"use client"
import MyPagesMobileDropdown from "@/components/Current/Header/MyPagesMobileDropdown"
export default function Error() {
return <MyPagesMobileDropdown navigation={null} />
}

View File

@@ -7,7 +7,6 @@ import TokenRefresher from "@/components/Auth/TokenRefresher"
import BookingWidget from "@/components/BookingWidget"
import AdobeScript from "@/components/Current/AdobeScript"
import Footer from "@/components/Current/Footer"
import Header from "@/components/Current/Header"
import LangPopup from "@/components/Current/LangPopup"
import SkipToMainContent from "@/components/SkipToMainContent"
import { getIntl } from "@/i18n"
@@ -27,12 +26,9 @@ export const metadata: Metadata = {
export default async function RootLayout({
children,
params,
languageSwitcher,
myPagesMobileDropdown,
header,
}: React.PropsWithChildren<
LayoutArgs<LangParams> & { languageSwitcher: React.ReactNode } & {
myPagesMobileDropdown: React.ReactNode
}
LayoutArgs<LangParams> & { header: React.ReactNode }
>) {
setLang(params.lang)
const { defaultLocale, locale, messages } = await getIntl()
@@ -68,10 +64,7 @@ export default async function RootLayout({
<LangPopup />
<SkipToMainContent />
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<Header
myPagesMobileDropdown={myPagesMobileDropdown}
languageSwitcher={languageSwitcher}
/>
{header}
<BookingWidget />
{children}
<Footer />

View File

@@ -123,6 +123,7 @@ html,
body {
margin: 0;
padding: 0;
scroll-behavior: smooth;
}
body {
@@ -130,6 +131,16 @@ body {
overflow-x: hidden;
}
body.overflow-hidden {
overflow: hidden;
}
@media screen and (min-width: 768px) {
body.overflow-hidden {
overflow: auto;
overflow-x: hidden;
}
}
ul {
padding-inline-start: 0;
margin-block-start: 0;

View File

@@ -0,0 +1,29 @@
import JsonToHtml from "@/components/JsonToHtml"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { renderOptions } from "./renderOptions"
import styles from "./textcols.module.css"
import type { TextColsProps } from "@/types/components/content/blocks"
export default function TextCols({ textCols }: TextColsProps) {
return (
<div className={styles.columns}>
{textCols.columns.map((col) => {
return (
<section key={col.title} className={styles.column}>
<Subtitle>{col.title}</Subtitle>
<div className={styles.text}>
<JsonToHtml
nodes={col.text.json.children}
embeds={col.text.embedded_itemsConnection.edges}
renderOptions={renderOptions}
/>
</div>
</section>
)
})}
</div>
)
}

View File

@@ -0,0 +1,70 @@
import Link from "@/components/TempDesignSystem/Link"
import styles from "./textcols.module.css"
import type { EmbedByUid } from "@/types/components/jsontohtml"
import { RTEItemTypeEnum, RTETypeEnum } from "@/types/rte/enums"
import type {
RTEDefaultNode,
RTENext,
RTENode,
RTERegularNode,
} from "@/types/rte/node"
import type { RenderOptions } from "@/types/rte/option"
export const renderOptions: RenderOptions = {
[RTETypeEnum.p]: (
node: RTEDefaultNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
return (
<p key={node.uid} className={styles.p}>
{next(node.children, embeds, fullRenderOptions)}
</p>
)
},
[RTETypeEnum.a]: (
node: RTERegularNode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
if (node.attrs.url) {
return (
<a
href={node.attrs.url}
target={node.attrs.target ?? "_blank"}
key={node.uid}
className={styles.a}
>
{next(node.children, embeds, fullRenderOptions)}
</a>
)
}
return null
},
[RTETypeEnum.reference]: (
node: RTENode,
embeds: EmbedByUid,
next: RTENext,
fullRenderOptions: RenderOptions
) => {
if ("attrs" in node) {
const type = node.attrs.type
if (type !== RTEItemTypeEnum.asset) {
const href = node.attrs?.locale
? `/${node.attrs.locale}${node.attrs.href}`
: node.attrs.href
return (
<Link href={href} key={node.uid} className={styles.a}>
{next(node.children, embeds, fullRenderOptions)}
</Link>
)
}
return null
}
},
}

View File

@@ -0,0 +1,40 @@
.columns {
display: flex;
flex-direction: column;
gap: var(--Spacing-x3);
padding: var(--Spacing-x3) var(--Spacing-x4);
}
.column {
padding-bottom: var(--Spacing-x2);
border-bottom: 1px solid var(--Base-Border-Subtle);
gap: var(--Spacing-x1);
display: flex;
flex-direction: column;
}
.p {
color: var(--UI-Text-High-contrast);
line-height: var(--Spacing-x3);
margin: 0;
}
.a {
color: var(--Base-Text-High-contrast);
}
.text > section {
gap: 0;
}
@media (min-width: 768px) {
.columns {
flex-direction: row;
flex-wrap: wrap;
}
.column {
flex: 0 0 calc(50% - var(--Spacing-x3));
max-width: calc(50% - var(--Spacing-x3));
}
}

View File

@@ -3,6 +3,7 @@ import JsonToHtml from "@/components/JsonToHtml"
import Shortcuts from "@/components/MyPages/Blocks/Shortcuts"
import CardsGrid from "./CardsGrid"
import TextCols from "./TextCols"
import type { BlocksProps } from "@/types/components/content/blocks"
import { ContentBlocksTypenameEnum } from "@/types/components/content/enums"
@@ -38,6 +39,8 @@ export function Blocks({ blocks }: BlocksProps) {
firstItem={firstItem}
/>
)
case ContentBlocksTypenameEnum.ContentPageBlocksTextCols:
return <TextCols textCols={block.text_cols} />
default:
return null
}

View File

@@ -4,25 +4,25 @@ import { serverClient } from "@/lib/trpc/server"
import { getLang } from "@/i18n/serverContext"
import LanguageSwitcher from "./LanguageSwitcher"
import { MainMenu } from "./MainMenu"
import MyPagesMobileDropdown from "./MyPagesMobileDropdown"
import OfflineBanner from "./OfflineBanner"
import TopMenu from "./TopMenu"
import styles from "./header.module.css"
export default async function Header({
languageSwitcher,
myPagesMobileDropdown,
}: {
languageSwitcher: React.ReactNode
myPagesMobileDropdown: React.ReactNode
}) {
const data = await serverClient().contentstack.base.currentHeader({
lang: getLang(),
})
const user = await serverClient().user.name()
export default async function Header() {
const [data, user, languages, navigation] = await Promise.all([
serverClient().contentstack.base.currentHeader({
lang: getLang(),
}),
serverClient().user.name(),
serverClient().contentstack.languageSwitcher.get(),
serverClient().contentstack.myPages.navigation.get(),
])
if (!data) {
if (!navigation || !languages || !data) {
return null
}
@@ -40,7 +40,7 @@ export default async function Header({
frontpageLinkText={frontpage_link_text}
homeHref={homeHref}
links={top_menu.links}
languageSwitcher={languageSwitcher}
languageSwitcher={<LanguageSwitcher urls={languages.urls} />}
/>
<MainMenu
frontpageLinkText={frontpage_link_text}
@@ -48,8 +48,10 @@ export default async function Header({
links={menu.links}
logo={logo}
topMenuMobileLinks={topMenuMobileLinks}
languageSwitcher={languageSwitcher}
myPagesMobileDropdown={myPagesMobileDropdown}
languageSwitcher={<LanguageSwitcher urls={languages.urls} />}
myPagesMobileDropdown={
<MyPagesMobileDropdown navigation={navigation} />
}
bookingHref={homeHref}
user={user}
/>

View File

@@ -1,6 +1,5 @@
"use client"
import { useEffect } from "react"
import { useIntl } from "react-intl"
import { languages } from "@/constants/languages"
@@ -28,12 +27,16 @@ export default function LanguageSwitcher({
}: LanguageSwitcherProps) {
const intl = useIntl()
const currentLanguage = useLang()
const {
toggleDropdown,
isFooterLanguageSwitcherOpen,
isHeaderLanguageSwitcherOpen,
isHeaderLanguageSwitcherMobileOpen,
} = useDropdownStore()
const toggleDropdown = useDropdownStore((state) => state.toggleDropdown)
const isFooterLanguageSwitcherOpen = useDropdownStore(
(state) => state.isFooterLanguageSwitcherOpen
)
const isHeaderLanguageSwitcherOpen = useDropdownStore(
(state) => state.isHeaderLanguageSwitcherOpen
)
const isHeaderLanguageSwitcherMobileOpen = useDropdownStore(
(state) => state.isHeaderLanguageSwitcherMobileOpen
)
const isFooter = type === LanguageSwitcherTypesEnum.Footer
const isHeader = !isFooter
@@ -58,17 +61,14 @@ export default function LanguageSwitcher({
}
})
useEffect(() => {
if (isFooter && isFooterLanguageSwitcherOpen) {
document.body.style.overflow = "hidden"
} else {
document.body.style.overflow = ""
}
function handleClick() {
const scrollPosition = window.scrollY
toggleDropdown(dropdownType)
return () => {
document.body.style.overflow = ""
}
}, [isFooter, isFooterLanguageSwitcherOpen])
requestAnimationFrame(() => {
window.scrollTo(0, scrollPosition)
})
}
const classNames = languageSwitcherVariants({ color, position })
@@ -82,7 +82,7 @@ export default function LanguageSwitcher({
? "Close language menu"
: "Open language menu",
})}
onClick={() => toggleDropdown(dropdownType)}
onClick={handleClick}
>
<GlobeIcon width={20} height={20} color={color} />
<span>{languages[currentLanguage]}</span>

7
env/server.ts vendored
View File

@@ -65,6 +65,12 @@ export const env = createEnv({
GOOGLE_STATIC_MAP_SIGNATURE_SECRET: z.string(),
GOOGLE_DYNAMIC_MAP_ID: z.string(),
GOOGLE_STATIC_MAP_ID: z.string(),
HIDE_FOR_NEXT_RELEASE: z
.string()
// only allow "true" or "false"
.refine((s) => s === "true" || s === "false")
// transform to boolean
.transform((s) => s === "true"),
},
emptyStringAsUndefined: true,
runtimeEnv: {
@@ -117,5 +123,6 @@ export const env = createEnv({
process.env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET,
GOOGLE_STATIC_MAP_ID: process.env.GOOGLE_STATIC_MAP_ID,
GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID,
HIDE_FOR_NEXT_RELEASE: process.env.HIDE_FOR_NEXT_RELEASE,
},
})

View File

@@ -98,6 +98,28 @@ query GetContentPage($locale: String!, $uid: String!) {
}
}
}
... on ContentPageBlocksTextCols {
__typename
text_cols {
columns {
title
text {
json
embedded_itemsConnection {
edges {
node {
__typename
...LoyaltyPageLink
...ContentPageLink
...HotelPageLink
}
}
totalCount
}
}
}
}
}
}
title
header {

View File

@@ -12,12 +12,8 @@ import {
SidebarDynamicComponentEnum,
SidebarTypenameEnum,
} from "@/types/components/content/enums"
import { ImageVaultAsset } from "@/types/components/imageVault"
import { Embeds } from "@/types/requests/embeds"
import { PageLinkEnum } from "@/types/requests/pageLinks"
import { RTEEmbedsEnum } from "@/types/requests/rte"
import { EdgesWithTotalCount } from "@/types/requests/utils/edges"
import { RTEDocument } from "@/types/rte/node"
// Block schemas
export const contentPageBlockTextContent = z.object({
@@ -135,11 +131,29 @@ export const contentPageCards = z.object({
}),
})
export const contentPageTextCols = z.object({
__typename: z.literal(ContentBlocksTypenameEnum.ContentPageBlocksTextCols),
text_cols: z.object({
columns: z.array(
z.object({
title: z.string(),
text: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(z.any()),
totalCount: z.number(),
}),
}),
})
),
}),
})
const contentPageBlockItem = z.discriminatedUnion("__typename", [
contentPageBlockTextContent,
contentPageCards,
contentPageDynamicContent,
contentPageShortcuts,
contentPageTextCols,
])
export const contentPageSidebarTextContent = z.object({

View File

@@ -4,13 +4,13 @@ import { ZodError } from "zod"
import { env } from "@/env/server"
import { type Context, createContext } from "./context"
import {
badRequestError,
internalServerError,
sessionExpiredError,
unauthorizedError,
} from "./errors/trpc"
import { type Context, createContext } from "./context"
import { fetchServiceToken } from "./tokenManager"
import { transformer } from "./transformer"

View File

@@ -85,6 +85,11 @@ const useDropdownStore = create<DropdownState>((set, get) => ({
state.isMyPagesMenuOpen = false
state.isHeaderLanguageSwitcherOpen = false
state.isHeaderLanguageSwitcherMobileOpen = false
if (state.isFooterLanguageSwitcherOpen) {
document.body.classList.add("overflow-hidden")
} else {
document.body.classList.remove("overflow-hidden")
}
break
}
})

View File

@@ -7,6 +7,7 @@ import {
Block,
CardsGrid,
DynamicContent,
TextCols,
} from "@/types/trpc/routers/contentstack/contentPage"
export type BlocksProps = {
@@ -17,6 +18,10 @@ export type CardsGridProps = Pick<CardsGrid, "cards_grid"> & {
firstItem?: boolean
}
export type TextColsProps = {
textCols: TextCols["text_cols"]
}
export type DynamicContentProps = {
dynamicContent: DynamicContent["dynamic_content"]
firstItem: boolean

View File

@@ -6,6 +6,7 @@ export enum ContentBlocksTypenameEnum {
ContentPageBlocksShortcuts = "ContentPageBlocksShortcuts",
ContentPageBlocksCardsGrid = "ContentPageBlocksCardsGrid",
ContentPageBlocksDynamicContent = "ContentPageBlocksDynamicContent",
ContentPageBlocksTextCols = "ContentPageBlocksTextCols",
}
export enum CardsGridEnum {

View File

@@ -9,6 +9,7 @@ import {
contentPageShortcuts,
contentPageSidebarDynamicContent,
contentPageSidebarTextContent,
contentPageTextCols,
loyaltyCardBlock,
validateContentPageRefsSchema,
validateContentPageSchema,
@@ -81,4 +82,22 @@ export type CardsGrid = Omit<CardsGridRaw, "cards"> & {
}
export type CardsRaw = CardsGrid["cards_grid"]["cards"][number]
export type Block = RteBlockContent | Shortcuts | CardsGrid | DynamicContent
type TextColsRaw = z.infer<typeof contentPageTextCols>
export interface TextCols extends TextColsRaw {
textCols: {
columns: {
title: string
text: {
json: RTEDocument
embedded_itemsConnection: EdgesWithTotalCount<Embeds>
}
}[]
}
}
export type Block =
| RteBlockContent
| Shortcuts
| CardsGrid
| DynamicContent
| TextCols