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 ? (
+
+
+ {errorMessages.map((d, _i) => (
+ -
+ {d.label ?? d.key}:
+ {d.message}
+
+ ))}
+
+
+ ) : 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 (
<>
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 (
- 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: {},
+}