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,
|
||||
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}
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,5 +16,8 @@ export const alertVariants = cva(styles.alert, {
|
||||
[AlertTypeEnum.Alarm]: styles.alarm,
|
||||
},
|
||||
},
|
||||
defaultVariants: {},
|
||||
defaultVariants: {
|
||||
variant: "inline",
|
||||
type: AlertTypeEnum.Info,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -750,7 +750,7 @@ export const baseQueryRouter = router({
|
||||
sitewideAlert: sitewideAlert
|
||||
? {
|
||||
...sitewideAlert,
|
||||
phone_contact: contactConfig
|
||||
phoneContact: contactConfig
|
||||
? getAlertPhoneContactData(sitewideAlert, contactConfig)
|
||||
: null,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user