Fix demo mode showing real user data after logout

React Query cache was shared between /app (real API) and /demo/app
(mock data) because query keys were identical. After login→logout→demo,
stale real data served from cache.

Two fixes:
1. Clear React Query cache on logout (auth store)
2. Namespace all query keys with basePath prefix so /app and /demo/app
   caches are completely isolated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-04 20:35:49 -06:00
parent 44993ae601
commit bd9b0ffb34
13 changed files with 263 additions and 70 deletions
+137
View File
@@ -0,0 +1,137 @@
# Casera Web (`myCribAPI-Web`)
Next.js web client for the Casera property management platform. Talks to the Go REST API backend.
## Build & Run
```bash
npm run dev # Dev server (localhost:3000)
npm run build # Production build (catches type errors)
npm run lint # ESLint
npm run test:e2e # Playwright end-to-end tests
npm run test:e2e:ui # Playwright with UI
npm run analyze # Bundle analysis
```
## Stack
- **Framework**: Next.js 16 (App Router, React 19)
- **Styling**: Tailwind CSS v4 + shadcn/ui (Radix primitives)
- **State**: Zustand (auth, onboarding, theme), TanStack React Query (server state)
- **Forms**: React Hook Form + Zod validation
- **Drag & Drop**: @dnd-kit (kanban boards)
- **Charts**: Recharts
- **Icons**: Lucide React
- **Analytics**: PostHog
- **Testing**: Playwright (E2E), Vitest (unit)
## Architecture
### Request Flow
```
Browser → Next.js page (client component)
→ apiFetch("/tasks/") → /api/proxy/tasks (Next.js route handler)
→ Go API (reads casera-token httpOnly cookie, forwards as Authorization header)
```
Auth tokens are stored as httpOnly cookies (`casera-token`), never exposed to JS. The Next.js `/api/proxy/[...path]` catch-all route forwards requests to the Go API.
### Directory Structure
```
src/
├── app/
│ ├── (auth)/ # Login, register, forgot-password (public)
│ ├── api/proxy/ # Catch-all proxy to Go API
│ ├── app/ # Authenticated app pages
│ │ ├── contractors/ # Contractor CRUD
│ │ ├── documents/ # Document management
│ │ ├── residences/ # Residence CRUD + detail with kanban
│ │ ├── settings/ # User settings, theme
│ │ └── tasks/ # Task CRUD, kanban board
│ ├── demo/ # Demo mode (fake data, no auth)
│ └── page.tsx # Landing page
├── components/
│ ├── contractors/ # Contractor cards, forms
│ ├── dashboard/ # Stats, activity, template suggestions
│ ├── demo/ # Demo banner
│ ├── documents/ # Document cards, forms
│ ├── forms/ # Auth form wrapper
│ ├── layout/ # TopBar, MobileNav, Sidebar
│ ├── notifications/ # Notification components
│ ├── onboarding/ # Onboarding flow
│ ├── residences/ # Residence cards, forms, summary, share
│ ├── settings/ # Theme picker
│ ├── shared/ # PageHeader, EmptyState, ErrorBanner, etc.
│ ├── sharing/ # Share code UI
│ ├── tasks/ # TaskCard, TaskForm, KanbanBoard, KanbanColumn
│ └── ui/ # shadcn primitives (button, card, dialog, etc.)
├── lib/
│ ├── api/ # API client functions per domain
│ │ ├── client.ts # apiFetch (client-side) + serverFetch (server-side)
│ │ ├── tasks.ts # Task API (CRUD, kanban, completions)
│ │ ├── residences.ts # Residence API (CRUD, sharing, reports)
│ │ ├── contractors.ts
│ │ ├── documents.ts
│ │ └── ...
│ ├── hooks/ # React Query hooks per domain
│ │ ├── use-tasks.ts # useTasks, useTask, useCreateTask, etc.
│ │ ├── use-residences.ts
│ │ └── ...
│ ├── demo/ # Demo mode: DataProvider abstraction
│ │ ├── data-provider-context.tsx # React context
│ │ ├── real-provider.ts # Real API calls
│ │ ├── demo-provider.ts # Fake data
│ │ └── mock-data/ # Static demo data
│ ├── themes/ # Theme system (Warm Sage palette)
│ ├── validations/ # Zod schemas
│ └── utils.ts # cn() helper
├── stores/
│ ├── auth.ts # Zustand: user, token, login/logout
│ ├── onboarding.ts # Zustand: onboarding state
│ └── theme.ts # Zustand: theme preferences
└── styles/
└── themes.css # CSS custom properties for theme
```
### Key Patterns
**Data fetching**: All server data uses React Query hooks (`use-*.ts`). Hooks call API functions from `lib/api/`. API functions call `apiFetch()` which proxies through Next.js.
**Demo mode**: The app supports a `/demo` route with fake data. Components use `useDataProvider()` to get API functions and `basePath` — this returns either real or demo implementations depending on the route.
**Kanban boards**: Tasks display in kanban columns (overdue, due_soon, in_progress, not_started, completed). Uses `@dnd-kit` for drag-and-drop. Column names match Go API: `overdue_tasks`, `due_soon_tasks`, `in_progress_tasks`, `not_started_tasks`, `completed_tasks`.
**Middleware** (`src/middleware.ts`): Checks `casera-token` cookie. Redirects unauthenticated users to `/login` for protected routes. Skips API routes, static files, and public paths.
## Conventions
- All pages under `/app/` are `"use client"` components
- Use `apiFetch()` for client-side API calls, `serverFetch()` for server components/route handlers
- React Query hooks go in `src/lib/hooks/use-{domain}.ts`
- API client functions go in `src/lib/api/{domain}.ts`
- shadcn components in `src/components/ui/` — don't modify these directly
- Custom components use Tailwind classes, warm shadow CSS variables (`--shadow-warm-sm`, `--shadow-warm-md`)
- Brand colors: `primary` (sage green), `brand-clay`, `brand-sage` variants defined in CSS custom properties
- Font: `font-heading` for headings (tracking-tight), system font for body
- Toast notifications via `sonner` — use `toast.success()`, `toast.error()`
- Form validation with Zod schemas in `src/lib/validations/`
- Icons from `lucide-react` — keep consistent sizes (size-4 for inline, size-5 for cards)
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `NEXT_PUBLIC_API_URL` | Go API URL (client-side) | `https://casera.treytartt.com/api` |
| `API_URL` | Go API URL (server-side, no proxy) | Falls back to `NEXT_PUBLIC_API_URL` |
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog analytics key | — |
| `NEXT_PUBLIC_POSTHOG_HOST` | PostHog host | — |
## Common Tasks
**Add a new page**: Create `src/app/app/{route}/page.tsx` (client component). Add nav item to `src/components/layout/nav-items.ts`.
**Add a new API domain**: Create `src/lib/api/{domain}.ts` with typed functions using `apiFetch()`. Create `src/lib/hooks/use-{domain}.ts` with React Query hooks. Add demo provider methods if needed.
**Add a shadcn component**: `npx shadcn@latest add {component}` — installs to `src/components/ui/`.
+5 -4
View File
@@ -12,7 +12,7 @@ import {
type DragEndEvent, type DragEndEvent,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
import { useMarkInProgress } from "@/lib/hooks/use-tasks"; import { useMarkInProgress } from "@/lib/hooks/use-tasks";
import { useDataProvider } from "@/lib/demo/data-provider-context"; import { useDataProvider, useQueryKeyPrefix } from "@/lib/demo/data-provider-context";
import { KanbanColumn } from "./kanban-column"; import { KanbanColumn } from "./kanban-column";
import type { KanbanResponse, KanbanColumn as KanbanColumnType } from "@/lib/api/tasks"; import type { KanbanResponse, KanbanColumn as KanbanColumnType } from "@/lib/api/tasks";
@@ -24,6 +24,7 @@ export function KanbanBoard({ data }: KanbanBoardProps) {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { basePath } = useDataProvider(); const { basePath } = useDataProvider();
const qk = useQueryKeyPrefix();
const markInProgress = useMarkInProgress(); const markInProgress = useMarkInProgress();
// Local columns state for instant optimistic updates // Local columns state for instant optimistic updates
@@ -88,7 +89,7 @@ export function KanbanBoard({ data }: KanbanBoardProps) {
markInProgress.mutate(taskId, { markInProgress.mutate(taskId, {
onError: () => setColumns(data.columns), onError: () => setColumns(data.columns),
onSettled: () => { onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["tasks"] }); queryClient.invalidateQueries({ queryKey: qk("tasks") });
}, },
}); });
return; return;
@@ -98,9 +99,9 @@ export function KanbanBoard({ data }: KanbanBoardProps) {
// (no API endpoint for moving to overdue/due_soon/upcoming — those are computed server-side) // (no API endpoint for moving to overdue/due_soon/upcoming — those are computed server-side)
moveTask(taskId, sourceCol.name, targetColumn); moveTask(taskId, sourceCol.name, targetColumn);
// Refetch to get correct server state // Refetch to get correct server state
queryClient.invalidateQueries({ queryKey: ["tasks"] }); queryClient.invalidateQueries({ queryKey: qk("tasks") });
}, },
[columns, data.columns, moveTask, markInProgress, queryClient, router, basePath] [columns, data.columns, moveTask, markInProgress, queryClient, qk, router, basePath]
); );
return ( return (
+7 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { createContext, useContext } from 'react'; import { createContext, useContext, useMemo } from 'react';
import type { DataProvider } from './data-provider'; import type { DataProvider } from './data-provider';
const DataProviderContext = createContext<DataProvider | null>(null); const DataProviderContext = createContext<DataProvider | null>(null);
@@ -13,6 +13,12 @@ export function useDataProvider(): DataProvider {
return ctx; return ctx;
} }
/** Returns a function that prefixes query keys with the current provider's basePath. */
export function useQueryKeyPrefix(): (...key: unknown[]) => unknown[] {
const { basePath } = useDataProvider();
return useMemo(() => (...key: unknown[]) => [basePath, ...key], [basePath]);
}
export function DataProviderProvider({ export function DataProviderProvider({
value, value,
children, children,
+3 -2
View File
@@ -1,13 +1,14 @@
"use client"; "use client";
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
export function useCurrentUser() { export function useCurrentUser() {
const { auth } = useDataProvider(); const { auth } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['auth', 'user'], queryKey: qk('auth', 'user'),
queryFn: () => auth.getCurrentUser(), queryFn: () => auth.getCurrentUser(),
retry: false, retry: false,
staleTime: 5 * 60 * 1000, // 5 minutes staleTime: 5 * 60 * 1000, // 5 minutes
+16 -9
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
import type { CreateContractorRequest, UpdateContractorRequest } from '@/lib/api/contractors'; import type { CreateContractorRequest, UpdateContractorRequest } from '@/lib/api/contractors';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -10,8 +10,9 @@ import type { CreateContractorRequest, UpdateContractorRequest } from '@/lib/api
export function useContractors() { export function useContractors() {
const { contractors } = useDataProvider(); const { contractors } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['contractors'], queryKey: qk('contractors'),
queryFn: () => contractors.list(), queryFn: () => contractors.list(),
select: (data) => (Array.isArray(data) ? data : []), select: (data) => (Array.isArray(data) ? data : []),
}); });
@@ -19,8 +20,9 @@ export function useContractors() {
export function useContractor(id: number) { export function useContractor(id: number) {
const { contractors } = useDataProvider(); const { contractors } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['contractors', id], queryKey: qk('contractors', id),
queryFn: () => contractors.get(id), queryFn: () => contractors.get(id),
enabled: !!id, enabled: !!id,
}); });
@@ -28,8 +30,9 @@ export function useContractor(id: number) {
export function useContractorTasks(id: number) { export function useContractorTasks(id: number) {
const { contractors } = useDataProvider(); const { contractors } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['contractors', id, 'tasks'], queryKey: qk('contractors', id, 'tasks'),
queryFn: () => contractors.getTasks(id), queryFn: () => contractors.getTasks(id),
enabled: !!id, enabled: !!id,
select: (data) => (Array.isArray(data) ? data : []), select: (data) => (Array.isArray(data) ? data : []),
@@ -43,11 +46,12 @@ export function useContractorTasks(id: number) {
export function useCreateContractor() { export function useCreateContractor() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { contractors } = useDataProvider(); const { contractors } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (data: CreateContractorRequest) => mutationFn: (data: CreateContractorRequest) =>
contractors.create(data), contractors.create(data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contractors'] }); queryClient.invalidateQueries({ queryKey: qk('contractors') });
}, },
}); });
} }
@@ -55,12 +59,13 @@ export function useCreateContractor() {
export function useUpdateContractor(id: number) { export function useUpdateContractor(id: number) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { contractors } = useDataProvider(); const { contractors } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (data: UpdateContractorRequest) => mutationFn: (data: UpdateContractorRequest) =>
contractors.update(id, data), contractors.update(id, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contractors'] }); queryClient.invalidateQueries({ queryKey: qk('contractors') });
queryClient.invalidateQueries({ queryKey: ['contractors', id] }); queryClient.invalidateQueries({ queryKey: qk('contractors', id) });
}, },
}); });
} }
@@ -68,10 +73,11 @@ export function useUpdateContractor(id: number) {
export function useDeleteContractor() { export function useDeleteContractor() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { contractors } = useDataProvider(); const { contractors } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (id: number) => contractors.delete(id), mutationFn: (id: number) => contractors.delete(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contractors'] }); queryClient.invalidateQueries({ queryKey: qk('contractors') });
}, },
}); });
} }
@@ -79,10 +85,11 @@ export function useDeleteContractor() {
export function useToggleFavorite() { export function useToggleFavorite() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { contractors } = useDataProvider(); const { contractors } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (id: number) => contractors.toggleFavorite(id), mutationFn: (id: number) => contractors.toggleFavorite(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contractors'] }); queryClient.invalidateQueries({ queryKey: qk('contractors') });
}, },
}); });
} }
+14 -8
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
import type { DocumentListParams, CreateDocumentRequest, UpdateDocumentRequest } from '@/lib/api/documents'; import type { DocumentListParams, CreateDocumentRequest, UpdateDocumentRequest } from '@/lib/api/documents';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -10,8 +10,9 @@ import type { DocumentListParams, CreateDocumentRequest, UpdateDocumentRequest }
export function useDocuments(params?: DocumentListParams) { export function useDocuments(params?: DocumentListParams) {
const { documents } = useDataProvider(); const { documents } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['documents', params], queryKey: qk('documents', params),
queryFn: () => documents.list(params), queryFn: () => documents.list(params),
select: (data) => (Array.isArray(data) ? data : []), select: (data) => (Array.isArray(data) ? data : []),
}); });
@@ -19,8 +20,9 @@ export function useDocuments(params?: DocumentListParams) {
export function useWarranties() { export function useWarranties() {
const { documents } = useDataProvider(); const { documents } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['documents', 'warranties'], queryKey: qk('documents', 'warranties'),
queryFn: () => documents.listWarranties(), queryFn: () => documents.listWarranties(),
select: (data) => (Array.isArray(data) ? data : []), select: (data) => (Array.isArray(data) ? data : []),
}); });
@@ -28,7 +30,8 @@ export function useWarranties() {
export function useDocument(id: number) { export function useDocument(id: number) {
const { documents } = useDataProvider(); const { documents } = useDataProvider();
return useQuery({ queryKey: ['documents', id], queryFn: () => documents.get(id), enabled: !!id }); const qk = useQueryKeyPrefix();
return useQuery({ queryKey: qk('documents', id), queryFn: () => documents.get(id), enabled: !!id });
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -38,6 +41,7 @@ export function useDocument(id: number) {
export function useCreateDocument() { export function useCreateDocument() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { documents } = useDataProvider(); const { documents } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: ({ data, file }: { data: CreateDocumentRequest; file?: File }) => { mutationFn: ({ data, file }: { data: CreateDocumentRequest; file?: File }) => {
if (file) { if (file) {
@@ -46,7 +50,7 @@ export function useCreateDocument() {
return documents.create(data); return documents.create(data);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['documents'] }); queryClient.invalidateQueries({ queryKey: qk('documents') });
}, },
}); });
} }
@@ -54,12 +58,13 @@ export function useCreateDocument() {
export function useUpdateDocument(id: number) { export function useUpdateDocument(id: number) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { documents } = useDataProvider(); const { documents } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (data: UpdateDocumentRequest) => mutationFn: (data: UpdateDocumentRequest) =>
documents.update(id, data), documents.update(id, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['documents'] }); queryClient.invalidateQueries({ queryKey: qk('documents') });
queryClient.invalidateQueries({ queryKey: ['documents', id] }); queryClient.invalidateQueries({ queryKey: qk('documents', id) });
}, },
}); });
} }
@@ -67,10 +72,11 @@ export function useUpdateDocument(id: number) {
export function useDeleteDocument() { export function useDeleteDocument() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { documents } = useDataProvider(); const { documents } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (id: number) => documents.delete(id), mutationFn: (id: number) => documents.delete(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['documents'] }); queryClient.invalidateQueries({ queryKey: qk('documents') });
}, },
}); });
} }
+3 -2
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useDataProvider } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Main hook — fetches all static data in a single request // Main hook — fetches all static data in a single request
@@ -9,8 +9,9 @@ import { useDataProvider } from '@/lib/demo/data-provider-context';
export function useLookups() { export function useLookups() {
const { lookups } = useDataProvider(); const { lookups } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['lookups', 'static-data'], queryKey: qk('lookups', 'static-data'),
queryFn: () => lookups.getStaticData(), queryFn: () => lookups.getStaticData(),
staleTime: Infinity, // ETag-based; never auto-refetch staleTime: Infinity, // ETag-based; never auto-refetch
}); });
+15 -9
View File
@@ -1,21 +1,23 @@
"use client"; "use client";
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
import type { UpdatePreferencesRequest } from '@/lib/api/notifications'; import type { UpdatePreferencesRequest } from '@/lib/api/notifications';
export function useNotifications(limit?: number) { export function useNotifications(limit?: number) {
const { notifications } = useDataProvider(); const { notifications } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['notifications', limit], queryKey: qk('notifications', limit),
queryFn: () => notifications.list(limit), queryFn: () => notifications.list(limit),
}); });
} }
export function useUnreadCount() { export function useUnreadCount() {
const { notifications } = useDataProvider(); const { notifications } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['notifications', 'unread-count'], queryKey: qk('notifications', 'unread-count'),
queryFn: () => notifications.getUnreadCount(), queryFn: () => notifications.getUnreadCount(),
refetchInterval: 60_000, refetchInterval: 60_000,
}); });
@@ -23,8 +25,9 @@ export function useUnreadCount() {
export function useNotificationPreferences() { export function useNotificationPreferences() {
const { notifications } = useDataProvider(); const { notifications } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['notifications', 'preferences'], queryKey: qk('notifications', 'preferences'),
queryFn: () => notifications.getPreferences(), queryFn: () => notifications.getPreferences(),
}); });
} }
@@ -32,10 +35,11 @@ export function useNotificationPreferences() {
export function useUpdatePreferences() { export function useUpdatePreferences() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { notifications } = useDataProvider(); const { notifications } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (data: UpdatePreferencesRequest) => notifications.updatePreferences(data), mutationFn: (data: UpdatePreferencesRequest) => notifications.updatePreferences(data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notifications', 'preferences'] }); queryClient.invalidateQueries({ queryKey: qk('notifications', 'preferences') });
}, },
}); });
} }
@@ -43,11 +47,12 @@ export function useUpdatePreferences() {
export function useMarkAsRead() { export function useMarkAsRead() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { notifications } = useDataProvider(); const { notifications } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (id: number) => notifications.markAsRead(id), mutationFn: (id: number) => notifications.markAsRead(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notifications'] }); queryClient.invalidateQueries({ queryKey: qk('notifications') });
queryClient.invalidateQueries({ queryKey: ['notifications', 'unread-count'] }); queryClient.invalidateQueries({ queryKey: qk('notifications', 'unread-count') });
}, },
}); });
} }
@@ -55,11 +60,12 @@ export function useMarkAsRead() {
export function useMarkAllAsRead() { export function useMarkAllAsRead() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { notifications } = useDataProvider(); const { notifications } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: () => notifications.markAllAsRead(), mutationFn: () => notifications.markAllAsRead(),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notifications'] }); queryClient.invalidateQueries({ queryKey: qk('notifications') });
queryClient.invalidateQueries({ queryKey: ['notifications', 'unread-count'] }); queryClient.invalidateQueries({ queryKey: qk('notifications', 'unread-count') });
}, },
}); });
} }
+12 -7
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
import type { CreateResidenceRequest, UpdateResidenceRequest } from '@/lib/api/residences'; import type { CreateResidenceRequest, UpdateResidenceRequest } from '@/lib/api/residences';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -10,8 +10,9 @@ import type { CreateResidenceRequest, UpdateResidenceRequest } from '@/lib/api/r
export function useResidences() { export function useResidences() {
const { residences } = useDataProvider(); const { residences } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['residences'], queryKey: qk('residences'),
queryFn: () => residences.getMyResidences(), queryFn: () => residences.getMyResidences(),
select: (data) => (Array.isArray(data) ? data : []), select: (data) => (Array.isArray(data) ? data : []),
}); });
@@ -19,8 +20,9 @@ export function useResidences() {
export function useResidence(id: number) { export function useResidence(id: number) {
const { residences } = useDataProvider(); const { residences } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['residences', id], queryKey: qk('residences', id),
queryFn: () => residences.get(id), queryFn: () => residences.get(id),
enabled: !!id, enabled: !!id,
}); });
@@ -33,11 +35,12 @@ export function useResidence(id: number) {
export function useCreateResidence() { export function useCreateResidence() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { residences } = useDataProvider(); const { residences } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (data: CreateResidenceRequest) => mutationFn: (data: CreateResidenceRequest) =>
residences.create(data), residences.create(data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['residences'] }); queryClient.invalidateQueries({ queryKey: qk('residences') });
}, },
}); });
} }
@@ -45,12 +48,13 @@ export function useCreateResidence() {
export function useUpdateResidence(id: number) { export function useUpdateResidence(id: number) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { residences } = useDataProvider(); const { residences } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (data: UpdateResidenceRequest) => mutationFn: (data: UpdateResidenceRequest) =>
residences.update(id, data), residences.update(id, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['residences'] }); queryClient.invalidateQueries({ queryKey: qk('residences') });
queryClient.invalidateQueries({ queryKey: ['residences', id] }); queryClient.invalidateQueries({ queryKey: qk('residences', id) });
}, },
}); });
} }
@@ -58,10 +62,11 @@ export function useUpdateResidence(id: number) {
export function useDeleteResidence() { export function useDeleteResidence() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { residences } = useDataProvider(); const { residences } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (id: number) => residences.delete(id), mutationFn: (id: number) => residences.delete(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['residences'] }); queryClient.invalidateQueries({ queryKey: qk('residences') });
}, },
}); });
} }
+11 -6
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Query hooks // Query hooks
@@ -9,8 +9,9 @@ import { useDataProvider } from '@/lib/demo/data-provider-context';
export function useShareCode(residenceId: number) { export function useShareCode(residenceId: number) {
const { sharing } = useDataProvider(); const { sharing } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['residences', residenceId, 'share-code'], queryKey: qk('residences', residenceId, 'share-code'),
queryFn: () => sharing.getShareCode(residenceId), queryFn: () => sharing.getShareCode(residenceId),
enabled: !!residenceId, enabled: !!residenceId,
}); });
@@ -18,8 +19,9 @@ export function useShareCode(residenceId: number) {
export function useResidenceUsers(residenceId: number) { export function useResidenceUsers(residenceId: number) {
const { sharing } = useDataProvider(); const { sharing } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['residences', residenceId, 'users'], queryKey: qk('residences', residenceId, 'users'),
queryFn: () => sharing.getResidenceUsers(residenceId), queryFn: () => sharing.getResidenceUsers(residenceId),
enabled: !!residenceId, enabled: !!residenceId,
}); });
@@ -32,10 +34,11 @@ export function useResidenceUsers(residenceId: number) {
export function useGenerateShareCode(residenceId: number) { export function useGenerateShareCode(residenceId: number) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { sharing } = useDataProvider(); const { sharing } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: () => sharing.generateShareCode(residenceId), mutationFn: () => sharing.generateShareCode(residenceId),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['residences', residenceId, 'share-code'] }); queryClient.invalidateQueries({ queryKey: qk('residences', residenceId, 'share-code') });
}, },
}); });
} }
@@ -43,10 +46,11 @@ export function useGenerateShareCode(residenceId: number) {
export function useRemoveResidenceUser(residenceId: number) { export function useRemoveResidenceUser(residenceId: number) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { sharing } = useDataProvider(); const { sharing } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (userId: number) => sharing.removeUser(residenceId, userId), mutationFn: (userId: number) => sharing.removeUser(residenceId, userId),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['residences', residenceId, 'users'] }); queryClient.invalidateQueries({ queryKey: qk('residences', residenceId, 'users') });
}, },
}); });
} }
@@ -54,10 +58,11 @@ export function useRemoveResidenceUser(residenceId: number) {
export function useJoinResidence() { export function useJoinResidence() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { sharing } = useDataProvider(); const { sharing } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (code: string) => sharing.joinWithCode({ code }), mutationFn: (code: string) => sharing.joinWithCode({ code }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['residences'] }); queryClient.invalidateQueries({ queryKey: qk('residences') });
}, },
}); });
} }
+7 -4
View File
@@ -1,20 +1,22 @@
"use client"; "use client";
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useDataProvider } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
export function useSubscriptionStatus() { export function useSubscriptionStatus() {
const { subscription } = useDataProvider(); const { subscription } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['subscription', 'status'], queryKey: qk('subscription', 'status'),
queryFn: () => subscription.getStatus(), queryFn: () => subscription.getStatus(),
}); });
} }
export function useFeatureBenefits() { export function useFeatureBenefits() {
const { subscription } = useDataProvider(); const { subscription } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['subscription', 'features'], queryKey: qk('subscription', 'features'),
queryFn: () => subscription.getFeatureBenefits(), queryFn: () => subscription.getFeatureBenefits(),
staleTime: Infinity, staleTime: Infinity,
}); });
@@ -22,8 +24,9 @@ export function useFeatureBenefits() {
export function useUpgradeTriggers() { export function useUpgradeTriggers() {
const { subscription } = useDataProvider(); const { subscription } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['subscription', 'upgrade-triggers'], queryKey: qk('subscription', 'upgrade-triggers'),
queryFn: () => subscription.getUpgradeTriggers(), queryFn: () => subscription.getUpgradeTriggers(),
staleTime: Infinity, staleTime: Infinity,
}); });
+30 -18
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useDataProvider } from '@/lib/demo/data-provider-context'; import { useDataProvider, useQueryKeyPrefix } from '@/lib/demo/data-provider-context';
import type { CreateTaskRequest, UpdateTaskRequest, CreateCompletionRequest } from '@/lib/api/tasks'; import type { CreateTaskRequest, UpdateTaskRequest, CreateCompletionRequest } from '@/lib/api/tasks';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -10,16 +10,18 @@ import type { CreateTaskRequest, UpdateTaskRequest, CreateCompletionRequest } fr
export function useTasks() { export function useTasks() {
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['tasks'], queryKey: qk('tasks'),
queryFn: () => tasks.list(), queryFn: () => tasks.list(),
}); });
} }
export function useTasksByResidence(residenceId: number) { export function useTasksByResidence(residenceId: number) {
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['tasks', 'by-residence', residenceId], queryKey: qk('tasks', 'by-residence', residenceId),
queryFn: () => tasks.getByResidence(residenceId), queryFn: () => tasks.getByResidence(residenceId),
enabled: !!residenceId, enabled: !!residenceId,
}); });
@@ -27,8 +29,9 @@ export function useTasksByResidence(residenceId: number) {
export function useTask(id: number) { export function useTask(id: number) {
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['tasks', id], queryKey: qk('tasks', id),
queryFn: () => tasks.get(id), queryFn: () => tasks.get(id),
enabled: !!id, enabled: !!id,
}); });
@@ -36,8 +39,9 @@ export function useTask(id: number) {
export function useKanbanBoard(residenceId: number) { export function useKanbanBoard(residenceId: number) {
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['tasks', 'kanban', residenceId], queryKey: qk('tasks', 'kanban', residenceId),
queryFn: () => tasks.getByResidence(residenceId), queryFn: () => tasks.getByResidence(residenceId),
enabled: !!residenceId, enabled: !!residenceId,
}); });
@@ -45,8 +49,9 @@ export function useKanbanBoard(residenceId: number) {
export function useTaskCompletions(taskId: number) { export function useTaskCompletions(taskId: number) {
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useQuery({ return useQuery({
queryKey: ['tasks', taskId, 'completions'], queryKey: qk('tasks', taskId, 'completions'),
queryFn: () => tasks.getCompletions(taskId), queryFn: () => tasks.getCompletions(taskId),
enabled: !!taskId, enabled: !!taskId,
}); });
@@ -59,11 +64,12 @@ export function useTaskCompletions(taskId: number) {
export function useCreateTask() { export function useCreateTask() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (data: CreateTaskRequest) => tasks.create(data), mutationFn: (data: CreateTaskRequest) => tasks.create(data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] }); queryClient.invalidateQueries({ queryKey: qk('tasks') });
queryClient.invalidateQueries({ queryKey: ['residences'] }); queryClient.invalidateQueries({ queryKey: qk('residences') });
}, },
}); });
} }
@@ -71,12 +77,13 @@ export function useCreateTask() {
export function useUpdateTask(id: number) { export function useUpdateTask(id: number) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (data: UpdateTaskRequest) => tasks.update(id, data), mutationFn: (data: UpdateTaskRequest) => tasks.update(id, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] }); queryClient.invalidateQueries({ queryKey: qk('tasks') });
queryClient.invalidateQueries({ queryKey: ['tasks', id] }); queryClient.invalidateQueries({ queryKey: qk('tasks', id) });
queryClient.invalidateQueries({ queryKey: ['residences'] }); queryClient.invalidateQueries({ queryKey: qk('residences') });
}, },
}); });
} }
@@ -84,11 +91,12 @@ export function useUpdateTask(id: number) {
export function useDeleteTask() { export function useDeleteTask() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (id: number) => tasks.delete(id), mutationFn: (id: number) => tasks.delete(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] }); queryClient.invalidateQueries({ queryKey: qk('tasks') });
queryClient.invalidateQueries({ queryKey: ['residences'] }); queryClient.invalidateQueries({ queryKey: qk('residences') });
}, },
}); });
} }
@@ -96,10 +104,11 @@ export function useDeleteTask() {
export function useMarkInProgress() { export function useMarkInProgress() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (id: number) => tasks.markInProgress(id), mutationFn: (id: number) => tasks.markInProgress(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] }); queryClient.invalidateQueries({ queryKey: qk('tasks') });
}, },
}); });
} }
@@ -107,10 +116,11 @@ export function useMarkInProgress() {
export function useCancelTask() { export function useCancelTask() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (id: number) => tasks.cancel(id), mutationFn: (id: number) => tasks.cancel(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] }); queryClient.invalidateQueries({ queryKey: qk('tasks') });
}, },
}); });
} }
@@ -118,10 +128,11 @@ export function useCancelTask() {
export function useArchiveTask() { export function useArchiveTask() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: (id: number) => tasks.archive(id), mutationFn: (id: number) => tasks.archive(id),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] }); queryClient.invalidateQueries({ queryKey: qk('tasks') });
}, },
}); });
} }
@@ -129,6 +140,7 @@ export function useArchiveTask() {
export function useCreateCompletion() { export function useCreateCompletion() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { tasks } = useDataProvider(); const { tasks } = useDataProvider();
const qk = useQueryKeyPrefix();
return useMutation({ return useMutation({
mutationFn: ({ mutationFn: ({
data, data,
@@ -151,8 +163,8 @@ export function useCreateCompletion() {
return tasks.createCompletion(data); return tasks.createCompletion(data);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks'] }); queryClient.invalidateQueries({ queryKey: qk('tasks') });
queryClient.invalidateQueries({ queryKey: ['residences'] }); queryClient.invalidateQueries({ queryKey: qk('residences') });
}, },
}); });
} }
+3
View File
@@ -1,5 +1,6 @@
import { create } from 'zustand'; import { create } from 'zustand';
import * as authApi from '@/lib/api/auth'; import * as authApi from '@/lib/api/auth';
import { getQueryClient } from '@/lib/query/query-client';
import type { UserResponse } from '@/lib/api/auth'; import type { UserResponse } from '@/lib/api/auth';
interface AuthState { interface AuthState {
@@ -62,6 +63,8 @@ export const useAuthStore = create<AuthState>()((set) => ({
} catch { } catch {
// Even if logout fails server-side, clear local state // Even if logout fails server-side, clear local state
} finally { } finally {
// Clear React Query cache to prevent stale data leaking into demo mode
getQueryClient().clear();
set({ set({
user: null, user: null,
isAuthenticated: false, isAuthenticated: false,