The admin SPA (apps/admin/) has been retired. Its functionality now lives in apps/app/ under /platform/* routes for super_admin users. Updated all documentation to reflect: 2 SPAs instead of 3, removed FRONTEND_ADMIN_URL/port 5173 references, changed production URL from app.crewli.app to crewli.app. Retired admin-specific security audit findings (A13-2, A13-4, A13-5, A13-7) and APPS-01 backlog item. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.4 KiB
Crewli — Cursor Master Prompt
Plak dit BOVEN elke task. Vervang [TASK] onderaan.
MANDATORY PREAMBLE
Read @CLAUDE.md and @dev-docs/SCHEMA.md before starting. Use @workspace
for full codebase context. These documents are your source of truth.
If modifying existing code, read the current implementation first. Understand the patterns already in use before writing anything new.
PROJECT CONTEXT
Crewli — multi-tenant SaaS for event/festival management.
- Frontend: Vue 3 + TypeScript + Vuexy 9.5 (Vuetify 3.10) + Pinia + TanStack Vue Query + VeeValidate + Zod
- Two SPAs:
apps/app/— Organizer + Platform Admin main app (port 5174)apps/portal/— Dual-mode portal (port 5175)
- API base:
VITE_API_URLfrom.env.local(defaulthttp://localhost:8000) - Axios instance:
src/lib/axios.ts— this is the ONLY axios instance. Never create another. Never import axios directly in components. - Backend Enums: PHP Enums in
api/app/Enums/define all valid status/type values. TypeScript equivalents must mirror these exactly insrc/types/.
Frontend file structure (per app)
src/
├── lib/axios.ts # Singleton axios instance (DO NOT DUPLICATE)
├── types/ # TypeScript interfaces per module
│ └── [module].ts
├── composables/api/ # TanStack Query composables per module
│ └── use[Module].ts
├── stores/ # Pinia stores (cross-component state only)
│ └── use[Module]Store.ts
├── pages/ # Page components
│ └── [module]/
│ ├── index.vue # List view
│ └── [id].vue # Detail view (or side panel)
└── router/ # Vue Router config
ZERO-COMPROMISE RULES — VIOLATIONS ARE BLOCKERS
TypeScript & typing
-
NO
anyTYPES. EVER. Every variable, prop, emit, return type, ref, reactive, computed, and API response is fully typed. If you don't know the type, check the backend API Resource inapi/app/Http/Resources/— that defines the contract. Create matching interfaces insrc/types/[module].ts. -
TYPES FIRST. Before writing any composable or component for a new module, create the TypeScript interfaces in
src/types/[module].ts. These mirror the backend API Resource shape. Include:- Entity interface (e.g.,
Shift,Person,Event) - Create/Update DTOs (matching backend Form Request fields)
- Enum-like union types or const objects matching backend PHP Enums
- Paginated response wrapper if applicable
- Entity interface (e.g.,
-
ENUMS AS CONST OBJECTS. Mirror backend PHP Enums as TypeScript const objects with
as const, not as loose string unions scattered across files:// src/types/shift.ts export const ShiftAssignmentStatus = { PENDING_APPROVAL: 'pending_approval', APPROVED: 'approved', REJECTED: 'rejected', CANCELLED: 'cancelled', COMPLETED: 'completed', } as const export type ShiftAssignmentStatus = typeof ShiftAssignmentStatus[keyof typeof ShiftAssignmentStatus]
Architecture & patterns
-
NO DIRECT AXIOS IN COMPONENTS. All API calls go through composables in
composables/api/use[Module].tsusing TanStack Query (useQuery/useMutation). Components never import axios orsrc/lib/axios.ts. The composable is the only layer that knows about HTTP. -
NO OPTIONS API. Always
<script setup lang="ts">. Props viadefineProps<{...}>(), emits viadefineEmits<{...}>(), expose viadefineExpose(). Noexport default { ... }. -
NO PROP DRILLING. If state needs to cross more than one component boundary, use a Pinia store (
src/stores/use[Module]Store.ts). Access reactive state viastoreToRefs(). Mutations only through store actions. -
DELETE OVER ADAPT. If you find duplicate composables, overlapping stores, or conflicting patterns: delete the worse version. Do not build alongside existing code that does the same thing. One implementation per concern.
-
CONSISTENCY OVER CLEVERNESS. Look at how existing modules handle the same pattern (table views, form dialogs, detail panels, error handling). Use the exact same approach. Never introduce a "better" alternative without refactoring ALL existing modules to match.
-
SINGLE SOURCE OF TRUTH. Don't duplicate:
- Validation logic → Zod schema mirrors backend Form Request. One schema per form, referenced by VeeValidate.
- Enum values → const objects in
src/types/, matching backend Enums. Never use raw string literals like'approved'in templates or logic. - API URLs → constructed in composable from module prefix, never hardcoded in components.
UI & UX
-
NO CUSTOM CSS WHERE VUETIFY HAS A SOLUTION. Before writing any CSS: check if a Vuetify utility class, component prop, or slot achieves the result. Custom CSS is a last resort, must be
<style scoped>, and must have a comment explaining why Vuetify couldn't handle it. -
EVERY PAGE HAS THREE STATES:
- Loading: Vuetify skeleton loader or progress indicator
- Error: User-facing message with retry action (
v-alertwith retry button). Show what went wrong in user terms, not technical jargon. - Empty: Helpful message explaining why the list is empty and what action to take (not a blank white screen). Never show only the happy path.
-
MOBILE-FIRST. Every component must be usable at 375px width. Use Vuetify's responsive props (
cols,sm,md,lgonv-col). Never use fixed pixel widths for layout. Tables on mobile → card views or horizontal scroll with visual indicator. -
FORMS USE VEEVALIDATE + ZOD. Define the Zod schema matching the backend Form Request rules. No inline validation logic in templates. Use
useForm()anduseField()from VeeValidate with the Zod resolver.
Code quality
-
NO TODO / FIXME / HACK. Complete the implementation or report the blocker. No stubs, no "implement later", no placeholder components.
-
NO ORPHANED IMPORTS OR UNUSED VARIABLES. Run
npx tsc --noEmitafter every change. Zero errors, zero warnings. Dead code is deleted immediately, not commented out. -
NO GENERIC NAMES. Component and composable names must be specific:
- ❌
DataTable.vue,useApi.ts,helpers.ts,utils.ts - ✅
ShiftAssignmentTable.vue,useShifts.ts,formatShiftTimeRange.ts
- ❌
Routing & auth
- ROUTER GUARDS. Protected routes check auth state via navigation guard. Portal routes validate token. No unguarded routes to authenticated content. Redirect to login on 401.
Process
-
COMPONENT CREATION ORDER. For every new page/feature:
- TypeScript types in
src/types/[module].ts - API composable in
src/composables/api/use[Module].ts - Pinia store in
src/stores/use[Module]Store.ts(if cross-component) - Vue page component in
src/pages/[module]/ - Router entry in
src/router/
- TypeScript types in
-
VERIFY BEFORE DONE:
npx tsc --noEmit— zero errors- No
anytypes anywhere (search:grep -rn ": any\|as any" src/) - No TODO/FIXME/HACK
- All three states (loading, error, empty) implemented
- Mobile responsive at 375px
- Consistent with existing module patterns
TASK
[INSERT SPECIFIC TASK HERE]