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:
Trey t
2026-03-03 09:31:29 -06:00
commit 5a50d77515
183 changed files with 34450 additions and 0 deletions
@@ -0,0 +1,105 @@
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { FormField } from "@/components/shared/form-field";
import { useJoinResidence } from "@/lib/hooks/use-sharing";
import { useOnboardingStore } from "@/stores/onboarding";
// ---------------------------------------------------------------------------
// Schema
// ---------------------------------------------------------------------------
const joinResidenceSchema = z.object({
code: z
.string()
.length(6, "Share code must be 6 characters")
.regex(/^[A-Za-z0-9]+$/, "Share code must be alphanumeric"),
});
type JoinResidenceFormData = z.infer<typeof joinResidenceSchema>;
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function JoinResidenceStep() {
const { nextStep, prevStep, setResidenceId } = useOnboardingStore();
const joinResidence = useJoinResidence();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<JoinResidenceFormData>({
resolver: zodResolver(joinResidenceSchema),
defaultValues: {
code: "",
},
});
const onSubmit = (data: JoinResidenceFormData) => {
joinResidence.mutate(data.code, {
onSuccess: (residence) => {
setResidenceId(residence.id);
nextStep();
},
});
};
return (
<div className="space-y-6">
<div className="text-center space-y-2">
<h2 className="text-2xl font-bold tracking-tight">
Join a residence
</h2>
<p className="text-muted-foreground">
Enter the 6-character share code you received from the property owner.
</p>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<FormField
label="Share Code"
htmlFor="code"
error={errors.code?.message}
required
>
<Input
id="code"
placeholder="ABC123"
maxLength={6}
className="text-center text-lg tracking-widest uppercase"
aria-invalid={!!errors.code}
{...register("code")}
/>
</FormField>
{joinResidence.error && (
<p className="text-sm text-destructive">
{joinResidence.error instanceof Error
? joinResidence.error.message
: "Invalid or expired share code. Please check and try again."}
</p>
)}
<div className="flex items-center justify-between pt-2">
<Button type="button" variant="ghost" onClick={prevStep}>
Back
</Button>
<Button type="submit" disabled={joinResidence.isPending}>
{joinResidence.isPending && (
<Loader2 className="size-4 mr-2 animate-spin" />
)}
Join Residence
</Button>
</div>
</form>
</div>
);
}