From 78238600ba689cb1d61f877909013a3b237388bc Mon Sep 17 00:00:00 2001 From: Michael Zetterberg Date: Fri, 25 Oct 2024 10:59:08 +0200 Subject: [PATCH 01/15] fix: guard against unsupported next level --- .../DynamicContent/Overview/Stats/Points/index.tsx | 11 ++++++----- .../DynamicContent/Points/Overview/Points/index.tsx | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx b/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx index a17e8d0ee..1a99f8985 100644 --- a/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx +++ b/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx @@ -14,11 +14,12 @@ export default async function Points({ user }: UserProps) { const membership = getMembership(user.memberships) - const nextLevel = membership?.nextLevel - ? await serverClient().contentstack.loyaltyLevels.byLevel({ - level: MembershipLevelEnum[membership.nextLevel], - }) - : null + const nextLevel = + membership?.nextLevel && MembershipLevelEnum[membership.nextLevel] + ? await serverClient().contentstack.loyaltyLevels.byLevel({ + level: MembershipLevelEnum[membership.nextLevel], + }) + : null return ( diff --git a/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx b/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx index 4e0f76291..79e0a9164 100644 --- a/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx +++ b/components/Blocks/DynamicContent/Points/Overview/Points/index.tsx @@ -20,7 +20,7 @@ export default async function Points({ user, lang }: UserProps & LangParams) { const { formatMessage } = await getIntl() const membership = getMembership(user.memberships) - if (!membership?.nextLevel) { + if (!membership?.nextLevel || !MembershipLevelEnum[membership.nextLevel]) { return null } const nextLevel = await serverClient().contentstack.loyaltyLevels.byLevel({ From 320631925464f323e7a694604e469a6e305000d7 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Tue, 8 Oct 2024 17:18:20 +0200 Subject: [PATCH 02/15] feat(SW-556): add surprise notification component --- .../Rewards/CurrentLevel/Client.tsx | 42 +++- .../Rewards/Surprises/index.tsx | 231 ++++++++++++++++++ .../Rewards/Surprises/surprises.module.css | 140 +++++++++++ components/Icons/ChevronRightSmall.tsx | 21 +- .../Text/CaptionLabel/captionLabel.module.css | 77 ++++++ .../Text/CaptionLabel/captionLabel.ts | 10 + .../Text/CaptionLabel/index.tsx | 32 +++ .../Text/CaptionLabel/variants.ts | 58 +++++ components/TempDesignSystem/Toasts/index.tsx | 8 +- components/TempDesignSystem/Toasts/toasts.ts | 2 +- i18n/dictionaries/da.json | 12 +- i18n/dictionaries/de.json | 12 +- i18n/dictionaries/en.json | 14 +- i18n/dictionaries/fi.json | 12 +- i18n/dictionaries/no.json | 12 +- i18n/dictionaries/sv.json | 12 +- public/_static/img/loyalty-award.png | Bin 0 -> 13358 bytes server/routers/contentstack/reward/input.ts | 4 + server/routers/contentstack/reward/output.ts | 40 +-- server/routers/contentstack/reward/query.ts | 49 +++- 20 files changed, 723 insertions(+), 65 deletions(-) create mode 100644 components/Blocks/DynamicContent/Rewards/Surprises/index.tsx create mode 100644 components/Blocks/DynamicContent/Rewards/Surprises/surprises.module.css create mode 100644 components/TempDesignSystem/Text/CaptionLabel/captionLabel.module.css create mode 100644 components/TempDesignSystem/Text/CaptionLabel/captionLabel.ts create mode 100644 components/TempDesignSystem/Text/CaptionLabel/index.tsx create mode 100644 components/TempDesignSystem/Text/CaptionLabel/variants.ts create mode 100644 public/_static/img/loyalty-award.png diff --git a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx index a57e5e160..bad1f6694 100644 --- a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx +++ b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx @@ -1,7 +1,7 @@ "use client" import { trpc } from "@/lib/trpc/client" -import { Reward } from "@/server/routers/contentstack/reward/output" +import { ApiReward, Reward } from "@/server/routers/contentstack/reward/output" import LoadingSpinner from "@/components/LoadingSpinner" import Grids from "@/components/TempDesignSystem/Grids" @@ -9,10 +9,16 @@ import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" import Title from "@/components/TempDesignSystem/Text/Title" import useLang from "@/hooks/useLang" +import Surprises from "../Surprises" + import styles from "./current.module.css" type CurrentRewardsClientProps = { - initialCurrentRewards: { rewards: Reward[]; nextCursor: number | undefined } + initialCurrentRewards: { + rewards: Reward[] + apiRewards: ApiReward[] + nextCursor: number | undefined + } } export default function ClientCurrentRewards({ initialCurrentRewards, @@ -32,25 +38,34 @@ export default function ClientCurrentRewards({ }, } ) - function loadMoreData() { - if (hasNextPage) { - fetchNextPage() - } - } - const filteredRewards = - data?.pages.filter((page) => page && page.rewards) ?? [] - const rewards = filteredRewards.flatMap((page) => page?.rewards) as Reward[] if (isLoading) { return } - if (!rewards.length) { + const rewards = + data?.pages + .flatMap((page) => page?.rewards) + .filter((reward): reward is Reward => !!reward) ?? [] + + const surprises = + data?.pages + .flatMap((page) => page?.apiRewards) + .filter((reward): reward is ApiReward => reward?.type === "surprise") ?? + [] + + if (!rewards.length && !surprises.length) { return null } + function loadMoreData() { + if (hasNextPage) { + fetchNextPage() + } + } + return ( -
+ <> {rewards.map((reward, idx) => (
@@ -71,6 +86,7 @@ export default function ClientCurrentRewards({ ) : ( ))} -
+ + ) } diff --git a/components/Blocks/DynamicContent/Rewards/Surprises/index.tsx b/components/Blocks/DynamicContent/Rewards/Surprises/index.tsx new file mode 100644 index 000000000..486b0423b --- /dev/null +++ b/components/Blocks/DynamicContent/Rewards/Surprises/index.tsx @@ -0,0 +1,231 @@ +"use client" + +import React, { useState } from "react" +import { Dialog, Modal, ModalOverlay } from "react-aria-components" +import { useIntl } from "react-intl" + +import { benefits } from "@/constants/routes/myPages" +import { trpc } from "@/lib/trpc/client" +import { ApiReward } from "@/server/routers/contentstack/reward/output" + +import { ChevronRightSmallIcon, CloseLargeIcon } from "@/components/Icons" +import Image from "@/components/Image" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import CaptionLabel from "@/components/TempDesignSystem/Text/CaptionLabel" +import Title from "@/components/TempDesignSystem/Text/Title" +import { toast } from "@/components/TempDesignSystem/Toasts" +import useLang from "@/hooks/useLang" + +import styles from "./surprises.module.css" + +interface SurprisesProps { + surprises: ApiReward[] +} + +export default function Surprises({ surprises }: SurprisesProps) { + const lang = useLang() + const [open, setOpen] = useState(true) + const [selectedSurprise, setSelectedSurprise] = useState(0) + const [showSurprises, setShowSurprises] = useState(false) + const update = trpc.contentstack.rewards.update.useMutation() + const intl = useIntl() + + if (!surprises.length) return null + + function showSurprise(n: number) { + setSelectedSurprise((surprise) => surprise + n) + } + + function viewRewards(id?: string) { + if (!id) return + update.mutate({ id }) + } + + const surprise = surprises[selectedSurprise] + + return ( + + + + {({ close }) => { + return ( + <> +
+ {surprises.length > 1 && showSurprises && ( + + {intl.formatMessage( + { id: "{amount} out of {total}" }, + { + amount: selectedSurprise + 1, + total: surprises.length, + } + )} + + )} + +
+ {showSurprises ? ( + <> +
+ + + This is just some dummy text describing the gift and + should be replaced. + +
+ Valid through DD M YYYY + Member ID 000000 +
+
+
+ {surprises.length > 1 && ( + <> + + + )} + + ) : ( +
+ {surprises.length > 1 ? ( + + + {intl.formatMessage( + { + id: "You have # gifts waiting for you!", + }, + { + amount: surprises.length, + b: (str) => {str}, + } + )} +
+ {intl.formatMessage({ + id: "Hurry up and use them before they expire!", + })} + + + {intl.formatMessage({ + id: "You'll find all your gifts in 'My benefits'", + })} + +
+ ) : ( + + + {intl.formatMessage({ + id: "We have a special gift waiting for you!", + })} + + + {intl.formatMessage({ + id: "You'll find all your gifts in 'My benefits'", + })} + + + )} + + +
+ )} + + ) + }} +
+
+
+ ) +} + +function Surprise({ + title, + children, +}: { + title?: string + children?: React.ReactNode +}) { + return ( + <> + Gift + + {title} + + + {children} + + ) +} diff --git a/components/Blocks/DynamicContent/Rewards/Surprises/surprises.module.css b/components/Blocks/DynamicContent/Rewards/Surprises/surprises.module.css new file mode 100644 index 000000000..5932145c8 --- /dev/null +++ b/components/Blocks/DynamicContent/Rewards/Surprises/surprises.module.css @@ -0,0 +1,140 @@ +.icon { + align-self: center; +} + +@keyframes modal-fade { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes slide-up { + from { + transform: translateY(100%); + } + + to { + transform: translateY(0); + } +} + +.overlay { + background: rgba(0, 0, 0, 0.5); + height: var(--visual-viewport-height); + position: fixed; + top: 0; + left: 0; + width: 100vw; + z-index: 100; + + &[data-entering] { + animation: modal-fade 200ms; + } + &[data-exiting] { + animation: modal-fade 200ms reverse ease-in; + } +} + +@media screen and (min-width: 768px) { + .overlay { + display: flex; + justify-content: center; + align-items: center; + } +} + +.modal { + background-color: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Medium); + box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08); + width: 100%; + position: absolute; + left: 0; + bottom: 0; + transition: height 200ms ease-in-out; + + &[data-entering] { + animation: slide-up 200ms; + } + &[data-exiting] { + animation: slide-up 200ms reverse ease-in-out; + } +} + +.dialog { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + padding-bottom: var(--Spacing-x2); +} + +@media screen and (min-width: 768px) { + .modal { + left: auto; + bottom: auto; + width: 400px; + } +} + +.top { + --button-height: 32px; + box-sizing: content-box; + display: flex; + align-items: center; + height: var(--button-height); + position: relative; + justify-content: center; + padding: var(--Spacing-x2) var(--Spacing-x2) 0; +} + +.content { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 var(--Spacing-x3); + gap: var(--Spacing-x2); +} + +.nav { + border-top: 1px solid var(--Base-Border-Subtle); + display: flex; + justify-content: space-between; + padding: 0 var(--Spacing-x2); + width: 100%; +} + +.nav button { + &[disabled] { + visibility: hidden; + } +} + +.chevron { + transform: rotate(180deg); +} + +.badge { + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--Spacing-x-half); + background-color: var(--Base-Surface-Secondary-light-Normal); + border-radius: var(--Corner-radius-Small); +} + +.close { + background: none; + border: none; + cursor: pointer; + position: absolute; + right: var(--Spacing-x2); + width: 32px; + height: var(--button-height); + display: flex; + align-items: center; +} diff --git a/components/Icons/ChevronRightSmall.tsx b/components/Icons/ChevronRightSmall.tsx index 25fb29002..1b69fd25b 100644 --- a/components/Icons/ChevronRightSmall.tsx +++ b/components/Icons/ChevronRightSmall.tsx @@ -18,23 +18,10 @@ export default function ChevronRightSmallIcon({ fill="none" {...props} > - - - - - - + ) } diff --git a/components/TempDesignSystem/Text/CaptionLabel/captionLabel.module.css b/components/TempDesignSystem/Text/CaptionLabel/captionLabel.module.css new file mode 100644 index 000000000..987f31d71 --- /dev/null +++ b/components/TempDesignSystem/Text/CaptionLabel/captionLabel.module.css @@ -0,0 +1,77 @@ +p.caption { + margin: 0; + padding: 0; +} + +.captionFontOnly { + font-style: normal; +} + +.uppercase { + font-family: var(--typography-Caption-Labels-fontFamily); + font-size: var(--typography-Caption-Labels-fontSize); + font-weight: var(--typography-Caption-Labels-fontWeight); + letter-spacing: var(--typography-Caption-Labels-letterSpacing); + line-height: var(--typography-Caption-Labels-lineHeight); + text-transform: uppercase; +} + +.regular { + font-family: var(--typography-Caption-Labels-fontFamily); + font-size: var(--typography-Caption-Labels-fontSize); + font-weight: var(--typography-Caption-Labels-fontWeight); + letter-spacing: var(--typography-Caption-Labels-letterSpacing); + line-height: var(--typography-Caption-Labels-lineHeight); +} + +.baseTextAccent { + color: var(--Base-Text-Accent); +} + +.black { + color: var(--Main-Grey-100); +} + +.burgundy { + color: var(--Scandic-Brand-Burgundy); +} + +.pale { + color: var(--Scandic-Brand-Pale-Peach); +} + +.textMediumContrast { + color: var(--Base-Text-Medium-contrast); +} + +.red { + color: var(--Scandic-Brand-Scandic-Red); +} + +.white { + color: var(--UI-Opacity-White-100); +} + +.uiTextActive { + color: var(--UI-Text-Active); +} + +.uiTextMediumContrast { + color: var(--UI-Text-Medium-contrast); +} + +.uiTextHighContrast { + color: var(--UI-Text-High-contrast); +} + +.disabled { + color: var(--Base-Text-Disabled); +} + +.center { + text-align: center; +} + +.left { + text-align: left; +} diff --git a/components/TempDesignSystem/Text/CaptionLabel/captionLabel.ts b/components/TempDesignSystem/Text/CaptionLabel/captionLabel.ts new file mode 100644 index 000000000..89bb313f0 --- /dev/null +++ b/components/TempDesignSystem/Text/CaptionLabel/captionLabel.ts @@ -0,0 +1,10 @@ +import { captionVariants } from "./variants" + +import type { VariantProps } from "class-variance-authority" + +export interface CaptionLabelProps + extends Omit, "color">, + VariantProps { + asChild?: boolean + fontOnly?: boolean +} diff --git a/components/TempDesignSystem/Text/CaptionLabel/index.tsx b/components/TempDesignSystem/Text/CaptionLabel/index.tsx new file mode 100644 index 000000000..21c696eea --- /dev/null +++ b/components/TempDesignSystem/Text/CaptionLabel/index.tsx @@ -0,0 +1,32 @@ +import { Slot } from "@radix-ui/react-slot" + +import { captionVariants, fontOnlycaptionVariants } from "./variants" + +import type { CaptionLabelProps } from "./captionLabel" + +export default function CaptionLabel({ + asChild = false, + className = "", + color, + fontOnly = false, + textAlign, + textTransform, + uppercase, + ...props +}: CaptionLabelProps) { + const Comp = asChild ? Slot : "span" + const classNames = fontOnly + ? fontOnlycaptionVariants({ + className, + textTransform, + uppercase, + }) + : captionVariants({ + className, + color, + textTransform, + textAlign, + uppercase, + }) + return +} diff --git a/components/TempDesignSystem/Text/CaptionLabel/variants.ts b/components/TempDesignSystem/Text/CaptionLabel/variants.ts new file mode 100644 index 000000000..ab2911a25 --- /dev/null +++ b/components/TempDesignSystem/Text/CaptionLabel/variants.ts @@ -0,0 +1,58 @@ +import { cva } from "class-variance-authority" + +import styles from "./captionLabel.module.css" + +const config = { + variants: { + color: { + baseTextAccent: styles.baseTextAccent, + black: styles.black, + burgundy: styles.burgundy, + pale: styles.pale, + textMediumContrast: styles.textMediumContrast, + red: styles.red, + white: styles.white, + uiTextHighContrast: styles.uiTextHighContrast, + uiTextActive: styles.uiTextActive, + uiTextMediumContrast: styles.uiTextMediumContrast, + disabled: styles.disabled, + }, + textTransform: { + regular: styles.regular, + uppercase: styles.uppercase, + }, + textAlign: { + center: styles.center, + left: styles.left, + }, + uppercase: { + true: styles.uppercase, + }, + }, + defaultVariants: { + color: "black", + textTransform: "regular", + }, +} as const + +export const captionVariants = cva(styles.caption, config) + +const fontOnlyConfig = { + variants: { + textTransform: { + regular: styles.regular, + uppercase: styles.uppercase, + }, + uppercase: { + true: styles.uppercase, + }, + }, + defaultVariants: { + textTransform: "regular", + }, +} as const + +export const fontOnlycaptionVariants = cva( + styles.captionFontOnly, + fontOnlyConfig +) diff --git a/components/TempDesignSystem/Toasts/index.tsx b/components/TempDesignSystem/Toasts/index.tsx index df08c6803..f78360a0b 100644 --- a/components/TempDesignSystem/Toasts/index.tsx +++ b/components/TempDesignSystem/Toasts/index.tsx @@ -49,7 +49,7 @@ export function Toast({ message, onClose, variant }: ToastsProps) { } export const toast = { - success: (message: string, options?: ExternalToast) => + success: (message: React.ReactNode, options?: ExternalToast) => sonnerToast.custom( (t) => ( + info: (message: React.ReactNode, options?: ExternalToast) => sonnerToast.custom( (t) => ( + error: (message: React.ReactNode, options?: ExternalToast) => sonnerToast.custom( (t) => ( + warning: (message: React.ReactNode, options?: ExternalToast) => sonnerToast.custom( (t) => ( , "color">, VariantProps { - message: string + message: React.ReactNode onClose: () => void } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 88ac5e6b3..3644b55ae 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -364,5 +364,15 @@ "Zoom out": "Zoom ud", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{width} cm × {length} cm": "{width} cm × {length} cm", + "Surprise!": "Overraskelse!", + "You have # gifts waiting for you!": "Du har {amount} gaver, der venter på dig!", + "Hurry up and use them before they expire!": "Skynd dig og brug dem, før de udløber!", + "We have a special gift waiting for you!": "Vi har en speciel gave, der venter på dig!", + "You'll find all your gifts in 'My benefits'": "Du finder alle dine gaver i ‘Mine fordele’", + "Open gift(s)": "Åbne {amount, plural, one {gave} other {gaver}}", + "{amount} out of {total}": "{amount} ud af {total}", + "Gift(s) added to your benefits": "{amount, plural, one {Gave} other {Gaver}} tilføjet til dine fordele", + "Go to My Benefits": "Gå til ‘Mine fordele’", + "Previous": "Forudgående" } diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 8c3c9e4ce..8d513f1e8 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -364,5 +364,15 @@ "Zoom out": "Verkleinern", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{width} cm × {length} cm": "{width} cm × {length} cm", + "Surprise!": "Überraschung!", + "You have # gifts waiting for you!": "Es warten {amount} Geschenke auf Sie!", + "Hurry up and use them before they expire!": "Beeilen Sie sich und nutzen Sie sie, bevor sie ablaufen!", + "We have a special gift waiting for you!": "Wir haben ein besonderes Geschenk für Sie!", + "You'll find all your gifts in 'My benefits'": "Alle Ihre Geschenke finden Sie unter „Meine Vorteile“", + "Open gift(s)": "{amount, plural, one {Geschenk} other {Geschenke}} öffnen", + "{amount} out of {total}": "{amount} von {total}", + "Gift(s) added to your benefits": "{amount, plural, one {Geschenk zu Ihren Vorteilen hinzugefügt} other {Geschenke, die zu Ihren Vorteilen hinzugefügt werden}}", + "Go to My Benefits": "Gehen Sie zu „Meine Vorteile“", + "Previous": "Früher" } diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 3500a8d01..aad71941d 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -387,5 +387,15 @@ "{amount} {currency}": "{amount} {currency}", "{card} ending with {cardno}": "{card} ending with {cardno}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" -} \ No newline at end of file + "{width} cm × {length} cm": "{width} cm × {length} cm", + "Surprise!": "Surprise!", + "You have # gifts waiting for you!": "You have {amount} gifts waiting for you!", + "Hurry up and use them before they expire!": "Hurry up and use them before they expire!", + "We have a special gift waiting for you!": "We have a special gift waiting for you!", + "You'll find all your gifts in 'My benefits'": "You’ll find all your gifts in ‘My benefits’", + "Open gift(s)": "Open {amount, plural, one {gift} other {gifts}}", + "{amount} out of {total}": "{amount} out of {total}", + "Gift(s) added to your benefits": "{amount, plural, one {Gift} other {Gifts}} added to your benefits", + "Go to My Benefits": "Go to My Benefits", + "Previous": "Previous" +} diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index ddb3b80fa..c18b9d7c3 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -365,5 +365,15 @@ "Zoom out": "Loitonna", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{width} cm × {length} cm": "{width} cm × {length} cm", + "Surprise!": "Yllätys!", + "You have # gifts waiting for you!": "Sinulla on {amount} lahjaa odottamassa sinua!", + "Hurry up and use them before they expire!": "Ole nopea ja käytä ne ennen kuin ne vanhenevat!", + "We have a special gift waiting for you!": "Meillä on erityinen lahja odottamassa sinua!", + "You'll find all your gifts in 'My benefits'": "Löydät kaikki lahjasi kohdasta ‘Omat edut’", + "Open gift(s)": "{amount, plural, one {Avoin lahja} other {Avoimet lahjat}}", + "{amount} out of {total}": "{amount}/{total}", + "Gift(s) added to your benefits": "{amount, plural, one {Lahja} other {Lahjat}} lisätty etuusi", + "Go to My Benefits": "Siirry kohtaan ‘Omat edut’", + "Previous": "Aikaisempi" } diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 56fcc5dc8..86e9a7809 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -361,5 +361,15 @@ "Zoom out": "Zoom ut", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{width} cm × {length} cm": "{width} cm × {length} cm", + "Surprise!": "Overraskelse!", + "You have # gifts waiting for you!": "Du har {amount} gaver som venter på deg!", + "Hurry up and use them before they expire!": "Skynd deg og bruk dem før de utløper!", + "We have a special gift waiting for you!": "Vi har en spesiell gave som venter på deg!", + "You'll find all your gifts in 'My benefits'": "Du finner alle gavene dine i ‘Mine fordeler’", + "Open gift(s)": "{amount, plural, one {Åpen gave} other {Åpnen gaver}}", + "{amount} out of {total}": "{amount} av {total}", + "Gift(s) added to your benefits": "{amount, plural, one {Gave} other {Gaver}} lagt til fordelene dine", + "Go to My Benefits": "Gå til ‘Mine fordeler’", + "Previous": "Tidligere" } diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index b22278223..c88b0f392 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -362,5 +362,15 @@ "Zoom out": "Zooma ut", "{amount} {currency}": "{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}", - "{width} cm × {length} cm": "{width} cm × {length} cm" + "{width} cm × {length} cm": "{width} cm × {length} cm", + "Surprise!": "Överraskning!", + "You have # gifts waiting for you!": "Du har {amount} presenter som väntar på dig!", + "Hurry up and use them before they expire!": "Skynda dig och använd dem innan de går ut!", + "We have a special gift waiting for you!": "Vi har en speciell present som väntar på dig!", + "You'll find all your gifts in 'My benefits'": "Du hittar alla dina gåvor i ‘Mina förmåner’", + "Open gift(s)": "Öppna {amount, plural, one {gåva} other {gåvor}}", + "{amount} out of {total}": "{amount} av {total}", + "Gift(s) added to your benefits": "{amount, plural, one {Gåva} other {Gåvor}} läggs till dina förmåner", + "Go to My Benefits": "Gå till ‘Mina förmåner’", + "Previous": "Föregående" } diff --git a/public/_static/img/loyalty-award.png b/public/_static/img/loyalty-award.png new file mode 100644 index 0000000000000000000000000000000000000000..b8e7054cac8bad69a1ee4184d4054e72434d5165 GIT binary patch literal 13358 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 data.data) enum TierKey { tier1 = MembershipLevelEnum.L1, @@ -37,19 +37,17 @@ export const validateApiTierRewardsSchema = z.record( return TierKey[data as unknown as Key] }), z.array( - z - .object({ - title: z.string().optional(), - id: z.string().optional(), - type: z.string().optional(), - status: z.string().optional(), - rewardId: z.string().optional(), - redeemLocation: z.string().optional(), - autoApplyReward: z.boolean().default(false), - rewardType: z.string().optional(), - rewardTierLevel: z.string().optional(), - }) - .optional() + z.object({ + title: z.string().optional(), + id: z.string().optional(), + type: z.string().optional(), + status: z.string().optional(), + rewardId: z.string().optional(), + redeemLocation: z.string().optional(), + autoApplyReward: z.boolean().default(false), + rewardType: z.string().optional(), + rewardTierLevel: z.string().optional(), + }) ) ) @@ -77,6 +75,8 @@ export const validateCmsRewardsSchema = z }) .transform((data) => data.data.all_reward.items) +export type ApiReward = z.output[0] + export type CmsRewardsResponse = z.input export type Reward = z.output[0] diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts index 85470553a..8f5ff7667 100644 --- a/server/routers/contentstack/reward/query.ts +++ b/server/routers/contentstack/reward/query.ts @@ -19,6 +19,7 @@ import { rewardsAllInput, rewardsByLevelInput, rewardsCurrentInput, + rewardsUpdateInput, } from "./input" import { CmsRewardsResponse, @@ -242,10 +243,10 @@ export const rewardQueryRouter = router({ return null } - const rewardIds = validatedApiRewards.data.data + const rewardIds = validatedApiRewards.data .map((reward) => reward?.rewardId) - .filter(Boolean) - .sort() as string[] + .filter((rewardId): rewardId is string => !!rewardId) + .sort() const slicedData = rewardIds.slice(cursor, limit + cursor) @@ -259,8 +260,35 @@ export const rewardQueryRouter = router({ limit + cursor < rewardIds.length ? limit + cursor : undefined getCurrentRewardSuccessCounter.add(1) + return { rewards: cmsRewards, + apiRewards: validatedApiRewards.data + // FIXME: Remove these mocks before merging + .concat([ + { + autoApplyReward: false, + title: "Free kids drink when staying", + id: "fake-id", + type: "surprise", + status: "active", + rewardId: "tier_free_kids_drink", + redeemLocation: "On-site", + rewardType: "Tier", + rewardTierLevel: "L1", + }, + { + autoApplyReward: false, + title: "Free kanelbulle", + id: "fake-id-2", + type: "surprise", + status: "active", + rewardId: "tier_free_kanelbulle", + redeemLocation: "On-site", + rewardType: "Tier", + rewardTierLevel: "L1", + }, + ]), nextCursor, } }), @@ -374,4 +402,19 @@ export const rewardQueryRouter = router({ getAllRewardSuccessCounter.add(1) return levelsWithRewards }), + update: contentStackBaseWithProtectedProcedure + .input(rewardsUpdateInput) + .mutation(async ({ input, ctx }) => { + const response = await Promise.resolve({ ok: true }) + // const response = await api.post(api.endpoints.v1.rewards, { + // body: { + // ids: [input.id], + // }, + // }) + if (!response.ok) { + return false + } + + return true + }), }) From e5b4b6f82e243c1ab1eb27d2badf2381eec51676 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Tue, 15 Oct 2024 10:50:14 +0200 Subject: [PATCH 03/15] refactor(SW-556): project specific fixes --- .../Rewards/CurrentLevel/Client.tsx | 9 +--- .../Rewards/Surprises/index.tsx | 52 ++++++++++--------- types/components/blocks/currentRewards.ts | 9 ++++ types/components/blocks/surprises.ts | 5 ++ 4 files changed, 44 insertions(+), 31 deletions(-) create mode 100644 types/components/blocks/currentRewards.ts create mode 100644 types/components/blocks/surprises.ts diff --git a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx index bad1f6694..f0528ed2c 100644 --- a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx +++ b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx @@ -13,13 +13,8 @@ import Surprises from "../Surprises" import styles from "./current.module.css" -type CurrentRewardsClientProps = { - initialCurrentRewards: { - rewards: Reward[] - apiRewards: ApiReward[] - nextCursor: number | undefined - } -} +import type { CurrentRewardsClientProps } from "@/types/components/blocks/currentRewards" + export default function ClientCurrentRewards({ initialCurrentRewards, }: CurrentRewardsClientProps) { diff --git a/components/Blocks/DynamicContent/Rewards/Surprises/index.tsx b/components/Blocks/DynamicContent/Rewards/Surprises/index.tsx index 486b0423b..917b835f5 100644 --- a/components/Blocks/DynamicContent/Rewards/Surprises/index.tsx +++ b/components/Blocks/DynamicContent/Rewards/Surprises/index.tsx @@ -6,11 +6,11 @@ import { useIntl } from "react-intl" import { benefits } from "@/constants/routes/myPages" import { trpc } from "@/lib/trpc/client" -import { ApiReward } from "@/server/routers/contentstack/reward/output" import { ChevronRightSmallIcon, CloseLargeIcon } from "@/components/Icons" import Image from "@/components/Image" import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import CaptionLabel from "@/components/TempDesignSystem/Text/CaptionLabel" @@ -20,9 +20,7 @@ import useLang from "@/hooks/useLang" import styles from "./surprises.module.css" -interface SurprisesProps { - surprises: ApiReward[] -} +import type { SurprisesProps } from "@/types/components/blocks/surprises" export default function Surprises({ surprises }: SurprisesProps) { const lang = useLang() @@ -32,17 +30,38 @@ export default function Surprises({ surprises }: SurprisesProps) { const update = trpc.contentstack.rewards.update.useMutation() const intl = useIntl() - if (!surprises.length) return null + if (!surprises.length) { + return null + } function showSurprise(n: number) { setSelectedSurprise((surprise) => surprise + n) } function viewRewards(id?: string) { - if (!id) return + if (!id) { + return + } update.mutate({ id }) } + function closeModal(close: VoidFunction) { + viewRewards() + toast.success( + <> + {intl.formatMessage( + { id: "Gift(s) added to your benefits" }, + { amount: surprises.length } + )} +
+ + {intl.formatMessage({ id: "Go to My Benefits" })} + + + ) + close() + } + const surprise = surprises[selectedSurprise] return ( @@ -69,22 +88,7 @@ export default function Surprises({ surprises }: SurprisesProps) { )}