diff --git a/frontend/src/components/ui/Card/KVCard.tsx b/frontend/src/components/ui/Card/KVCard.tsx index e69de29..e1bffcc 100644 --- a/frontend/src/components/ui/Card/KVCard.tsx +++ b/frontend/src/components/ui/Card/KVCard.tsx @@ -0,0 +1,17 @@ +import { Icon } from "@iconify/react"; + +interface KVCardProps { + icon?: string; + myKey: string; + value: string; +} + +export const KVCard = ({icon, myKey, value}: KVCardProps) => { + return ( +
+ {icon && } +

{myKey}

+

{value}

+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/ui/TabsBar.tsx b/frontend/src/components/ui/TabsBar.tsx index e69de29..d5503a4 100644 --- a/frontend/src/components/ui/TabsBar.tsx +++ b/frontend/src/components/ui/TabsBar.tsx @@ -0,0 +1,68 @@ +import { Link, type LinkProps } from "react-router"; +import { type ReactNode } from "react"; + +type Tab = { + label: string; + value: T; + to?: LinkProps["to"]; +} + +interface TabsBarProps { + variant?: "underline" | "segmented"; + items: Tab[]; + current: T; + onChange?: (value: T) => void; + children?: ReactNode; +} + + +export const TabsBar = ({ + items, + onChange, + current, + children, + variant = "segmented" + }: TabsBarProps) => { + + let tabsClass = ""; + let tabItemClass = ""; + let tabItemActiveClass = ""; + + switch (variant) { + case 'segmented': + tabsClass = "bg-[#e5e5e5] rounded-md"; + tabItemClass = "rounded-lg px-4 py-2 md:py-3 md:px-6" + tabItemActiveClass = "bg-white"; + break; + case 'underline': + tabsClass = "pt-3 bg-white"; + tabItemClass = "pb-3 mr-3.5 text-sm text-gray-500" + tabItemActiveClass = "border-b-2 border-black text-black!" + break; + } + + return ( +
+ + + {children} +
+ ) +} \ No newline at end of file diff --git a/frontend/src/features/auth/pages/Login.tsx b/frontend/src/features/auth/pages/Login.tsx index 77e1bc9..c934a90 100644 --- a/frontend/src/features/auth/pages/Login.tsx +++ b/frontend/src/features/auth/pages/Login.tsx @@ -1,62 +1,46 @@ import HwLogo from "@components/Icon.tsx"; import HButton from "@ui/HButton.tsx"; import HInput from "@ui/HInput.tsx"; + import FormField from "@ui/Form/FormField.tsx"; import FormLabel from "@ui/Form/FormLabel.tsx"; -import { type SubmitEvent, useState } from "react"; import FormError from "@ui/Form/FormError.tsx"; -import { loginSchema } from "@utils/schema.ts"; -import { useAuth } from "@features/auth/provider/AuthProvider.tsx"; -const Login = () => { +import { useAuth } from "@features/auth/provider/AuthProvider.tsx"; +import { loginSchema } from "@utils/schema.ts"; - const { login } = useAuth(); - const [isSubmitting, setIsSubmitting] = useState(false); - const [errors, setErrorState] = useState>({}) +import { Link } from "react-router"; - async function onFormLoginSubmit(event: SubmitEvent) { - event.preventDefault(); - const formData = new FormData(event.currentTarget) - const result = loginSchema.safeParse(Object.fromEntries(formData)) +import { useUncontrolledForm } from "@utils/hooks/useUncontrolledForm.ts"; +import { FetchError } from "ofetch"; - if (!result.success) { - const errors: Record = {}; - result.error.issues.map((issue) => { - const field = issue.path[0]; - if (typeof field === "string") { - errors[field] = issue.message; - } - }) - setErrorState(errors); - return; - } +const Login = () => { - setErrorState({}); + const { login } = useAuth(); - try { - setIsSubmitting(true) - await login(result.data); - } catch (e) { - console.log(e) - } finally { - setIsSubmitting(false); + const {errors, onFormSubmit, isSubmitting} = useUncontrolledForm({ + schema: loginSchema, + onSubmit: async (values) => { + await login(values); + }, + onError: (error, {setFieldError}) => { + if (error instanceof FetchError) { + if(!error.status) setFieldError("email", "Une erreur est survenue veuillez réessayée"); + } } - } + }) return (
-
-
+
+ -
+ -

Rejoignez notre communauté - de plus de 2000 employeurs afin - de trouver le job qui vous correspond

-
+

Se connecter

@@ -78,7 +62,11 @@ const Login = () => {
- Se connecter + +
+

Vous n'avez pas de compte ? Inscrivez vous ici

+ Se connecter +
) diff --git a/frontend/src/features/auth/pages/Register.tsx b/frontend/src/features/auth/pages/Register.tsx index e69de29..a84b620 100644 --- a/frontend/src/features/auth/pages/Register.tsx +++ b/frontend/src/features/auth/pages/Register.tsx @@ -0,0 +1,86 @@ +import FormField from "@ui/Form/FormField.tsx"; +import FormLabel from "@ui/Form/FormLabel.tsx"; +import FormError from "@ui/Form/FormError.tsx"; + +import HwLogo from "@components/Icon.tsx"; +import HInput from "@ui/HInput.tsx"; +import HButton from "@ui/HButton.tsx"; + +import { Link } from "react-router"; + +import { useUncontrolledForm } from "@utils/hooks/useUncontrolledForm.ts"; +import { registerSchema } from "@utils/schema.ts"; +import { useAuth } from "@features/auth/provider/AuthProvider.tsx"; +import { FetchError } from "ofetch"; + +const Register = () => { + + const { register } = useAuth(); + + const {errors, onFormSubmit, isSubmitting} = useUncontrolledForm({ + schema: registerSchema, + onSubmit: async (values) => { + await register(values); + }, + onError: (error, {setFieldError}) => { + if (error instanceof FetchError) { + if(!error.status) setFieldError("email", "Une erreur est survenue veuillez réessayée"); + } + } + }) + + return ( +
+
+
+ + + + +

+ S'inscrire +

+ +
+

+ Rejoignez notre communauté + de plus de 2000 employeurs afin + de trouver le job qui vous correspond +

+
+
+ +
+ + Adresse email + + + + + + Mot de passe + + + + + + Mot de passe de confirmation + + + + +
+

Vous avez dêja un compte ? Connectez vous ici

+ S'inscrire +
+
+
+
+ ) +} + +export default Register \ No newline at end of file diff --git a/frontend/src/utils/hooks/useControlledForm.ts b/frontend/src/utils/hooks/useControlledForm.ts index 088505b..3228c6f 100644 --- a/frontend/src/utils/hooks/useControlledForm.ts +++ b/frontend/src/utils/hooks/useControlledForm.ts @@ -1,141 +1,186 @@ -import { z, type ZodObject } from "zod"; -import { useReducer } from "react"; - -type UseFormOptions = { - initialValues: z.infer; - schema: TSchema; - onSubmit: (values: z.infer) => Promise; - onSuccess?: (data: unknown) => void; - onError?: (error: unknown) => void; -}; - -type UseFormReducerAction = - | { - type: "SET_FIELD"; - key: keyof TSchema; - value: TSchema[keyof TSchema]; -} - | { type: "FORM_SUBMIT" } - | { type: "FORM_ERROR"; errors: Record } - | { type: "FORM_SUCCESS" } - | { type: "CLEAR_ERROR" } - -type UseFormState = { - values: TSchema, - errors: Record, - isSubmitting: boolean, -} -export const useForm = ({ - initialValues, - schema, - onSubmit, - onSuccess, - onError, - }: UseFormOptions) => { - - type Values = z.infer; - - function formReducer( - state: UseFormState, - action: UseFormReducerAction - ): UseFormState { - switch (action.type) { - case "SET_FIELD": - return { - ...state, - values: { - ...state.values, - [action.key]: action.value, - }, - }; - case "FORM_SUBMIT": - return { - ...state, - isSubmitting: true, - }; + import { z, type ZodObject } from "zod"; + import { useReducer } from "react"; + + type UseFormOptions = { + initialValues: z.infer; + schema: TSchema; + onSubmit: (values: z.infer) => Promise; + onSuccess?: (data: unknown) => void; + onError?: (error: unknown) => void; + }; - case "FORM_SUCCESS": - return { - ...state, - isSubmitting: false, - }; + type FormErrorKey = keyof T | "global"; - case "FORM_ERROR": - return { - ...state, - isSubmitting: false, - errors: action.errors, - }; + type FormErrors = { + [K in keyof T]?: string; + } & { + global?: string; + }; - case "CLEAR_ERROR": - return { - ...state, - errors: {}, - }; - } + type UseFormReducerAction = + | + { type: "SET_FIELD"; key: keyof TSchema; value: TSchema[keyof TSchema]; } + | { type: "FORM_SUBMIT" } + | { type: "FORM_ERROR"; errors: FormErrors } + | { type: "FORM_SUCCESS" } + | { type: "CLEAR_ERRORS" } + | { type: "FORM_RESET" } + | { type: "CLEAR_ERROR", key: FormErrorKey }; + + + type UseFormState = { + values: TSchema, + errors: FormErrors, + isSubmitting: boolean, } - - const [state, dispatch] = useReducer(formReducer, { - values: initialValues, - errors: {}, - isSubmitting: false, - }); - - const submit = async () => { - const result = schema.safeParse(state.values); - - if (!result.success) { - const errors: Record = {}; - result.error.issues.forEach((issue) => { - const field = issue.path[0]; - if (typeof field === "string") { - errors[field] = issue.message; - } - }) - dispatch({type: "FORM_ERROR", errors}); - return; + export const useControlledForm = ({ + initialValues, + schema, + onSubmit, + onSuccess, + onError, + }: UseFormOptions) => { + + type Values = z.infer; + + function formReducer( + state: UseFormState, + action: UseFormReducerAction + ): UseFormState { + switch (action.type) { + case "SET_FIELD": + return { + ...state, + values: { + ...state.values, + [action.key]: action.value, + }, + }; + case "FORM_SUBMIT": + return { + ...state, + isSubmitting: true, + }; + case "FORM_SUCCESS": + return { + ...state, + isSubmitting: false, + }; + case "FORM_ERROR": + return { + ...state, + isSubmitting: false, + errors: action.errors, + }; + case "CLEAR_ERROR": + const errors = {...state.errors}; + delete errors[action.key as string]; + + return { + ...state, + errors, + }; + case "CLEAR_ERRORS": + return { + ...state, + errors: {} + } + case "FORM_RESET": + return { + ...state, + values: initialValues, + errors: {}, + isSubmitting: false, + }; + } } - dispatch({type: "FORM_SUBMIT"}); - try { - const data = await onSubmit(state.values); + const [state, dispatch] = useReducer(formReducer, { + values: initialValues, + errors: {}, + isSubmitting: false, + }); + + const submit = async () => { + const result = schema.safeParse(state.values); + + if (!result.success) { + const errors: Record = {global: ""}; + + result.error.issues.forEach((issue) => { + const field = issue.path[0]; + + if (typeof field === "string") { + errors[field] = issue.message; + } + }); + + dispatch({ + type: "FORM_ERROR", + errors: errors as FormErrors, + }); + + return; + } + + dispatch({type: "FORM_SUBMIT"}); + try { + const data = await onSubmit(result.data); + + dispatch({type: "FORM_SUCCESS"}); + onSuccess?.(data); + } catch (error) { - dispatch({type: "FORM_SUCCESS"}); - onSuccess?.(data); - } catch (error) { + const errors: FormErrors = { + global: "Une erreur est survenue", + }; + + dispatch({ + type: "FORM_ERROR", + errors + }); + + onError?.(error); + } + }; + + const setValue = ( + key: K, + value: Values[K] + ) => { dispatch({ - type: "FORM_ERROR", - errors: { - form: "Une erreur est survenue", - }, + type: "SET_FIELD", + key, + value, }); + }; - onError?.(error); + const clearErrors = () => { + dispatch({ + type: "CLEAR_ERRORS" + }); } - }; - const setValue = ( - key: K, - value: Values[K] - ) => { - dispatch({ - type: "SET_FIELD", - key, - value, - }); - }; + const clearError = (key: FormErrorKey) => { + dispatch({ + type: "CLEAR_ERROR", + key, + }) + } - const clearError = () => { - dispatch({ - type: "CLEAR_ERROR" - }); - } + const reset = () => { + dispatch({ + type: "FORM_RESET" + }) + } - return { - ...state, - setValue, - clearError, - submit - } + return { + ...state, + reset, + setValue, + clearError, + clearErrors, + submit + } -} \ No newline at end of file + } \ No newline at end of file diff --git a/frontend/src/utils/hooks/useUncontrolledForm.ts b/frontend/src/utils/hooks/useUncontrolledForm.ts new file mode 100644 index 0000000..47a1fd7 --- /dev/null +++ b/frontend/src/utils/hooks/useUncontrolledForm.ts @@ -0,0 +1,65 @@ +import { type SubmitEvent, useState } from "react"; +import { z, type ZodObject } from "zod"; + +interface uncontrolledFormArgs { + schema: T, + onSubmit: (values: z.infer) => Promise | void; + onError: ( + error: unknown, + actions: { + setFieldError: (field: Extract, string>, message: string) => void + } + ) => void; +} + +export const useUncontrolledForm = ({schema, onError, onSubmit}: uncontrolledFormArgs) => { + + const [errors, setError] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + + function setFieldError(field: string, message: string) { + setError(prev => ({ + ...prev, + [field]: message + })); + } + + async function validateForm(formData: FormData) { + const result = schema.safeParse(Object.fromEntries(formData)); + + if (!result.success) { + const errors: Record = {}; + result.error.issues.forEach((issue) => { + const field = issue.path[0]; + if (typeof field === "string") { + errors[field] = issue.message; + } + }) + setError(errors); + return; + } + + setError({}); + + try { + setIsSubmitting(true) + await onSubmit(result.data); + } catch (e) { + onError(e, { setFieldError }); + } finally { + setIsSubmitting(false); + } + } + + async function onFormSubmit(event: SubmitEvent) { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + await validateForm(formData); + } + + return { + errors, + isSubmitting, + onFormSubmit + } +} \ No newline at end of file diff --git a/frontend/src/utils/schema.ts b/frontend/src/utils/schema.ts index d0fb454..eb03f8f 100644 --- a/frontend/src/utils/schema.ts +++ b/frontend/src/utils/schema.ts @@ -3,4 +3,10 @@ import { z } from "zod" export const loginSchema = z.object({ email: z.email({error: "Adresse email invalide"}), password: z.string().min(8, {error: "Votre mot de passe est trop court"}).max(64, {error: "Votre mot de passe est trop long"}).regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/, "Le mot de passe doit container un chiffre, une majuscule et un caractère spéciale") +}); + +export const registerSchema = z.object({ + email: z.email({error: "Adresse email invalide"}), + password: z.string().min(8, {error: "Votre mot de passe est trop court"}).max(64, {error: "Votre mot de passe est trop long"}).regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/, "Le mot de passe doit container un chiffre, une majuscule et un caractère spéciale"), + confirmPassword: z.string().min(8, {error: "Votre mot de passe est trop court"}).max(64, {error: "Votre mot de passe est trop long"}).regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/, "Le mot de passe doit container un chiffre, une majuscule et un caractère spéciale") }); \ No newline at end of file