Files
TX/artifacts/tx-os/src/pages/reset-password.tsx
T
riyadhafraa d0e6912017 Rename project and remove unused services
Update project name from teaboy-os to tx-os and remove obsolete services.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3154f23a-748a-4118-aa41-fc01b7b1f04d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/PVuelRZ
Replit-Helium-Checkpoint-Created: true
2026-04-22 10:52:09 +00:00

243 lines
9.5 KiB
TypeScript

import { useEffect, useMemo, useState, type FormEvent } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "wouter";
import {
useResetPassword,
useVerifyResetToken,
} from "@workspace/api-client-react";
import { useAppName, useAppSettings } from "@/hooks/use-app-settings";
import i18n from "@/i18n";
import {
Eye,
EyeOff,
Loader2,
AlertCircle,
CheckCircle2,
} from "lucide-react";
import { AnimatedArt } from "@/components/animated-art";
export default function ResetPasswordPage() {
const { t, i18n: i18nInstance } = useTranslation();
const lang = i18nInstance.language;
const dir = lang === "ar" ? "rtl" : "ltr";
const appName = useAppName();
const { data: settings } = useAppSettings();
const [, setLocation] = useLocation();
const token = useMemo(() => {
if (typeof window === "undefined") return "";
return new URLSearchParams(window.location.search).get("token") ?? "";
}, []);
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState<string | null>(null);
const [done, setDone] = useState(false);
const [tokenValid, setTokenValid] = useState<boolean | null>(null);
const verify = useVerifyResetToken();
const reset = useResetPassword();
useEffect(() => {
if (!token) {
setTokenValid(false);
return;
}
verify.mutate(
{ data: { token } },
{
onSuccess: (resp) => setTokenValid(resp.valid),
onError: () => setTokenValid(false),
},
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (password.length < 6) {
setError(t("auth.reset.errMinLength"));
return;
}
if (password !== confirm) {
setError(t("auth.reset.errMismatch"));
return;
}
setError(null);
reset.mutate(
{ data: { token, newPassword: password } },
{
onSuccess: () => setDone(true),
onError: () => setError(t("auth.reset.errGeneric")),
},
);
};
const toggleLanguage = () => i18n.changeLanguage(lang === "ar" ? "en" : "ar");
const footerText =
lang === "ar" ? settings?.footerTextAr : settings?.footerTextEn;
return (
<div className="min-h-screen flex w-full text-foreground relative" dir={dir}>
<button
type="button"
onClick={toggleLanguage}
className={`absolute top-4 z-50 text-sm font-medium hover:bg-foreground/5 px-3 py-1.5 rounded-md transition-colors text-foreground bg-background/70 backdrop-blur-sm border border-border/50 ${
dir === "rtl" ? "left-4 md:left-8" : "right-4 md:right-8"
}`}
>
{lang === "ar" ? "EN" : "عربي"}
</button>
<div className="hidden lg:block lg:w-1/2 relative overflow-hidden shrink-0">
<AnimatedArt />
</div>
<div className="flex-1 min-w-0 flex flex-col items-center justify-center p-6 sm:p-8 relative os-bg">
<div className="w-full max-w-[400px]">
<div className="glass-panel rounded-2xl p-6 sm:p-8">
<h1 className="text-xl font-bold mb-1">{t("auth.reset.title")}</h1>
<p className="text-sm text-muted-foreground mb-5">
{t("auth.reset.subtitle")}
</p>
{tokenValid === null ? (
<div className="flex items-center justify-center py-6 text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin" />
</div>
) : tokenValid === false ? (
<div className="space-y-4">
<div
role="alert"
className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2.5 text-sm text-destructive"
>
<AlertCircle className="h-4 w-4 shrink-0 mt-0.5" />
<span>{t("auth.reset.invalidToken")}</span>
</div>
<button
type="button"
onClick={() => setLocation("/forgot-password")}
className="w-full inline-flex items-center justify-center gap-2 h-10 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:opacity-90 transition-opacity"
>
{t("auth.reset.requestNew")}
</button>
</div>
) : done ? (
<div className="space-y-4">
<div className="flex items-start gap-2 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2.5 text-sm text-emerald-700 dark:text-emerald-300">
<CheckCircle2 className="h-4 w-4 shrink-0 mt-0.5" />
<span>{t("auth.reset.successMessage")}</span>
</div>
<button
type="button"
onClick={() => setLocation("/login")}
className="w-full inline-flex items-center justify-center gap-2 h-10 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:opacity-90 transition-opacity"
>
{t("auth.reset.goToLogin")}
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-5" noValidate>
<div className="space-y-1.5">
<label
htmlFor="newPassword"
className="block font-semibold text-sm text-foreground"
>
{t("auth.reset.newPassword")}
</label>
<div className="relative">
<input
id="newPassword"
type={showPassword ? "text" : "password"}
autoComplete="new-password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
setError(null);
}}
disabled={reset.isPending}
className={`block w-full h-10 rounded-md border border-input bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary disabled:opacity-60 ${
dir === "rtl" ? "pl-10" : "pr-10"
}`}
/>
<button
type="button"
onClick={() => setShowPassword((v) => !v)}
className={`absolute top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors ${
dir === "rtl" ? "left-3" : "right-3"
}`}
aria-label={
showPassword
? t("auth.hidePassword")
: t("auth.showPassword")
}
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
</div>
<div className="space-y-1.5">
<label
htmlFor="confirmPassword"
className="block font-semibold text-sm text-foreground"
>
{t("auth.reset.confirmPassword")}
</label>
<input
id="confirmPassword"
type={showPassword ? "text" : "password"}
autoComplete="new-password"
value={confirm}
onChange={(e) => {
setConfirm(e.target.value);
setError(null);
}}
disabled={reset.isPending}
className="block w-full h-10 rounded-md border border-input bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary disabled:opacity-60"
/>
</div>
{error && (
<div
role="alert"
className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2.5 text-sm text-destructive"
>
<AlertCircle className="h-4 w-4 shrink-0 mt-0.5" />
<span>{error}</span>
</div>
)}
<button
type="submit"
disabled={reset.isPending}
className="w-full inline-flex items-center justify-center gap-2 h-10 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:opacity-90 transition-opacity disabled:opacity-60"
>
{reset.isPending && (
<Loader2 className="h-4 w-4 animate-spin" />
)}
{reset.isPending
? t("auth.reset.saving")
: t("auth.reset.submit")}
</button>
</form>
)}
</div>
</div>
{footerText && (
<div className="absolute bottom-4 inset-x-0 text-center text-xs text-muted-foreground px-4">
&copy; {new Date().getFullYear()} {appName}. {footerText}
</div>
)}
</div>
</div>
);
}