diff --git a/src/components/ConfirmRegistrationModal.tsx b/src/components/ConfirmRegistrationModal.tsx new file mode 100644 index 0000000..85a073b --- /dev/null +++ b/src/components/ConfirmRegistrationModal.tsx @@ -0,0 +1,52 @@ +import { Col, Container, Modal, Row } from "react-bootstrap" +import { BrowserViewConfirmRegistration } from "../constants" +import { useBrowserStore } from "../store" +import Alert from "./Alert" + +const ConfirmRegistrationModal = () => { + const view = useBrowserStore((state) => state.view) + const setView = useBrowserStore((state) => state.setView) + return ( + setView(null)} + > + + Registration almost completed... + + + + + +

+ Thank you for completing the first step of the registration. +

+ Action required. + +

+ Next, please download this{" "} + + Non-Disclosure Agreement (NDA) + + , sign it and email it to{" "} + + info@impresso-project.ch + + .
+
+ Once we have received the signed NDA, your account will be + activated within two working days. +

+ +
+
+
+
+ ) +} + +export default ConfirmRegistrationModal diff --git a/src/components/ErrorManager.tsx b/src/components/ErrorManager.tsx new file mode 100644 index 0000000..19b69a2 --- /dev/null +++ b/src/components/ErrorManager.tsx @@ -0,0 +1,42 @@ +import { + BadRequest, + NotAuthenticated, + type FeathersError, +} from "@feathersjs/errors" + +type ErrorManagerProps = { + error?: FeathersError | Error | null +} +export type BadRequestData = { key?: string; message: string; label?: string } + +const ErrorManager: React.FC = ({ error }) => { + let errorMessages: BadRequestData[] = [] + + if (error instanceof BadRequest && error.data) { + errorMessages = Object.keys(error.data).map((key) => { + return { + key, + message: error.data[key].message, + label: error.data[key].label, + } + }) + } else if (error instanceof NotAuthenticated) { + errorMessages = [{ key: "Error", message: error.message }] + } else if (error instanceof Error) { + errorMessages = [{ key: "Error", message: error.message }] + } + return errorMessages.length > 0 ? ( +
+ +
+ ) : null +} + +export default ErrorManager diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index be3d20a..68dfb5f 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -7,6 +7,7 @@ import React, { useRef } from "react" import { Form } from "react-bootstrap" import { useBrowserStore } from "../store" import { BrowserViewRegister } from "../constants" +import ErrorManager from "./ErrorManager" export interface LoginFormPayload { email: string @@ -33,32 +34,11 @@ const LoginForm: React.FC = ({ } console.info("[LoginForm] @render", { error }) - let errorMessages: { key: string; message: string }[] = [] - if (error instanceof BadRequest && error.data) { - errorMessages = Object.keys(error.data).map((key) => { - return { key, message: error.data[key].message } - }) - } else if (error instanceof NotAuthenticated) { - errorMessages = [{ key: "Error", message: error.message }] - } else if (error) { - errorMessages = [{ key: "Error", message: error.message }] - } return ( <>
- {errorMessages.length > 0 ? ( -
-
    - {errorMessages.map((d, _i) => ( -
  • - {d.key}:  - {d.message} -
  • - ))} -
-
- ) : null} + Email address = ({
+
) diff --git a/src/components/RegisterForm.tsx b/src/components/RegisterForm.tsx index 35d1369..441f510 100644 --- a/src/components/RegisterForm.tsx +++ b/src/components/RegisterForm.tsx @@ -6,7 +6,12 @@ import { PlanImpressoUser, PlanStudentUser, PlanLabels, + BrowserViewTermsOfUse, } from "../constants" +import { useBrowserStore, usePersistentStore } from "../store" +import { DateTime } from "luxon" +import { BadRequest, type FeathersError } from "@feathersjs/errors" +import ErrorManager, { type BadRequestData } from "./ErrorManager" const Colors: string[] = [ "#96ceb4", @@ -49,6 +54,7 @@ const Plans = [PlanImpressoUser, PlanStudentUser, PlanAcademicUser] export interface RegisterFormPayload { email: string password: string + username: string verifyPassword: string firstname: string lastname: string @@ -58,11 +64,18 @@ export interface RegisterFormPayload { export interface RegisterFormProps { className?: string onSubmit: (payload: RegisterFormPayload) => void + error?: FeathersError | null } -const RegisterForm: React.FC = ({ className, onSubmit }) => { +const RegisterForm: React.FC = ({ + className, + onSubmit, + error, +}) => { const previewDelayTimerRef = useRef(null) - + const acceptTermsDate = usePersistentStore((state) => state.acceptTermsDate) + const setView = useBrowserStore((state) => state.setView) + const [formError, setFormError] = useState(null) const [formPreview, setFormPreview] = useState(() => ({ email: "", firstname: "-", @@ -79,6 +92,7 @@ const RegisterForm: React.FC = ({ className, onSubmit }) => { email: "", password: "", verifyPassword: "", + username: "", firstname: "-", lastname: "-", plan: PlanImpressoUser, @@ -86,7 +100,65 @@ const RegisterForm: React.FC = ({ className, onSubmit }) => { const handleOnSubmit = (e: React.FormEvent) => { e.preventDefault() // check errors + const errorsAsData: { [key: string]: BadRequestData } = {} console.info("[RegisterForm] @handleOnSubmit") + if (formPayload.current.password !== formPayload.current.verifyPassword) { + errorsAsData.verifyPassword = { + label: "Verify password", + message: 'Values of "password" and "verify password" do not match.', + } + } + // verufy password is complicated enough using a nice regex, numbers, uppercase andlowervase letter and a punctuation mark + if ( + !formPayload.current.password.match( + /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/, + ) + ) { + errorsAsData.password = { + label: "Password", + message: + "Password must contain at least 8 characters, including uppercase, lowercase, numbers and a punctuation mark.", + } + } + + // check email address is ok + if (!formPayload.current.email.match(/.+@.+\..+/)) { + errorsAsData.email = { + label: "Email", + message: "Please enter a valid email address.", + } + } + // check username is lwercase and number only, with '.' and '_' and "-" + if (!formPayload.current.username.match(/^[a-z0-9-]{8,}$/)) { + errorsAsData.username = { + label: "Username", + message: + 'Please enter a valid username that contains at least 8 characters. We accept usernames containing only lowercase letters and numbers, e.g. "johndoe84".', + } + } + // check lastname and firstname not to be empty + if (formPayload.current.firstname.trim().length < 2) { + errorsAsData.firstname = { + label: "First name", + message: "Please enter your first name.", + } + } + if (formPayload.current.lastname.trim().length < 2) { + errorsAsData.lastname = { + label: "Last name", + message: "Please enter your last name.", + } + } + if (!formPayload.current.plan) { + errorsAsData.plan = { + label: "Plan", + message: "Please select a plan.", + } + } + if (Object.keys(errorsAsData).length > 0) { + setFormError(new BadRequest("Please check your entries.", errorsAsData)) + return + } onSubmit(formPayload.current) } const changeProfileColors = (e: React.MouseEvent) => { @@ -102,14 +174,6 @@ const RegisterForm: React.FC = ({ className, onSubmit }) => { })) } - const updateAgreement = (agreedToTerms: boolean) => { - setFormPreview((state) => ({ - ...state, - agreedToTerms, - })) - console.info("[RegisterForm] @updateAgreement", agreedToTerms) - } - const updatePreview = (key: keyof RegisterFormPayload, value: string) => { formPayload.current[key] = value console.info("[RegisterForm] @updatePreview", key, value) @@ -141,6 +205,7 @@ const RegisterForm: React.FC = ({ className, onSubmit }) => { return ( +
{Plans.map((plan) => ( = ({ className, onSubmit }) => { key={plan} type="radio" label={PlanLabels[plan]} + checked={formPayload.current.plan === plan} name="plan" onChange={() => updatePreview("plan", plan)} id={`ModalRegisterForm.${plan}`} /> ))}
- - Email address - updatePreview("email", e.target.value)} - type="email" - placeholder="name@example.com" - /> - + + + + Email address + updatePreview("email", e.target.value)} + type="email" + placeholder="name@example.com" + /> + + + + {/* username */} + + Username + updatePreview("username", e.target.value)} + placeholder="your username" + /> + + + First name = ({ className, onSubmit }) => { updateAgreement(e.target.checked)} + checked={acceptTermsDate !== null} + onChange={(e) => { + if (acceptTermsDate) { + return + } + setView(BrowserViewTermsOfUse) + }} label="I agree to the terms and conditions" /> + {acceptTermsDate !== null && ( +

+ You accepted the Terms of Use
+ + {DateTime.fromISO(acceptTermsDate) + .setLocale("en-GB") + .toLocaleString(DateTime.DATETIME_FULL)} + +

+ )}
Preview:
diff --git a/src/components/RegisterModal.tsx b/src/components/RegisterModal.tsx index 253c222..15951f2 100644 --- a/src/components/RegisterModal.tsx +++ b/src/components/RegisterModal.tsx @@ -1,24 +1,43 @@ import { Modal } from "react-bootstrap" import { useBrowserStore } from "../store" -import { BrowserViewRegister } from "../constants" +import { + BrowserViewConfirmRegistration, + BrowserViewRegister, +} from "../constants" // import { useState } from "react" // import { type User } from "./UserCard" -import RegisterForm from "./RegisterForm" +import RegisterForm, { type RegisterFormPayload } from "./RegisterForm" import Link from "./Link" +import { useEffect, useState } from "react" +import type { FeathersError } from "@feathersjs/errors" +import { usersService } from "../services" const RegisterModal = () => { const view = useBrowserStore((state) => state.view) const setView = useBrowserStore((state) => state.setView) - // const [candidate, setCandidate] = useState(() => ({ - // username: "", - // isStaff: false, - // firstname: "", - // lastname: "", - // profile: { - // pattern: [], - // }, - // agreedToTerms: false, - // })) + + const [error, setError] = useState(null) + + const createUser = (payload: RegisterFormPayload) => { + usersService + .create({ + ...payload, + displayName: `${payload.firstname} ${payload.lastname}`, + }) + .then((data) => { + console.log("[RegisterModal] create", data) + // setAuthenticatedUser(data.user, data.accessToken) + setView(BrowserViewConfirmRegistration) + }) + .catch((err: FeathersError) => { + setError(err) + console.error("[RegisterModal] create", err, err.data) + }) + } + + useEffect(() => { + setError(null) + }, [view]) return ( { to check which one describes best your situation.

+ onSubmit={(payload) => { console.info("[RegisterModal] @onSubmit", payload) - } + createUser(payload) + }} + error={error} />
diff --git a/src/constants.ts b/src/constants.ts index 523f738..41cba43 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -120,6 +120,7 @@ export const PlanLabels: Record = { export const BrowserViewLogin = "login" export const BrowserViewRegister = "signup" +export const BrowserViewConfirmRegistration = "confirm-registration" export const BrowserViewTermsOfUse = "terms-of-use" export const BrowserViews: string[] = [BrowserViewLogin, BrowserViewRegister] diff --git a/src/services.tsx b/src/services.tsx index 96d1b90..d2eaf14 100644 --- a/src/services.tsx +++ b/src/services.tsx @@ -62,5 +62,6 @@ socket.on("reconnect", (attemptNumber) => { export const versionService = app.service("version") export const userService = app.service("me") +export const usersService = app.service("users") export const accountDetailsService = app.service("account-details") export const loginService = app.service("authentication") diff --git a/src/stories/components/ConfirmRegistrationModal.stories.tsx b/src/stories/components/ConfirmRegistrationModal.stories.tsx new file mode 100644 index 0000000..e92e2e0 --- /dev/null +++ b/src/stories/components/ConfirmRegistrationModal.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from "@storybook/react" +// import { fn } from "@storybook/test" +import ConfirmRegistrationModal from "../../components/ConfirmRegistrationModal" +import { useBrowserStore } from "../../store" +import { useEffect } from "react" +import { BrowserViewConfirmRegistration } from "../../constants" + +const meta: Meta = { + component: ConfirmRegistrationModal, + render: () => { + const setView = useBrowserStore((state) => state.setView) + useEffect(() => { + setView(BrowserViewConfirmRegistration) + }, []) + return ( +
+ +
+ ) + }, +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: {}, +}