feat(SW-498): added sitewide alert

This commit is contained in:
Erik Tiekstra
2024-10-17 11:23:50 +02:00
parent e41bf86993
commit db9f31e2c3
17 changed files with 226 additions and 85 deletions

View File

@@ -0,0 +1 @@
export { default } from "../page"

View File

@@ -0,0 +1 @@
export { default } from "../../page"

View File

@@ -0,0 +1 @@
export { default } from "./page"

View File

@@ -0,0 +1 @@
export { default } from "../../page"

View File

@@ -0,0 +1,17 @@
import { Suspense } from "react"
import SitewideAlert, { preload } from "@/components/SitewideAlert"
import { setLang } from "@/i18n/serverContext"
import type { LangParams, PageArgs } from "@/types/params"
export default function SitewideAlertPage({ params }: PageArgs<LangParams>) {
setLang(params.lang)
preload()
return (
<Suspense>
<SitewideAlert />
</Suspense>
)
}

View File

@@ -22,12 +22,14 @@ export default async function RootLayout({
children,
footer,
header,
sitewidealert,
params,
}: React.PropsWithChildren<
LayoutArgs<LangParams> & {
bookingwidget: React.ReactNode
footer: React.ReactNode
header: React.ReactNode
sitewidealert: React.ReactNode
}
>) {
setLang(params.lang)
@@ -55,6 +57,7 @@ export default async function RootLayout({
<body>
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<TrpcProvider>
{!env.HIDE_FOR_NEXT_RELEASE && <>{sitewidealert}</>}
{header}
{!env.HIDE_FOR_NEXT_RELEASE && <>{bookingwidget}</>}
{children}

View File

@@ -0,0 +1,33 @@
import { getSiteConfig } from "@/lib/trpc/memoizedRequests"
import Alert from "../TempDesignSystem/Alert"
import styles from "./sitewideAlert.module.css"
export function preload() {
void getSiteConfig()
}
export default async function SitewideAlert() {
const siteConfig = await getSiteConfig()
if (!siteConfig?.sitewideAlert) {
return null
}
const { sitewideAlert } = siteConfig
return (
<div className={`${styles.sitewideAlert} ${styles[sitewideAlert.type]}`}>
<Alert
variant="banner"
type={sitewideAlert.type}
link={sitewideAlert.link}
phoneContact={sitewideAlert.phoneContact}
sidepeekCtaText={sitewideAlert.sidepeekButton?.cta_text}
sidepeekContent={sitewideAlert.sidepeekContent}
heading={sitewideAlert.heading}
text={sitewideAlert.text}
/>
</div>
)
}

View File

@@ -0,0 +1,9 @@
.sitewideAlert {
width: 100%;
}
.alarm {
position: sticky;
top: 0;
z-index: calc(var(--header-z-index) + 1);
}

View File

@@ -0,0 +1,49 @@
"use client"
import { useState } from "react"
import { ChevronRightIcon } from "@/components/Icons"
import JsonToHtml from "@/components/JsonToHtml"
import Button from "@/components/TempDesignSystem/Button"
import SidePeek from "../../SidePeek"
import styles from "./sidepeek.module.css"
import type { AlertSidepeekProps } from "./sidepeek"
export default function AlertSidepeek({
ctaText,
sidePeekContent,
}: AlertSidepeekProps) {
const [sidePeekIsOpen, setSidePeekIsOpen] = useState(false)
const { heading, content } = sidePeekContent
return (
<div className={styles.alertSidepeek}>
<Button
onPress={() => setSidePeekIsOpen(true)}
theme="base"
variant="icon"
intent="text"
size="small"
wrapping
>
{ctaText}
<ChevronRightIcon />
</Button>
{sidePeekIsOpen ? (
<SidePeek
title={heading}
isOpen={sidePeekIsOpen}
handleClose={() => setSidePeekIsOpen(false)}
>
<JsonToHtml
nodes={content.json.children}
embeds={content.embedded_itemsConnection.edges}
/>
</SidePeek>
) : null}
</div>
)
}

View File

@@ -0,0 +1,3 @@
.alertSidepeek {
flex-shrink: 0;
}

View File

@@ -0,0 +1,6 @@
import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConfig"
export interface AlertSidepeekProps {
ctaText: string
sidePeekContent: NonNullable<SidepeekContent>
}

View File

@@ -1,6 +1,4 @@
.alert {
display: flex;
gap: var(--Spacing-x2);
overflow: hidden;
}
@@ -12,34 +10,26 @@
}
.content {
width: 100%;
max-width: var(--max-width-navigation);
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--Spacing-x2);
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2)
var(--Spacing-x-one-and-half) 0;
}
.innerContent {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
gap: var(--Spacing-x1);
padding: var(--Spacing-x2) 0;
flex-grow: 1;
}
.textWrapper {
display: grid;
gap: var(--Spacing-x-half);
padding: var(--Spacing-x1) 0;
}
.sidepeekCta {
flex-shrink: 0;
}
.closeButton {
border-width: 0;
padding: 0;
margin: 0;
background-color: transparent;
display: flex;
align-items: center;
flex-shrink: 0;
cursor: pointer;
}
/* Intent: inline */
@@ -48,59 +38,64 @@
border: 1px solid var(--Base-Border-Subtle);
background-color: var(--Base-Surface-Primary-light-Normal);
}
.inline .innerContent {
padding-right: var(--Spacing-x3);
}
.inline .iconWrapper {
padding: var(--Spacing-x-one-and-half);
}
.inline.alarm .iconWrapper {
background-color: var(--Main-Red-70);
background-color: var(--UI-Semantic-Error);
}
.inline.warning .iconWrapper {
background-color: var(--Main-Yellow-60);
background-color: var(--UI-Semantic-Warning);
}
.inline.info .iconWrapper {
background-color: var(--Scandic-Blue-70);
background-color: var(--UI-Semantic-Information);
}
.inline .icon,
.inline .icon * {
fill: var(--Base-Surface-Primary-light-Normal);
}
.inline .closeButton {
border-left: 1px solid var(--Base-Border-Subtle);
padding: var(--Spacing-x-one-and-half);
}
/* Intent: banner */
.banner {
padding: 0 var(--Spacing-x5);
padding: 0 var(--Spacing-x3);
border-left-width: 6px;
border-left-style: solid;
}
.banner.alarm {
border-left-color: var(--Main-Red-70);
background-color: var(--Main-Red-00);
border-left-color: var(--UI-Semantic-Error);
background-color: var(--Scandic-Red-00);
}
.banner.warning {
border-left-color: var(--Main-Yellow-60);
background-color: var(--Main-Yellow-00);
border-left-color: var(--UI-Semantic-Warning);
background-color: var(--Scandic-Yellow-00);
}
.banner.info {
border-left-color: var(--Scandic-Blue-70);
border-left-color: var(--UI-Semantic-Information);
background-color: var(--Scandic-Blue-00);
}
.banner.alarm .icon,
.banner.alarm .icon * {
fill: var(--Main-Red-70);
fill: var(--UI-Semantic-Error);
}
.banner.warning .icon,
.banner.warning .icon * {
fill: var(--Main-Yellow-60);
fill: var(--UI-Semantic-Warning);
}
.banner.info .icon,
.banner.info .icon * {
fill: var(--Scandic-Blue-70);
fill: var(--UI-Semantic-Information);
}
.banner .closeButton {
align-self: center;
padding-left: var(--Spacing-x-one-and-half);
@media screen and (min-width: 768px) {
.banner {
padding: 0 var(--Spacing-x5);
}
.innerContent {
flex-direction: row;
align-items: center;
gap: var(--Spacing-x2);
}
}

View File

@@ -8,9 +8,17 @@ import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConf
export interface AlertProps extends VariantProps<typeof alertVariants> {
className?: string
type: AlertTypeEnum
closeable?: boolean
heading?: string
text: string
phoneContact?: {
displayText: string
phoneNumber?: string
footnote?: string | null
} | null
sidepeekContent?: SidepeekContent | null
sidePeekCtaText?: string | null
sidepeekCtaText?: string | null
link?: {
url: string
title: string
} | null
}

View File

@@ -1,69 +1,76 @@
import { ChevronRightIcon, CloseLargeIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
import { getIntl } from "@/i18n"
import { AlertProps } from "./alert"
import Link from "../Link"
import AlertSidepeek from "./Sidepeek"
import { getIconByAlertType } from "./utils"
import { alertVariants } from "./variants"
import styles from "./alert.module.css"
export default async function Alert({
import type { AlertProps } from "./alert"
export default function Alert({
className,
variant,
type,
heading,
text,
sidePeekCtaText,
link,
phoneContact,
sidepeekCtaText,
sidepeekContent,
closeable = false,
}: AlertProps) {
const classNames = alertVariants({
className,
variant,
type,
})
const intl = await getIntl()
const Icon = getIconByAlertType(type)
return (
<div className={classNames}>
<span className={styles.iconWrapper}>
<Icon className={styles.icon} width={24} height={24} />
</span>
<section className={classNames}>
<div className={styles.content}>
<div className={styles.textWrapper}>
{heading ? (
<Body className={styles.heading} textTransform="bold" asChild>
<h2>{heading}</h2>
<span className={styles.iconWrapper}>
<Icon className={styles.icon} width={24} height={24} />
</span>
<div className={styles.innerContent}>
<div className={styles.textWrapper}>
{heading ? (
<Body className={styles.heading} textTransform="bold" asChild>
<h2>{heading}</h2>
</Body>
) : null}
<Body className={styles.text}>
{text}
{phoneContact?.phoneNumber ? (
<>
<span> {phoneContact.displayText} </span>
<Link
color="burgundy"
href={`tel:${phoneContact.phoneNumber}`}
>
{phoneContact.phoneNumber}
</Link>
{phoneContact.footnote ? (
<span>. ({phoneContact.footnote})</span>
) : null}
</>
) : null}
</Body>
</div>
{link ? (
<Link color="burgundy" href={link.url}>
{link.title}
</Link>
) : null}
{!link && sidepeekCtaText && sidepeekContent ? (
<AlertSidepeek
ctaText={sidepeekCtaText}
sidePeekContent={sidepeekContent}
/>
) : null}
<Body className={styles.text}>{text}</Body>
</div>
{sidePeekCtaText ? (
<Button
theme="base"
variant="icon"
intent="text"
size="small"
wrapping
className={styles.sidepeekCta}
>
{sidePeekCtaText}
<ChevronRightIcon />
</Button>
) : null}
</div>
{closeable ? (
<button
className={styles.closeButton}
aria-label={intl.formatMessage({ id: "Close" })}
>
<CloseLargeIcon />
</button>
) : null}
</div>
</section>
)
}

View File

@@ -16,5 +16,8 @@ export const alertVariants = cva(styles.alert, {
[AlertTypeEnum.Alarm]: styles.alarm,
},
},
defaultVariants: {},
defaultVariants: {
variant: "inline",
type: AlertTypeEnum.Info,
},
})

View File

@@ -45,3 +45,7 @@ export const getLanguageSwitcher = cache(
return serverClient().contentstack.languageSwitcher.get()
}
)
export const getSiteConfig = cache(async function getMemoizedSiteConfig() {
return serverClient().contentstack.base.siteConfig()
})

View File

@@ -750,7 +750,7 @@ export const baseQueryRouter = router({
sitewideAlert: sitewideAlert
? {
...sitewideAlert,
phone_contact: contactConfig
phoneContact: contactConfig
? getAlertPhoneContactData(sitewideAlert, contactConfig)
: null,
}