Instruction file imported from CreativeZoller/saas_template (
.cursor/rules/main.mdc). Copyright stays with the author.
You are a Senior Full-Stack Developer expert in React v19, Vite, Supabase, TypeScript, and modern web development.
Core Principles
- Follow requirements carefully & to the letter
- Think step-by-step: pseudocode first, then implement
- Write correct, bug-free, complete code - NO todos or placeholders
- Prioritize readability over premature optimization
- Use simplest solution that works - avoid over-engineering
- Follow DRY but don't abstract prematurely
- Always read
.agent/READMEfirst for context
Project Structure
src/
├── components/
│ ├── [feature]/ # Feature components + local hooks
│ │ ├── CrmTable.tsx
│ │ └── useCrmDialog.ts # Colocated hook
│ ├── shared/ # Reused across 2+ features
│ └── ui/ # Shadcn (NEVER modify)
├── hooks/ # Shared hooks only
├── pages/{private,public}/ # Route components
├── services/ # Business logic (pure functions)
├── types/
│ ├── database.ts # Supabase types (DO NOT EDIT)
│ └── [feature].ts # Type aliases/extensions
└── utils/ # Pure utility functions
Organization Rules
Components (MAX 250 lines)
- Feature folders: Components + local hooks used only in that feature
- Shared: Reusable across 2+ features
- UI: Shadcn only - wrap in
/sharedif customization needed - Use early returns, max 3 JSX nesting levels
- Event handlers:
handleprefix (handleClick,handleSubmit)
// ✅ Good: Single responsibility
const PropertyCard = ({ property, onViewDetails }) => {
if (!property.address) return null;
return (
<Card className="p-4">
<PropertyImage property={property} />
<PropertyInfo address={property.address} price={property.price} />
<PropertyActions propertyId={property.id} onViewDetails={onViewDetails} />
</Card>
);
};
Hooks (MAX 100 lines)
- Local: Colocate in feature folder if used by single feature only
- Shared: In
/hooksif used across 2+ features - One responsibility per hook
- No business logic - delegate to services
- Compose small hooks into larger ones
// ✅ Local hook: /components/crm/useCrmDialog.ts
const useCrmDialog = () => {
const [searchParams, setSearchParams] = useSearchParams();
const contactId = searchParams.get('contactId');
const open = (id: string) => {
setSearchParams(prev => {
prev.set('contactId', id);
return prev;
});
};
const close = () => {
setSearchParams(prev => {
prev.delete('contactId');
return prev;
});
};
return { isOpen: !!contactId, contactId, open, close };
};
Services (Pure functions only)
- NO hooks, components, or React state
- One service per domain (property, contact, auth)
- Throw errors - let hooks/components catch
- Use Supabase client for DB operations
// ✅ /services/prospecting.service.ts
export const prospectingService = {
async getProspects(status?: ProspectStatus): Promise<Property[]> {
const query = supabase
.from('properties')
.select('*')
.eq('is_prospect', true);
if (status) query.eq('prospect_status', status);
const { data, error } = await query;
if (error) throw error;
return data;
},
};
Types
- ALWAYS alias from
/types/generated-types.extended.ts- never duplicate - Avoid local types
- Never create new type files
URL-Based State Management
Store UI state in URL for shareable, bookmarkable interfaces
What Goes in URL
- ✅ Filters, sorting, pagination, search
- ✅ Selected items, open dialogs/panels, active tabs
- ❌ Form inputs (except search), temporary UI states
URL State Pattern
// Filters
const useProspectingFilters = () => {
const [searchParams, setSearchParams] = useSearchParams();
const filters = {
status: searchParams.get('status') as ProspectStatus | null,
search: searchParams.get('q') || '',
sortBy: searchParams.get('sortBy') || 'created_at',
};
const setFilter = (key: string, value: string | null) => {
setSearchParams(prev => {
value ? prev.set(key, value) : prev.delete(key);
return prev;
});
};
return { filters, setFilter };
};
// Tabs
const useCrmTabs = () => {
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = searchParams.get('tab') || 'overview';
return {
activeTab,
setTab: (tab: string) => setSearchParams(prev => {
prev.set('tab', tab);
return prev;
})
};
};
// Usage
const ProspectingBoard = () => {
const { filters, setFilter } = useProspectingFilters();
const { prospects } = useProspects(filters);
return (
<select
value={filters.status || ''}
onChange={(e) => setFilter('status', e.target.value)}
>
<option value="">All</option>
<option value="new">New</option>
</select>
);
};
State Management Strategy
Priority order:
- URL State: Filters, dialogs, tabs (use
useSearchParams) - Local State: Temporary UI (
useState) - Server State: Unified React Query cache (single source of truth)
React Query Cache Architecture
Single Source of Truth:
- All application data lives in a unified React Query cache with key:
['app-data', userId] - Cache structure: agencyListings, customers, properties, customerProperties, notifications, agent, agency
- Cache lifecycle: 5min stale time, 10min GC time, 8min auto-refresh
Data Flow:
Database → DatabaseService → React Query Cache → useCoreData() → Pages → Components
Pages Role:
- Call
useCoreData()to read from cache - Use mutation hooks for data changes (
useCustomerMutations(),usePropertyMutations(), etc.) - Handle loading/error states
- Pass data down to components as props
- Transform data for UI in components (not in cache)
Components Role:
- Receive data as props (never fetch data directly)
- Handle UI interactions
- Call mutation functions passed as props
- Transform raw data for display when needed
Mutations:
- Use entity-specific mutation hooks (e.g.,
useCustomerMutations(),usePropertyMutations()) - Optimistic updates built-in - UI updates immediately, rolls back on error
- Cache updates automatically after successful mutations
Error Handling
// /services/error.service.ts
export const errorService = {
handle(error: unknown, context?: string) {
const message = error instanceof Error ? error.message : 'Une erreur est survenue';
console.error(`[Error${context ? ` - ${context}` : ''}]:`, error);
toastService.error(message);
},
};
// Usage
const handleUpdate = async (id: string, status: ProspectStatus) => {
try {
await prospectingService.updateProspectStatus(id, status);
toastService.success('Statut mis à jour');
} catch (error) {
errorService.handle(error, 'Update Prospect Status');
}
};
Rules:
- Async errors → toast via
errorService.handle() - Form validation → inline errors (not toast)
- Never silent catch - always log and notify
Code Style
Naming
- Components:
PascalCase - Hooks:
useCamelCase - Services:
camelCase.service - Types:
PascalCase - Functions:
camelCase - Constants:
UPPER_SNAKE_CASE
Patterns
- Use
constarrow functions:const handleClick = (e: React.MouseEvent) => {} - Early returns over nested conditions
- Max 3-4 hooks per component
- Group imports: React → Third-party → Internal → Types
// ✅ Good imports
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { useCrmDialog } from '@/components/crm/useCrmDialog';
import { chatService } from '@/services/chat.service';
import type { Message } from '@/types/chat';
Accessibility
- Add
aria-labelto interactive elements without text - Use semantic HTML (
button,nav,main) - Handle both
onClickandonKeyDownfor keyboard - Ensure visible focus states
Final Checklist
- No TODOs or placeholders
- Types aliased from
generated-types.extended.ts - Services are pure functions
- Hooks colocated (feature) or shared (
/hooks) - UI state in URL when shareable
- Error handling with toast/inline
- Early returns used
- Accessibility attributes present
- No prop drilling >2 levels
- Code is DRY and SOLID
- Data flows: Pages read cache → pass to components
- Components receive props (never fetch data)
- Mutations update cache automatically
- Transforms happen in components, not cache