Files
web/apps/scandic-web/components/Blocks/DynamicContent/SAS/TransferPoints/TransferPointsFormClient.tsx
Rasmus Langvad b9a62b5280 Merged in feat/use-new-input-component (pull request #3324)
feat(SW-3659): Use new input component

* Use new input component

* Update error formatter

* Merged master into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Merge branch 'master' into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Update Input stories

* Merge branch 'feat/use-new-input-component' of bitbucket.org:scandic-swap/web into feat/use-new-input-component

* Update Storybook logo

* Add some new demo icon input story

* Fix the clear content button position

* Fix broken password input icon

* Merged master into feat/use-new-input-component

* Merged master into feat/use-new-input-component

* Add aria-hidden to required asterisk

* Merge branch 'feat/use-new-input-component' of bitbucket.org:scandic-swap/web into feat/use-new-input-component

* Merge branch 'master' into feat/use-new-input-component


Approved-by: Bianca Widstam
Approved-by: Matilda Landström
2025-12-18 15:42:09 +00:00

280 lines
8.7 KiB
TypeScript

"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 { FormInput } from "@scandic-hotels/design-system/Form/FormInput"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Image from "@scandic-hotels/design-system/Image"
import Modal from "@scandic-hotels/design-system/Modal"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { SAS_TRANSFER_POINT_KEY } from "@scandic-hotels/trpc/constants/partnerSAS"
import styles from "./transferPoints.module.css"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { LangParams } from "@/types/params"
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 ? Math.min(Math.max(points, 0), sasPoints ?? 0) : null)
}
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: "partnerSas.ebPointsToExchange",
defaultMessage: "EB points to exchange",
})}
formatOptions={{
useGrouping: true,
maximumFractionDigits: 0,
}}
isDisabled={disabled || hasNoSasPoints}
>
<SliderTrack className={styles.sliderTrack}>
<SliderFill />
<SliderThumb className={styles.sliderThumb}>
<SliderOutput className={styles.sliderOutput} />
<MaterialIcon icon="swipe" color="Icon/Inverted" />
</SliderThumb>
</SliderTrack>
</Slider>
</I18nProvider>
<div className={styles.inputsWrapper}>
<TextField type="number" isDisabled={disabled}>
<FormInput
name="ebPointsToExchange"
label={intl.formatMessage({
id: "partnerSas.ebPointsToExchange",
defaultMessage: "EB points to exchange",
})}
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: "partnerSas.conversionRateInfo",
defaultMessage: "1 EuroBonus point = 2 Scandic Friends points",
})}
</p>
</Typography>
<div className={styles.pointsOutput}>
<Typography variant="Label/xsRegular">
<p>
{intl.formatMessage({
id: "partnerSas.sfPointsToReceive",
defaultMessage: "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: "partnerSas.exchangePoints",
defaultMessage: "Exchange 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: "partnerSas.proceedWithPointExchange",
defaultMessage: "Proceed with point exchange?",
})}
</h3>
</Typography>
<div>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage({
id: "partnerSas.youAreAboutToExchange",
defaultMessage: "You are about to exchange:",
})}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage(
{
id: "partnerSas.exchangePointsDetails",
defaultMessage:
"<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: "partnerSas.exchangePointsExpiry",
defaultMessage:
"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: "partnerSas.yesIWantToExchangeMyPoints",
defaultMessage: "Yes, I want to exchange my points",
})}
</Link>
</Button>
<Button
fullWidth
intent="text"
theme="base"
onClick={() => handleToggle(false)}
>
{intl.formatMessage({
id: "common.cancel",
defaultMessage: "Cancel",
})}
</Button>
</div>
</div>
</Modal>
</>
)
}