Merge branch 'develop' into feature/tracking
This commit is contained in:
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>
|
||||
}
|
||||
101
components/TempDesignSystem/Alert/alert.module.css
Normal file
101
components/TempDesignSystem/Alert/alert.module.css
Normal file
@@ -0,0 +1,101 @@
|
||||
.alert {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
max-width: var(--max-width-navigation);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Intent: inline */
|
||||
.inline {
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
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(--UI-Semantic-Error);
|
||||
}
|
||||
.inline.warning .iconWrapper {
|
||||
background-color: var(--UI-Semantic-Warning);
|
||||
}
|
||||
.inline.info .iconWrapper {
|
||||
background-color: var(--UI-Semantic-Information);
|
||||
}
|
||||
.inline .icon,
|
||||
.inline .icon * {
|
||||
fill: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
|
||||
/* Intent: banner */
|
||||
.banner {
|
||||
padding: 0 var(--Spacing-x3);
|
||||
border-left-width: 6px;
|
||||
border-left-style: solid;
|
||||
}
|
||||
.banner.alarm {
|
||||
border-left-color: var(--UI-Semantic-Error);
|
||||
background-color: var(--Scandic-Red-00);
|
||||
}
|
||||
.banner.warning {
|
||||
border-left-color: var(--UI-Semantic-Warning);
|
||||
background-color: var(--Scandic-Yellow-00);
|
||||
}
|
||||
.banner.info {
|
||||
border-left-color: var(--UI-Semantic-Information);
|
||||
background-color: var(--Scandic-Blue-00);
|
||||
}
|
||||
.banner.alarm .icon,
|
||||
.banner.alarm .icon * {
|
||||
fill: var(--UI-Semantic-Error);
|
||||
}
|
||||
.banner.warning .icon,
|
||||
.banner.warning .icon * {
|
||||
fill: var(--UI-Semantic-Warning);
|
||||
}
|
||||
.banner.info .icon,
|
||||
.banner.info .icon * {
|
||||
fill: var(--UI-Semantic-Information);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.banner {
|
||||
padding: 0 var(--Spacing-x5);
|
||||
}
|
||||
.innerContent {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
24
components/TempDesignSystem/Alert/alert.ts
Normal file
24
components/TempDesignSystem/Alert/alert.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { alertVariants } from "./variants"
|
||||
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConfig"
|
||||
|
||||
export interface AlertProps extends VariantProps<typeof alertVariants> {
|
||||
className?: string
|
||||
type: AlertTypeEnum
|
||||
heading?: string | null
|
||||
text?: string | null
|
||||
phoneContact?: {
|
||||
displayText: string
|
||||
phoneNumber?: string
|
||||
footnote?: string | null
|
||||
} | null
|
||||
sidepeekContent?: SidepeekContent | null
|
||||
sidepeekCtaText?: string | null
|
||||
link?: {
|
||||
url: string
|
||||
title: string
|
||||
} | null
|
||||
}
|
||||
80
components/TempDesignSystem/Alert/index.tsx
Normal file
80
components/TempDesignSystem/Alert/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import Link from "../Link"
|
||||
import AlertSidepeek from "./Sidepeek"
|
||||
import { getIconByAlertType } from "./utils"
|
||||
import { alertVariants } from "./variants"
|
||||
|
||||
import styles from "./alert.module.css"
|
||||
|
||||
import type { AlertProps } from "./alert"
|
||||
|
||||
export default function Alert({
|
||||
className,
|
||||
variant,
|
||||
type,
|
||||
heading,
|
||||
text,
|
||||
link,
|
||||
phoneContact,
|
||||
sidepeekCtaText,
|
||||
sidepeekContent,
|
||||
}: AlertProps) {
|
||||
const classNames = alertVariants({
|
||||
className,
|
||||
variant,
|
||||
type,
|
||||
})
|
||||
const Icon = getIconByAlertType(type)
|
||||
|
||||
if (!text && !heading) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={classNames}>
|
||||
<div className={styles.content}>
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
17
components/TempDesignSystem/Alert/utils.ts
Normal file
17
components/TempDesignSystem/Alert/utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { InfoCircleIcon } from "@/components/Icons"
|
||||
import CrossCircleIcon from "@/components/Icons/CrossCircle"
|
||||
import WarningTriangleIcon from "@/components/Icons/WarningTriangle"
|
||||
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
export function getIconByAlertType(alertType: AlertTypeEnum) {
|
||||
switch (alertType) {
|
||||
case AlertTypeEnum.Alarm:
|
||||
return CrossCircleIcon
|
||||
case AlertTypeEnum.Warning:
|
||||
return WarningTriangleIcon
|
||||
case AlertTypeEnum.Info:
|
||||
default:
|
||||
return InfoCircleIcon
|
||||
}
|
||||
}
|
||||
23
components/TempDesignSystem/Alert/variants.ts
Normal file
23
components/TempDesignSystem/Alert/variants.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
import styles from "./alert.module.css"
|
||||
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
export const alertVariants = cva(styles.alert, {
|
||||
variants: {
|
||||
variant: {
|
||||
inline: styles.inline,
|
||||
banner: styles.banner,
|
||||
},
|
||||
type: {
|
||||
[AlertTypeEnum.Info]: styles.info,
|
||||
[AlertTypeEnum.Warning]: styles.warning,
|
||||
[AlertTypeEnum.Alarm]: styles.alarm,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "inline",
|
||||
type: AlertTypeEnum.Info,
|
||||
},
|
||||
})
|
||||
@@ -24,6 +24,7 @@ export default function CardImage({
|
||||
alt={backgroundImage.title}
|
||||
width={180}
|
||||
height={180}
|
||||
focalPoint={backgroundImage.focalPoint}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function Card({
|
||||
|
||||
imageWidth =
|
||||
imageWidth ||
|
||||
(backgroundImage && "dimensions" in backgroundImage
|
||||
(backgroundImage?.dimensions
|
||||
? backgroundImage.dimensions.aspectRatio * imageHeight
|
||||
: 420)
|
||||
|
||||
@@ -53,6 +53,7 @@ export default function Card({
|
||||
alt={backgroundImage.meta.alt || backgroundImage.title}
|
||||
width={imageWidth}
|
||||
height={imageHeight}
|
||||
focalPoint={backgroundImage.focalPoint}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
flex-grow: 1;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Card({
|
||||
const { register } = useFormContext()
|
||||
|
||||
return (
|
||||
<label className={styles.label} data-declined={declined}>
|
||||
<label className={styles.label} data-declined={declined} tabIndex={0}>
|
||||
<Caption className={styles.title} type="label" uppercase>
|
||||
{title}
|
||||
</Caption>
|
||||
|
||||
@@ -28,62 +28,36 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
rules: registerOptions,
|
||||
})
|
||||
|
||||
const [dateSegments, setDateSegment] = useState<{
|
||||
year: number | null
|
||||
month: number | null
|
||||
date: number | null
|
||||
daysInMonth: number
|
||||
}>({
|
||||
year: null,
|
||||
month: null,
|
||||
date: null,
|
||||
daysInMonth: 31,
|
||||
})
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
const months = rangeArray(1, 12).map((month) => ({
|
||||
value: month,
|
||||
label: `${month}`,
|
||||
}))
|
||||
|
||||
const years = rangeArray(1900, currentYear - 18)
|
||||
.reverse()
|
||||
.map((year) => ({ value: year, label: year.toString() }))
|
||||
|
||||
// Ensure the user can't select a date that doesn't exist.
|
||||
const daysInMonth = dt(currentValue).daysInMonth()
|
||||
const days = rangeArray(1, daysInMonth).map((day) => ({
|
||||
value: day,
|
||||
label: `${day}`,
|
||||
}))
|
||||
|
||||
function createOnSelect(selector: DateName) {
|
||||
/**
|
||||
* Months are 0 index based and therefore we
|
||||
* must subtract by 1 to get the selected month
|
||||
*/
|
||||
return (select: Key) => {
|
||||
const value =
|
||||
selector === DateName.month ? Number(select) - 1 : Number(select)
|
||||
const newSegments = { ...dateSegments, [selector]: value }
|
||||
|
||||
/**
|
||||
* Update daysInMonth when year or month changes
|
||||
* to ensure the user can't select a date that doesn't exist.
|
||||
*/
|
||||
if (selector === DateName.year || selector === DateName.month) {
|
||||
const year = selector === DateName.year ? value : newSegments.year
|
||||
const month = selector === DateName.month ? value : newSegments.month
|
||||
if (year !== null && month !== null) {
|
||||
newSegments.daysInMonth = dt().year(year).month(month).daysInMonth()
|
||||
} else if (month !== null) {
|
||||
newSegments.daysInMonth = dt().month(month).daysInMonth()
|
||||
}
|
||||
if (selector === DateName.month) {
|
||||
select = Number(select) - 1
|
||||
}
|
||||
|
||||
if (Object.values(newSegments).every((val) => val !== null)) {
|
||||
const newDate = dt()
|
||||
.utc()
|
||||
.set("year", newSegments.year!)
|
||||
.set("month", newSegments.month!)
|
||||
.set("date", Math.min(newSegments.date!, newSegments.daysInMonth))
|
||||
|
||||
setValue(name, newDate.format("YYYY-MM-DD"))
|
||||
trigger(name)
|
||||
}
|
||||
setDateSegment(newSegments)
|
||||
const newDate = dt(currentValue).set(selector, Number(select))
|
||||
setValue(name, newDate.format("YYYY-MM-DD"))
|
||||
trigger(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,11 +91,6 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
{(segment) => {
|
||||
switch (segment.type) {
|
||||
case "day":
|
||||
const maxDays = dateSegments.daysInMonth
|
||||
const days = rangeArray(1, maxDays).map((day) => ({
|
||||
value: day,
|
||||
label: `${day}`,
|
||||
}))
|
||||
return (
|
||||
<div className={styles.day}>
|
||||
<Select
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.phone {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: max(164px) 1fr;
|
||||
grid-template-columns: minmax(124px, 164px) 1fr;
|
||||
|
||||
--react-international-phone-background-color: var(--Main-Grey-White);
|
||||
--react-international-phone-border-color: var(--Scandic-Beige-40);
|
||||
|
||||
@@ -3,6 +3,7 @@ import NextLink from "next/link"
|
||||
import { usePathname, useSearchParams } from "next/navigation"
|
||||
import { useCallback, useMemo } from "react"
|
||||
|
||||
import { useCheckIfExternalLink } from "@/hooks/useCheckIfExternalLink"
|
||||
import { trackClick } from "@/utils/tracking"
|
||||
|
||||
import { linkVariants } from "./variants"
|
||||
@@ -46,22 +47,40 @@ export default function Link({
|
||||
})
|
||||
|
||||
const fullUrl = useMemo(() => {
|
||||
const search =
|
||||
keepSearchParams && searchParams.size ? `?${searchParams}` : ""
|
||||
return `${href}${search}`
|
||||
if (!keepSearchParams || !searchParams.size) return href
|
||||
|
||||
const delimiter = href.includes("?") ? "&" : "?"
|
||||
return `${href}${delimiter}${searchParams}`
|
||||
}, [href, searchParams, keepSearchParams])
|
||||
|
||||
// TODO: Remove this check (and hook) and only return <Link /> when current web is deleted
|
||||
const isExternal = useCheckIfExternalLink(href)
|
||||
|
||||
const trackClickById = useCallback(() => {
|
||||
if (trackingId) {
|
||||
trackClick(trackingId)
|
||||
}
|
||||
}, [trackingId])
|
||||
|
||||
return (
|
||||
const linkProps = {
|
||||
href: fullUrl,
|
||||
className: classNames,
|
||||
}
|
||||
|
||||
return isExternal ? (
|
||||
<a
|
||||
{...linkProps}
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
if (onClick) {
|
||||
onClick(e)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<NextLink
|
||||
scroll={scroll}
|
||||
prefetch={prefetch}
|
||||
className={classNames}
|
||||
onClick={(e) => {
|
||||
if (onClick) {
|
||||
onClick(e)
|
||||
@@ -70,9 +89,9 @@ export default function Link({
|
||||
trackClickById()
|
||||
}
|
||||
}}
|
||||
href={fullUrl}
|
||||
id={trackingId}
|
||||
{...props}
|
||||
{...linkProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function LoyaltyCard({
|
||||
height={160}
|
||||
className={styles.image}
|
||||
alt={image.meta.alt || image.title}
|
||||
focalPoint={image.focalPoint}
|
||||
/>
|
||||
) : null}
|
||||
<Title as="h5" level="h3" textAlign="center">
|
||||
|
||||
@@ -16,8 +16,8 @@ export default function ShowMoreButton({
|
||||
intent,
|
||||
disabled,
|
||||
showLess,
|
||||
textShowMore = "Show less",
|
||||
textShowLess = "Show more",
|
||||
textShowMore = "Show more",
|
||||
textShowLess = "Show less",
|
||||
loadMoreData,
|
||||
}: ShowMoreButtonProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function TeaserCard({
|
||||
className={styles.backgroundImage}
|
||||
width={399}
|
||||
height={201}
|
||||
focalPoint={image.focalPoint}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 399px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user