From 3375278dec0bef8e22d311dee5661beb461e6e29 Mon Sep 17 00:00:00 2001 From: Ion Dormenco Date: Thu, 16 Nov 2023 17:28:15 +0200 Subject: [PATCH 1/2] Implement edit profile --- .../ExternalLinkItem/index.tsx} | 0 .../SocialNetworkLinks/index.tsx} | 2 +- client/src/pages/Profile/index.tsx | 2 +- client/src/pages/admin/CreateUser/index.tsx | 4 +- .../pages/authenticated/EditProfile/index.tsx | 489 ++++++++++++++++++ client/src/pages/public/Register/index.tsx | 4 +- client/src/redux/api/types.ts | 8 + client/src/redux/api/userApi.ts | 2 +- client/src/router/index.tsx | 2 + 9 files changed, 506 insertions(+), 7 deletions(-) rename client/src/{pages/public/Register/ExternalLinkItem.tsx => components/ExternalLinkItem/index.tsx} (100%) rename client/src/{pages/public/Register/SocialNetworkLinks.tsx => components/SocialNetworkLinks/index.tsx} (93%) create mode 100644 client/src/pages/authenticated/EditProfile/index.tsx diff --git a/client/src/pages/public/Register/ExternalLinkItem.tsx b/client/src/components/ExternalLinkItem/index.tsx similarity index 100% rename from client/src/pages/public/Register/ExternalLinkItem.tsx rename to client/src/components/ExternalLinkItem/index.tsx diff --git a/client/src/pages/public/Register/SocialNetworkLinks.tsx b/client/src/components/SocialNetworkLinks/index.tsx similarity index 93% rename from client/src/pages/public/Register/SocialNetworkLinks.tsx rename to client/src/components/SocialNetworkLinks/index.tsx index 27ff1ad..0a230ba 100644 --- a/client/src/pages/public/Register/SocialNetworkLinks.tsx +++ b/client/src/components/SocialNetworkLinks/index.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import Button from "@/components/Button"; -import ExternalLinkItem from "@/pages/public/Register/ExternalLinkItem"; +import ExternalLinkItem from "@/components/ExternalLinkItem"; const SOCIAL_NETWORKS = [ { name: "accountFacebook", label: "Facebook" }, diff --git a/client/src/pages/Profile/index.tsx b/client/src/pages/Profile/index.tsx index daf7b3d..3c16d67 100644 --- a/client/src/pages/Profile/index.tsx +++ b/client/src/pages/Profile/index.tsx @@ -30,7 +30,7 @@ const Profile = () => { ["Telefon organizație", user.ongName], [ "Domenii de activitate", - user.domains?.map((domain) => domain.name), + user.domains?.map((domain) => domain.name)?.join(", "), ], ["Cuvinte cheie despre activitate", user.ongName], ["Descriere organizație", user.ongName], diff --git a/client/src/pages/admin/CreateUser/index.tsx b/client/src/pages/admin/CreateUser/index.tsx index 5a534d9..af0b22f 100644 --- a/client/src/pages/admin/CreateUser/index.tsx +++ b/client/src/pages/admin/CreateUser/index.tsx @@ -8,7 +8,7 @@ import Button from "@/components/Button"; import { setToken } from "@/redux/features/userSlice"; import { useAppDispatch } from "@/redux/store"; import { toast } from "react-toastify"; -import SocialNetworkLinks from "@/pages/public/Register/SocialNetworkLinks"; +import SocialNetworkLinks from "@/components/SocialNetworkLinks"; import { useCreateUserMutation, useGetDomainsQuery, @@ -104,7 +104,7 @@ const CreateUser = () => { const onSubmitHandler: SubmitHandler = async (values) => { const formData = new FormData(); const res = await submitRegister({ ...values, username: values.email }); - if (values.avatar[0].name) { + if (values.avatar?.length && values.avatar[0]?.name) { formData.append(`files`, values.avatar[0], values.avatar[0].name); formData.append(`ref`, "plugin::users-permissions.user"); formData.append(`refId`, res.data.user.id); diff --git a/client/src/pages/authenticated/EditProfile/index.tsx b/client/src/pages/authenticated/EditProfile/index.tsx new file mode 100644 index 0000000..a9ae0d1 --- /dev/null +++ b/client/src/pages/authenticated/EditProfile/index.tsx @@ -0,0 +1,489 @@ +import { useEffect, useMemo } from "react"; +import { custom, literal, number, object, string, TypeOf } from "zod"; +import { SubmitHandler, useForm, FormProvider } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod/dist/zod"; +import Heading from "@/components/Heading"; +import Section from "@/components/Section"; +import Button from "@/components/Button"; +import { useAppDispatch, useAppSelector } from "@/redux/store"; +import { toast } from "react-toastify"; +import { + useGetDomainsQuery, + useUpdateUserMutation, useUploadMutation, +} from "@/redux/api/userApi"; +import { useNavigate } from "react-router-dom"; +import MultiSelect from "@/components/MultiSelect"; +import { ErrorMessage } from "@hookform/error-message"; +import Select from "@/components/Select"; +import citiesByCounty from "@/lib/orase-dupa-judet.json"; +import SocialNetworkLinks from "@/components/SocialNetworkLinks"; + +const ongProfileSchema = object({ + id: number(), + ongName: string().min(1, "Numele organizatiei este obligatoriu"), + ongIdentificationNumber: string().min( + 1, + "Numarul de identifiare este obligatoriu" + ), + county: string().min(1, "Judetul este obligatoriu"), + city: string().min(1, "Orasul este obligatoriu"), + email: string() + .min(1, "Adresa de email este obligatorie") + .email("Email Address is invalid"), + phone: string().min(1, "Telefonul este obligatoriu"), + avatar: custom(), + domains: number().array().optional(), + website: string(), + keywords: string(), + description: string(), + contactFirstName: string(), + contactLastName: string(), + contactEmail: string() + .email("Adresa de email este invalida") + .optional() + .or(literal("")), + contactPhone: string(), + accountFacebook: string().optional(), + accountTwitter: string().optional(), + accountTiktok: string().optional(), + accountInstagram: string().optional(), + accountLinkedin: string().optional(), +}); + +export type OngProfileInput = TypeOf; + +const OngEditProfile = () => { + const user = useAppSelector((state) => state.userState.user); + + const ongDomains = useMemo(() => { + return user?.domains + ? user?.domains?.map(d=>({id: d.id, name: d.name, label: d.name})) + : []; + }, [user]); + + const [updateOngProfile, { isSuccess, isError, error, data }] = useUpdateUserMutation(); + + const [ + uploadAvatar, + { isError: isUploadAvatarError, error: uploadAvatarError }, + ] = useUploadMutation(); + const { data: domains } = useGetDomainsQuery(null); + + const methods = useForm({ + resolver: zodResolver(ongProfileSchema), + defaultValues: { + ...user, + domains: user?.domains?.map(d => d.id), + accountFacebook: user?.accountFacebook || '', + accountTwitter: user?.accountTwitter || '', + accountTiktok: user?.accountTiktok || '', + accountInstagram: user?.accountInstagram || '', + accountLinkedin: user?.accountLinkedin || '', + }, + }); + + const avatar = methods.watch("avatar"); + const county = methods.watch("county"); + + const hasAvatar = !!avatar; + + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + useEffect(() => { + if (error?.data?.error?.message) { + toast.error(error.data.error.message); + } + }, [error?.data?.error?.message]); + + useEffect(() => { + if (isSuccess) { + navigate("/profile"); + } + }, [isSuccess, dispatch]); + + const onSubmitHandler: SubmitHandler = async (data) => { + updateOngProfile({ + ...data, + }); + + if (data.avatar?.length && data.avatar[0].name) { + const formData = new FormData(); + formData.append(`files`, data.avatar[0], data.avatar[0].name); + formData.append(`ref`, "plugin::users-permissions.user"); + formData.append(`refId`, user!.id!.toString()); + formData.append(`field`, "avatar"); + uploadAvatar(formData); + } + }; + const onError: any = (data) => { + console.log(data); + }; + + const counties = Object.keys(citiesByCounty).sort().map((county: string) => ({ + label: county, + name: county, + })); + + const cities = useMemo( + () => + county + ? [...new Set(citiesByCounty[county].map((city) => city.nume))].sort().map( + (city) => ({ + name: city, + label: city, + }) + ) + : [], + [citiesByCounty, county] + ); + + + return ( +
+
+
+ Editeaza profilul +
+
+
+ +
+
+
+
+

+ Informații obligatorii despre ONG +

+
+
+
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+

+ Informații adiționale despre ONG (opționale) +

+
+
+ {domains?.length > 0 && ( +
+ +
+ +
+
+ )} + +
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+

+ Numărul maxim de caractere: 1000 +

+
+
+
+
+
+

+ Comunicare și social media +

+
+
Link-uri către social media
+ +
Persoana de contact a organizației
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+ ); +}; +export default OngEditProfile; diff --git a/client/src/pages/public/Register/index.tsx b/client/src/pages/public/Register/index.tsx index 9c6bd39..b8798fb 100644 --- a/client/src/pages/public/Register/index.tsx +++ b/client/src/pages/public/Register/index.tsx @@ -9,7 +9,7 @@ import Button from "@/components/Button"; import { setToken } from "@/redux/features/userSlice"; import { useAppDispatch } from "@/redux/store"; import { toast } from "react-toastify"; -import SocialNetworkLinks from "@/pages/public/Register/SocialNetworkLinks"; +import SocialNetworkLinks from "@/components/SocialNetworkLinks"; import { useGetDomainsQuery, useUploadMutation } from "@/redux/api/userApi"; import { useNavigate } from "react-router-dom"; import Cookies from "js-cookie"; @@ -110,7 +110,7 @@ const Register = () => { const onSubmitHandler: SubmitHandler = async (values) => { const formData = new FormData(); const res = await submitRegister({ ...values, username: values.email }); - if (values.avatar[0].name) { + if (values.avatar?.length && values.avatar[0].name) { formData.append(`files`, values.avatar[0], values.avatar[0].name); formData.append(`ref`, "plugin::users-permissions.user"); formData.append(`refId`, res.data.user.id); diff --git a/client/src/redux/api/types.ts b/client/src/redux/api/types.ts index 6008d60..fa5844b 100644 --- a/client/src/redux/api/types.ts +++ b/client/src/redux/api/types.ts @@ -51,6 +51,13 @@ export interface User { domains?: Domain[]; program: Program; role?: Role; + accountFacebook?: string; + accountTwitter?: string; + accountTiktok?: string; + accountInstagram?: string; + accountLinkedin?: string; + + // Mentor firstName: string; lastName: string; @@ -93,6 +100,7 @@ export interface RegisterResponse { } export interface Domain { + id: number; name: string; } diff --git a/client/src/redux/api/userApi.ts b/client/src/redux/api/userApi.ts index 8ff55f8..da2ceb1 100644 --- a/client/src/redux/api/userApi.ts +++ b/client/src/redux/api/userApi.ts @@ -42,7 +42,7 @@ export const userApi = createApi({ getMe: builder.query({ query() { return { - url: "users/me?populate[0]=reports.evaluations.dimensions.quiz&populate[1]=avatar&populate[2]=role&populate[3]=programs.users&populate[4]=userActivities&populate[5]=mentorActivities.user&populate[6]=mentorActivities.type&populate[7]=mentorActivities.dimension&populate[8]=program&populate[9]=dimensions", + url: "users/me?populate[0]=reports.evaluations.dimensions.quiz&populate[1]=avatar&populate[2]=role&populate[3]=programs.users&populate[4]=userActivities&populate[5]=mentorActivities.user&populate[6]=mentorActivities.type&populate[7]=mentorActivities.dimension&populate[8]=program&populate[9]=dimensions&populate[10]=domains", }; }, async onQueryStarted(args, { dispatch, queryFulfilled }) { diff --git a/client/src/router/index.tsx b/client/src/router/index.tsx index 33a2b79..252892e 100644 --- a/client/src/router/index.tsx +++ b/client/src/router/index.tsx @@ -33,6 +33,7 @@ import AuthenticatedMentorsList from "@/pages/authenticated/MentorsList"; import AuthenticatedMentor from "@/pages/authenticated/Mentor"; import CreateUser from "@/pages/admin/CreateUser"; import MentorReport from "@/pages/mentor/Report"; +import OngEditProfile from "@/pages/authenticated/EditProfile"; const Router = () => { const user = useAppSelector((state) => state.userState.user); @@ -64,6 +65,7 @@ const Router = () => { } /> } /> } /> + } /> } /> } /> From 0655a2d4043defba57a4a06beb530f4c8c482150 Mon Sep 17 00:00:00 2001 From: Ion Dormenco Date: Thu, 16 Nov 2023 17:40:55 +0200 Subject: [PATCH 2/2] small papercuts fixez --- client/src/pages/NewReport/index.tsx | 2 +- client/src/pages/Profile/index.tsx | 1 - client/src/pages/admin/CreateUser/index.tsx | 2 +- client/src/pages/authenticated/EditProfile/index.tsx | 2 +- client/src/pages/mentor/EditProfile/index.tsx | 2 +- client/src/pages/public/ForgotPassword/index.tsx | 4 ++-- client/src/pages/public/Login/index.tsx | 10 +++++----- client/src/pages/public/Register/index.tsx | 8 ++++---- 8 files changed, 15 insertions(+), 16 deletions(-) diff --git a/client/src/pages/NewReport/index.tsx b/client/src/pages/NewReport/index.tsx index 3105db4..77718ff 100644 --- a/client/src/pages/NewReport/index.tsx +++ b/client/src/pages/NewReport/index.tsx @@ -9,7 +9,7 @@ import { zodResolver } from "@hookform/resolvers/zod/dist/zod"; import { ErrorMessage } from "@hookform/error-message"; import { toast } from "react-toastify"; -const emailListSchema = array(string().email()).nonempty(); +const emailListSchema = array(string().email("Adresa de email este invalidă")).nonempty(); const validateEmails = (input: string) => { const emails = input.trim().split(/\r?\n/); // split input by new line const emailList = emailListSchema.safeParse(emails); diff --git a/client/src/pages/Profile/index.tsx b/client/src/pages/Profile/index.tsx index 3c16d67..21d40de 100644 --- a/client/src/pages/Profile/index.tsx +++ b/client/src/pages/Profile/index.tsx @@ -19,7 +19,6 @@ const Profile = () => {
Editeaza} body={[ ["Nume organizație", user.ongName], diff --git a/client/src/pages/admin/CreateUser/index.tsx b/client/src/pages/admin/CreateUser/index.tsx index af0b22f..2b992d0 100644 --- a/client/src/pages/admin/CreateUser/index.tsx +++ b/client/src/pages/admin/CreateUser/index.tsx @@ -29,7 +29,7 @@ const registerSchema = object({ city: string().min(1, "Orasul este obligatoriu"), email: string() .min(1, "Adresa de email este obligatorie") - .email("Email Address is invalid"), + .email("Adresa de email este invalidă"), phone: string().min(1, "Telefonul este obligatoriu"), avatar: custom(), domains: array(number()), diff --git a/client/src/pages/authenticated/EditProfile/index.tsx b/client/src/pages/authenticated/EditProfile/index.tsx index a9ae0d1..1848882 100644 --- a/client/src/pages/authenticated/EditProfile/index.tsx +++ b/client/src/pages/authenticated/EditProfile/index.tsx @@ -29,7 +29,7 @@ const ongProfileSchema = object({ city: string().min(1, "Orasul este obligatoriu"), email: string() .min(1, "Adresa de email este obligatorie") - .email("Email Address is invalid"), + .email("Adresa de email este invalidă"), phone: string().min(1, "Telefonul este obligatoriu"), avatar: custom(), domains: number().array().optional(), diff --git a/client/src/pages/mentor/EditProfile/index.tsx b/client/src/pages/mentor/EditProfile/index.tsx index 34fc5a2..0712b4e 100644 --- a/client/src/pages/mentor/EditProfile/index.tsx +++ b/client/src/pages/mentor/EditProfile/index.tsx @@ -23,7 +23,7 @@ const mentorProfileSchema = object({ lastName: string(), email: string() .min(1, "Adresa de email este obligatorie") - .email("Email Address is invalid"), + .email("Adresa de email este invalidă"), bio: string().min(1, "Adaugati o scurta descriere"), expertise: string(), dimensions: array(number()), diff --git a/client/src/pages/public/ForgotPassword/index.tsx b/client/src/pages/public/ForgotPassword/index.tsx index 54f23e5..fc2cc51 100644 --- a/client/src/pages/public/ForgotPassword/index.tsx +++ b/client/src/pages/public/ForgotPassword/index.tsx @@ -10,8 +10,8 @@ import { useForgotPasswordMutation } from "@/redux/api/authApi"; const forgotPasswordSchema = object({ email: string() - .min(1, "Email address is required") - .email("Email Address is invalid"), + .min(1, "Adresa de email este obligatorie") + .email("Adresa de email este invalidă"), }); export type ForgotPasswordInput = TypeOf; diff --git a/client/src/pages/public/Login/index.tsx b/client/src/pages/public/Login/index.tsx index 8847acd..34b0a6d 100644 --- a/client/src/pages/public/Login/index.tsx +++ b/client/src/pages/public/Login/index.tsx @@ -16,12 +16,12 @@ import Cookies from "js-cookie"; const loginSchema = object({ identifier: string() - .min(1, "Email address is required") - .email("Email Address is invalid"), + .min(1, "Adresa de email este obligatorie") + .email("Adresa de email este invalidă"), password: string() - .min(1, "Password is required") - .min(8, "Password must be more than 8 characters") - .max(32, "Password must be less than 32 characters"), + .min(1, "Parola este obligatorie") + .min(8, "Parola trebuie sa contina cel putin 8 caractere") + .max(32, "Parola trebuie sa contina cel mult 32 caractere"), }); export type LoginInput = TypeOf; diff --git a/client/src/pages/public/Register/index.tsx b/client/src/pages/public/Register/index.tsx index b8798fb..0396d5d 100644 --- a/client/src/pages/public/Register/index.tsx +++ b/client/src/pages/public/Register/index.tsx @@ -28,12 +28,12 @@ const registerSchema = object({ city: string().min(1, "Orasul este obligatoriu"), email: string() .min(1, "Adresa de email este obligatorie") - .email("Email Address is invalid"), + .email("Adresa de email este invalidă"), phone: string().min(1, "Telefonul este obligatoriu"), password: string() - .min(1, "Password is required") - .min(8, "Password must be more than 8 characters") - .max(32, "Password must be less than 32 characters"), + .min(1, "Parola este obligatorie") + .min(8, "Parola trebuie sa contina cel putin 8 caractere") + .max(32, "Parola trebuie sa contina cel mult 32 caractere"), confirmPassword: string(), avatar: custom(), domains: array(number()),