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 ? 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