Some checks failed
Build Frontend / build (push) Failing after 7s
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
198 lines
6.4 KiB
TypeScript
198 lines
6.4 KiB
TypeScript
"use client";
|
|
import { useState, useEffect, Suspense } from "react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import Link from "next/link";
|
|
import { fetchClient } from "@/lib/api/api-client";
|
|
import { toast } from "sonner";
|
|
import { Button } from "@/components/common/button";
|
|
import { Input } from "@/components/common/input";
|
|
import { Label } from "@/components/common/label";
|
|
import { Loader2 } from "lucide-react";
|
|
|
|
interface Vendor {
|
|
id: string;
|
|
username: string;
|
|
}
|
|
|
|
function ResetPasswordForm() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const token = searchParams.get('token');
|
|
|
|
const [vendor, setVendor] = useState<Vendor | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [password, setPassword] = useState("");
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (!token) {
|
|
toast.error("No reset token provided");
|
|
router.push("/auth/login");
|
|
return;
|
|
}
|
|
|
|
validateToken();
|
|
}, [token]);
|
|
|
|
async function validateToken() {
|
|
try {
|
|
const data = await fetchClient<{ valid: boolean; vendor?: Vendor; message?: string }>(`/auth/validate-reset-token/${token}`);
|
|
|
|
if (data.valid && data.vendor) {
|
|
setVendor(data.vendor);
|
|
} else {
|
|
toast.error(data.message || "Invalid or expired token");
|
|
router.push("/auth/login");
|
|
}
|
|
} catch (error: any) {
|
|
toast.error("Failed to validate token");
|
|
router.push("/auth/login");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
|
|
if (password !== confirmPassword) {
|
|
toast.error("Passwords do not match");
|
|
return;
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
toast.error("Password must be at least 6 characters");
|
|
return;
|
|
}
|
|
|
|
setSubmitting(true);
|
|
|
|
try {
|
|
await fetchClient("/auth/reset-password", {
|
|
method: "POST",
|
|
body: { token, newPassword: password }
|
|
});
|
|
|
|
toast.success("Password reset successfully");
|
|
router.push("/auth/login");
|
|
} catch (error: any) {
|
|
toast.error(error?.message || "Failed to reset password");
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-[#0F0F12]">
|
|
<div className="w-full max-w-md p-8 space-y-8 bg-white dark:bg-[#1F1F23] rounded-xl shadow-lg text-center">
|
|
<div className="mt-6 flex flex-col items-center justify-center">
|
|
<div className="w-12 h-12 border-4 border-t-blue-500 border-b-transparent border-l-transparent border-r-transparent rounded-full animate-spin"></div>
|
|
<p className="mt-4 text-gray-600 dark:text-gray-400">Validating token...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!vendor) {
|
|
return null; // Will redirect
|
|
}
|
|
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-[#0F0F12]">
|
|
<div className="w-full max-w-md p-8 space-y-8 bg-white dark:bg-[#1F1F23] rounded-xl shadow-lg">
|
|
<div className="text-center">
|
|
<h2 className="mt-6 text-3xl font-bold text-gray-900 dark:text-white">Reset Password</h2>
|
|
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
Reset password for <span className="font-medium">{vendor.username}</span>
|
|
</p>
|
|
</div>
|
|
|
|
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
<div className="space-y-4">
|
|
<div className="animate-in fade-in duration-500">
|
|
<Label htmlFor="password">New Password</Label>
|
|
<Input
|
|
id="password"
|
|
name="password"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
required
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="mt-1"
|
|
disabled={submitting}
|
|
placeholder="Enter new password"
|
|
/>
|
|
</div>
|
|
<div className="animate-in fade-in duration-500 delay-150">
|
|
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
|
<Input
|
|
id="confirmPassword"
|
|
name="confirmPassword"
|
|
type="password"
|
|
autoComplete="new-password"
|
|
required
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
className="mt-1"
|
|
disabled={submitting}
|
|
placeholder="Confirm new password"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
type="submit"
|
|
className="w-full animate-in fade-in-50 duration-500 delay-300"
|
|
disabled={submitting}
|
|
>
|
|
{submitting ? (
|
|
<span className="flex items-center justify-center">
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Resetting password...
|
|
</span>
|
|
) : (
|
|
"Reset Password"
|
|
)}
|
|
</Button>
|
|
</form>
|
|
|
|
<p className="mt-10 text-sm text-center text-gray-600 dark:text-gray-400 animate-in fade-in duration-500 delay-500">
|
|
Remember your password?{" "}
|
|
<Link href="/auth/login" className="text-blue-600 hover:underline dark:text-blue-400">
|
|
Sign in
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Loading component for Suspense fallback
|
|
function ResetPasswordLoading() {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen bg-gray-100 dark:bg-[#0F0F12]">
|
|
<div className="w-full max-w-md p-8 space-y-8 bg-white dark:bg-[#1F1F23] rounded-xl shadow-lg text-center">
|
|
<div className="mt-6 flex flex-col items-center justify-center">
|
|
<div className="w-12 h-12 border-4 border-t-blue-500 border-b-transparent border-l-transparent border-r-transparent rounded-full animate-spin"></div>
|
|
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading reset form...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Main page component that uses Suspense
|
|
export default function ResetPasswordPage() {
|
|
return (
|
|
<Suspense fallback={<ResetPasswordLoading />}>
|
|
<ResetPasswordForm />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
|