feat(SW-498): added sitewide alert
This commit is contained in:
1
app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx
Normal file
1
app/[lang]/(live)/@sitewidealert/[...paths]/page.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "../page"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "../../page"
|
||||||
1
app/[lang]/(live)/@sitewidealert/default.tsx
Normal file
1
app/[lang]/(live)/@sitewidealert/default.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "./page"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from "../../page"
|
||||||
17
app/[lang]/(live)/@sitewidealert/page.tsx
Normal file
17
app/[lang]/(live)/@sitewidealert/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -22,12 +22,14 @@ export default async function RootLayout({
|
|||||||
children,
|
children,
|
||||||
footer,
|
footer,
|
||||||
header,
|
header,
|
||||||
|
sitewidealert,
|
||||||
params,
|
params,
|
||||||
}: React.PropsWithChildren<
|
}: React.PropsWithChildren<
|
||||||
LayoutArgs<LangParams> & {
|
LayoutArgs<LangParams> & {
|
||||||
bookingwidget: React.ReactNode
|
bookingwidget: React.ReactNode
|
||||||
footer: React.ReactNode
|
footer: React.ReactNode
|
||||||
header: React.ReactNode
|
header: React.ReactNode
|
||||||
|
sitewidealert: React.ReactNode
|
||||||
}
|
}
|
||||||
>) {
|
>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
@@ -55,6 +57,7 @@ export default async function RootLayout({
|
|||||||
<body>
|
<body>
|
||||||
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
|
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
|
||||||
<TrpcProvider>
|
<TrpcProvider>
|
||||||
|
{!env.HIDE_FOR_NEXT_RELEASE && <>{sitewidealert}</>}
|
||||||
{header}
|
{header}
|
||||||
{!env.HIDE_FOR_NEXT_RELEASE && <>{bookingwidget}</>}
|
{!env.HIDE_FOR_NEXT_RELEASE && <>{bookingwidget}</>}
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
33
components/SitewideAlert/index.tsx
Normal file
33
components/SitewideAlert/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
components/SitewideAlert/sitewideAlert.module.css
Normal file
9
components/SitewideAlert/sitewideAlert.module.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.sitewideAlert {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alarm {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: calc(var(--header-z-index) + 1);
|
||||||
|
}
|
||||||
49
components/TempDesignSystem/Alert/Sidepeek/index.tsx
Normal file
49
components/TempDesignSystem/Alert/Sidepeek/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.alertSidepeek {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
6
components/TempDesignSystem/Alert/Sidepeek/sidepeek.ts
Normal file
6
components/TempDesignSystem/Alert/Sidepeek/sidepeek.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConfig"
|
||||||
|
|
||||||
|
export interface AlertSidepeekProps {
|
||||||
|
ctaText: string
|
||||||
|
sidePeekContent: NonNullable<SidepeekContent>
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
.alert {
|
.alert {
|
||||||
display: flex;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,34 +10,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--max-width-navigation);
|
||||||
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--Spacing-x2);
|
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;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textWrapper {
|
.textWrapper {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x-half);
|
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 */
|
/* Intent: inline */
|
||||||
@@ -48,59 +38,64 @@
|
|||||||
border: 1px solid var(--Base-Border-Subtle);
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
}
|
}
|
||||||
|
.inline .innerContent {
|
||||||
|
padding-right: var(--Spacing-x3);
|
||||||
|
}
|
||||||
.inline .iconWrapper {
|
.inline .iconWrapper {
|
||||||
padding: var(--Spacing-x-one-and-half);
|
padding: var(--Spacing-x-one-and-half);
|
||||||
}
|
}
|
||||||
.inline.alarm .iconWrapper {
|
.inline.alarm .iconWrapper {
|
||||||
background-color: var(--Main-Red-70);
|
background-color: var(--UI-Semantic-Error);
|
||||||
}
|
}
|
||||||
.inline.warning .iconWrapper {
|
.inline.warning .iconWrapper {
|
||||||
background-color: var(--Main-Yellow-60);
|
background-color: var(--UI-Semantic-Warning);
|
||||||
}
|
}
|
||||||
.inline.info .iconWrapper {
|
.inline.info .iconWrapper {
|
||||||
background-color: var(--Scandic-Blue-70);
|
background-color: var(--UI-Semantic-Information);
|
||||||
}
|
}
|
||||||
.inline .icon,
|
.inline .icon,
|
||||||
.inline .icon * {
|
.inline .icon * {
|
||||||
fill: var(--Base-Surface-Primary-light-Normal);
|
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 */
|
/* Intent: banner */
|
||||||
.banner {
|
.banner {
|
||||||
padding: 0 var(--Spacing-x5);
|
padding: 0 var(--Spacing-x3);
|
||||||
border-left-width: 6px;
|
border-left-width: 6px;
|
||||||
border-left-style: solid;
|
border-left-style: solid;
|
||||||
}
|
}
|
||||||
.banner.alarm {
|
.banner.alarm {
|
||||||
border-left-color: var(--Main-Red-70);
|
border-left-color: var(--UI-Semantic-Error);
|
||||||
background-color: var(--Main-Red-00);
|
background-color: var(--Scandic-Red-00);
|
||||||
}
|
}
|
||||||
.banner.warning {
|
.banner.warning {
|
||||||
border-left-color: var(--Main-Yellow-60);
|
border-left-color: var(--UI-Semantic-Warning);
|
||||||
background-color: var(--Main-Yellow-00);
|
background-color: var(--Scandic-Yellow-00);
|
||||||
}
|
}
|
||||||
.banner.info {
|
.banner.info {
|
||||||
border-left-color: var(--Scandic-Blue-70);
|
border-left-color: var(--UI-Semantic-Information);
|
||||||
background-color: var(--Scandic-Blue-00);
|
background-color: var(--Scandic-Blue-00);
|
||||||
}
|
}
|
||||||
.banner.alarm .icon,
|
.banner.alarm .icon,
|
||||||
.banner.alarm .icon * {
|
.banner.alarm .icon * {
|
||||||
fill: var(--Main-Red-70);
|
fill: var(--UI-Semantic-Error);
|
||||||
}
|
}
|
||||||
.banner.warning .icon,
|
.banner.warning .icon,
|
||||||
.banner.warning .icon * {
|
.banner.warning .icon * {
|
||||||
fill: var(--Main-Yellow-60);
|
fill: var(--UI-Semantic-Warning);
|
||||||
}
|
}
|
||||||
.banner.info .icon,
|
.banner.info .icon,
|
||||||
.banner.info .icon * {
|
.banner.info .icon * {
|
||||||
fill: var(--Scandic-Blue-70);
|
fill: var(--UI-Semantic-Information);
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner .closeButton {
|
@media screen and (min-width: 768px) {
|
||||||
align-self: center;
|
.banner {
|
||||||
padding-left: var(--Spacing-x-one-and-half);
|
padding: 0 var(--Spacing-x5);
|
||||||
|
}
|
||||||
|
.innerContent {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,17 @@ import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConf
|
|||||||
export interface AlertProps extends VariantProps<typeof alertVariants> {
|
export interface AlertProps extends VariantProps<typeof alertVariants> {
|
||||||
className?: string
|
className?: string
|
||||||
type: AlertTypeEnum
|
type: AlertTypeEnum
|
||||||
closeable?: boolean
|
|
||||||
heading?: string
|
heading?: string
|
||||||
text: string
|
text: string
|
||||||
|
phoneContact?: {
|
||||||
|
displayText: string
|
||||||
|
phoneNumber?: string
|
||||||
|
footnote?: string | null
|
||||||
|
} | null
|
||||||
sidepeekContent?: SidepeekContent | null
|
sidepeekContent?: SidepeekContent | null
|
||||||
sidePeekCtaText?: string | null
|
sidepeekCtaText?: string | null
|
||||||
|
link?: {
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
} | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,76 @@
|
|||||||
import { ChevronRightIcon, CloseLargeIcon } from "@/components/Icons"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
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 { getIconByAlertType } from "./utils"
|
||||||
import { alertVariants } from "./variants"
|
import { alertVariants } from "./variants"
|
||||||
|
|
||||||
import styles from "./alert.module.css"
|
import styles from "./alert.module.css"
|
||||||
|
|
||||||
export default async function Alert({
|
import type { AlertProps } from "./alert"
|
||||||
|
|
||||||
|
export default function Alert({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
type,
|
type,
|
||||||
heading,
|
heading,
|
||||||
text,
|
text,
|
||||||
sidePeekCtaText,
|
link,
|
||||||
|
phoneContact,
|
||||||
|
sidepeekCtaText,
|
||||||
sidepeekContent,
|
sidepeekContent,
|
||||||
closeable = false,
|
|
||||||
}: AlertProps) {
|
}: AlertProps) {
|
||||||
const classNames = alertVariants({
|
const classNames = alertVariants({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
type,
|
type,
|
||||||
})
|
})
|
||||||
const intl = await getIntl()
|
|
||||||
|
|
||||||
const Icon = getIconByAlertType(type)
|
const Icon = getIconByAlertType(type)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames}>
|
<section className={classNames}>
|
||||||
<span className={styles.iconWrapper}>
|
|
||||||
<Icon className={styles.icon} width={24} height={24} />
|
|
||||||
</span>
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.textWrapper}>
|
<span className={styles.iconWrapper}>
|
||||||
{heading ? (
|
<Icon className={styles.icon} width={24} height={24} />
|
||||||
<Body className={styles.heading} textTransform="bold" asChild>
|
</span>
|
||||||
<h2>{heading}</h2>
|
<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>
|
</Body>
|
||||||
|
</div>
|
||||||
|
{link ? (
|
||||||
|
<Link color="burgundy" href={link.url}>
|
||||||
|
{link.title}
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
{!link && sidepeekCtaText && sidepeekContent ? (
|
||||||
|
<AlertSidepeek
|
||||||
|
ctaText={sidepeekCtaText}
|
||||||
|
sidePeekContent={sidepeekContent}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Body className={styles.text}>{text}</Body>
|
|
||||||
</div>
|
</div>
|
||||||
{sidePeekCtaText ? (
|
|
||||||
<Button
|
|
||||||
theme="base"
|
|
||||||
variant="icon"
|
|
||||||
intent="text"
|
|
||||||
size="small"
|
|
||||||
wrapping
|
|
||||||
className={styles.sidepeekCta}
|
|
||||||
>
|
|
||||||
{sidePeekCtaText}
|
|
||||||
<ChevronRightIcon />
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
{closeable ? (
|
</section>
|
||||||
<button
|
|
||||||
className={styles.closeButton}
|
|
||||||
aria-label={intl.formatMessage({ id: "Close" })}
|
|
||||||
>
|
|
||||||
<CloseLargeIcon />
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,8 @@ export const alertVariants = cva(styles.alert, {
|
|||||||
[AlertTypeEnum.Alarm]: styles.alarm,
|
[AlertTypeEnum.Alarm]: styles.alarm,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {},
|
defaultVariants: {
|
||||||
|
variant: "inline",
|
||||||
|
type: AlertTypeEnum.Info,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -45,3 +45,7 @@ export const getLanguageSwitcher = cache(
|
|||||||
return serverClient().contentstack.languageSwitcher.get()
|
return serverClient().contentstack.languageSwitcher.get()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const getSiteConfig = cache(async function getMemoizedSiteConfig() {
|
||||||
|
return serverClient().contentstack.base.siteConfig()
|
||||||
|
})
|
||||||
|
|||||||
@@ -750,7 +750,7 @@ export const baseQueryRouter = router({
|
|||||||
sitewideAlert: sitewideAlert
|
sitewideAlert: sitewideAlert
|
||||||
? {
|
? {
|
||||||
...sitewideAlert,
|
...sitewideAlert,
|
||||||
phone_contact: contactConfig
|
phoneContact: contactConfig
|
||||||
? getAlertPhoneContactData(sitewideAlert, contactConfig)
|
? getAlertPhoneContactData(sitewideAlert, contactConfig)
|
||||||
: null,
|
: null,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user