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