diff --git a/src/App.css b/src/App.css index dcd5ee2..fe7b0a8 100644 --- a/src/App.css +++ b/src/App.css @@ -147,7 +147,7 @@ body { --cd-surface-2: #f1f5f9; --cd-surface-3: #eef2f7; --cd-border: #d8e1ee; - --cd-border-subtle: #e9eef6; + --cd-border-subtle: rgb(165, 166, 167); --cd-text: #1e293b; --cd-text-2: #53657d; --cd-text-muted: #8392a8; diff --git a/src/Component/ui/DropDown.tsx b/src/Component/ui/DropDown.tsx index 96e0857..ff9fe84 100644 --- a/src/Component/ui/DropDown.tsx +++ b/src/Component/ui/DropDown.tsx @@ -5,6 +5,7 @@ import { useTheme } from "@/theme"; type DropDownProps = { options: string[]; label?: string; + error?: string; onSelect: (option: string) => void; className?: string; value?: string; @@ -20,6 +21,7 @@ const DropDown: React.FC = ({ value, placeholder = "Select an option", disabled = false, + error, }) => { const { theme } = useTheme(); const [open, setOpen] = useState(false); @@ -69,7 +71,7 @@ const DropDown: React.FC = ({ className="w-full rounded-lg px-3 py-2 text-left flex justify-between items-center text-sm transition-all duration-150 disabled:cursor-not-allowed disabled:opacity-60" style={{ backgroundColor: theme.bg.surface, - border: `1px solid ${theme.border.default}`, + border: `1px solid ${error ? "var(--cd-danger)" : "var(--cd-border)"}`, color: selected ? theme.text.primary : theme.text.muted, }} > @@ -80,12 +82,12 @@ const DropDown: React.FC = ({ /> - {open && options.length > 0 && ( + {!error && open && options.length > 0 && (
= ({ ))}
)} + + {error && ( +

+ {error} +

+ )} ); }; diff --git a/src/Component/ui/Input.tsx b/src/Component/ui/Input.tsx index 629f819..edf845e 100644 --- a/src/Component/ui/Input.tsx +++ b/src/Component/ui/Input.tsx @@ -6,6 +6,7 @@ type InputType = "text" | "email" | "password" | "number" | "url" | "tel" | "tim type InputProps = { label?: string; name: string; + placeholder?: string; value?: string | number; type?: InputType; diff --git a/src/Component/ui/Url.tsx b/src/Component/ui/Url.tsx index eb806d7..e120bd8 100644 --- a/src/Component/ui/Url.tsx +++ b/src/Component/ui/Url.tsx @@ -2,7 +2,7 @@ import React from "react"; interface UrlProps { protocol?: string; - domain: string; + domain: string | undefined; themeMode?: "light" | "dark"; className?: string; style?: React.CSSProperties; diff --git a/src/features/AddMember/v1/Component/AddMemberHeader.tsx b/src/features/AddMember/v1/Component/AddMemberHeader.tsx index 2f0b3e3..cdae618 100644 --- a/src/features/AddMember/v1/Component/AddMemberHeader.tsx +++ b/src/features/AddMember/v1/Component/AddMemberHeader.tsx @@ -3,7 +3,12 @@ import { Link } from "react-router"; import Button from "../../../../Component/ui/Button"; import { memo } from "react"; -const AddMemberHeader = () => { +type AddMemberHeaderProps = { + onCreate: () => void; + onDiscard: () => void; +}; + +const AddMemberHeader = ({ onCreate, onDiscard }: AddMemberHeaderProps) => { return (
{
-
); diff --git a/src/features/AddMember/v1/Component/Administrative_MetaData.tsx b/src/features/AddMember/v1/Component/Administrative_MetaData.tsx index 724e009..36ccf86 100644 --- a/src/features/AddMember/v1/Component/Administrative_MetaData.tsx +++ b/src/features/AddMember/v1/Component/Administrative_MetaData.tsx @@ -14,7 +14,7 @@ const Administrative_MetaData = () => { Administrative Metadata

- + {/* */} ); }; diff --git a/src/features/AddMember/v1/Component/Community_Involment.tsx b/src/features/AddMember/v1/Component/Community_Involment.tsx index 24e915b..b6ad07e 100644 --- a/src/features/AddMember/v1/Component/Community_Involment.tsx +++ b/src/features/AddMember/v1/Component/Community_Involment.tsx @@ -3,9 +3,24 @@ import { GrGroup } from "react-icons/gr"; import InterestBox from "./InterestBox"; import AREA_OF_INTEREST from "../Constant/Interest.constant"; import { TextArea } from "../../../../Component/ui/TextArea"; +import { useFormContext } from "react-hook-form"; +import type { MemberFormValues } from "../Validator/AddMember.Validator"; const Community_Involvement = () => { - const [internalNotes, setInternalNotes] = useState(""); + const { watch, setValue , formState} = useFormContext(); + + const {errors} = formState + + const [internalNotes, setInternalNotes] = useState(watch("internalNotes") ?? ""); + const areaOfInterest = watch("areaOfInterest") ?? []; + + const toggleInterest = (interest: string, isChecked: boolean) => { + const nextInterests = isChecked + ? [...areaOfInterest, interest] + : areaOfInterest.filter((value) => value !== interest); + + setValue("areaOfInterest", nextInterests, { shouldDirty: true }); + }; return (
{
{AREA_OF_INTEREST.map((interest, index) => ( - {}} /> + toggleInterest(interest, clicked)} + /> ))}
@@ -39,7 +60,11 @@ const Community_Involvement = () => { name="internalNotes" placeholder="Enter internal notes" value={internalNotes} - onChange={(_, value) => setInternalNotes(value)} + error={errors.internalNotes?.message} + onChange={(_, value) => { + setInternalNotes(value); + setValue("internalNotes", value, { shouldDirty: true }); + }} className="mt-[3vh]" rows={5} /> diff --git a/src/features/AddMember/v1/Component/InterestBox.tsx b/src/features/AddMember/v1/Component/InterestBox.tsx index 464069f..67a9a80 100644 --- a/src/features/AddMember/v1/Component/InterestBox.tsx +++ b/src/features/AddMember/v1/Component/InterestBox.tsx @@ -15,7 +15,7 @@ const InterestBox = (props: InterestBoxProps) => { className={`InterestBox flex p-3 font-bold ${ Clicked ? "bg-blue-100 border-blue-500 text-blue-600" - : "bg-gray-100 border-gray-300 text-black" + : "bg-[cd-surface] border-gray-300 text-[--cd-text)" } border rounded-lg cursor-pointer`} onClick={() => { props.onClick(!Clicked); @@ -24,7 +24,7 @@ const InterestBox = (props: InterestBoxProps) => { >
{Clicked && } diff --git a/src/features/AddMember/v1/Component/MemberShip_Status.tsx b/src/features/AddMember/v1/Component/MemberShip_Status.tsx index e126fe1..54e6d1d 100644 --- a/src/features/AddMember/v1/Component/MemberShip_Status.tsx +++ b/src/features/AddMember/v1/Component/MemberShip_Status.tsx @@ -1,10 +1,11 @@ import { useState } from "react"; +import { useFormContext } from "react-hook-form"; +import type { MemberFormValues } from "../Validator/AddMember.Validator"; + type MembershipStatus = "Active" | "Inactive" | "Pending" | "Suspended" | "On Boarding"; const MemberShip_Status = () => { - const [membershipStatus, setMembershipStatus] = useState("Active"); - const statusColorMap: Record = { Active: "bg-green-500", Inactive: "bg-gray-400", @@ -13,10 +14,12 @@ const MemberShip_Status = () => { "On Boarding": "bg-blue-400", }; + const { watch, setValue , formState} = useFormContext(); + const {errors} = formState + const membershipStatus = (watch("membershipStatus") ?? "On Boarding") as MembershipStatus; + return (
-

Membership Status

- {/* Active */}
{ id="Active" value="Active" checked={membershipStatus === "Active"} - onChange={() => setMembershipStatus("Active")} + onChange={() => setValue("membershipStatus", "Active", { shouldDirty: true })} /> {
+ {errors.membershipStatus && ( +

{errors.membershipStatus.message}

+ )} + {/* Inactive */}
setMembershipStatus("Inactive")} + onChange={() => setValue("membershipStatus", "Inactive", { shouldDirty: true })} /> { value="Pending" id="Pending" checked={membershipStatus === "Pending"} - onChange={() => setMembershipStatus("Pending")} + onChange={() => setValue("membershipStatus", "Pending", { shouldDirty: true })} /> { id="OnBoarding" value="On Boarding" checked={membershipStatus === "On Boarding"} - onChange={() => setMembershipStatus("On Boarding")} + onChange={() => setValue("membershipStatus", "On Boarding", { shouldDirty: true })} /> { id="Suspended" value="Suspended" checked={membershipStatus === "Suspended"} - onChange={() => setMembershipStatus("Suspended")} + onChange={() => setValue("membershipStatus", "Suspended", { shouldDirty: true })} /> { return ( -
{props.skill}
+
{props.skill}
); }; diff --git a/src/features/AddMember/v1/Page/AddMemberPage.tsx b/src/features/AddMember/v1/Page/AddMemberPage.tsx index cfa19d6..c92305f 100644 --- a/src/features/AddMember/v1/Page/AddMemberPage.tsx +++ b/src/features/AddMember/v1/Page/AddMemberPage.tsx @@ -1,22 +1,37 @@ +import { FormProvider } from "react-hook-form"; import AddMemberHeader from "../Component/AddMemberHeader"; import Administrative_MetaData from "../Component/Administrative_MetaData"; import Community_Involvement from "../Component/Community_Involment"; import PersonalInfoCard from "../Sections/PersonalInfoCard"; import ProfessionalDetails from "../Sections/ProfessionalDetails"; +import { useAddMember } from "../hook/useAddMember"; const AddMemberPage = () => { + const methods = useAddMember(); + + const handleCreateMember = methods.handleSubmit((data) => { + try { + alert("Member created successfully!"); + console.log("Add member submit", data); + } catch (error) { + console.error("Error creating member:", error); + } + }); + return ( -
- -
-
- - - + +
+ methods.reset()} /> +
+
+ + + +
+
-
-
+ ); }; diff --git a/src/features/AddMember/v1/Sections/PersonalInfoCard.tsx b/src/features/AddMember/v1/Sections/PersonalInfoCard.tsx index 7ddb7d6..2ec0ecb 100644 --- a/src/features/AddMember/v1/Sections/PersonalInfoCard.tsx +++ b/src/features/AddMember/v1/Sections/PersonalInfoCard.tsx @@ -1,14 +1,20 @@ -import React, { memo } from "react"; +import { memo } from "react"; import { Input } from "../../../../Component/ui/Input"; import Url from "../../../../Component/ui/Url"; import { CiCamera } from "react-icons/ci"; import { FaUser } from "react-icons/fa"; +import { useFormContext } from "react-hook-form"; +import type { MemberFormValues } from "../Validator/AddMember.Validator"; const PersonalInfoCard = () => { - const [firstName, setFirstName] = React.useState(""); - const [lastName, setLastName] = React.useState(""); - const [email, setEmail] = React.useState(""); - const [profileUrl, setProfileUrl] = React.useState("example.com"); + const { setValue, watch , formState } = useFormContext(); + + const {errors} = formState; + + const firstName = watch("firstName") ?? ""; + const lastName = watch("lastName") ?? ""; + const email = watch("email") ?? ""; + const profileUrl = (watch("publicProfileUrl") ?? "") as string; return (
{
{
+ + + setFirstName(value)} + onChange={(_, value) => setValue("firstName", value, { shouldDirty: true })} /> + setLastName(value)} + onChange={(_, value) => setValue("lastName", value, { shouldDirty: true })} />
@@ -75,10 +87,11 @@ const PersonalInfoCard = () => { setEmail(value)} + onChange={(_, value) => setValue("email", value, { shouldDirty: true })} />
{ protocol="http://" domain={profileUrl} className="w-[80%] text-2xl flex" - setDomain={(domain) => setProfileUrl(domain)} + setDomain={(domain) => setValue("publicProfileUrl", domain, { shouldDirty: true })} />
diff --git a/src/features/AddMember/v1/Sections/ProfessionalDetails.tsx b/src/features/AddMember/v1/Sections/ProfessionalDetails.tsx index 3909697..39cd02a 100644 --- a/src/features/AddMember/v1/Sections/ProfessionalDetails.tsx +++ b/src/features/AddMember/v1/Sections/ProfessionalDetails.tsx @@ -4,18 +4,24 @@ import { Input } from "../../../../Component/ui/Input"; import DropDown from "../../../../Component/ui/DropDown"; import { Roles } from "../Constant/Role.constant"; import { SkillColor } from "../Constant/Skill.constant"; +import { useFormContext } from "react-hook-form"; +import type { MemberFormValues } from "../Validator/AddMember.Validator"; +import { theme } from "@/theme"; const ProfessionalDetails = () => { + const { watch, setValue , formState} = useFormContext(); + + const {errors} = formState + const [skillInput, setSkillInput] = React.useState(""); - const [, setSelectedRole] = React.useState(""); - const [location, setLocation] = React.useState(""); - const [skills, setSkills] = React.useState([]); + const location = watch("location") ?? ""; + const skills = watch("skills") ?? []; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") { const skill = skillInput.trim(); if (skill !== "" && !skills.includes(skill)) { - setSkills((prev) => [...prev, skill]); + setValue("skills", [...skills, skill], { shouldDirty: true }); setSkillInput(""); } } @@ -47,16 +53,21 @@ const ProfessionalDetails = () => {

Primary Role

- setSelectedRole(opt)} /> + setValue("primaryRole", opt, { shouldDirty: true })} + />
setLocation(value)} + onChange={(_, value) => setValue("location", value, { shouldDirty: true })} />
@@ -66,6 +77,7 @@ const ProfessionalDetails = () => { { onChange={(_, value) => setSkillInput(value)} /> -
+
{skills.map((skill, index) => (
{skill} setSkills((prev) => prev.filter((_, i) => i !== index))} + onClick={() => + setValue( + "skills", + skills.filter((_, i) => i !== index), + { shouldDirty: true }, + ) + } > × diff --git a/src/features/AddMember/v1/Type/AddMember.Type.ts b/src/features/AddMember/v1/Type/AddMember.Type.ts new file mode 100644 index 0000000..0b6e0b2 --- /dev/null +++ b/src/features/AddMember/v1/Type/AddMember.Type.ts @@ -0,0 +1,25 @@ +type onBoardingSourceType = + | "website" + | "referral" + | "social_media" + | "event" + | "direct_invitation" + | "other"; + +type memberStatusType = "On Boarding" | "Pending" | "Active" | "Inactive" | "Suspended" | "Banned"; + +export type MemberType = { + firstName: string; + lastName: string; + imageUrl?: string; + publicProfileUrl?: string; + + email: string; + membershipStatus?: memberStatusType; + onboardingSource?: onBoardingSourceType; + primaryRole?: string; + location?: string; + skills?: string[]; + areaOfInterest?: string[]; + internalNotes?: string; +}; diff --git a/src/features/AddMember/v1/Validator/AddMember.Validator.ts b/src/features/AddMember/v1/Validator/AddMember.Validator.ts new file mode 100644 index 0000000..15ce823 --- /dev/null +++ b/src/features/AddMember/v1/Validator/AddMember.Validator.ts @@ -0,0 +1,40 @@ +import z from "zod"; + +const onboardingSourceEnum = z.enum([ + "website", + "referral", + "social_media", + "event", + "direct_invitation", + "other", +]); + +const membershipStatusEnum = z.enum(["On Boarding", "Pending", "Active", "Inactive", "Suspended"]); + +const optionalUrl = z.preprocess( + (value) => (value === "" ? undefined : value), + z.string().url("Invalid URL").optional(), +); + +export const MemberValidationSchema = z.object({ + firstName: z.string().min(1, "First name is required"), + lastName: z.string().min(1, "Last name is required"), + imageUrl: optionalUrl, + publicProfileUrl: optionalUrl, + email: z.string().email("Invalid email format").toLowerCase().trim(), + AuthId: z + .string() + .regex(/^[0-9a-fA-F]{24}$/, "Invalid Mongo ObjectId") + .optional(), + membershipStatus: membershipStatusEnum.default("On Boarding").optional(), + onboardingSource: onboardingSourceEnum.default("website").optional(), + primaryRole: z.string().min(1, "Primary role is required"), + location: z.string().min(2, "Location is required"), + skills: z.array(z.string()).optional(), + areaOfInterest: z.array(z.string()).min(1, "Area of interest is required"), + internalNotes: z.string().optional(), + +}); + +export type MemberFormValues = z.input; +export type MemberSubmitValues = z.output; diff --git a/src/features/AddMember/v1/hook/useAddMember.ts b/src/features/AddMember/v1/hook/useAddMember.ts new file mode 100644 index 0000000..b3dea76 --- /dev/null +++ b/src/features/AddMember/v1/hook/useAddMember.ts @@ -0,0 +1,26 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + MemberValidationSchema, + type MemberFormValues, + type MemberSubmitValues, +} from "../Validator/AddMember.Validator"; + +export function useAddMember() { + return useForm({ + resolver: zodResolver(MemberValidationSchema), + + defaultValues: { + firstName: "", + lastName: "", + email: "", + primaryRole: "", + location: "", + skills: [], + areaOfInterest: [], + internalNotes: "", + imageUrl: "", + publicProfileUrl: "", + }, + }); +}