React 18 + TypeScript + Vite 5 + Tailwind CSS + shadcn/ui frontend: - Multi-role auth: student, teacher, admin, corporate, agent, developer - Student portal: dashboard, courses, adaptive learning, exams, placement - Teacher portal: courses, assignments, grading, attendance, AI workbench - Admin portal: 60+ management pages (LMS, exams, users, entities, AI) - AI-powered features: study coach, generation page, gap analysis, TTS - Exam system: session, auto-save, results, grading queue - Full IELTS workflow: reading/listening/writing/speaking modules Made-with: Cursor
148 lines
5.4 KiB
TypeScript
148 lines
5.4 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { z } from "zod";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { useForm } from "react-hook-form";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { authService } from "@/services/auth.service";
|
|
import { brandingService } from "@/services/branding.service";
|
|
import { cn } from "@/lib/utils";
|
|
import { toast } from "sonner";
|
|
|
|
const schema = z
|
|
.object({
|
|
password: z.string().min(8, "At least 8 characters"),
|
|
confirm: z.string(),
|
|
})
|
|
.refine((d) => d.password === d.confirm, { message: "Passwords must match", path: ["confirm"] });
|
|
|
|
type FormValues = z.infer<typeof schema>;
|
|
|
|
function strengthLabel(p: string): "weak" | "medium" | "strong" {
|
|
let s = 0;
|
|
if (p.length >= 8) s++;
|
|
if (p.length >= 12) s++;
|
|
if (/[A-Z]/.test(p) && /[a-z]/.test(p)) s++;
|
|
if (/\d/.test(p)) s++;
|
|
if (/[^A-Za-z0-9]/.test(p)) s++;
|
|
if (s <= 2) return "weak";
|
|
if (s <= 4) return "medium";
|
|
return "strong";
|
|
}
|
|
|
|
export default function ResetPassword() {
|
|
const [searchParams] = useSearchParams();
|
|
const token = searchParams.get("token") ?? "";
|
|
const navigate = useNavigate();
|
|
const { data: branding } = useQuery({
|
|
queryKey: ["branding", "current"],
|
|
queryFn: () => brandingService.getCurrentBranding(),
|
|
});
|
|
|
|
const entityName = branding?.entity_name ?? "your organization";
|
|
|
|
const form = useForm<FormValues>({
|
|
resolver: zodResolver(schema),
|
|
defaultValues: { password: "", confirm: "" },
|
|
});
|
|
|
|
const pwd = form.watch("password");
|
|
const strength = useMemo(() => strengthLabel(pwd), [pwd]);
|
|
|
|
useEffect(() => {
|
|
const root = document.documentElement;
|
|
if (branding?.primary_color) root.style.setProperty("--encoach-brand-primary", branding.primary_color);
|
|
if (branding?.secondary_color) root.style.setProperty("--encoach-brand-secondary", branding.secondary_color);
|
|
if (branding?.background_color) root.style.setProperty("--encoach-brand-bg", branding.background_color);
|
|
return () => {
|
|
root.style.removeProperty("--encoach-brand-primary");
|
|
root.style.removeProperty("--encoach-brand-secondary");
|
|
root.style.removeProperty("--encoach-brand-bg");
|
|
};
|
|
}, [branding]);
|
|
|
|
const submitMut = useMutation({
|
|
mutationFn: (values: FormValues) => authService.setInvitePassword({ token, password: values.password }),
|
|
onSuccess: () => {
|
|
toast.success("Password saved.");
|
|
navigate("/student/placement", { replace: true });
|
|
},
|
|
onError: (e) => toast.error(e instanceof Error ? e.message : "Could not set password"),
|
|
});
|
|
|
|
const onSubmit = (values: FormValues) => {
|
|
if (!token) {
|
|
toast.error("Missing invite token.");
|
|
return;
|
|
}
|
|
submitMut.mutate(values);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="min-h-screen flex items-center justify-center p-6"
|
|
style={{
|
|
backgroundColor: "var(--encoach-brand-bg, hsl(var(--background)))",
|
|
}}
|
|
>
|
|
<Card className="w-full max-w-md border shadow-lg">
|
|
<CardHeader className="text-center space-y-1">
|
|
<CardTitle style={{ color: "var(--encoach-brand-primary, hsl(var(--primary)))" }}>
|
|
Welcome to {entityName}'s Learning Platform
|
|
</CardTitle>
|
|
<CardDescription>Set a new password</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="password"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>New password</FormLabel>
|
|
<FormControl>
|
|
<Input type="password" autoComplete="new-password" {...field} />
|
|
</FormControl>
|
|
<div
|
|
className={cn(
|
|
"text-xs font-medium",
|
|
strength === "weak" && "text-red-600",
|
|
strength === "medium" && "text-amber-600",
|
|
strength === "strong" && "text-green-600",
|
|
)}
|
|
>
|
|
{pwd ? `${strength.charAt(0).toUpperCase() + strength.slice(1)} password` : " "}
|
|
</div>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="confirm"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Confirm password</FormLabel>
|
|
<FormControl>
|
|
<Input type="password" autoComplete="new-password" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<Button type="submit" className="w-full" disabled={submitMut.isPending}>
|
|
Set Password & Continue
|
|
</Button>
|
|
</form>
|
|
</Form>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|