From f45eb793c78f2d1a2a327a7f97e6272a0123c595 Mon Sep 17 00:00:00 2001 From: raineer Date: Thu, 4 Sep 2025 20:49:05 +0800 Subject: [PATCH 01/10] nda signup --- .../_components/MarketingNavigation.tsx | 14 +- .../app/api/nda-document/[id]/route.ts | 10 +- apps/codebility/app/auth/actions.ts | 59 +- .../app/auth/declined/_service/actions.ts | 11 +- apps/codebility/app/auth/sign-up/page.tsx | 598 +++++++++++++++++- .../_components/DashboardTokenPoints.tsx | 25 +- apps/codebility/store/codev-store.ts | 15 +- apps/codebility/utils/nda-helpers.ts | 199 ++++++ 8 files changed, 840 insertions(+), 91 deletions(-) create mode 100644 apps/codebility/utils/nda-helpers.ts diff --git a/apps/codebility/app/(marketing)/_components/MarketingNavigation.tsx b/apps/codebility/app/(marketing)/_components/MarketingNavigation.tsx index fb4ad67ce..e5bda6acc 100644 --- a/apps/codebility/app/(marketing)/_components/MarketingNavigation.tsx +++ b/apps/codebility/app/(marketing)/_components/MarketingNavigation.tsx @@ -172,6 +172,7 @@ const UserMenu = ({ alt="Avatar" src={image_url || defaultAvatar} fill + sizes="52px" title={`${first_name}'s Avatar`} className="rounded-full" /> @@ -221,7 +222,6 @@ const UserMenu = ({ }; const Navigation = () => { - /* const supabase = createClientClientComponent(); */ const { color } = useChangeBgNavigation(); const pathname = usePathname(); const [openSheet, setOpenSheet] = useState(false); @@ -239,10 +239,8 @@ const Navigation = () => { async function fetchUser() { try { - const { - data: { session }, - error: authError, - } = await supabase.auth.getSession(); + // FIXED: Use getUser() instead of getSession() + const { data: { user }, error: authError } = await supabase.auth.getUser(); if (authError) { console.error("Auth error:", authError.message); @@ -250,7 +248,7 @@ const Navigation = () => { return; } - if (!session) { + if (!user) { // not logged in → silent setUserData(null); return; @@ -259,7 +257,7 @@ const Navigation = () => { const { data: userRow, error: fetchError } = await supabase .from("codev") .select(`*, applicant (id, codev_id)`) - .eq("id", session.user.id) + .eq("id", user.id) .single(); console.log("User Row:", userRow); @@ -334,4 +332,4 @@ const Navigation = () => { ); }; -export default Navigation; +export default Navigation; \ No newline at end of file diff --git a/apps/codebility/app/api/nda-document/[id]/route.ts b/apps/codebility/app/api/nda-document/[id]/route.ts index e16e93ded..c560ad6b1 100644 --- a/apps/codebility/app/api/nda-document/[id]/route.ts +++ b/apps/codebility/app/api/nda-document/[id]/route.ts @@ -1,16 +1,16 @@ - import { createClientServerComponent } from "@/utils/supabase/server"; - import { NextRequest, NextResponse } from "next/server"; export async function GET(request: NextRequest, props: { params: Promise<{ id: string }> }) { const params = await props.params; + try { const supabase = await createClientServerComponent(); - // Check if user is authenticated - const { data: { session } } = await supabase.auth.getSession(); - if (!session) { + // FIXED: Use getUser() instead of getSession() + const { data: { user }, error: authError } = await supabase.auth.getUser(); + + if (authError || !user) { return NextResponse.json( { error: "Unauthorized" }, { status: 401 } diff --git a/apps/codebility/app/auth/actions.ts b/apps/codebility/app/auth/actions.ts index 094457ff9..e93143d2f 100644 --- a/apps/codebility/app/auth/actions.ts +++ b/apps/codebility/app/auth/actions.ts @@ -5,8 +5,6 @@ import { useUserStore } from "@/store/codev-store"; import { Codev } from "@/types/home/codev"; import { createClientServerComponent } from "@/utils/supabase/server"; - - const uploadProfileImage = async ( file: File, folderName: string, @@ -55,7 +53,7 @@ export const signupUser = async (formData: FormData) => { // Extract form data with proper typing and convert email to lowercase const rawEmail = formData.get("email_address") as string; - const email_address = rawEmail.toLowerCase(); // Normalize the email to lowercase + const email_address = rawEmail.toLowerCase(); const password = formData.get("password") as string; const tech_stacks = JSON.parse(formData.get("tech_stacks") as string) || []; const positions: { id: number; name: string }[] = @@ -83,7 +81,7 @@ export const signupUser = async (formData: FormData) => { } // Handle profile image upload if provided - const image_url = profileImage + const image_url = profileImage && profileImage.size > 0 ? await uploadProfileImage(profileImage, "profileImage", "codebility") : null; @@ -102,12 +100,33 @@ export const signupUser = async (formData: FormData) => { if (authError) throw authError; if (!user) throw new Error("Failed to create user"); + // Extract NDA data from formData (passed from client-side localStorage) + let nda_status = false; + let nda_signature: string | undefined = undefined; + let nda_document: string | undefined = undefined; + let nda_signed_at: string | undefined = undefined; + + // Check if NDA data was included in the form submission + const ndaSigned = formData.get("ndaSigned") as string; + const signature = formData.get("ndaSignature") as string; + const document = formData.get("ndaDocument") as string; + const signedAt = formData.get("ndaSignedAt") as string; + + if (ndaSigned === "true" && signature && document && signedAt) { + nda_status = true; + nda_signature = signature; + nda_document = document; + nda_signed_at = signedAt; + + console.log("NDA data found in form submission, integrating into signup..."); + } + // Prepare user data for insertion into the "codev" table const userData: Codev = { id: user.id, first_name: formData.get("first_name") as string, last_name: formData.get("last_name") as string, - email_address, // already normalized to lowercase + email_address, phone_number: formData.get("phone_number") as string, address: null, about: (formData.get("about") as string) || null, @@ -117,9 +136,12 @@ export const signupUser = async (formData: FormData) => { tech_stacks: tech_stacks as string[], image_url, availability_status: true, - nda_status: false, + nda_status, + nda_signature, + nda_document, + nda_signed_at, level: {}, - application_status: "applying", // Matches DB constraint + application_status: "applying", rejected_count: 0, facebook: (formData.get("facebook") as string) || null, linkedin: (formData.get("linkedin") as string) || null, @@ -140,21 +162,26 @@ export const signupUser = async (formData: FormData) => { if (insertError) throw insertError; - // create applicant entry + // Create applicant entry const { error } = await supabase .from("applicant") - .insert( - { - codev_id: user.id - } - ) + .insert({ + codev_id: user.id + }); if (error) { - throw error + throw error; } setUser(userData); - return { success: true, data: user }; + + // Return success with all necessary data + return { + success: true, + data: user, + ndaProcessed: nda_status, + message: "Account created successfully! Redirecting to sign-in page..." + }; } catch (error: any) { console.error("Signup error:", error); return { @@ -247,4 +274,4 @@ export const resendVerificationEmail = async (email: string) => { error: error.message || "Failed to resend verification email" }; } -}; +}; \ No newline at end of file diff --git a/apps/codebility/app/auth/declined/_service/actions.ts b/apps/codebility/app/auth/declined/_service/actions.ts index e080597cc..3edab90c5 100644 --- a/apps/codebility/app/auth/declined/_service/actions.ts +++ b/apps/codebility/app/auth/declined/_service/actions.ts @@ -1,6 +1,5 @@ "use server"; - import { createClientServerComponent } from "@/utils/supabase/server"; import { redirect } from "next/navigation"; @@ -8,10 +7,10 @@ export async function getUserData(): Promise { try { const supabase = await createClientServerComponent(); - const { - data: { session }, - } = await supabase.auth.getSession(); - if (!session) { + // FIXED: Use getUser() instead of getSession() + const { data: { user }, error: authError } = await supabase.auth.getUser(); + + if (authError || !user) { redirect("/auth/sign-in"); } @@ -20,7 +19,7 @@ export async function getUserData(): Promise { .select(`*, applicant (*) `) - .eq("id", session.user.id) + .eq("id", user.id) .single(); if (!data) { diff --git a/apps/codebility/app/auth/sign-up/page.tsx b/apps/codebility/app/auth/sign-up/page.tsx index 95d9cadfd..fa314535e 100644 --- a/apps/codebility/app/auth/sign-up/page.tsx +++ b/apps/codebility/app/auth/sign-up/page.tsx @@ -1,41 +1,571 @@ -import Image from "next/image"; -import Link from "next/link"; +"use client"; -import SignUpForm from "./_components/SignUpForm"; +import React, { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "react-hot-toast"; +import { z } from "zod"; +import { CheckCircle, AlertCircle, ChevronDown, Eye, EyeOff } from "lucide-react"; +import { useRouter } from "next/navigation"; -export const dynamic = "force-dynamic"; -export const revalidate = 0; +import { Button } from "@codevs/ui/button"; +import { Input } from "@codevs/ui/input"; +import { Label } from "@codevs/ui/label"; +import { Checkbox } from "@codevs/ui/checkbox"; +import { Textarea } from "@codevs/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@codevs/ui/select"; + +import { signupUser } from "@/app/auth/actions"; +import { getNdaDataFromStorage, hasCompleteNdaData, cleanupNdaStorage } from "@/utils/nda-helpers"; + +const SignupFormSchema = z.object({ + first_name: z.string().min(1, "First name is required"), + last_name: z.string().min(1, "Last name is required"), + email_address: z.string().email("Please enter a valid email address"), + phone_number: z.string().min(1, "Phone number is required"), + password: z.string().min(8, "Password must be at least 8 characters"), + confirmPassword: z.string(), + about: z.string().optional(), + portfolio_website: z.string().url().optional().or(z.literal("")), + positions: z.array(z.object({ id: z.number(), name: z.string() })).min(1, "Please select at least one position"), + tech_stacks: z.array(z.string()).min(1, "Please select at least one tech stack"), + years_of_experience: z.number().min(0, "Experience must be 0 or more years"), + facebook: z.string().optional(), + linkedin: z.string().optional(), + github: z.string().optional(), + discord: z.string().optional(), + privacyPolicy: z.boolean().refine(val => val === true, "You must agree to the Privacy Policy"), + ndaAgreement: z.boolean().refine(val => val === true, "You must agree to the Non-Disclosure Agreement"), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], +}); + +type SignupFormData = z.infer; + +const POSITIONS = [ + { id: 1, name: "Frontend Developer" }, + { id: 2, name: "Backend Developer" }, + { id: 3, name: "Full Stack Developer" }, + { id: 4, name: "UI/UX Designer" }, + { id: 5, name: "QA Engineer" }, +]; + +const TECH_STACKS = [ + "javascript", "typescript", "react", "nextjs", "nodejs", + "python", "php", "java", "angular", "vue", "laravel", "django" +]; + +export default function SignUpForm() { + const router = useRouter(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [profileImage, setProfileImage] = useState(null); + const [ndaSigned, setNdaSigned] = useState(false); + const [selectedPositions, setSelectedPositions] = useState([]); + const [selectedTechStacks, setSelectedTechStacks] = useState([]); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const form = useForm({ + resolver: zodResolver(SignupFormSchema), + defaultValues: { + first_name: "", + last_name: "", + email_address: "", + phone_number: "", + password: "", + confirmPassword: "", + about: "", + portfolio_website: "", + positions: [], + tech_stacks: [], + years_of_experience: 0, + facebook: "", + linkedin: "", + github: "", + discord: "", + privacyPolicy: false, + ndaAgreement: false, + }, + }); + + // Initialize NDA status + useEffect(() => { + const initialNdaStatus = hasCompleteNdaData(); + setNdaSigned(initialNdaStatus); + + if (initialNdaStatus) { + form.setValue("ndaAgreement", true); + } + + const handleMessage = (event: MessageEvent) => { + if (event.data?.type === "NDA_SIGNED" && event.data?.signed === true) { + setNdaSigned(true); + form.setValue("ndaAgreement", true); + toast.success("NDA signed successfully! You can now complete your registration."); + } + }; + + window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, [form]); + + const handleSignNda = () => { + const ndaUrl = "/nda-signing/public"; + const popup = window.open(ndaUrl, "nda-signing", "width=800,height=600,scrollbars=yes,resizable=yes"); + + if (!popup) { + toast.error("Please allow pop-ups to sign the NDA"); + return; + } + + const checkClosed = setInterval(() => { + if (popup.closed) { + clearInterval(checkClosed); + setTimeout(() => { + const newNdaStatus = hasCompleteNdaData(); + setNdaSigned(newNdaStatus); + if (newNdaStatus) { + form.setValue("ndaAgreement", true); + } + }, 500); + } + }, 1000); + }; + + const onSubmit = async (data: SignupFormData) => { + try { + setIsSubmitting(true); + + if (!ndaSigned) { + toast.error("Please sign the NDA before submitting your application"); + return; + } + + const formData = new FormData(); + + formData.append("first_name", data.first_name); + formData.append("last_name", data.last_name); + formData.append("email_address", data.email_address); + formData.append("phone_number", data.phone_number); + formData.append("password", data.password); + formData.append("about", data.about || ""); + formData.append("portfolio_website", data.portfolio_website || ""); + formData.append("years_of_experience", data.years_of_experience.toString()); + formData.append("facebook", data.facebook || ""); + formData.append("linkedin", data.linkedin || ""); + formData.append("github", data.github || ""); + formData.append("discord", data.discord || ""); + formData.append("positions", JSON.stringify(selectedPositions)); + formData.append("tech_stacks", JSON.stringify(selectedTechStacks)); + + if (profileImage) { + formData.append("profileImage", profileImage); + } + + const ndaData = getNdaDataFromStorage(); + if (hasCompleteNdaData()) { + formData.append("ndaSigned", "true"); + formData.append("ndaSignature", ndaData.signature!); + formData.append("ndaDocument", ndaData.document!); + formData.append("ndaSignedAt", ndaData.signedAt!); + } + + const result = await signupUser(formData); + + if (result.success) { + if (result.ndaProcessed) { + cleanupNdaStorage(); + } + toast.success("Account created successfully! Please check your email to verify your account."); + + // Show redirect message + toast.success("Redirecting to sign-in page...", { + duration: 2000, + }); + + // Redirect after 2 seconds + setTimeout(() => { + router.push('/auth/signin'); + }, 2000); + + } else { + toast.error(result.error || "Failed to create account"); + } + + } catch (error) { + console.error("Signup error:", error); + toast.error("An unexpected error occurred. Please try again."); + } finally { + setIsSubmitting(false); + } + }; + + const handleImageChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + if (file.size > 5 * 1024 * 1024) { + toast.error("Image size must be less than 5MB"); + return; + } + + const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; + if (!allowedTypes.includes(file.type)) { + toast.error("Please select a valid image file (JPEG, PNG, or WebP)"); + return; + } + + setProfileImage(file); + } + }; -const SignUpPage = () => { return ( -
-
- - Codebility Logo +
+ {/* Upload Picture */} +
+
+ + + +
+ + - -

Create new account

-

- To use Codebility, please enter your details -

-

- All fields not labeled {"optional"} are required. -

- -

- Have an account?{" "} - - Sign In - -

-
+ {profileImage && ( +

{profileImage.name}

+ )} +
+ + {/* Main Form Grid */} +
+ {/* Column 1 */} +
+
+ + + {form.formState.errors.first_name && ( +

{form.formState.errors.first_name.message}

+ )} +
+ +
+ + + {form.formState.errors.last_name && ( +

{form.formState.errors.last_name.message}

+ )} +
+ +
+ + + {form.formState.errors.positions && ( +

{form.formState.errors.positions.message}

+ )} +
+ +
+ + + {form.formState.errors.years_of_experience && ( +

{form.formState.errors.years_of_experience.message}

+ )} +
+ +
+ + + {form.formState.errors.tech_stacks && ( +

{form.formState.errors.tech_stacks.message}

+ )} +
+ +
+ +