From 9caa560b8dad2ef4492251c634d36d51ad747730 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Fri, 6 Sep 2024 10:56:58 +0200 Subject: [PATCH 01/26] feat(SW-360): Added checkbox component --- .../Form/Checkbox/checkbox.module.css | 39 +++++++++++++++ .../Form/Checkbox/checkbox.ts | 7 +++ .../TempDesignSystem/Form/Checkbox/index.tsx | 48 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 components/TempDesignSystem/Form/Checkbox/checkbox.module.css create mode 100644 components/TempDesignSystem/Form/Checkbox/checkbox.ts create mode 100644 components/TempDesignSystem/Form/Checkbox/index.tsx diff --git a/components/TempDesignSystem/Form/Checkbox/checkbox.module.css b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css new file mode 100644 index 000000000..7f7c8f04f --- /dev/null +++ b/components/TempDesignSystem/Form/Checkbox/checkbox.module.css @@ -0,0 +1,39 @@ +.container { + display: flex; + flex-direction: column; + color: var(--text-color); +} + +.container[data-selected] .checkbox { + border: none; + background: var(--UI-Input-Controls-Fill-Selected); +} + +.checkboxContainer { + display: flex; + align-items: flex-start; + gap: var(--Spacing-x-one-and-half); +} + +.checkbox { + flex-grow: 1; + width: 24px; + height: 24px; + min-width: 24px; + border: 2px solid var(--UI-Input-Controls-Border-Normal); + border-radius: 4px; + transition: all 200ms; + display: flex; + align-items: center; + justify-content: center; + transition: all 200ms; + forced-color-adjust: none; +} + +.error { + align-items: center; + color: var(--Scandic-Red-60); + display: flex; + gap: var(--Spacing-x-half); + margin: var(--Spacing-x1) 0 0; +} diff --git a/components/TempDesignSystem/Form/Checkbox/checkbox.ts b/components/TempDesignSystem/Form/Checkbox/checkbox.ts new file mode 100644 index 000000000..8588b7401 --- /dev/null +++ b/components/TempDesignSystem/Form/Checkbox/checkbox.ts @@ -0,0 +1,7 @@ +import { RegisterOptions } from "react-hook-form" + +export interface CheckboxProps + extends React.InputHTMLAttributes { + name: string + registerOptions?: RegisterOptions +} diff --git a/components/TempDesignSystem/Form/Checkbox/index.tsx b/components/TempDesignSystem/Form/Checkbox/index.tsx new file mode 100644 index 000000000..339dff676 --- /dev/null +++ b/components/TempDesignSystem/Form/Checkbox/index.tsx @@ -0,0 +1,48 @@ +import { Checkbox as AriaCheckbox } from "react-aria-components" +import { useController, useFormContext } from "react-hook-form" + +import { InfoCircleIcon } from "@/components/Icons" +import CheckIcon from "@/components/Icons/Check" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import { CheckboxProps } from "./checkbox" + +import styles from "./checkbox.module.css" + +export default function Checkbox({ + name, + children, + registerOptions, +}: React.PropsWithChildren) { + const { control } = useFormContext() + const { field, fieldState } = useController({ + control, + name, + rules: registerOptions, + }) + + return ( + + {({ isSelected }) => ( + <> +
+
+ {isSelected && } +
+ {children} +
+ {fieldState.error ? ( + + + {fieldState.error.message} + + ) : null} + + )} +
+ ) +} From 943505909703b271e57ab8b1d82ee548b038cbca Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Tue, 10 Sep 2024 10:33:34 +0200 Subject: [PATCH 02/26] feat(SW-360): Refactored NewPassword input --- components/Forms/Edit/Profile/index.tsx | 1 + components/Forms/Edit/Profile/schema.ts | 27 +--- .../Form/NewPassword/index.tsx | 126 +++++++++++------- .../Form/NewPassword/newPassword.module.css | 78 +++++++++++ .../Form/NewPassword/newPassword.ts | 11 +- i18n/dictionaries/da.json | 1 + i18n/dictionaries/de.json | 1 + i18n/dictionaries/en.json | 1 + i18n/dictionaries/fi.json | 1 + i18n/dictionaries/no.json | 1 + i18n/dictionaries/sv.json | 1 + public/_static/img/icons/eye-show.svg | 8 ++ utils/passwordValidator.ts | 48 +++++++ 13 files changed, 222 insertions(+), 83 deletions(-) create mode 100644 public/_static/img/icons/eye-show.svg create mode 100644 utils/passwordValidator.ts diff --git a/components/Forms/Edit/Profile/index.tsx b/components/Forms/Edit/Profile/index.tsx index 33f93e0fd..89097ae31 100644 --- a/components/Forms/Edit/Profile/index.tsx +++ b/components/Forms/Edit/Profile/index.tsx @@ -69,6 +69,7 @@ export default function Form({ user }: EditFormProps) { retypeNewPassword: "", }, mode: "all", + criteriaMode: "all", resolver: zodResolver(editProfileSchema), reValidateMode: "onChange", }) diff --git a/components/Forms/Edit/Profile/schema.ts b/components/Forms/Edit/Profile/schema.ts index 7330214ff..bf4e374cc 100644 --- a/components/Forms/Edit/Profile/schema.ts +++ b/components/Forms/Edit/Profile/schema.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { Key } from "@/components/TempDesignSystem/Form/NewPassword/newPassword" +import { passwordValidator } from "@/utils/passwordValidator" import { phoneValidator } from "@/utils/phoneValidator" const countryRequiredMsg = "Country is required" @@ -26,7 +26,7 @@ export const editProfileSchema = z ), password: z.string().optional(), - newPassword: z.string().optional(), + newPassword: passwordValidator(), retypeNewPassword: z.string().optional(), }) .superRefine((data, ctx) => { @@ -55,29 +55,6 @@ export const editProfileSchema = z } } - if (data.newPassword) { - const msgs = [] - if (data.newPassword.length < 10 || data.newPassword.length > 40) { - msgs.push(Key.CHAR_LENGTH) - } - if (!data.newPassword.match(/[A-Z]/g)) { - msgs.push(Key.UPPERCASE) - } - if (!data.newPassword.match(/[0-9]/g)) { - msgs.push(Key.NUM) - } - if (!data.newPassword.match(/[^A-Za-z0-9]/g)) { - msgs.push(Key.SPECIAL_CHAR) - } - if (msgs.length) { - ctx.addIssue({ - code: "custom", - message: msgs.join(","), - path: ["newPassword"], - }) - } - } - if (data.newPassword && !data.retypeNewPassword) { ctx.addIssue({ code: "custom", diff --git a/components/TempDesignSystem/Form/NewPassword/index.tsx b/components/TempDesignSystem/Form/NewPassword/index.tsx index 3c9950e1f..558247e04 100644 --- a/components/TempDesignSystem/Form/NewPassword/index.tsx +++ b/components/TempDesignSystem/Form/NewPassword/index.tsx @@ -1,33 +1,60 @@ "use client" + import { Text, TextField } from "react-aria-components" import { Controller, useFormContext } from "react-hook-form" import { useIntl } from "react-intl" -import { CheckIcon, CloseIcon } from "@/components/Icons" -import Error from "@/components/TempDesignSystem/Form/ErrorMessage/Error" import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel" -import Caption from "@/components/TempDesignSystem/Text/Caption" +import Image from "next/image" +import { useState } from "react" -import { type IconProps, Key, type NewPasswordProps } from "./newPassword" +import { CheckIcon, CloseIcon, InfoCircleIcon } from "@/components/Icons" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import { + PasswordValidatorKey, + passwordValidators, +} from "@/utils/passwordValidator" + +import Button from "../../Button" +import { IconProps, type NewPasswordProps } from "./newPassword" import styles from "./newPassword.module.css" export default function NewPassword({ + name = "newPassword", "aria-label": ariaLabel, disabled = false, placeholder = "", registerOptions = {}, + label, }: NewPasswordProps) { const { control } = useFormContext() - const { formatMessage } = useIntl() + const intl = useIntl() + const [isPasswordVisible, setPasswordVisible] = useState(false) + + function getErrorMessage(key: PasswordValidatorKey) { + switch (key) { + case "length": + return `10 ${intl.formatMessage({ id: "to" })} 40 ${intl.formatMessage({ id: "characters" })}` + case "hasUppercase": + return `1 ${intl.formatMessage({ id: "uppercase letter" })}` + case "hasLowercase": + return `1 ${intl.formatMessage({ id: "lowercase letter" })}` + case "hasNumber": + return `1 ${intl.formatMessage({ id: "number" })}` + case "hasSpecialChar": + return `1 ${intl.formatMessage({ id: "special character" })}` + } + } + return ( { - const messages = (fieldState.error?.message?.split(",") ?? []) as Key[] + render={({ field, fieldState, formState }) => { + const errors = Object.values(formState.errors[name]?.types ?? []).flat() return ( - +
+ + +
{field.value ? (
- - - - 10 {formatMessage({ id: "to" })} 40{" "} - {formatMessage({ id: "characters" })} - - - - - 1{" "} - {formatMessage({ id: "uppercase letter" })} - - - - - 1{" "} - {formatMessage({ id: "number" })} - - - - - 1{" "} - {formatMessage({ id: "special character" })} - - + {Object.entries(passwordValidators).map( + ([key, { message }]) => ( + + + + {getErrorMessage(key as PasswordValidatorKey)} + + + ) + )}
) : null} {!field.value && fieldState.error ? ( - - - {fieldState.error.message} - - + + + {fieldState.error.message} + ) : null}
) @@ -92,8 +120,8 @@ export default function NewPassword({ ) } -function Icon({ matcher, messages }: IconProps) { - return messages.includes(matcher) ? ( +function Icon({ errorMessage, errors }: IconProps) { + return errors.includes(errorMessage) ? ( ) : ( diff --git a/components/TempDesignSystem/Form/NewPassword/newPassword.module.css b/components/TempDesignSystem/Form/NewPassword/newPassword.module.css index 7b0c4509f..7d57c6850 100644 --- a/components/TempDesignSystem/Form/NewPassword/newPassword.module.css +++ b/components/TempDesignSystem/Form/NewPassword/newPassword.module.css @@ -1,12 +1,90 @@ +.container { + align-content: center; + background-color: var(--Main-Grey-White); + border-color: var(--Scandic-Beige-40); + border-style: solid; + border-width: 1px; + border-radius: var(--Corner-radius-Medium); + display: grid; + height: 60px; + padding: var(--Spacing-x1) var(--Spacing-x2); + transition: border-color 200ms ease; + position: relative; +} + +.container:has(.input:active, .input:focus) { + border-color: var(--Scandic-Blue-90); +} + +.container:has(.input:disabled) { + background-color: var(--Main-Grey-10); + border: none; + color: var(--Main-Grey-40); +} + +.container:has(.input[data-invalid="true"], .input[aria-invalid="true"]) { + border-color: var(--Scandic-Red-60); +} + +.input { + background: none; + border: none; + color: var(--Main-Grey-100); + height: 18px; + margin: 0; + order: 2; + overflow: visible; + padding: 0; +} + +.input:not(:active, :focus):placeholder-shown { + height: 0px; + transition: height 150ms ease; +} + +.input:focus, +.input:focus:placeholder-shown, +.input:active, +.input:active:placeholder-shown { + height: 18px; + transition: height 150ms ease; + outline: none; +} + +.input:disabled { + color: var(--Main-Grey-40); +} + .helpText { align-items: flex-start; display: flex; gap: var(--Spacing-x-half); } +.error { + align-items: center; + color: var(--Scandic-Red-60); + display: flex; + gap: var(--Spacing-x-half); + margin: var(--Spacing-x1) 0 0; +} + .errors { display: flex; flex-wrap: wrap; gap: var(--Spacing-x-one-and-half) var(--Spacing-x1); padding-top: var(--Spacing-x1); } + +.eyeIcon { + position: absolute; + right: var(--Spacing-x2); + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; +} + +.inputWrapper { + position: relative; +} diff --git a/components/TempDesignSystem/Form/NewPassword/newPassword.ts b/components/TempDesignSystem/Form/NewPassword/newPassword.ts index d9d44a20b..e6835cb58 100644 --- a/components/TempDesignSystem/Form/NewPassword/newPassword.ts +++ b/components/TempDesignSystem/Form/NewPassword/newPassword.ts @@ -1,12 +1,5 @@ import type { RegisterOptions } from "react-hook-form" -export enum Key { - CHAR_LENGTH = "CHAR_LENGTH", - NUM = "NUM", - SPECIAL_CHAR = "SPECIAL_CHAR", - UPPERCASE = "UPPERCASE", -} - export interface NewPasswordProps extends React.InputHTMLAttributes { label?: string @@ -14,6 +7,6 @@ export interface NewPasswordProps } export interface IconProps { - matcher: Key - messages: Key[] + errorMessage: string + errors: string[] } diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 76237d5a5..57d493bf9 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -138,6 +138,7 @@ "Log in here": "Log ind her", "Log in/Join": "Log på/Tilmeld dig", "Log out": "Log ud", + "lowercase letter": "lille bogstav", "Main menu": "Hovedmenu", "Manage preferences": "Administrer præferencer", "Map": "Kort", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 742890d7c..ffb0f018b 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -138,6 +138,7 @@ "Log in here": "Hier einloggen", "Log in/Join": "Log in/Anmelden", "Log out": "Ausloggen", + "lowercase letter": "Kleinbuchstabe", "Main menu": "Hauptmenü", "Manage preferences": "Verwalten von Voreinstellungen", "Map": "Karte", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index fa28c7399..fd989d49d 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -138,6 +138,7 @@ "Log in here": "Log in here", "Log in/Join": "Log in/Join", "Log out": "Log out", + "lowercase letter": "lowercase letter", "Main menu": "Main menu", "Manage preferences": "Manage preferences", "Map": "Map", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index ec0b1ee0c..716a58765 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -138,6 +138,7 @@ "Log in here": "Kirjaudu sisään", "Log in/Join": "Kirjaudu sisään/Liittyä", "Log out": "Kirjaudu ulos", + "lowercase letter": "pien kirjain", "Main menu": "Päävalikko", "Manage preferences": "Asetusten hallinta", "Map": "Kartta", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index 4a7ab2908..d422258a6 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -137,6 +137,7 @@ "Log in here": "Logg inn her", "Log in/Join": "Logg på/Bli med", "Log out": "Logg ut", + "lowercase letter": "liten bokstav", "Main menu": "Hovedmeny", "Manage preferences": "Administrer preferanser", "Map": "Kart", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 28c7a9889..51543e59c 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -138,6 +138,7 @@ "Log in here": "Logga in här", "Log in/Join": "Logga in/Gå med", "Log out": "Logga ut", + "lowercase letter": "liten bokstav", "Main menu": "Huvudmeny", "Manage preferences": "Hantera inställningar", "Map": "Karta", diff --git a/public/_static/img/icons/eye-show.svg b/public/_static/img/icons/eye-show.svg new file mode 100644 index 000000000..f14b33977 --- /dev/null +++ b/public/_static/img/icons/eye-show.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/utils/passwordValidator.ts b/utils/passwordValidator.ts new file mode 100644 index 000000000..8a8d74778 --- /dev/null +++ b/utils/passwordValidator.ts @@ -0,0 +1,48 @@ +import { z } from "zod" + +export const passwordValidators = { + length: { + matcher: (password: string) => + password.length >= 10 && password.length <= 40, + message: "10 to 40 characters", + }, + hasUppercase: { + matcher: (password: string) => /[A-Z]/.test(password), + message: "1 uppercase letter", + }, + hasLowercase: { + matcher: (password: string) => /[a-z]/.test(password), + message: "1 lowercase letter", + }, + hasNumber: { + matcher: (password: string) => /[0-9]/.test(password), + message: "1 number", + }, + hasSpecialChar: { + matcher: (password: string) => + /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(password), + message: "1 special character", + }, +} + +export type PasswordValidatorKey = keyof typeof passwordValidators + +export const passwordValidator = (msg = "Required field") => + z + .string() + .min(1, msg) + .refine(passwordValidators.length.matcher, { + message: passwordValidators.length.message, + }) + .refine(passwordValidators.hasUppercase.matcher, { + message: passwordValidators.hasUppercase.message, + }) + .refine(passwordValidators.hasLowercase.matcher, { + message: passwordValidators.hasLowercase.message, + }) + .refine(passwordValidators.hasNumber.matcher, { + message: passwordValidators.hasNumber.message, + }) + .refine(passwordValidators.hasSpecialChar.matcher, { + message: passwordValidators.hasSpecialChar.message, + }) From da95c32a0dbbb85b56576a177ad2efb72c078fd9 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Tue, 10 Sep 2024 16:52:12 +0200 Subject: [PATCH 03/26] fix(SW-360): Solved issue with date field not using placeholder values --- components/TempDesignSystem/Form/Date/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index 25caf72f3..7b9dbc083 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -60,7 +60,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { * date, but we can't check isNan since * we recieve the date as "1999-01-01" */ - dateValue = parseDate(d) + dateValue = d ? parseDate(d) : null } catch (error) { console.error(error) } @@ -100,7 +100,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { placeholder="DD" required tabIndex={3} - value={segment.value} + value={segment.isPlaceholder ? undefined : segment.value} /> ) @@ -116,7 +116,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { placeholder="MM" required tabIndex={2} - value={segment.value} + value={segment.isPlaceholder ? undefined : segment.value} /> ) @@ -132,7 +132,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { placeholder="YYYY" required tabIndex={1} - value={segment.value} + value={segment.isPlaceholder ? undefined : segment.value} /> ) From c69e4b4b297f23cd7aaf589f1fd530f293ce323d Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 11 Sep 2024 16:23:54 +0200 Subject: [PATCH 04/26] fix(SW-360): Changed how DateSelect sets date, because it was working properly --- .../TempDesignSystem/Form/Date/index.tsx | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index 7b9dbc083..4fe680bf6 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -1,5 +1,6 @@ "use client" import { parseDate } from "@internationalized/date" +import { useState } from "react" import { DateInput, DatePicker, Group } from "react-aria-components" import { useController, useFormContext, useWatch } from "react-hook-form" import { useIntl } from "react-intl" @@ -18,7 +19,7 @@ import type { Key } from "react-aria-components" import type { DateProps } from "./date" export default function DateSelect({ name, registerOptions = {} }: DateProps) { - const { formatMessage } = useIntl() + const intl = useIntl() const d = useWatch({ name }) const { control, setValue } = useFormContext() const { field } = useController({ @@ -26,6 +27,17 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { name, rules: registerOptions, }) + + const [dateSegments, setDateSegment] = useState<{ + year: number | null + month: number | null + date: number | null + }>({ + year: null, + month: null, + date: null, + }) + const currentYear = new Date().getFullYear() const months = rangeArray(1, 12).map((month) => ({ value: month, @@ -41,17 +53,25 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { * must subtract by 1 to get the selected month */ return (select: Key) => { - if (selector === DateName.month) { - select = Number(select) - 1 + const value = + selector === DateName.month ? Number(select) - 1 : Number(select) + const newSegments = { ...dateSegments, [selector]: value } + + if (Object.values(newSegments).every((val) => val !== null)) { + const newDate = dt(new Date()) + .set("year", newSegments.year!) + .set("month", newSegments.month!) + .set("day", newSegments.date!) + + setValue(name, newDate.format("YYYY-MM-DD")) } - const newDate = dt(d).set(selector, Number(select)) - setValue(name, newDate.format("YYYY-MM-DD")) + setDateSegment(newSegments) } } - const dayLabel = formatMessage({ id: "Day" }) - const monthLabel = formatMessage({ id: "Month" }) - const yearLabel = formatMessage({ id: "Year" }) + const dayLabel = intl.formatMessage({ id: "Day" }) + const monthLabel = intl.formatMessage({ id: "Month" }) + const yearLabel = intl.formatMessage({ id: "Year" }) let dateValue = null try { @@ -60,15 +80,14 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { * date, but we can't check isNan since * we recieve the date as "1999-01-01" */ - dateValue = d ? parseDate(d) : null + dateValue = dt(d).isValid() ? parseDate(d) : null } catch (error) { console.error(error) } return ( Date: Wed, 11 Sep 2024 17:10:57 +0200 Subject: [PATCH 05/26] feat(SW-360): Added register form with server action --- actions/registerUser.ts | 82 ++++++++++++ components/Forms/Register/form.module.css | 53 ++++++++ components/Forms/Register/index.tsx | 156 ++++++++++++++++++++++ components/Forms/Register/schema.ts | 35 +++++ i18n/dictionaries/da.json | 5 + i18n/dictionaries/de.json | 6 + i18n/dictionaries/en.json | 4 + i18n/dictionaries/fi.json | 6 +- i18n/dictionaries/no.json | 4 + i18n/dictionaries/sv.json | 4 + server/trpc.ts | 28 ++++ 11 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 actions/registerUser.ts create mode 100644 components/Forms/Register/form.module.css create mode 100644 components/Forms/Register/index.tsx create mode 100644 components/Forms/Register/schema.ts diff --git a/actions/registerUser.ts b/actions/registerUser.ts new file mode 100644 index 000000000..3bca2da81 --- /dev/null +++ b/actions/registerUser.ts @@ -0,0 +1,82 @@ +"use server" + +import { z } from "zod" + +import * as api from "@/lib/api" +import { profileServiceServerActionProcedure } from "@/server/trpc" + +import { registerSchema } from "@/components/Forms/Register/schema" +import { passwordValidator } from "@/utils/passwordValidator" +import { phoneValidator } from "@/utils/phoneValidator" + +const registerUserPayload = z.object({ + language: z.string(), + firstName: z.string(), + lastName: z.string(), + email: z.string(), + phoneNumber: phoneValidator("Phone is required"), + dateOfBirth: z.string(), + address: z.object({ + city: z.string().default(""), + country: z.string().default(""), + countryCode: z.string().default(""), + zipCode: z.string().default(""), + streetAddress: z.string().default(""), + }), + password: passwordValidator("Password is required"), +}) + +export const registerUser = profileServiceServerActionProcedure + .input(registerSchema) + .mutation(async function ({ ctx, input }) { + const payload = { + ...input, + language: ctx.lang, + phoneNumber: input.phoneNumber.replace(/\s+/g, ""), + } + + const parsedPayload = registerUserPayload.safeParse(payload) + if (!parsedPayload.success) { + console.error( + "registerUser payload validation error", + JSON.stringify({ + query: input, + error: parsedPayload.error, + }) + ) + + return false + } + + try { + const apiResponse = await api.post(api.endpoints.v1.profile, { + body: parsedPayload.data, + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + }) + + if (!apiResponse.ok) { + const text = apiResponse.text() + console.error( + "registerUser api error", + JSON.stringify({ + query: input, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + error: text, + }, + }) + ) + return false + } + + const json = await apiResponse.json() + console.log("json", json) + + return true + } catch (error) { + return false + } + }) diff --git a/components/Forms/Register/form.module.css b/components/Forms/Register/form.module.css new file mode 100644 index 000000000..59358dc28 --- /dev/null +++ b/components/Forms/Register/form.module.css @@ -0,0 +1,53 @@ +.container { + display: grid; + gap: var(--Spacing-x3); +} + +.title { + grid-area: title; +} + +.form { + display: grid; + gap: var(--Spacing-x5); + grid-area: form; +} + +.btnContainer { + display: flex; + flex-direction: column-reverse; + gap: var(--Spacing-x1); + grid-area: buttons; +} + +@media screen and (min-width: 768px) { + .form { + grid-template-columns: 1fr; + } + + .btnContainer { + align-self: center; + flex-direction: row; + gap: var(--Spacing-x2); + justify-self: flex-end; + } +} + +.password, +.user, +.terms { + align-self: flex-start; + display: grid; + gap: var(--Spacing-x2); +} + +.container { + display: grid; + gap: var(--Spacing-x2); +} + +@media (min-width: 768px) { + .divider { + display: none; + } +} diff --git a/components/Forms/Register/index.tsx b/components/Forms/Register/index.tsx new file mode 100644 index 000000000..9494e59a7 --- /dev/null +++ b/components/Forms/Register/index.tsx @@ -0,0 +1,156 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { FormProvider, useForm } from "react-hook-form" +import { useIntl } from "react-intl" + +import { registerUser } from "@/actions/registerUser" +import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" +import CountrySelect from "@/components/TempDesignSystem/Form/Country" +import DateSelect from "@/components/TempDesignSystem/Form/Date" +import Input from "@/components/TempDesignSystem/Form/Input" +import NewPassword from "@/components/TempDesignSystem/Form/NewPassword" +import Phone from "@/components/TempDesignSystem/Form/Phone" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import { toast } from "@/components/TempDesignSystem/Toasts" + +import { RegisterSchema, registerSchema } from "./schema" + +import styles from "./form.module.css" + +export default function Form() { + const intl = useIntl() + const methods = useForm({ + defaultValues: { + firstName: "", + lastName: "", + email: "", + phoneNumber: "", + dateOfBirth: "", + address: { + countryCode: "", + zipCode: "", + }, + password: "", + termsAccepted: false, + }, + mode: "all", + criteriaMode: "all", + resolver: zodResolver(registerSchema), + reValidateMode: "onChange", + }) + const country = intl.formatMessage({ id: "Country" }) + const email = `${intl.formatMessage({ id: "Email" })} ${intl.formatMessage({ id: "Address" }).toLowerCase()}` + const phoneNumber = intl.formatMessage({ id: "Phone number" }) + const zipCode = intl.formatMessage({ id: "Zip code" }) + + async function handleSubmit(data: RegisterSchema) { + console.log("submit", data) + const isSuccessResponse = await registerUser(data) + if (!isSuccessResponse) { + toast.error("Something went wrong!") + } else { + // TODO: Toast should be removed and we should show a different page + toast.success("Form submitted successfully") + // should we navigate to sub route like /signup/success ? + // router.push("/") + + methods.reset() + } + } + + return ( +
+ +
+
+
+ + +
+ +
+ + +
+ + +
+ +
+
+ + {intl.formatMessage({ id: "Password" })} + +
+ +
+
+
+ + {intl.formatMessage({ id: "Terms and conditions" })} + +
+ + + {intl.formatMessage({ + id: "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", + })}{" "} + + {intl.formatMessage({ id: "Scandic's Privacy Policy." })} + + + +
+ + +
+
+ ) +} diff --git a/components/Forms/Register/schema.ts b/components/Forms/Register/schema.ts new file mode 100644 index 000000000..982641d2c --- /dev/null +++ b/components/Forms/Register/schema.ts @@ -0,0 +1,35 @@ +import { z } from "zod" + +import { passwordValidator } from "@/utils/passwordValidator" +import { phoneValidator } from "@/utils/phoneValidator" + +export const registerSchema = z.object({ + firstName: z + .string() + .max(250) + .refine((value) => value.trim().length > 0, { + message: "First name is required", + }), + lastName: z + .string() + .max(250) + .refine((value) => value.trim().length > 0, { + message: "Last name is required", + }), + email: z.string().max(250).email(), + phoneNumber: phoneValidator( + "Phone is required", + "Please enter a valid phone number" + ), + dateOfBirth: z.string().min(1), + address: z.object({ + countryCode: z.string(), + zipCode: z.string().min(1), + }), + password: passwordValidator("Password is required"), + termsAccepted: z.boolean().refine((value) => value === true, { + message: "You must accept the terms and conditions", + }), +}) + +export type RegisterSchema = z.infer diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index 57d493bf9..3f30b3f79 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -193,6 +193,7 @@ "Phone number": "Telefonnummer", "Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer", "Points": "Point", + "points": "Point", "Points being calculated": "Point udregnes", "Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021", "Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.", @@ -222,6 +223,7 @@ "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Search": "Søge", + "Scandic's Privacy Policy.": "Scandic's integritetspolicy.", "See all photos": "Se alle billeder", "See hotel details": "Se hoteloplysninger", "See room details": "Se værelsesdetaljer", @@ -241,6 +243,7 @@ "Show map": "Vis kort", "Show more": "Vis mere", "Sign up bonus": "Velkomstbonus", + "Sign up to Scandic Friends": "Tilmeld dig Scandic Friends", "Skip to main content": "Spring over og gå til hovedindhold", "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.", @@ -252,6 +255,7 @@ "Summary": "Opsummering", "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.", + "Terms and conditions": "Vilkår og betingelser", "Thank you": "Tak", "Theatre": "Teater", "There are no transactions to display": "Der er ingen transaktioner at vise", @@ -286,6 +290,7 @@ "Which room class suits you the best?": "Hvilken rumklasse passer bedst til dig", "Year": "År", "Yes, discard changes": "Ja, kasser ændringer", + "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jeg accepterer vilkårene for Scandic Friends og forstår, at Scandic vil behandle mine personlige oplysninger i henhold til", "Yes, remove my card": "Ja, fjern mit kort", "You can always change your mind later and add breakfast at the hotel.": "Du kan altid ombestemme dig senere og tilføje morgenmad på hotellet.", "You canceled adding a new credit card.": "Du har annulleret tilføjelsen af et nyt kreditkort.", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index ffb0f018b..6dbe84117 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -193,6 +193,7 @@ "Phone number": "Telefonnummer", "Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein", "Points": "Punkte", + "points": "Punkte", "Points being calculated": "Punkte werden berechnet", "Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021", "Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.", @@ -222,6 +223,7 @@ "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Search": "Suchen", + "Scandic's Privacy Policy.": "Scandics Datenschutzrichtlinie.", "See all photos": "Alle Fotos ansehen", "See hotel details": "Hotelinformationen ansehen", "See room details": "Zimmerdetails ansehen", @@ -241,6 +243,7 @@ "Show map": "Karte anzeigen", "Show more": "Mehr anzeigen", "Sign up bonus": "Anmelde-Bonus", + "Sign up to Scandic Friends": "Treten Sie Scandic Friends bei", "Skip to main content": "Direkt zum Inhalt", "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.", @@ -252,6 +255,7 @@ "Summary": "Zusammenfassung", "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.", + "Terms and conditions": "Geschäftsbedingungen", "Thank you": "Danke", "Theatre": "Theater", "There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden", @@ -286,6 +290,7 @@ "Which room class suits you the best?": "Welche Zimmerklasse passt am besten zu Ihnen?", "Year": "Jahr", "Yes, discard changes": "Ja, Änderungen verwerfen", + "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, ich akzeptiere die Geschäftsbedingungen für Scandic Friends und erkenne an, dass Scandic meine persönlichen Daten in Übereinstimmung mit", "Yes, remove my card": "Ja, meine Karte entfernen", "You can always change your mind later and add breakfast at the hotel.": "Sie können es sich später jederzeit anders überlegen und das Frühstück im Hotel hinzufügen.", "You canceled adding a new credit card.": "Sie haben das Hinzufügen einer neuen Kreditkarte abgebrochen.", @@ -327,3 +332,4 @@ "{difference}{amount} {currency}": "{difference}{amount} {currency}", "{width} cm × {length} cm": "{width} cm × {length} cm" } + diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index fd989d49d..16e62c357 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -221,6 +221,7 @@ "Save": "Save", "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", + "Scandic's Privacy Policy.": "Scandic's Privacy Policy.", "Search": "Search", "See all photos": "See all photos", "See hotel details": "See hotel details", @@ -241,6 +242,7 @@ "Show map": "Show map", "Show more": "Show more", "Sign up bonus": "Sign up bonus", + "Sign up to Scandic Friends": "Sign up to Scandic Friends", "Skip to main content": "Skip to main content", "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.", @@ -252,6 +254,7 @@ "Summary": "Summary", "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.", + "Terms and conditions": "Terms and conditions", "Thank you": "Thank you", "Theatre": "Theatre", "There are no transactions to display": "There are no transactions to display", @@ -286,6 +289,7 @@ "Which room class suits you the best?": "Which room class suits you the best?", "Year": "Year", "Yes, discard changes": "Yes, discard changes", + "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with", "Yes, remove my card": "Yes, remove my card", "You can always change your mind later and add breakfast at the hotel.": "You can always change your mind later and add breakfast at the hotel.", "You canceled adding a new credit card.": "You canceled adding a new credit card.", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 716a58765..0d6e9e617 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -193,6 +193,7 @@ "Phone number": "Puhelinnumero", "Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero", "Points": "Pisteet", + "points": "pistettä", "Points being calculated": "Pisteitä lasketaan", "Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021", "Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.", @@ -223,6 +224,7 @@ "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Search": "Haku", + "Scandic's Privacy Policy.": "Scandicin tietosuojavalmioksi.", "See all photos": "Katso kaikki kuvat", "See hotel details": "Katso hotellin tiedot", "See room details": "Katso huoneen tiedot", @@ -242,6 +244,7 @@ "Show map": "Näytä kartta", "Show more": "Näytä lisää", "Sign up bonus": "Liittymisbonus", + "Sign up to Scandic Friends": "Liity Scandic Friends -jäseneksi", "Skip to main content": "Siirry pääsisältöön", "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.", @@ -253,6 +256,7 @@ "Summary": "Yhteenveto", "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.", + "Terms and conditions": "Käyttöehdot", "Thank you": "Kiitos", "Theatre": "Teatteri", "There are no transactions to display": "Näytettäviä tapahtumia ei ole", @@ -287,6 +291,7 @@ "Which room class suits you the best?": "Mikä huoneluokka sopii sinulle parhaiten?", "Year": "Vuosi", "Yes, discard changes": "Kyllä, hylkää muutokset", + "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Kyllä, hyväksyn Scandic Friends -käyttöehdot ja ymmärrän, että Scandic käsittelee minun henkilötietoni asianmukaisesti", "Yes, remove my card": "Kyllä, poista korttini", "You can always change your mind later and add breakfast at the hotel.": "Voit aina muuttaa mieltäsi myöhemmin ja lisätä aamiaisen hotelliin.", "You canceled adding a new credit card.": "Peruutit uuden luottokortin lisäämisen.", @@ -319,7 +324,6 @@ "nights": "yötä", "number": "määrä", "or": "tai", - "points": "pistettä", "special character": "erikoishahmo", "spendable points expiring by": "{points} pistettä vanhenee {date} mennessä", "to": "to", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index d422258a6..a44ecc1e3 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -221,6 +221,7 @@ "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Search": "Søk", + "Scandic's Privacy Policy.": "Scandics integritetspolicy.", "See all photos": "Se alle bilder", "See hotel details": "Se hotellinformasjon", "See room details": "Se detaljer om rommet", @@ -240,6 +241,7 @@ "Show map": "Vis kart", "Show more": "Vis mer", "Sign up bonus": "Velkomstbonus", + "Sign up to Scandic Friends": "Bli med i Scandic Friends", "Skip to main content": "Gå videre til hovedsiden", "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.", @@ -251,6 +253,7 @@ "Summary": "Sammendrag", "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.", + "Terms and conditions": "Vilkår og betingelser", "Thank you": "Takk", "Theatre": "Teater", "There are no transactions to display": "Det er ingen transaksjoner å vise", @@ -285,6 +288,7 @@ "Which room class suits you the best?": "Hvilken romklasse passer deg best?", "Year": "År", "Yes, discard changes": "Ja, forkast endringer", + "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jeg aksepterer vilkårene for Scandic Friends og forstår at Scandic vil behandle mine personlige opplysninger i henhold til", "Yes, remove my card": "Ja, fjern kortet mitt", "You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ombestemme deg senere og legge til frokost på hotellet.", "You canceled adding a new credit card.": "Du kansellerte å legge til et nytt kredittkort.", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 51543e59c..5f72ef6f7 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -222,6 +222,7 @@ "Scandic Friends Mastercard": "Scandic Friends Mastercard", "Scandic Friends Point Shop": "Scandic Friends Point Shop", "Search": "Sök", + "Scandic's Privacy Policy.": "Scandics integritetspolicy.", "See all photos": "Se alla foton", "See hotel details": "Se hotellinformation", "See room details": "Se rumsdetaljer", @@ -241,6 +242,7 @@ "Show map": "Visa karta", "Show more": "Visa mer", "Sign up bonus": "Välkomstbonus", + "Sign up to Scandic Friends": "Bli medlem i Scandic Friends", "Skip to main content": "Fortsätt till huvudinnehåll", "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.", @@ -252,6 +254,7 @@ "Summary": "Sammanfattning", "TUI Points": "TUI Points", "Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.", + "Terms and conditions": "Allmänna villkor", "Thank you": "Tack", "Theatre": "Teater", "There are no transactions to display": "Det finns inga transaktioner att visa", @@ -286,6 +289,7 @@ "Which room class suits you the best?": "Vilken rumsklass passar dig bäst?", "Year": "År", "Yes, discard changes": "Ja, ignorera ändringar", + "Yes, I accept the Terms and conditions for Scandic Friends and understand that Scandic will process my personal data in accordance with": "Ja, jag accepterar villkoren för Scandic Friends och förstår att Scandic kommer att bearbeta mina personliga uppgifter i enlighet med", "Yes, remove my card": "Ja, ta bort mitt kort", "You can always change your mind later and add breakfast at the hotel.": "Du kan alltid ändra dig senare och lägga till frukost på hotellet.", "You canceled adding a new credit card.": "Du avbröt att lägga till ett nytt kreditkort.", diff --git a/server/trpc.ts b/server/trpc.ts index dafd26690..1000d053c 100644 --- a/server/trpc.ts +++ b/server/trpc.ts @@ -140,6 +140,34 @@ export const serverActionProcedure = t.procedure.experimental_caller( }) ) +export const hotelServiceServerActionProcedure = serverActionProcedure.use( + async (opts) => { + const { access_token } = await fetchServiceToken("hotel") + if (!access_token) { + throw internalServerError("Failed to obtain service token") + } + return opts.next({ + ctx: { + serviceToken: access_token, + }, + }) + } +) + +export const profileServiceServerActionProcedure = serverActionProcedure.use( + async (opts) => { + const { access_token } = await fetchServiceToken("profile") + if (!access_token) { + throw internalServerError("Failed to obtain service token") + } + return opts.next({ + ctx: { + serviceToken: access_token, + }, + }) + } +) + export const protectedServerActionProcedure = serverActionProcedure.use( async (opts) => { const session = await opts.ctx.auth() From c22fa9fa8c72ec8821934acc490c7f58e43dfe3c Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Thu, 12 Sep 2024 16:05:22 +0200 Subject: [PATCH 06/26] fix(SW-360): Added test ids to form fields --- components/Forms/Register/index.tsx | 1 + components/TempDesignSystem/Form/Checkbox/index.tsx | 1 + components/TempDesignSystem/Form/Country/index.tsx | 1 + components/TempDesignSystem/Form/Date/index.tsx | 13 ++++++++++--- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/components/Forms/Register/index.tsx b/components/Forms/Register/index.tsx index 9494e59a7..28d10a4f6 100644 --- a/components/Forms/Register/index.tsx +++ b/components/Forms/Register/index.tsx @@ -146,6 +146,7 @@ export default function Form() { type="submit" intent="primary" disabled={methods.formState.isSubmitting} + data-testid="submit" > {intl.formatMessage({ id: "Sign up to Scandic Friends" })} diff --git a/components/TempDesignSystem/Form/Checkbox/index.tsx b/components/TempDesignSystem/Form/Checkbox/index.tsx index 339dff676..8b2885ebc 100644 --- a/components/TempDesignSystem/Form/Checkbox/index.tsx +++ b/components/TempDesignSystem/Form/Checkbox/index.tsx @@ -26,6 +26,7 @@ export default function Checkbox({ className={styles.container} isSelected={field.value} onChange={field.onChange} + data-testid={name} > {({ isSelected }) => ( <> diff --git a/components/TempDesignSystem/Form/Country/index.tsx b/components/TempDesignSystem/Form/Country/index.tsx index 9777a4a48..0a5406640 100644 --- a/components/TempDesignSystem/Form/Country/index.tsx +++ b/components/TempDesignSystem/Form/Country/index.tsx @@ -68,6 +68,7 @@ export default function CountrySelect({ onSelectionChange={handleChange} ref={field.ref} selectedKey={field.value} + data-testid={name} >
) @@ -135,7 +138,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { placeholder="MM" required tabIndex={2} - value={segment.isPlaceholder ? undefined : segment.value} + defaultValue={ + segment.isPlaceholder ? undefined : segment.value + } /> ) @@ -151,7 +156,9 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { placeholder="YYYY" required tabIndex={1} - value={segment.isPlaceholder ? undefined : segment.value} + defaultValue={ + segment.isPlaceholder ? undefined : segment.value + } /> ) From 661effeefa2c7c714b0c46aeffb515d46b83ed91 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Thu, 12 Sep 2024 16:08:03 +0200 Subject: [PATCH 07/26] feat(SW-360): Added test for register user form --- components/Forms/Register/index.tsx | 6 -- components/Forms/Register/register.test.tsx | 68 +++++++++++++++++++++ jest.config.ts | 2 +- jest.setup.ts | 11 ++++ package-lock.json | 14 +++++ package.json | 1 + 6 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 components/Forms/Register/register.test.tsx diff --git a/components/Forms/Register/index.tsx b/components/Forms/Register/index.tsx index 28d10a4f6..0a39c8ec2 100644 --- a/components/Forms/Register/index.tsx +++ b/components/Forms/Register/index.tsx @@ -48,7 +48,6 @@ export default function Form() { const zipCode = intl.formatMessage({ id: "Zip code" }) async function handleSubmit(data: RegisterSchema) { - console.log("submit", data) const isSuccessResponse = await registerUser(data) if (!isSuccessResponse) { toast.error("Something went wrong!") @@ -66,11 +65,6 @@ export default function Form() {
({ + registerUser: jest.fn().mockResolvedValue(true), +})) + +describe("Register user form", () => { + test("Should submit form with correct data", async () => { + render() + + const values = { + firstName: "John", + lastName: "Doe", + dateOfBirth: "1990-01-08", + address: { + zipCode: "11111", + countryCode: "SE", + }, + email: "john.doe@example.com", + phoneNumber: "+46123456789", + password: "securePassword123!", + termsAccepted: true, + } + + await userEvent.type(screen.getByTestId("firstName"), values.firstName) + await userEvent.type(screen.getByTestId("lastName"), values.lastName) + + const dateOfBirthInput = screen.getByTestId("dateOfBirth") + const daySelect = dateOfBirthInput.querySelector("select[name='date']")! + const monthSelect = dateOfBirthInput.querySelector("select[name='month']")! + const yearSelect = dateOfBirthInput.querySelector("select[name='year']")! + + await userEvent.selectOptions(daySelect, "1") + await userEvent.selectOptions(monthSelect, "1") + await userEvent.selectOptions(yearSelect, "1990") + + await userEvent.type( + screen.getByTestId("address.zipCode"), + values.address.zipCode + ) + + const countryInput = screen.getByLabelText("Select a country") + await userEvent.click(countryInput) + await userEvent.type(countryInput, "Sweden") + await userEvent.keyboard("{ArrowDown}{Enter}") + + await userEvent.type(screen.getByTestId("email"), values.email) + await userEvent.type( + screen.getByTestId("phoneNumber"), + values.phoneNumber.slice(3) + ) + await userEvent.type(screen.getByTestId("password"), values.password) + await userEvent.click(screen.getAllByTestId("termsAccepted")[0]) + + await userEvent.click(screen.getByTestId("submit")) + + await waitFor(() => { + expect(registerUser).toHaveBeenCalledWith(values) + }) + }) +}) diff --git a/jest.config.ts b/jest.config.ts index 28055b8c6..137b2d260 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -154,7 +154,7 @@ const config: Config = { // snapshotSerializers: [], // The test environment that will be used for testing - // testEnvironment: "jest-environment-node", + testEnvironment: "jsdom", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, diff --git a/jest.setup.ts b/jest.setup.ts index df6631eeb..d175d7187 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1 +1,12 @@ import "@testing-library/jest-dom" + +jest.mock("react-intl", () => ({ + useIntl: () => ({ + formatMessage: (message: { id: string }) => message.id, + }), +})) + +jest.mock("next/navigation", () => ({ + useRouter: jest.fn(), + usePathname: jest.fn().mockReturnValue("/"), +})) diff --git a/package-lock.json b/package-lock.json index c12152ff6..db6a1ca42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,7 @@ "@svgr/webpack": "^8.1.0", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", "@types/node": "^20", "@types/react": "^18", @@ -6167,6 +6168,19 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", diff --git a/package.json b/package.json index ab79709d3..669ab58da 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@svgr/webpack": "^8.1.0", "@testing-library/jest-dom": "^6.4.6", "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", "@types/jest": "^29.5.12", "@types/node": "^20", "@types/react": "^18", From 4c1dca0ce841d5fd053ce11504291fa34145dac7 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Mon, 16 Sep 2024 13:27:35 +0200 Subject: [PATCH 08/26] fix(SW-360): Added test for edit profile form --- .../Forms/Edit/Profile/editProfile.test.tsx | 111 ++++++++++++++++++ components/Forms/Register/index.tsx | 5 + components/Forms/Register/register.test.tsx | 2 +- .../TempDesignSystem/Form/Date/index.tsx | 2 +- .../TempDesignSystem/Form/Phone/index.tsx | 1 + .../TempDesignSystem/Form/Select/index.tsx | 2 + components/TempDesignSystem/Select/index.tsx | 3 +- jest.config.ts | 2 +- jest.setup.ts | 7 ++ 9 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 components/Forms/Edit/Profile/editProfile.test.tsx diff --git a/components/Forms/Edit/Profile/editProfile.test.tsx b/components/Forms/Edit/Profile/editProfile.test.tsx new file mode 100644 index 000000000..9cbb2e2b3 --- /dev/null +++ b/components/Forms/Edit/Profile/editProfile.test.tsx @@ -0,0 +1,111 @@ +import { expect } from "@jest/globals" +import { render, screen, waitFor } from "@testing-library/react" +import { userEvent } from "@testing-library/user-event" + +import { editProfile } from "@/actions/editProfile" + +import Form from "." + +import { type User } from "@/types/user" + +jest.mock("@/actions/editProfile", () => ({ + editProfile: jest.fn().mockResolvedValue({ status: "" }), +})) + +describe("EditProfile", () => { + it("Should submit form with correct data", async () => { + const user: User = { + email: "test@test.com", + name: "Test User", + phoneNumber: "+4612345678", + dateOfBirth: "1990-01-08", + address: { + city: "Test City", + countryCode: "SE", + streetAddress: "Test Street", + zipCode: "12345", + }, + journeys: [], + nights: 0, + shortcuts: [], + victories: [], + language: "en", + firstName: "Test", + lastName: "User", + memberships: [], + profileId: "1", + } + + render() + + const newValues = { + email: "new@test.com", + dateOfBirth: "2000-01-01", + address: { + city: "New City", + countryCode: "SE", + streetAddress: "New Street", + zipCode: "67890", + }, + phoneNumber: "+469999999", + language: "No", + password: "oldPassword123!", + newPassword: "newPassword123!", + retypeNewPassword: "newPassword123!", + } + + const dateOfBirthInput = screen.getByTestId("dateOfBirth") + const daySelect = dateOfBirthInput.querySelector("select[name='date']")! + const monthSelect = dateOfBirthInput.querySelector("select[name='month']")! + const yearSelect = dateOfBirthInput.querySelector("select[name='year']")! + + await userEvent.selectOptions(daySelect, "1") + await userEvent.selectOptions(monthSelect, "1") + await userEvent.selectOptions(yearSelect, "2000") + + const streetAddressInput = screen.getByTestId("address.streetAddress") + await userEvent.clear(streetAddressInput) + await userEvent.type(streetAddressInput, newValues.address.streetAddress) + + const cityInput = screen.getByTestId("address.city") + await userEvent.clear(cityInput) + await userEvent.type(cityInput, newValues.address.city) + + const zipCodeInput = screen.getByTestId("address.zipCode") + await userEvent.clear(zipCodeInput) + await userEvent.type(zipCodeInput, newValues.address.zipCode) + + const countryInput = screen.getByLabelText("Select a country") + await userEvent.click(countryInput) + await userEvent.type(countryInput, "Sweden") + await userEvent.keyboard("{ArrowDown}{Enter}") + + const emailInput = screen.getByTestId("email") + await userEvent.clear(emailInput) + await userEvent.type(emailInput, newValues.email) + + const phoneNumberInput = screen.getByTestId("phoneNumber") + await userEvent.clear(phoneNumberInput) + await userEvent.type(phoneNumberInput, newValues.phoneNumber.slice(3)) // Remove country code + + const languageInput = screen.getByTestId("language") + await userEvent.click(languageInput) + await userEvent.click(screen.getByTestId("Norwegian")) + + await userEvent.type(screen.getByTestId("password"), newValues.password) + await userEvent.type( + screen.getByTestId("newPassword"), + newValues.newPassword + ) + await userEvent.type( + screen.getByTestId("retypeNewPassword"), + newValues.retypeNewPassword + ) + + await userEvent.click(screen.getByText("Save")) + + await waitFor(() => { + expect(editProfile).toHaveBeenCalledWith(newValues) + }) + }) +}) diff --git a/components/Forms/Register/index.tsx b/components/Forms/Register/index.tsx index 0a39c8ec2..319a0483a 100644 --- a/components/Forms/Register/index.tsx +++ b/components/Forms/Register/index.tsx @@ -67,6 +67,11 @@ export default function Form() {
diff --git a/components/Forms/Register/register.test.tsx b/components/Forms/Register/register.test.tsx index c83f63c59..e9188abff 100644 --- a/components/Forms/Register/register.test.tsx +++ b/components/Forms/Register/register.test.tsx @@ -18,7 +18,7 @@ describe("Register user form", () => { const values = { firstName: "John", lastName: "Doe", - dateOfBirth: "1990-01-08", + dateOfBirth: "1990-01-01", address: { zipCode: "11111", countryCode: "SE", diff --git a/components/TempDesignSystem/Form/Date/index.tsx b/components/TempDesignSystem/Form/Date/index.tsx index 63456c3e3..311650fc2 100644 --- a/components/TempDesignSystem/Form/Date/index.tsx +++ b/components/TempDesignSystem/Form/Date/index.tsx @@ -61,7 +61,7 @@ export default function DateSelect({ name, registerOptions = {} }: DateProps) { const newDate = dt(new Date()) .set("year", newSegments.year!) .set("month", newSegments.month!) - .set("day", newSegments.date!) + .set("date", newSegments.date!) setValue(name, newDate.format("YYYY-MM-DD")) } diff --git a/components/TempDesignSystem/Form/Phone/index.tsx b/components/TempDesignSystem/Form/Phone/index.tsx index 6e9c2024b..d09598296 100644 --- a/components/TempDesignSystem/Form/Phone/index.tsx +++ b/components/TempDesignSystem/Form/Phone/index.tsx @@ -85,6 +85,7 @@ export default function Phone({ className={styles.select} tabIndex={0} type="button" + data-testid="country-selector" >