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
+115
View File
@@ -0,0 +1,115 @@
# Casera Web App — Build Plan Overview
## What We're Building
Full parity web app for Casera: **46 screens**, **104 API operations**, **4 domains** (Residences, Tasks, Contractors, Documents), plus auth, onboarding, subscriptions, settings, and a **sandboxed demo mode** with mock data.
## Tech Stack
| Layer | Choice |
|---|---|
| Framework | Next.js 15 (App Router) |
| Language | TypeScript |
| Styling | Tailwind CSS 4 |
| Components | shadcn/ui + Radix primitives |
| State (server) | TanStack Query v5 |
| State (client) | Zustand |
| Forms | React Hook Form + Zod validation |
| Auth | httpOnly cookie storing API token, Next.js middleware for route protection |
| Kanban | dnd-kit (drag-and-drop) for task board |
| Charts/metrics | Recharts (for summary dashboard) |
| Icons | Lucide (matches shadcn/ui) |
| Testing | Vitest + Playwright |
| Deployment | Docker → Dokku (separate app) |
## Why Next.js + shadcn/ui
- SSR for the marketing landing + demo preview (SEO, social sharing)
- shadcn/ui gives the exact component primitives this app needs (kanban boards, forms, dialogs, data tables)
- TanStack Query maps perfectly to the existing `APILayer``DataManager` cache pattern
- Separate Dokku app keeps deployment independent
- Biggest ecosystem for finding solutions
## Project Structure
```
myCribAPI-Web/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (marketing)/ # Public pages (landing, pricing)
│ │ ├── (auth)/ # Login, Register, Forgot Password, Reset
│ │ ├── (onboarding)/ # Multi-step onboarding flow
│ │ ├── (demo)/ # Demo mode (sandboxed)
│ │ │ └── layout.tsx # Wraps demo in DemoProvider context
│ │ ├── (app)/ # Authenticated app
│ │ │ ├── residences/ # List, [id] detail, new, [id]/edit
│ │ │ ├── tasks/ # Kanban board, new, [id]/edit, [id]/complete
│ │ │ ├── contractors/ # List, [id] detail, new, [id]/edit
│ │ │ ├── documents/ # Tabs (warranties/documents), [id] detail
│ │ │ └── settings/ # Profile, notifications, themes, subscription
│ │ └── layout.tsx # Root layout
│ ├── components/
│ │ ├── ui/ # shadcn/ui primitives
│ │ ├── forms/ # Reusable form components
│ │ ├── cards/ # ResidenceCard, TaskCard, ContractorCard, etc.
│ │ ├── kanban/ # KanbanBoard, KanbanColumn, TaskActions
│ │ └── shared/ # EmptyState, LoadingOverlay, ErrorBanner, etc.
│ ├── lib/
│ │ ├── api/ # API client (typed fetch wrappers per domain)
│ │ ├── demo/ # DemoDataProvider + mock data seeds
│ │ ├── auth/ # Token management, middleware helpers
│ │ ├── hooks/ # useResidences, useTasks, etc. (TanStack Query)
│ │ └── types/ # TypeScript types matching API DTOs
│ ├── stores/ # Zustand stores (auth, theme, demo state)
│ └── styles/ # Tailwind theme config, design tokens
├── public/ # Static assets
├── Dockerfile
├── next.config.ts
├── tailwind.config.ts
├── package.json
└── CLAUDE.md
```
## Build Phases
| Phase | Focus | Details |
|-------|-------|---------|
| [Phase 1](./01-foundation.md) | Foundation | Scaffold, auth, layout, design system |
| [Phase 2](./02-core-crud.md) | Core CRUD | 4 domains: residences, tasks, contractors, documents |
| [Phase 3](./03-advanced-features.md) | Advanced Features | Sharing, subscription, notifications, onboarding |
| [Phase 4](./04-demo-mode.md) | Demo Mode | Mock data, in-memory store, session isolation |
| [Phase 5](./05-polish-deploy.md) | Polish & Deploy | Responsive, error handling, Dockerfile, tests |
## Web-Only Adaptations
| Mobile Feature | Web Equivalent |
|---|---|
| Push notifications | Not needed (no push API for web in scope) |
| Widgets | Not applicable |
| .casera file import/export | File download (export) + drag-and-drop or file picker (import) |
| Apple/Google Sign In | OAuth redirect flow (same backend endpoints) |
| StoreKit subscription | Stripe Checkout or link to App Store (TBD) |
| Camera capture | File upload input (no camera API needed) |
| Haptics | N/A |
| Background refresh | TanStack Query `refetchInterval` + window focus refetch |
## Risks & Mitigations
| Risk | Mitigation |
|---|---|
| Kanban board performance with many tasks | Virtualize columns with `react-window`, limit initial render to 50 tasks per column |
| Demo mode state leaking between sessions | Session ID in httpOnly cookie, Zustand store scoped to session, auto-clear on expiry |
| File upload size limits | Match backend 10MB limit, client-side validation before upload |
| OAuth redirect for Apple/Google | Use backend's existing social auth endpoints, redirect flow instead of native SDK |
| Subscription without StoreKit | Phase 1: link to App Store. Phase 2: add Stripe for web-only purchases (requires backend work) |
| 46 screens is a lot of UI | shadcn/ui + consistent patterns reduce per-screen effort to ~1-2 hours each after foundation is built |
## Security Considerations
| Concern | Approach |
|---|---|
| Auth token in browser | httpOnly cookie, not accessible to JS, CSRF protection via SameSite=Strict |
| Demo mode data isolation | No backend calls, purely client-side, session-scoped store, no persistence |
| API compatibility | 100% same endpoints, same token format (`Token <hex>`), same request/response shapes |
| Mobile feature gaps | Push notifications and widgets explicitly excluded. Everything else covered. |
| Deployment independence | Separate Dokku app, own domain (e.g., `app.casera.treytartt.com`), no coupling to Go API deployment |
+275
View File
@@ -0,0 +1,275 @@
# Phase 1 — Foundation
Scaffold the project, wire up auth, build the app shell, and establish the design system.
## Checklist
- [ ] Next.js 15 project init with App Router + TypeScript
- [ ] Tailwind CSS 4 configuration
- [ ] shadcn/ui setup (install CLI, add base components)
- [ ] TypeScript types from API DTOs (all request/response shapes)
- [ ] API client layer (typed fetch wrappers, token injection, error handling)
- [ ] Auth flows: Login, Register, Verify Email, Forgot/Reset Password
- [ ] Auth middleware (route protection, token cookie)
- [ ] App shell: sidebar nav + top bar
- [ ] Design system: color tokens matching mobile app
- [ ] Theme switcher (11 themes, persisted to localStorage)
## 1. Project Scaffold
```bash
npx create-next-app@latest myCribAPI-Web --typescript --tailwind --eslint --app --src-dir
cd myCribAPI-Web
npx shadcn@latest init
```
Install core dependencies:
```bash
npm install @tanstack/react-query zustand react-hook-form @hookform/resolvers zod
npm install lucide-react recharts @dnd-kit/core @dnd-kit/sortable
npm install -D vitest @playwright/test
```
## 2. TypeScript Types
Generate TypeScript types matching the Go API DTOs. Source from:
- `myCribAPI-go/internal/dto/requests/` — request shapes
- `myCribAPI-go/internal/dto/responses/` — response shapes
- `myCribAPI-go/internal/models/` — entity shapes
Key types to define in `src/lib/types/`:
```
types/
├── api.ts # ApiResult<T>, PaginatedResponse, ErrorResponse
├── auth.ts # LoginRequest, RegisterRequest, UserResponse, TokenResponse
├── residence.ts # Residence, ResidenceDetail, MyResidencesResponse, CreateResidenceRequest
├── task.ts # Task, TaskDetail, TaskColumnsResponse, CreateTaskRequest, TaskCompletionRequest
├── contractor.ts # Contractor, ContractorDetail, CreateContractorRequest
├── document.ts # Document, DocumentDetail, CreateDocumentRequest
├── subscription.ts # SubscriptionStatus, FeatureBenefit, UpgradeTrigger
├── lookups.ts # TaskCategory, TaskPriority, TaskFrequency, ContractorSpecialty
└── notification.ts # NotificationPreference, Notification
```
## 3. API Client Layer
Create typed fetch wrappers in `src/lib/api/`:
```
api/
├── client.ts # Base fetch wrapper (token injection, error handling, base URL)
├── auth.ts # login(), register(), forgotPassword(), resetPassword()
├── residences.ts # getResidences(), getResidence(), createResidence(), updateResidence(), deleteResidence()
├── tasks.ts # getTasks(), getTasksByResidence(), createTask(), updateTask(), completeTask(), etc.
├── contractors.ts # getContractors(), getContractor(), createContractor(), etc.
├── documents.ts # getDocuments(), getDocument(), createDocument(), uploadFile(), etc.
├── lookups.ts # getStaticData(), getUpgradeTriggers()
├── subscription.ts # getSubscriptionStatus(), etc.
└── notifications.ts # getPreferences(), updatePreference(), etc.
```
**Base client pattern:**
```typescript
// src/lib/api/client.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://mycrib.treytartt.com/api';
async function apiFetch<T>(
path: string,
options: RequestInit = {}
): Promise<T> {
const res = await fetch(`${API_BASE_URL}${path}`, {
...options,
credentials: 'include', // Send httpOnly cookie
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
if (!res.ok) {
const error = await res.json().catch(() => ({ detail: 'Unknown error' }));
throw new ApiError(res.status, error.detail || error.message);
}
return res.json();
}
```
## 4. Auth Flows
### Pages
| Route | Screen | Components |
|-------|--------|------------|
| `/login` | Login form | Email, password, "Forgot password?" link, social sign-in buttons |
| `/register` | Registration form | First name, last name, email, password, confirm password |
| `/verify-email` | Email verification | Code input, resend button |
| `/forgot-password` | Forgot password | Email input, submit |
| `/reset-password` | Reset password | New password, confirm, token from URL |
### Auth Token Strategy
```
Login → Server returns token → API route stores token in httpOnly cookie → Client redirects to /app
All subsequent requests include cookie
Next.js middleware reads cookie for route protection
```
- Token format: `Token <hex>` (Django-compatible, same as mobile)
- Cookie: `httpOnly`, `SameSite=Strict`, `Secure` in production
- Next.js middleware in `src/middleware.ts` checks cookie existence for protected routes
### Auth Store (Zustand)
```typescript
// src/stores/auth.ts
interface AuthState {
user: UserResponse | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (data: RegisterRequest) => Promise<void>;
logout: () => Promise<void>;
fetchUser: () => Promise<void>;
}
```
## 5. App Shell
### Layout Structure
```
┌─────────────────────────────────────────────────────┐
│ Top Bar: Logo | Search (optional) | Profile Menu │
├──────────┬──────────────────────────────────────────┤
│ │ │
│ Sidebar │ Main Content │
│ │ │
│ 🏠 Home │ │
│ 🏘 Resid │ │
│ ✅ Tasks │ │
│ 👷 Contr │ │
│ 📄 Docs │ │
│ │ │
│ ──────── │ │
│ ⚙ Setti │ │
│ │ │
├──────────┴──────────────────────────────────────────┤
│ (mobile: bottom tab bar instead of sidebar) │
└─────────────────────────────────────────────────────┘
```
- **Desktop (≥1024px)**: Collapsible sidebar + top bar
- **Tablet (768-1023px)**: Collapsed sidebar (icons only) + top bar
- **Mobile (<768px)**: Bottom tab bar (matches iOS app), no sidebar
### Navigation Items
| Icon | Label | Route |
|------|-------|-------|
| Home | Home | `/app` |
| Building | Residences | `/app/residences` |
| CheckSquare | Tasks | `/app/tasks` |
| HardHat | Contractors | `/app/contractors` |
| FileText | Documents | `/app/documents` |
| Settings | Settings | `/app/settings` |
## 6. Design System
### Color Tokens
Map the mobile app's color palette to CSS custom properties and Tailwind config:
```css
/* src/styles/theme.css */
:root {
/* Default theme (matches iOS appPrimary, etc.) */
--color-primary: #07A0C3; /* BlueGreen */
--color-secondary: #0055A5; /* Cerulean */
--color-accent: #F5A623; /* BrightAmber */
--color-error: #DD1C1A; /* PrimaryScarlet */
--color-bg-primary: #FFF1D0; /* Cream */
--color-bg-secondary: #F0F4F8; /* Blue-gray */
--color-text-primary: #1A1A1A;
--color-text-secondary: #6B7280;
--color-text-on-primary: #FFFFFF;
}
[data-theme="dark"] {
--color-bg-primary: #0A1929;
--color-bg-secondary: #1A2A3A;
--color-text-primary: #F0F0F0;
--color-text-secondary: #9CA3AF;
}
```
### 11 Themes
Match the mobile app's theme system:
| Theme | Primary | Secondary |
|-------|---------|-----------|
| Default | #07A0C3 | #0055A5 |
| Teal | TBD | TBD |
| Ocean | TBD | TBD |
| Forest | TBD | TBD |
| Sunset | TBD | TBD |
| Monochrome | TBD | TBD |
| Lavender | TBD | TBD |
| Crimson | TBD | TBD |
| Midnight | TBD | TBD |
| Desert | TBD | TBD |
| Mint | TBD | TBD |
Theme values sourced from `MyCribKMM/composeApp/src/commonMain/.../ui/theme/ThemeColors.kt`.
### Spacing
```css
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 12px;
--spacing-lg: 16px;
--spacing-xl: 24px;
```
## 7. TanStack Query Setup
```typescript
// src/lib/hooks/useResidences.ts
export function useResidences() {
return useQuery({
queryKey: ['residences'],
queryFn: () => api.residences.getResidences(),
staleTime: 60 * 60 * 1000, // 1 hour (matches mobile cache timeout)
refetchOnWindowFocus: true,
});
}
```
## 8. Lookups Initialization
Match the mobile pattern — after login, fetch `/api/static_data/` and `/api/upgrade-triggers/`:
```typescript
// After successful login
await queryClient.prefetchQuery({
queryKey: ['lookups'],
queryFn: () => api.lookups.getStaticData(),
staleTime: Infinity, // ETag-based, manually invalidate
});
```
## Deliverables
At the end of Phase 1, you should have:
1. A working Next.js app with Tailwind + shadcn/ui
2. Login and registration flows that authenticate against the Go API
3. A protected app shell with sidebar navigation
4. A design system with color tokens matching the mobile app
5. Typed API client with error handling
6. Theme switching (light/dark + 11 color themes)
+355
View File
@@ -0,0 +1,355 @@
# Phase 2 — Core CRUD
Build the 4 primary domains: Residences, Tasks, Contractors, and Documents.
## Checklist
- [ ] Residences: list (with summary card), detail, create/edit form, delete
- [ ] Tasks: kanban board (drag-and-drop columns), create/edit form, task actions (complete, cancel, archive, in-progress)
- [ ] Task completion: form with file upload (photos), completion history with image viewer
- [ ] Task templates: autocomplete search + template browser
- [ ] Contractors: list (search, filter by favorite/specialty), detail (quick actions), create/edit form, delete
- [ ] Documents: tabbed view (warranties/documents), detail, create/edit form (type-specific fields), file upload/download, image gallery
---
## 1. Residences
### Screens
| Route | Screen | Description |
|-------|--------|-------------|
| `/app/residences` | Residence List | Grid of residence cards with summary data |
| `/app/residences/new` | Create Residence | Form: name, address, type, photo |
| `/app/residences/[id]` | Residence Detail | Summary dashboard, task/contractor/document counts, users |
| `/app/residences/[id]/edit` | Edit Residence | Same form as create, pre-filled |
### API Endpoints Used
| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/api/residences/my/` | User's residences with summaries |
| GET | `/api/residences/:id/` | Residence detail |
| POST | `/api/residences/` | Create residence |
| PUT | `/api/residences/:id/` | Update residence |
| DELETE | `/api/residences/:id/` | Delete residence |
### Components
- **ResidenceCard**: Summary card showing name, address, type icon, task counts (overdue, due soon, total)
- **ResidenceForm**: React Hook Form + Zod. Fields: name (required), address, residence type (dropdown from lookups), photo upload
- **ResidenceSummary**: Dashboard showing task breakdown, contractor count, document count
### TanStack Query Hooks
```typescript
useResidences() // GET /residences/my/
useResidence(id) // GET /residences/:id/
useCreateResidence() // POST /residences/
useUpdateResidence(id) // PUT /residences/:id/
useDeleteResidence(id) // DELETE /residences/:id/
```
### Cache Invalidation
After create/update/delete → invalidate `['residences']` and `['residence', id]` queries.
---
## 2. Tasks
### Screens
| Route | Screen | Description |
|-------|--------|-------------|
| `/app/tasks` | Kanban Board | All tasks across residences, grouped by column |
| `/app/residences/[id]/tasks` | Residence Kanban | Tasks for a specific residence |
| `/app/tasks/new` | Create Task | Full task form |
| `/app/tasks/[id]` | Task Detail | Full task info, completion history, actions |
| `/app/tasks/[id]/edit` | Edit Task | Same form as create, pre-filled |
| `/app/tasks/[id]/complete` | Complete Task | Completion form with photo upload |
### Kanban Board
Columns (from backend `TaskColumnsResponse`):
1. **Overdue** (red indicator)
2. **Due Today** (orange indicator)
3. **Due Soon** (yellow indicator, within 30 days)
4. **Upcoming** (blue indicator)
5. **In Progress** (green indicator)
6. **Completed** (gray indicator)
**Drag-and-drop**: Use `@dnd-kit/core` + `@dnd-kit/sortable` for dragging tasks between columns. Dropping a task triggers the appropriate API action:
- Drop on "In Progress" → `POST /tasks/:id/in-progress/`
- Drop on "Completed" → Opens completion form
- Other moves may just reorder (no API call)
**Board controls**:
- Filter by residence (dropdown)
- Filter by category, priority
- Search by title
### Task Form Fields
| Field | Type | Required | Source |
|-------|------|----------|--------|
| Title | Text / Autocomplete | Yes | Free text or template |
| Residence | Select | Yes | User's residences |
| Category | Select | No | Lookups (categories) |
| Priority | Select | No | Lookups (priorities) |
| Due Date | Date picker | No | Calendar |
| Frequency | Select | No | Lookups (frequencies) — for recurring tasks |
| Estimated Cost | Number | No | Currency input |
| Notes | Textarea | No | Free text |
| Assigned Contractor | Select | No | User's contractors |
### Task Actions
| Action | Endpoint | UI Trigger |
|--------|----------|------------|
| Complete | `POST /tasks/:id/complete/` | Button + completion form |
| Mark In Progress | `POST /tasks/:id/in-progress/` | Button or drag |
| Cancel | `POST /tasks/:id/cancel/` | Menu action |
| Archive | `POST /tasks/:id/archive/` | Menu action |
| Uncancel | `POST /tasks/:id/uncancel/` | Menu action |
| Unarchive | `POST /tasks/:id/unarchive/` | Menu action |
### Task Completion Form
| Field | Type | Required |
|-------|------|----------|
| Completed At | DateTime | Yes (default: now) |
| Actual Cost | Number | No |
| Notes | Textarea | No |
| Rating | Star rating (1-5) | No |
| Photos | File upload (multiple) | No |
File upload uses `FormData` with multipart, same as mobile's `submitFormWithBinaryData`.
### Task Templates
- **Autocomplete search**: As user types in title field, search `/api/task-templates/search/?q=...`
- **Template browser**: Modal with categorized templates, click to prefill form
- Template data populates: title, category, priority, estimated cost, frequency
### API Endpoints Used
| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/api/tasks/` | All user's tasks (kanban columns) |
| GET | `/api/tasks/by-residence/:id/` | Tasks for one residence |
| POST | `/api/tasks/` | Create task |
| PUT | `/api/tasks/:id/` | Update task |
| DELETE | `/api/tasks/:id/` | Delete task |
| POST | `/api/tasks/:id/complete/` | Complete task |
| POST | `/api/tasks/:id/in-progress/` | Mark in progress |
| POST | `/api/tasks/:id/cancel/` | Cancel task |
| POST | `/api/tasks/:id/archive/` | Archive task |
| POST | `/api/task-completions/` | Create completion (with images) |
| GET | `/api/task-completions/` | List completions |
| GET | `/api/task-templates/search/` | Search templates |
### TanStack Query Hooks
```typescript
useTasks() // GET /tasks/
useTasksByResidence(residenceId) // GET /tasks/by-residence/:id/
useTask(id) // GET /tasks/:id/
useCreateTask() // POST /tasks/
useUpdateTask(id) // PUT /tasks/:id/
useDeleteTask(id) // DELETE /tasks/:id/
useCompleteTask(id) // POST /tasks/:id/complete/
useTaskTemplateSearch(query) // GET /task-templates/search/?q=...
useCreateCompletion() // POST /task-completions/
```
---
## 3. Contractors
### Screens
| Route | Screen | Description |
|-------|--------|-------------|
| `/app/contractors` | Contractor List | Filterable list with search |
| `/app/contractors/new` | Create Contractor | Form with contact details |
| `/app/contractors/[id]` | Contractor Detail | Full info, quick actions, linked tasks |
| `/app/contractors/[id]/edit` | Edit Contractor | Same form as create, pre-filled |
### List Features
- **Search**: Filter by name, company
- **Filter**: By specialty (from lookups), by favorite
- **Sort**: By name, recently added
- **Quick actions**: Call, email, favorite toggle
### Contractor Form Fields
| Field | Type | Required | Source |
|-------|------|----------|--------|
| Name | Text | Yes | Free text |
| Company | Text | No | Free text |
| Phone | Phone | No | Tel input |
| Email | Email | No | Email input |
| Specialty | Select | No | Lookups (specialties) |
| Notes | Textarea | No | Free text |
| Is Favorite | Toggle | No | Boolean |
| Residence | Select | Yes | User's residences |
### Contractor Detail
- Contact info with click-to-call (`tel:`) and click-to-email (`mailto:`)
- Favorite toggle
- Linked tasks (tasks assigned to this contractor)
- Edit / delete actions
### API Endpoints Used
| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/api/contractors/` | User's contractors |
| GET | `/api/contractors/:id/` | Contractor detail |
| POST | `/api/contractors/` | Create contractor |
| PUT | `/api/contractors/:id/` | Update contractor |
| DELETE | `/api/contractors/:id/` | Delete contractor |
| POST | `/api/contractors/:id/favorite/` | Toggle favorite |
### TanStack Query Hooks
```typescript
useContractors() // GET /contractors/
useContractor(id) // GET /contractors/:id/
useCreateContractor() // POST /contractors/
useUpdateContractor(id) // PUT /contractors/:id/
useDeleteContractor(id) // DELETE /contractors/:id/
useToggleFavorite(id) // POST /contractors/:id/favorite/
```
---
## 4. Documents
### Screens
| Route | Screen | Description |
|-------|--------|-------------|
| `/app/documents` | Document List | Tabbed view: Warranties / Documents |
| `/app/documents/new` | Create Document | Form with type-specific fields |
| `/app/documents/[id]` | Document Detail | Full info, file viewer, image gallery |
| `/app/documents/[id]/edit` | Edit Document | Same form as create, pre-filled |
### Tabbed View
- **Warranties tab**: Documents with `is_warranty = true`. Shows expiry dates, status (active/expired/expiring soon)
- **Documents tab**: All other documents. Shows type, date added, file preview
### Document Form Fields
| Field | Type | Required | Source |
|-------|------|----------|--------|
| Title | Text | Yes | Free text |
| Residence | Select | Yes | User's residences |
| Type | Select | No | Document type options |
| Notes | Textarea | No | Free text |
| File | File upload | No | File picker |
| Is Warranty | Toggle | No | Boolean |
| Purchase Date | Date | Conditional | If warranty |
| Expiry Date | Date | Conditional | If warranty |
| Purchase Price | Number | Conditional | If warranty |
### Document Detail
- File preview (PDF viewer, image gallery)
- Download button
- Warranty status indicator (if warranty)
- Edit / delete actions
### API Endpoints Used
| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/api/documents/` | User's documents |
| GET | `/api/documents/:id/` | Document detail |
| POST | `/api/documents/` | Create document |
| PUT | `/api/documents/:id/` | Update document |
| DELETE | `/api/documents/:id/` | Delete document |
| POST | `/api/documents/:id/activate/` | Activate document |
| POST | `/api/documents/:id/deactivate/` | Deactivate document |
### TanStack Query Hooks
```typescript
useDocuments() // GET /documents/
useDocument(id) // GET /documents/:id/
useCreateDocument() // POST /documents/
useUpdateDocument(id) // PUT /documents/:id/
useDeleteDocument(id) // DELETE /documents/:id/
```
---
## Shared Patterns
### Form Pattern
Every CRUD form follows the same structure:
```typescript
// src/components/forms/TaskForm.tsx
const taskSchema = z.object({
title: z.string().min(1, 'Title is required'),
residenceId: z.number().min(1, 'Residence is required'),
categoryId: z.number().optional(),
// ...
});
type TaskFormData = z.infer<typeof taskSchema>;
export function TaskForm({ task, onSubmit }: Props) {
const form = useForm<TaskFormData>({
resolver: zodResolver(taskSchema),
defaultValues: task ? mapTaskToFormData(task) : defaults,
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Fields */}
</form>
</Form>
);
}
```
### Mutation Pattern
```typescript
export function useCreateTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateTaskRequest) => api.tasks.createTask(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
queryClient.invalidateQueries({ queryKey: ['residences'] }); // Summary counts change
},
});
}
```
### Loading / Error / Empty States
Every list and detail page handles three states:
1. **Loading**: Skeleton loader (shadcn Skeleton component)
2. **Error**: Error banner with retry button
3. **Empty**: Empty state illustration with CTA to create first item
## Deliverables
At the end of Phase 2, you should have:
1. Full CRUD for all 4 domains
2. Kanban board with drag-and-drop
3. Task completion with photo upload
4. Template search and autocomplete
5. All forms validated with Zod
6. Proper cache invalidation after mutations
+309
View File
@@ -0,0 +1,309 @@
# Phase 3 — Advanced Features
Build sharing, subscriptions, notifications, profile, onboarding, and summary metrics.
## Checklist
- [ ] Residence sharing: generate/display share code, join residence, manage users, .casera file export/import
- [ ] Contractor sharing: .casera file export/import
- [ ] Subscription: status display, feature comparison, upgrade prompt, usage tracking
- [ ] Notification preferences: toggle + time picker per notification type
- [ ] Profile: edit name/email, change password, delete account
- [ ] Onboarding: multi-step wizard (fresh vs join paths)
- [ ] Summary metrics: dashboard cards with Recharts
- [ ] Task report: PDF generation + download
---
## 1. Residence Sharing
### Screens
| Route | Screen | Description |
|-------|--------|-------------|
| `/app/residences/[id]/share` | Share Residence | Generate/display share code, manage users |
| `/app/residences/join` | Join Residence | Enter share code or import .casera file |
### Share Code Flow
```
Owner opens Share screen
→ If no active share code: "Generate Share Code" button
→ POST /api/residences/:id/share/ → returns { code: "ABC123", expires_at: "..." }
→ Display code with copy button + expiry countdown
→ "Revoke" button to disable code
Invitee opens Join screen
→ Enter share code manually, OR
→ Upload .casera file (drag-and-drop zone)
→ POST /api/residences/join/ { code: "ABC123" }
→ On success: redirect to residence detail
```
### .casera File Handling
**Export** (owner):
- Button on residence detail: "Export as .casera"
- Generates JSON file with `{ type: "residence", code: "ABC123", ... }`
- Browser downloads the file
**Import** (invitee):
- Drag-and-drop zone on join page
- Or file picker button
- Read JSON, extract code, auto-submit join request
### Manage Users
| Action | Endpoint | Description |
|--------|----------|-------------|
| List users | `GET /api/residences/:id/users/` | Show all users with roles |
| Remove user | `DELETE /api/residences/:id/users/:userId/` | Owner removes a member |
### API Endpoints
| Method | Endpoint | Purpose |
|--------|----------|---------|
| POST | `/api/residences/:id/share/` | Generate share code |
| DELETE | `/api/residences/:id/share/` | Revoke share code |
| POST | `/api/residences/join/` | Join with share code |
| GET | `/api/residences/:id/users/` | List users |
| DELETE | `/api/residences/:id/users/:userId/` | Remove user |
---
## 2. Contractor Sharing
### .casera File Export/Import
**Export**:
- Button on contractor detail: "Share Contractor"
- Generates JSON file with `{ type: "contractor", name: "...", phone: "...", ... }`
- Browser downloads the file
**Import**:
- Button on contractor list: "Import Contractor"
- File picker or drag-and-drop for .casera file
- Read JSON, show confirmation dialog, create contractor
- `POST /api/contractors/import/`
---
## 3. Subscription
### Screens
| Route | Screen | Description |
|-------|--------|-------------|
| `/app/settings/subscription` | Subscription Status | Current plan, usage, upgrade CTA |
### Status Display
```
┌─────────────────────────────────────────┐
│ Current Plan: Free │
│ Residences: 1/1 Tasks: 5/5 │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Upgrade to Premium │ │
│ │ • Unlimited residences │ │
│ │ • Unlimited tasks │ │
│ │ • Document storage │ │
│ │ • Priority support │ │
│ │ [Upgrade on App Store] │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
### Feature Gating
Check tier limits before allowing creation:
- If at limit → show upgrade prompt dialog instead of create form
- Limits come from `GET /api/subscription/status/` response
### Upgrade Path (Phase 1)
Web users are directed to the mobile app for purchases:
- "Download Casera on the App Store to upgrade"
- Link to App Store listing
### Upgrade Path (Future — Phase 2)
Add Stripe Checkout for web-only subscription purchases. Requires backend work to support Stripe webhooks alongside StoreKit/Play Store.
### API Endpoints
| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/api/subscription/status/` | Current subscription + limits |
| GET | `/api/upgrade-triggers/` | Feature comparison data |
---
## 4. Notification Preferences
### Screen
| Route | Screen | Description |
|-------|--------|-------------|
| `/app/settings/notifications` | Notification Preferences | Toggle + time picker per type |
### Preference Types
| Preference | Description | Controls |
|------------|-------------|----------|
| Task Reminders | Reminders for upcoming due dates | Toggle on/off + time of day |
| Overdue Alerts | Alerts when tasks become overdue | Toggle on/off |
| Completion Confirmations | Notifications when tasks are completed | Toggle on/off |
| Residence Updates | When someone else modifies your residence | Toggle on/off |
### API Endpoints
| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/api/notifications/preferences/` | Get all preferences |
| PUT | `/api/notifications/preferences/:id/` | Update a preference |
| GET | `/api/notifications/` | List notifications (in-app inbox) |
| POST | `/api/notifications/:id/read/` | Mark notification as read |
### In-App Notification Inbox
- Bell icon in top bar with unread count badge
- Dropdown panel showing recent notifications
- Click to navigate to related item
- "Mark all as read" action
---
## 5. Profile
### Screen
| Route | Screen | Description |
|-------|--------|-------------|
| `/app/settings/profile` | Profile Settings | Edit name, email, password, delete account |
### Sections
**Personal Info:**
- First name, Last name, Email
- `PUT /api/auth/profile/`
**Change Password:**
- Current password, New password, Confirm new password
- `POST /api/auth/change-password/`
**Danger Zone:**
- Delete account button with confirmation dialog
- `DELETE /api/auth/account/`
**Theme:**
- Theme picker (11 options, preview swatches)
- Dark/light mode toggle
- Persisted to localStorage
---
## 6. Onboarding
### Screen
| Route | Screen | Description |
|-------|--------|-------------|
| `/onboarding` | Onboarding Wizard | Multi-step flow after first registration |
### Flow
```
Step 1: Welcome
→ "Welcome to Casera! Let's set up your first property."
Step 2: Choose Path
→ "Create a new residence" OR "Join an existing residence"
Path A: Create Residence
Step 3a: Residence form (name, address, type)
Step 4a: "Add your first task?" (optional quick-add)
Step 5a: Done → redirect to residence detail
Path B: Join Residence
Step 3b: Enter share code
Step 4b: Success → redirect to residence detail
```
### State Management
Use Zustand store to track onboarding progress:
```typescript
interface OnboardingState {
currentStep: number;
path: 'create' | 'join' | null;
residenceData: Partial<CreateResidenceRequest>;
isComplete: boolean;
nextStep: () => void;
prevStep: () => void;
setPath: (path: 'create' | 'join') => void;
complete: () => void;
}
```
After completion, set a flag so onboarding isn't shown again:
- `POST /api/auth/profile/` with `onboarding_complete: true`
- Or store in localStorage as fallback
---
## 7. Summary Metrics
### Dashboard (Home Page)
```
┌──────────────────────────────────────────────────────┐
│ Welcome back, Trey │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐│
│ │ 3 │ │ 2 │ │ 12 │ │ 5 ││
│ │ Overdue │ │ Due Today│ │ Active │ │ Done ││
│ │ 🔴 │ │ 🟠 │ │ 🔵 │ │ ✅ ││
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘│
│ │
│ ┌─────────────────────────────────────────────────┐│
│ │ Task Completion Trend (last 30 days) ││
│ │ [Area chart via Recharts] ││
│ └─────────────────────────────────────────────────┘│
│ │
│ Recent Activity │
│ • Task "Fix leak" completed 2h ago │
│ • New contractor "Bob's Plumbing" added yesterday │
│ • Residence "Main House" updated 3d ago │
│ │
└──────────────────────────────────────────────────────┘
```
Data sources:
- Task counts from `GET /api/tasks/` response (column counts)
- Summary calculated client-side from kanban data (same as mobile)
- Charts via Recharts library
---
## 8. Task Reports
### Feature
- Button on residence detail: "Download Task Report"
- `GET /api/residences/:id/report/` → returns PDF
- Browser triggers download
---
## Deliverables
At the end of Phase 3, you should have:
1. Residence sharing with code generation, join flow, and user management
2. Contractor sharing via .casera files
3. Subscription status with tier limits and upgrade prompts
4. Notification preferences with toggles
5. Profile editing (name, email, password, delete account)
6. Onboarding wizard for new users
7. Home dashboard with summary metrics and charts
8. Task report PDF download
+360
View File
@@ -0,0 +1,360 @@
# Phase 4 — Demo Mode
Build a fully sandboxed demo experience with realistic mock data and no backend dependency.
## Checklist
- [ ] Mock data seeds (2 residences, 15 tasks across columns, 5 contractors, 8 documents)
- [ ] DemoDataProvider implementing same interface as real API client
- [ ] Session-scoped Zustand store (resets on close)
- [ ] All CRUD operations work against in-memory store
- [ ] Demo banner + "Sign Up" CTA
- [ ] Demo landing page with feature preview
---
## Architecture
```
User visits /demo
→ Server creates a session ID (cookie, no account needed)
→ App loads with pre-built mock data (realistic residences, tasks, contractors, docs)
→ All CRUD operations work against an in-memory store scoped to that session
→ No backend API calls — everything is client-side
→ Store resets on session expiry (or browser close)
→ Banner: "You're in demo mode — Sign up to save your data"
→ "Sign Up" converts session → real account (data NOT migrated, clean start)
```
### Context Switch
A `DataProvider` context determines whether the app uses real API calls or demo data:
```typescript
// src/lib/demo/demo-context.tsx
interface DataProvider {
residences: {
list: () => Promise<Residence[]>;
get: (id: number) => Promise<ResidenceDetail>;
create: (data: CreateResidenceRequest) => Promise<Residence>;
update: (id: number, data: UpdateResidenceRequest) => Promise<Residence>;
delete: (id: number) => Promise<void>;
};
tasks: { /* same pattern */ };
contractors: { /* same pattern */ };
documents: { /* same pattern */ };
}
const DataProviderContext = createContext<DataProvider>(realApiProvider);
export function DemoProvider({ children }: { children: ReactNode }) {
const demoStore = useDemoStore();
const demoProvider = createDemoProvider(demoStore);
return (
<DataProviderContext.Provider value={demoProvider}>
<DemoBanner />
{children}
</DataProviderContext.Provider>
);
}
export function useDataProvider() {
return useContext(DataProviderContext);
}
```
### Layout Integration
```
src/app/(demo)/layout.tsx → wraps children in <DemoProvider>
src/app/(app)/layout.tsx → wraps children in <RealProvider> (or just uses default)
```
Both route groups share the same page components — the data provider abstraction means zero code duplication.
---
## 1. Mock Data Seeds
### Residences (2)
```typescript
const mockResidences = [
{
id: 1,
name: "Maple Street House",
address: "142 Maple Street, Austin, TX",
residenceType: "House",
taskSummary: { overdue: 2, dueToday: 1, dueSoon: 3, upcoming: 5, completed: 8 },
contractorCount: 3,
documentCount: 5,
},
{
id: 2,
name: "Downtown Apartment",
address: "800 Congress Ave #412, Austin, TX",
residenceType: "Apartment",
taskSummary: { overdue: 0, dueToday: 1, dueSoon: 2, upcoming: 3, completed: 4 },
contractorCount: 2,
documentCount: 3,
},
];
```
### Tasks (15 across columns)
Realistic tasks distributed across kanban columns:
| Column | Count | Examples |
|--------|-------|---------|
| Overdue | 2 | "Replace smoke detector batteries" (due 2 weeks ago), "Clean gutters" (due 1 month ago) |
| Due Today | 2 | "HVAC filter replacement", "Test garage door opener" |
| Due Soon | 3 | "Power wash driveway" (5 days), "Inspect roof" (2 weeks), "Service water heater" (3 weeks) |
| Upcoming | 4 | Various maintenance tasks due 1-3 months out |
| In Progress | 1 | "Paint master bedroom" (in progress since yesterday) |
| Completed | 3 | "Fix kitchen faucet leak", "Install new doorbell", "Reseal bathroom grout" |
Each task has realistic:
- Categories (Maintenance, Cleaning, Repair, etc.)
- Priorities (High, Medium, Low)
- Estimated costs ($25 - $500)
- Some assigned to contractors
- Completed tasks have completion records with notes
### Contractors (5)
```typescript
const mockContractors = [
{ id: 1, name: "Bob Martinez", company: "Bob's Plumbing", phone: "(512) 555-0101", email: "bob@bobsplumbing.com", specialty: "Plumbing", isFavorite: true },
{ id: 2, name: "Sarah Chen", company: "Chen Electric", phone: "(512) 555-0202", email: "sarah@chenelectric.com", specialty: "Electrical", isFavorite: true },
{ id: 3, name: "Mike Johnson", company: null, phone: "(512) 555-0303", email: null, specialty: "General Handyman", isFavorite: false },
{ id: 4, name: "Lisa Park", company: "Park Landscaping", phone: "(512) 555-0404", email: "lisa@parklandscaping.com", specialty: "Landscaping", isFavorite: false },
{ id: 5, name: "James Wilson", company: "Wilson HVAC", phone: "(512) 555-0505", email: "james@wilsonhvac.com", specialty: "HVAC", isFavorite: true },
];
```
### Documents (8)
Mix of warranties and general documents:
| Type | Count | Examples |
|------|-------|---------|
| Warranty | 4 | "Samsung Refrigerator Warranty" (expires 2027), "Roof Warranty" (expires 2035), "HVAC System Warranty" (expired 2024), "Dishwasher Warranty" (expires 2026) |
| Document | 4 | "Home Insurance Policy", "Property Tax Statement 2025", "HOA Agreement", "Electrical Inspection Certificate" |
### Lookups
Pre-loaded categories, priorities, frequencies, specialties — same as production static data.
---
## 2. DemoDataProvider
```typescript
// src/lib/demo/demo-provider.ts
export function createDemoProvider(store: DemoStore): DataProvider {
return {
residences: {
list: async () => store.getResidences(),
get: async (id) => store.getResidence(id),
create: async (data) => store.createResidence(data),
update: async (id, data) => store.updateResidence(id, data),
delete: async (id) => store.deleteResidence(id),
},
tasks: {
list: async () => store.getTasks(),
create: async (data) => store.createTask(data),
complete: async (id, data) => store.completeTask(id, data),
// ... all task actions
},
contractors: { /* ... */ },
documents: { /* ... */ },
};
}
```
All operations are synchronous under the hood (wrapped in async for interface compatibility). They:
1. Validate input (same Zod schemas as real forms)
2. Generate auto-increment IDs
3. Update the Zustand store
4. Return the created/updated object
---
## 3. Session-Scoped Zustand Store
```typescript
// src/lib/demo/demo-store.ts
interface DemoStore {
// Data
residences: Residence[];
tasks: Task[];
contractors: Contractor[];
documents: Document[];
completions: TaskCompletion[];
lookups: StaticData;
// Auto-increment IDs
nextIds: { residence: number; task: number; contractor: number; document: number };
// CRUD operations
createResidence: (data: CreateResidenceRequest) => Residence;
updateResidence: (id: number, data: UpdateResidenceRequest) => Residence;
deleteResidence: (id: number) => void;
// ... same for all domains
// Task-specific
completeTask: (id: number, data: TaskCompletionRequest) => Task;
markInProgress: (id: number) => Task;
cancelTask: (id: number) => Task;
archiveTask: (id: number) => Task;
// Reset
reset: () => void;
}
export const useDemoStore = create<DemoStore>((set, get) => ({
residences: mockResidences,
tasks: mockTasks,
contractors: mockContractors,
documents: mockDocuments,
completions: mockCompletions,
lookups: mockLookups,
nextIds: { residence: 3, task: 16, contractor: 6, document: 9 },
createResidence: (data) => {
const id = get().nextIds.residence;
const residence = { id, ...data, createdAt: new Date().toISOString() };
set((state) => ({
residences: [...state.residences, residence],
nextIds: { ...state.nextIds, residence: id + 1 },
}));
return residence;
},
// ... other operations
reset: () => set({
residences: mockResidences,
tasks: mockTasks,
contractors: mockContractors,
documents: mockDocuments,
completions: mockCompletions,
nextIds: { residence: 3, task: 16, contractor: 6, document: 9 },
}),
}));
```
### Session Isolation
- Store is created fresh when `/demo` is accessed
- No `persist` middleware — store is pure in-memory
- Closing the browser tab or navigating away clears the store
- Each visitor gets their own Zustand store instance
---
## 4. Demo Banner
Persistent banner at the top of the demo app:
```
┌─────────────────────────────────────────────────────────────────┐
│ 🎯 You're exploring Casera in demo mode. [Sign Up Free] │
│ Changes aren't saved. Create an account to get started! │
└─────────────────────────────────────────────────────────────────┘
```
- Fixed at top, doesn't scroll
- "Sign Up Free" links to `/register`
- Subtle, non-intrusive (light background, small text)
- Can be temporarily dismissed (per session only)
---
## 5. Demo Landing Page
### Route: `/demo`
Before entering the demo, show a brief preview page:
```
┌─────────────────────────────────────────────────────┐
│ │
│ 🏠 Try Casera — No Account Needed │
│ │
│ Manage your home maintenance, track tasks, │
│ organize contractors, and store documents. │
│ │
│ [Start Demo →] │
│ │
│ ──────────────────────────────────────── │
│ │
│ Preview: │
│ [Screenshot of kanban board] │
│ [Screenshot of residence detail] │
│ │
│ Already have an account? [Log In] │
│ │
└─────────────────────────────────────────────────────┘
```
Clicking "Start Demo" initializes the demo store and redirects to `/demo/residences`.
---
## 6. TanStack Query Integration
In demo mode, TanStack Query hooks still work — they just call the demo provider instead of real API:
```typescript
// src/lib/hooks/useResidences.ts
export function useResidences() {
const provider = useDataProvider();
return useQuery({
queryKey: ['residences'],
queryFn: () => provider.residences.list(),
staleTime: Infinity, // Demo data never goes stale
});
}
```
Mutations also work through the provider:
```typescript
export function useCreateResidence() {
const provider = useDataProvider();
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateResidenceRequest) => provider.residences.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['residences'] });
},
});
}
```
---
## Key Design Decisions
| Decision | Rationale |
|----------|-----------|
| No backend calls in demo | Zero infrastructure cost, no rate limiting, instant responses, no auth needed |
| Session-scoped (no persistence) | Prevents demo data from accumulating, ensures fresh experience each visit |
| Same components as real app | Zero code duplication, demo accurately represents the product |
| Provider pattern (not if/else) | Clean separation, easy to test, no demo logic leaking into components |
| Data NOT migrated on signup | Avoids edge cases with ID conflicts, mock data quality issues |
## Deliverables
At the end of Phase 4, you should have:
1. Realistic mock data covering all 4 domains
2. Fully functional CRUD in demo mode (create, edit, delete tasks/residences/etc.)
3. Demo banner with sign-up CTA
4. Landing page with preview
5. Zero backend dependency for the demo experience
6. Same UI components used for both demo and real app
+399
View File
@@ -0,0 +1,399 @@
# Phase 5 — Polish & Deploy
Responsive design, error handling, deployment, testing, and performance optimization.
## Checklist
- [ ] Responsive design (mobile-first, works on phone browsers too)
- [ ] Loading states, empty states, error handling for every screen
- [ ] Dockerfile + Dokku deployment config
- [ ] E2E tests (Playwright) for critical paths
- [ ] Performance optimization (code splitting, image optimization)
- [ ] SEO + Open Graph meta tags for marketing/demo pages
- [ ] Accessibility audit (keyboard navigation, screen readers, ARIA labels)
- [ ] Analytics integration (PostHog)
---
## 1. Responsive Design
### Breakpoint Strategy
| Breakpoint | Target | Layout |
|------------|--------|--------|
| `<640px` | Mobile phones | Bottom tab bar, stacked cards, full-width forms |
| `640-1023px` | Tablets | Collapsed sidebar (icons only), 2-column grids |
| `≥1024px` | Desktop | Full sidebar, 3-column grids, side panels |
### Mobile Adaptations
- **Navigation**: Bottom tab bar replaces sidebar (matches iOS app experience)
- **Kanban board**: Horizontal scroll with swipeable columns (single column visible at a time on small screens)
- **Cards**: Full-width stacked layout
- **Forms**: Single column, full-width inputs
- **Tables**: Convert to card-based layout on mobile
- **Dialogs**: Full-screen on mobile, centered modal on desktop
### Implementation
Use Tailwind responsive prefixes consistently:
```tsx
// Grid that adapts from 1 → 2 → 3 columns
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{residences.map(r => <ResidenceCard key={r.id} residence={r} />)}
</div>
// Sidebar visibility
<aside className="hidden lg:flex lg:w-64 ..."> {/* Desktop sidebar */}
<nav className="lg:hidden fixed bottom-0 ..."> {/* Mobile bottom bar */}
```
---
## 2. Loading, Empty, and Error States
### Loading States
Every data-dependent component must show a loading skeleton:
```tsx
function ResidenceList() {
const { data, isLoading, error } = useResidences();
if (isLoading) return <ResidenceListSkeleton />;
if (error) return <ErrorBanner error={error} onRetry={() => refetch()} />;
if (!data?.length) return <EmptyState icon="building" title="No residences" cta="Add Residence" />;
return <div>{data.map(r => <ResidenceCard key={r.id} residence={r} />)}</div>;
}
```
### Skeleton Components
Use shadcn/ui `Skeleton` for consistent loading states:
- Card skeletons for list pages
- Form skeletons for detail pages
- Kanban column skeletons for task board
### Empty States
Every list has a meaningful empty state:
| Screen | Icon | Title | Subtitle | CTA |
|--------|------|-------|----------|-----|
| Residences | Building | No residences yet | Add your first property to get started | Add Residence |
| Tasks | CheckSquare | No tasks | Create a task to start tracking maintenance | Add Task |
| Contractors | HardHat | No contractors | Save your trusted service providers | Add Contractor |
| Documents | FileText | No documents | Store important home documents | Add Document |
### Error Handling
```typescript
// Global error boundary
// src/app/error.tsx
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px]">
<AlertCircle className="h-12 w-12 text-destructive" />
<h2 className="mt-4 text-lg font-semibold">Something went wrong</h2>
<p className="mt-2 text-muted-foreground">{error.message}</p>
<Button onClick={reset} className="mt-4">Try Again</Button>
</div>
);
}
```
### Toast Notifications
Use shadcn/ui `Toaster` for success/error feedback:
- "Residence created successfully" (green)
- "Task completed" (green)
- "Failed to save changes" (red)
- "Connection lost — retrying..." (yellow)
---
## 3. Dockerfile + Deployment
### Dockerfile
```dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 nodejs && adduser -u 1001 -G nodejs -s /bin/sh -D nextjs
# Copy standalone build
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD curl -f http://localhost:3000/ || exit 1
CMD ["node", "server.js"]
```
### next.config.ts
```typescript
const nextConfig = {
output: 'standalone', // Required for Docker
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'mycrib.treytartt.com' }, // API media
],
},
};
```
### Dokku Deployment
```bash
# On mycribDev server
dokku apps:create casera-web
dokku domains:add casera-web app.casera.treytartt.com
dokku config:set casera-web NEXT_PUBLIC_API_URL=https://mycrib.treytartt.com/api
dokku letsencrypt:enable casera-web
# Deploy
git remote add dokku-web dokku@mycribDev:casera-web
git push dokku-web main
```
### Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `NEXT_PUBLIC_API_URL` | Go API base URL | Yes |
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog project key | No |
| `NEXT_PUBLIC_POSTHOG_HOST` | PostHog host URL | No |
| `PORT` | Server port (default: 3000) | No |
---
## 4. E2E Tests (Playwright)
### Critical Path Tests
```
tests/
├── auth.spec.ts # Login, register, logout
├── residences.spec.ts # Create, view, edit, delete residence
├── tasks.spec.ts # Create task, kanban drag, complete task
├── contractors.spec.ts # Create, view, edit, delete contractor
├── documents.spec.ts # Create, upload, view, delete document
├── demo.spec.ts # Demo mode flows (no backend)
└── responsive.spec.ts # Mobile/tablet viewport tests
```
### Test Structure
```typescript
// tests/auth.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('login with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/app/residences');
});
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'bad@example.com');
await page.fill('[name="password"]', 'wrong');
await page.click('button[type="submit"]');
await expect(page.locator('[role="alert"]')).toBeVisible();
});
});
```
### Demo Mode Tests (No Backend)
```typescript
// tests/demo.spec.ts
test.describe('Demo Mode', () => {
test('loads with mock data', async ({ page }) => {
await page.goto('/demo');
await page.click('text=Start Demo');
await expect(page.locator('text=Maple Street House')).toBeVisible();
});
test('can create a task in demo mode', async ({ page }) => {
await page.goto('/demo/tasks');
await page.click('text=Add Task');
await page.fill('[name="title"]', 'Test Demo Task');
await page.click('button[type="submit"]');
await expect(page.locator('text=Test Demo Task')).toBeVisible();
});
});
```
### Playwright Config
```typescript
// playwright.config.ts
export default defineConfig({
testDir: './tests',
use: {
baseURL: 'http://localhost:3000',
screenshot: 'only-on-failure',
trace: 'on-first-retry',
},
projects: [
{ name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 14'] } },
{ name: 'Tablet', use: { viewport: { width: 768, height: 1024 } } },
],
});
```
---
## 5. Performance Optimization
### Code Splitting
- Dynamic imports for heavy components:
```typescript
const KanbanBoard = dynamic(() => import('@/components/kanban/KanbanBoard'), {
loading: () => <KanbanSkeleton />,
});
const RechartsDashboard = dynamic(() => import('@/components/dashboard/Charts'));
```
### Image Optimization
- Use Next.js `<Image>` component for all images
- API media served through Next.js image optimization proxy
- Lazy load images below the fold
### Bundle Analysis
```bash
npm install -D @next/bundle-analyzer
# Analyze with: ANALYZE=true npm run build
```
### TanStack Query Optimization
- `staleTime: 60 * 60 * 1000` (1 hour) for most queries
- `refetchOnWindowFocus: true` for fresh data when user returns
- `keepPreviousData: true` for smooth pagination/filtering transitions
- Prefetch on hover for navigation links
---
## 6. SEO + Open Graph
Marketing and demo pages need proper meta tags:
```typescript
// src/app/(marketing)/page.tsx
export const metadata: Metadata = {
title: 'Casera — Home Maintenance Made Simple',
description: 'Track tasks, organize contractors, store documents. Manage your home maintenance in one place.',
openGraph: {
title: 'Casera — Home Maintenance Made Simple',
description: 'Track tasks, organize contractors, store documents.',
images: ['/og-image.png'],
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Casera',
description: 'Home Maintenance Made Simple',
images: ['/og-image.png'],
},
};
```
---
## 7. Accessibility
### Requirements
- All interactive elements keyboard accessible
- Proper ARIA labels on icons and buttons
- Focus management on modals and route changes
- Color contrast meets WCAG 2.1 AA
- Screen reader compatible forms with error announcements
### Testing
```bash
# axe-core integration
npm install -D @axe-core/playwright
# In tests:
import AxeBuilder from '@axe-core/playwright';
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
```
---
## 8. Analytics (PostHog)
```typescript
// src/lib/analytics.ts
import posthog from 'posthog-js';
export function initAnalytics() {
if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://analytics.88oakapps.com',
});
}
}
export function trackEvent(event: string, properties?: Record<string, any>) {
posthog.capture(event, properties);
}
export function trackScreen(screenName: string) {
posthog.capture('$pageview', { $current_url: screenName });
}
```
Track same events as mobile app for consistent analytics.
---
## Deliverables
At the end of Phase 5, you should have:
1. Fully responsive web app (mobile, tablet, desktop)
2. Consistent loading, empty, and error states everywhere
3. Deployed on Dokku at `app.casera.treytartt.com`
4. E2E tests covering auth, CRUD, demo mode, and responsive viewports
5. Optimized bundle with code splitting and image optimization
6. SEO-ready marketing and demo pages
7. Accessible UI meeting WCAG 2.1 AA
8. PostHog analytics tracking
File diff suppressed because it is too large Load Diff
+781
View File
@@ -0,0 +1,781 @@
# Phase 3 — Advanced Features Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Build sharing, subscriptions, notifications, profile settings, onboarding wizard, summary metrics with charts, and task report PDF download.
**Architecture:** A shared foundation layer (hooks, settings hub, notification bell) is built first, then 5 domain agents run concurrently on independent feature areas. Each agent produces pages and components that compile independently.
**Tech Stack:** Next.js 16 (App Router), TanStack Query v5, React Hook Form + Zod 4, shadcn/ui + Radix, Tailwind CSS 4, Recharts (new), Zustand, Lucide icons.
---
## Execution Strategy: 7 Tasks, 5 Run in Parallel
```
Task 1: Shared Foundation (sequential — all features depend on this)
Task 2: Sharing (residence + contractor) ─┐
Task 3: Profile Settings ─┤
Task 4: Notifications + Subscription ─┤ ← 5 parallel agents
Task 5: Onboarding Wizard ─┤
Task 6: Dashboard + Task Reports ─┘
Task 7: Integration + Verification (sequential — cross-feature wiring)
```
**Verification gate:** Each task ends with `npm run build`. No task is complete until it compiles clean.
---
## Task 1: Shared Foundation
**Files:**
- Create: `src/lib/hooks/use-notifications.ts`
- Create: `src/lib/hooks/use-subscription.ts`
- Create: `src/components/notifications/notification-bell.tsx`
- Create: `src/app/app/settings/layout.tsx`
- Modify: `src/app/app/settings/page.tsx` (overwrite stub → settings hub)
- Modify: `src/components/layout/top-bar.tsx` (add notification bell)
- Modify: `src/lib/hooks/index.ts` (add new hook exports)
### Step 1: Install Recharts
```bash
cd /Users/treyt/Desktop/code/MyCrib/myCribAPI-Web
npm install recharts
```
### Step 2: Create notification hooks
```typescript
// src/lib/hooks/use-notifications.ts
"use client";
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as notificationsApi from '@/lib/api/notifications';
import type { UpdatePreferencesRequest } from '@/lib/api/notifications';
export function useNotifications(limit?: number) {
return useQuery({
queryKey: ['notifications', limit],
queryFn: () => notificationsApi.listNotifications(limit),
});
}
export function useUnreadCount() {
return useQuery({
queryKey: ['notifications', 'unread-count'],
queryFn: () => notificationsApi.getUnreadCount(),
refetchInterval: 60_000, // Poll every minute
});
}
export function useNotificationPreferences() {
return useQuery({
queryKey: ['notifications', 'preferences'],
queryFn: () => notificationsApi.getPreferences(),
});
}
export function useUpdatePreferences() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UpdatePreferencesRequest) => notificationsApi.updatePreferences(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notifications', 'preferences'] });
},
});
}
export function useMarkAsRead() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => notificationsApi.markAsRead(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notifications'] });
queryClient.invalidateQueries({ queryKey: ['notifications', 'unread-count'] });
},
});
}
export function useMarkAllAsRead() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => notificationsApi.markAllAsRead(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notifications'] });
queryClient.invalidateQueries({ queryKey: ['notifications', 'unread-count'] });
},
});
}
```
### Step 3: Create subscription hooks
```typescript
// src/lib/hooks/use-subscription.ts
"use client";
import { useQuery } from '@tanstack/react-query';
import * as subscriptionApi from '@/lib/api/subscription';
export function useSubscriptionStatus() {
return useQuery({
queryKey: ['subscription', 'status'],
queryFn: () => subscriptionApi.getSubscriptionStatus(),
});
}
export function useFeatureBenefits() {
return useQuery({
queryKey: ['subscription', 'features'],
queryFn: () => subscriptionApi.getFeatureBenefits(),
staleTime: Infinity,
});
}
export function useUpgradeTriggers() {
return useQuery({
queryKey: ['subscription', 'upgrade-triggers'],
queryFn: () => subscriptionApi.getAllUpgradeTriggers(),
staleTime: Infinity,
});
}
```
### Step 4: Create NotificationBell component
```tsx
// src/components/notifications/notification-bell.tsx
"use client";
import { useState } from "react";
import { Bell } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Badge } from "@/components/ui/badge";
import { useNotifications, useUnreadCount, useMarkAsRead, useMarkAllAsRead } from "@/lib/hooks/use-notifications";
export function NotificationBell() {
const { data: unreadData } = useUnreadCount();
const { data: notifData } = useNotifications(10);
const markAsRead = useMarkAsRead();
const markAllAsRead = useMarkAllAsRead();
const unreadCount = unreadData?.unread_count ?? 0;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Bell className="size-5" />
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 flex size-5 items-center justify-center rounded-full bg-destructive text-[10px] font-bold text-destructive-foreground">
{unreadCount > 9 ? "9+" : unreadCount}
</span>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80 max-h-96 overflow-y-auto">
<div className="flex items-center justify-between px-3 py-2">
<p className="text-sm font-semibold">Notifications</p>
{unreadCount > 0 && (
<Button variant="ghost" size="sm" className="text-xs h-auto py-1" onClick={() => markAllAsRead.mutate()}>
Mark all read
</Button>
)}
</div>
<DropdownMenuSeparator />
{(!notifData || notifData.results.length === 0) ? (
<div className="px-3 py-6 text-center text-sm text-muted-foreground">No notifications</div>
) : (
notifData.results.map((n) => (
<DropdownMenuItem key={n.id} className="flex-col items-start gap-1 py-2"
onClick={() => { if (!n.is_read) markAsRead.mutate(n.id); }}>
<p className={`text-sm ${n.is_read ? "text-muted-foreground" : "font-medium"}`}>{n.title}</p>
<p className="text-xs text-muted-foreground">{n.body}</p>
<p className="text-xs text-muted-foreground">{new Date(n.created_at).toLocaleDateString()}</p>
</DropdownMenuItem>
))
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
```
### Step 5: Modify TopBar to include notification bell
In `src/components/layout/top-bar.tsx`, add `<NotificationBell />` before the profile dropdown. Import from `@/components/notifications/notification-bell`.
Add a `<div className="flex items-center gap-2">` wrapper around the bell and avatar dropdown.
### Step 6: Create settings layout with sidebar navigation
```tsx
// src/app/app/settings/layout.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import { User, Bell, CreditCard } from "lucide-react";
const settingsNav = [
{ label: "Profile", href: "/app/settings/profile", icon: User },
{ label: "Notifications", href: "/app/settings/notifications", icon: Bell },
{ label: "Subscription", href: "/app/settings/subscription", icon: CreditCard },
];
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold tracking-tight">Settings</h1>
<div className="flex flex-col sm:flex-row gap-6">
<nav className="flex sm:flex-col gap-1 sm:w-48 shrink-0">
{settingsNav.map((item) => (
<Link key={item.href} href={item.href}
className={cn(
"flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors",
"hover:bg-accent hover:text-accent-foreground",
pathname === item.href ? "bg-accent text-accent-foreground" : "text-muted-foreground"
)}>
<item.icon className="size-4" />
{item.label}
</Link>
))}
</nav>
<div className="flex-1 min-w-0">{children}</div>
</div>
</div>
);
}
```
### Step 7: Overwrite settings page stub → redirect to profile
```tsx
// src/app/app/settings/page.tsx
import { redirect } from "next/navigation";
export default function SettingsPage() {
redirect("/app/settings/profile");
}
```
### Step 8: Update hooks barrel export
Add to `src/lib/hooks/index.ts`:
```typescript
export * from './use-notifications';
export * from './use-subscription';
```
### Step 9: Verify build
```bash
cd /Users/treyt/Desktop/code/MyCrib/myCribAPI-Web && npm run build
```
### Step 10: Commit
```bash
git add -A && git commit -m "feat: add Phase 3 foundation — notification hooks, subscription hooks, settings layout, notification bell"
```
---
## Task 2: Sharing (Residence + Contractor)
**Depends on:** Task 1
**Files:**
- Create: `src/app/app/residences/[id]/share/page.tsx`
- Create: `src/app/app/residences/join/page.tsx`
- Create: `src/components/sharing/share-code-display.tsx`
- Create: `src/components/sharing/user-management.tsx`
- Create: `src/components/sharing/casera-file-handler.tsx`
- Create: `src/lib/hooks/use-sharing.ts`
- Modify: `src/app/app/residences/[id]/page.tsx` (add Share button)
- Modify: `src/app/app/contractors/[id]/page.tsx` (add Share/Export button)
- Modify: `src/app/app/contractors/page.tsx` (add Import button)
### Residence Sharing Components
**share-code-display.tsx** — Displays share code with copy button and expiry countdown:
- If no active code: "Generate Share Code" button → calls `POST /residences/:id/generate-share-code/`
- If active code: displays code in large monospace text, copy-to-clipboard button, expiry timer
- Uses existing `residencesApi.generateShareCode()` and `residencesApi.getShareCode()`
**user-management.tsx** — Lists residence users with remove action:
- Uses `residencesApi.getResidenceUsers(id)` to fetch user list
- Each user row: name, email, role badge (Owner/Member)
- Owner can click "Remove" → ConfirmDialog → `residencesApi.removeResidenceUser(residenceId, userId)`
### .casera File Handler
**casera-file-handler.tsx** — Reusable component for both residence and contractor sharing:
- **Export mode**: Takes data object, generates JSON `{ type: "residence"|"contractor", ... }`, triggers browser download as `.casera` file
- **Import mode**: FileUpload drop zone accepting `.casera` files, reads JSON, validates type, calls callback with parsed data
- Uses `URL.createObjectURL` + anchor click for download
- Uses `FileReader.readAsText` for import
### Pages
**Residence Share page** (`/app/residences/[id]/share`):
- ShareCodeDisplay for generating/displaying share codes
- .casera export button → downloads file with share code embedded
- UserManagement table
- Only accessible to residence owner
**Residence Join page** (`/app/residences/join`):
- Text input for manual code entry
- .casera file import drop zone
- On submit: calls `residencesApi.joinWithCode()` → redirect to residence detail
### Contractor Sharing
On contractor detail page, add "Share" button → generates .casera file with contractor data for download.
On contractor list page, add "Import" button → opens import dialog → reads .casera file → creates contractor via API.
### Query hooks
```typescript
// src/lib/hooks/use-sharing.ts
"use client";
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as residencesApi from '@/lib/api/residences';
export function useShareCode(residenceId: number) {
return useQuery({
queryKey: ['residences', residenceId, 'share-code'],
queryFn: () => residencesApi.getShareCode(residenceId),
enabled: !!residenceId,
});
}
export function useGenerateShareCode(residenceId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => residencesApi.generateShareCode(residenceId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['residences', residenceId, 'share-code'] });
},
});
}
export function useResidenceUsers(residenceId: number) {
return useQuery({
queryKey: ['residences', residenceId, 'users'],
queryFn: () => residencesApi.getResidenceUsers(residenceId),
enabled: !!residenceId,
});
}
export function useRemoveResidenceUser(residenceId: number) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userId: number) => residencesApi.removeResidenceUser(residenceId, userId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['residences', residenceId, 'users'] });
},
});
}
export function useJoinResidence() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (code: string) => residencesApi.joinWithCode({ code }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['residences'] });
},
});
}
```
### Verify build
```bash
cd /Users/treyt/Desktop/code/MyCrib/myCribAPI-Web && npm run build
```
### Commit
```bash
git add -A && git commit -m "feat: add residence sharing (share code, join, user management) and contractor .casera export/import"
```
---
## Task 3: Profile Settings
**Depends on:** Task 1
**Files:**
- Create: `src/app/app/settings/profile/page.tsx`
- Create: `src/components/settings/profile-form.tsx`
- Create: `src/components/settings/change-password-form.tsx`
- Create: `src/components/settings/delete-account-section.tsx`
- Create: `src/components/settings/theme-picker.tsx`
- Modify: `src/lib/api/auth.ts` (add changePassword and deleteAccount functions if missing)
### API additions needed in auth.ts
Check if these exist, add if missing:
```typescript
export async function changePassword(data: { current_password: string; new_password: string }): Promise<MessageResponse> {
const res = await fetch('/api/proxy/auth/change-password/', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Timezone': timezone() },
body: JSON.stringify(data),
});
return handleResponse<MessageResponse>(res);
}
export async function deleteAccount(): Promise<MessageResponse> {
const res = await fetch('/api/proxy/auth/delete-account/', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'X-Timezone': timezone() },
});
return handleResponse<MessageResponse>(res);
}
```
### Components
**profile-form.tsx** — Edit first name, last name, email:
- Zod schema: first_name (required), last_name (required), email (required, valid email)
- Pre-fills from current user (from auth store)
- On submit: calls `authApi.updateProfile()` → updates auth store user
- Success toast / inline success message
**change-password-form.tsx** — Change password:
- Zod schema: current_password (required, min 8), new_password (required, min 8), confirm_password (must match new_password)
- On submit: calls `authApi.changePassword()` → clear form on success
**delete-account-section.tsx** — Danger zone:
- Red bordered card with warning text
- "Delete Account" button → ConfirmDialog with "Type DELETE to confirm" pattern
- On confirm: calls `authApi.deleteAccount()` → logout → redirect to login
**theme-picker.tsx** — Visual theme selector:
- Grid of 11 theme swatches (colored circles/squares from theme-config)
- Click to select → applies theme via `useTheme()` hook from existing theme store
- Dark/light toggle using existing theme store
- Read theme config from `src/lib/themes/theme-config.ts`
### Profile page
```tsx
// src/app/app/settings/profile/page.tsx
"use client";
// 4 sections stacked vertically:
// 1. ProfileForm (personal info)
// 2. ChangePasswordForm
// 3. ThemePicker
// 4. DeleteAccountSection (danger zone at bottom)
```
### Verify build
```bash
cd /Users/treyt/Desktop/code/MyCrib/myCribAPI-Web && npm run build
```
### Commit
```bash
git add -A && git commit -m "feat: add profile settings — edit info, change password, theme picker, delete account"
```
---
## Task 4: Notifications + Subscription
**Depends on:** Task 1
**Files:**
- Create: `src/app/app/settings/notifications/page.tsx`
- Create: `src/app/app/settings/subscription/page.tsx`
- Create: `src/components/settings/notification-preferences.tsx`
- Create: `src/components/settings/subscription-status.tsx`
- Create: `src/components/settings/feature-comparison.tsx`
- Create: `src/components/shared/upgrade-prompt.tsx`
### Notification Preferences
**notification-preferences.tsx** — Toggle switches per notification type:
- Uses `useNotificationPreferences()` to load current state
- Each preference row: label, description, toggle switch (shadcn Switch component — install via shadcn if not present)
- On toggle: calls `useUpdatePreferences()` with changed field
- Preference types: Task Reminders, Task Completions, Residence Updates, Share Notifications, Marketing
**Notifications page** (`/app/settings/notifications`):
- NotificationPreferences component
- Section header "Notification Preferences"
- Each toggle saves immediately (optimistic update)
### Subscription
**subscription-status.tsx** — Current plan display:
- Uses `useSubscriptionStatus()` to load tier, limits, usage
- Shows current tier name (Free/Premium)
- Progress bars for usage: residences (X/max), tasks per residence (X/max), contractors (X/max), documents (X/max)
- If free tier: show upgrade CTA section
**feature-comparison.tsx** — Free vs Premium comparison:
- Uses `useFeatureBenefits()` to load feature list
- Two-column comparison table: Free vs Premium
- Check/cross icons per feature
- "Upgrade on App Store" button linking to App Store
**upgrade-prompt.tsx** — Reusable upgrade prompt dialog:
- Shown when user hits a tier limit (e.g., max residences reached)
- Takes `feature` prop (what they're trying to do)
- Shows limit info + upgrade CTA
- Uses shadcn Dialog
- Can be imported by any domain page that needs feature gating
**Subscription page** (`/app/settings/subscription`):
- SubscriptionStatus card at top
- FeatureComparison below
- If premium: show expiry date, management info
### Install Switch component
```bash
npx shadcn@latest add switch
```
### Verify build
```bash
cd /Users/treyt/Desktop/code/MyCrib/myCribAPI-Web && npm run build
```
### Commit
```bash
git add -A && git commit -m "feat: add notification preferences and subscription status with feature comparison"
```
---
## Task 5: Onboarding Wizard
**Depends on:** Task 1
**Files:**
- Create: `src/app/onboarding/page.tsx`
- Create: `src/app/onboarding/layout.tsx`
- Create: `src/components/onboarding/welcome-step.tsx`
- Create: `src/components/onboarding/choose-path-step.tsx`
- Create: `src/components/onboarding/create-residence-step.tsx`
- Create: `src/components/onboarding/first-task-step.tsx`
- Create: `src/components/onboarding/join-residence-step.tsx`
- Create: `src/components/onboarding/complete-step.tsx`
- Create: `src/stores/onboarding.ts`
### Onboarding Zustand Store
```typescript
// src/stores/onboarding.ts
import { create } from "zustand";
interface OnboardingState {
currentStep: number;
path: "create" | "join" | null;
residenceId: number | null;
isComplete: boolean;
nextStep: () => void;
prevStep: () => void;
setPath: (path: "create" | "join") => void;
setResidenceId: (id: number) => void;
complete: () => void;
reset: () => void;
}
export const useOnboardingStore = create<OnboardingState>()((set) => ({
currentStep: 0,
path: null,
residenceId: null,
isComplete: false,
nextStep: () => set((s) => ({ currentStep: s.currentStep + 1 })),
prevStep: () => set((s) => ({ currentStep: Math.max(0, s.currentStep - 1) })),
setPath: (path) => set({ path, currentStep: 2 }),
setResidenceId: (id) => set({ residenceId: id }),
complete: () => set({ isComplete: true }),
reset: () => set({ currentStep: 0, path: null, residenceId: null, isComplete: false }),
}));
```
### Onboarding Layout
- Clean, centered layout (no sidebar or app shell)
- Progress indicator (step dots or progress bar)
- Casera logo at top
### Steps
**Step 0: Welcome** — "Welcome to Casera!" message, illustration, "Get Started" button
**Step 1: Choose Path** — Two cards: "Create a new residence" (House icon) and "Join an existing residence" (Users icon). Clicking sets path and advances.
**Path A — Create:**
- **Step 2a: Create Residence** — Simplified ResidenceForm (name, address, type only). On submit: creates residence via `useCreateResidence()`, stores ID, advances.
- **Step 3a: First Task (optional)** — "Add your first task?" Quick task form (title + due date only) or "Skip" button. On submit: creates task, advances. On skip: advances.
- **Step 4a: Complete** — "You're all set!" Redirect to `/app/residences/${id}`
**Path B — Join:**
- **Step 2b: Join Residence** — Code input (6-char) or .casera file import. On submit: joins via API, advances.
- **Step 3b: Complete** — "Welcome to the residence!" Redirect to residence detail.
### Onboarding trigger
After login/register, check if user has 0 residences → redirect to `/onboarding`.
This is done via middleware or the auth store's login flow.
For now, just mark `localStorage.setItem('onboarding_complete', 'true')` after completion, and check in the app layout.
### Verify build
```bash
cd /Users/treyt/Desktop/code/MyCrib/myCribAPI-Web && npm run build
```
### Commit
```bash
git add -A && git commit -m "feat: add onboarding wizard with create/join paths"
```
---
## Task 6: Enhanced Dashboard + Task Reports
**Depends on:** Task 1
**Files:**
- Modify: `src/app/app/page.tsx` (enhance existing dashboard)
- Create: `src/components/dashboard/task-completion-chart.tsx`
- Create: `src/components/dashboard/recent-activity.tsx`
- Create: `src/components/dashboard/stats-cards.tsx`
- Modify: `src/app/app/residences/[id]/page.tsx` (add "Download Report" button)
### Enhanced Dashboard
Replace the current simple dashboard with the spec's design:
**stats-cards.tsx** — Top row of stat cards:
- 4 cards: Overdue (red), Due Today (orange), Active (blue), Completed (green)
- Data from `useTasks()` kanban response `total_summary` field
- Each card: count, label, colored icon
**task-completion-chart.tsx** — Area chart using Recharts:
- Uses task completion data (from completions API or derived from kanban data)
- Shows last 30 days of completions as an area chart
- `<ResponsiveContainer>` + `<AreaChart>` + `<Area>` from recharts
- Styled with theme colors via CSS variables
- If no completion data available, show "No completion data yet" empty state
**recent-activity.tsx** — Recent activity feed:
- Uses `useNotifications(5)` to show last 5 notifications as activity items
- Each item: icon, title, time ago (relative)
- "View all" link to settings/notifications
- If no notifications: "No recent activity"
### Dashboard page structure
```tsx
// src/app/app/page.tsx (enhanced)
// 1. Welcome message with user name (from auth store)
// 2. StatsCards (4-column grid)
// 3. TaskCompletionChart (full width card)
// 4. RecentActivity (list)
```
### Task Report PDF
On residence detail page (`src/app/app/residences/[id]/page.tsx`), add a "Download Report" button:
- Calls `residencesApi.generateTasksReport(residenceId)`
- The API generates a PDF and emails it (existing behavior)
- Show success message "Report sent to your email"
- Alternative: if API returns a PDF URL, trigger browser download
### Verify build
```bash
cd /Users/treyt/Desktop/code/MyCrib/myCribAPI-Web && npm run build
```
### Commit
```bash
git add -A && git commit -m "feat: add enhanced dashboard with charts and task report PDF download"
```
---
## Task 7: Integration + Verification
**Depends on:** Tasks 2-6
### Step 1: Full build verification
```bash
cd /Users/treyt/Desktop/code/MyCrib/myCribAPI-Web && npm run build
```
### Step 2: Route verification
Confirm all new routes compile:
- `/app/settings/profile`
- `/app/settings/notifications`
- `/app/settings/subscription`
- `/app/residences/[id]/share`
- `/app/residences/join`
- `/onboarding`
### Step 3: Cross-feature integration checks
- TopBar notification bell renders and shows unread count
- Settings hub sidebar navigates between profile/notifications/subscription
- Residence detail has Share and Download Report buttons
- Contractor detail has Share button, list has Import button
- Dashboard shows stats, chart, and recent activity
### Step 4: Commit
```bash
git add -A && git commit -m "feat: complete Phase 3 — advanced features integration verified"
```
---
## Deliverables Checklist
At the end of Phase 3, verify:
- [ ] **Residence sharing**: generate share code, copy, join with code, manage users (list/remove)
- [ ] **Contractor sharing**: .casera file export from detail, import on list page
- [ ] **.casera files**: download as JSON, import via drag-and-drop
- [ ] **Subscription**: status display with usage bars, feature comparison table, upgrade CTA
- [ ] **Feature gating**: upgrade prompt dialog available for tier limit enforcement
- [ ] **Notification preferences**: toggle switches per notification type, saves immediately
- [ ] **In-app notifications**: bell icon in TopBar with unread count badge, dropdown with recent notifications
- [ ] **Profile**: edit name/email form, change password form, delete account with confirmation
- [ ] **Theme picker**: 11 theme swatches + dark/light toggle in profile settings
- [ ] **Onboarding**: multi-step wizard with create/join paths, stores completion state
- [ ] **Dashboard**: enhanced with stats cards, completion trend chart (Recharts), recent activity feed
- [ ] **Task reports**: "Download Report" button on residence detail, triggers PDF generation
- [ ] **Settings hub**: sidebar navigation between profile, notifications, subscription
- [ ] **Build passes**: `npm run build` exits 0