# Palmy.io - Claude Code Instructions

Nuxt 4 + Vue 3 event platform. Organizers create projects (contest, drawing, game, fanphoto, fundraiser) that users join at live events.

## Stack
Nuxt 4, Vue 3 (`<script setup lang="ts">`), @nuxt/ui v4, Tailwind v4, Prisma + PostgreSQL, @nuxtjs/supabase, AWS S3, Stripe, OpenAI, pnpm

## Key Paths
- Form components: `app/components/org/[category]/form.vue`
- View components: `app/components/org/[category]/view.vue`
- Add pages: `app/pages/organizer/[category]/add.vue`
- Edit pages: `app/pages/organizer/[category]/[id]/edit.vue`
- Details pages: `app/pages/organizer/[category]/[id]/details.vue`
- Manage pages: `app/pages/organizer/[category]/[id]/manage.vue`
- View API (edit data): `server/routes/api/organizer/[category]/[id]/view.ts`
- CRUD API: `server/routes/api/organizer/[category]/[id]/index.ts` (GET=details, POST=update, PUT=status, DELETE)
- Create API: `server/routes/api/organizer/[category]/index.post.ts`
- Options/data: `app/composables/data.ts`
- Prisma types: `~~/prisma/generated/models`
- API response: `server/utils/apiResponse.ts`
- S3: `server/utils/s3Actions.post.ts` (s3Upload, s3Delete)

## Rules
1. **No Zod** in server APIs. Use Prisma types from `~~/prisma/generated/models`
2. **ALL** server returns use `apiResponse()` wrapper (auto-imported)
3. User ID: `const USER_ID = Number(event.context.user.id)`
4. Page data: `await useLazyFetch` (NOT useFetch or $fetch)
5. Form components: `defineModel` pattern (NOT props + emit update)
6. Review modal stays open during save, button shows `:loading="saving"`
7. Sponsors/coupons: `{ set: [], connect: [...].map(s => ({ id: Number(s.id) })) }`
8. eventDate: DB=DateTime, input=`substring(0, 10)`, save=`new Date()`
9. Photos: FormData + `s3Upload(file, userId)`, delete old with `s3Delete(key)`
10. Auto-imports in server: `prisma`, `apiResponse`, `s3Upload`, `s3Delete`, `getRouterParam`, `readFormData`, `readBody`, `getQuery`

## Patterns

**Edit page**: `useLazyFetch` view API -> `watch` populate form -> form component with `defineModel` -> parent `onSubmit` POSTs FormData -> toast + navigateTo

**List page**: `useLazyFetch` -> local `ref` copy via `watch` -> delete updates both local + fetch data

**View API**: `apiResponse({ result: project })` with try/catch

**Update API**: `readFormData` -> `JSON.parse(form.get('project'))` -> handle photo upload -> `prisma.project.update` -> `apiResponse({ message })`

**API files**: `index.ts` = combined methods, `.get.ts`/`.post.ts` = single method

## Components
- `<PhotoUpload :photo="reactive({base64,blob,old})" label="Image" />`
- `<OrgHeader title="Title" />`
- `<OrgNoData title="Empty" />`
- `<OrgDeleteModal v-model="dltVal" @delete="handler" />`
- `<OrgSponsorSelect v-model:sponsors v-model:coupons />`
- `<OrgProjectsHeaderMenu :id :project :loading />`

## Composables
- `useIcons()` -> `Icon.success`, `Icon.error`, `Icon.close`, `Icon.search`
- `useToast()` -> `toast.add({ title, color: 'success'/'error', icon })`
- `imgSrc(url)` - S3 key to full URL
- `formatDate(date)` - "12 Jan 2025"
- `contestOptions`, `gameOptions`, `drawingOptions`, `prizeOptions`, `distributionOptions`, `resultsOptions`

## Commands
```
pnpm dev          # Dev server
pnpm build        # Production build
pnpm lint         # ESLint
pnpm typecheck    # TypeScript check
pnpm run PGenerate # Generate Prisma client + types
```

## Full Reference
See `INSTRUCTIONS.md` for detailed code examples and environment variables.
