diff --git a/.cursor/agents/edit-codebase.md b/.cursor/agents/edit-codebase.md new file mode 100644 index 0000000..d2ee802 --- /dev/null +++ b/.cursor/agents/edit-codebase.md @@ -0,0 +1,35 @@ +--- +name: edit-codebase +description: Expert in modifying or extending existing features. Proactively handles updates to database schemas, tRPC procedures, UI components (tables, forms), and pages. Use when the user wants to add a filter, update a field, or change existing functionality. +--- + +You are a senior full-stack developer specializing in the Flower project stack (Nuxt 4, Vue 3, Nuxt UI 4, tRPC, Drizzle). + +When invoked to modify or extend an existing feature, you must follow this systematic workflow: + +1. Analyze Existing Implementation: + - Identify the relevant files: database schema, tRPC routers, components, and pages. + - Understand the current data flow and UI structure. + +2. Plan the Changes: + - Database: Determine if schema changes or new migrations are needed. + - tRPC: Identify which procedures need modification (input validation, logic, output). + - UI: Determine which components (Table*, Form*) or pages need updates. + +3. Systematic Execution: + - Update `server/db/schema.ts` if necessary + - Check [Database Patterns](mdc:.cursor/rules/database.mdc). + - Modify tRPC procedures in `server/trpc/routers/`. + - Check [tRPC Route Patterns](mdc:.cursor/rules/trpc-routes.mdc). + - Update UI components: + - Check [Table Rules](mdc:.cursor/rules/ui-table.mdc) + - Check [Form Rules](mdc:.cursor/rules/ui-form.mdc) + - Update pages in `app/pages/`. + - Check [Data Fetching Patterns](mdc:.cursor/rules/ui-pages.mdc). + +Always adhere to the project's coding standards. Check: + +- [Common Patterns](mdc:.cursor/rules/common-patterns.mdc) +- [Structure](mdc:.cursor/rules/structure.mdc) + +You first elaborate a plan, no coding, and wait for user approval before starting to code. diff --git a/.cursor/agents/new-feature.md b/.cursor/agents/new-feature.md new file mode 100644 index 0000000..ec239c8 --- /dev/null +++ b/.cursor/agents/new-feature.md @@ -0,0 +1,34 @@ +--- +name: new-feature +description: Expert in implementing new features from scratch. Proactively handles database schema changes, table listings, creation forms, and detail pages. Use when the user wants to add a new entity or functionality to the application. +--- + +You are a senior full-stack developer specializing in the Flower project stack (Nuxt 4, Vue 3, Nuxt +UI 4, tRPC, Drizzle). + +When invoked to implement a new feature, you must follow this systematic workflow: + +1. Analyze Requirements: + - Read the user input carefully to understand the entity and its properties. +2. Database Schema: + - Check `server/db/schema.ts` (or relevant schema files). + - Check [Database Patterns](mdc:.cursor/rules/database.mdc) +3. tRPC Procedures: + - Implement necessary tRPC procedures in `server/trpc/routers/`. + - Check [tRPC Route Patterns](mdc:.cursor/rules/trpc-routes.mdc) +4. Table Component: + - Create a `Table[Entity].vue` component in `app/components/`. + - Check [Table Rules](mdc:.cursor/rules/ui-table.mdc) +5. Form Component: + - Create a `Form[Entity].vue` component in `app/components/`. + - Check [Form Rules](mdc:.cursor/rules/ui-form.mdc) +6. Page Implementation: + - Create or update pages in `app/pages/` to integrate the table and form. + - Check [Data Fetching Patterns](mdc:.cursor/rules/ui-pages.mdc) + +Always adhere to the project's coding standards. Check: + +- [Common Patterns](mdc:.cursor/rules/common-patterns.mdc) +- [Structure](mdc:.cursor/rules/structure.mdc) for more details. + +You first elaborate a plan, no coding, and wait for user approval before starting to code. diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..06110d5 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,20 @@ +{ + "mcpServers": { + "nuxt": { + "type": "http", + "url": "https://nuxt.com/mcp" + }, + "nuxt-ui": { + "type": "http", + "url": "https://ui.nuxt.com/mcp" + }, + "trpc-nuxt": { + "type": "http", + "url": "https://gitmcp.io/wobsoriano/trpc-nuxt" + }, + "better-auth": { + "type": "http", + "url": "https://gitmcp.io/better-auth/better-auth" + } + } +} \ No newline at end of file diff --git a/.cursor/rules/auth-client.mdc b/.cursor/rules/auth-client.mdc new file mode 100644 index 0000000..e293cf7 --- /dev/null +++ b/.cursor/rules/auth-client.mdc @@ -0,0 +1,313 @@ +--- +alwaysApply: false +globs: app/**/*.vue, app/**/*.ts +--- + +From the user input, and using the guide below, you will implement client-side +authentication features using Better Auth. + +# Hands on guide + +This project uses [Better Auth](https://www.better-auth.com/) for +authentication. For server-side patterns, see [Auth +Server](mdc:.cursor/rules/auth-server.mdc). + +## The `useAuth()` Composable + +The `useAuth()` composable provides all auth functionality on the client: + +```ts +const { + user, // Computed - current user + session, // Computed - current session + loggedIn, // Computed - is user logged in + client, // Better Auth client - for advanced operations + signIn, // Sign in methods + signUp, // Sign up methods + signOut, // Sign out function + fetchSession, // Refresh session data + apiKey, // API key management +} = useAuth(); +``` + +## Sign In with Email/Password + +```vue + +``` + +## Sign Up + +```vue + +``` + +## Sign In with Magic Link + +```vue + +``` + +## Sign Out + +```vue + +``` + +## Protecting Pages + +Use `definePageMeta({ auth: true })` to protect pages. The global middleware +will redirect unauthenticated users to a 404 page. + +```vue + + + +``` + +## Accessing User Data in Components + +```vue + + + +``` + +## Managing API Keys + +```vue + +``` + +## Organization Management + +Better Auth's organization plugin provides multi-tenancy support: + +```vue + +``` + +## Stripe Subscriptions + +```vue + +``` + +## Auth Form with Nuxt UI + +Use `UAuthForm` for sign in/up forms: + +```vue + + + +``` diff --git a/.cursor/rules/auth-server.mdc b/.cursor/rules/auth-server.mdc new file mode 100644 index 0000000..9194fc9 --- /dev/null +++ b/.cursor/rules/auth-server.mdc @@ -0,0 +1,207 @@ +--- +alwaysApply: false +globs: server/**/*.ts +--- + +From the user input, and using the guide below, you will implement server-side +authentication features using Better Auth. + +# Hands on guide + +This project uses [Better Auth](https://www.better-auth.com/) for +authentication. For client-side patterns, see [Auth +Client](mdc:.cursor/rules/auth-client.mdc). + +## Architecture Overview + +``` +server/ +├── lib/auth.ts → Better Auth server config +├── api/auth/[...all].ts → Auth API catch-all handler +├── lib/trpc.ts → protectedProcedure middleware +└── db/auth.ts → Auth-related DB schema +``` + +## Protecting tRPC Procedures + +Use `protectedProcedure` instead of `baseProcedure` for routes that require +authentication. The `ctx.session` is guaranteed to be non-null in protected +procedures. + +```ts +import { + createTRPCRouter, + protectedProcedure, + baseProcedure, +} from "../lib/trpc"; +import * as z from "zod"; + +export const itemsRouter = createTRPCRouter({ + // Public route - anyone can access + public: baseProcedure.query(({ ctx }) => { + // ctx.session might be null + return { message: "Hello, world!" }; + }), + + // Protected route - requires authentication + list: protectedProcedure.query(({ ctx }) => { + // ctx.session is guaranteed to exist (TRPCContextProtected) + const userId = ctx.session.user.id; + return ctx.db.query.items.findMany({ + where: (items, { eq }) => eq(items.userId, userId), + }); + }), + + // Protected mutation with input validation + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(({ input, ctx }) => { + return ctx.db.insert(items).values({ + id: crypto.randomUUID(), + name: input.name, + userId: ctx.session.user.id, + }); + }), +}); +``` + +## Server-side Auth Context + +In tRPC procedures, auth context is available via `ctx`: + +```ts +protectedProcedure.query(({ ctx }) => { + // User info + ctx.session.user.id; + ctx.session.user.email; + ctx.session.user.name; + + // Session info + ctx.session.session.id; + ctx.session.session.activeOrganizationId; + + // Auth API (for server-side operations) + ctx.auth.api.getSession({ headers: ctx.event.headers }); + ctx.auth.api.verifyApiKey({ body: { key } }); +}); +``` + +## API Key Authentication (H3 Endpoints) + +For external API endpoints (non-tRPC), use API key authentication: + +```ts +// server/api/protected/my-endpoint.ts +import { serverAuth } from "~~/server/lib/auth"; + +export default defineEventHandler(async (event) => { + const auth = serverAuth(event); + + // Get API key from header + const apiKey = getHeader(event, "x-api-key"); + + if (!apiKey) { + throw createError({ + status: 401, + message: "Missing API key. Provide x-api-key header.", + }); + } + + // Verify the API key + const result = await auth.api.verifyApiKey({ body: { key: apiKey } }); + + if (!result.valid) { + throw createError({ status: 401, message: "Unauthorized" }); + } + + // Proceed with authenticated logic + return { message: "Success!" }; +}); +``` + +## Session-based Auth in H3 Endpoints + +For session-based authentication in non-tRPC endpoints: + +```ts +// server/api/protected/session-endpoint.ts +import { serverAuth } from "~~/server/lib/auth"; + +export default defineEventHandler(async (event) => { + const auth = serverAuth(event); + + // Get session from cookies/headers + const session = await auth.api.getSession({ headers: event.headers }); + + if (!session?.user) { + throw createError({ status: 401, message: "Unauthorized" }); + } + + // Proceed with authenticated logic + return { + message: "Hello!", + userId: session.user.id, + }; +}); +``` + +## Database Schema + +Auth-related tables are defined in `server/db/auth.ts`: + +| Table | Description | +| -------------- | -------------------------------- | +| `user` | User accounts | +| `session` | Active sessions | +| `account` | OAuth/credential accounts | +| `verification` | Email verification tokens | +| `apikey` | API keys for programmatic access | +| `organization` | Organizations for multi-tenancy | +| `member` | Organization members | +| `invitation` | Pending organization invitations | +| `subscription` | Stripe subscriptions | + +## TRPCContext Types + +```ts +// Available in baseProcedure - session may be null +type TRPCContext = { + event: H3Event; + db: DrizzleDB; + storage: StorageInterface; + vector: VectorInterface; + queue: QueueInterface; + auth: BetterAuth; + session: Session | null; +}; + +// Available in protectedProcedure - session is guaranteed +type TRPCContextProtected = TRPCContext & { + session: Session; // non-null +}; +``` + +## Server Auth Configuration + +The auth server is configured in `server/lib/auth.ts` with these plugins: + +- **emailAndPassword**: Email/password authentication +- **magicLink**: Passwordless email authentication +- **apiKey**: API key generation and verification +- **organization**: Multi-tenancy with roles and invitations +- **stripe**: Subscription management + +```ts +// Accessing the auth instance in any H3 handler +import { serverAuth } from "~~/server/lib/auth"; + +export default defineEventHandler(async (event) => { + const auth = serverAuth(event); + + // Available APIs: + // auth.api.getSession({ headers }) + // auth.api.verifyApiKey({ body: { key } }) + // auth.api.createApiKey({ body: { userId, name } }) + // ... and more +}); +``` diff --git a/.cursor/rules/common.mdc b/.cursor/rules/common.mdc new file mode 100644 index 0000000..bf016d1 --- /dev/null +++ b/.cursor/rules/common.mdc @@ -0,0 +1,46 @@ +--- +description: Applies common coding patterns and conventions for the Flower project +alwaysApply: true +--- + +# Common Patterns + +## Tech Stack + +- Nuxt 4, Vue 3, Nuxt UI 4, Tailwind v4 +- tRPC, Drizzle ORM (SQLite/D1) +- Cloudflare R2, Vectorize, Queues, KV +- Better Auth +- TypeScript (strict mode) + +## Style + +- No semicolons, trailing commas, 100 char lines, single quotes + +## Frontend + +- Use ` -``` - -## Query Methods - -### `.query()` - Direct API call - -```vue - -``` - -### `useQuery()` - Reactive queries - -```vue - -``` - -### `useAsyncData()` with `.query()` - -```vue - ``` -## Mutation Methods +## Mutations -### `.mutate()` - Direct mutation call +For mutations (create, update, delete), use direct `$trpc.*.mutate()` calls +inside handler functions. This keeps the code simple and avoids unnecessary +complexity. ```vue -``` - -### `useMutation()` - Reactive mutations -```vue - -``` -### `useAsyncData()` with `.mutate()` + ``` -## When to Use Each Method +## When to use each method + +| Method | Use Case | Features | +| :--------------- | :------------------------- | :--------------------------------- | +| `useAsyncData` | Data fetching (queries) | SSR, Caching, Reactive (via watch) | +| `$trpc.*.mutate` | Mutations in handlers | Simple, direct, no overhead | +| `$trpc.*.query` | One-off, non-reactive call | Simple scripts, utilities | -| Method | Use Case | Features | -|------------------------------|----------------------------|-----------------------------------------| -| `.query()` | One-time data fetch | Simple, direct call | -| `useQuery()` | Reactive data that updates | Auto-refresh, reactive | -| `useAsyncData() + .query()` | SSR + caching | Server-side rendering, cache management | -| `.mutate()` | Simple form submissions | Direct mutation call | -| `useMutation()` | Complex mutation state | Loading states, error handling | -| `useAsyncData() + .mutate()` | Cached mutations | Rare, specific use cases | +## Complete example -## Complete Example +A full page example combining queries and mutations. ```vue -``` \ No newline at end of file +``` diff --git a/.cursor/rules/trpc-routes.mdc b/.cursor/rules/trpc-routes.mdc index e9d5251..b29086c 100644 --- a/.cursor/rules/trpc-routes.mdc +++ b/.cursor/rules/trpc-routes.mdc @@ -3,106 +3,203 @@ globs: server/trpc/**/*.ts alwaysApply: false --- -# tRPC Route +From the user input, and using the guide below, you will implement a new tRPC +router. -This guide shows how to create minimal tRPC routes following the project's -established patterns. +# Hands on guide + +Below is a hands on guide on the patterns used in this project to implement a +new tRPC router. ## File Structure ``` server/trpc/ -├── index.ts # Main router -+── [route].ts # Route operations +├── index.ts # Main app router (merges all other routers) +└── [route].ts # Domain-specific routers (e.g., users.ts, files.ts, items.ts) ``` -## Basic Route Template - -Create a new file in `server/trpc/` following this template: +## Basic Router Template ```typescript -import { z } from 'zod' -import { TTable } from '../db/schema' -import { useDrizzle } from '../utils/drizzle' -import { baseProcedure, createTRPCRouter } from '../utils/trpc' - -export const router = createTRPCRouter({ - list: baseProcedure - .input(z.object({ /* input fields */ })) - .query(async ({ input, ctx }) => { - const db = useDrizzle() - return await db +// server/trpc/items.ts +import { and, desc, eq } from "drizzle-orm"; +import * as z from "zod"; +// import the table from the schema +import { TItems } from "~~/server/db/schema"; +// import the router and procedure helpers +import { createTRPCRouter, protectedProcedure } from "../lib/trpc"; + +export const itemsRouter = createTRPCRouter({ + // LIST: get all items for the current user + // use protectedProcedure for authenticated routes + list: protectedProcedure + // no input needed for list, but you can add pagination: + // .input(z.object({ limit: z.number().optional().default(10) })) + .query(async ({ ctx }) => { + return await ctx.db .select() - .from(TTable) - .where(/* where conditions */) - .orderBy(/* order by (if needed) */) + .from(TItems) + // always filter by user id for user-scoped data + .where(eq(TItems.userId, ctx.session.user.id)) + .orderBy(desc(TItems.createdAt)) + // always use .all() for lists - it throws an error if the query fails + .all(); }), - // Mutation example - creates data - create: baseProcedure - .input(z.object({ /* input fields */ })) + // CREATE: insert a new item + create: protectedProcedure + // always use zod for input validation + // always add defaults in the schema if possible + .input(z.object({ name: z.string().min(1).max(255) })) .mutation(async ({ input, ctx }) => { - const db = useDrizzle() - const [result] = await db - .insert(TTable) - .values({ /* map your input fields */}) + return await ctx.db + .insert(TItems) + // always include userId for user-scoped data + .values({ name: input.name, userId: ctx.session.user.id }) .returning() - return result + // always use .get() for single results - it throws an error if the query + // fails + .get(); }), - // Delete example - deletes data - delete: baseProcedure - .input(z.object({ /* input fields */ })) + // UPDATE: update an existing item + update: protectedProcedure + .input( + z.object({ id: z.string().uuid(), name: z.string().min(1).max(255) }), + ) .mutation(async ({ input, ctx }) => { - const db = useDrizzle() - const [result] = await db - .delete(TTable) - .where(/* where conditions */) + return await ctx.db + .update(TItems) + .set({ name: input.name }) + // always check both id AND userId to prevent unauthorized access + .where( + and(eq(TItems.id, input.id), eq(TItems.userId, ctx.session.user.id)), + ) .returning() - return result + .get(); }), - update: baseProcedure - .input(z.object({ /* input fields */ })) + // DELETE: remove an item + delete: protectedProcedure + .input(z.object({ id: z.string().uuid() })) .mutation(async ({ input, ctx }) => { - const db = useDrizzle() - const [result] = await db - .update(TTable) - .set({ /* map your input fields */}) - .where(/* where conditions */) - .returning() - return result + // delete doesn't need to return anything + await ctx.db + .delete(TItems) + .where( + and(eq(TItems.id, input.id), eq(TItems.userId, ctx.session.user.id)), + ); }), -}) +}); ``` -## Key Patterns +## Registering a Router -#### Simplicity +After creating a router, register it in the main app router: -Now we don't care about catching any errors, just input validation and fetching -the data. So, ignore errors for now. +```typescript +// server/trpc/index.ts +import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; +import { itemsRouter } from "~~/server/trpc/items"; +import { baseProcedure, createTRPCRouter } from "../lib/trpc"; + +export const appRouter = createTRPCRouter({ + // public health check endpoints + ping: baseProcedure.query(() => "pong"), + health: baseProcedure.query(() => ({ status: "ok" })), + + // register domain routers + items: itemsRouter, +}); + +// export type definition of API for client usage +export type AppRouter = typeof appRouter; +export type AppRouterInputs = inferRouterInputs; +export type AppRouterOutputs = inferRouterOutputs; +``` + +## Using Context Services -Good example: +The context (`ctx`) provides access to various services. Check +[`server/lib/trpc.ts`](mdc:server/lib/trpc.ts) for the full context definition. ```typescript -baseProcedure - .input(z.object({ /* input fields */ })) - .mutation(async ({ input, ctx }) => { - console.log(input) - }) +// ctx.db - Drizzle database instance +const items = await ctx.db.select().from(TItems).all() + +// ctx.storage - Cloudflare R2 file storage +await ctx.storage.put(key, file) +await ctx.storage.get(key) +await ctx.storage.del(key) +const files = await ctx.storage.list(prefix) +const url = await ctx.storage.presign(key) + +// ctx.vector - Cloudflare Vectorize for embeddings +await ctx.vector.set(key, vector, metadata) +await ctx.vector.get(key) +await ctx.vector.del(key) +const results = await ctx.vector.search(vector, topK) + +// ctx.queue - Cloudflare Queues for background jobs +await ctx.queue.send({ type: 'process', data: { ... } }) + +// ctx.session - Better Auth session (available in protectedProcedure) +ctx.session.user.id +ctx.session.user.email +ctx.session.session.id + +// ctx.event - H3 event for advanced use cases +ctx.event.headers +ctx.event.waitUntil(asyncTask) // for background tasks ``` -Bad example: +## File Upload Handling + +For file uploads, use FormData with zod transforms: ```typescript -baseProcedure - .input(z.object({ /* input fields */ })) - .mutation(async ({ input, ctx }) => { - try { - console.log(input) - } catch (error) { - console.error(error) - } - }) +import { createTRPCRouter, protectedProcedure } from "../lib/trpc"; +import { onFileUpload } from "../tasks/onFileUpload"; + +export const filesRouter = createTRPCRouter({ + create: protectedProcedure + .input( + z + // accept FormData from the client + .instanceof(FormData) + // transform to object for easier access + .transform((fd) => Object.fromEntries(fd.entries())) + // then validate the shape + .pipe( + z.object({ + file: z + .instanceof(File) + .refine((f) => f.size > 0, "File is empty") + .refine( + (f) => f.size <= 10 * 1024 * 1024, + "File too large (max 10MB)", + ) + .refine((f) => ["text/plain", "text/markdown"].includes(f.type)), + }), + ), + ) + .mutation(async ({ input, ctx }) => { + // namespace files by user id + const key = `${ctx.session.user.id}/${input.file.name}`; + await ctx.storage.put(key, input.file); + + // trigger background processing with waitUntil + ctx.event.waitUntil(onFileUpload(key, ctx)); + + return { key }; + }), +}); ``` + +## Procedure Types + +- `baseProcedure`: Public procedure with logging. Use for health checks, public + data. +- `protectedProcedure`: Requires authentication, throws 401 if no session. Use + for all user-scoped data. diff --git a/.cursor/rules/ui-colors.mdc b/.cursor/rules/ui-colors.mdc index ee56167..6db235b 100644 --- a/.cursor/rules/ui-colors.mdc +++ b/.cursor/rules/ui-colors.mdc @@ -2,70 +2,19 @@ globs: app/pages/**,app/components/** alwaysApply: false --- -# Nuxt UI + Tailwind Colors Cheat Sheet - -## Theming with Tailwind - -```css -@import "tailwindcss"; -@import "@nuxt/ui"; - -@theme { - /* Custom theme tokens */ - --font-sans: 'Public Sans', system-ui, sans-serif; - --font-mono: 'JetBrains Mono', monospace; -} -``` - -## Colors: Semantic Map - -| Semantic | Default | Use | -|-------------|---------|---------------------------------------| -| primary | green | Main CTAs, active UI, brand | -| secondary | blue | Secondary actions, alt UI | -| success | green | Success, confirmations | -| info | blue | Info, neutral alerts | -| warning | yellow | Warnings, attention-needed | -| error | red | Errors, destructive actions | -| neutral | slate | Background, text, borders, disabled | - -**Usage:** -```vue -Save -``` - -**Add or Override Colors (main.css):** -```css -@theme static { - /* Custom/override shades */ - --color-brand-500: #ef4444; - --color-green-400: #00DC82; - /* ... all shades 50-950 ... */ -} -``` - -## Runtime Color Mapping - -`app.config.ts` (for dynamic assignment of semantic colors): -```ts -export default defineAppConfig({ - ui: { - colors: { - primary: 'brand', // or any color you've defined in @theme - secondary: 'blue', - neutral: 'zinc' - } - } -}) -``` - -> Only use color names that match Tailwind or those you define in `@theme`. - - -## Docs - -- [Tailwind Theme Docs](https://tailwindcss.com/docs/theme) -- [Nuxt UI Theming](https://ui.nuxt.com/getting-started/theme/colors) -- [Add Fonts](https://tailwindcss.com/docs/font-family#customizing-your-theme) - +# Semantic colors + +Nuxt UI provides semantic color aliases that describe the purpose of the color. +Each alias is defined based on a color from your `@theme` configuration, which +can be any color you define in addition to the default Tailwind CSS palette. + +| Color | Default | Description | +| --------- | ------- | ----------------------------------------------------------------- | +| primary | green | Main CTAs, active navigation, brand elements, important links | +| secondary | blue | Secondary buttons, alternative actions, complementary UI elements | +| success | green | Success messages, completed states, positive confirmations | +| info | blue | Info alerts, tooltips, help text, neutral notifications | +| warning | yellow | Warning messages, pending states, attention-needed items | +| error | red | Error messages, validation errors, destructive actions | +| neutral | slate | Text, borders, backgrounds, disabled states | diff --git a/.cursor/rules/ui-form.mdc b/.cursor/rules/ui-form.mdc index 2e58b75..39a30ed 100644 --- a/.cursor/rules/ui-form.mdc +++ b/.cursor/rules/ui-form.mdc @@ -2,535 +2,97 @@ globs: app/pages/**/*.vue,app/app.vue alwaysApply: false --- -# UI Form Component Rules -## Basic Form Setup with Zod +# Sample Form\* component with best practices -Use Zod for schema validation with proper TypeScript typing: +Follow this example when creating a new form component. ```vue -``` - -## Form Component with Props - -Use `withDefaults` for props with default values and export form data type: - -```vue - - - ``` -## Form Field Types - -Common form field patterns with Zod validation: +## Usage ```vue - - -``` - -## Form Submission Patterns - -### Direct Emit Pattern (Recommended) - -```vue - - - -``` - -### Handler Function Pattern - -```vue - -``` - -## Custom Validation - -Use the `validate` prop for complex validation logic: - -```vue - - - ``` - -## Form Methods & Error Handling - -Access form methods using template refs: - -```vue - - - -``` - -## Customizable Submit Button - -Use slots to allow parent components to customize the submit button: - -```vue - - - -``` - -## Nested Forms - -For complex forms with conditional fields: - -```vue - - - -``` - -## Complete Example - -```vue - - - -``` - -## Key Guidelines - -- Always use TypeScript with `z.output` for type safety -- Use `reactive>({})` for form state -- Use `withDefaults` for props with default values -- Export form data type: `export type FormData = Schema` -- Initialize state from props: `name: props.defaultValue?.name ?? ''` -- Use `watch` to sync props changes to state: `watch(() => props.defaultValue, v => v && Object.assign(state, v))` -- Use `@submit.prevent` with direct emit: `@submit.prevent="(e) => emits('submit', e.data)"` -- Use `class="flex flex-col gap-4"` for form layout -- Use `description` prop on `UFormField` for help text -- Use slots for submit button customization: `` -- Use `defineEmits<{ submit: [e: Schema] }>()` for simple emit patterns -- Handle form submission with `FormSubmitEvent` when using handler functions -- Access form methods via template refs: `const form = useTemplateRef('form')` -- Common validation: `validate-on` prop controls when validation triggers -- Error handling: Listen to `@error` event for custom error handling \ No newline at end of file diff --git a/.cursor/rules/ui-page.mdc b/.cursor/rules/ui-page.mdc new file mode 100644 index 0000000..c91e8d4 --- /dev/null +++ b/.cursor/rules/ui-page.mdc @@ -0,0 +1,155 @@ +--- +alwaysApply: false +--- + +From the user input, and using the guide below, you will implement a new nuxt +page. + +# Hands on guide + +Below is a hands on guide on the patterns used in this project to implement a +new nuxt page. + +```vue + +``` + +## Form handling + +If you need to handle a form, you can use the following pattern: + +Always leave the actual page calls to the top page, if possible. Most +communication should be via emits and props. + +Create a `FormItem.vue` component and use it in the page. Check [Form +Rules](mdc:.cursor/rules/ui-form.mdc) for details. Basically, it should, at +least, have a `@submit({ data })` event that will be triggered when the form is +already validated and submitted + +```vue + + + +``` + +## Nested pages + +We can make use of nested `` components to render the subpages. + +For example, the following page structure: + +```bash +app/pages/ +├── demo +│ ├── page-a.vue # /demo/page-a +│ ├── page-b.vue # /demo/page-b +│ └── page-c.vue # /demo/page-c +└── demo.vue # /demo +``` + +The `demo.vue` page will render the subpages in the `` component: + +```vue + + +``` + +## Param validation + +If you need to validate the route params, you can use the following pattern: + +```vue + + +``` diff --git a/.cursor/rules/ui-pages.mdc b/.cursor/rules/ui-pages.mdc deleted file mode 100644 index d2c417a..0000000 --- a/.cursor/rules/ui-pages.mdc +++ /dev/null @@ -1,51 +0,0 @@ ---- -globs: app/pages/**/*.vue,app/app.vue -alwaysApply: false ---- - -## Data Fetching - -The data of the app SHOULD be fetched at the page level and passed down to -child components. - -The child components SHOULD NOT make API calls or fetch data themselves, but -instead define and use `emits` to communicate changes back to the parent page. - -## Good example - -```vue - - - -``` - -## Bad example - -```vue - - - -``` diff --git a/.cursor/rules/ui-table.mdc b/.cursor/rules/ui-table.mdc index 565948e..692f774 100644 --- a/.cursor/rules/ui-table.mdc +++ b/.cursor/rules/ui-table.mdc @@ -2,433 +2,81 @@ globs: app/**/Table*.vue alwaysApply: false --- -# UI Table Component Rules -## Data Structure & Columns +# UI Table Component Rules -Define table data as typed arrays and columns using the `TableColumn[]` -interface: +Follow this example when creating a new table component. ```vue -``` - -## Using Template Slots for Cell Rendering +]; -Prefer template slots (`#columnId-cell`) for complex cell content over `h()` -function: +// Create MAP_ID_TO_LABEL for dropdown menu labels +const MAP_ID_TO_LABEL: Record = columns.reduce( + (a, c) => ({ ...a, [c.id as Keys]: c.header }), + {}, +); -```vue - - - -``` - -## Column Visibility Control - -Use `v-model:column-visibility` with a dropdown menu for toggling columns: - -```vue - -``` - -## Component Rendering in Cells (h function) - -Use Vue's `h` function and `resolveComponent` for rendering components in column -definitions when not using slots: - -```vue - -``` - -## Table Configuration - -Common table props and configuration patterns: - -```vue - -``` - -## Common Patterns - -### Row Selection - -```vue - - - -``` - -### Column Sorting - -```vue - - - -``` - -### Expandable Rows - -```vue - - - -``` - -### Component with Events - -```vue - - - -``` - -## Complete Example - -```vue - - -