Merge branch 'develop' into feature/tracking

This commit is contained in:
Linus Flood
2024-10-24 12:39:34 +02:00
221 changed files with 5789 additions and 1491 deletions

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

@@ -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);
}
}

View 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
}

View 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>
)
}

View 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
}
}

View 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,
},
})

View File

@@ -24,6 +24,7 @@ export default function CardImage({
alt={backgroundImage.title}
width={180}
height={180}
focalPoint={backgroundImage.focalPoint}
/>
)
)}

View File

@@ -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>
)}

View File

@@ -21,7 +21,6 @@
}
.checkbox {
flex-grow: 1;
width: 24px;
height: 24px;
min-width: 24px;

View File

@@ -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>

View File

@@ -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

View File

@@ -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);

View File

@@ -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}
/>
)
}

View File

@@ -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">

View File

@@ -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()

View File

@@ -35,6 +35,7 @@ export default function TeaserCard({
className={styles.backgroundImage}
width={399}
height={201}
focalPoint={image.focalPoint}
/>
</div>
)}

View File

@@ -2,7 +2,6 @@
border-radius: var(--Corner-radius-Medium);
display: flex;
flex-direction: column;
max-width: 399px;
overflow: hidden;
}