feat: initial frontend codebase — EnCoach v3
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
This commit is contained in:
147
src/pages/ResetPassword.tsx
Normal file
147
src/pages/ResetPassword.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user