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,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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user