diff --git a/app/(admin)/admin-layout-client.tsx b/app/(admin)/admin-layout-client.tsx
new file mode 100644
index 00000000..267da70c
--- /dev/null
+++ b/app/(admin)/admin-layout-client.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { useState } from "react";
+import { Bell, User, Menu } from "lucide-react";
+import { AdminSidebar } from "@/components/admin/AdminSidebar";
+import { AdminGuard } from "@/components/admin/AdminGuard";
+import { usePathname } from "next/navigation";
+
+export default function AdminLayoutClient({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const pathname = usePathname();
+ const title = pathname.split("/").filter(Boolean).pop() || "Admin";
+ const capitalisedTitle =
+ title.charAt(0).toUpperCase() + title.slice(1).replace(/-/g, " ");
+
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false);
+
+ return (
+
+
+ {/* Sidebar */}
+
setIsSidebarOpen(false)}
+ />
+
+ {/* Main Content Area */}
+
+ {/* Topbar */}
+
+
+ {/* Page Content */}
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx
index 9b42c637..0e28eded 100644
--- a/app/(admin)/layout.tsx
+++ b/app/(admin)/layout.tsx
@@ -1,76 +1,14 @@
-"use client";
+import type { Metadata } from "next";
+import AdminLayoutClient from "./admin-layout-client";
-import { useState } from "react";
-import { Bell, User, Menu } from "lucide-react";
-import { AdminSidebar } from "@/components/admin/AdminSidebar";
-import { AdminGuard } from "@/components/admin/AdminGuard";
-import { usePathname } from "next/navigation";
+export const metadata: Metadata = {
+ robots: { index: false, follow: false },
+};
export default function AdminLayout({
- children,
+ children,
}: {
- children: React.ReactNode;
+ children: React.ReactNode;
}) {
- const pathname = usePathname();
- const title = pathname.split("/").filter(Boolean).pop() || "Admin";
- const capitalisedTitle =
- title.charAt(0).toUpperCase() + title.slice(1).replace(/-/g, " ");
-
- const [isSidebarOpen, setIsSidebarOpen] = useState(false);
-
- return (
-
-
- {/* Sidebar */}
-
setIsSidebarOpen(false)}
- />
-
- {/* Main Content Area */}
-
- {/* Topbar */}
-
-
- {/* Page Content */}
-
- {children}
-
-
-
-
- );
+ return {children};
}
diff --git a/app/(auth)/forgot-password/forgot-password-client.tsx b/app/(auth)/forgot-password/forgot-password-client.tsx
new file mode 100644
index 00000000..0cd8ffba
--- /dev/null
+++ b/app/(auth)/forgot-password/forgot-password-client.tsx
@@ -0,0 +1,221 @@
+"use client";
+
+import { useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { CheckCircle2 } from "lucide-react";
+import { forgotPassword } from "@/lib/api/auth";
+
+export default function ForgotPasswordClient() {
+ const router = useRouter();
+ const [email, setEmail] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [status, setStatus] = useState<"form" | "confirmation">("form");
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError("");
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+ if (!emailRegex.test(email)) {
+ setError("Please enter a valid email address");
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ await forgotPassword({ email });
+
+ setStatus("confirmation");
+
+ // Redirect after short delay
+ setTimeout(() => {
+ router.push(
+ `/reset-password?email=${encodeURIComponent(email)}`,
+ );
+ }, 2000);
+ } catch (err: unknown) {
+ setError(err instanceof Error ? err.message : 'Something went wrong');
+ } finally {
+ setIsLoading(false);
+ }
+
+ {
+ /*
+ setTimeout(() => {
+ setIsLoading(false);
+ setStatus("confirmation");
+ }, 1000);
+
+ setTimeout(() => {
+ setStatus("form");
+ router.push("/reset-password");
+ }, 3000);
+ */
+ }
+ };
+
+ const ConfirmationModal = () => (
+
+
+
+
+ Reset Code Sent
+
+
+
+
+ Check your email for a reset code
+
+
+
+ );
+
+ return (
+
+ {/* Desktop Header */}
+
+
+ {/* Desktop View */}
+
+
+ {status === "form" ? (
+
+
+
+ Forgot password
+
+
+ Please enter your email address and you we would
+ send you an OTP
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ ) : (
+
+ )}
+
+
+ {/* Mobile View */}
+
+ {status === "form" ? (
+
+
+
+
+
+
+
+ Forgot password
+
+
+ Please enter your email address and you we would
+ send you an OTP
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/app/(auth)/forgot-password/page.tsx b/app/(auth)/forgot-password/page.tsx
index 28a0cc68..3e703e4b 100644
--- a/app/(auth)/forgot-password/page.tsx
+++ b/app/(auth)/forgot-password/page.tsx
@@ -1,221 +1,10 @@
-"use client";
+import type { Metadata } from "next";
+import ForgotPasswordClient from "./forgot-password-client";
-import { useState } from "react";
-import Image from "next/image";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { CheckCircle2 } from "lucide-react";
-import { forgotPassword } from "@/lib/api/auth";
+export const metadata: Metadata = {
+ title: "Reset Password",
+};
export default function ForgotPasswordPage() {
- const router = useRouter();
- const [email, setEmail] = useState("");
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState("");
- const [status, setStatus] = useState<"form" | "confirmation">("form");
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setError("");
-
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
-
- if (!emailRegex.test(email)) {
- setError("Please enter a valid email address");
- return;
- }
-
- setIsLoading(true);
-
- try {
- await forgotPassword({ email });
-
- setStatus("confirmation");
-
- // Redirect after short delay
- setTimeout(() => {
- router.push(
- `/reset-password?email=${encodeURIComponent(email)}`,
- );
- }, 2000);
- } catch (err: unknown) {
- setError(err instanceof Error ? err.message : 'Something went wrong');
- } finally {
- setIsLoading(false);
- }
-
- {
- /*
- setTimeout(() => {
- setIsLoading(false);
- setStatus("confirmation");
- }, 1000);
-
- setTimeout(() => {
- setStatus("form");
- router.push("/reset-password");
- }, 3000);
- */
- }
- };
-
- const ConfirmationModal = () => (
-
-
-
-
- Reset Code Sent
-
-
-
-
- Check your email for a reset code
-
-
-
- );
-
- return (
-
- {/* Desktop Header */}
-
-
- {/* Desktop View */}
-
-
- {status === "form" ? (
-
-
-
- Forgot password
-
-
- Please enter your email address and you we would
- send you an OTP
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
- ) : (
-
- )}
-
-
- {/* Mobile View */}
-
- {status === "form" ? (
-
-
-
-
-
-
-
- Forgot password
-
-
- Please enter your email address and you we would
- send you an OTP
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
- ) : (
-
- )}
-
-
- );
+ return ;
}
diff --git a/app/(auth)/reset-password/page.tsx b/app/(auth)/reset-password/page.tsx
index 62550e4c..2c8fd9ef 100644
--- a/app/(auth)/reset-password/page.tsx
+++ b/app/(auth)/reset-password/page.tsx
@@ -1,422 +1,10 @@
-"use client";
+import type { Metadata } from "next";
+import ResetPasswordClient from "./reset-password-client";
-import { Suspense, useEffect, useRef, useState } from "react";
-import { useRouter, useSearchParams } from "next/navigation";
-import { resetPassword } from "@/lib/api/auth";
-
-import Image from "next/image";
+export const metadata: Metadata = {
+ title: "Reset Password",
+};
export default function ResetPasswordPage() {
- return (
-
-
-
- );
-}
-
-function ResetPasswordContent() {
- const router = useRouter();
- const searchParams = useSearchParams();
- const email = searchParams.get("email") || "";
-
- const [otp, setOtp] = useState(["", "", "", "", "", ""]);
- const [newPassword, setNewPassword] = useState("");
- const [confirmPassword, setConfirmPassword] = useState("");
-
- const [errors, setErrors] = useState<{ [key: string]: string }>({});
- const [showPassword, setShowPassword] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [apiError, setApiError] = useState("");
- const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
-
- useEffect(() => {
- inputRefs.current[0]?.focus();
- }, []);
-
- const handleChange = (index: number, value: string) => {
- if (!/^\d*$/.test(value)) return;
-
- const updatedOtp = [...otp];
- updatedOtp[index] = value.slice(-1);
- setOtp(updatedOtp);
-
- if (value && index < 5) {
- inputRefs.current[index + 1]?.focus();
- }
- };
-
- const handleKeyDown = (
- index: number,
- e: React.KeyboardEvent,
- ) => {
- if (e.key === "Backspace" && !otp[index] && index > 0) {
- inputRefs.current[index - 1]?.focus();
- }
- };
-
- const handlePaste = (e: React.ClipboardEvent) => {
- e.preventDefault();
- const pastedData = e.clipboardData.getData("text").slice(0, 6);
- if (!/^\d+$/.test(pastedData)) return;
-
- const updatedOtp = [...otp];
- pastedData.split("").forEach((char, index) => {
- if (index < 6) updatedOtp[index] = char;
- });
- setOtp(updatedOtp);
- const lastFilledIndex = Math.min(pastedData.length, 5);
- inputRefs.current[lastFilledIndex]?.focus();
- };
-
- const validate = () => {
- const newErrors: { [key: string]: string } = {};
- const otpCode = otp.join("");
-
- if (otpCode.length !== 6) {
- newErrors.otp = "Please enter all 6 digits";
- }
-
- if (!newPassword) newErrors.newPassword = "Password is required";
- else if (newPassword.length < 8)
- newErrors.newPassword = "Password must be at least 8 characters";
-
- if (newPassword !== confirmPassword) {
- newErrors.confirmPassword = "Passwords do not match";
- }
-
- setErrors(newErrors);
- return Object.keys(newErrors).length === 0;
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!validate()) return;
-
- if (!email) {
- setApiError("Session expired. Please start again.");
- return;
- }
-
- const otpCode = otp.join("");
-
- setIsLoading(true);
- setApiError("");
-
- try {
- await resetPassword({
- email,
- otp: otpCode,
- password: newPassword,
- });
-
- router.push("/sign-in?reset=success");
- } catch (err) {
- setApiError(
- err instanceof Error ? err.message : "Invalid or expired OTP",
- );
- } finally {
- setIsLoading(false);
- }
- };
-
- return (
-
- {/* Desktop Header */}
-
-
- {/* Desktop View */}
-
-
-
-
- Reset Password
-
-
-
- {apiError && (
-
- {apiError}
-
- )}
-
-
-
-
-
- {/* Mobile View */}
-
-
-
-
-
-
-
-
- Reset Password
-
-
-
- {apiError && (
-
- {apiError}
-
- )}
-
-
-
-
-
- );
+ return ;
}
diff --git a/app/(auth)/reset-password/reset-password-client.tsx b/app/(auth)/reset-password/reset-password-client.tsx
new file mode 100644
index 00000000..de38c3c6
--- /dev/null
+++ b/app/(auth)/reset-password/reset-password-client.tsx
@@ -0,0 +1,422 @@
+"use client";
+
+import { Suspense, useEffect, useRef, useState } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { resetPassword } from "@/lib/api/auth";
+
+import Image from "next/image";
+
+export default function ResetPasswordClient() {
+ return (
+
+
+
+ );
+}
+
+function ResetPasswordContent() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const email = searchParams.get("email") || "";
+
+ const [otp, setOtp] = useState(["", "", "", "", "", ""]);
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+
+ const [errors, setErrors] = useState<{ [key: string]: string }>({});
+ const [showPassword, setShowPassword] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [apiError, setApiError] = useState("");
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
+
+ useEffect(() => {
+ inputRefs.current[0]?.focus();
+ }, []);
+
+ const handleChange = (index: number, value: string) => {
+ if (!/^\d*$/.test(value)) return;
+
+ const updatedOtp = [...otp];
+ updatedOtp[index] = value.slice(-1);
+ setOtp(updatedOtp);
+
+ if (value && index < 5) {
+ inputRefs.current[index + 1]?.focus();
+ }
+ };
+
+ const handleKeyDown = (
+ index: number,
+ e: React.KeyboardEvent,
+ ) => {
+ if (e.key === "Backspace" && !otp[index] && index > 0) {
+ inputRefs.current[index - 1]?.focus();
+ }
+ };
+
+ const handlePaste = (e: React.ClipboardEvent) => {
+ e.preventDefault();
+ const pastedData = e.clipboardData.getData("text").slice(0, 6);
+ if (!/^\d+$/.test(pastedData)) return;
+
+ const updatedOtp = [...otp];
+ pastedData.split("").forEach((char, index) => {
+ if (index < 6) updatedOtp[index] = char;
+ });
+ setOtp(updatedOtp);
+ const lastFilledIndex = Math.min(pastedData.length, 5);
+ inputRefs.current[lastFilledIndex]?.focus();
+ };
+
+ const validate = () => {
+ const newErrors: { [key: string]: string } = {};
+ const otpCode = otp.join("");
+
+ if (otpCode.length !== 6) {
+ newErrors.otp = "Please enter all 6 digits";
+ }
+
+ if (!newPassword) newErrors.newPassword = "Password is required";
+ else if (newPassword.length < 8)
+ newErrors.newPassword = "Password must be at least 8 characters";
+
+ if (newPassword !== confirmPassword) {
+ newErrors.confirmPassword = "Passwords do not match";
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!validate()) return;
+
+ if (!email) {
+ setApiError("Session expired. Please start again.");
+ return;
+ }
+
+ const otpCode = otp.join("");
+
+ setIsLoading(true);
+ setApiError("");
+
+ try {
+ await resetPassword({
+ email,
+ otp: otpCode,
+ password: newPassword,
+ });
+
+ router.push("/sign-in?reset=success");
+ } catch (err) {
+ setApiError(
+ err instanceof Error ? err.message : "Invalid or expired OTP",
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ {/* Desktop Header */}
+
+
+ {/* Desktop View */}
+
+
+
+
+ Reset Password
+
+
+
+ {apiError && (
+
+ {apiError}
+
+ )}
+
+
+
+
+
+ {/* Mobile View */}
+
+
+
+
+
+
+
+
+ Reset Password
+
+
+
+ {apiError && (
+
+ {apiError}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx
index 891bc26e..d08a5e9e 100644
--- a/app/(auth)/sign-in/page.tsx
+++ b/app/(auth)/sign-in/page.tsx
@@ -1,316 +1,10 @@
-'use client';
+import type { Metadata } from "next";
+import SignInClient from "./sign-in-client";
-import Image from 'next/image';
-import Link from 'next/link';
-import { login } from '@/lib/api/auth';
-import { useRouter } from 'next/navigation';
-import { useState } from 'react';
+export const metadata: Metadata = {
+ title: "Sign In",
+};
export default function SignInPage() {
- const router = useRouter();
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [showPassword, setShowPassword] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState('');
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setError('');
-
- if (!email || !password) {
- setError('Please fill in all fields');
- return;
- }
-
- setIsLoading(true);
-
- try {
- await login({ email, password });
- setIsLoading(false);
- sessionStorage.setItem('login-email', email);
- router.push('/verify-otp');
- } catch (err: unknown) {
- setIsLoading(false);
- setError(err instanceof Error ? err.message : 'Login failed');
- }
- };
-
- return (
-
- {/* Desktop Header */}
-
-
-
-
- Don't have an account?{' '}
-
- Sign up
-
-
-
-
-
- {/* Desktop View */}
-
-
-
-
Sign in
-
- Hey, Welcome back
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
-
- Don't have an account?{' '}
-
- Sign up
-
-
-
-
-
- {/* Mobile View */}
-
-
-
-
-
-
-
-
Sign in
-
- Hey, welcome back
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
-
- Don't have an account?{' '}
-
- Sign up
-
-
-
-
-
- );
+ return ;
}
diff --git a/app/(auth)/sign-in/sign-in-client.tsx b/app/(auth)/sign-in/sign-in-client.tsx
new file mode 100644
index 00000000..b708bd31
--- /dev/null
+++ b/app/(auth)/sign-in/sign-in-client.tsx
@@ -0,0 +1,316 @@
+'use client';
+
+import Image from 'next/image';
+import Link from 'next/link';
+import { login } from '@/lib/api/auth';
+import { useRouter } from 'next/navigation';
+import { useState } from 'react';
+
+export default function SignInClient() {
+ const router = useRouter();
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+
+ if (!email || !password) {
+ setError('Please fill in all fields');
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ await login({ email, password });
+ setIsLoading(false);
+ sessionStorage.setItem('login-email', email);
+ router.push('/verify-otp');
+ } catch (err: unknown) {
+ setIsLoading(false);
+ setError(err instanceof Error ? err.message : 'Login failed');
+ }
+ };
+
+ return (
+
+ {/* Desktop Header */}
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+
+
+ {/* Desktop View */}
+
+
+
+
Sign in
+
+ Hey, Welcome back
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+
+
+ {/* Mobile View */}
+
+
+
+
+
+
+
+
Sign in
+
+ Hey, welcome back
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up
+
+
+
+
+
+ );
+}
diff --git a/app/(auth)/verify-otp/page.tsx b/app/(auth)/verify-otp/page.tsx
index d9c8e786..1b45d401 100644
--- a/app/(auth)/verify-otp/page.tsx
+++ b/app/(auth)/verify-otp/page.tsx
@@ -1,279 +1,10 @@
-'use client';
+import type { Metadata } from "next";
+import VerifyOtpClient from "./verify-otp-client";
-import { verifyLoginOtp, resendLoginOtp } from '@/lib/api/auth';
-import { useEffect, useRef, useState } from 'react';
-
-import Image from 'next/image';
-import Link from 'next/link';
-import { useAuthStore } from '@/hooks/use-auth-store';
-import { useRouter } from 'next/navigation';
+export const metadata: Metadata = {
+ title: "Verify OTP",
+};
export default function VerifyOtpPage() {
- const router = useRouter();
- const setAuth = useAuthStore((s) => s.setAuth);
- const [otp, setOtp] = useState(['', '', '', '', '', '']);
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState('');
- const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
-
- useEffect(() => {
- inputRefs.current[0]?.focus();
- }, []);
-
- const handleChange = (index: number, value: string) => {
- if (!/^\d*$/.test(value)) return;
-
- const newOtp = [...otp];
- newOtp[index] = value.slice(-1);
- setOtp(newOtp);
- setError('');
-
- if (value && index < 5) {
- inputRefs.current[index + 1]?.focus();
- }
- };
-
- const handleKeyDown = (
- index: number,
- e: React.KeyboardEvent,
- ) => {
- if (e.key === 'Backspace' && !otp[index] && index > 0) {
- inputRefs.current[index - 1]?.focus();
- }
- };
-
- const handlePaste = (e: React.ClipboardEvent) => {
- e.preventDefault();
- const pastedData = e.clipboardData.getData('text').slice(0, 6);
- if (!/^\d+$/.test(pastedData)) return;
-
- const newOtp = [...otp];
- pastedData.split('').forEach((char, index) => {
- if (index < 6) newOtp[index] = char;
- });
- setOtp(newOtp);
-
- const lastFilledIndex = Math.min(pastedData.length, 5);
- inputRefs.current[lastFilledIndex]?.focus();
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- const otpCode = otp.join('');
- if (otpCode.length !== 6) {
- setError('Please enter all 6 digits');
- return;
- }
- const storedEmail = sessionStorage.getItem('login-email');
- if (!storedEmail) {
- setError('No email found. Please sign in again.');
- return;
- }
- setIsLoading(true);
- setError('');
- try {
- const res = await verifyLoginOtp({ email: storedEmail, otp: otpCode }) as { user: { id: string; firstName: string; lastName: string; email: string; role: 'USER' | 'ADMIN' }; accessToken: string; refreshToken: string };
- const fullName = [res.user.firstName, res.user.lastName].filter(Boolean).join(' ');
- setAuth({ ...res.user, name: fullName }, res.accessToken, res.refreshToken);
- setIsLoading(false);
- router.push('/dashboard');
- } catch (err: unknown) {
- setIsLoading(false);
- setError(err instanceof Error ? err.message : 'Invalid or expired OTP');
- }
- };
-
- const handleResend = async () => {
- setOtp(['', '', '', '', '', '']);
- setError('');
- inputRefs.current[0]?.focus();
- const storedEmail = sessionStorage.getItem('login-email');
- if (!storedEmail) {
- setError('Missing email. Please sign in again.');
- return;
- }
- try {
- await resendLoginOtp({ email: storedEmail });
- } catch {
- setError('Failed to resend code');
- }
- };
-
- const isComplete = otp.every((digit) => digit !== '');
-
- return (
-
- {/* Desktop Header */}
-
-
- {/* Desktop View */}
-
-
-
-
- VERIFY CODE
-
-
- Confirmation code sent. Check inbox or spam folder for the code
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
-
-
- {/* Mobile View */}
-
- {/* Mobile Header */}
-
-
-
- Request access
-
-
-
-
-
-
-
- VERIFY CODE
-
-
- Confirmation code sent. Check inbox or spam folder for the code
-
-
-
- {error && (
-
- {error}
-
- )}
-
-
-
-
-
-
- );
+ return ;
}
diff --git a/app/(auth)/verify-otp/verify-otp-client.tsx b/app/(auth)/verify-otp/verify-otp-client.tsx
new file mode 100644
index 00000000..fa007563
--- /dev/null
+++ b/app/(auth)/verify-otp/verify-otp-client.tsx
@@ -0,0 +1,279 @@
+'use client';
+
+import { verifyLoginOtp, resendLoginOtp } from '@/lib/api/auth';
+import { useEffect, useRef, useState } from 'react';
+
+import Image from 'next/image';
+import Link from 'next/link';
+import { useAuthStore } from '@/hooks/use-auth-store';
+import { useRouter } from 'next/navigation';
+
+export default function VerifyOtpClient() {
+ const router = useRouter();
+ const setAuth = useAuthStore((s) => s.setAuth);
+ const [otp, setOtp] = useState(['', '', '', '', '', '']);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
+
+ useEffect(() => {
+ inputRefs.current[0]?.focus();
+ }, []);
+
+ const handleChange = (index: number, value: string) => {
+ if (!/^\d*$/.test(value)) return;
+
+ const newOtp = [...otp];
+ newOtp[index] = value.slice(-1);
+ setOtp(newOtp);
+ setError('');
+
+ if (value && index < 5) {
+ inputRefs.current[index + 1]?.focus();
+ }
+ };
+
+ const handleKeyDown = (
+ index: number,
+ e: React.KeyboardEvent,
+ ) => {
+ if (e.key === 'Backspace' && !otp[index] && index > 0) {
+ inputRefs.current[index - 1]?.focus();
+ }
+ };
+
+ const handlePaste = (e: React.ClipboardEvent) => {
+ e.preventDefault();
+ const pastedData = e.clipboardData.getData('text').slice(0, 6);
+ if (!/^\d+$/.test(pastedData)) return;
+
+ const newOtp = [...otp];
+ pastedData.split('').forEach((char, index) => {
+ if (index < 6) newOtp[index] = char;
+ });
+ setOtp(newOtp);
+
+ const lastFilledIndex = Math.min(pastedData.length, 5);
+ inputRefs.current[lastFilledIndex]?.focus();
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ const otpCode = otp.join('');
+ if (otpCode.length !== 6) {
+ setError('Please enter all 6 digits');
+ return;
+ }
+ const storedEmail = sessionStorage.getItem('login-email');
+ if (!storedEmail) {
+ setError('No email found. Please sign in again.');
+ return;
+ }
+ setIsLoading(true);
+ setError('');
+ try {
+ const res = await verifyLoginOtp({ email: storedEmail, otp: otpCode }) as { user: { id: string; firstName: string; lastName: string; email: string; role: 'USER' | 'ADMIN' }; accessToken: string; refreshToken: string };
+ const fullName = [res.user.firstName, res.user.lastName].filter(Boolean).join(' ');
+ setAuth({ ...res.user, name: fullName }, res.accessToken, res.refreshToken);
+ setIsLoading(false);
+ router.push('/dashboard');
+ } catch (err: unknown) {
+ setIsLoading(false);
+ setError(err instanceof Error ? err.message : 'Invalid or expired OTP');
+ }
+ };
+
+ const handleResend = async () => {
+ setOtp(['', '', '', '', '', '']);
+ setError('');
+ inputRefs.current[0]?.focus();
+ const storedEmail = sessionStorage.getItem('login-email');
+ if (!storedEmail) {
+ setError('Missing email. Please sign in again.');
+ return;
+ }
+ try {
+ await resendLoginOtp({ email: storedEmail });
+ } catch {
+ setError('Failed to resend code');
+ }
+ };
+
+ const isComplete = otp.every((digit) => digit !== '');
+
+ return (
+
+ {/* Desktop Header */}
+
+
+ {/* Desktop View */}
+
+
+
+
+ VERIFY CODE
+
+
+ Confirmation code sent. Check inbox or spam folder for the code
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ {/* Mobile View */}
+
+ {/* Mobile Header */}
+
+
+
+ Request access
+
+
+
+
+
+
+
+ VERIFY CODE
+
+
+ Confirmation code sent. Check inbox or spam folder for the code
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/app/(dashboard)/dashboard-layout-client.tsx b/app/(dashboard)/dashboard-layout-client.tsx
new file mode 100644
index 00000000..d199c503
--- /dev/null
+++ b/app/(dashboard)/dashboard-layout-client.tsx
@@ -0,0 +1,75 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+
+import { Sidebar } from '../../components/dashboard/sidebar';
+import { Topbar } from '../../components/dashboard/topbar';
+import { cn } from '../../lib/utils';
+import { useAuthStore } from '../../hooks/use-auth-store';
+import { useRouter } from 'next/navigation';
+import { useSidebarStore } from '../../hooks/use-sidebar-store';
+
+export default function DashboardLayoutClient({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const { isOpen, close } = useSidebarStore();
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
+ const { isAuthenticated, accessToken } = useAuthStore();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (!isAuthenticated || !accessToken) {
+ router.replace('/sign-in');
+ }
+ }, [isAuthenticated, accessToken, router]);
+
+ if (!isAuthenticated || !accessToken) {
+ return null;
+ }
+
+ return (
+
+ {/* Sidebar - Desktop */}
+
+
+ {/* Sidebar - Mobile Drawer Overlay */}
+ {isOpen && (
+
+ )}
+
+ {/* Sidebar - Mobile Drawer */}
+
+
+
+
+ );
+}
diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx
index eae4b06d..2c8b85d4 100644
--- a/app/(dashboard)/layout.tsx
+++ b/app/(dashboard)/layout.tsx
@@ -1,75 +1,14 @@
-'use client';
+import type { Metadata } from "next";
+import DashboardLayoutClient from "./dashboard-layout-client";
-import { useEffect, useState } from 'react';
-
-import { Sidebar } from '../../components/dashboard/sidebar';
-import { Topbar } from '../../components/dashboard/topbar';
-import { cn } from '../../lib/utils';
-import { useAuthStore } from '../../hooks/use-auth-store';
-import { useRouter } from 'next/navigation';
-import { useSidebarStore } from '../../hooks/use-sidebar-store';
+export const metadata: Metadata = {
+ robots: { index: false, follow: false },
+};
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
- const { isOpen, close } = useSidebarStore();
- const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
- const { isAuthenticated, accessToken } = useAuthStore();
- const router = useRouter();
-
- useEffect(() => {
- if (!isAuthenticated || !accessToken) {
- router.replace('/sign-in');
- }
- }, [isAuthenticated, accessToken, router]);
-
- if (!isAuthenticated || !accessToken) {
- return null;
- }
-
- return (
-
- {/* Sidebar - Desktop */}
-
-
- {/* Sidebar - Mobile Drawer Overlay */}
- {isOpen && (
-
- )}
-
- {/* Sidebar - Mobile Drawer */}
-
-
-
-
- );
+ return {children};
}
diff --git a/app/landing-page-client.tsx b/app/landing-page-client.tsx
new file mode 100644
index 00000000..98335a23
--- /dev/null
+++ b/app/landing-page-client.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { useAuthStore } from "@/hooks/use-auth-store";
+
+import Navbar from "@/components/landing/Navbar";
+import Hero from "@/components/landing/Hero";
+import Features from "@/components/landing/Features";
+import CTA from "@/components/landing/CTA";
+import Footer from "@/components/landing/Footer";
+
+export default function LandingPageClient() {
+ const { user } = useAuthStore();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (user) router.replace("/dashboard");
+ }, [user, router]);
+
+ if (user) return null;
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 9190fbd2..7d991c22 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -12,15 +12,49 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});
-export const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
-export const manrope = Manrope({
+const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
+const manrope = Manrope({
subsets: ["latin"],
variable: "--font-manrope",
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ metadataBase: new URL("https://nexafx.io"),
+ title: {
+ default: "NexaFx \u2014 Multi-Currency Finance on Stellar",
+ template: "%s | NexaFx",
+ },
+ description:
+ "Convert, deposit, and transfer currencies instantly on the Stellar blockchain.",
+ keywords: [
+ "currency exchange",
+ "Stellar blockchain",
+ "cross-border payments",
+ "NGN to USD",
+ "crypto finance",
+ ],
+ authors: [{ name: "Nexacore" }],
+ creator: "Nexacore",
+ openGraph: {
+ type: "website",
+ locale: "en_US",
+ url: "https://nexafx.io",
+ siteName: "NexaFx",
+ title: "NexaFx \u2014 Multi-Currency Finance on Stellar",
+ description:
+ "Convert, deposit, and transfer currencies instantly on the Stellar blockchain.",
+ images: [
+ { url: "/og-image.png", width: 1200, height: 630, alt: "NexaFx" },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "NexaFx \u2014 Multi-Currency Finance on Stellar",
+ description:
+ "Convert, deposit, and transfer currencies instantly on the Stellar blockchain.",
+ images: ["/og-image.png"],
+ },
+ robots: { index: true, follow: true },
};
export default function RootLayout({
diff --git a/app/page.tsx b/app/page.tsx
index 3120bc62..0bfb4f8d 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,34 +1,18 @@
-"use client";
+import type { Metadata } from "next";
+import LandingPageClient from "./landing-page-client";
-import { useEffect } from "react";
-import { useRouter } from "next/navigation";
-import { useAuthStore } from "@/hooks/use-auth-store";
-
-import Navbar from "@/components/landing/Navbar";
-import Hero from "@/components/landing/Hero";
-import Features from "@/components/landing/Features";
-import CTA from "@/components/landing/CTA";
-import Footer from "@/components/landing/Footer";
+export const metadata: Metadata = {
+ title: "NexaFx \u2014 Multi-Currency Finance on Stellar",
+ description:
+ "Convert, deposit, and transfer currencies instantly on the Stellar blockchain.",
+ openGraph: {
+ title: "NexaFx \u2014 Multi-Currency Finance on Stellar",
+ description:
+ "Convert, deposit, and transfer currencies instantly on the Stellar blockchain.",
+ url: "https://nexafx.io",
+ },
+};
export default function HomePage() {
- const { user } = useAuthStore();
- const router = useRouter();
-
- useEffect(() => {
- if (user) router.replace("/dashboard");
- }, [user, router]);
-
- if (user) return null;
-
- return (
-
-
-
-
-
-
-
-
-
- );
+ return ;
}
diff --git a/app/signup/page.tsx b/app/signup/page.tsx
index 7cc43a66..31588d59 100644
--- a/app/signup/page.tsx
+++ b/app/signup/page.tsx
@@ -1,209 +1,10 @@
-"use client";
+import type { Metadata } from "next";
+import SignupClient from "./signup-client";
-import { useState } from "react";
-import { useRouter } from "next/navigation";
-import { Eye, EyeOff } from "lucide-react";
-import { signUp } from "@/lib/api/auth";
+export const metadata: Metadata = {
+ title: "Create Account",
+};
-export default function CreateAccountPage() {
- const router = useRouter();
- const [showPassword, setShowPassword] = useState(false);
- const [showConfirmPassword, setShowConfirmPassword] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const [apiError, setApiError] = useState("");
-
- const [formData, setFormData] = useState({
- email: "",
- phone: "",
- password: "",
- confirmPassword: "",
- acceptTerms: true,
- });
-
- const [errors, setErrors] = useState<{ [key: string]: string }>({});
-
- const validate = () => {
- const newErrors: { [key: string]: string } = {};
- if (!formData.email) newErrors.email = "Email is required";
- else if (!/\S+@\S+\.\S+/.test(formData.email))
- newErrors.email = "Invalid email address";
-
- if (!formData.phone) newErrors.phone = "Phone number is required";
-
- if (!formData.password) newErrors.password = "Password is required";
- else if (formData.password.length < 8)
- newErrors.password = "Password must be at least 8 characters";
-
- if (formData.password !== formData.confirmPassword) {
- newErrors.confirmPassword = "Passwords do not match";
- }
-
- setErrors(newErrors);
- return Object.keys(newErrors).length === 0;
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!validate()) return;
-
- setIsLoading(true);
- setApiError("");
- try {
- await signUp({
- email: formData.email,
- phone: formData.phone,
- password: formData.password,
- });
- sessionStorage.setItem("signup_email", formData.email);
- router.push("/signup/verify");
- } catch (err) {
- setApiError(err instanceof Error ? err.message : "Something went wrong");
- } finally {
- setIsLoading(false);
- }
- };
-
- return (
-
-
-
- Create an account
-
-
Let's get started...
-
-
-
-
- );
+export default function SignupPage() {
+ return ;
}
diff --git a/app/signup/signup-client.tsx b/app/signup/signup-client.tsx
new file mode 100644
index 00000000..279d900b
--- /dev/null
+++ b/app/signup/signup-client.tsx
@@ -0,0 +1,209 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { Eye, EyeOff } from "lucide-react";
+import { signUp } from "@/lib/api/auth";
+
+export default function SignupClient() {
+ const router = useRouter();
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [apiError, setApiError] = useState("");
+
+ const [formData, setFormData] = useState({
+ email: "",
+ phone: "",
+ password: "",
+ confirmPassword: "",
+ acceptTerms: true,
+ });
+
+ const [errors, setErrors] = useState<{ [key: string]: string }>({});
+
+ const validate = () => {
+ const newErrors: { [key: string]: string } = {};
+ if (!formData.email) newErrors.email = "Email is required";
+ else if (!/\S+@\S+\.\S+/.test(formData.email))
+ newErrors.email = "Invalid email address";
+
+ if (!formData.phone) newErrors.phone = "Phone number is required";
+
+ if (!formData.password) newErrors.password = "Password is required";
+ else if (formData.password.length < 8)
+ newErrors.password = "Password must be at least 8 characters";
+
+ if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = "Passwords do not match";
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!validate()) return;
+
+ setIsLoading(true);
+ setApiError("");
+ try {
+ await signUp({
+ email: formData.email,
+ phone: formData.phone,
+ password: formData.password,
+ });
+ sessionStorage.setItem("signup_email", formData.email);
+ router.push("/signup/verify");
+ } catch (err) {
+ setApiError(err instanceof Error ? err.message : "Something went wrong");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ Create an account
+
+
Let's get started...
+
+
+
+
+ );
+}
diff --git a/app/signup/success/page.tsx b/app/signup/success/page.tsx
index 4f973e21..07656dab 100644
--- a/app/signup/success/page.tsx
+++ b/app/signup/success/page.tsx
@@ -1,62 +1,10 @@
-"use client";
+import type { Metadata } from "next";
+import SuccessClient from "./success-client";
-import { useEffect, useState } from "react";
-import { CheckCircle2 } from "lucide-react";
-import { useRouter } from "next/navigation";
+export const metadata: Metadata = {
+ title: "Email Confirmed",
+};
export default function SuccessPage() {
- const router = useRouter();
- const [dots, setDots] = useState("");
-
- useEffect(() => {
- const interval = setInterval(() => {
- setDots((prev) => (prev.length >= 3 ? "" : prev + "."));
- }, 500);
-
- // Redirect after 5 seconds
- const timeout = setTimeout(() => {
- router.push("/");
- }, 5000);
-
- return () => {
- clearInterval(interval);
- clearTimeout(timeout);
- };
- }, [router]);
-
- return (
-
-
-
-
- EMAIL CONFIRMED
-
-
-
-
- Your account has been successfully verified. You now have full access
- to NexaFX.
-
-
-
-
- Redirecting you to dashboard{dots}
-
-
-
-
-
- );
+ return ;
}
diff --git a/app/signup/success/success-client.tsx b/app/signup/success/success-client.tsx
new file mode 100644
index 00000000..7c69a180
--- /dev/null
+++ b/app/signup/success/success-client.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { CheckCircle2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+
+export default function SuccessClient() {
+ const router = useRouter();
+ const [dots, setDots] = useState("");
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setDots((prev) => (prev.length >= 3 ? "" : prev + "."));
+ }, 500);
+
+ // Redirect after 5 seconds
+ const timeout = setTimeout(() => {
+ router.push("/");
+ }, 5000);
+
+ return () => {
+ clearInterval(interval);
+ clearTimeout(timeout);
+ };
+ }, [router]);
+
+ return (
+
+
+
+
+ EMAIL CONFIRMED
+
+
+
+
+ Your account has been successfully verified. You now have full access
+ to NexaFX.
+
+
+
+
+ Redirecting you to dashboard{dots}
+
+
+
+
+
+ );
+}
diff --git a/app/signup/verify/page.tsx b/app/signup/verify/page.tsx
index 26c1cd0e..d18989fd 100644
--- a/app/signup/verify/page.tsx
+++ b/app/signup/verify/page.tsx
@@ -1,144 +1,10 @@
-"use client";
+import type { Metadata } from "next";
+import VerifyClient from "./verify-client";
-import { useState, useRef, useEffect } from "react";
-import { useRouter } from "next/navigation";
-import { verifySignupOtp, resendSignupOtp } from "@/lib/api/auth";
+export const metadata: Metadata = {
+ title: "Verify Email",
+};
-export default function VerifyOtpPage() {
- const router = useRouter();
- const [otp, setOtp] = useState(new Array(6).fill(""));
- const [isLoading, setIsLoading] = useState(false);
- const [apiError, setApiError] = useState("");
- const [resendMessage, setResendMessage] = useState("");
- const [isResending, setIsResending] = useState(false);
- const [email, setEmail] = useState("");
- const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
-
- useEffect(() => {
- inputRefs.current[0]?.focus();
- const stored = sessionStorage.getItem("signup_email");
- if (stored) setEmail(stored);
- }, []);
-
- const handleChange = (element: HTMLInputElement, index: number) => {
- if (isNaN(Number(element.value))) return;
-
- const newOtp = [...otp];
- newOtp[index] = element.value.substring(element.value.length - 1);
- setOtp(newOtp);
-
- // Auto-advance
- if (element.value && index < 5) {
- inputRefs.current[index + 1]?.focus();
- }
- };
-
- const handleKeyDown = (
- e: React.KeyboardEvent,
- index: number,
- ) => {
- if (e.key === "Backspace" && !otp[index] && index > 0) {
- inputRefs.current[index - 1]?.focus();
- }
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (otp.some((digit) => digit === "")) return;
-
- setIsLoading(true);
- setApiError("");
- try {
- await verifySignupOtp({ email, otp: otp.join("") });
- router.push("/signup/success");
- } catch (err) {
- setApiError(
- err instanceof Error ? err.message : "Invalid or expired OTP",
- );
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleResend = async () => {
- setIsResending(true);
- setApiError("");
- setResendMessage("");
- try {
- await resendSignupOtp({ email });
- setResendMessage("Code resent successfully");
- } catch (err) {
- setApiError(err instanceof Error ? err.message : "Failed to resend code");
- } finally {
- setIsResending(false);
- }
- };
-
- const isOtpComplete = otp.every((digit) => digit !== "");
-
- return (
-
-
-
- VERIFY CODE
-
-
- Confirmation code sent. Check inbox or spam folder for the code
-
-
-
-
-
- );
+export default function VerifyPage() {
+ return ;
}
diff --git a/app/signup/verify/verify-client.tsx b/app/signup/verify/verify-client.tsx
new file mode 100644
index 00000000..a1e3cdd4
--- /dev/null
+++ b/app/signup/verify/verify-client.tsx
@@ -0,0 +1,144 @@
+"use client";
+
+import { useState, useRef, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { verifySignupOtp, resendSignupOtp } from "@/lib/api/auth";
+
+export default function VerifyClient() {
+ const router = useRouter();
+ const [otp, setOtp] = useState(new Array(6).fill(""));
+ const [isLoading, setIsLoading] = useState(false);
+ const [apiError, setApiError] = useState("");
+ const [resendMessage, setResendMessage] = useState("");
+ const [isResending, setIsResending] = useState(false);
+ const [email, setEmail] = useState("");
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
+
+ useEffect(() => {
+ inputRefs.current[0]?.focus();
+ const stored = sessionStorage.getItem("signup_email");
+ if (stored) setEmail(stored);
+ }, []);
+
+ const handleChange = (element: HTMLInputElement, index: number) => {
+ if (isNaN(Number(element.value))) return;
+
+ const newOtp = [...otp];
+ newOtp[index] = element.value.substring(element.value.length - 1);
+ setOtp(newOtp);
+
+ // Auto-advance
+ if (element.value && index < 5) {
+ inputRefs.current[index + 1]?.focus();
+ }
+ };
+
+ const handleKeyDown = (
+ e: React.KeyboardEvent,
+ index: number,
+ ) => {
+ if (e.key === "Backspace" && !otp[index] && index > 0) {
+ inputRefs.current[index - 1]?.focus();
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (otp.some((digit) => digit === "")) return;
+
+ setIsLoading(true);
+ setApiError("");
+ try {
+ await verifySignupOtp({ email, otp: otp.join("") });
+ router.push("/signup/success");
+ } catch (err) {
+ setApiError(
+ err instanceof Error ? err.message : "Invalid or expired OTP",
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleResend = async () => {
+ setIsResending(true);
+ setApiError("");
+ setResendMessage("");
+ try {
+ await resendSignupOtp({ email });
+ setResendMessage("Code resent successfully");
+ } catch (err) {
+ setApiError(err instanceof Error ? err.message : "Failed to resend code");
+ } finally {
+ setIsResending(false);
+ }
+ };
+
+ const isOtpComplete = otp.every((digit) => digit !== "");
+
+ return (
+
+
+
+ VERIFY CODE
+
+
+ Confirmation code sent. Check inbox or spam folder for the code
+
+
+
+
+
+ );
+}
diff --git a/app/sitemap.ts b/app/sitemap.ts
new file mode 100644
index 00000000..e9d0838a
--- /dev/null
+++ b/app/sitemap.ts
@@ -0,0 +1,32 @@
+import type { MetadataRoute } from "next";
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ const baseUrl = "https://nexafx.io";
+
+ return [
+ {
+ url: baseUrl,
+ lastModified: new Date(),
+ changeFrequency: "monthly",
+ priority: 1,
+ },
+ {
+ url: `${baseUrl}/sign-in`,
+ lastModified: new Date(),
+ changeFrequency: "monthly",
+ priority: 0.8,
+ },
+ {
+ url: `${baseUrl}/signup`,
+ lastModified: new Date(),
+ changeFrequency: "monthly",
+ priority: 0.8,
+ },
+ {
+ url: `${baseUrl}/forgot-password`,
+ lastModified: new Date(),
+ changeFrequency: "monthly",
+ priority: 0.3,
+ },
+ ];
+}
diff --git a/package-lock.json b/package-lock.json
index 49392d9e..c94c5866 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,8 @@
"react": "19.2.3",
"react-dom": "19.2.3",
"recharts": "^3.7.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
"tailwind-merge": "^3.4.0",
"zustand": "^5.0.10"
},
diff --git a/package.json b/package.json
index dc7461f0..319ae9cc 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,8 @@
"react": "19.2.3",
"react-dom": "19.2.3",
"recharts": "^3.7.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
"tailwind-merge": "^3.4.0",
"zustand": "^5.0.10"
},
diff --git a/public/og-image.png b/public/og-image.png
new file mode 100644
index 00000000..7e64eee0
Binary files /dev/null and b/public/og-image.png differ
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 00000000..fdf3d912
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,6 @@
+User-agent: *
+Allow: /
+Disallow: /dashboard
+Disallow: /admin
+
+Sitemap: https://nexafx.io/sitemap.xml