diff --git a/package.json b/package.json index ec7cf4e..2ad60f6 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "format:check": "prettier . --check --ignore-unknown" }, "dependencies": { + "@hookform/resolvers": "^5.2.2", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", "@tauri-apps/plugin-process": "^2", @@ -57,4 +58,4 @@ "typescript-eslint": "^8.56.1", "vite": "^7.0.4" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85d0989..6789f09 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.71.2(react@19.2.4)) '@tauri-apps/api': specifier: ^2 version: 2.10.1 @@ -505,6 +508,11 @@ packages: peerDependencies: hono: ^4 + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1346,66 +1354,79 @@ packages: resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.57.1': resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.57.1': resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.57.1': resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.57.1': resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.57.1': resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.57.1': resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.57.1': resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.57.1': resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.57.1': resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.57.1': resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.57.1': resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.57.1': resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.57.1': resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} @@ -1444,6 +1465,9 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@tabby_ai/hijri-converter@1.0.5': resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==} engines: {node: '>=16.0.0'} @@ -1486,24 +1510,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -1569,30 +1597,35 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-arm64-musl@2.10.0': resolution: {integrity: sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tauri-apps/cli-linux-riscv64-gnu@2.10.0': resolution: {integrity: sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-gnu@2.10.0': resolution: {integrity: sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tauri-apps/cli-linux-x64-musl@2.10.0': resolution: {integrity: sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tauri-apps/cli-win32-arm64-msvc@2.10.0': resolution: {integrity: sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==} @@ -2570,24 +2603,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} @@ -3798,6 +3835,11 @@ snapshots: dependencies: hono: 4.12.5 + '@hookform/resolvers@5.2.2(react-hook-form@7.71.2(react@19.2.4))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.71.2(react@19.2.4) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -4738,6 +4780,8 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@standard-schema/utils@0.3.0': {} + '@tabby_ai/hijri-converter@1.0.5': {} '@tailwindcss/node@4.2.1': diff --git a/src/features/Auth/v1/Pages/LoginPage.tsx b/src/features/Auth/v1/Pages/LoginPage.tsx index 2a8e746..9d72488 100644 --- a/src/features/Auth/v1/Pages/LoginPage.tsx +++ b/src/features/Auth/v1/Pages/LoginPage.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { MdEmail } from "react-icons/md"; import { RiLockPasswordFill } from "react-icons/ri"; import { Link } from "react-router"; diff --git a/src/features/Auth/v1/Pages/SignUpPage.tsx b/src/features/Auth/v1/Pages/SignUpPage.tsx index 7a1b39b..d88f3d2 100644 --- a/src/features/Auth/v1/Pages/SignUpPage.tsx +++ b/src/features/Auth/v1/Pages/SignUpPage.tsx @@ -1,95 +1,220 @@ -import React from "react"; -import { MdEmail } from "react-icons/md"; -import { RiLockPasswordFill } from "react-icons/ri"; -import Community_Info from "../Sections/Community_Info"; -import { Link } from "react-router"; - -let Steps = [ - { - id: 1, - title: "Info", - Element: , - }, - { - id: 2, - title: "Set Up Your Organization", - description: "Provide details about your organization to create a workspace.", - }, - { - id: 3, - title: "Invite Team Members", - description: "Add your team members to collaborate and manage tasks together.", - }, - { - id: 4, - title: "Start Managing Tasks", - description: "Create and assign tasks, track progress, and stay organized.", - }, -]; - -const SignUpPage = () => { - let [currentStep, setCurrentStep] = React.useState(1); +import { useEffect, useState } from "react"; +import { FormProvider } from "react-hook-form"; +import { Link } from "react-router-dom"; +import { ArrowLeft, ArrowRight, Loader2 } from "lucide-react"; + +import { useSignupForm, STEP_FIELDS, SignupFormData } from "../hooks/useSignupForm"; +import { submitCommunitySignup } from "../api/signup"; +import StepProgress from "../components/signup/StepProgress"; +import CommunityStep from "../components/signup/CommunityStep"; +import ContactStep from "../components/signup/ContactStep"; +import SocialStep from "../components/signup/SocialStep"; +import OwnerStep from "../components/signup/OwnerStep"; +import ReviewStep from "../components/signup/ReviewStep"; +import SuccessScreens from "../components/signup/SuccessScreens"; + +const TOTAL_STEPS = 5; + +const STEP_COMPONENTS: Record React.ReactElement> = { + 1: () => , + 2: () => , + 3: () => , + 4: () => , + 5: () => , +}; + +const CURRENT_YEAR = new Date().getFullYear(); + +type PostState = "idle" | "email" | "pending"; + +export default function SignUpPage() { + const methods = useSignupForm(); + const { handleSubmit, trigger, setError, formState: { isSubmitting } } = methods; + + const [step, setStep] = useState(1); + const [post, setPost] = useState("idle"); + const [serverError, setServerError] = useState(null); + + const goNext = async () => { + const fields = STEP_FIELDS[step] as (keyof SignupFormData)[]; + const valid = fields.length ? await trigger(fields) : true; + if (valid) setStep((s) => Math.min(s + 1, TOTAL_STEPS)); + }; + + const goBack = () => setStep((s) => Math.max(s - 1, 1)); + + const onSubmit = handleSubmit(async (data) => { + setServerError(null); + try { + await submitCommunitySignup(data); + setPost("email"); + } catch (err: unknown) { + const e = err as { status?: number; data?: { message?: string; errors?: Record } }; + if (e.status === 422 && e.data?.errors) { + Object.entries(e.data.errors).forEach(([rawField, msg]) => { + // Normalize snake_case backend keys → camelCase RHF keys + const field = rawField.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase()); + if (field in methods.getValues()) { + setError(field as keyof SignupFormData, { message: msg }); + } + }); + } else { + setServerError(e.data?.message ?? "Something went wrong. Please try again."); + } + } + }); + + useEffect(() => { + if (post !== "email") return; + const id = setTimeout(() => setPost("pending"), 3500); + return () => clearTimeout(id); + }, [post]); return ( -
-
-
- Logo -

CommDesk

-
+
+ {/* Left panel */} +
Login Icon -
-
-
- {Steps.map((step) => ( -
setCurrentStep(step.id)} - > -
- {step.id} -
- {currentStep === step.id && ( -
-

{step.title}

+
+ +
+ {/* Logo */} +
+ Logo + CommDesk +
+ + {/* Pitch */} +
+
+ + Join the future of community management + +

+ Build something
+ + extraordinary + +

+
+

+ CommDesk gives communities a powerful workspace to manage members, events, and communications in one place. +

+ + {/* Stats */} +
+ {[ + { value: "10k+", label: "Communities" }, + { value: "500k+", label: "Members" }, + { value: "99.9%", label: "Uptime" }, + { value: "4.9★", label: "Rating" }, + ].map(({ value, label }) => ( +
+

{value}

+

{label}

- )} - {Steps.length !== step.id &&
} + ))}
- ))} +
+ +

