5a50d77515
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>
361 lines
12 KiB
Markdown
361 lines
12 KiB
Markdown
# 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
|