diff --git a/app/auth/verify-email/VerifyEmail.tsx b/app/auth/verify-email/VerifyEmail.tsx new file mode 100644 index 0000000..596a3d9 --- /dev/null +++ b/app/auth/verify-email/VerifyEmail.tsx @@ -0,0 +1,275 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import Link from "next/link"; +import { useSearchParams, useRouter } from "next/navigation"; +import { Button, Input, useToast, Card, Spinner } from "@/components/ui"; +import { authApi } from "@/lib/api/auth"; +import { Mail, ArrowLeft, RefreshCw, CheckCircle2, AlertCircle, Edit2 } from "lucide-react"; + +const COOLDOWN_SECONDS = 60; + +const VerifyEmail = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const toast = useToast(); + const token = searchParams.get("token"); + + const [email, setEmail] = useState(""); + const [isVerifying, setIsVerifying] = useState(!!token); + const [isResending, setIsResending] = useState(false); + const [isChangingEmail, setIsChangingEmail] = useState(false); + const [newEmail, setNewEmail] = useState(""); + const [cooldown, setCooldown] = useState(0); + const [status, setStatus] = useState<"pending" | "success" | "error">(token ? "pending" : "pending"); + const [errorMessage, setErrorMessage] = useState(""); + + const handleVerify = useCallback(async (verificationToken: string) => { + setIsVerifying(true); + setStatus("pending"); + try { + await authApi.verifyEmail({ token: verificationToken }); + setStatus("success"); + toast.success("Email verified successfully! You can now log in."); + // Redirect to login after 3 seconds + setTimeout(() => { + router.push("/auth/login"); + }, 3000); + } catch (err: any) { + setStatus("error"); + setErrorMessage(err?.response?.data?.message || "Verification failed. The link may have expired or is invalid."); + toast.error("Verification failed"); + } finally { + setIsVerifying(false); + } + }, [router, toast]); + + // Handle auto-verification if token is present + useEffect(() => { + if (token) { + handleVerify(token); + } + }, [token, handleVerify]); + + // Cooldown timer + useEffect(() => { + if (cooldown > 0) { + const timer = setTimeout(() => setCooldown(cooldown - 1), 1000); + return () => clearTimeout(timer); + } + }, [cooldown]); + + const handleResend = async () => { + if (cooldown > 0) return; + + setIsResending(true); + try { + // In a real app, you might want to get this from a state or context if available + // For now we use the email entered/changed + const targetEmail = email || "your email"; + await authApi.resendVerification({ email: targetEmail }); + toast.success(`Verification link sent to ${targetEmail}`); + setCooldown(COOLDOWN_SECONDS); + } catch (err: any) { + toast.error(err?.response?.data?.message || "Failed to resend verification link."); + } finally { + setIsResending(false); + } + }; + + const handleChangeEmail = async () => { + if (!newEmail || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) { + toast.error("Please enter a valid email address"); + return; + } + + setIsResending(true); + try { + await authApi.changeEmail({ email: newEmail }); + setEmail(newEmail); + setIsChangingEmail(false); + toast.success("Email updated and new verification link sent!"); + setCooldown(COOLDOWN_SECONDS); + } catch (err: any) { + toast.error(err?.response?.data?.message || "Failed to update email."); + } finally { + setIsResending(false); + } + }; + + const renderContent = () => { + if (isVerifying) { + return ( +
+ +

Verifying your email address...

+
+ ); + } + + if (status === "success") { + return ( +
+
+
+ +
+
+
+

Email Verified!

+

Your account is now fully activated. We're redirecting you to the login page.

+
+ +
+ ); + } + + if (status === "error") { + return ( +
+
+
+ +
+
+
+

Verification Failed

+

{errorMessage}

+

Don't worry, you can request a new link below.

+
+
+ + +
+
+ ); + } + + // Default "pending" view (the initial view if no token or after successful resend) + return ( +
+
+
+
+ +
+
+

Check your email

+

+ We've sent a verification link to {email || "your email address"}. +

+
+ + {isChangingEmail ? ( +
+

Update Email Address

+ setNewEmail(e.target.value)} + placeholder="Enter new email" + autoFocus + /> +
+ + +
+
+ ) : ( +
+ + + +
+ )} + +
+ + + Back to Sign In + +
+
+ ); + }; + + return ( +
+ + {renderContent()} + + + {/* Visual background decoration */} +
+
+
+ ); +}; + +export default VerifyEmail; diff --git a/app/auth/verify-email/page.tsx b/app/auth/verify-email/page.tsx new file mode 100644 index 0000000..0c5c9d6 --- /dev/null +++ b/app/auth/verify-email/page.tsx @@ -0,0 +1,11 @@ +import VerifyEmail from './VerifyEmail'; +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Verify Email | StellarAid', + description: 'Verify your email address to activate your StellarAid account.', +}; + +export default function VerifyEmailPage() { + return ; +} diff --git a/components/ProfileDropdown.tsx b/components/ProfileDropdown.tsx index cd3c041..b1754bd 100644 --- a/components/ProfileDropdown.tsx +++ b/components/ProfileDropdown.tsx @@ -4,6 +4,7 @@ import { useState, useRef, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { User, Settings, LayoutDashboard, LogOut, ChevronDown } from 'lucide-react'; +import Image from 'next/image'; import { useAuthStore } from '@/store/authStore'; export default function ProfileDropdown() { @@ -108,10 +109,13 @@ export default function ProfileDropdown() { {/* User Avatar */}
{user?.avatar ? ( - {user.name} ) : ( {user?.name ? getUserInitials(user.name) : 'U'} diff --git a/lib/api/auth.ts b/lib/api/auth.ts index 16066b3..2681978 100644 --- a/lib/api/auth.ts +++ b/lib/api/auth.ts @@ -1,5 +1,12 @@ import { apiClient } from "./interceptors"; -import { LoginResponse, ApiResponse, RegisterRequest } from "@/types/api"; +import { + LoginResponse, + ApiResponse, + RegisterRequest, + VerifyEmailRequest, + ResendEmailRequest, + ChangeEmailRequest +} from "@/types/api"; export const authApi = { login: async (credentials: any): Promise> => { @@ -10,9 +17,7 @@ export const authApi = { return response.data; }, - register: async ( - data: RegisterRequest, - ): Promise> => { + register: async (data: RegisterRequest): Promise> => { const response = await apiClient.post>( "/auth/register", data, @@ -29,9 +34,7 @@ export const authApi = { return response.data; }, - forgotPassword: async (data: { - email: string; - }): Promise> => { + forgotPassword: async (data: { email: string }): Promise> => { const response = await apiClient.post>( "/users/forgot-password", data, @@ -39,10 +42,7 @@ export const authApi = { return response.data; }, - resetPassword: async (data: { - token: string; - password: string; - }): Promise> => { + resetPassword: async (data: { token: string; password: string }): Promise> => { const response = await apiClient.post>( "/users/reset-password", data, @@ -50,6 +50,30 @@ export const authApi = { return response.data; }, + verifyEmail: async (data: VerifyEmailRequest): Promise> => { + const response = await apiClient.post>( + "/auth/verify-email", + data, + ); + return response.data; + }, + + resendVerification: async (data: ResendEmailRequest): Promise> => { + const response = await apiClient.post>( + "/auth/resend-verification", + data, + ); + return response.data; + }, + + changeEmail: async (data: ChangeEmailRequest): Promise> => { + const response = await apiClient.patch>( + "/auth/change-email", + data, + ); + return response.data; + }, + refreshToken: async (): Promise> => { const response = await apiClient.post>( "/auth/refresh-token", diff --git a/types/api.ts b/types/api.ts index a4e17e5..588f219 100644 --- a/types/api.ts +++ b/types/api.ts @@ -21,8 +21,21 @@ export interface LoginResponse { export interface RegisterRequest { email: string; - name: string; - password?: string; // Optional if using wallet-only or social + role: "donor" | "creator"; + password?: string; + confirmPassword?: string; +} + +export interface VerifyEmailRequest { + token: string; +} + +export interface ResendEmailRequest { + email: string; +} + +export interface ChangeEmailRequest { + email: string; } // Project Types