Merged in feat/sw-1314-transfer-sas-points (pull request #1508)
SW-1314 Transfer SAS points Approved-by: Linus Flood
This commit is contained in:
+165
@@ -0,0 +1,165 @@
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import ArrowFromIcon from "@/components/Icons/ArrowFrom"
|
||||
import ArrowToIcon from "@/components/Icons/ArrowTo"
|
||||
import Image from "@/components/Image"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { TransferPointsFormClient } from "./TransferPointsFormClient"
|
||||
|
||||
import styles from "./transferPoints.module.css"
|
||||
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
export async function TransferPointsForm({ lang }: { lang: Lang }) {
|
||||
const profile = await getProfileSafely()
|
||||
const scandicPoints = profile?.membership?.currentPoints ?? 0
|
||||
|
||||
// TODO get from api
|
||||
await wait(1_000)
|
||||
const sasPoints = 250_000
|
||||
const exchangeRate = 2
|
||||
|
||||
return (
|
||||
<TransferPointsFormContent
|
||||
sasPoints={sasPoints}
|
||||
scandicPoints={scandicPoints}
|
||||
exchangeRate={exchangeRate}
|
||||
lang={lang}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export async function TransferPointsFormSkeleton({ lang }: { lang: Lang }) {
|
||||
return (
|
||||
<TransferPointsFormContent
|
||||
sasPoints={null}
|
||||
scandicPoints={null}
|
||||
exchangeRate={null}
|
||||
lang={lang}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
async function TransferPointsFormContent({
|
||||
sasPoints,
|
||||
scandicPoints,
|
||||
exchangeRate,
|
||||
lang,
|
||||
}: {
|
||||
sasPoints: number | null
|
||||
scandicPoints: number | null
|
||||
exchangeRate: number | null
|
||||
lang: Lang
|
||||
}) {
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<section className={styles.card}>
|
||||
<div className={styles.highFive}>
|
||||
<Image
|
||||
src="/_static/img/scandic-high-five.svg"
|
||||
alt=""
|
||||
width="111"
|
||||
height="139"
|
||||
sizes="100vw"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.transferContainer}>
|
||||
<div className={styles.transferFrom}>
|
||||
<div>
|
||||
<div className={styles.labelWithIcon}>
|
||||
<Typography variant="Tag/sm">
|
||||
<p>{intl.formatMessage({ id: "Transfer from" })}</p>
|
||||
</Typography>
|
||||
<ArrowFromIcon />
|
||||
</div>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<p>{intl.formatMessage({ id: "SAS EuroBonus" })}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="Tag/sm">
|
||||
<p className={styles.balanceLabel}>
|
||||
{intl.formatMessage({ id: "Balance" })}
|
||||
</p>
|
||||
</Typography>
|
||||
{sasPoints === null ? (
|
||||
<SkeletonShimmer width="10ch" height="20px" />
|
||||
) : (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
{ id: "{points, number} p" },
|
||||
{ points: sasPoints }
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.noPointsWarning}>
|
||||
{sasPoints === 0 && (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "You have no points to transfer.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.transferTo}>
|
||||
<div>
|
||||
<div className={styles.labelWithIcon}>
|
||||
<ArrowToIcon />
|
||||
<Typography variant="Tag/sm">
|
||||
<p>{intl.formatMessage({ id: "Transfer to" })}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<p>{intl.formatMessage({ id: "Scandic Friends" })}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="Tag/sm">
|
||||
<p className={styles.balanceLabel}>
|
||||
{intl.formatMessage({ id: "Balance" })}
|
||||
</p>
|
||||
</Typography>
|
||||
{scandicPoints === null ? (
|
||||
<SkeletonShimmer width="10ch" height="20px" />
|
||||
) : (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
{ id: "{points, number} p" },
|
||||
{ points: scandicPoints }
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TransferPointsFormClient
|
||||
sasPoints={sasPoints}
|
||||
exchangeRate={exchangeRate}
|
||||
lang={lang}
|
||||
/>
|
||||
</section>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p style={{ color: "var(--Text-Tertiary)" }}>
|
||||
{intl.formatMessage({
|
||||
id: "Transferred points will not be level qualifying",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useParams } from "next/navigation"
|
||||
import { useContext, useState } from "react"
|
||||
import {
|
||||
I18nProvider,
|
||||
Slider,
|
||||
SliderOutput,
|
||||
SliderStateContext,
|
||||
SliderThumb,
|
||||
SliderTrack,
|
||||
TextField,
|
||||
} from "react-aria-components"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { SAS_TRANSFER_POINT_KEY } from "@/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/sasUtils"
|
||||
import SwipeIcon from "@/components/Icons/Swipe"
|
||||
import Image from "@/components/Image"
|
||||
import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel"
|
||||
|
||||
import styles from "./transferPoints.module.css"
|
||||
|
||||
import type { LangParams } from "@/types/params"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
type TransferPointsFormClientProps = {
|
||||
sasPoints: number | null
|
||||
exchangeRate: number | null
|
||||
lang: Lang
|
||||
}
|
||||
|
||||
export function TransferPointsFormClient({
|
||||
sasPoints,
|
||||
exchangeRate,
|
||||
lang,
|
||||
}: TransferPointsFormClientProps) {
|
||||
const intl = useIntl()
|
||||
const formMethods = useForm()
|
||||
const [pointState, setPointState] = useState<number | null>(0)
|
||||
const selectedPoints = pointState ?? 0
|
||||
|
||||
const disabled = !exchangeRate
|
||||
|
||||
const parsedPoints = Math.min(selectedPoints, sasPoints ?? 0)
|
||||
|
||||
const calculatedPoints = parsedPoints * (exchangeRate ?? 0)
|
||||
|
||||
const handleUpdatePoints = (points: number | null) => {
|
||||
setPointState(points)
|
||||
}
|
||||
|
||||
const hasNoSasPoints = !sasPoints || sasPoints === 0
|
||||
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<I18nProvider locale={lang}>
|
||||
<Slider
|
||||
value={parsedPoints}
|
||||
onChange={handleUpdatePoints}
|
||||
className={styles.slider}
|
||||
// Set max value to 1 if sasPoints is 0 since slider requires a range
|
||||
maxValue={hasNoSasPoints ? 1 : sasPoints}
|
||||
aria-label={intl.formatMessage({ id: "EB points to transfer" })}
|
||||
formatOptions={{
|
||||
useGrouping: true,
|
||||
maximumFractionDigits: 0,
|
||||
}}
|
||||
isDisabled={disabled || hasNoSasPoints}
|
||||
>
|
||||
<SliderTrack className={styles.sliderTrack}>
|
||||
<SliderFill />
|
||||
<SliderThumb className={styles.sliderThumb}>
|
||||
<SliderOutput className={styles.sliderOutput} />
|
||||
<SwipeIcon color="white" />
|
||||
</SliderThumb>
|
||||
</SliderTrack>
|
||||
</Slider>
|
||||
</I18nProvider>
|
||||
<div className={styles.inputsWrapper}>
|
||||
<TextField type="number" isDisabled={disabled}>
|
||||
<AriaInputWithLabel
|
||||
label={intl.formatMessage({ id: "EB points to transfer" })}
|
||||
type="number"
|
||||
min={0}
|
||||
value={pointState ?? ""}
|
||||
className={styles.pointsInput}
|
||||
disabled={disabled}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10)
|
||||
|
||||
handleUpdatePoints(isNaN(value) ? null : value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
handleUpdatePoints(parsedPoints)
|
||||
}}
|
||||
/>
|
||||
</TextField>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.conversionRate}>
|
||||
{/* TODO maybe dynamic string based on exchange rate */}
|
||||
{intl.formatMessage({
|
||||
id: "1 EuroBonus point = 2 Scandic Friends points",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<div className={styles.pointsOutput}>
|
||||
<Typography variant="Label/xsRegular">
|
||||
<p>{intl.formatMessage({ id: "SF points to receive" })}</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>{intl.formatNumber(calculatedPoints)}</p>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
disabled={disabled || calculatedPoints === 0}
|
||||
sasPoints={parsedPoints}
|
||||
scandicPoints={calculatedPoints}
|
||||
/>
|
||||
</FormProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function SliderFill() {
|
||||
const state = useContext(SliderStateContext)!
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: `${state.getThumbPercent(0) * 100}%`,
|
||||
}}
|
||||
className={styles.sliderFill}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type ConfirmModalProps = {
|
||||
sasPoints: number
|
||||
scandicPoints: number
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function ConfirmModal({
|
||||
sasPoints,
|
||||
scandicPoints,
|
||||
disabled,
|
||||
}: ConfirmModalProps) {
|
||||
const { lang } = useParams<LangParams>()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const intl = useIntl()
|
||||
|
||||
const handleToggle = (open: boolean) => {
|
||||
setIsOpen(open)
|
||||
if (open) {
|
||||
const expireIn15Minutes = new Date(
|
||||
Date.now() + 15 * 60 * 1000
|
||||
).toUTCString()
|
||||
document.cookie = `${SAS_TRANSFER_POINT_KEY}=${JSON.stringify(sasPoints)};path=/;expires=${expireIn15Minutes}`
|
||||
} else {
|
||||
document.cookie = `${SAS_TRANSFER_POINT_KEY}=;path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className={styles.transferButton}
|
||||
onClick={() => handleToggle(true)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{intl.formatMessage({ id: "Transfer points" })}
|
||||
</Button>
|
||||
<Modal isOpen={isOpen} onToggle={handleToggle}>
|
||||
<div className={styles.modalContainer}>
|
||||
<Image
|
||||
src="/_static/img/scandic-money-hand.svg"
|
||||
alt=""
|
||||
width="133"
|
||||
height="119"
|
||||
/>
|
||||
<Typography variant="Title/Subtitle/lg">
|
||||
<h3>
|
||||
{intl.formatMessage({ id: "Proceed with point transfer?" })}
|
||||
</h3>
|
||||
</Typography>
|
||||
<div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>{intl.formatMessage({ id: "You are about to exchange:" })}</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "<bold>{sasPoints, number} EuroBonus points</bold> to <bold>{scandicPoints, number} Scandic Friends points</bold>",
|
||||
},
|
||||
{
|
||||
sasPoints,
|
||||
scandicPoints,
|
||||
bold: (text) => (
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<span>{text}</span>
|
||||
</Typography>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.expiryText}>
|
||||
{intl.formatMessage({
|
||||
id: "Your exchanged points will retain their original expiry date with a maximum validity of 12 months.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<div className={styles.divider} />
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button asChild theme="base" fullWidth>
|
||||
<Link
|
||||
href={`/${lang}/sas-x-scandic/login?intent=transfer`}
|
||||
color="none"
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "Yes, I want to transfer my points",
|
||||
})}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
intent="text"
|
||||
theme="base"
|
||||
onClick={() => handleToggle(false)}
|
||||
>
|
||||
{intl.formatMessage({ id: "Cancel" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import SectionContainer from "@/components/Section/Container"
|
||||
import SectionHeader from "@/components/Section/Header"
|
||||
import SectionLink from "@/components/Section/Link"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import {
|
||||
TransferPointsForm,
|
||||
TransferPointsFormSkeleton,
|
||||
} from "./TransferPointsForm"
|
||||
|
||||
type Props = {
|
||||
title?: string
|
||||
link?: { href: string; text: string }
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
export default async function SASTransferPoints({
|
||||
title,
|
||||
subtitle,
|
||||
link,
|
||||
}: Props) {
|
||||
if (!env.SAS_ENABLED) {
|
||||
return null
|
||||
}
|
||||
|
||||
const lang = getLang()
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<SectionHeader link={link} preamble={subtitle} title={title} />
|
||||
<SectionLink link={link} variant="mobile" />
|
||||
<Suspense fallback={<TransferPointsFormSkeleton lang={lang} />}>
|
||||
<TransferPointsForm lang={lang} />
|
||||
</Suspense>
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
padding: var(--Spacing-x3) var(--Spacing-x9);
|
||||
background-color: var(--Background-Secondary);
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.1);
|
||||
margin-top: var(--Spacing-x9);
|
||||
}
|
||||
|
||||
.highFive {
|
||||
height: 110px;
|
||||
width: 110px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
& > img {
|
||||
position: absolute;
|
||||
bottom: -5%;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.labelWithIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-half);
|
||||
color: var(--Text-Tertiary);
|
||||
margin-bottom: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.transferContainer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
}
|
||||
|
||||
.transferFrom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.transferTo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: right;
|
||||
|
||||
.labelWithIcon {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.noPointsWarning {
|
||||
align-self: end;
|
||||
color: var(--Surface-Feedback-Information-Accent);
|
||||
}
|
||||
|
||||
.balanceLabel {
|
||||
color: var(--Text-Tertiary);
|
||||
margin-top: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.formWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
width: 100%;
|
||||
padding: var(--Spacing-x4);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
color: var(--text-color);
|
||||
width: 100%;
|
||||
padding-top: 24px;
|
||||
|
||||
.sliderTrack {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
|
||||
&:before {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
background-color: var(--Border-Divider-Accent);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
opacity: 0.3;
|
||||
transition: background-color 300ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
.sliderFill {
|
||||
height: 10px;
|
||||
background: linear-gradient(90deg, #8f4350 25.5%, #4d001b 100%);
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
}
|
||||
|
||||
.sliderOutput {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(4px);
|
||||
background-color: var(--Surface-Brand-Primary-1-OnSurface-Default);
|
||||
color: var(--Text-Brand-OnPrimary-2-Accent);
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.15s,
|
||||
transform 0.15s;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
.sliderThumb {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--Surface-Brand-Primary-1-OnSurface-Default);
|
||||
box-shadow: 0px 0px 14px 6px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: background-color 300ms ease;
|
||||
|
||||
& > svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
|
||||
&[data-focus-visible] {
|
||||
outline: 2px solid var(--Surface-Feedback-Neutral);
|
||||
}
|
||||
|
||||
&[data-dragging] .sliderOutput {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-disabled] {
|
||||
.sliderTrack:before {
|
||||
background-color: var(--Border-Interactive-Disabled);
|
||||
}
|
||||
.sliderThumb {
|
||||
background-color: var(--Surface-UI-Fill-Disabled);
|
||||
box-shadow: none;
|
||||
|
||||
& > svg {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pointsInput {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.conversionRate {
|
||||
color: var(--Text-Tertiary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.inputsWrapper {
|
||||
display: grid;
|
||||
gap: 36px;
|
||||
width: 100%;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.transferButton {
|
||||
max-width: 260px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.modalContainer {
|
||||
max-width: 512px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: var(--Spacing-x3);
|
||||
padding-inline: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.expiryText {
|
||||
color: var(--Text-Tertiary);
|
||||
}
|
||||
|
||||
.divider {
|
||||
background-color: var(--Border-Divider-Subtle);
|
||||
width: calc(100% + var(--Spacing-x6) + var(--Spacing-x6));
|
||||
height: 1px;
|
||||
margin-inline: calc(var(--Spacing-x6) * -1);
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.pointsOutput {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
background-color: var(--Surface-Primary-Disabled);
|
||||
height: 100%;
|
||||
color: var(--Text-Tertiary);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.card {
|
||||
padding: var(--Spacing-x3);
|
||||
padding-top: var(--Spacing-x6);
|
||||
}
|
||||
|
||||
.highFive {
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.transferContainer {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--Spacing-x4);
|
||||
}
|
||||
.transferFrom {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.transferTo {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
text-align: left;
|
||||
|
||||
.labelWithIcon {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.noPointsWarning {
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.balanceLabel {
|
||||
margin-top: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.inputsWrapper {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.conversionRate {
|
||||
grid-row: 2;
|
||||
grid-column: span 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.slider {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.modalContainer {
|
||||
padding-inline: var(--Spacing-x2);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import PointsOverview from "@/components/Blocks/DynamicContent/Points/Overview"
|
||||
import CurrentRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/CurrentRewards"
|
||||
import NextLevelRewardsBlock from "@/components/Blocks/DynamicContent/Rewards/NextLevel"
|
||||
import SASLinkedAccount from "@/components/Blocks/DynamicContent/SAS/LinkedAccounts"
|
||||
import SASTransferPoints from "@/components/Blocks/DynamicContent/SAS/TransferPoints"
|
||||
import SASTierComparisonBlock from "@/components/Blocks/DynamicContent/SASTierComparison"
|
||||
import SignupFormWrapper from "@/components/Blocks/DynamicContent/SignupFormWrapper"
|
||||
import PreviousStays from "@/components/Blocks/DynamicContent/Stays/Previous"
|
||||
@@ -67,6 +68,8 @@ function DynamicContentBlocks(props: DynamicContentProps) {
|
||||
return <UpcomingStays {...dynamic_content} />
|
||||
case DynamicContentEnum.Blocks.components.sas_linked_account:
|
||||
return <SASLinkedAccount {...dynamic_content} />
|
||||
case DynamicContentEnum.Blocks.components.sas_transfer_points:
|
||||
return <SASTransferPoints {...dynamic_content} />
|
||||
case DynamicContentEnum.Blocks.components.sas_tier_comparison:
|
||||
return (
|
||||
<SASTierComparisonBlock
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function ArrowFromIcon({
|
||||
className,
|
||||
color,
|
||||
...props
|
||||
}: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M12 5V15M12 5C11.2998 5 9.99153 6.9943 9.5 7.5M12 5C12.7002 5 14.0085 6.9943 14.5 7.5"
|
||||
stroke="black"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5 19H19.0001"
|
||||
stroke="black"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function ArrowToIcon({ className, color, ...props }: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M5 5H19.0001"
|
||||
stroke="black"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 9V19M12 9C11.2998 9 9.99153 10.9943 9.5 11.5M12 9C12.7002 9 14.0085 10.9943 14.5 11.5"
|
||||
stroke="black"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { iconVariants } from "./variants"
|
||||
|
||||
import type { IconProps } from "@/types/components/icon"
|
||||
|
||||
export default function SwipeIcon({ className, color, ...props }: IconProps) {
|
||||
const classNames = iconVariants({ className, color })
|
||||
return (
|
||||
<svg
|
||||
className={classNames}
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<mask
|
||||
id="mask0_4743_35236"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<rect width="24" height="24" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_4743_35236)">
|
||||
<path
|
||||
d="M11.825 22C11.425 22 11.0417 21.925 10.675 21.775C10.3083 21.625 9.98333 21.4083 9.7 21.125L4.6 16L5.35 15.225C5.61667 14.9583 5.92917 14.7792 6.2875 14.6875C6.64583 14.5958 7 14.6 7.35 14.7L9 15.175V7C9 6.71667 9.09583 6.47917 9.2875 6.2875C9.47917 6.09583 9.71667 6 10 6C10.2833 6 10.5208 6.09583 10.7125 6.2875C10.9042 6.47917 11 6.71667 11 7V17.825L8.575 17.15L11.125 19.7C11.2083 19.7833 11.3125 19.8542 11.4375 19.9125C11.5625 19.9708 11.6917 20 11.825 20H16C16.55 20 17.0208 19.8042 17.4125 19.4125C17.8042 19.0208 18 18.55 18 18V14C18 13.7167 18.0958 13.4792 18.2875 13.2875C18.4792 13.0958 18.7167 13 19 13C19.2833 13 19.5208 13.0958 19.7125 13.2875C19.9042 13.4792 20 13.7167 20 14V18C20 19.1 19.6083 20.0417 18.825 20.825C18.0417 21.6083 17.1 22 16 22H11.825ZM12 15V11C12 10.7167 12.0958 10.4792 12.2875 10.2875C12.4792 10.0958 12.7167 10 13 10C13.2833 10 13.5208 10.0958 13.7125 10.2875C13.9042 10.4792 14 10.7167 14 11V15H12ZM15 15V12C15 11.7167 15.0958 11.4792 15.2875 11.2875C15.4792 11.0958 15.7167 11 16 11C16.2833 11 16.5208 11.0958 16.7125 11.2875C16.9042 11.4792 17 11.7167 17 12V15H15ZM22 7H17V5.5H19.9C18.8 4.53333 17.575 3.79167 16.225 3.275C14.875 2.75833 13.4667 2.5 12 2.5C10.5333 2.5 9.125 2.75833 7.775 3.275C6.425 3.79167 5.2 4.53333 4.1 5.5H7V7H2V2H3.5V4.025C4.7 3.04167 6.025 2.29167 7.475 1.775C8.925 1.25833 10.4333 1 12 1C13.5667 1 15.075 1.25833 16.525 1.775C17.975 2.29167 19.3 3.04167 20.5 4.025V2H22V7Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
AccountCircleIcon,
|
||||
AirIcon,
|
||||
AirplaneIcon,
|
||||
ArrowFromIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowToIcon,
|
||||
BarIcon,
|
||||
BedIcon,
|
||||
BikingIcon,
|
||||
@@ -106,6 +108,7 @@ import {
|
||||
StarFilledIcon,
|
||||
StreetIcon,
|
||||
SwimIcon,
|
||||
SwipeIcon,
|
||||
ThermostatIcon,
|
||||
TrainIcon,
|
||||
TripAdvisorIcon,
|
||||
@@ -135,8 +138,12 @@ export function getIconByIconName(
|
||||
return AirIcon
|
||||
case IconName.Airplane:
|
||||
return AirplaneIcon
|
||||
case IconName.ArrowFrom:
|
||||
return ArrowFromIcon
|
||||
case IconName.ArrowRight:
|
||||
return ArrowRightIcon
|
||||
case IconName.ArrowTo:
|
||||
return ArrowToIcon
|
||||
case IconName.Bar:
|
||||
return BarIcon
|
||||
case IconName.Bed:
|
||||
@@ -339,6 +346,8 @@ export function getIconByIconName(
|
||||
return StreetIcon
|
||||
case IconName.Swim:
|
||||
return SwimIcon
|
||||
case IconName.Swipe:
|
||||
return SwipeIcon
|
||||
case IconName.Thermostat:
|
||||
return ThermostatIcon
|
||||
case IconName.Tshirt:
|
||||
|
||||
@@ -5,7 +5,9 @@ export { default as AccountCircleIcon } from "./AccountCircle"
|
||||
export { default as AirIcon } from "./Air"
|
||||
export { default as AirplaneIcon } from "./Airplane"
|
||||
export { default as AllergyIcon } from "./Allergy"
|
||||
export { default as ArrowFromIcon } from "./ArrowFrom"
|
||||
export { default as ArrowRightIcon } from "./ArrowRight"
|
||||
export { default as ArrowToIcon } from "./ArrowTo"
|
||||
export { default as ArrowUpIcon } from "./ArrowUp"
|
||||
export { default as BalconyIcon } from "./Balcony"
|
||||
export { default as BarIcon } from "./Bar"
|
||||
@@ -170,6 +172,7 @@ export { default as StoreIcon } from "./Store"
|
||||
export { default as StreetIcon } from "./Street"
|
||||
export { default as SurpriseIcon } from "./Surprise"
|
||||
export { default as SwimIcon } from "./Swim"
|
||||
export { default as SwipeIcon } from "./Swipe"
|
||||
export { default as ThermostatIcon } from "./Thermostat"
|
||||
export { default as TrainIcon } from "./Train"
|
||||
export { default as TripAdvisorIcon } from "./TripAdvisor"
|
||||
|
||||
@@ -19,7 +19,7 @@ export type ModalProps = {
|
||||
| {
|
||||
trigger?: never
|
||||
isOpen: boolean
|
||||
onToggle: Dispatch<SetStateAction<boolean>>
|
||||
onToggle: (open: boolean) => void
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+7
-1
@@ -1,3 +1,4 @@
|
||||
import { cx } from "class-variance-authority"
|
||||
import { type ForwardedRef, forwardRef, useId } from "react"
|
||||
import { Input as AriaInput, Label as AriaLabel } from "react-aria-components"
|
||||
|
||||
@@ -18,7 +19,12 @@ const AriaInputWithLabel = forwardRef(function AriaInputWithLabelComponent(
|
||||
return (
|
||||
<AriaLabel className={styles.container} htmlFor={inputId}>
|
||||
<Body asChild fontOnly>
|
||||
<AriaInput {...props} className={styles.input} ref={ref} id={inputId} />
|
||||
<AriaInput
|
||||
{...props}
|
||||
className={cx(styles.input, props.className)}
|
||||
ref={ref}
|
||||
id={inputId}
|
||||
/>
|
||||
</Body>
|
||||
<Label required={!!props.required}>{label}</Label>
|
||||
</AriaLabel>
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function Input({
|
||||
evt.currentTarget.blur()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Controller
|
||||
disabled={disabled}
|
||||
|
||||
Reference in New Issue
Block a user