feat: complete Phase 3 — advanced features for Casera web app
Adds sharing (residence share codes, join, user management, .casera file export/import), subscription status with feature comparison, notification preferences with bell icon, profile settings (edit info, change password, theme picker, delete account), onboarding wizard with create/join paths, enhanced dashboard with stats cards, Recharts completion chart, recent activity feed, and task report PDF download. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { AuthFormWrapper } from "@/components/forms/auth-form-wrapper";
|
||||
import { PasswordInput } from "@/components/forms/password-input";
|
||||
import { CodeInput } from "@/components/forms/code-input";
|
||||
import { resetPasswordSchema, type ResetPasswordFormData } from "@/lib/validations/auth";
|
||||
import * as authApi from "@/lib/api/auth";
|
||||
import { ApiError } from "@/lib/api/client";
|
||||
|
||||
type Step = "code" | "password";
|
||||
|
||||
function ResetPasswordForm() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const email = searchParams.get("email") ?? "";
|
||||
|
||||
const [step, setStep] = useState<Step>("code");
|
||||
const [code, setCode] = useState("");
|
||||
const [resetToken, setResetToken] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<ResetPasswordFormData>({
|
||||
resolver: zodResolver(resetPasswordSchema),
|
||||
values: {
|
||||
email,
|
||||
code,
|
||||
new_password: "",
|
||||
confirm_password: "",
|
||||
},
|
||||
});
|
||||
|
||||
// Step 1: Verify the 6-digit code
|
||||
async function handleVerifyCode(submittedCode: string) {
|
||||
if (submittedCode.length !== 6 || isLoading) return;
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await authApi.verifyResetCode({
|
||||
email,
|
||||
code: submittedCode,
|
||||
});
|
||||
setResetToken(result.reset_token);
|
||||
setStep("password");
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof ApiError
|
||||
? err.message
|
||||
: "Invalid code. Please try again.";
|
||||
setError(message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCodeChange(newCode: string) {
|
||||
setCode(newCode);
|
||||
if (newCode.length === 6) {
|
||||
handleVerifyCode(newCode);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Reset password with the token
|
||||
async function onSubmitPassword(data: ResetPasswordFormData) {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await authApi.resetPassword({
|
||||
reset_token: resetToken,
|
||||
new_password: data.new_password,
|
||||
});
|
||||
router.push("/login");
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof ApiError
|
||||
? err.message
|
||||
: "Failed to reset password. Please try again.";
|
||||
setError(message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (step === "code") {
|
||||
return (
|
||||
<AuthFormWrapper
|
||||
title="Enter reset code"
|
||||
subtitle={
|
||||
email
|
||||
? `Enter the 6-digit code sent to ${email}`
|
||||
: "Enter the 6-digit code sent to your email"
|
||||
}
|
||||
footer={
|
||||
<p>
|
||||
<Link href="/login" className="text-primary hover:underline">
|
||||
Back to login
|
||||
</Link>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col gap-6">
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CodeInput
|
||||
value={code}
|
||||
onChange={handleCodeChange}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="w-full"
|
||||
disabled={code.length !== 6 || isLoading}
|
||||
onClick={() => handleVerifyCode(code)}
|
||||
>
|
||||
{isLoading && <Loader2 className="animate-spin" />}
|
||||
Verify code
|
||||
</Button>
|
||||
</div>
|
||||
</AuthFormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthFormWrapper
|
||||
title="Set new password"
|
||||
subtitle="Enter your new password below"
|
||||
footer={
|
||||
<p>
|
||||
<Link href="/login" className="text-primary hover:underline">
|
||||
Back to login
|
||||
</Link>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmitPassword)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="new_password">New password</Label>
|
||||
<PasswordInput
|
||||
id="new_password"
|
||||
autoComplete="new-password"
|
||||
aria-invalid={!!errors.new_password}
|
||||
{...register("new_password")}
|
||||
/>
|
||||
{errors.new_password && (
|
||||
<p className="text-sm text-destructive">
|
||||
{errors.new_password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="confirm_password">Confirm password</Label>
|
||||
<PasswordInput
|
||||
id="confirm_password"
|
||||
autoComplete="new-password"
|
||||
aria-invalid={!!errors.confirm_password}
|
||||
{...register("confirm_password")}
|
||||
/>
|
||||
{errors.confirm_password && (
|
||||
<p className="text-sm text-destructive">
|
||||
{errors.confirm_password.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading && <Loader2 className="animate-spin" />}
|
||||
Reset password
|
||||
</Button>
|
||||
</form>
|
||||
</AuthFormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<ResetPasswordForm />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user