© {CURRENT_YEAR} CommDesk. All rights reserved.

+
-
- {Steps[currentStep - 1].Element} - - -

Cancel registration

+ {/* Right panel */} +
+ {/* Mobile logo */} +
+ Logo + CommDesk
-
-

- Already have an account?{" "} - - Login Here! - -

+
+
+ + {post !== "idle" ? ( + + ) : ( +
+ {/* Step progress */} + + + {/* Form */} + +
+ {/* Step content with slide animation */} +
+ {STEP_COMPONENTS[step]()} +
+ + {/* Server error */} + {serverError && ( +
+ {serverError} +
+ )} + + {/* Navigation */} +
+ {step > 1 ? ( + + ) : ( +
+ )} + + {step < TOTAL_STEPS ? ( + + ) : ( + + )} +
+ + + + {/* Footer */} +

+ Already have an account?{" "} + + Sign in + +

+
+ )} +
); -}; - -export default SignUpPage; +} diff --git a/src/features/Auth/v1/api/signup.ts b/src/features/Auth/v1/api/signup.ts new file mode 100644 index 0000000..a6e5a09 --- /dev/null +++ b/src/features/Auth/v1/api/signup.ts @@ -0,0 +1,38 @@ +import { SignupFormData } from "../hooks/useSignupForm"; + +export type SignupResponse = { + message: string; + communityId?: string; +}; + +/** + * Submits the final community signup form. + */ +export async function submitCommunitySignup(data: SignupFormData): Promise { + const res = await fetch("/api/v1/auth/signup-community", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), // Using spread/data directly as schema matches backend requirements + }); + + const json = await res.json(); + + if (!res.ok) { + throw { status: res.status, data: json }; + } + + return json as SignupResponse; +} + +/** + * Handles logo upload. Currently simulated with FileReader. + * In production: Replace with real FormData upload to S3/Cloudinary. + */ +export async function uploadCommunityLogo(file: File): Promise { + // SIMULATION: If you need to switch to a real API, change this implementation + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.readAsDataURL(file); + }); +} diff --git a/src/features/Auth/v1/components/signup/CommunityStep.tsx b/src/features/Auth/v1/components/signup/CommunityStep.tsx new file mode 100644 index 0000000..785b926 --- /dev/null +++ b/src/features/Auth/v1/components/signup/CommunityStep.tsx @@ -0,0 +1,184 @@ +import { useRef, useState } from "react"; +import { useFormContext } from "react-hook-form"; +import { Upload, X, AlertCircle } from "lucide-react"; +import { SignupFormData } from "../../hooks/useSignupForm"; +import { uploadCommunityLogo } from "../../api/signup"; + +export default function CommunityStep() { + const { + register, + setValue, + watch, + formState: { errors }, + } = useFormContext(); + + const [uploading, setUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); + const fileRef = useRef(null); + const logoUrl = watch("communityLogo"); + + const handleFile = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setUploadError(null); + + if (!["image/jpeg", "image/png", "image/webp"].includes(file.type)) { + setUploadError("Only JPG, PNG, and WEBP files are accepted."); + if (fileRef.current) fileRef.current.value = ""; + return; + } + if (file.size > 2 * 1024 * 1024) { + setUploadError("File must be under 2MB."); + if (fileRef.current) fileRef.current.value = ""; + return; + } + + setUploading(true); + const url = await uploadCommunityLogo(file); + setValue("communityLogo", url, { shouldValidate: true }); + setUploading(false); + }; + + const handleRemoveLogo = () => { + setValue("communityLogo", ""); + setUploadError(null); + if (fileRef.current) fileRef.current.value = ""; + }; + + return ( +
+
+

Community Information

+

Tell us about your community to get started.

+
+ + {/* Logo Upload */} +
+
+
fileRef.current?.click()} + > + {uploading ? ( +
+ ) : logoUrl ? ( + Community Logo + ) : ( + + )} +
+
+ +

JPG, PNG, WEBP · Max 2MB · 400×400px recommended

+ {logoUrl && ( + + )} +
+ +
+ {uploadError && ( +
+ + {uploadError} +
+ )} +
+ + {/* Community Name */} +
+ + + {errors.communityName && ( + {errors.communityName.message} + )} +
+ + {/* Community Bio */} +
+ +