Merged in feat/server-action-trpc (pull request #422)
feat(SW-160): update profile Approved-by: Christel Westerberg
This commit is contained in:
@@ -1,64 +1,100 @@
|
||||
"use server"
|
||||
import { ZodError } from "zod"
|
||||
|
||||
import { z } from "zod"
|
||||
|
||||
import { ApiLang } from "@/constants/languages"
|
||||
import * as api from "@/lib/api"
|
||||
import { protectedServerActionProcedure } from "@/server/trpc"
|
||||
|
||||
import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema"
|
||||
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
|
||||
import { type State, Status } from "@/types/components/myPages/myProfile/edit"
|
||||
import { Status } from "@/types/components/myPages/myProfile/edit"
|
||||
|
||||
export async function editProfile(_prevState: State, values: FormData) {
|
||||
try {
|
||||
const data: Record<string, any> = Object.fromEntries(values.entries())
|
||||
|
||||
/**
|
||||
* TODO: Update profile data when endpoint from
|
||||
* API team is ready
|
||||
*/
|
||||
|
||||
console.info(`Raw Data`)
|
||||
console.log(data)
|
||||
data.address = {
|
||||
city: data["address.city"],
|
||||
countryCode: data["address.countryCode"],
|
||||
streetAddress: data["address.streetAddress"],
|
||||
zipCode: data["address.zipCode"],
|
||||
const editProfilePayload = z
|
||||
.object({
|
||||
address: z.object({
|
||||
city: z.string().optional(),
|
||||
countryCode: z.nativeEnum(countriesMap),
|
||||
streetAddress: z.string().optional(),
|
||||
zipCode: z.string().min(1, { message: "Zip code is required" }),
|
||||
}),
|
||||
dateOfBirth: z.string(),
|
||||
email: z.string().email(),
|
||||
language: z.nativeEnum(ApiLang),
|
||||
newPassword: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
phoneNumber: phoneValidator("Phone is required"),
|
||||
})
|
||||
.transform((data) => {
|
||||
if (!data.password || !data.newPassword) {
|
||||
delete data.password
|
||||
delete data.newPassword
|
||||
}
|
||||
const parsedData = editProfileSchema.safeParse(data)
|
||||
if (parsedData.success) {
|
||||
console.info(`Success`)
|
||||
console.log(parsedData.data)
|
||||
return {
|
||||
message: "All good!",
|
||||
status: Status.success,
|
||||
}
|
||||
} else {
|
||||
console.error("Error parsing edit profile")
|
||||
console.error(parsedData.error)
|
||||
return {
|
||||
message: "Invalid data, parse failed!",
|
||||
status: Status.error,
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
console.error(`ZodError handling profile edit`)
|
||||
console.error(error)
|
||||
return data
|
||||
})
|
||||
|
||||
export const editProfile = protectedServerActionProcedure
|
||||
.input(editProfileSchema)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const payload = editProfilePayload.safeParse(input)
|
||||
if (!payload.success) {
|
||||
return {
|
||||
errors: error.issues.map((issue) => ({
|
||||
message: `Server validation: ${issue.message}`,
|
||||
path: issue.path.join("."),
|
||||
data: input,
|
||||
issues: payload.error.issues.map((issue) => ({
|
||||
field: issue.path.join("."),
|
||||
message: issue.message,
|
||||
})),
|
||||
message: "Invalid form data",
|
||||
message: "Validation failed.",
|
||||
status: Status.error,
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`EditProfile Server Action Error`)
|
||||
console.error(error)
|
||||
const response = await api.patch(api.endpoints.v1.profile, {
|
||||
body: payload.data,
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.info(`Response not ok`)
|
||||
console.error(response)
|
||||
return {
|
||||
data: input,
|
||||
issues: [],
|
||||
message: "Server error",
|
||||
status: Status.error,
|
||||
}
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
if (json.errors?.length) {
|
||||
json.errors.forEach((error: any) => {
|
||||
console.info(`API Fail in response`)
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
const validatedData = editProfileSchema.safeParse(json.data.attributes)
|
||||
if (!validatedData.success) {
|
||||
console.log({ ees: validatedData.error })
|
||||
return {
|
||||
data: input,
|
||||
issues: validatedData.error.issues.map((issue) => ({
|
||||
field: issue.path.join("."),
|
||||
message: issue.message,
|
||||
})),
|
||||
message: "Data is insufficient",
|
||||
status: Status.error,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: "Something went wrong. Please try again.",
|
||||
status: Status.error,
|
||||
data: validatedData.data,
|
||||
message: "All good!",
|
||||
status: Status.success,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
justify-items: flex-start;
|
||||
max-width: 510px;
|
||||
}
|
||||
|
||||
@@ -18,4 +19,4 @@
|
||||
.container {
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Due to css import issues with parallell routes we are forced to
|
||||
* Due to css import issues with parallel routes we are forced to
|
||||
* use a regular css file and import it in the page.tsx
|
||||
*/
|
||||
.profile-layout {
|
||||
|
||||
2
auth.ts
2
auth.ts
@@ -233,4 +233,4 @@ export const {
|
||||
auth,
|
||||
signIn,
|
||||
signOut,
|
||||
} = NextAuth(config)
|
||||
} = NextAuth(config)
|
||||
@@ -10,3 +10,9 @@
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: max(164px) 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { languageSelect } from "@/constants/languages"
|
||||
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import CountrySelect from "@/components/TempDesignSystem/Form/Country"
|
||||
import DateSelect from "@/components/TempDesignSystem/Form/Date"
|
||||
import Input from "@/components/TempDesignSystem/Form/Input"
|
||||
@@ -15,24 +16,24 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import styles from "./formContent.module.css"
|
||||
|
||||
export default function FormContent() {
|
||||
const { formatMessage } = useIntl()
|
||||
const intl = useIntl()
|
||||
// const { pending } = useFormStatus()
|
||||
|
||||
const city = formatMessage({ id: "City" })
|
||||
const country = formatMessage({ id: "Country" })
|
||||
const email = `${formatMessage({ id: "Email" })} ${formatMessage({ id: "Address" }).toLowerCase()}`
|
||||
const street = formatMessage({ id: "Address" })
|
||||
const phoneNumber = formatMessage({ id: "Phone number" })
|
||||
const password = formatMessage({ id: "Current password" })
|
||||
const retypeNewPassword = formatMessage({ id: "Retype new password" })
|
||||
const zipCode = formatMessage({ id: "Zip code" })
|
||||
const city = intl.formatMessage({ id: "City" })
|
||||
const country = intl.formatMessage({ id: "Country" })
|
||||
const email = `${intl.formatMessage({ id: "Email" })} ${intl.formatMessage({ id: "Address" }).toLowerCase()}`
|
||||
const street = intl.formatMessage({ id: "Address" })
|
||||
const phoneNumber = intl.formatMessage({ id: "Phone number" })
|
||||
const password = intl.formatMessage({ id: "Current password" })
|
||||
const retypeNewPassword = intl.formatMessage({ id: "Retype new password" })
|
||||
const zipCode = intl.formatMessage({ id: "Zip code" })
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className={styles.user}>
|
||||
<header>
|
||||
<Body textTransform="bold">
|
||||
{formatMessage({ id: "User information" })}
|
||||
{intl.formatMessage({ id: "User information" })}
|
||||
</Body>
|
||||
</header>
|
||||
<DateSelect name="dateOfBirth" registerOptions={{ required: true }} />
|
||||
@@ -59,16 +60,19 @@ export default function FormContent() {
|
||||
<Phone label={phoneNumber} name="phoneNumber" />
|
||||
<Select
|
||||
items={languageSelect}
|
||||
label={formatMessage({ id: "Language" })}
|
||||
label={intl.formatMessage({ id: "Language" })}
|
||||
name="language"
|
||||
placeholder={formatMessage({ id: "Select language" })}
|
||||
placeholder={intl.formatMessage({ id: "Select language" })}
|
||||
/>
|
||||
</section>
|
||||
<Divider className={styles.divider} color="subtle" />
|
||||
<section className={styles.password}>
|
||||
<header>
|
||||
<Body textTransform="bold">{formatMessage({ id: "Password" })}</Body>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage({ id: "Password" })}
|
||||
</Body>
|
||||
</header>
|
||||
<Input label={password} name="currentPassword" type="password" />
|
||||
<Input label={password} name="password" type="password" />
|
||||
<NewPassword />
|
||||
<Input
|
||||
label={retypeNewPassword}
|
||||
|
||||
@@ -24,10 +24,6 @@
|
||||
grid-area: buttons;
|
||||
}
|
||||
|
||||
.btn {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
grid-template-areas:
|
||||
@@ -40,9 +36,9 @@
|
||||
}
|
||||
|
||||
.btnContainer {
|
||||
align-self: center;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x2);
|
||||
justify-self: flex-end;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { isValidPhoneNumber, parsePhoneNumber } from "libphonenumber-js"
|
||||
import { useParams } from "next/navigation"
|
||||
import { useFormState as useReactFormState } from "react-dom"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import { FormProvider, useForm } from "react-hook-form"
|
||||
import { usePhoneInput } from "react-international-phone"
|
||||
import { useIntl } from "react-intl"
|
||||
@@ -13,32 +13,32 @@ import { editProfile } from "@/actions/editProfile"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
|
||||
import FormContent from "./FormContent"
|
||||
import { type EditProfileSchema, editProfileSchema } from "./schema"
|
||||
|
||||
import styles from "./form.module.css"
|
||||
|
||||
import type {
|
||||
EditFormProps,
|
||||
State,
|
||||
import {
|
||||
type EditFormProps,
|
||||
Status,
|
||||
} from "@/types/components/myPages/myProfile/edit"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
const formId = "edit-profile"
|
||||
|
||||
export default function Form({ user }: EditFormProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
const intl = useIntl()
|
||||
const router = useRouter()
|
||||
const params = useParams()
|
||||
const lang = params.lang as Lang
|
||||
/**
|
||||
* like react, react-hook-form also exports a useFormState hook,
|
||||
* we want to clearly keep them separate by naming.
|
||||
* RHF isValid defaults to false and never
|
||||
* changes when JS is disabled, therefore
|
||||
* we need to keep it insync ourselves
|
||||
*/
|
||||
const [state, formAction] = useReactFormState<State, FormData>(
|
||||
editProfile,
|
||||
null
|
||||
)
|
||||
const [isValid, setIsValid] = useState(true)
|
||||
|
||||
const { inputValue: phoneInput } = usePhoneInput({
|
||||
defaultCountry:
|
||||
@@ -61,7 +61,7 @@ export default function Form({ user }: EditFormProps) {
|
||||
email: user.email,
|
||||
language: user.language,
|
||||
phoneNumber: phoneInput,
|
||||
currentPassword: "",
|
||||
password: "",
|
||||
newPassword: "",
|
||||
retypeNewPassword: "",
|
||||
},
|
||||
@@ -70,44 +70,77 @@ export default function Form({ user }: EditFormProps) {
|
||||
reValidateMode: "onChange",
|
||||
})
|
||||
|
||||
async function handleSubmit(data: EditProfileSchema) {
|
||||
const response = await editProfile(data)
|
||||
switch (response.status) {
|
||||
case Status.error:
|
||||
if (response.issues?.length) {
|
||||
response.issues.forEach((issue) => {
|
||||
console.error(issue)
|
||||
})
|
||||
}
|
||||
toast.error(
|
||||
intl.formatMessage({
|
||||
id: "An error occured when trying to update profile.",
|
||||
})
|
||||
)
|
||||
break
|
||||
case Status.success:
|
||||
toast.success(
|
||||
intl.formatMessage({ id: "Successfully updated profile!" })
|
||||
)
|
||||
/**
|
||||
* TODO: Toaster?
|
||||
* Design only has toasters for credit cards
|
||||
* and membership cards
|
||||
*/
|
||||
router.push(profile[lang])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsValid(methods.formState.isValid)
|
||||
}, [setIsValid, methods.formState.isValid])
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<hgroup className={styles.title}>
|
||||
<Title as="h4" color="red" level="h1" textTransform="capitalize">
|
||||
{formatMessage({ id: "Welcome" })}
|
||||
{intl.formatMessage({ id: "Welcome" })}
|
||||
</Title>
|
||||
<Title as="h4" color="burgundy" level="h2" textTransform="capitalize">
|
||||
{user.name}
|
||||
</Title>
|
||||
</hgroup>
|
||||
<div className={styles.btnContainer}>
|
||||
<Button
|
||||
asChild
|
||||
intent="secondary"
|
||||
size="small"
|
||||
theme="base"
|
||||
className={styles.btn}
|
||||
>
|
||||
<Button asChild intent="secondary" size="small" theme="base">
|
||||
<Link href={profile[lang]}>
|
||||
{formatMessage({ id: "Discard changes" })}
|
||||
{intl.formatMessage({ id: "Discard changes" })}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
!methods.formState.isValid || methods.formState.isSubmitting
|
||||
}
|
||||
className={styles.btn}
|
||||
disabled={!isValid || methods.formState.isSubmitting}
|
||||
form={formId}
|
||||
intent="primary"
|
||||
size="small"
|
||||
theme="base"
|
||||
type="submit"
|
||||
>
|
||||
{formatMessage({ id: "Save" })}
|
||||
{intl.formatMessage({ id: "Save" })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form action={formAction} className={styles.form} id={formId}>
|
||||
<form
|
||||
/**
|
||||
* Ignoring since ts doesn't recognize that tRPC
|
||||
* parses FormData before reaching the route
|
||||
* @ts-ignore */
|
||||
action={editProfile}
|
||||
className={styles.form}
|
||||
id={formId}
|
||||
onSubmit={methods.handleSubmit(handleSubmit)}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<FormContent />
|
||||
</FormProvider>
|
||||
|
||||
@@ -25,12 +25,12 @@ export const editProfileSchema = z
|
||||
"Please enter a valid phone number"
|
||||
),
|
||||
|
||||
currentPassword: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
newPassword: z.string().optional(),
|
||||
retypeNewPassword: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.currentPassword) {
|
||||
if (data.password) {
|
||||
if (!data.newPassword) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
@@ -50,7 +50,7 @@ export const editProfileSchema = z
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Current password is required",
|
||||
path: ["currentPassword"],
|
||||
path: ["password"],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,12 +58,14 @@ a.default {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* SIZES */
|
||||
@@ -798,4 +800,4 @@ a.default {
|
||||
.icon.tertiaryLightSecondary:disabled svg,
|
||||
.icon.tertiaryLightSecondary:disabled svg * {
|
||||
fill: var(--Tertiary-Light-Button-Secondary-On-Fill-Disabled);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import type { Key } from "react-aria-components"
|
||||
|
||||
import type { DateProps } from "./date"
|
||||
|
||||
/** TODO: Get selecting with Enter-key to work */
|
||||
export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
const { formatMessage } = useIntl()
|
||||
const d = useWatch({ name })
|
||||
@@ -54,6 +53,18 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
const monthLabel = formatMessage({ id: "Month" })
|
||||
const yearLabel = formatMessage({ id: "Year" })
|
||||
|
||||
let dateValue = null
|
||||
try {
|
||||
/**
|
||||
* parseDate throws when its not a valid
|
||||
* date, but we can't check isNan since
|
||||
* we recieve the date as "1999-01-01"
|
||||
*/
|
||||
dateValue = parseDate(d)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
aria-label={formatMessage({ id: "Select date of birth" })}
|
||||
@@ -61,7 +72,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) {
|
||||
isRequired={!!registerOptions.required}
|
||||
name={name}
|
||||
ref={field.ref}
|
||||
value={isNaN(d) ? undefined : parseDate(d)}
|
||||
value={dateValue}
|
||||
>
|
||||
<Group>
|
||||
<DateInput className={styles.container}>
|
||||
|
||||
@@ -32,8 +32,8 @@ const config = {
|
||||
defaultVariants: {
|
||||
color: "burgundy",
|
||||
textAlign: "left",
|
||||
type: "h1",
|
||||
textTransform: "uppercase",
|
||||
type: "h1",
|
||||
},
|
||||
} as const
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export enum Lang {
|
||||
en = "en",
|
||||
sv = "sv",
|
||||
no = "no",
|
||||
fi = "fi",
|
||||
da = "da",
|
||||
de = "de",
|
||||
en = "en",
|
||||
fi = "fi",
|
||||
no = "no",
|
||||
sv = "sv",
|
||||
}
|
||||
|
||||
export const languages: Record<Lang, string> = {
|
||||
@@ -56,11 +56,21 @@ export const localeToLang: Record<string, Lang> = {
|
||||
"se-NO": Lang.no,
|
||||
} as const
|
||||
|
||||
export enum ApiLang {
|
||||
Da = "Da",
|
||||
De = "De",
|
||||
En = "En",
|
||||
Fi = "Fi",
|
||||
No = "No",
|
||||
Sv = "Sv",
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
export const languageSelect = [
|
||||
{ label: "Danish", value: "Da" },
|
||||
{ label: "German", value: "De" },
|
||||
{ label: "English", value: "En" },
|
||||
{ label: "Finnish", value: "Fi" },
|
||||
{ label: "Norwegian", value: "No" },
|
||||
{ label: "Swedish", value: "Sv" },
|
||||
{ label: "Danish", value: ApiLang.Da },
|
||||
{ label: "German", value: ApiLang.De },
|
||||
{ label: "English", value: ApiLang.En },
|
||||
{ label: "Finnish", value: ApiLang.Fi },
|
||||
{ label: "Norwegian", value: ApiLang.No },
|
||||
{ label: "Swedish", value: ApiLang.Sv },
|
||||
]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"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",
|
||||
"An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.",
|
||||
"Arrival date": "Ankomstdato",
|
||||
"as of today": "fra idag",
|
||||
"As our": "Som vores",
|
||||
@@ -133,6 +134,7 @@
|
||||
"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",
|
||||
"Successfully updated profile!": "Profilen er opdateret med succes!",
|
||||
"special character": "speciel karakter",
|
||||
"Total Points": "Samlet antal point",
|
||||
"Your points to spend": "Dine brugbare pointer",
|
||||
@@ -185,4 +187,4 @@
|
||||
"Use bonus cheque": "Brug bonuscheck",
|
||||
"Book reward night": "Book belønningsaften",
|
||||
"Find hotels": "Find hoteller"
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
"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",
|
||||
"An error occurred when trying to update profile.": "Beim Versuch, das Profil zu aktualisieren, ist ein Fehler aufgetreten.",
|
||||
"Arrival date": "Ankunftsdatum",
|
||||
"as of today": "Stand heute",
|
||||
"As our": "Als unser",
|
||||
@@ -128,6 +129,7 @@
|
||||
"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",
|
||||
"Successfully updated profile!": "Profil erfolgreich aktualisiert!",
|
||||
"special character": "sonderzeichen",
|
||||
"Total Points": "Gesamtpunktzahl",
|
||||
"Your points to spend": "Meine Punkte",
|
||||
@@ -179,4 +181,4 @@
|
||||
"Use bonus cheque": "Bonusscheck nutzen",
|
||||
"Book reward night": "Bonusnacht buchen",
|
||||
"Find hotels": "Hotels finden"
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
"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",
|
||||
"An error occurred when trying to update profile.": "An error occurred when trying to update profile.",
|
||||
"Arrival date": "Arrival date",
|
||||
"as of today": "as of today",
|
||||
"As our": "As our",
|
||||
@@ -139,6 +140,7 @@
|
||||
"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",
|
||||
"Successfully updated profile!": "Successfully updated profile!",
|
||||
"special character": "special character",
|
||||
"Total Points": "Total Points",
|
||||
"Your points to spend": "Your points to spend",
|
||||
@@ -150,9 +152,9 @@
|
||||
"TUI Points": "TUI Points",
|
||||
"User information": "User information",
|
||||
"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": "Welcome",
|
||||
"Welcome to": "Welcome to",
|
||||
"Wellness & Exercise": "Wellness & Exercise",
|
||||
"Where should you go next?": "Where should you go next?",
|
||||
@@ -191,4 +193,4 @@
|
||||
"Use bonus cheque": "Use bonus cheque",
|
||||
"Book reward night": "Book reward night",
|
||||
"Find hotels": "Find hotels"
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
"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",
|
||||
"An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.",
|
||||
"Arrival date": "Saapumispäivä",
|
||||
"as of today": "tästä päivästä lähtien",
|
||||
"As our": "Kuin meidän",
|
||||
@@ -133,6 +134,7 @@
|
||||
"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",
|
||||
"Successfully updated profile!": "Profiilin päivitys onnistui!",
|
||||
"special character": "erikoishahmo",
|
||||
"Total Points": "Kokonaispisteet",
|
||||
"Your points to spend": "Sinun pisteesi käytettäväksi",
|
||||
@@ -185,4 +187,4 @@
|
||||
"Use bonus cheque": "Käytä bonussekkiä",
|
||||
"Book reward night": "Kirjapalkinto-ilta",
|
||||
"Find hotels": "Etsi hotelleja"
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
"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å",
|
||||
"An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.",
|
||||
"Arrival date": "Ankomstdato",
|
||||
"as of today": "per idag",
|
||||
"As our": "Som vår",
|
||||
@@ -133,6 +134,7 @@
|
||||
"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",
|
||||
"Successfully updated profile!": "Vellykket oppdatert profil!",
|
||||
"special character": "spesiell karakter",
|
||||
"Total Points": "Totale poeng",
|
||||
"Your points to spend": "Dine brukbare poeng",
|
||||
@@ -185,4 +187,4 @@
|
||||
"Use bonus cheque": "Bruk bonussjekk",
|
||||
"Book reward night": "Bestill belønningskveld",
|
||||
"Find hotels": "Finn hotell"
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
"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",
|
||||
"An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.",
|
||||
"Arrival date": "Ankomstdatum",
|
||||
"as of today": "från och med idag",
|
||||
"As our": "Som vår",
|
||||
@@ -136,6 +137,7 @@
|
||||
"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",
|
||||
"Successfully updated profile!": "Profilen har uppdaterats framgångsrikt!",
|
||||
"special character": "speciell karaktär",
|
||||
"Total Points": "Poäng totalt",
|
||||
"Your points to spend": "Dina spenderbara poäng",
|
||||
@@ -187,4 +189,4 @@
|
||||
"Use bonus cheque": "Use bonus cheque",
|
||||
"Book reward night": "Book reward night",
|
||||
"Find hotels": "Hitta hotell"
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export { endpoints } from "./endpoints"
|
||||
const defaultOptions: RequestInit = {
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
mode: "cors",
|
||||
@@ -25,25 +26,40 @@ const fetch = fetchRetry(global.fetch, {
|
||||
return Math.pow(2, attempt) * 150 // 150, 300, 600
|
||||
},
|
||||
})
|
||||
const url = new URL(env.API_BASEURL)
|
||||
|
||||
export async function get(
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params?: URLSearchParams
|
||||
params = {}
|
||||
) {
|
||||
const url = new URL(
|
||||
`${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`
|
||||
)
|
||||
url.pathname = endpoint
|
||||
const searchParams = new URLSearchParams(params)
|
||||
if (searchParams.size) {
|
||||
searchParams.forEach((value, key) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
url.searchParams.sort()
|
||||
}
|
||||
return fetch(url, merge.all([defaultOptions, { method: "GET" }, options]))
|
||||
}
|
||||
|
||||
export async function patch(
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithJSONBody
|
||||
options: RequestOptionsWithJSONBody,
|
||||
params = {}
|
||||
) {
|
||||
const { body, ...requestOptions } = options
|
||||
url.pathname = endpoint
|
||||
const searchParams = new URLSearchParams(params)
|
||||
if (searchParams.size) {
|
||||
searchParams.forEach((value, key) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
url.searchParams.sort()
|
||||
}
|
||||
return fetch(
|
||||
`${env.API_BASEURL}/${endpoint}`,
|
||||
url,
|
||||
merge.all([
|
||||
defaultOptions,
|
||||
{ body: JSON.stringify(body), method: "PATCH" },
|
||||
@@ -55,11 +71,19 @@ export async function patch(
|
||||
export async function post(
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithJSONBody,
|
||||
params?: URLSearchParams
|
||||
params = {},
|
||||
) {
|
||||
const { body, ...requestOptions } = options
|
||||
url.pathname = endpoint
|
||||
const searchParams = new URLSearchParams(params)
|
||||
if (searchParams.size) {
|
||||
searchParams.forEach((value, key) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
url.searchParams.sort()
|
||||
}
|
||||
return fetch(
|
||||
`${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`,
|
||||
url,
|
||||
merge.all([
|
||||
defaultOptions,
|
||||
{ body: JSON.stringify(body), method: "POST" },
|
||||
@@ -71,10 +95,15 @@ export async function post(
|
||||
export async function remove(
|
||||
endpoint: Endpoint | `${Endpoint}/${string}`,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params?: URLSearchParams
|
||||
params = {},
|
||||
) {
|
||||
return fetch(
|
||||
`${env.API_BASEURL}/${endpoint}${params ? `?${params.toString()}` : ""}`,
|
||||
merge.all([defaultOptions, { method: "DELETE" }, options])
|
||||
)
|
||||
url.pathname = endpoint
|
||||
const searchParams = new URLSearchParams(params)
|
||||
if (searchParams.size) {
|
||||
searchParams.forEach((value, key) => {
|
||||
url.searchParams.set(key, value)
|
||||
})
|
||||
url.searchParams.sort()
|
||||
}
|
||||
return fetch(url, merge.all([defaultOptions, { method: "DELETE" }, options]))
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import "dayjs/locale/fi"
|
||||
import "dayjs/locale/sv"
|
||||
|
||||
import d from "dayjs"
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat"
|
||||
import isToday from "dayjs/plugin/isToday"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
@@ -55,6 +56,7 @@ d.locale("no", {
|
||||
/**
|
||||
* If more plugins are needed https://day.js.org/docs/en/plugin/plugin
|
||||
*/
|
||||
d.extend(advancedFormat)
|
||||
d.extend(isToday)
|
||||
d.extend(relativeTime)
|
||||
d.extend(utc)
|
||||
|
||||
108
package-lock.json
generated
108
package-lock.json
generated
@@ -19,9 +19,9 @@
|
||||
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.8",
|
||||
"@t3-oss/env-nextjs": "^0.9.2",
|
||||
"@tanstack/react-query": "^5.28.6",
|
||||
"@trpc/client": "^11.0.0-next-beta.318",
|
||||
"@trpc/react-query": "^11.0.0-next-beta.318",
|
||||
"@trpc/server": "^11.0.0-next-beta.318",
|
||||
"@trpc/client": "^11.0.0-rc.467",
|
||||
"@trpc/react-query": "^11.0.0-rc.467",
|
||||
"@trpc/server": "^11.0.0-rc.467",
|
||||
"@vercel/otel": "^1.9.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clean-deep": "^3.4.0",
|
||||
@@ -33,7 +33,7 @@
|
||||
"graphql-tag": "^2.12.6",
|
||||
"libphonenumber-js": "^1.10.60",
|
||||
"next": "^14.2.3",
|
||||
"next-auth": "^5.0.0-beta.15",
|
||||
"next-auth": "^5.0.0-beta.19",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-feather": "^2.0.10",
|
||||
@@ -106,15 +106,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core": {
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.28.1.tgz",
|
||||
"integrity": "sha512-gvp74mypYZADpTlfGRp6HE0G3pIHWvtJpy+KZ+8FvY0cmlIpHog+jdMOdd29dQtLtN25kF2YbfHsesCFuGUQbg==",
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.32.0.tgz",
|
||||
"integrity": "sha512-3+ssTScBd+1fd0/fscAyQN1tSygXzuhysuVVzB942ggU4mdfiTbv36P0ccVnExKWYJKvu3E2r3/zxXCCAmTOrg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@panva/hkdf": "^1.1.1",
|
||||
"@types/cookie": "0.6.0",
|
||||
"cookie": "0.6.0",
|
||||
"jose": "^5.1.3",
|
||||
"oauth4webapi": "^2.4.0",
|
||||
"oauth4webapi": "^2.9.0",
|
||||
"preact": "10.11.3",
|
||||
"preact-render-to-string": "5.2.3"
|
||||
},
|
||||
@@ -3676,9 +3677,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@panva/hkdf": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz",
|
||||
"integrity": "sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
@@ -5641,20 +5643,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.29.0.tgz",
|
||||
"integrity": "sha512-WgPTRs58hm9CMzEr5jpISe8HXa3qKQ8CxewdYZeVnA54JrPY9B1CZiwsCoLpLkf0dGRZq+LcX5OiJb0bEsOFww==",
|
||||
"version": "5.51.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.9.tgz",
|
||||
"integrity": "sha512-HsAwaY5J19MD18ykZDS3aVVh+bAt0i7m6uQlFC2b77DLV9djo+xEN7MWQAQQTR8IM+7r/zbozTQ7P0xr0bHuew==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.29.2.tgz",
|
||||
"integrity": "sha512-nyuWILR4u7H5moLGSiifLh8kIqQDLNOHGuSz0rcp+J75fNc8aQLyr5+I2JCHU3n+nJrTTW1ssgAD8HiKD7IFBQ==",
|
||||
"version": "5.51.11",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.11.tgz",
|
||||
"integrity": "sha512-4Kq2x0XpDlpvSnaLG+8pHNH60zEc3mBvb3B2tOMDjcPCi/o+Du3p/9qpPLwJOTliVxxPJAP27fuIhLrsRdCr7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.29.0"
|
||||
"@tanstack/query-core": "5.51.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5838,38 +5842,41 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/client": {
|
||||
"version": "11.0.0-rc.334",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.0.0-rc.334.tgz",
|
||||
"integrity": "sha512-nVKOxCfhKxBXvs1MwjfNNsWHQH/rHWzS4SxYPtYld7zqANTfNqn8cuClHMKlkeMKljObFiCfWhbdrBamKO/dDw==",
|
||||
"version": "11.0.0-rc.467",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-11.0.0-rc.467.tgz",
|
||||
"integrity": "sha512-ovZaGdAUl+EEmtJJc5uuo95B0gw8+q3jwNjUQQmmSMU5Isq4sYdjIWNkhbrFtR8CovllFyrRrjAgCWdaOTEY4g==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@trpc/server": "11.0.0-rc.334+fdf26e552"
|
||||
"@trpc/server": "11.0.0-rc.467+8f72171d6"
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/react-query": {
|
||||
"version": "11.0.0-rc.334",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/react-query/-/react-query-11.0.0-rc.334.tgz",
|
||||
"integrity": "sha512-DvhU7qMfQkLyrhQoDlrtMAGIgJyin9I8WCsF0oGlAD95ygoJcviD8D+lQzSCUttul9rPcNqWDuGhm5laS7V43A==",
|
||||
"version": "11.0.0-rc.467",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/react-query/-/react-query-11.0.0-rc.467.tgz",
|
||||
"integrity": "sha512-PNpHgISXJ60s0fJc6JUomKe3iu1wj6pZNFHJgQecAEK0gs1y6VM8Oh8CHgZg8+J/KDP/UtUmBcbpFP9l8Nq48w==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^5.25.0",
|
||||
"@trpc/client": "11.0.0-rc.334+fdf26e552",
|
||||
"@trpc/server": "11.0.0-rc.334+fdf26e552",
|
||||
"@tanstack/react-query": "^5.49.2",
|
||||
"@trpc/client": "11.0.0-rc.467+8f72171d6",
|
||||
"@trpc/server": "11.0.0-rc.467+8f72171d6",
|
||||
"react": ">=18.2.0",
|
||||
"react-dom": ">=18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/server": {
|
||||
"version": "11.0.0-rc.334",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.0.0-rc.334.tgz",
|
||||
"integrity": "sha512-ckg+f4z3Lc0wYvm1Cx8Zjz8b2hguNlUFQeF8vLrtWYBL8HEolshmxOHMG9MLc6WDE6T1R7DZrG+xEwjI44XH9w==",
|
||||
"version": "11.0.0-rc.467",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-11.0.0-rc.467.tgz",
|
||||
"integrity": "sha512-94Gv26ALuBfxgFlSGV3x2uF2ixUEViuK0m3IPKOvCTMreisZkBqyTa3NkBcuPZW/AMUieM5P4Q2NrbHTIA0fKQ==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
]
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
@@ -5968,7 +5975,8 @@
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
@@ -7960,6 +7968,7 @@
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -12346,9 +12355,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.2.4.tgz",
|
||||
"integrity": "sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==",
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz",
|
||||
"integrity": "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
@@ -14366,18 +14376,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth": {
|
||||
"version": "5.0.0-beta.16",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.16.tgz",
|
||||
"integrity": "sha512-dX2snB+ezN23tFzSes3n3uosT9iBf0eILPYWH/R2fd9n3ZzdMQlRzq7JIOPeS1aLc84IuRlyuyXyx9XmmZB6og==",
|
||||
"version": "5.0.0-beta.19",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.19.tgz",
|
||||
"integrity": "sha512-YHu1igcAxZPh8ZB7GIM93dqgY6gcAzq66FOhQFheAdOx1raxNcApt05nNyNCSB6NegSiyJ4XOPsaNow4pfDmsg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@auth/core": "0.28.1"
|
||||
"@auth/core": "0.32.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"@simplewebauthn/server": "^9.0.2",
|
||||
"next": "^14",
|
||||
"next": "^14 || ^15.0.0-0",
|
||||
"nodemailer": "^6.6.5",
|
||||
"react": "^18.2.0"
|
||||
"react": "^18.2.0 || ^19.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@simplewebauthn/browser": {
|
||||
@@ -14508,9 +14519,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/oauth4webapi": {
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.10.4.tgz",
|
||||
"integrity": "sha512-DSoj8QoChzOCQlJkRmYxAJCIpnXFW32R0Uq7avyghIeB6iJq0XAblOD7pcq3mx4WEBDwMuKr0Y1qveCBleG2Xw==",
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.11.1.tgz",
|
||||
"integrity": "sha512-aNzOnL98bL6izG97zgnZs1PFEyO4WDVRhz2Pd066NPak44w5ESLRCYmJIyey8avSBPOMtBjhF3ZDDm7bIb7UOg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
@@ -15235,6 +15247,7 @@
|
||||
"version": "10.11.3",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
|
||||
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
@@ -15244,6 +15257,7 @@
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz",
|
||||
"integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pretty-format": "^3.8.0"
|
||||
},
|
||||
@@ -15290,7 +15304,8 @@
|
||||
"node_modules/pretty-format": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
|
||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
@@ -16543,6 +16558,7 @@
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz",
|
||||
"integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
|
||||
@@ -35,9 +35,9 @@
|
||||
"@scandic-hotels/design-system": "git+https://x-token-auth:$DESIGN_SYSTEM_ACCESS_TOKEN@bitbucket.org/scandic-swap/design-system.git#v0.1.0-rc.8",
|
||||
"@t3-oss/env-nextjs": "^0.9.2",
|
||||
"@tanstack/react-query": "^5.28.6",
|
||||
"@trpc/client": "^11.0.0-next-beta.318",
|
||||
"@trpc/react-query": "^11.0.0-next-beta.318",
|
||||
"@trpc/server": "^11.0.0-next-beta.318",
|
||||
"@trpc/client": "^11.0.0-rc.467",
|
||||
"@trpc/react-query": "^11.0.0-rc.467",
|
||||
"@trpc/server": "^11.0.0-rc.467",
|
||||
"@vercel/otel": "^1.9.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clean-deep": "^3.4.0",
|
||||
@@ -49,7 +49,7 @@
|
||||
"graphql-tag": "^2.12.6",
|
||||
"libphonenumber-js": "^1.10.60",
|
||||
"next": "^14.2.3",
|
||||
"next-auth": "^5.0.0-beta.15",
|
||||
"next-auth": "^5.0.0-beta.19",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-feather": "^2.0.10",
|
||||
|
||||
@@ -5,8 +5,6 @@ import { Lang } from "@/constants/languages"
|
||||
|
||||
import { auth } from "@/auth"
|
||||
|
||||
import { unauthorizedError } from "./errors/trpc"
|
||||
|
||||
typeof auth
|
||||
|
||||
type CreateContextOptions = {
|
||||
|
||||
@@ -49,3 +49,13 @@ export function sessionExpiredError() {
|
||||
cause: new SessionExpiredError(SESSION_EXPIRED),
|
||||
})
|
||||
}
|
||||
|
||||
export const PUBLIC_UNAUTHORIZED = "PUBLIC_UNAUTHORIZED"
|
||||
export class PublicUnauthorizedError extends Error {}
|
||||
export function publicUnauthorizedError() {
|
||||
return new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: PUBLIC_UNAUTHORIZED,
|
||||
cause: new PublicUnauthorizedError(PUBLIC_UNAUTHORIZED),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import { router } from "./trpc"
|
||||
|
||||
export const appRouter = router({
|
||||
contentstack: contentstackRouter,
|
||||
user: userRouter,
|
||||
hotel: hotelsRouter,
|
||||
user: userRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
|
||||
@@ -20,16 +20,16 @@ import tempRatesData from "./tempRatesData.json"
|
||||
export const hotelQueryRouter = router({
|
||||
getHotel: serviceProcedure
|
||||
.input(getHotelInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { hotelId, language, include } = input
|
||||
|
||||
const params = new URLSearchParams()
|
||||
|
||||
const apiLang = toApiLang(language)
|
||||
params.set("language", apiLang)
|
||||
|
||||
const params: Record<string, string> = {
|
||||
hotelId,
|
||||
language: apiLang,
|
||||
}
|
||||
if (include) {
|
||||
params.set("include", include.join(","))
|
||||
params.include = include.join(",")
|
||||
}
|
||||
|
||||
const apiResponse = await api.get(
|
||||
@@ -61,16 +61,16 @@ export const hotelQueryRouter = router({
|
||||
|
||||
const roomCategories = included
|
||||
? included
|
||||
.filter((item) => item.type === "roomcategories")
|
||||
.map((roomCategory) => {
|
||||
const validatedRoom = roomSchema.safeParse(roomCategory)
|
||||
if (!validatedRoom.success) {
|
||||
console.error(`Get Room Category Data - Verified Data Error`)
|
||||
console.error(validatedRoom.error)
|
||||
throw badRequestError()
|
||||
}
|
||||
return validatedRoom.data
|
||||
})
|
||||
.filter((item) => item.type === "roomcategories")
|
||||
.map((roomCategory) => {
|
||||
const validatedRoom = roomSchema.safeParse(roomCategory)
|
||||
if (!validatedRoom.success) {
|
||||
console.error(`Get Room Category Data - Verified Data Error`)
|
||||
console.error(validatedRoom.error)
|
||||
throw badRequestError()
|
||||
}
|
||||
return validatedRoom.data
|
||||
})
|
||||
: []
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
// Query
|
||||
export const getUserInputSchema = z
|
||||
.object({
|
||||
mask: z.boolean().default(true),
|
||||
@@ -10,17 +9,27 @@ export const getUserInputSchema = z
|
||||
|
||||
export const staysInput = z
|
||||
.object({
|
||||
cursor: z.number().optional(),
|
||||
limit: z.number().min(0).default(6),
|
||||
cursor: z
|
||||
.number()
|
||||
.optional()
|
||||
.transform((num) => (num ? String(num) : undefined)),
|
||||
limit: z
|
||||
.number()
|
||||
.min(0)
|
||||
.default(6)
|
||||
.transform((num) => String(num)),
|
||||
})
|
||||
.default({})
|
||||
|
||||
export const soonestUpcomingStaysInput = z
|
||||
export const friendTransactionsInput = z
|
||||
.object({
|
||||
limit: z.number().int().positive(),
|
||||
page: z.number().int().positive(),
|
||||
})
|
||||
.default({ limit: 3 })
|
||||
.default({ limit: 5, page: 1 })
|
||||
|
||||
|
||||
// Mutation
|
||||
export const addCreditCardInput = z.object({
|
||||
language: z.string(),
|
||||
})
|
||||
@@ -33,9 +42,3 @@ export const saveCreditCardInput = z.object({
|
||||
transactionId: z.string(),
|
||||
merchantId: z.string().optional(),
|
||||
})
|
||||
export const friendTransactionsInput = z
|
||||
.object({
|
||||
limit: z.number().int().positive(),
|
||||
page: z.number().int().positive(),
|
||||
})
|
||||
.default({ limit: 5, page: 1 })
|
||||
|
||||
@@ -79,6 +79,7 @@ async function getVerifiedUser({ session }: { session: Session }) {
|
||||
console.error(verifiedData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return verifiedData
|
||||
}
|
||||
|
||||
@@ -335,12 +336,9 @@ export const userQueryRouter = router({
|
||||
.input(staysInput)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { limit, cursor } = input
|
||||
|
||||
const params = new URLSearchParams()
|
||||
params.set("limit", limit.toString())
|
||||
|
||||
const params: Record<string, string> = { limit }
|
||||
if (cursor) {
|
||||
params.set("offset", cursor.toString())
|
||||
params.offset = cursor
|
||||
}
|
||||
|
||||
const apiResponse = await api.get(
|
||||
@@ -403,11 +401,9 @@ export const userQueryRouter = router({
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { limit, cursor } = input
|
||||
|
||||
const params = new URLSearchParams()
|
||||
params.set("limit", limit.toString())
|
||||
|
||||
const params: Record<string, string> = { limit }
|
||||
if (cursor) {
|
||||
params.set("offset", cursor.toString())
|
||||
params.offset = cursor
|
||||
}
|
||||
|
||||
const apiResponse = await api.get(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { initTRPC } from "@trpc/server"
|
||||
import { experimental_nextAppDirCaller } from "@trpc/server/adapters/next-app-dir"
|
||||
import { ZodError } from "zod"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
@@ -9,15 +11,30 @@ import {
|
||||
unauthorizedError,
|
||||
} from "./errors/trpc"
|
||||
import { fetchServiceToken } from "./tokenManager"
|
||||
import { type Context, createContext } from "./context"
|
||||
import { transformer } from "./transformer"
|
||||
import { langInput } from "./utils"
|
||||
|
||||
import type { Session } from "next-auth"
|
||||
|
||||
import type { Meta } from "@/types/trpc/meta"
|
||||
import type { Context } from "./context"
|
||||
|
||||
const t = initTRPC.context<Context>().meta<Meta>().create({ transformer })
|
||||
const t = initTRPC
|
||||
.context<Context>()
|
||||
.meta<Meta>()
|
||||
.create({
|
||||
transformer,
|
||||
errorFormatter({ shape, error }) {
|
||||
return {
|
||||
...shape,
|
||||
data: {
|
||||
...shape.data,
|
||||
zodError:
|
||||
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const { createCallerFactory, mergeRouters, router } = t
|
||||
export const publicProcedure = t.procedure
|
||||
@@ -113,3 +130,30 @@ export const serviceProcedure = t.procedure.use(async (opts) => {
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
export const serverActionProcedure = t.procedure.experimental_caller(
|
||||
experimental_nextAppDirCaller({
|
||||
createContext,
|
||||
normalizeFormData: true,
|
||||
})
|
||||
)
|
||||
|
||||
export const protectedServerActionProcedure = serverActionProcedure.use(
|
||||
async (opts) => {
|
||||
const session = await opts.ctx.auth()
|
||||
if (!session) {
|
||||
throw unauthorizedError()
|
||||
}
|
||||
|
||||
if (session && session.error === "RefreshAccessTokenError") {
|
||||
throw sessionExpiredError()
|
||||
}
|
||||
|
||||
return opts.next({
|
||||
ctx: {
|
||||
...opts.ctx,
|
||||
session,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,25 +4,31 @@ export type EditFormProps = {
|
||||
user: User
|
||||
}
|
||||
|
||||
type E = {
|
||||
message: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export const enum Status {
|
||||
error = "error",
|
||||
success = "success",
|
||||
}
|
||||
|
||||
type ErrorState = {
|
||||
errors?: E[]
|
||||
type Data = Record<
|
||||
string,
|
||||
string | undefined | Record<string, string | undefined>
|
||||
>
|
||||
|
||||
type Issue = {
|
||||
field: string
|
||||
message: string
|
||||
status: Status.error
|
||||
}
|
||||
|
||||
type SuccessState = {
|
||||
export type State = {
|
||||
data: Data
|
||||
message: string
|
||||
status: Status.success
|
||||
}
|
||||
|
||||
export type State = ErrorState | SuccessState | null
|
||||
} & (
|
||||
| {
|
||||
issues: never
|
||||
status: Status.success
|
||||
}
|
||||
| {
|
||||
issues: Issue[]
|
||||
status: Status.error
|
||||
}
|
||||
)
|
||||
|
||||
4
types/jwt.d.ts
vendored
4
types/jwt.d.ts
vendored
@@ -11,9 +11,9 @@ declare module "next-auth/jwt" {
|
||||
interface JWT extends DefaultJWT, RefreshTokenError {
|
||||
access_token: string
|
||||
expires_at?: number
|
||||
refresh_token: string
|
||||
loginType: LoginType
|
||||
mfa_scope: boolean
|
||||
mfa_expires_at: number
|
||||
mfa_scope: boolean
|
||||
refresh_token: string
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user