Merged in feat/SW-245-delete-card (pull request #407)
Feat/SW-245 delete card Approved-by: Michael Zetterberg Approved-by: Christel Westerberg
This commit is contained in:
@@ -14,17 +14,6 @@
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
column-gap: var(--Spacing-x1);
|
||||
grid-template-columns: auto auto auto 1fr;
|
||||
justify-items: flex-end;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half,);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
gap: var(--Spacing-x3);
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { env } from "@/env/server"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { CreditCard, Delete } from "@/components/Icons"
|
||||
import AddCreditCardButton from "@/components/Profile/AddCreditCardButton"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import CreditCardList from "@/components/Profile/CreditCardList"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
@@ -33,38 +31,8 @@ export default async function CreditCardSlot({ params }: PageArgs<LangParams>) {
|
||||
})}
|
||||
</Body>
|
||||
</article>
|
||||
{creditCards?.length ? (
|
||||
<div className={styles.cardContainer}>
|
||||
{creditCards.map((card, idx) => (
|
||||
<CreditCardRow
|
||||
key={idx}
|
||||
cardType={card.attribute.cardType}
|
||||
truncatedNumber={card.attribute.truncatedNumber}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<CreditCardList initialData={creditCards} />
|
||||
<AddCreditCardButton />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function CreditCardRow({
|
||||
truncatedNumber,
|
||||
cardType,
|
||||
}: {
|
||||
truncatedNumber: string
|
||||
cardType: string
|
||||
}) {
|
||||
const maskedCardNumber = `**** ${truncatedNumber.slice(12, 16)}`
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<CreditCard color="black" />
|
||||
<Body textTransform="bold">{cardType}</Body>
|
||||
<Caption color="textMediumContrast">{maskedCardNumber}</Caption>
|
||||
<Button variant="icon" theme="base" intent="text">
|
||||
<Delete color="burgundy" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,46 +2,46 @@ import { NextRequest } from "next/server"
|
||||
import { env } from "process"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { profile } from "@/constants/routes/myPages"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
import { badRequest, internalServerError } from "@/server/errors/next"
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { lang: string } }
|
||||
) {
|
||||
try {
|
||||
const lang = params.lang as Lang
|
||||
const lang = params.lang as Lang
|
||||
const returnUrl = new URL(`${env.PUBLIC_URL}/${profile[lang ?? Lang.en]}`)
|
||||
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const success = searchParams.get("success")
|
||||
const failure = searchParams.get("failure")
|
||||
const cancel = searchParams.get("cancel")
|
||||
const trxId = searchParams.get("datatransTrxId")
|
||||
|
||||
const returnUrl = new URL(
|
||||
`${env.PUBLIC_URL}/${lang ?? Lang.en}/scandic-friends/my-pages/profile`
|
||||
)
|
||||
|
||||
if (success) {
|
||||
if (!trxId) {
|
||||
return badRequest("Missing datatransTrxId param")
|
||||
}
|
||||
if (trxId) {
|
||||
const saveCardSuccess = await serverClient().user.creditCard.save({
|
||||
transactionId: trxId,
|
||||
})
|
||||
|
||||
const saveCardSuccess = await serverClient().user.saveCard({
|
||||
transactionId: trxId,
|
||||
})
|
||||
|
||||
if (saveCardSuccess) {
|
||||
returnUrl.searchParams.set("success", "true")
|
||||
if (saveCardSuccess) {
|
||||
returnUrl.searchParams.set("success", "true")
|
||||
} else {
|
||||
returnUrl.searchParams.set("failure", "true")
|
||||
}
|
||||
} else {
|
||||
returnUrl.searchParams.set("failure", "true")
|
||||
returnUrl.searchParams.set("error", "true")
|
||||
}
|
||||
} else if (failure) {
|
||||
returnUrl.searchParams.set("failure", "true")
|
||||
} else if (cancel) {
|
||||
returnUrl.searchParams.set("cancel", "true")
|
||||
}
|
||||
|
||||
return Response.redirect(returnUrl, 307)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return internalServerError()
|
||||
console.error("Error saving credit card", error)
|
||||
returnUrl.searchParams.set("error", "true")
|
||||
}
|
||||
|
||||
return Response.redirect(returnUrl, 307)
|
||||
}
|
||||
|
||||
@@ -1,47 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
import { useEffect, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { PlusCircleIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./addCreditCardButton.module.css"
|
||||
|
||||
let hasRunOnce = false
|
||||
|
||||
function useAddCardResultToast() {
|
||||
const hasRunOnce = useRef(false)
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
useEffect(() => {
|
||||
if (hasRunOnce) return
|
||||
if (hasRunOnce.current) return
|
||||
|
||||
const success = searchParams.get("success")
|
||||
const failure = searchParams.get("failure")
|
||||
const cancel = searchParams.get("cancel")
|
||||
const error = searchParams.get("error")
|
||||
|
||||
if (success) {
|
||||
// setTimeout is used to make sure DOM is loaded before triggering toast. See documentation for more info: https://sonner.emilkowal.ski/toast#render-toast-on-page-load
|
||||
setTimeout(() => {
|
||||
toast.success(
|
||||
intl.formatMessage({ id: "Your card was successfully saved!" })
|
||||
)
|
||||
})
|
||||
} else if (failure) {
|
||||
setTimeout(() => {
|
||||
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
|
||||
})
|
||||
toast.success(
|
||||
intl.formatMessage({ id: "Your card was successfully saved!" })
|
||||
)
|
||||
} else if (cancel) {
|
||||
toast.warning(
|
||||
intl.formatMessage({
|
||||
id: "You canceled adding a new credit card.",
|
||||
})
|
||||
)
|
||||
} else if (failure || error) {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong and we couldn't add your card. Please try again later.",
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
router.replace(pathname)
|
||||
hasRunOnce = true
|
||||
hasRunOnce.current = true
|
||||
}, [intl, pathname, router, searchParams])
|
||||
}
|
||||
|
||||
@@ -51,10 +57,25 @@ export default function AddCreditCardButton() {
|
||||
const lang = useLang()
|
||||
useAddCardResultToast()
|
||||
|
||||
const initiateAddCard = trpc.user.initiateSaveCard.useMutation({
|
||||
onSuccess: (result) => (result ? router.push(result.attribute.link) : null),
|
||||
onError: () =>
|
||||
toast.error(intl.formatMessage({ id: "Something went wrong!" })),
|
||||
const initiateAddCard = trpc.user.creditCard.add.useMutation({
|
||||
onSuccess: (result) => {
|
||||
if (result?.attribute.link) {
|
||||
router.push(result.attribute.link)
|
||||
} else {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "We could not add a card right now, please try again later.",
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "An error occurred when adding a credit card, please try again later.",
|
||||
})
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.cardContainer {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
31
components/Profile/CreditCardList/index.tsx
Normal file
31
components/Profile/CreditCardList/index.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import CreditCardRow from "../CreditCardRow"
|
||||
|
||||
import styles from "./CreditCardList.module.css"
|
||||
|
||||
import type { CreditCard } from "@/types/user"
|
||||
|
||||
export default function CreditCardList({
|
||||
initialData,
|
||||
}: {
|
||||
initialData?: CreditCard[] | null
|
||||
}) {
|
||||
const creditCards = trpc.user.creditCards.useQuery(undefined, { initialData })
|
||||
|
||||
if (!creditCards.data || !creditCards.data.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.cardContainer}>
|
||||
{creditCards.data.map((card) => (
|
||||
<CreditCardRow key={card.id} card={card} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
10
components/Profile/CreditCardRow/creditCardRow.module.css
Normal file
10
components/Profile/CreditCardRow/creditCardRow.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.card {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
column-gap: var(--Spacing-x1);
|
||||
grid-template-columns: auto auto auto 1fr;
|
||||
justify-items: flex-end;
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half,);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
}
|
||||
22
components/Profile/CreditCardRow/index.tsx
Normal file
22
components/Profile/CreditCardRow/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { CreditCard } from "@/components/Icons"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import DeleteCreditCardConfirmation from "../DeleteCreditCardConfirmation"
|
||||
|
||||
import styles from "./creditCardRow.module.css"
|
||||
|
||||
import type { CreditCardRowProps } from "@/types/components/myPages/myProfile/creditCards"
|
||||
|
||||
export default function CreditCardRow({ card }: CreditCardRowProps) {
|
||||
const maskedCardNumber = `**** ${card.truncatedNumber.slice(-4)}`
|
||||
|
||||
return (
|
||||
<div className={styles.card}>
|
||||
<CreditCard color="black" />
|
||||
<Body textTransform="bold">{card.type}</Body>
|
||||
<Caption color="textMediumContrast">{maskedCardNumber}</Caption>
|
||||
<DeleteCreditCardConfirmation card={card} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
40
components/Profile/DeleteCreditCardButton/index.tsx
Normal file
40
components/Profile/DeleteCreditCardButton/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { Delete } from "@/components/Icons"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
|
||||
export default function DeleteCreditCardButton({
|
||||
creditCardId,
|
||||
}: {
|
||||
creditCardId: string
|
||||
}) {
|
||||
const { formatMessage } = useIntl()
|
||||
const trpcUtils = trpc.useUtils()
|
||||
|
||||
const deleteCreditCardMutation = trpc.user.creditCard.delete.useMutation({
|
||||
onSuccess() {
|
||||
trpcUtils.user.creditCards.invalidate()
|
||||
toast.success(formatMessage({ id: "Credit card deleted successfully" }))
|
||||
},
|
||||
onError() {
|
||||
toast.error(
|
||||
formatMessage({
|
||||
id: "Failed to delete credit card, please try again later.",
|
||||
})
|
||||
)
|
||||
},
|
||||
})
|
||||
async function handleDelete() {
|
||||
deleteCreditCardMutation.mutate({ creditCardId })
|
||||
}
|
||||
return (
|
||||
<Button variant="icon" theme="base" intent="text" onClick={handleDelete}>
|
||||
<Delete color="burgundy" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: var(--visual-viewport-height);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
|
||||
&[data-entering] {
|
||||
animation: modal-fade 200ms;
|
||||
}
|
||||
|
||||
&[data-exiting] {
|
||||
animation: modal-fade 150ms reverse ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.modal section {
|
||||
background: var(--Main-Grey-White);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
padding: var(--Spacing-x4);
|
||||
padding-bottom: var(--Spacing-x6);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: var(--typography-Subtitle-1-fontFamily);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding-bottom: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.bodyText {
|
||||
text-align: center;
|
||||
max-width: 425px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--Spacing-x2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.buttonContainer button {
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@keyframes modal-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
99
components/Profile/DeleteCreditCardConfirmation/index.tsx
Normal file
99
components/Profile/DeleteCreditCardConfirmation/index.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
Heading,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
} from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import { Delete } from "@/components/Icons"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
|
||||
import styles from "./deleteCreditCardConfirmation.module.css"
|
||||
|
||||
import type { DeleteCreditCardConfirmationProps } from "@/types/components/myPages/myProfile/creditCards"
|
||||
|
||||
export default function DeleteCreditCardConfirmation({
|
||||
card,
|
||||
}: DeleteCreditCardConfirmationProps) {
|
||||
const intl = useIntl()
|
||||
const trpcUtils = trpc.useUtils()
|
||||
|
||||
const deleteCard = trpc.user.creditCard.delete.useMutation({
|
||||
onSuccess() {
|
||||
trpcUtils.user.creditCards.invalidate()
|
||||
|
||||
toast.success(
|
||||
intl.formatMessage({ id: "Your card was successfully removed!" })
|
||||
)
|
||||
},
|
||||
onError() {
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "Something went wrong and we couldn't remove your card. Please try again later.",
|
||||
})
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const lastFourDigits = card.truncatedNumber.slice(-4)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogTrigger>
|
||||
<Button variant="icon" theme="base" intent="text">
|
||||
<Delete color="burgundy" />
|
||||
</Button>
|
||||
<ModalOverlay className={styles.overlay} isDismissable>
|
||||
<Modal className={styles.modal}>
|
||||
<Dialog role="alertdialog">
|
||||
{({ close }) => (
|
||||
<div className={styles.container}>
|
||||
<Heading slot="title" className={styles.title}>
|
||||
{intl.formatMessage({
|
||||
id: "Remove card from member profile",
|
||||
})}
|
||||
</Heading>
|
||||
<p className={styles.bodyText}>
|
||||
{`${intl.formatMessage({
|
||||
id: "Are you sure you want to remove the card ending with",
|
||||
})} ${lastFourDigits} ${intl.formatMessage({ id: "from your member profile?" })}`}
|
||||
</p>
|
||||
|
||||
{deleteCard.isPending ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button intent="secondary" theme="base" onClick={close}>
|
||||
{intl.formatMessage({ id: "No, keep card" })}
|
||||
</Button>
|
||||
<Button
|
||||
intent="primary"
|
||||
theme="base"
|
||||
onClick={() => {
|
||||
deleteCard.mutate(
|
||||
{ creditCardId: card.id },
|
||||
{ onSettled: close }
|
||||
)
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage({ id: "Yes, remove my card" })}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
</DialogTrigger>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,20 @@
|
||||
import { buttonVariants } from "./variants"
|
||||
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import type { ButtonProps as ReactAriaButtonProps } from "react-aria-components"
|
||||
|
||||
export interface ButtonProps
|
||||
export interface ButtonPropsRAC
|
||||
extends Omit<ReactAriaButtonProps, "isDisabled">,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: false | undefined | never
|
||||
disabled?: ReactAriaButtonProps["isDisabled"]
|
||||
onClick?: ReactAriaButtonProps["onPress"]
|
||||
}
|
||||
|
||||
export interface ButtonPropsSlot
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
asChild: true
|
||||
}
|
||||
|
||||
export type ButtonProps = ButtonPropsSlot | ButtonPropsRAC
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { Button as ButtonRAC } from "react-aria-components"
|
||||
|
||||
import { buttonVariants } from "./variants"
|
||||
|
||||
import type { ButtonProps } from "./button"
|
||||
|
||||
export default function Button({
|
||||
asChild = false,
|
||||
theme,
|
||||
className,
|
||||
disabled,
|
||||
intent,
|
||||
size,
|
||||
variant,
|
||||
wrapping,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
export default function Button(props: ButtonProps) {
|
||||
const { className, intent, size, theme, wrapping, variant, ...restProps } =
|
||||
props
|
||||
|
||||
const classNames = buttonVariants({
|
||||
className,
|
||||
intent,
|
||||
@@ -26,5 +19,19 @@ export default function Button({
|
||||
wrapping,
|
||||
variant,
|
||||
})
|
||||
return <Comp className={classNames} disabled={disabled} {...props} />
|
||||
|
||||
if (restProps.asChild) {
|
||||
const { asChild, ...slotProps } = restProps
|
||||
return <Slot className={classNames} {...slotProps} />
|
||||
}
|
||||
|
||||
const { asChild, onClick, disabled, ...racProps } = restProps
|
||||
return (
|
||||
<ButtonRAC
|
||||
className={classNames}
|
||||
isDisabled={disabled}
|
||||
onPress={onClick}
|
||||
{...racProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { toastVariants } from "./variants"
|
||||
import styles from "./toasts.module.css"
|
||||
|
||||
export function ToastHandler() {
|
||||
return <Toaster />
|
||||
return <Toaster position="bottom-right" />
|
||||
}
|
||||
|
||||
function getIcon(variant: ToastsProps["variant"]) {
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
|
||||
.iconContainer {
|
||||
display: flex;
|
||||
background-color: var(--icon-background-color);
|
||||
padding: var(--Spacing-x2);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--icon-background-color);
|
||||
padding: var(--Spacing-x2);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"All rooms comes with standard amenities": "Alle værelser er udstyret med standardfaciliteter",
|
||||
"Already a friend?": "Allerede en ven?",
|
||||
"Amenities": "Faciliteter",
|
||||
"An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.",
|
||||
"Are you sure you want to remove the card ending with": "Er du sikker på, at du vil fjerne kortet, der slutter med",
|
||||
"Arrival date": "Ankomstdato",
|
||||
"as of today": "fra idag",
|
||||
"As our": "Som vores",
|
||||
@@ -31,6 +33,7 @@
|
||||
"Could not find requested resource": "Kunne ikke finde den anmodede ressource",
|
||||
"Country": "Land",
|
||||
"Country code": "Landekode",
|
||||
"Credit card deleted successfully": "Kreditkort blev slettet",
|
||||
"Your current level": "Dit nuværende niveau",
|
||||
"Current password": "Nuværende kodeord",
|
||||
"characters": "tegn",
|
||||
@@ -45,10 +48,12 @@
|
||||
"Extras to your booking": "Ekstra til din booking",
|
||||
"There are no transactions to display": "Der er ingen transaktioner at vise",
|
||||
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
|
||||
"Find booking": "Find booking",
|
||||
"Flexibility": "Fleksibilitet",
|
||||
"Former Scandic Hotel": "Tidligere Scandic Hotel",
|
||||
"From": "Fra",
|
||||
"from your member profile?": "fra din medlemsprofil?",
|
||||
"Get inspired": "Bliv inspireret",
|
||||
"Go back to overview": "Gå tilbage til oversigten",
|
||||
"Level 1": "Niveau 1",
|
||||
@@ -82,6 +87,7 @@
|
||||
"Next": "Næste",
|
||||
"next level:": "Næste niveau:",
|
||||
"No content published": "Intet indhold offentliggjort",
|
||||
"No, keep card": "Nej, behold kortet",
|
||||
"No transactions available": "Ingen tilgængelige transaktioner",
|
||||
"Not found": "Ikke fundet",
|
||||
"night": "nat",
|
||||
@@ -107,6 +113,7 @@
|
||||
"Previous victories": "Tidligere sejre",
|
||||
"Read more": "Læs mere",
|
||||
"Read more about the hotel": "Læs mere om hotellet",
|
||||
"Remove card from member profile": "Fjern kortet fra medlemsprofilen",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Retype new password": "Gentag den nye adgangskode",
|
||||
"Rooms": "Værelser",
|
||||
@@ -123,6 +130,8 @@
|
||||
"Skip to main content": "Spring over og gå til hovedindhold",
|
||||
"Sign up bonus": "Tilmeldingsbonus",
|
||||
"Something went wrong!": "Noget gik galt!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noget gik galt, og vi kunne ikke tilføje dit kort. Prøv venligst igen senere.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noget gik galt, og vi kunne ikke fjerne dit kort. Prøv venligst igen senere.",
|
||||
"Street": "Gade",
|
||||
"special character": "speciel karakter",
|
||||
"Total Points": "Samlet antal point",
|
||||
@@ -135,14 +144,18 @@
|
||||
"User information": "Brugeroplysninger",
|
||||
"uppercase letter": "stort bogstav",
|
||||
"Visiting address": "Besøgsadresse",
|
||||
"We could not add a card right now, please try again later.": "Vi kunne ikke tilføje et kort lige nu. Prøv venligst igen senere.",
|
||||
"Welcome": "Velkommen",
|
||||
"Welcome to": "Velkommen til",
|
||||
"Wellness & Exercise": "Velvære & Motion",
|
||||
"Where should you go next?": "Find inspiration til dit næste ophold",
|
||||
"Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig",
|
||||
"Year": "År",
|
||||
"You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.",
|
||||
"Yes, remove my card": "Ja, fjern mit kort",
|
||||
"You have no previous stays.": "Du har ingen tidligere ophold.",
|
||||
"You have no upcoming stays.": "Du har ingen kommende ophold.",
|
||||
"Your card was successfully removed!": "Dit kort blev fjernet!",
|
||||
"Your card was successfully saved!": "Dit kort blev gemt!",
|
||||
"Your Challenges Conquer & Earn!": "Dine udfordringer Overvind og tjen!",
|
||||
"Your level": "Dit niveau",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"All rooms comes with standard amenities": "Alle Zimmer sind mit den üblichen Annehmlichkeiten ausgestattet",
|
||||
"Already a friend?": "Sind wir schon Freunde?",
|
||||
"Amenities": "Annehmlichkeiten",
|
||||
"An error occurred when adding a credit card, please try again later.": "Beim Hinzufügen einer Kreditkarte ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
|
||||
"Are you sure you want to remove the card ending with": "Möchten Sie die Karte mit der Endung",
|
||||
"Arrival date": "Ankunftsdatum",
|
||||
"as of today": "Ab heute",
|
||||
"As our": "Als unser",
|
||||
@@ -30,6 +32,7 @@
|
||||
"Could not find requested resource": "Die angeforderte Ressource konnte nicht gefunden werden.",
|
||||
"Country": "Land",
|
||||
"Country code": "Landesvorwahl",
|
||||
"Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht",
|
||||
"Your current level": "Ihr aktuelles Level",
|
||||
"Current password": "Aktuelles Passwort",
|
||||
"characters": "figuren",
|
||||
@@ -44,10 +47,12 @@
|
||||
"Extras to your booking": "Extras zu Ihrer Buchung",
|
||||
"There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden",
|
||||
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
||||
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
|
||||
"Find booking": "Buchung finden",
|
||||
"Flexibility": "Flexibilität",
|
||||
"Former Scandic Hotel": "Ehemaliges Scandic Hotel",
|
||||
"From": "Fromm",
|
||||
"from your member profile?": "wirklich aus Ihrem Mitgliedsprofil entfernen?",
|
||||
"Get inspired": "Lassen Sie sich inspieren",
|
||||
"Go back to overview": "Zurück zur Übersicht",
|
||||
"Level 1": "Level 1",
|
||||
@@ -80,6 +85,7 @@
|
||||
"Next": "Nächste",
|
||||
"next level:": "Nächstes Level:",
|
||||
"No content published": "Kein Inhalt veröffentlicht",
|
||||
"No, keep card": "Nein, Karte behalten",
|
||||
"No transactions available": "Keine Transaktionen verfügbar",
|
||||
"Not found": "Nicht gefunden",
|
||||
"night": "nacht",
|
||||
@@ -104,6 +110,7 @@
|
||||
"Previous victories": "Bisherige Siege",
|
||||
"Read more": "Mehr lesen",
|
||||
"Read more about the hotel": "Lesen Sie mehr über das Hotel",
|
||||
"Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen",
|
||||
"Retype new password": "Neues Passwort erneut eingeben",
|
||||
"Save": "Speichern",
|
||||
"Scandic Friends Mastercard": "Scandic Friends Mastercard",
|
||||
@@ -118,6 +125,8 @@
|
||||
"Skip to main content": "Direkt zum Inhalt",
|
||||
"Sign up bonus": "Anmeldebonus",
|
||||
"Something went wrong!": "Etwas ist schief gelaufen!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht hinzufügen. Bitte versuchen Sie es später erneut.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Ein Fehler ist aufgetreten und wir konnten Ihre Karte nicht entfernen. Bitte versuchen Sie es später noch einmal.",
|
||||
"Street": "Straße",
|
||||
"special character": "sonderzeichen",
|
||||
"Total Points": "Gesamtpunktzahl",
|
||||
@@ -130,13 +139,17 @@
|
||||
"User information": "Nutzerinformation",
|
||||
"uppercase letter": "großbuchstabe",
|
||||
"Visiting address": "Besuchsadresse",
|
||||
"We could not add a card right now, please try again later.": "Wir konnten momentan keine Karte hinzufügen. Bitte versuchen Sie es später noch einmal.",
|
||||
"Welcome to": "Willkommen zu",
|
||||
"Welcome": "Willkommen",
|
||||
"Where should you go next?": "Wo geht es als Nächstes hin?",
|
||||
"Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?",
|
||||
"Year": "Jahr",
|
||||
"You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.",
|
||||
"Yes, remove my card": "Ja, meine Karte entfernen",
|
||||
"You have no previous stays.": "Sie haben keine vorherigen Aufenthalte.",
|
||||
"You have no upcoming stays.": "Sie haben keine bevorstehenden Aufenthalte.",
|
||||
"Your card was successfully removed!": "Ihre Karte wurde erfolgreich entfernt!",
|
||||
"Your card was successfully saved!": "Ihre Karte wurde erfolgreich gespeichert!",
|
||||
"Your Challenges Conquer & Earn!": "Meistern Sie Ihre Herausforderungen und verdienen Sie Geld!",
|
||||
"Your level": "Dein level",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"All rooms comes with standard amenities": "All rooms comes with standard amenities",
|
||||
"Already a friend?": "Already a friend?",
|
||||
"Amenities": "Amenities",
|
||||
"An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.",
|
||||
"Are you sure you want to remove the card ending with": "Are you sure you want to remove the card ending with",
|
||||
"Arrival date": "Arrival date",
|
||||
"as of today": "as of today",
|
||||
"As our": "As our",
|
||||
@@ -34,6 +36,7 @@
|
||||
"Your current level": "Your current level",
|
||||
"Current password": "Current password",
|
||||
"characters": "characters",
|
||||
"Credit card deleted successfully": "Credit card deleted successfully",
|
||||
"Date of Birth": "Date of Birth",
|
||||
"Day": "Day",
|
||||
"Description": "Description",
|
||||
@@ -45,12 +48,14 @@
|
||||
"Email": "Email",
|
||||
"There are no transactions to display": "There are no transactions to display",
|
||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
|
||||
"Extras to your booking": "Extras to your booking",
|
||||
"FAQ": "FAQ",
|
||||
"Find booking": "Find booking",
|
||||
"Former Scandic Hotel": "Former Scandic Hotel",
|
||||
"Flexibility": "Flexibility",
|
||||
"From": "From",
|
||||
"from your member profile?": "from your member profile?",
|
||||
"Get inspired": "Get inspired",
|
||||
"Go back to overview": "Go back to overview",
|
||||
"hotelPages.rooms.roomCard.person": "person",
|
||||
@@ -87,6 +92,7 @@
|
||||
"Next": "Next",
|
||||
"next level:": "next level:",
|
||||
"No content published": "No content published",
|
||||
"No, keep card": "No, keep card",
|
||||
"No transactions available": "No transactions available",
|
||||
"Not found": "Not found",
|
||||
"night": "night",
|
||||
@@ -112,6 +118,7 @@
|
||||
"Previous victories": "Previous victories",
|
||||
"Read more": "Read more",
|
||||
"Read more about the hotel": "Read more about the hotel",
|
||||
"Remove card from member profile": "Remove card from member profile",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Retype new password": "Retype new password",
|
||||
"Rooms": "Rooms",
|
||||
@@ -129,10 +136,13 @@
|
||||
"Skip to main content": "Skip to main content",
|
||||
"Sign up bonus": "Sign up bonus",
|
||||
"Something went wrong!": "Something went wrong!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Something went wrong and we couldn't add your card. Please try again later.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Something went wrong and we couldn't remove your card. Please try again later.",
|
||||
"Street": "Street",
|
||||
"special character": "special character",
|
||||
"Total Points": "Total Points",
|
||||
"Your points to spend": "Your points to spend",
|
||||
"You canceled adding a new credit card.": "You canceled adding a new credit card.",
|
||||
"Transaction date": "Transaction date",
|
||||
"Transactions": "Transactions",
|
||||
"Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)",
|
||||
@@ -142,13 +152,16 @@
|
||||
"uppercase letter": "uppercase letter",
|
||||
"Welcome": "Welcome",
|
||||
"Visiting address": "Visiting address",
|
||||
"We could not add a card right now, please try again later.": "We could not add a card right now, please try again later.",
|
||||
"Welcome to": "Welcome to",
|
||||
"Wellness & Exercise": "Wellness & Exercise",
|
||||
"Where should you go next?": "Where should you go next?",
|
||||
"Which room class suits you the best?": "Which room class suits you the best?",
|
||||
"Year": "Year",
|
||||
"Yes, remove my card": "Yes, remove my card",
|
||||
"You have no previous stays.": "You have no previous stays.",
|
||||
"You have no upcoming stays.": "You have no upcoming stays.",
|
||||
"Your card was successfully removed!": "Your card was successfully removed!",
|
||||
"Your card was successfully saved!": "Your card was successfully saved!",
|
||||
"Your Challenges Conquer & Earn!": "Your Challenges Conquer & Earn!",
|
||||
"Your level": "Your level",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"All rooms comes with standard amenities": "Kaikissa huoneissa on perusmukavuudet",
|
||||
"Already a friend?": "Oletko jo ystävä?",
|
||||
"Amenities": "Mukavuudet",
|
||||
"An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.",
|
||||
"Are you sure you want to remove the card ending with": "Haluatko varmasti poistaa kortin, joka päättyy numeroon",
|
||||
"Arrival date": "Saapumispäivä",
|
||||
"as of today": "tästä päivästä lähtien",
|
||||
"As our": "Kuin meidän",
|
||||
@@ -31,6 +33,7 @@
|
||||
"Could not find requested resource": "Pyydettyä resurssia ei löytynyt",
|
||||
"Country": "Maa",
|
||||
"Country code": "Maatunnus",
|
||||
"Credit card deleted successfully": "Luottokortti poistettu onnistuneesti",
|
||||
"Your current level": "Nykyinen tasosi",
|
||||
"Current password": "Nykyinen salasana",
|
||||
"characters": "hahmoja",
|
||||
@@ -45,10 +48,12 @@
|
||||
"Extras to your booking": "Lisävarusteet varaukseesi",
|
||||
"There are no transactions to display": "Näytettäviä tapahtumia ei ole",
|
||||
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
||||
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
|
||||
"Find booking": "Etsi varaus",
|
||||
"Flexibility": "Joustavuus",
|
||||
"Former Scandic Hotel": "Entinen Scandic Hotel",
|
||||
"From": "From",
|
||||
"from your member profile?": "jäsenprofiilistasi?",
|
||||
"Get inspired": "Inspiroidu",
|
||||
"Go back to overview": "Palaa yleiskatsaukseen",
|
||||
"Level 1": "Taso 1",
|
||||
@@ -82,6 +87,7 @@
|
||||
"Next": "Seuraava",
|
||||
"next level:": "Seuraava taso:",
|
||||
"No content published": "Ei julkaistua sisältöä",
|
||||
"No, keep card": "Ei, pidä kortti",
|
||||
"No transactions available": "Ei tapahtumia saatavilla",
|
||||
"Not found": "Ei löydetty",
|
||||
"night": "yö",
|
||||
@@ -107,6 +113,7 @@
|
||||
"Previous victories": "Edelliset voitot",
|
||||
"Read more": "Lue lisää",
|
||||
"Read more about the hotel": "Lue lisää hotellista",
|
||||
"Remove card from member profile": "Poista kortti jäsenprofiilista",
|
||||
"Restaurant & Bar": "Ravintola & Baari",
|
||||
"Retype new password": "Kirjoita uusi salasana uudelleen",
|
||||
"Rooms": "Huoneet",
|
||||
@@ -123,6 +130,8 @@
|
||||
"Skip to main content": "Siirry pääsisältöön",
|
||||
"Sign up bonus": "Rekisteröidy bonus",
|
||||
"Something went wrong!": "Jotain meni pieleen!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Jotain meni pieleen, emmekä voineet lisätä korttiasi. Yritä myöhemmin uudelleen.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Jotain meni pieleen, emmekä voineet poistaa korttiasi. Yritä myöhemmin uudelleen.",
|
||||
"Street": "Katu",
|
||||
"special character": "erikoishahmo",
|
||||
"Total Points": "Kokonaispisteet",
|
||||
@@ -135,15 +144,19 @@
|
||||
"User information": "Käyttäjän tiedot",
|
||||
"uppercase letter": "iso kirjain",
|
||||
"Visiting address": "Käyntiosoite",
|
||||
"We could not add a card right now, please try again later.": "Emme voineet lisätä korttia juuri nyt. Yritä myöhemmin uudelleen.",
|
||||
"Welcome": "Tervetuloa",
|
||||
"Welcome to": "Tervetuloa",
|
||||
"Wellness & Exercise": "Hyvinvointi & Liikunta",
|
||||
"Where should you go next?": "Mihin menisit seuraavaksi?",
|
||||
"Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?",
|
||||
"Year": "Vuosi",
|
||||
"Yes, remove my card": "Kyllä, poista korttini",
|
||||
"You have no previous stays.": "Sinulla ei ole aiempaa oleskelua.",
|
||||
"You have no upcoming stays.": "Sinulla ei ole tulevia oleskeluja.",
|
||||
"Your card was successfully removed!": "Korttisi poistettiin onnistuneesti!",
|
||||
"Your card was successfully saved!": "Korttisi tallennettu onnistuneesti!",
|
||||
"You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.",
|
||||
"Your Challenges Conquer & Earn!": "Voita ja ansaitse haasteesi!",
|
||||
"Your level": "Tasosi",
|
||||
"Zip code": "Postinumero",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"All rooms comes with standard amenities": "Alle rommene har standard fasiliteter",
|
||||
"Already a friend?": "Allerede Friend?",
|
||||
"Amenities": "Fasiliteter",
|
||||
"An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.",
|
||||
"Are you sure you want to remove the card ending with": "Er du sikker på at du vil fjerne kortet som slutter på",
|
||||
"Arrival date": "Ankomstdato",
|
||||
"as of today": "per idag",
|
||||
"As our": "Som vår",
|
||||
@@ -34,6 +36,7 @@
|
||||
"Your current level": "Ditt nåværende nivå",
|
||||
"Current password": "Nåværende passord",
|
||||
"characters": "tegn",
|
||||
"Credit card deleted successfully": "Kredittkort slettet",
|
||||
"Date of Birth": "Fødselsdato",
|
||||
"Day": "Dag",
|
||||
"Description": "Beskrivelse",
|
||||
@@ -45,10 +48,12 @@
|
||||
"Extras to your booking": "Ekstra til din bestilling",
|
||||
"There are no transactions to display": "Det er ingen transaksjoner å vise",
|
||||
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
|
||||
"Find booking": "Finn booking",
|
||||
"Flexibility": "Fleksibilitet",
|
||||
"Former Scandic Hotel": "Tidligere Scandic Hotel",
|
||||
"From": "Fra",
|
||||
"from your member profile?": "fra medlemsprofilen din?",
|
||||
"Get inspired": "Bli inspirert",
|
||||
"Go back to overview": "Gå tilbake til oversikten",
|
||||
"Level 1": "Nivå 1",
|
||||
@@ -82,6 +87,7 @@
|
||||
"Next": "Neste",
|
||||
"next level:": "Neste nivå:",
|
||||
"No content published": "Ingen innhold publisert",
|
||||
"No, keep card": "Nei, behold kortet",
|
||||
"No transactions available": "Ingen transaksjoner tilgjengelig",
|
||||
"Not found": "Ikke funnet",
|
||||
"night": "natt",
|
||||
@@ -107,6 +113,7 @@
|
||||
"Previous victories": "Tidligere seire",
|
||||
"Read more": "Les mer",
|
||||
"Read more about the hotel": "Les mer om hotellet",
|
||||
"Remove card from member profile": "Fjern kortet fra medlemsprofilen",
|
||||
"Restaurant & Bar": "Restaurant & Bar",
|
||||
"Retype new password": "Skriv inn nytt passord på nytt",
|
||||
"Rooms": "Rom",
|
||||
@@ -123,6 +130,8 @@
|
||||
"Skip to main content": "Gå videre til hovedsiden",
|
||||
"Sign up bonus": "Registreringsbonus",
|
||||
"Something went wrong!": "Noe gikk galt!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Noe gikk galt, og vi kunne ikke legge til kortet ditt. Prøv igjen senere.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Noe gikk galt, og vi kunne ikke fjerne kortet ditt. Vennligst prøv igjen senere.",
|
||||
"Street": "Gate",
|
||||
"special character": "spesiell karakter",
|
||||
"Total Points": "Totale poeng",
|
||||
@@ -135,14 +144,18 @@
|
||||
"User information": "Brukerinformasjon",
|
||||
"uppercase letter": "stor bokstav",
|
||||
"Visiting address": "Besøksadresse",
|
||||
"We could not add a card right now, please try again later.": "Vi kunne ikke legge til et kort akkurat nå. Prøv igjen senere.",
|
||||
"Welcome": "Velkommen",
|
||||
"Welcome to": "Velkommen til",
|
||||
"Wellness & Exercise": "Velvære & Trening",
|
||||
"Where should you go next?": "Hvor ønsker du å reise neste gang?",
|
||||
"Which room class suits you the best?": "Hvilken romklasse passer deg best?",
|
||||
"Year": "År",
|
||||
"You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.",
|
||||
"Yes, remove my card": "Ja, fjern kortet mitt",
|
||||
"You have no previous stays.": "Du har ingen tidligere opphold.",
|
||||
"You have no upcoming stays.": "Du har ingen kommende opphold.",
|
||||
"Your card was successfully removed!": "Kortet ditt ble fjernet!",
|
||||
"Your card was successfully saved!": "Kortet ditt ble lagret!",
|
||||
"Your Challenges Conquer & Earn!": "Dine utfordringer Erobre og tjen!",
|
||||
"Your level": "Ditt nivå",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"All rooms comes with standard amenities": "Alla rum har standardbekvämligheter",
|
||||
"Already a friend?": "Är du redan en vän?",
|
||||
"Amenities": "Bekvämligheter",
|
||||
"An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.",
|
||||
"Are you sure you want to remove the card ending with": "Är du säker på att du vill ta bort kortet som slutar med",
|
||||
"Arrival date": "Ankomstdatum",
|
||||
"as of today": "från och med idag",
|
||||
"As our": "Som vår",
|
||||
@@ -31,6 +33,7 @@
|
||||
"Could not find requested resource": "Det gick inte att hitta den begärda resursen",
|
||||
"Country": "Land",
|
||||
"Country code": "Landskod",
|
||||
"Credit card deleted successfully": "Kreditkort har tagits bort",
|
||||
"Your current level": "Din nuvarande nivå",
|
||||
"Current password": "Nuvarande lösenord",
|
||||
"characters": "tecken",
|
||||
@@ -45,10 +48,12 @@
|
||||
"Extras to your booking": "Extra till din bokning",
|
||||
"There are no transactions to display": "Det finns inga transaktioner att visa",
|
||||
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
||||
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
|
||||
"Find booking": "Hitta bokning",
|
||||
"Flexibility": "Flexibilitet",
|
||||
"Former Scandic Hotel": "Tidigare Scandic Hotel",
|
||||
"From": "Från",
|
||||
"from your member profile?": "från din medlemsprofil?",
|
||||
"Get inspired": "Bli inspirerad",
|
||||
"Go back to overview": "Gå tillbaka till översikten",
|
||||
"Level 1": "Nivå 1",
|
||||
@@ -85,6 +90,7 @@
|
||||
"Next": "Nästa",
|
||||
"next level:": "Nästa nivå:",
|
||||
"No content published": "Inget innehåll publicerat",
|
||||
"No, keep card": "Nej, behåll kortet",
|
||||
"No transactions available": "Inga transaktioner tillgängliga",
|
||||
"Not found": "Hittades inte",
|
||||
"night": "natt",
|
||||
@@ -110,6 +116,7 @@
|
||||
"Previous victories": "Tidigare segrar",
|
||||
"Read more": "Läs mer",
|
||||
"Read more about the hotel": "Läs mer om hotellet",
|
||||
"Remove card from member profile": "Ta bort kortet från medlemsprofilen",
|
||||
"Restaurant & Bar": "Restaurang & Bar",
|
||||
"Retype new password": "Upprepa nytt lösenord",
|
||||
"Rooms": "Rum",
|
||||
@@ -126,6 +133,8 @@
|
||||
"Skip to main content": "Fortsätt till huvudinnehåll",
|
||||
"Sign up bonus": "Registreringsbonus",
|
||||
"Something went wrong!": "Något gick fel!",
|
||||
"Something went wrong and we couldn't add your card. Please try again later.": "Något gick fel och vi kunde inte lägga till ditt kort. Försök igen senare.",
|
||||
"Something went wrong and we couldn't remove your card. Please try again later.": "Något gick fel och vi kunde inte ta bort ditt kort. Försök igen senare.",
|
||||
"Street": "Gata",
|
||||
"special character": "speciell karaktär",
|
||||
"Total Points": "Poäng totalt",
|
||||
@@ -138,13 +147,17 @@
|
||||
"User information": "Användar information",
|
||||
"uppercase letter": "stor bokstav",
|
||||
"Visiting address": "Besöksadress",
|
||||
"We could not add a card right now, please try again later.": "Vi kunde inte lägga till ett kort just nu, vänligen försök igen senare.",
|
||||
"Welcome": "Välkommen",
|
||||
"Wellness & Exercise": "Hälsa & Träning",
|
||||
"Where should you go next?": "Låter inte en spontanweekend härligt?",
|
||||
"Which room class suits you the best?": "Vilken rumsklass passar dig bäst?",
|
||||
"Year": "År",
|
||||
"You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.",
|
||||
"Yes, remove my card": "Ja, ta bort mitt kort",
|
||||
"You have no previous stays.": "Du har inga tidigare vistelser.",
|
||||
"You have no upcoming stays.": "Du har inga planerade resor.",
|
||||
"Your card was successfully removed!": "Ditt kort har tagits bort!",
|
||||
"Your card was successfully saved!": "Ditt kort har sparats!",
|
||||
"Your Challenges Conquer & Earn!": "Dina utmaningar Erövra och tjäna!",
|
||||
"Your level": "Din nivå",
|
||||
|
||||
@@ -13,6 +13,8 @@ export namespace endpoints {
|
||||
upcomingStays = "booking/v1/Stays/future",
|
||||
previousStays = "booking/v1/Stays/past",
|
||||
hotels = "hotel/v1/Hotels",
|
||||
intiateSaveCard = `${creditCards}/initiateSaveCard`,
|
||||
deleteCreditCard = `${profile}/creditCards`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const fetch = fetchRetry(global.fetch, {
|
||||
})
|
||||
|
||||
export async function get(
|
||||
endpoint: Endpoint | `${endpoints.v1.hotels}/${string}`,
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params?: URLSearchParams
|
||||
) {
|
||||
@@ -38,7 +38,7 @@ export async function get(
|
||||
}
|
||||
|
||||
export async function patch(
|
||||
endpoint: Endpoint,
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithJSONBody
|
||||
) {
|
||||
const { body, ...requestOptions } = options
|
||||
@@ -54,11 +54,12 @@ export async function patch(
|
||||
|
||||
export async function post(
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithJSONBody
|
||||
options: RequestOptionsWithJSONBody,
|
||||
params?: URLSearchParams
|
||||
) {
|
||||
const { body, ...requestOptions } = options
|
||||
return fetch(
|
||||
`${env.API_BASEURL}/${endpoint}`,
|
||||
`${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`,
|
||||
merge.all([
|
||||
defaultOptions,
|
||||
{ body: JSON.stringify(body), method: "POST" },
|
||||
@@ -68,11 +69,12 @@ export async function post(
|
||||
}
|
||||
|
||||
export async function remove(
|
||||
endpoint: Endpoint,
|
||||
options: RequestOptionsWithOutBody
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params?: URLSearchParams
|
||||
) {
|
||||
return fetch(
|
||||
`${env.API_BASEURL}/${endpoint}`,
|
||||
`${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`,
|
||||
merge.all([defaultOptions, { method: "DELETE" }, options])
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { createTRPCReact } from "@trpc/react-query"
|
||||
import { inferRouterInputs, inferRouterOutputs } from "@trpc/server"
|
||||
|
||||
import type { AppRouter } from "@/server"
|
||||
|
||||
export const trpc = createTRPCReact<AppRouter>()
|
||||
|
||||
export type RouterInput = inferRouterInputs<AppRouter>
|
||||
export type RouterOutput = inferRouterOutputs<AppRouter>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { userMutationRouter } from "./mutation"
|
||||
import { userQueryRouter } from "./query"
|
||||
|
||||
export const userRouter = mergeRouters(userQueryRouter)
|
||||
export const userRouter = mergeRouters(userQueryRouter, userMutationRouter)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
export const getUserInputSchema = z
|
||||
.object({
|
||||
mask: z.boolean().default(true),
|
||||
@@ -19,11 +21,15 @@ export const soonestUpcomingStaysInput = z
|
||||
})
|
||||
.default({ limit: 3 })
|
||||
|
||||
export const initiateSaveCardInput = z.object({
|
||||
export const addCreditCardInput = z.object({
|
||||
language: z.string(),
|
||||
})
|
||||
|
||||
export const saveCardInput = z.object({
|
||||
export const deleteCreditCardInput = z.object({
|
||||
creditCardId: z.string(),
|
||||
})
|
||||
|
||||
export const saveCreditCardInput = z.object({
|
||||
transactionId: z.string(),
|
||||
merchantId: z.string().optional(),
|
||||
})
|
||||
|
||||
85
server/routers/user/mutation.ts
Normal file
85
server/routers/user/mutation.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as api from "@/lib/api"
|
||||
import { initiateSaveCardSchema } from "@/server/routers/user/output"
|
||||
import { protectedProcedure, router } from "@/server/trpc"
|
||||
|
||||
import {
|
||||
addCreditCardInput,
|
||||
deleteCreditCardInput,
|
||||
saveCreditCardInput,
|
||||
} from "./input"
|
||||
|
||||
export const userMutationRouter = router({
|
||||
creditCard: router({
|
||||
add: protectedProcedure.input(addCreditCardInput).mutation(async function ({
|
||||
ctx,
|
||||
input,
|
||||
}) {
|
||||
const apiResponse = await api.post(api.endpoints.v1.intiateSaveCard, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
body: {
|
||||
language: input.language,
|
||||
mobileToken: false,
|
||||
redirectUrl: `api/web/add-card-callback/${input.language}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
console.info(`API Response Failed - Initiating add Creadit Card flow`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = initiateSaveCardSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
console.error(`Failed to initiate save card data`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return verifiedData.data.data
|
||||
}),
|
||||
save: protectedProcedure
|
||||
.input(saveCreditCardInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const apiResponse = await api.post(
|
||||
`${api.endpoints.v1.creditCards}/${input.transactionId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
console.error(`API Response Failed - Save card`)
|
||||
console.error(apiResponse)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(deleteCreditCardInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const apiResponse = await api.remove(
|
||||
`${api.endpoints.v1.creditCards}/${input.creditCardId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
console.error(`API Response Failed - Delete credit card`)
|
||||
console.error(apiResponse)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}),
|
||||
}),
|
||||
})
|
||||
@@ -176,20 +176,28 @@ type GetFriendTransactionsData = z.infer<typeof getFriendTransactionsSchema>
|
||||
|
||||
export type FriendTransaction = GetFriendTransactionsData["data"][number]
|
||||
|
||||
export const getCreditCardsSchema = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
attribute: z.object({
|
||||
cardName: z.string().optional(),
|
||||
alias: z.string(),
|
||||
truncatedNumber: z.string(),
|
||||
expirationDate: z.string(),
|
||||
cardType: z.string(),
|
||||
}),
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
),
|
||||
export const creditCardSchema = z
|
||||
.object({
|
||||
attribute: z.object({
|
||||
cardName: z.string().optional(),
|
||||
alias: z.string(),
|
||||
truncatedNumber: z.string(),
|
||||
expirationDate: z.string(),
|
||||
cardType: z.string(),
|
||||
}),
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
.transform((apiResponse) => {
|
||||
return {
|
||||
id: apiResponse.id,
|
||||
type: apiResponse.attribute.cardType,
|
||||
truncatedNumber: apiResponse.attribute.truncatedNumber,
|
||||
}
|
||||
})
|
||||
|
||||
export const creditCardsSchema = z.object({
|
||||
data: z.array(creditCardSchema),
|
||||
})
|
||||
|
||||
export const getMembershipCardsSchema = z.array(
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import { internalServerError } from "@/server/errors/next"
|
||||
import {
|
||||
badRequestError,
|
||||
forbiddenError,
|
||||
unauthorizedError,
|
||||
} from "@/server/errors/trpc"
|
||||
import {
|
||||
protectedProcedure,
|
||||
router,
|
||||
@@ -21,18 +15,15 @@ import encryptValue from "../utils/encryptValue"
|
||||
import {
|
||||
friendTransactionsInput,
|
||||
getUserInputSchema,
|
||||
initiateSaveCardInput,
|
||||
saveCardInput,
|
||||
staysInput,
|
||||
} from "./input"
|
||||
import {
|
||||
creditCardsSchema,
|
||||
FriendTransaction,
|
||||
getCreditCardsSchema,
|
||||
getFriendTransactionsSchema,
|
||||
getMembershipCardsSchema,
|
||||
getStaysSchema,
|
||||
getUserSchema,
|
||||
initiateSaveCardSchema,
|
||||
Stay,
|
||||
} from "./output"
|
||||
import { benefits, extendedUser, nextLevelPerks } from "./temp"
|
||||
@@ -566,7 +557,7 @@ export const userQueryRouter = router({
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = getCreditCardsSchema.safeParse(apiJson)
|
||||
const verifiedData = creditCardsSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
console.error(`Failed to validate Credit Cards Data`)
|
||||
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
@@ -577,69 +568,6 @@ export const userQueryRouter = router({
|
||||
return verifiedData.data.data
|
||||
}),
|
||||
|
||||
initiateSaveCard: protectedProcedure
|
||||
.input(initiateSaveCardInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const apiResponse = await api.post(api.endpoints.v1.initiateSaveCard, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
body: {
|
||||
language: input.language,
|
||||
mobileToken: false,
|
||||
redirectUrl: `api/web/add-card-callback/${input.language}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
switch (apiResponse.status) {
|
||||
case 400:
|
||||
throw badRequestError(apiResponse)
|
||||
case 401:
|
||||
throw unauthorizedError(apiResponse)
|
||||
case 403:
|
||||
throw forbiddenError(apiResponse)
|
||||
default:
|
||||
throw internalServerError(apiResponse)
|
||||
}
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = initiateSaveCardSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
console.error(`Failed to initiate save card data`)
|
||||
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return verifiedData.data.data
|
||||
}),
|
||||
|
||||
saveCard: protectedProcedure.input(saveCardInput).mutation(async function ({
|
||||
ctx,
|
||||
input,
|
||||
}) {
|
||||
const apiResponse = await api.post(
|
||||
`${api.endpoints.v1.creditCards}/${input.transactionId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
body: {},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
console.error(`API Response Failed - Save card`)
|
||||
console.error(`User: (${JSON.stringify(ctx.session.user)})`)
|
||||
console.error(apiResponse)
|
||||
return null
|
||||
}
|
||||
|
||||
return true
|
||||
}),
|
||||
|
||||
membershipCards: protectedProcedure.query(async function ({ ctx }) {
|
||||
const apiResponse = await api.get(api.endpoints.v1.profile, {
|
||||
cache: "no-store",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface CardProps extends React.HTMLAttributes<HTMLElement> {
|
||||
tag?: "article" | "div" | "section"
|
||||
}
|
||||
9
types/components/myPages/myProfile/creditCards.ts
Normal file
9
types/components/myPages/myProfile/creditCards.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { CreditCard } from "@/types/user"
|
||||
|
||||
export type CreditCardRowProps = {
|
||||
card: CreditCard
|
||||
}
|
||||
|
||||
export type DeleteCreditCardConfirmationProps = {
|
||||
card: CreditCard
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface RequestOptionsWithJSONBody
|
||||
extends Omit<RequestInit, "body" | "method"> {
|
||||
body: Record<string, unknown>
|
||||
body?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface RequestOptionsWithOutBody
|
||||
extends Omit<RequestInit, "body" | "method"> { }
|
||||
extends Omit<RequestInit, "body" | "method"> {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { getUserSchema } from "@/server/routers/user/output"
|
||||
import { creditCardSchema, getUserSchema } from "@/server/routers/user/output"
|
||||
|
||||
type Journey = {
|
||||
tag: string
|
||||
@@ -28,3 +28,5 @@ export interface User extends z.infer<typeof getUserSchema> {
|
||||
shortcuts: ShortcutLink[]
|
||||
victories: Victory[]
|
||||
}
|
||||
|
||||
export type CreditCard = z.output<typeof creditCardSchema>
|
||||
|
||||
Reference in New Issue
Block a user