Files
honeyDueWeb/docs/04-demo-mode.md
Trey t e2172c20f2 Rebrand from Casera/MyCrib to honeyDue
Total rebrand across Web project:
- Package name: casera-web -> honeydue-web
- Cookie: casera-token -> honeydue-token
- Theme store: casera-theme -> honeydue-theme
- File sharing: .casera -> .honeydue, component/function renames
- casera-file-handler.tsx -> honeydue-file-handler.tsx
- All UI text, metadata, OG tags updated
- Domains: casera.treytartt.com -> honeyDue.treytartt.com
- Demo data emails updated
- All documentation updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:59 -06:00

12 KiB

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:

// 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)

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)

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

// 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

// 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 honeyDue 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 honeyDue — 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:

// 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:

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