Merge remote-tracking branch 'origin' into feature/tracking
This commit is contained in:
@@ -12,7 +12,7 @@ import SectionWrapper from "../SectionWrapper"
|
||||
|
||||
import styles from "./loyaltyLevels.module.css"
|
||||
|
||||
import { LoyaltyLevelsProps } from "@/types/components/blocks/dynamicContent"
|
||||
import type { LoyaltyLevelsProps } from "@/types/components/blocks/dynamicContent"
|
||||
import type { LevelCardProps } from "@/types/components/overviewTable"
|
||||
|
||||
export default async function LoyaltyLevels({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { VariantProps } from "class-variance-authority"
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
|
||||
import { heroVariants } from "./heroVariants"
|
||||
import type { heroVariants } from "./heroVariants"
|
||||
|
||||
export interface HeroProps
|
||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HeroProps } from "./hero"
|
||||
import { heroVariants } from "./heroVariants"
|
||||
|
||||
import type { HeroProps } from "./hero"
|
||||
|
||||
export default function Hero({ className, color, children }: HeroProps) {
|
||||
const classNames = heroVariants({ className, color })
|
||||
return <section className={classNames}>{children}</section>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getMembership } from "@/utils/user"
|
||||
import PointsContainer from "./Container"
|
||||
import { NextLevelPointsColumn, YourPointsColumn } from "./PointsColumn"
|
||||
|
||||
import { UserProps } from "@/types/components/myPages/user"
|
||||
import type { UserProps } from "@/types/components/myPages/user"
|
||||
|
||||
export default async function Points({ user }: UserProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useReducer } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import {
|
||||
MembershipLevel,
|
||||
type MembershipLevel,
|
||||
MembershipLevelEnum,
|
||||
} from "@/constants/membershipLevels"
|
||||
|
||||
@@ -22,8 +22,8 @@ import styles from "./overviewTable.module.css"
|
||||
import type { Key } from "react-aria-components"
|
||||
|
||||
import {
|
||||
ComparisonLevel,
|
||||
DesktopSelectColumns,
|
||||
type ComparisonLevel,
|
||||
type DesktopSelectColumns,
|
||||
type MobileColumnHeaderProps,
|
||||
OverviewTableActionsEnum,
|
||||
type OverviewTableClientProps,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { serverClient } from "@/lib/trpc/server"
|
||||
import SectionWrapper from "../SectionWrapper"
|
||||
import OverviewTableClient from "./Client"
|
||||
|
||||
import { OverviewTableProps } from "@/types/components/blocks/dynamicContent"
|
||||
import type { OverviewTableProps } from "@/types/components/blocks/dynamicContent"
|
||||
|
||||
export default async function OverviewTable({
|
||||
dynamic_content,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type LevelWithRewards,
|
||||
OverviewTableActionsEnum,
|
||||
type OverviewTableClientProps,
|
||||
OverviewTableReducerAction,
|
||||
type OverviewTableReducerAction,
|
||||
} from "@/types/components/overviewTable"
|
||||
|
||||
export function getLevel(
|
||||
|
||||
@@ -6,11 +6,11 @@ import { useState } from "react"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Pagination from "@/components/MyPages/Pagination"
|
||||
|
||||
import ClientTable from "./ClientTable"
|
||||
import Pagination from "./Pagination"
|
||||
|
||||
import { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
import type { Transactions } from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
export default function TransactionTable({
|
||||
initialJourneyTransactions,
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { ChevronRightIcon } from "@/components/Icons"
|
||||
|
||||
import styles from "./pagination.module.css"
|
||||
|
||||
import {
|
||||
PaginationButtonProps,
|
||||
PaginationProps,
|
||||
} from "@/types/components/myPages/myPage/earnAndBurn"
|
||||
|
||||
function PaginationButton({
|
||||
children,
|
||||
isActive,
|
||||
handleClick,
|
||||
disabled,
|
||||
}: React.PropsWithChildren<PaginationButtonProps>) {
|
||||
return (
|
||||
<button
|
||||
type={"button"}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
className={`${styles.paginationButton} ${isActive ? styles.paginationButtonActive : ""}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Pagination({
|
||||
pageCount,
|
||||
isFetching,
|
||||
handlePageChange,
|
||||
currentPage,
|
||||
}: PaginationProps) {
|
||||
const isOnFirstPage = currentPage === 1
|
||||
const isOnLastPage = currentPage === pageCount
|
||||
return (
|
||||
<div className={styles.pagination}>
|
||||
<PaginationButton
|
||||
disabled={isFetching || isOnFirstPage}
|
||||
handleClick={() => {
|
||||
handlePageChange(currentPage - 1)
|
||||
}}
|
||||
>
|
||||
<ChevronRightIcon
|
||||
className={styles.chevronLeft}
|
||||
height={20}
|
||||
width={20}
|
||||
/>
|
||||
</PaginationButton>
|
||||
{[...Array(pageCount)].map((_, idx) => (
|
||||
<PaginationButton
|
||||
isActive={currentPage === idx + 1}
|
||||
disabled={isFetching || currentPage === idx + 1}
|
||||
key={idx}
|
||||
handleClick={() => {
|
||||
handlePageChange(idx + 1)
|
||||
}}
|
||||
>
|
||||
{idx + 1}
|
||||
</PaginationButton>
|
||||
))}
|
||||
<PaginationButton
|
||||
disabled={isFetching || isOnLastPage}
|
||||
handleClick={() => {
|
||||
handlePageChange(currentPage + 1)
|
||||
}}
|
||||
>
|
||||
<ChevronRightIcon height={20} width={20} />
|
||||
</PaginationButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
padding: var(--Spacing-x2);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Rounded);
|
||||
margin: auto;
|
||||
gap: var(--Spacing-x5);
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.paginationButton {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
font-size: var(--typography-Body-Bold-fontSize);
|
||||
font-weight: var(--typography-Body-Bold-fontWeight);
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.paginationButton[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.chevronLeft {
|
||||
transform: rotate(180deg);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.paginationButtonActive {
|
||||
color: var(--WHITE);
|
||||
background-color: var(--Base-Text-Accent);
|
||||
border-radius: var(--Corner-radius-Rounded);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import SectionHeader from "@/components/Section/Header"
|
||||
|
||||
import ExpiringPointsTable from "./ExpiringPointsTable"
|
||||
|
||||
import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
|
||||
export default async function ExpiringPoints({
|
||||
link,
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
YourPointsColumn,
|
||||
} from "../../../Overview/Stats/Points/PointsColumn"
|
||||
|
||||
import { UserProps } from "@/types/components/myPages/user"
|
||||
import { LangParams } from "@/types/params"
|
||||
import type { UserProps } from "@/types/components/myPages/user"
|
||||
import type { LangParams } from "@/types/params"
|
||||
|
||||
/* TODO */
|
||||
export default async function Points({ user, lang }: UserProps & LangParams) {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import { Reward } from "@/server/routers/contentstack/reward/output"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Grids from "@/components/TempDesignSystem/Grids"
|
||||
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./current.module.css"
|
||||
|
||||
type CurrentRewardsClientProps = {
|
||||
initialCurrentRewards: { rewards: Reward[]; nextCursor: number | undefined }
|
||||
}
|
||||
export default function ClientCurrentRewards({
|
||||
initialCurrentRewards,
|
||||
}: CurrentRewardsClientProps) {
|
||||
const lang = useLang()
|
||||
const { data, isFetching, fetchNextPage, hasNextPage, isLoading } =
|
||||
trpc.contentstack.rewards.current.useInfiniteQuery(
|
||||
{
|
||||
limit: 3,
|
||||
lang,
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage?.nextCursor,
|
||||
initialData: {
|
||||
pageParams: [undefined, 1],
|
||||
pages: [initialCurrentRewards],
|
||||
},
|
||||
}
|
||||
)
|
||||
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 <LoadingSpinner />
|
||||
}
|
||||
|
||||
if (!rewards.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grids.Stackable>
|
||||
{rewards.map((reward, idx) => (
|
||||
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
|
||||
<Title
|
||||
as="h4"
|
||||
level="h3"
|
||||
textAlign="center"
|
||||
textTransform="regular"
|
||||
>
|
||||
{reward.label}
|
||||
</Title>
|
||||
</article>
|
||||
))}
|
||||
</Grids.Stackable>
|
||||
{hasNextPage &&
|
||||
(isFetching ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<ShowMoreButton loadMoreData={loadMoreData} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
.card {
|
||||
align-items: center;
|
||||
background-color: var(--UI-Opacity-White-100);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: center;
|
||||
min-height: 280px;
|
||||
padding: var(--Spacing-x7) var(--Spacing-x3);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client"
|
||||
|
||||
import { useRef, useState } from "react"
|
||||
|
||||
import { RewardIcon } from "@/components/Blocks/DynamicContent/Rewards/RewardIcon"
|
||||
import Pagination from "@/components/MyPages/Pagination"
|
||||
import Grids from "@/components/TempDesignSystem/Grids"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import Redeem from "./Redeem"
|
||||
|
||||
import styles from "./current.module.css"
|
||||
|
||||
import type { CurrentRewardsClientProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
|
||||
export default function ClientCurrentRewards({
|
||||
rewards,
|
||||
pageSize,
|
||||
showRedeem,
|
||||
}: CurrentRewardsClientProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
|
||||
const totalPages = Math.ceil(rewards.length / pageSize)
|
||||
const startIndex = (currentPage - 1) * pageSize
|
||||
const endIndex = startIndex + pageSize
|
||||
const currentRewards = rewards.slice(startIndex, endIndex)
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
requestAnimationFrame(() => {
|
||||
setCurrentPage(page)
|
||||
containerRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
inline: "nearest",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={styles.container}>
|
||||
<Grids.Stackable>
|
||||
{currentRewards.map((reward, idx) => (
|
||||
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
|
||||
<div className={styles.content}>
|
||||
<RewardIcon rewardId={reward.reward_id} />
|
||||
<Title
|
||||
as="h4"
|
||||
level="h3"
|
||||
textAlign="center"
|
||||
textTransform="regular"
|
||||
>
|
||||
{reward.label}
|
||||
</Title>
|
||||
</div>
|
||||
{showRedeem && "redeem_description" in reward && (
|
||||
<div className={styles.btnContainer}>
|
||||
<Redeem reward={reward} />
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</Grids.Stackable>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
pageCount={totalPages}
|
||||
currentPage={currentPage}
|
||||
handlePageChange={handlePageChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
"use client"
|
||||
|
||||
import { motion } from "framer-motion"
|
||||
import { useState } from "react"
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
} from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import Countdown from "@/components/Countdown"
|
||||
import { CheckCircleIcon, CloseLargeIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import { RewardIcon } from "../RewardIcon"
|
||||
|
||||
import styles from "./current.module.css"
|
||||
|
||||
import type {
|
||||
RedeemModalState,
|
||||
RedeemProps,
|
||||
RedeemStep,
|
||||
} from "@/types/components/myPages/myPage/accountPage"
|
||||
|
||||
const MotionOverlay = motion(ModalOverlay)
|
||||
const MotionModal = motion(Modal)
|
||||
|
||||
export default function Redeem({ reward }: RedeemProps) {
|
||||
const [animation, setAnimation] = useState<RedeemModalState>("unmounted")
|
||||
const intl = useIntl()
|
||||
const update = trpc.contentstack.rewards.redeem.useMutation()
|
||||
const [redeemStep, setRedeemStep] = useState<RedeemStep>("initial")
|
||||
|
||||
function onProceed() {
|
||||
if (reward.id) {
|
||||
update.mutate(
|
||||
{ rewardId: reward.id },
|
||||
{
|
||||
onSuccess() {
|
||||
setRedeemStep("redeemed")
|
||||
},
|
||||
onError(error) {
|
||||
console.error("Failed to redeem", error)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function modalStateHandler(newAnimationState: RedeemModalState) {
|
||||
setAnimation((currentAnimationState) =>
|
||||
newAnimationState === "hidden" && currentAnimationState === "hidden"
|
||||
? "unmounted"
|
||||
: currentAnimationState
|
||||
)
|
||||
if (newAnimationState === "unmounted") {
|
||||
setRedeemStep("initial")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogTrigger
|
||||
onOpenChange={(isOpen) => setAnimation(isOpen ? "visible" : "hidden")}
|
||||
>
|
||||
<Button intent="primary" fullWidth>
|
||||
{intl.formatMessage({ id: "Open" })}
|
||||
</Button>
|
||||
<MotionOverlay
|
||||
className={styles.overlay}
|
||||
isExiting={animation === "hidden"}
|
||||
onAnimationComplete={modalStateHandler}
|
||||
variants={variants.fade}
|
||||
initial="hidden"
|
||||
animate={animation}
|
||||
>
|
||||
<MotionModal
|
||||
className={styles.modal}
|
||||
variants={variants.slideInOut}
|
||||
initial="hidden"
|
||||
animate={animation}
|
||||
>
|
||||
<Dialog className={styles.dialog} aria-label={reward.label}>
|
||||
{({ close }) => (
|
||||
<>
|
||||
<header className={styles.modalHeader}>
|
||||
<button
|
||||
onClick={close}
|
||||
type="button"
|
||||
className={styles.modalClose}
|
||||
>
|
||||
<CloseLargeIcon />
|
||||
</button>
|
||||
</header>
|
||||
<div className={styles.modalContent}>
|
||||
{redeemStep === "redeemed" && (
|
||||
<div className={styles.badge}>
|
||||
<div className={styles.redeemed}>
|
||||
<CheckCircleIcon color="uiSemanticSuccess" />
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "Redeemed & valid through:",
|
||||
})}
|
||||
</Caption>
|
||||
</div>
|
||||
<Countdown />
|
||||
</div>
|
||||
)}
|
||||
<RewardIcon rewardId={reward.reward_id} />
|
||||
<Title level="h3" textAlign="center" textTransform="regular">
|
||||
{reward.label}
|
||||
</Title>
|
||||
|
||||
{redeemStep === "initial" && (
|
||||
<Body textAlign="center">{reward.description}</Body>
|
||||
)}
|
||||
|
||||
{redeemStep === "confirmation" &&
|
||||
"redeem_description" in reward && (
|
||||
<Body textAlign="center">
|
||||
{reward.redeem_description}
|
||||
</Body>
|
||||
)}
|
||||
</div>
|
||||
{redeemStep === "initial" && (
|
||||
<footer className={styles.modalFooter}>
|
||||
<Button
|
||||
onClick={() => setRedeemStep("confirmation")}
|
||||
intent="primary"
|
||||
theme="base"
|
||||
>
|
||||
{intl.formatMessage({ id: "Redeem benefit" })}
|
||||
</Button>
|
||||
</footer>
|
||||
)}
|
||||
|
||||
{redeemStep === "confirmation" && (
|
||||
<footer className={styles.modalFooter}>
|
||||
<Button
|
||||
onClick={onProceed}
|
||||
disabled={update.isPending}
|
||||
intent="primary"
|
||||
theme="base"
|
||||
>
|
||||
{intl.formatMessage({ id: "Yes, redeem" })}
|
||||
</Button>
|
||||
<Button onClick={close} intent="secondary" theme="base">
|
||||
{intl.formatMessage({ id: "Go back" })}
|
||||
</Button>
|
||||
</footer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</MotionModal>
|
||||
</MotionOverlay>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
const variants = {
|
||||
fade: {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
},
|
||||
|
||||
slideInOut: {
|
||||
hidden: {
|
||||
opacity: 0,
|
||||
y: 32,
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.4, ease: "easeInOut" },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x4);
|
||||
position: relative;
|
||||
scroll-margin-top: calc(var(--current-mobile-site-header-height) * 2);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--UI-Opacity-White-100);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.btnContainer {
|
||||
padding: 0 var(--Spacing-x3) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-radius: var(--Small, 4px);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
display: flex;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.redeemed {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
height: var(--visual-viewport-height);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@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;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.modal {
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
--button-height: 32px;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--button-height);
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2) 0;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: 0 var(--Spacing-x3) var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 var(--Spacing-x3) var(--Spacing-x1);
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.modalFooter > button {
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
|
||||
.modalClose {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: var(--Spacing-x2);
|
||||
width: 32px;
|
||||
height: var(--button-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { env } from "@/env/server"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import SectionContainer from "@/components/Section/Container"
|
||||
@@ -13,19 +14,20 @@ export default async function CurrentRewardsBlock({
|
||||
subtitle,
|
||||
link,
|
||||
}: AccountPageComponentProps) {
|
||||
const initialCurrentRewards =
|
||||
await serverClient().contentstack.rewards.current({
|
||||
limit: 3,
|
||||
})
|
||||
const rewardsResponse = await serverClient().contentstack.rewards.current()
|
||||
|
||||
if (!initialCurrentRewards) {
|
||||
if (!rewardsResponse?.rewards.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<SectionHeader title={title} link={link} preamble={subtitle} />
|
||||
<ClientCurrentRewards initialCurrentRewards={initialCurrentRewards} />
|
||||
<ClientCurrentRewards
|
||||
rewards={rewardsResponse.rewards}
|
||||
pageSize={6}
|
||||
showRedeem={env.USE_NEW_REWARDS_ENDPOINT && env.USE_NEW_REWARD_MODEL}
|
||||
/>
|
||||
<SectionLink link={link} variant="mobile" />
|
||||
</SectionContainer>
|
||||
)
|
||||
68
components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts
Normal file
68
components/Blocks/DynamicContent/Rewards/RewardIcon/data.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name"
|
||||
import { isValidRewardId } from "@/utils/rewards"
|
||||
|
||||
import type { FC } from "react"
|
||||
|
||||
import { IconName, type IconProps } from "@/types/components/icon"
|
||||
import { RewardId } from "@/types/enums/rewards"
|
||||
|
||||
function getIconForRewardId(rewardId: RewardId): IconName {
|
||||
switch (rewardId) {
|
||||
// Food & beverage
|
||||
case RewardId.TenPercentFood:
|
||||
case RewardId.FifteenPercentFood:
|
||||
return IconName.CroissantCoffeeEgg
|
||||
case RewardId.TwoForOneBreakfast:
|
||||
return IconName.CutleryTwo
|
||||
case RewardId.FreeBreakfast:
|
||||
return IconName.CutleryOne
|
||||
case RewardId.FreeKidsDrink:
|
||||
return IconName.KidsMocktail
|
||||
|
||||
// Monetary vouchers
|
||||
case RewardId.Bonus50SEK:
|
||||
case RewardId.Bonus75SEK:
|
||||
case RewardId.Bonus100SEK:
|
||||
case RewardId.Bonus150SEK:
|
||||
case RewardId.Bonus200SEK:
|
||||
return IconName.Voucher
|
||||
|
||||
// Hotel perks
|
||||
case RewardId.EarlyCheckin:
|
||||
return IconName.HandKey
|
||||
case RewardId.LateCheckout:
|
||||
return IconName.HotelNight
|
||||
case RewardId.FreeUpgrade:
|
||||
return IconName.MagicWand
|
||||
case RewardId.RoomGuarantee48H:
|
||||
return IconName.Bed
|
||||
|
||||
// Earnings
|
||||
case RewardId.EarnRate25Percent:
|
||||
case RewardId.EarnRate50Percent:
|
||||
return IconName.MoneyHand
|
||||
case RewardId.StayBoostForKids:
|
||||
return IconName.Kids
|
||||
case RewardId.MemberRate:
|
||||
return IconName.Coin
|
||||
|
||||
// Special
|
||||
case RewardId.YearlyExclusiveGift:
|
||||
return IconName.GiftOpen
|
||||
|
||||
default: {
|
||||
const unhandledRewardId: never = rewardId
|
||||
return IconName.GiftOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function mapRewardToIcon(rewardId: string): FC<IconProps> | null {
|
||||
if (!isValidRewardId(rewardId)) {
|
||||
// TODO: Update once UX has decided on fallback icon.
|
||||
return getIconByIconName(IconName.GiftOpen)
|
||||
}
|
||||
|
||||
const iconName = getIconForRewardId(rewardId)
|
||||
return getIconByIconName(iconName)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { mapRewardToIcon } from "./data"
|
||||
|
||||
import type { RewardIconProps } from "@/types/components/myPages/rewards"
|
||||
|
||||
// Original SVG aspect ratio is 358:202 (≈1.77:1)
|
||||
const sizeMap = {
|
||||
small: { width: 120, height: 68 }, // 40% of card width
|
||||
medium: { width: 180, height: 102 }, // 60% of card width
|
||||
large: { width: 240, height: 135 }, // 80% of card width
|
||||
} as const
|
||||
|
||||
export function RewardIcon({
|
||||
rewardId,
|
||||
size = "medium",
|
||||
...props
|
||||
}: RewardIconProps) {
|
||||
const IconComponent = mapRewardToIcon(rewardId)
|
||||
if (!IconComponent) return null
|
||||
|
||||
return (
|
||||
<IconComponent
|
||||
{...props}
|
||||
width={sizeMap[size].width}
|
||||
height={sizeMap[size].height}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||
import SignupForm from "@/components/Forms/Signup"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent"
|
||||
import type { SignupFormWrapperProps } from "@/types/components/blocks/dynamicContent"
|
||||
|
||||
export default async function SignupFormWrapper({
|
||||
dynamic_content,
|
||||
|
||||
@@ -8,7 +8,7 @@ import Grids from "@/components/TempDesignSystem/Grids"
|
||||
import StayCard from "../StayCard"
|
||||
import EmptyUpcomingStaysBlock from "./EmptyUpcomingStays"
|
||||
|
||||
import { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
import type { AccountPageComponentProps } from "@/types/components/myPages/myPage/accountPage"
|
||||
|
||||
export default async function SoonestStays({
|
||||
title,
|
||||
|
||||
@@ -9,7 +9,7 @@ import OverviewTable from "@/components/Blocks/DynamicContent/OverviewTable"
|
||||
import EarnAndBurn from "@/components/Blocks/DynamicContent/Points/EarnAndBurn"
|
||||
import ExpiringPoints from "@/components/Blocks/DynamicContent/Points/ExpiringPoints"
|
||||
import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
||||
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentLevel"
|
||||
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentRewards"
|
||||
import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel"
|
||||
import SignupFormWrapper from "@/components/Blocks/DynamicContent/SignupFormWrapper"
|
||||
import SignUpVerification from "@/components/Blocks/DynamicContent/SignUpVerification"
|
||||
|
||||
Reference in New Issue
Block a user