diff --git a/.agent/AGENTS.md b/.agent/AGENTS.md new file mode 100644 index 0000000..6eae984 --- /dev/null +++ b/.agent/AGENTS.md @@ -0,0 +1,91 @@ +--- +--- + +# .agent — Development Guidelines + +## Stack + +- Next.js (App Router, latest stable patch) +- React 19 +- TypeScript (strict) +- Convex +- shadcn/ui + Tailwind CSS + +--- + +## Defaults + +- **Server Components by default.** Add `'use client'` only at the leaves that need interactivity/hooks. +- **Convex is the source of truth** for server state; local state is for UI-only concerns. +- **Always leverage typescript**; avoid usage of `any` which defeats the point of using Typescript. + +--- + +## Components & Composition + +- Prefer **small, focused components**. Split when a component mixes concerns or becomes hard to scan. +- **Presentational components:** props in → JSX out (no data fetching; minimal/no app logic). +- **Feature components:** coordinate data + state + composition. +- Prefer **composition** (children, slots) over giant prop APIs. + +--- + +## State & Side Effects + +- Keep state **closest to where it’s used**; lift only for shared ownership. +- Don’t mirror Convex query results into local state. Store **UI state** (selection, filters, draft text), not server data. +- Avoid `useEffect` for app data fetching. Use it for **browser-only side effects** (subscriptions, observers, localStorage). + +--- + +## Convex + Next.js (Best Practice Patterns) + +- **Client reactivity:** `useQuery` / `useMutation` in Client Components. +- **SSR + reactivity:** in a Server Component, `preloadQuery(...)` and pass the payload to a Client Component using `usePreloadedQuery(...)`. +- **Server-only (non-reactive) rendering:** `fetchQuery(...)` in Server Components. +- **Server Actions / Route Handlers:** call Convex via `fetchMutation` / `fetchAction` when you must mutate from the server boundary (e.g., form action, webhook, route handler). +- **Optimistic UI:** prefer Convex optimistic updates (`useMutation(...).withOptimisticUpdate(...)`) over duplicating server logic in ad-hoc local state. +- **Consistency:** avoid multiple `preloadQuery` calls on the same page; consolidate queries or design around a single preload. + +--- + +## Next.js App Router Conventions + +- Use `loading.tsx` and `error.tsx` for route-segment UX. +- Use `not-found.tsx` for 404 states. +- Prefer colocating components with the route/feature that owns them; extract to shared only when reused. + +--- + +## React 19 Usage + +- Prefer **Actions + `useActionState`** for form submission state when using form actions. +- Use `useOptimistic` for instant UI feedback when appropriate. +- Use `useFormStatus` inside design-system components that need form pending state. +- `use` can read a Promise/Context during render (only where Suspense semantics make sense). + +--- + +## TypeScript (Strict, Practical) + +- Let inference work; **export explicit prop types** for shared/public components. +- No `any`. Use `unknown` and narrow. +- Avoid `as` casts; prefer `satisfies`, generics, and runtime validation. +- `@ts-expect-error` allowed only with a comment explaining why + link/TODO to remove. + +--- + +## Performance + +- Minimize client bundles: keep heavy logic/components server-side when possible. +- Use dynamic imports for non-critical client UI. +- Use `next/image` for images. +- Memoization is opt-in: use `memo`/`useMemo`/`useCallback` only when profiling shows value. + +--- + +## Styling (shadcn/ui + Tailwind) + +- Use shadcn/ui as the base; extend via composition + variants. +- Tailwind utilities in JSX; extract repetition into components, not `@apply`. +- Theme via CSS variables; keep design tokens centralized. diff --git a/.eslintcache b/.eslintcache new file mode 100644 index 0000000..0b12a09 --- /dev/null +++ b/.eslintcache @@ -0,0 +1 @@ +[{"C:\\Code\\pixelstream\\convex\\generatedImages.test.ts":"1","C:\\Code\\pixelstream\\convex\\generatedImages.ts":"2","C:\\Code\\pixelstream\\convex\\generatedImages\\helpers.ts":"3","C:\\Code\\pixelstream\\convex\\generatedImages\\internal.ts":"4","C:\\Code\\pixelstream\\convex\\generatedImages\\mutations.ts":"5","C:\\Code\\pixelstream\\convex\\generatedImages\\queries.ts":"6","C:\\Code\\pixelstream\\convex\\generatedImages\\types.ts":"7"},{"size":12275,"mtime":1768840417883,"results":"8","hashOfConfig":"9"},{"size":888,"mtime":1768838997931,"results":"10","hashOfConfig":"11"},{"size":10454,"mtime":1768838750161,"results":"12","hashOfConfig":"11"},{"size":5249,"mtime":1768838693204,"results":"13","hashOfConfig":"11"},{"size":10478,"mtime":1768838660696,"results":"14","hashOfConfig":"11"},{"size":15924,"mtime":1768838750173,"results":"15","hashOfConfig":"11"},{"size":4411,"mtime":1768838482327,"results":"16","hashOfConfig":"11"},{"filePath":"17","messages":"18","suppressedMessages":"19","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1hc6ftq",{"filePath":"20","messages":"21","suppressedMessages":"22","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1ekn74t",{"filePath":"23","messages":"24","suppressedMessages":"25","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"26","messages":"27","suppressedMessages":"28","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"29","messages":"30","suppressedMessages":"31","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"32","messages":"33","suppressedMessages":"34","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"35","messages":"36","suppressedMessages":"37","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"C:\\Code\\pixelstream\\convex\\generatedImages.test.ts",[],[],"C:\\Code\\pixelstream\\convex\\generatedImages.ts",[],[],"C:\\Code\\pixelstream\\convex\\generatedImages\\helpers.ts",[],[],"C:\\Code\\pixelstream\\convex\\generatedImages\\internal.ts",[],[],"C:\\Code\\pixelstream\\convex\\generatedImages\\mutations.ts",[],[],"C:\\Code\\pixelstream\\convex\\generatedImages\\queries.ts",[],[],"C:\\Code\\pixelstream\\convex\\generatedImages\\types.ts",[],[]] \ No newline at end of file diff --git a/.github/instructions.md b/.github/instructions.md new file mode 100644 index 0000000..aa440b6 --- /dev/null +++ b/.github/instructions.md @@ -0,0 +1,84 @@ +--- +trigger: always_on +--- + +# .agent — Development Guidelines + +## Stack +- Next.js (App Router, latest stable patch) +- React 19 +- TypeScript (strict) +- Convex +- shadcn/ui + Tailwind CSS +- Use `bun run test` to run tests - NOT `bun test`. + +--- + +## Defaults +- **Server Components by default.** Add `'use client'` only at the leaves that need interactivity/hooks. +- **Convex is the source of truth** for server state; local state is for UI-only concerns. +- **Always leverage typescript**; avoid usage of `any` which defeats the point of using Typescript. +- **Ensure** any new or updated code have had their test files created or updated idiomatically using RTL gold standards and vite test usage. + +--- + +## Components & Composition +- Prefer **small, focused components**. Split when a component mixes concerns or becomes hard to scan. +- **Presentational components:** props in → JSX out (no data fetching; minimal/no app logic). +- **Feature components:** coordinate data + state + composition. +- Prefer **composition** (children, slots) over giant prop APIs. + +--- + +## State & Side Effects +- Keep state **closest to where it’s used**; lift only for shared ownership. +- Don’t mirror Convex query results into local state. Store **UI state** (selection, filters, draft text), not server data. +- Avoid `useEffect` for app data fetching. Use it for **browser-only side effects** (subscriptions, observers, localStorage). + +--- + +## Convex + Next.js (Best Practice Patterns) +- **Client reactivity:** `useQuery` / `useMutation` in Client Components. +- **SSR + reactivity:** in a Server Component, `preloadQuery(...)` and pass the payload to a Client Component using `usePreloadedQuery(...)`. +- **Server-only (non-reactive) rendering:** `fetchQuery(...)` in Server Components. +- **Server Actions / Route Handlers:** call Convex via `fetchMutation` / `fetchAction` when you must mutate from the server boundary (e.g., form action, webhook, route handler). +- **Optimistic UI:** prefer Convex optimistic updates (`useMutation(...).withOptimisticUpdate(...)`) over duplicating server logic in ad-hoc local state. +- **Consistency:** avoid multiple `preloadQuery` calls on the same page; consolidate queries or design around a single preload. + +--- + +## Next.js App Router Conventions +- Use `loading.tsx` and `error.tsx` for route-segment UX. +- Use `not-found.tsx` for 404 states. +- Prefer colocating components with the route/feature that owns them; extract to shared only when reused. + +--- + +## React 19 Usage +- Prefer **Actions + `useActionState`** for form submission state when using form actions. +- Use `useOptimistic` for instant UI feedback when appropriate. +- Use `useFormStatus` inside design-system components that need form pending state. +- `use` can read a Promise/Context during render (only where Suspense semantics make sense). + +--- + +## TypeScript (Strict, Practical) +- Let inference work; **export explicit prop types** for shared/public components. +- No `any`. Use `unknown` and narrow. +- Avoid `as` casts; prefer `satisfies`, generics, and runtime validation. +- `@ts-expect-error` allowed only with a comment explaining why + link/TODO to remove. + +--- + +## Performance +- Minimize client bundles: keep heavy logic/components server-side when possible. +- Use dynamic imports for non-critical client UI. +- Use `next/image` for images. +- Memoization is opt-in: use `memo`/`useMemo`/`useCallback` only when profiling shows value. + +--- + +## Styling (shadcn/ui + Tailwind) +- Use shadcn/ui as the base; extend via composition + variants. +- Tailwind utilities in JSX; extract repetition into components, not `@apply`. +- Theme via CSS variables; keep design tokens centralized. \ No newline at end of file diff --git a/.github/skills/adding-convex-table/SKILL.md b/.github/skills/adding-convex-table/SKILL.md new file mode 100644 index 0000000..a4f12e2 --- /dev/null +++ b/.github/skills/adding-convex-table/SKILL.md @@ -0,0 +1,143 @@ +--- +name: adding-convex-table +description: | + Adds a new table to Convex schema with indexes. + Input: Table name, fields, and query patterns. + Output: Updated schema.ts with new table definition. +--- + +# Adding a Convex Table + +Adds a new table to `convex/schema.ts` with proper indexes for query performance. + +## Preconditions + +- Table doesn't already exist in schema +- User has provided: table name, fields, and expected query patterns + +## Algorithm + +```markdown +1. Read current schema: + - [ ] Open `convex/schema.ts` + - [ ] Identify existing tables for pattern reference + +2. Define table: + - [ ] Add `defineTable()` with field validators + - [ ] Use appropriate `v.*` types for each field + - [ ] Add `userId` if user-scoped data + +3. Add indexes: + - [ ] Add `.index()` for each field used in queries + - [ ] Name indexes descriptively: `by_[field]` or `by_[field1]_and_[field2]` + +4. Verify: + - [ ] Save file — Convex auto-pushes in dev + - [ ] Check Convex dashboard for new table + - [ ] Run `bun run build` to regenerate types +``` + +## Validator Reference + +| Type | Validator | Example | +|------|-----------|---------| +| String | `v.string()` | `name: v.string()` | +| Number | `v.number()` | `count: v.number()` | +| Boolean | `v.boolean()` | `isActive: v.boolean()` | +| Optional | `v.optional(v.string())` | `bio: v.optional(v.string())` | +| ID reference | `v.id("tableName")` | `userId: v.id("users")` | +| Union | `v.union(v.literal("a"), v.literal("b"))` | `status: v.union(...)` | +| Array | `v.array(v.string())` | `tags: v.array(v.string())` | +| Object | `v.object({ ... })` | Nested objects | +| Any (avoid) | `v.any()` | Only if truly dynamic | + +## Template + +```typescript +// convex/schema.ts +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + // Existing tables... + + // NEW TABLE + myNewTable: defineTable({ + // Required fields + userId: v.string(), + title: v.string(), + + // Optional fields + description: v.optional(v.string()), + + // Enum/status fields + status: v.union( + v.literal("draft"), + v.literal("published"), + v.literal("archived") + ), + + // Timestamps + createdAt: v.number(), + updatedAt: v.optional(v.number()), + }) + // Indexes for queries + .index("by_user", ["userId"]) + .index("by_user_and_status", ["userId", "status"]) + .index("by_created", ["createdAt"]), +}); +``` + +## Existing Tables (Reference) + +| Table | Key Fields | Indexes | +|-------|------------|---------| +| `users` | `email`, `stripeCustomerId` | `by_email`, `by_clerk_id` | +| `generatedImages` | `userId`, `model`, `status` | `by_user`, `by_user_and_status` | +| `favorites` | `userId`, `imageId` | `by_user`, `by_image` | +| `follows` | `followerId`, `followedId` | `by_follower`, `by_followed` | +| `promptLibrary` | `userId`, `prompt` | `by_user` | +| `referenceImages` | `userId`, `storageId` | `by_user` | + +## Index Strategy + +| Query Pattern | Index Needed | +|--------------|--------------| +| Filter by single field | `.index("by_field", ["field"])` | +| Filter by multiple fields | `.index("by_a_and_b", ["a", "b"])` | +| Sort by field | Same as filter (Convex sorts on index) | +| Unique lookup | `.index("by_field", ["field"])` | + +## Guardrails + +- **Always index** fields used in `.withIndex()` or `.filter()` +- **User-scoped data** must have `userId` field + `by_user` index +- **Timestamps** use `v.number()` (Unix ms), not Date +- **Never use raw SQL** — Convex handles persistence +- **If schema push fails**, report error and stop + +## Output Format + +```markdown +## Summary +Added table `[tableName]` for [purpose]. + +## Schema +```typescript +myTable: defineTable({ + field1: v.string(), + field2: v.number(), +}).index("by_field1", ["field1"]) +``` + +## Indexes +- `by_field1` — For querying by field1 + +## Verification +- Schema pushed ✅ +- Types regenerated ✅ +- Table visible in dashboard ✅ + +## Next Steps +- Create queries/mutations in `convex/[tableName].ts` +``` diff --git a/.github/skills/adding-route/SKILL.md b/.github/skills/adding-route/SKILL.md new file mode 100644 index 0000000..10697cf --- /dev/null +++ b/.github/skills/adding-route/SKILL.md @@ -0,0 +1,141 @@ +--- +name: adding-route +description: | + Creates a new page/route in Next.js App Router with all boilerplate. + Input: Route path (e.g., "/dashboard") and page purpose. + Output: page.tsx, layout.tsx (if needed), loading.tsx, error.tsx. +--- + +# Adding a Route + +Creates a new App Router page with proper structure and loading/error states. + +## Preconditions + +- Confirm the route doesn't already exist in `app/` +- User has provided: route path and page purpose/description + +## Algorithm + +```markdown +1. Create page file: + - [ ] Create `app/[route]/page.tsx` as Server Component + - [ ] Add SEO: `export const metadata = { title, description }` + - [ ] Add semantic structure: `
`, `

`, sections + +2. Create loading state: + - [ ] Create `app/[route]/loading.tsx` + - [ ] Use skeleton with `animate-pulse bg-muted/50` + +3. Create error boundary: + - [ ] Create `app/[route]/error.tsx` as Client Component ('use client') + - [ ] Include reset button + +4. Create layout (if needed): + - [ ] Only if route needs custom wrapper/providers + - [ ] Create `app/[route]/layout.tsx` + +5. Verify: + - [ ] Run `bun run build` — report errors + - [ ] Navigate to route in dev mode +``` + +## Template: page.tsx + +```tsx +// app/[route]/page.tsx +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: '[Page Title] | Bloom Studio', + description: '[Page description for SEO]', +}; + +export default function [RouteName]Page() { + return ( +
+

+ [Page Title] +

+
+ {/* Page content */} +
+
+ ); +} +``` + +## Template: loading.tsx + +```tsx +// app/[route]/loading.tsx +export default function Loading() { + return ( +
+
+
+
+ ); +} +``` + +## Template: error.tsx + +```tsx +// app/[route]/error.tsx +'use client'; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( +
+

Something went wrong

+ +
+ ); +} +``` + +## Existing Routes (Reference) + +| Route | Path | +|-------|------| +| Landing | `app/page.tsx` | +| Studio | `app/studio/page.tsx` | +| History | `app/history/page.tsx` | +| Feed | `app/feed/page.tsx` | +| Favorites | `app/favorites/page.tsx` | +| Settings | `app/settings/page.tsx` | +| Pricing | `app/pricing/page.tsx` | +| Profile | `app/profile/page.tsx` | + +## Guardrails + +- **Do NOT add `'use client'`** to page.tsx unless absolutely required +- **If route already exists**, stop and ask user for clarification +- **If build fails**, report error and stop + +## Output Format + +```markdown +## Summary +Created route `/[path]` with [purpose]. + +## Files Created +- `app/[route]/page.tsx` +- `app/[route]/loading.tsx` +- `app/[route]/error.tsx` + +## Verification +- `bun run build` passed ✅ +``` diff --git a/.github/skills/brainstorming-ui-and-design/SKILL.md b/.github/skills/brainstorming-ui-and-design/SKILL.md new file mode 100644 index 0000000..65c2c51 --- /dev/null +++ b/.github/skills/brainstorming-ui-and-design/SKILL.md @@ -0,0 +1,43 @@ +--- +name: brainstorming-ui-and-design +description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics. +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. + +Refer to `.agent/skills/styling-ui/SKILL.md` for repo specific tailwind/css rules. + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. +- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. + +NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. + +Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. \ No newline at end of file diff --git a/.github/skills/creating-client-component/SKILL.md b/.github/skills/creating-client-component/SKILL.md new file mode 100644 index 0000000..e14253d --- /dev/null +++ b/.github/skills/creating-client-component/SKILL.md @@ -0,0 +1,162 @@ +--- +name: creating-client-component +description: | + Creates interactive React Client Components with hooks and event handlers. + Input: Component name, purpose, required interactivity. + Output: Client component file with proper structure and tests. +--- + +# Creating a Client Component + +Builds React Client Components for interactive UI. Only use when hooks/events are required. + +## Preconditions + +- Confirm interactivity is needed (hooks, events, browser APIs) +- If no interactivity needed, use Server Component instead + +## Decision: Client or Server? + +``` +Need any of these? +├─ useState, useEffect, useRef → Client +├─ onClick, onChange, onSubmit → Client +├─ useRouter, usePathname → Client +├─ useQuery, useMutation (Convex) → Client +├─ Browser APIs (localStorage, window) → Client +└─ None of above → Server Component (no 'use client') +``` + +## Algorithm + +```markdown +1. Create component file: + - [ ] Create `components/[feature]/[name].tsx` + - [ ] Add `'use client'` directive at top + - [ ] Define typed props interface + +2. Structure component: + - [ ] Import `cn` from `@/lib/utils` for class merging + - [ ] Use semantic tokens for styling + - [ ] Add `className` prop for overrides + +3. Add interactivity: + - [ ] Implement hooks (useState, etc.) + - [ ] Wire event handlers + - [ ] Handle loading/error states + +4. Create test: + - [ ] Create `[name].test.tsx` alongside + - [ ] Test user interactions, not implementation + +5. Verify: + - [ ] Run `bun run test [file]` + - [ ] Check TypeScript: `bun run build` +``` + +## Template + +```tsx +// components/[feature]/[name].tsx +'use client'; + +import { useState } from 'react'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; + +interface MyComponentProps { + initialValue?: string; + onSubmit?: (value: string) => void; + className?: string; +} + +export function MyComponent({ + initialValue = '', + onSubmit, + className, +}: MyComponentProps) { + const [value, setValue] = useState(initialValue); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async () => { + setIsLoading(true); + try { + onSubmit?.(value); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ setValue(e.target.value)} + className="px-3 py-2 border rounded bg-input" + /> + +
+ ); +} +``` + +## With Convex + +```tsx +'use client'; + +import { useQuery, useMutation } from 'convex/react'; +import { api } from '@/convex/_generated/api'; + +export function DataComponent() { + const data = useQuery(api.myTable.list); + const create = useMutation(api.myTable.create); + + if (!data) return
; + + return ( +
+ {data.map(item =>
{item.name}
)} + +
+ ); +} +``` + +## File Locations + +| Type | Location | +|------|----------| +| Feature components | `components/[feature]/[name].tsx` | +| Shared/generic | `components/[name].tsx` | +| Route-specific | `app/[route]/_components/[name].tsx` | + +## Guardrails + +- **Never** use `'use client'` unless actually needed +- **Never** call `fetch()` directly — use Convex or Server Actions +- **Never** mirror Convex data to local state +- **Always** include `className` prop for override flexibility +- **Always** use `cn()` for class merging + +## Output Format + +```markdown +## Summary +Created client component `[Name]` for [purpose]. + +## Files Created +- `components/[feature]/[name].tsx` +- `components/[feature]/[name].test.tsx` + +## Verification +- `bun run test [file]` passed ✅ +- No TypeScript errors ✅ + +## Props +- `initialValue?: string` — Default value +- `onSubmit?: (value: string) => void` — Callback +``` diff --git a/.github/skills/developing-nextjs/SKILL.md b/.github/skills/developing-nextjs/SKILL.md new file mode 100644 index 0000000..31c8e96 --- /dev/null +++ b/.github/skills/developing-nextjs/SKILL.md @@ -0,0 +1,62 @@ +--- +name: developing-nextjs +description: | + Creates pages, layouts, and server/client components in Next.js 16 App Router. + Use for: new routes, RSC/RCC decisions, Server Actions, loading/error states. + DO NOT use for: Convex mutations (use managing-convex), styling (use styling-ui). +--- + +# Developing with Next.js 16 + +Next.js 16 App Router overview. **Use the specific skills below for actual tasks.** + +## Skill Routing + +| Task | Use Skill | +|------|-----------| +| Create a new page/route | `adding-route` | +| Build interactive component | `creating-client-component` | +| Server-side form mutation | `writing-server-actions` | +| SSR with Convex reactivity | `preloading-convex-data` | +| Styling components | `styling-ui` | + +## Core Principles + +- **Server Components Default**: All components are Server Components unless marked `'use client'` +- **Client Leaves**: Use `'use client'` only for interactive leaves (hooks, events) +- **Strict Types**: No `any`. Explicitly type props. +- **Convex is Truth**: Don't mirror Convex data to local state + +## Decision Tree + +``` +Need data from Convex? +├─ Server Component → preloadQuery() → see `preloading-convex-data` +├─ Client-only reactive → useQuery() directly +└─ Non-reactive SSR → fetchQuery() + +Need interactivity? +├─ Yes → see `creating-client-component` +└─ No → Server Component (default) + +Need form mutation? +├─ Yes → see `writing-server-actions` +└─ No → Use what's appropriate +``` + +## Source of Truth + +| Concern | Location | +|---------|----------| +| Routes | `app/` | +| Shared Components | `components/` | +| UI Primitives | `components/ui/` | +| Providers | `components/providers/` | +| Layouts | `components/layout/` | + +## Guardrails + +- **Never** call `fetch()` directly — use Convex or Server Actions +- **Never** mirror Convex data to `useState` +- **Never** add `'use client'` unless hooks/events are needed +- **If build fails**, report error and stop diff --git a/.github/skills/generating-images/SKILL.md b/.github/skills/generating-images/SKILL.md new file mode 100644 index 0000000..c725d86 --- /dev/null +++ b/.github/skills/generating-images/SKILL.md @@ -0,0 +1,102 @@ +--- +name: generating-images +description: | + Generates images using Pollinations API. Validates model constraints and dimensions. + Use for: image generation logic, model selection, constraint validation. + DO NOT use for: video generation (use generating-videos), UI styling (use styling-ui). +--- + +# Generating Images + +Generates images via Pollinations API. All model config lives in `lib/config/models.ts`. + +## Source of Truth + +- **Model Registry**: `lib/config/models.ts` — READ THIS FIRST +- **Generation Processor**: `convex/singleGenerationProcessor.ts` +- **URL Building**: `convex/lib/pollinations.ts` +- **Resolution Tiers**: `lib/config/resolution-tiers.ts` +- **Standard Resolutions**: `lib/config/standard-resolutions.ts` + +## Current Image Models + +See `lib/config/models.ts` for current image models. + +## Workflow: Generate Image + +```markdown +- [ ] 1. Read `lib/config/models.ts` to confirm model exists and `type === "image"` +- [ ] 2. Validate dimensions: + - `width % step === 0` + - `height % step === 0` + - `width * height <= maxPixels` + - `width <= maxDimension && height <= maxDimension` +- [ ] 3. If model `supportsNegativePrompt === false`, do NOT send negative prompt param +- [ ] 4. Call `startGeneration` mutation in `convex/singleGeneration.ts` +- [ ] 5. Verify generation completes or report error +``` + +## Dimension Validation + +```typescript +// Example validation logic +function validateDimensions(model: ModelDefinition, width: number, height: number): boolean { + const { constraints } = model; + const pixels = width * height; + + return ( + width % constraints.step === 0 && + height % constraints.step === 0 && + pixels <= constraints.maxPixels && + width <= constraints.maxDimension && + height <= constraints.maxDimension && + width >= constraints.minDimension && + height >= constraints.minDimension + ); +} +``` + +## Error Handling + +| HTTP Code | Behavior | +|-----------|----------| +| 200 | Success | +| 400 | Fail immediately — invalid params | +| 429 | Retry with backoff (rate limit) | +| 5xx | Retry with backoff (server error) | + +Reference: `convex/lib/retry.ts` for retry logic. + +## Guardrails + +- **Never guess model IDs.** Always read `MODEL_REGISTRY` from `lib/config/models.ts`. +- **Never send unsupported params.** Check `supportsNegativePrompt` before including it. +- **If validation fails**, return clear error message. Don't attempt generation. +- **If model not found**, list available image models and ask user to choose. + +## Testing + +Reference files: +- `lib/config/models.test.ts` — Model validation tests +- `convex/lib/pollinations.test.ts` — API integration tests + +Run with: `bun run test lib/config/models.test.ts` + +## Output Format + +```markdown +## Summary +Generated [model] image at [width]x[height]. + +## Validation +- Model: `flux` ✅ +- Dimensions: 768x768 ✅ (step=8, max=768) +- Pixels: 589,824 ≤ 589,824 ✅ + +## Result +- Generation ID: `abc123` +- Status: completed ✅ + +## Errors (if any) +- [Error details] +``` diff --git a/.github/skills/generating-videos/SKILL.md b/.github/skills/generating-videos/SKILL.md new file mode 100644 index 0000000..3ee029d --- /dev/null +++ b/.github/skills/generating-videos/SKILL.md @@ -0,0 +1,123 @@ +--- +name: generating-videos +description: | + Generates videos using Pollinations API. Validates duration, audio, and interpolation capabilities. + Use for: video generation logic, model selection, duration constraints. + DO NOT use for: image generation (use generating-images), thumbnailing (handled automatically). +--- + +# Generating Videos + +Generates videos via Pollinations API. All model config lives in `lib/config/models.ts`. + +## Source of Truth + +- **Model Registry**: `lib/config/models.ts` — READ THIS FIRST +- **Generation Processor**: `convex/singleGenerationProcessor.ts` +- **Thumbnail Extraction**: `convex/lib/videoThumbnail.ts` + +## Current Video Models + +See `lib/config/models.ts` for current video models. + +## Model Capabilities + +```typescript +// From lib/config/models.ts +interface VideoModel { + supportsAudio?: boolean; // Can generate video with audio (Veo only) + supportsInterpolation?: boolean; // Supports first/last frame images (Veo only) + durationConstraints: { + min: number; // Minimum seconds + max: number; // Maximum seconds + fixedOptions?: number[]; // If set, only these values allowed + defaultDuration: number; // Default if not specified + }; +} +``` + +## Workflow: Generate Video + +```markdown +- [ ] 1. Read `lib/config/models.ts` to confirm model exists and `type === "video"` +- [ ] 2. Validate duration: + - `duration >= durationConstraints.min` + - `duration <= durationConstraints.max` + - If `fixedOptions` exists, `duration` must be in list +- [ ] 3. If `supportsAudio === false`, do NOT send `audio` param +- [ ] 4. If `supportsInterpolation === false`, do NOT send `lastFrameImage` param +- [ ] 5. Call `startGeneration` mutation in `convex/singleGeneration.ts` +- [ ] 6. Processor auto-generates thumbnail via `videoThumbnail.ts` +``` + +## Duration Validation + +```typescript +function validateDuration(model: ModelDefinition, duration: number): boolean { + const { durationConstraints } = model; + if (!durationConstraints) return false; + + // Check range + if (duration < durationConstraints.min || duration > durationConstraints.max) { + return false; + } + + // Check fixed options if applicable + if (durationConstraints.fixedOptions) { + return durationConstraints.fixedOptions.includes(duration); + } + + return true; +} +``` + +## Decision Tree + +``` +Model selected? +├─ veo → supports audio & interpolation +│ ├─ User wants audio? → Include audio=true +│ └─ User has reference images? → Include firstFrameImage, lastFrameImage +├─ seedance-pro → no audio, no interpolation +│ └─ Duration: 2-10s (any value) +└─ seedance → no audio, no interpolation + └─ Duration: 2-10s (any value) +``` + +## Guardrails + +- **Never guess model IDs.** Always read `MODEL_REGISTRY` from `lib/config/models.ts`. +- **Never send unsupported params.** Check `supportsAudio` before including `audio`. +- **Veo requires fixed durations.** Only 4, 6, or 8 seconds allowed. +- **Thumbnail is automatic.** Do not manually call thumbnail generation. +- **If duration invalid**, return error with valid range/options. + +## Testing + +Extend `convex/lib/pollinations.test.ts`: +- Verify `duration` only sent for video models +- Verify `audio` only sent when `supportsAudio === true` +- Mock ffmpeg when testing thumbnail extraction + +Run with: `bun run test convex/lib/pollinations.test.ts` + +## Output Format + +```markdown +## Summary +Generated [model] video, [duration]s. + +## Validation +- Model: `veo` ✅ +- Duration: 6s ✅ (fixed options: 4, 6, 8) +- Audio: requested ✅ (supported) +- Interpolation: not requested + +## Result +- Generation ID: `xyz789` +- Status: completed ✅ +- Thumbnail: generated ✅ + +## Errors (if any) +- [Error details] +``` diff --git a/.github/skills/managing-convex/SKILL.md b/.github/skills/managing-convex/SKILL.md new file mode 100644 index 0000000..2086106 --- /dev/null +++ b/.github/skills/managing-convex/SKILL.md @@ -0,0 +1,72 @@ +--- +name: managing-convex +description: | + Manages Convex backend: schema, queries, mutations, and actions. + Use for: database operations, server logic, real-time subscriptions. + DO NOT use for: frontend components (use developing-nextjs), API routes (use Next.js actions). +--- + +# Managing Convex + +Convex backend overview. **Use the specific skills below for actual tasks.** + +## Skill Routing + +| Task | Use Skill | +|------|-----------| +| Add a new database table | `adding-convex-table` | +| Write query/mutation/action | `writing-convex-functions` | +| Preload data for SSR | `preloading-convex-data` | + +## Source of Truth + +| Concern | Location | +|---------|----------| +| Schema | `convex/schema.ts` | +| Functions | `convex/*.ts` | +| Lib Utilities | `convex/lib/` | +| Generated Types | `convex/_generated/` | + +## Function Types + +| Type | Use Case | Access | +|------|----------|--------| +| `query` | Read data (reactive) | `ctx.db.query()` | +| `mutation` | Write data | `ctx.db.insert/patch/delete()` | +| `action` | External APIs | `fetch()`, `ctx.runQuery()` | + +## Current Tables + +| Table | Purpose | +|-------|---------| +| `users` | User accounts | +| `generatedImages` | Generation history | +| `favorites` | Saved images | +| `follows` | User follows | +| `promptLibrary` | Saved prompts | +| `referenceImages` | Uploaded references | + +## Lib Utilities + +| File | Purpose | +|------|---------| +| `convex/lib/pollinations.ts` | API URL building | +| `convex/lib/r2.ts` | Cloudflare R2 storage | +| `convex/lib/retry.ts` | Retry with backoff | +| `convex/lib/subscription.ts` | Subscription helpers | +| `convex/lib/videoThumbnail.ts` | FFmpeg thumbnails | + +## Auth Pattern + +```typescript +const identity = await ctx.auth.getUserIdentity(); +if (!identity) throw new Error("Unauthenticated"); +const userId = identity.subject; // Clerk user ID +``` + +## Guardrails + +- **Always index** fields used in queries +- **Always validate** args with `v.*` validators +- **Auth required** for user-scoped data +- **Actions can't access db** — use `ctx.runQuery/runMutation` diff --git a/.github/skills/preloading-convex-data/SKILL.md b/.github/skills/preloading-convex-data/SKILL.md new file mode 100644 index 0000000..ac04895 --- /dev/null +++ b/.github/skills/preloading-convex-data/SKILL.md @@ -0,0 +1,183 @@ +--- +name: preloading-convex-data +description: | + Preloads Convex data in Server Components for SSR with client-side reactivity. + Input: Query to preload and component that consumes it. + Output: Server Component with preloadQuery + Client Component with usePreloadedQuery. +--- + +# Preloading Convex Data + +Combines SSR performance with Convex real-time reactivity using `preloadQuery`. + +## When to Use + +| Scenario | Pattern | +|----------|---------| +| Page needs SEO + real-time updates | `preloadQuery` (this skill) | +| Client-only reactive data | `useQuery` directly | +| Static SSR, no reactivity | `fetchQuery` | +| Server mutation | `fetchMutation` | + +## Algorithm + +```markdown +1. Server Component (page): + - [ ] Import `preloadQuery` from `convex/nextjs` + - [ ] Import `api` from `@/convex/_generated/api` + - [ ] Call `await preloadQuery(api.table.query, args)` + - [ ] Pass preloaded data to Client Component + +2. Client Component: + - [ ] Add `'use client'` directive + - [ ] Import `usePreloadedQuery` from `convex/react` + - [ ] Import `Preloaded` type from `convex/react` + - [ ] Define prop type with `Preloaded` + - [ ] Call `usePreloadedQuery(preloadedData)` + +3. Verify: + - [ ] Check SSR: view source should have data + - [ ] Check reactivity: data updates in real-time +``` + +## Template: Server Component (Page) + +```tsx +// app/history/page.tsx +import { preloadQuery } from 'convex/nextjs'; +import { api } from '@/convex/_generated/api'; +import { HistoryClient } from '@/components/gallery/history-client'; + +export const metadata = { + title: 'History | Pixelstream', +}; + +export default async function HistoryPage() { + const preloadedImages = await preloadQuery( + api.generatedImages.listUserImages, + { limit: 50 } + ); + + return ( +
+

Your History

+ +
+ ); +} +``` + +## Template: Client Component + +```tsx +// components/gallery/history-client.tsx +'use client'; + +import { usePreloadedQuery } from 'convex/react'; +import type { Preloaded } from 'convex/react'; +import type { api } from '@/convex/_generated/api'; + +interface HistoryClientProps { + preloadedImages: Preloaded; +} + +export function HistoryClient({ preloadedImages }: HistoryClientProps) { + // Hydrates SSR data, then subscribes to real-time updates + const images = usePreloadedQuery(preloadedImages); + + if (!images || images.length === 0) { + return

No images yet.

; + } + + return ( +
+ {images.map((image) => ( +
+ {/* Render image */} +
+ ))} +
+ ); +} +``` + +## Common Queries (Reference) + +| Query | Args | Returns | +|-------|------|---------| +| `api.generatedImages.listUserImages` | `{ limit }` | User's images | +| `api.generatedImages.getPublicFeed` | `{ limit }` | Public feed | +| `api.favorites.list` | `{}` | User's favorites | +| `api.promptLibrary.list` | `{}` | Saved prompts | +| `api.users.getCurrent` | `{}` | Current user | + +## Important: Type Safety + +```tsx +// The type must match exactly +import type { Preloaded } from 'convex/react'; +import type { api } from '@/convex/_generated/api'; + +// Correct type annotation: +type Props = { + preloaded: Preloaded; +}; + +// NOT this (will error): +type Props = { + preloaded: any; // ❌ Never use any +}; +``` + +## Guardrails + +- **Never call `useQuery`** in client component when preloaded data is passed +- **Never use `any`** for preloaded types — always use `Preloaded` +- **Avoid multiple `preloadQuery`** calls on same page — consolidate or redesign +- **If query returns null**, handle loading state in client component +- **If SSR fails**, the page will error — add error.tsx boundary + +## Anti-Pattern: Don't Do This + +```tsx +// ❌ WRONG: Mirroring Convex data to local state +'use client'; + +export function BadComponent({ preloadedImages }) { + const images = usePreloadedQuery(preloadedImages); + const [localImages, setLocalImages] = useState(images); // ❌ Don't mirror! + + // ... +} +``` + +```tsx +// ✅ CORRECT: Use Convex data directly +'use client'; + +export function GoodComponent({ preloadedImages }) { + const images = usePreloadedQuery(preloadedImages); // ✅ Source of truth + + return
{images.map(...)}
; +} +``` + +## Output Format + +```markdown +## Summary +Added preloading for `[query]` on `[route]`. + +## Files Modified/Created +- `app/[route]/page.tsx` — Added preloadQuery +- `components/[name].tsx` — Created client consumer + +## Data Flow +1. Server: `preloadQuery(api.x.y, args)` +2. Client: `usePreloadedQuery(preloaded)` +3. Reactivity: Real-time updates work ✅ + +## Verification +- SSR works (view source has data) ✅ +- Real-time updates work ✅ +``` diff --git a/.github/skills/styling-ui/SKILL.md b/.github/skills/styling-ui/SKILL.md new file mode 100644 index 0000000..eeb4ee3 --- /dev/null +++ b/.github/skills/styling-ui/SKILL.md @@ -0,0 +1,171 @@ +--- +name: styling-ui +description: | + Enforces design system using Tailwind CSS v4 and shadcn/ui. + Use for: all styling, theming, responsive design, animations. + DO NOT use for: component logic (use developing-nextjs), data fetching (use managing-convex). +--- + +# Styling UI + +Tailwind CSS v4 + shadcn/ui design system. Semantic tokens are mandatory. + +## Source of Truth + +- **Globals + Tokens**: `app/globals.css` +- **UI Primitives**: `components/ui/` (shadcn/ui) +- **Class Merger**: `import { cn } from "@/lib/utils"` + +## Mandatory Standards + +| Requirement | Correct | Incorrect | +|-------------|---------|-----------| +| Class merging | `cn("base", className)` | `className={...}` only | +| Colors | `bg-background`, `text-foreground` | `bg-white`, `text-black` | +| Spacing | `gap-4`, `p-6` | `gap-[18px]`, `padding: 20px` | +| Fonts | `font-sans`, `font-brand`, `font-mono` | `font-family: Arial` | +| Responsive | `md:flex`, `lg:grid-cols-3` | Fixed widths | + +## Font System + +| Token | Font | Usage | +|-------|------|-------| +| `font-sans` | Geist | Default body/UI text | +| `font-brand` | Bricolage Grotesque | Headings, brand moments | +| `font-mono` | Geist Mono | Code, numbers, IDs | + +## Color Tokens (oklch-based) + +Use semantic tokens exclusively: + +```css +/* From app/globals.css */ +--background /* Page background */ +--foreground /* Primary text */ +--card /* Card surfaces */ +--muted /* Secondary backgrounds */ +--muted-foreground /* Secondary text */ +--primary /* Brand color (orange ember) */ +--accent /* Hover highlights */ +--destructive /* Error states */ +--border /* Borders */ +``` + +## Animation Utilities + +| Class | Effect | Duration | +|-------|--------|----------| +| `animate-fade-in` | Fade + slide up | 0.6s | +| `animate-scale-in` | Pop in | 0.3s | +| `animate-shimmer` | Loading shimmer | 2s loop | +| `animate-float` | Levitation | 6s loop | +| `animate-pulse-glow` | Attention glow | 3s loop | +| `animate-lightbulb-glow` | Subtle glow | 1.5s loop | +| `animate-loading-bar` | Progress bar | 1.5s loop | + +## Custom Utilities + +| Class | Purpose | +|-------|---------| +| `glass-effect` | Frosted glass panels (blur + transparency) | +| `glass-effect-home` | Dark glass for landing page | +| `container` | Centered content with responsive padding | + +## Workflow: Style Component + +```markdown +- [ ] 1. Import `cn` from `@/lib/utils` +- [ ] 2. Use semantic HTML (`
`, `
`, `
`) +- [ ] 3. Apply `font-sans` (default) or `font-brand` for headings +- [ ] 4. Use tokens: `bg-card`, `text-foreground`, `border-border` +- [ ] 5. Layout with `flex`/`grid` and `gap-*` +- [ ] 6. Add responsive: mobile-first, then `md:`, `lg:` overrides +- [ ] 7. Add interactivity: `hover:bg-muted/50 transition-colors cursor-pointer` +- [ ] 8. Polish: animations, `glass-effect` where appropriate +``` + +## Common Patterns + +### Card Component + +```tsx +
+ {children} +
+``` + +### Interactive Button + +```tsx + +``` + +### Responsive Grid + +```tsx +
+ {items.map(item => )} +
+``` + +### Glass Panel + +```tsx +
+ {/* Content with frosted glass background */} +
+``` + +## Breakpoints + +| Token | Width | Target | +|-------|-------|--------| +| `xs` | 480px | Mobile landscape | +| `sm` | 640px | Large mobile | +| `md` | 768px | Tablet | +| `lg` | 1024px | Laptop | +| `xl` | 1280px | Desktop | +| `2xl` | 1536px | Large desktop | +| `3xl` | 2000px | 1440p monitors | +| `4xl` | 2400px | 2K monitors | +| `5xl` | 3600px | 4K monitors | + +## Guardrails + +- **Never** use hardcoded colors (`#fff`, `rgb(...)`, `bg-gray-100`) +- **Never** use inline styles (`style={{ padding: 20 }}`) +- **Never** create custom CSS files — use `globals.css` utilities only +- **Always** use `cn()` for className props to enable overrides +- **Always** test mobile-first, then add `md:` / `lg:` variants + +## Output Format + +```markdown +## Summary +Styled [component] with [pattern]. + +## Tokens Used +- Colors: `bg-card`, `text-foreground` +- Spacing: `p-4`, `gap-2` +- Animation: `animate-fade-in` + +## Responsive +- Mobile: single column +- `md:`: 2 columns +- `lg:`: 3 columns + +## Verification +- Dark mode tested ✅ +- Mobile tested ✅ +``` diff --git a/.github/skills/testing-code/SKILL.md b/.github/skills/testing-code/SKILL.md new file mode 100644 index 0000000..dba289d --- /dev/null +++ b/.github/skills/testing-code/SKILL.md @@ -0,0 +1,62 @@ +--- +name: testing-code +description: | + Tests code using Vitest and React Testing Library. + Use for: unit tests, component tests, regression tests, bug fix verification. + DO NOT use for: E2E tests (not set up), visual testing. +--- + +# Testing Code + +Testing overview. **Use the specific skills below for actual tasks.** + +## Skill Routing + +| Task | Use Skill | +|------|-----------| +| Test React component | `testing-components` | +| Test pure function/utility | `testing-functions` | + +## Commands + +```bash +bun run test # Run all tests once +bun run test:watch # Watch mode +bun run test [pattern] # Run matching files +``` + +## File Location + +| Source | Test | +|--------|------| +| `components/foo.tsx` | `components/foo.test.tsx` | +| `lib/utils.ts` | `lib/utils.test.ts` | +| `convex/lib/x.ts` | `convex/lib/x.test.ts` | + +## Test Structure + +```typescript +import { describe, it, expect, vi } from "vitest"; + +describe("Feature", () => { + it("does something", () => { + expect(result).toBe(expected); + }); +}); +``` + +## Key Mocks + +| Dependency | Mock Pattern | +|------------|--------------| +| `next/navigation` | `useRouter`, `usePathname` | +| `convex/react` | `useQuery`, `useMutation` | +| `@clerk/nextjs` | `useUser`, `useAuth` | +| `next/image` | Replace with `` | + +## Guardrails + +- **Test behavior**, not implementation +- **Use accessible queries** — `getByRole` over `getByTestId` +- **Mock at boundaries** — hooks, not internals +- **If tests fail for unrelated reasons**, report and stop diff --git a/.github/skills/testing-components/SKILL.md b/.github/skills/testing-components/SKILL.md new file mode 100644 index 0000000..46e1c29 --- /dev/null +++ b/.github/skills/testing-components/SKILL.md @@ -0,0 +1,207 @@ +--- +name: testing-components +description: | + Tests React components with Vitest and React Testing Library. + Input: Component to test. + Output: Test file with render, interaction, and assertion tests. +--- + +# Testing Components + +Creates tests for React components focusing on user behavior, not implementation. + +## Preconditions + +- Component exists and is functional +- Testing Library is available (`@testing-library/react`) + +## Algorithm + +```markdown +1. Create test file: + - [ ] Create `[component].test.tsx` next to component + - [ ] Import component and testing utilities + +2. Mock dependencies: + - [ ] Mock Next.js navigation if used + - [ ] Mock Convex hooks if used + - [ ] Mock Clerk auth if used + +3. Write tests: + - [ ] Test initial render + - [ ] Test user interactions + - [ ] Test error/loading states + +4. Run and verify: + - [ ] Run `bun run test [file]` + - [ ] All tests pass +``` + +## Template: Basic Component Test + +```typescript +// components/ui/button.test.tsx +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import { Button } from "./button"; + +describe("Button", () => { + it("renders with text", () => { + render(); + expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument(); + }); + + it("calls onClick when clicked", () => { + const handleClick = vi.fn(); + render(); + + fireEvent.click(screen.getByRole("button")); + expect(handleClick).toHaveBeenCalledOnce(); + }); + + it("is disabled when disabled prop is true", () => { + render(); + expect(screen.getByRole("button")).toBeDisabled(); + }); + + it("applies custom className", () => { + render(); + expect(screen.getByRole("button")).toHaveClass("custom-class"); + }); +}); +``` + +## Mocking Patterns + +### Next.js Navigation + +```typescript +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + back: vi.fn(), + }), + usePathname: () => "/studio", + useSearchParams: () => new URLSearchParams("model=flux"), +})); +``` + +### Convex Hooks + +```typescript +import { useQuery, useMutation } from "convex/react"; + +vi.mock("convex/react", () => ({ + useQuery: vi.fn(), + useMutation: vi.fn(() => vi.fn()), + useConvex: vi.fn(), +})); + +// In test: +(useQuery as vi.Mock).mockReturnValue([ + { _id: "1", name: "Test Image" }, + { _id: "2", name: "Another Image" }, +]); +``` + +### Clerk Auth + +```typescript +vi.mock("@clerk/nextjs", () => ({ + useUser: () => ({ + user: { id: "user_123", firstName: "Test" }, + isLoaded: true, + isSignedIn: true, + }), + useAuth: () => ({ + isSignedIn: true, + userId: "user_123", + }), +})); +``` + +### Next/Image + +```typescript +vi.mock("next/image", () => ({ + default: ({ src, alt, ...props }: any) => ( + {alt} + ), +})); +``` + +## Query Selector Priority + +Use in this order (most accessible first): + +| Priority | Selector | Example | +|----------|----------|---------| +| 1 | `getByRole` | `getByRole("button", { name: /submit/i })` | +| 2 | `getByLabelText` | `getByLabelText(/email/i)` | +| 3 | `getByPlaceholderText` | `getByPlaceholderText(/search/i)` | +| 4 | `getByText` | `getByText(/loading/i)` | +| 5 | `getByTestId` | `getByTestId("custom-element")` (last resort) | + +## Async Testing + +```typescript +import { render, screen, waitFor } from "@testing-library/react"; + +it("shows data after loading", async () => { + render(); + + // Wait for element to appear + expect(await screen.findByText(/loaded/i)).toBeInTheDocument(); + + // Or use waitFor for custom assertions + await waitFor(() => { + expect(screen.getByRole("list")).toHaveAttribute("data-loaded", "true"); + }); +}); +``` + +## User Event (Preferred over fireEvent) + +```typescript +import userEvent from "@testing-library/user-event"; + +it("types in input", async () => { + const user = userEvent.setup(); + render(); + + const input = screen.getByRole("textbox"); + await user.type(input, "hello world"); + + expect(input).toHaveValue("hello world"); +}); +``` + +## Guardrails + +- **Test behavior, not implementation** — don't test internal state +- **Use accessible queries** — prefer `getByRole` over `getByTestId` +- **Mock at boundaries** — mock Convex hooks, not db internals +- **No arbitrary waits** — use `findBy*` or `waitFor` +- **If tests fail for unrelated reasons**, report and stop + +## Output Format + +```markdown +## Summary +Added tests for `[ComponentName]`. + +## Test Cases +- [x] renders correctly +- [x] handles click event +- [x] shows loading state +- [x] displays error message + +## Mocks Used +- `next/navigation` (useRouter) +- `convex/react` (useQuery) + +## Verification +- `bun run test [file]` passed ✅ +- [N] tests, [M] assertions +``` diff --git a/.github/skills/testing-functions/SKILL.md b/.github/skills/testing-functions/SKILL.md new file mode 100644 index 0000000..f347b1b --- /dev/null +++ b/.github/skills/testing-functions/SKILL.md @@ -0,0 +1,239 @@ +--- +name: testing-functions +description: | + Tests pure functions, utilities, and logic with Vitest. + Input: Function/module to test. + Output: Test file with unit tests for all cases. +--- + +# Testing Functions + +Creates unit tests for pure functions, utilities, and business logic. + +## Preconditions + +- Function/module exists +- Function is testable (ideally pure — same input → same output) + +## Algorithm + +```markdown +1. Create test file: + - [ ] Create `[filename].test.ts` next to source + - [ ] Import function(s) to test + +2. Identify test cases: + - [ ] Happy path (normal inputs) + - [ ] Edge cases (empty, null, boundaries) + - [ ] Error cases (invalid inputs) + +3. Write tests: + - [ ] Use descriptive `it()` names + - [ ] One assertion per test (usually) + - [ ] Group related tests in `describe()` + +4. Run and verify: + - [ ] Run `bun run test [file]` +``` + +## Template: Basic Function Test + +```typescript +// lib/utils/format.test.ts +import { describe, it, expect } from "vitest"; +import { formatBytes, formatDuration, truncate } from "./format"; + +describe("formatBytes", () => { + it("formats bytes correctly", () => { + expect(formatBytes(0)).toBe("0 B"); + expect(formatBytes(1024)).toBe("1 KB"); + expect(formatBytes(1048576)).toBe("1 MB"); + }); + + it("handles large values", () => { + expect(formatBytes(1073741824)).toBe("1 GB"); + }); + + it("rounds to 2 decimal places", () => { + expect(formatBytes(1536)).toBe("1.5 KB"); + }); +}); + +describe("truncate", () => { + it("returns string unchanged if under limit", () => { + expect(truncate("hello", 10)).toBe("hello"); + }); + + it("truncates with ellipsis", () => { + expect(truncate("hello world", 8)).toBe("hello..."); + }); + + it("handles empty string", () => { + expect(truncate("", 10)).toBe(""); + }); +}); +``` + +## Template: Validation Logic + +```typescript +// lib/config/models.test.ts +import { describe, it, expect } from "vitest"; +import { MODEL_REGISTRY, getModel, getModelConstraints } from "./models"; + +describe("MODEL_REGISTRY", () => { + it("contains expected image models", () => { + expect(MODEL_REGISTRY.flux).toBeDefined(); + expect(MODEL_REGISTRY.flux.type).toBe("image"); + }); + + it("contains expected video models", () => { + expect(MODEL_REGISTRY.veo).toBeDefined(); + expect(MODEL_REGISTRY.veo.type).toBe("video"); + }); + + it("all models have required fields", () => { + Object.values(MODEL_REGISTRY).forEach((model) => { + expect(model.id).toBeDefined(); + expect(model.displayName).toBeDefined(); + expect(model.type).toMatch(/^(image|video)$/); + expect(model.constraints).toBeDefined(); + }); + }); +}); + +describe("getModel", () => { + it("returns model by ID", () => { + const model = getModel("flux"); + expect(model?.displayName).toBe("Flux Schnell"); + }); + + it("returns undefined for unknown model", () => { + expect(getModel("unknown")).toBeUndefined(); + }); + + it("is case-insensitive", () => { + expect(getModel("FLUX")).toEqual(getModel("flux")); + }); +}); +``` + +## Template: Async Function + +```typescript +// lib/api/client.test.ts +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { fetchWithRetry } from "./client"; + +describe("fetchWithRetry", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns data on success", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ data: "test" }), + }); + + const result = await fetchWithRetry("/api/test"); + expect(result).toEqual({ data: "test" }); + }); + + it("retries on 5xx errors", async () => { + global.fetch = vi + .fn() + .mockResolvedValueOnce({ ok: false, status: 500 }) + .mockResolvedValueOnce({ ok: true, json: () => ({ data: "test" }) }); + + const result = await fetchWithRetry("/api/test"); + + expect(fetch).toHaveBeenCalledTimes(2); + expect(result).toEqual({ data: "test" }); + }); + + it("throws on 4xx errors without retry", async () => { + global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 400 }); + + await expect(fetchWithRetry("/api/test")).rejects.toThrow(); + expect(fetch).toHaveBeenCalledTimes(1); + }); +}); +``` + +## Existing Test Files (Reference) + +| File | Tests | +|------|-------| +| `lib/config/models.test.ts` | Model registry validation | +| `lib/config/resolution-tiers.test.ts` | Resolution calculations | +| `lib/config/standard-resolutions.test.ts` | Resolution presets | +| `convex/lib/pollinations.test.ts` | API URL building | +| `convex/lib/retry.test.ts` | Retry logic | +| `convex/lib/r2.test.ts` | Storage helpers | +| `convex/rateLimits.test.ts` | Rate limiting | + +## Test Patterns + +### Table-Driven Tests + +```typescript +it.each([ + [768, 768, true], // square within limit + [1024, 768, true], // landscape within limit + [2048, 2048, false], // exceeds max pixels +])("validates dimensions (%i x %i) = %s", (width, height, expected) => { + expect(isValidDimension(width, height)).toBe(expected); +}); +``` + +### Error Testing + +```typescript +it("throws on invalid input", () => { + expect(() => parseConfig(null)).toThrow("Config is required"); +}); + +// Async errors +it("rejects with error message", async () => { + await expect(fetchData("invalid")).rejects.toThrow(/not found/i); +}); +``` + +### Snapshot Testing + +```typescript +it("generates correct config", () => { + const config = buildConfig({ model: "flux", width: 768 }); + expect(config).toMatchSnapshot(); +}); +``` + +## Guardrails + +- **Test pure logic** — no DOM, no React +- **Isolated tests** — each test independent +- **Mock external deps** — fetch, timers, env vars +- **No implementation details** — test inputs/outputs +- **If tests fail for unrelated reasons**, report and stop + +## Output Format + +```markdown +## Summary +Added tests for `[module/function]`. + +## Test Cases +- [x] handles normal input +- [x] handles edge case: empty +- [x] handles edge case: max value +- [x] throws on invalid input + +## Coverage +- Functions tested: `formatBytes`, `truncate` +- Edge cases covered: empty, null, boundaries + +## Verification +- `bun run test [file]` passed ✅ +- [N] tests, [M] assertions +``` diff --git a/.github/skills/writing-and-authoring-skills/SKILL.md b/.github/skills/writing-and-authoring-skills/SKILL.md new file mode 100644 index 0000000..cb380df --- /dev/null +++ b/.github/skills/writing-and-authoring-skills/SKILL.md @@ -0,0 +1,240 @@ +They basically treat a `SKILL.md` as “how I, a senior, would reliably do X in this repo” written down in a way a model can’t misunderstand. + +Under the Agent Skills standard, a skill is a folder with a `SKILL.md` that packages metadata + instructions + optional scripts/resources, and the agent learns “when” and “how” to use it. ([Agent Skills][1]) Claude Code, Codex CLI, etc. then auto-load those skills and apply them whenever the description matches the task. ([OpenAI Developers][2]) + +Let’s talk about the mindset and then the concrete approach. + +--- + +## Mindset: how seniors think about skills + +### 1. Skills as “frozen workflows”, not magic prompts + +Senior devs don’t think “let’s give the agent more creativity.” They think: + +> “What exact steps do I follow when I do this manually, and how can I encode that so the agent can’t skip or improvise the dangerous parts?” + +So each skill is: + +* **Narrow** – “Add a REST endpoint in service X” rather than “build backend stuff”. +* **Deterministic** – same inputs → same general flow. +* **Opinionated** – it encodes team conventions, not generic best practices. + +This matches your observation: they already have strong priors on what “correct” looks like, so the skill just automates that workflow. + +--- + +### 2. Skills as extensions of *their* judgment, not a replacement + +They assume: + +* *The model is good at pattern matching + glue code.* +* *The human is still the arbiter of architecture, tradeoffs, and ‘is this actually safe?’* + +So the skill: + +* **Pushes verification work onto the agent** (`rg`, tests, linters, typechecks, reading related files). +* **Surfaces artifacts for the human** (summaries, diffs, checklists) so review is fast and high-signal. + +The senior’s job becomes designing the skill so that, when they skim the output, it already feels like something they might have written. + +--- + +### 3. Skills as small, composable “lego blocks” + +Modern agent stacks treat skills as modular capabilities: each skill is a specialist (e.g. “write tests for an existing change”, “migrate a DB column”, “update API docs”). ([Claude Developer Platform][3]) + +With 10–20 terminals going, you’re usually seeing some combination of: + +* Different **repos/services** each with their own local skills. +* Different **modes** in the same repo – exploration, refactor, test-writing, PR-prepping, migration, etc. +* Agents that **chain skills** (e.g. “code search skill → refactor skill → test skill → PR skill”). + +Seniors design skills assuming they’ll be composed like this, so each one has a clear contract and narrow surface area. + +--- + +## Approach: how to actually craft a SKILL.md + +The Agent Skills spec itself pushes you toward “progressive disclosure”: short metadata; more detailed instructions when the skill is activated; deeper resources/scripts only when needed. ([Agent Skills][1]) Good authors lean into that. + +Here’s how they typically approach it. + +--- + +### Step 1: Start from a real workflow you already do well + +Pick something you do often and consistently: + +* “Add a new feature flag.” +* “Add a REST endpoint and wire it through to the frontend.” +* “Fix a bug and add regression tests.” +* “Do a PR review using our standards.” + +Then ask: *If I had to explain this to a sharp but junior dev so they can’t mess it up, what would I write?* +That text is your raw material for `SKILL.md`. + +--- + +### Step 2: Define a crisp contract in the metadata + +In the frontmatter / metadata block, good skills are ruthless about: + +* **Name** – specific, action-oriented (e.g. `create-rest-endpoint`, `typescript-api-refactor`). +* **Description / when to use** – 2–3 lines that let the agent route correctly: + + * What it does. + * What it *doesn’t* do. + * Key constraints (language, framework, repo assumptions). +* **Inputs & outputs** – what the agent should expect and what it must produce: + + * “Input: a description of the new endpoint and existing route file.” + * “Output: code changes applied + updated tests + brief summary.” + +This makes the skill easy for the agent to discover and for your future self to understand. + +--- + +### Step 3: Encode your algorithm, not vibes + +In the `SKILL.md` body, seniors write an **algorithm**, not just “best practices”: + +Instead of: + +> “Follow REST best practices and add tests.” + +They write something closer to: + +1. Locate the main router file in `src/api/routes/*.ts`. +2. Identify the module that matches the feature area. If unclear, ask the user which module to extend before continuing. +3. Add a new route handler that: + + * Accepts `X` and `Y` parameters. + * Validates inputs using `zod` schemas from `src/api/schemas`. + * Calls the existing service function or creates a new one if needed. +4. Update or create tests: + + * Use `jest` in `__tests__/api`. + * For new handlers, add at least one happy-path and one failure-path test. +5. Run `npm test -- ` and include the output. If tests fail, fix them before returning. +6. In your final response: + + * Summarize the changes. + * Paste the relevant diffs. + * Note any TODOs or follow-up questions. + +Key patterns: + +* **Checklists** – reduce ambiguity. +* **Conditionals** – “if schema doesn’t exist, create it here”. +* **Explicit commands** – tell the agent what shell commands / tools to run. +* **Forced verification** – tests, builds, searches must be run, not “suggested”. + +You’re basically teaching the agent your **internal SOP**. + +--- + +### Step 4: Bake in codebase-specific context + +Skills shine when they encode things a generic model doesn’t know: + +* **Directory structure & naming** – “React components live in `src/components`, tests in `src/components/__tests__`.” +* **Conventions** – “Use our `useApi` hook, don’t call `fetch` directly.” +* **Safety rules** – “Never write raw SQL; always use the query builder in `db/queries.ts`.” +* **Edge cases** – “This service is multi-tenant; never assume a single org. Always pass `tenantId` through.” + +This is where high-skill engineers get a lot of leverage: they know the sharp edges and encode that knowledge so the agent avoids them. + +--- + +### Step 5: Guardrails: when *not* to use the skill & how to fail + +Senior authors are explicit about **non-goals** and failure modes: + +* “Do not use this skill for database schema changes; use `db-migration.skill` instead.” +* “If the project doesn’t appear to be TypeScript/React, stop and ask the user.” +* “If running tests or `npm run lint` fails for reasons unrelated to your changes, report that and stop instead of trying to fix the whole repo.” + +This stops the model from over-extending and doing heroic (dangerous) things. + +--- + +### Step 6: Specify output format for fast human review + +They optimize the skill for **their own review loop**: + +* Structured final output: + + * `## Summary` + * `## Files changed` + * `## Diffs` + * `## Tests` +* Clear markers of confidence: + + * “All tests passed ✅” vs. “Tests failing ❌ – needs human follow-up.” +* Call out open questions, TODOs, and assumptions. + +Remember your earlier point: seniors are fast at verification. A good skill hands them exactly the evidence they need. + +--- + +### Step 7: Iterate like code, not like a one-off prompt + +The good ones treat skills as **versioned artifacts**: + +* They run the skill in real work. +* When the agent misbehaves or gets stuck, they: + + * Add a clarifying step. + * Tighten a heuristic (“always search for existing examples before creating a new pattern”). + * Split a too-broad skill into two narrower ones. + +The SKILL.md becomes a living document of what’s currently “the right way” to do a task in that codebase. + +--- + +## How this plays with 10–20 terminals and deep stacks + +When you see someone with an army of terminals, imagine something like: + +* Terminal 1-3: repo A (backend), with: + + * `bugfix.skill` + * `api-endpoint.skill` + * `db-migration.skill` +* Terminal 4-6: repo B (frontend), with: + + * `ui-component.skill` + * `accessibility-review.skill` + * `storybook-docs.skill` +* Terminal 7-10: cross-cutting: + + * `pr-review.skill` + * `commit-message.skill` + * `changelog-update.skill` + * `perf-investigation.skill` + +Each skill is **surgical**. The senior knows which combination to invoke for a given problem, and the skills themselves encode how to interact with tests, linters, and the project’s layout. + +--- + +## A short mindset checklist when you’re writing a skill + +When you sit down to craft a SKILL.md, think: + +1. **What narrow workflow am I capturing?** + If it’s more than one, split it. +2. **What must be true before/after?** + Preconditions, invariants, tests. +3. **What exact steps do I follow?** + Turn that into an algorithm, not prose advice. +4. **What repo-specific traps can I prevent?** + Hard-won tribal knowledge goes here. +5. **What should the agent show me at the end so I can say “yes” in 10–20 seconds?** + Shape the output around your review. + +That’s the senior-engineer mindset: skills as distilled, reusable engineering judgment. The agents then just become really fast, really obedient juniors running those playbooks for you. + +[1]: https://agentskills.io/specification?utm_source=chatgpt.com "Specification - Agent Skills" +[2]: https://developers.openai.com/codex/skills?utm_source=chatgpt.com "Agent Skills - developers.openai.com" +[3]: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview?utm_source=chatgpt.com "Agent Skills - Claude Docs" diff --git a/.github/skills/writing-convex-functions/SKILL.md b/.github/skills/writing-convex-functions/SKILL.md new file mode 100644 index 0000000..3453735 --- /dev/null +++ b/.github/skills/writing-convex-functions/SKILL.md @@ -0,0 +1,235 @@ +--- +name: writing-convex-functions +description: | + Creates Convex queries, mutations, or actions for database operations. + Input: Function type (query/mutation/action), table, and operation. + Output: Typed Convex function with args validation. +--- + +# Writing Convex Functions + +Creates queries (read), mutations (write), or actions (external APIs) in Convex. + +## Preconditions + +- Table exists in `convex/schema.ts` +- User has provided: function type, table name, operation purpose + +## Decision: Which Type? + +| Need | Type | Can Access | +|------|------|------------| +| Read data (reactive) | `query` | `ctx.db.query()`, `ctx.db.get()` | +| Write data | `mutation` | `ctx.db.insert/patch/delete()` | +| Call external API | `action` | `fetch()`, but NOT `ctx.db` directly | +| Scheduled job | `action` | Use with `ctx.scheduler` | + +## Algorithm + +```markdown +1. Create or open function file: + - [ ] File: `convex/[tableName].ts` (or existing file) + - [ ] Import `query`, `mutation`, or `action` from `./_generated/server` + +2. Define function: + - [ ] Add `args` with `v.*` validators + - [ ] Implement `handler` with correct return type + - [ ] Check auth if user-specific + +3. Export function: + - [ ] Use descriptive name: `get`, `list`, `create`, `update`, `delete` + - [ ] Named export for client access + +4. Verify: + - [ ] Run `bun run build` to check types + - [ ] Test in Convex dashboard +``` + +## Template: Query + +```typescript +// convex/users.ts +import { query } from "./_generated/server"; +import { v } from "convex/values"; + +// Get single record by ID +export const get = query({ + args: { id: v.id("users") }, + handler: async (ctx, args) => { + return await ctx.db.get(args.id); + }, +}); + +// List with filter +export const listByStatus = query({ + args: { status: v.string() }, + handler: async (ctx, args) => { + return await ctx.db + .query("generatedImages") + .withIndex("by_status", (q) => q.eq("status", args.status)) + .order("desc") + .take(50); + }, +}); + +// User-scoped query +export const listMine = query({ + args: {}, + handler: async (ctx) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) return []; + + return await ctx.db + .query("generatedImages") + .withIndex("by_user", (q) => q.eq("userId", identity.subject)) + .order("desc") + .collect(); + }, +}); +``` + +## Template: Mutation + +```typescript +// convex/generatedImages.ts +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +// Create record +export const create = mutation({ + args: { + prompt: v.string(), + model: v.string(), + width: v.number(), + height: v.number(), + }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) throw new Error("Unauthenticated"); + + return await ctx.db.insert("generatedImages", { + ...args, + userId: identity.subject, + status: "pending", + createdAt: Date.now(), + }); + }, +}); + +// Update record +export const updateStatus = mutation({ + args: { + id: v.id("generatedImages"), + status: v.union(v.literal("completed"), v.literal("failed")), + }, + handler: async (ctx, args) => { + await ctx.db.patch(args.id, { + status: args.status, + updatedAt: Date.now(), + }); + }, +}); + +// Delete record +export const remove = mutation({ + args: { id: v.id("generatedImages") }, + handler: async (ctx, args) => { + const identity = await ctx.auth.getUserIdentity(); + if (!identity) throw new Error("Unauthenticated"); + + const record = await ctx.db.get(args.id); + if (!record || record.userId !== identity.subject) { + throw new Error("Not found or unauthorized"); + } + + await ctx.db.delete(args.id); + }, +}); +``` + +## Template: Action + +```typescript +// convex/singleGenerationProcessor.ts +import { action } from "./_generated/server"; +import { v } from "convex/values"; +import { api } from "./_generated/api"; + +export const processGeneration = action({ + args: { generationId: v.id("generatedImages") }, + handler: async (ctx, args) => { + // 1. Get data via runQuery (actions can't access db directly) + const generation = await ctx.runQuery(api.generatedImages.get, { + id: args.generationId, + }); + if (!generation) throw new Error("Not found"); + + // 2. Call external API + const response = await fetch("https://api.pollinations.ai/..."); + const result = await response.json(); + + // 3. Update via runMutation + await ctx.runMutation(api.generatedImages.updateStatus, { + id: args.generationId, + status: "completed", + }); + + return result; + }, +}); +``` + +## Auth Pattern + +```typescript +// Always check auth for user-specific operations +const identity = await ctx.auth.getUserIdentity(); +if (!identity) { + throw new Error("Unauthenticated"); +} +const userId = identity.subject; // Clerk user ID +``` + +## Lib Utilities + +| File | Purpose | +|------|---------| +| `convex/lib/pollinations.ts` | Build API URLs | +| `convex/lib/r2.ts` | Cloudflare R2 storage | +| `convex/lib/retry.ts` | Retry with backoff | +| `convex/lib/subscription.ts` | Subscription checks | + +## Guardrails + +- **Always validate args** with `v.*` validators +- **Always check auth** for user-scoped data +- **Never use `any`** in args or return types +- **Actions can't access `ctx.db`** — use `ctx.runQuery`/`ctx.runMutation` +- **If index doesn't exist**, add it in schema first + +## Output Format + +```markdown +## Summary +Added `[functionType]` `[name]` for [purpose]. + +## Function +```typescript +export const myFunction = query({ + args: { ... }, + handler: async (ctx, args) => { ... }, +}); +``` + +## Usage (Client) +```tsx +const data = useQuery(api.table.myFunction, { arg: value }); +``` + +## Verification +- Types check ✅ +- Tested in dashboard ✅ + +## Related +- Index used: `by_field` ✅ +``` diff --git a/.github/skills/writing-server-actions/SKILL.md b/.github/skills/writing-server-actions/SKILL.md new file mode 100644 index 0000000..fce4d8d --- /dev/null +++ b/.github/skills/writing-server-actions/SKILL.md @@ -0,0 +1,214 @@ +--- +name: writing-server-actions +description: | + Creates Next.js Server Actions for form mutations and server-side operations. + Input: Action purpose and input schema. + Output: Server action file with validation and error handling. +--- + +# Writing Server Actions + +Creates Next.js Server Actions for mutations from forms or client code. + +## Preconditions + +- Action involves data mutation (create, update, delete) +- Data validation is required (use Zod) + +## When to Use + +| Scenario | Use | +|----------|-----| +| Form submission | Server Action | +| Calling Convex from server | Server Action + `fetchMutation` | +| External API call from server | Server Action | +| Real-time client data | `useMutation` (Convex) instead | + +## Algorithm + +```markdown +1. Create action file: + - [ ] Create `app/actions/[name].ts` + - [ ] Add `'use server'` directive at top + +2. Define schema: + - [ ] Use Zod for input validation + - [ ] Handle both FormData and direct args + +3. Implement handler: + - [ ] Parse and validate input + - [ ] Check auth if needed + - [ ] Perform mutation (Convex/external) + - [ ] Return typed result + +4. Connect to form: + - [ ] Use `action={serverAction}` on form + - [ ] Or call via `startTransition` + +5. Verify: + - [ ] Test with form submission + - [ ] Check error handling +``` + +## Template: Basic Action + +```ts +// app/actions/save-profile.ts +'use server'; + +import { z } from 'zod'; +import { auth } from '@clerk/nextjs/server'; + +const schema = z.object({ + name: z.string().min(2, 'Name must be at least 2 characters'), + bio: z.string().max(500).optional(), +}); + +export type SaveProfileResult = + | { success: true; message: string } + | { success: false; error: string }; + +export async function saveProfile(formData: FormData): Promise { + // 1. Check auth + const { userId } = await auth(); + if (!userId) { + return { success: false, error: 'Unauthorized' }; + } + + // 2. Validate input + const parsed = schema.safeParse({ + name: formData.get('name'), + bio: formData.get('bio'), + }); + + if (!parsed.success) { + return { success: false, error: parsed.error.errors[0].message }; + } + + // 3. Perform mutation + try { + // Call Convex or database here + return { success: true, message: 'Profile saved' }; + } catch (error) { + return { success: false, error: 'Failed to save profile' }; + } +} +``` + +## Template: With Convex + +```ts +// app/actions/create-prompt.ts +'use server'; + +import { z } from 'zod'; +import { fetchMutation } from 'convex/nextjs'; +import { api } from '@/convex/_generated/api'; +import { auth } from '@clerk/nextjs/server'; + +const schema = z.object({ + prompt: z.string().min(1).max(2000), + title: z.string().min(1).max(100), +}); + +export async function createPrompt(formData: FormData) { + const { userId } = await auth(); + if (!userId) throw new Error('Unauthorized'); + + const parsed = schema.safeParse({ + prompt: formData.get('prompt'), + title: formData.get('title'), + }); + + if (!parsed.success) { + return { error: parsed.error.flatten() }; + } + + const id = await fetchMutation(api.promptLibrary.create, { + prompt: parsed.data.prompt, + title: parsed.data.title, + }); + + return { success: true, id }; +} +``` + +## Client Usage + +### In Form + +```tsx +// components/profile-form.tsx +'use client'; + +import { useActionState } from 'react'; +import { saveProfile } from '@/app/actions/save-profile'; + +export function ProfileForm() { + const [state, formAction, isPending] = useActionState(saveProfile, null); + + return ( +
+ + {state?.error &&

{state.error}

} + +
+ ); +} +``` + +### Programmatic + +```tsx +'use client'; + +import { useTransition } from 'react'; +import { createPrompt } from '@/app/actions/create-prompt'; + +export function SaveButton() { + const [isPending, startTransition] = useTransition(); + + const handleClick = () => { + startTransition(async () => { + const formData = new FormData(); + formData.set('prompt', 'My prompt'); + formData.set('title', 'My title'); + await createPrompt(formData); + }); + }; + + return ; +} +``` + +## Guardrails + +- **Always validate** input with Zod before processing +- **Always check auth** for user-specific actions +- **Never expose raw errors** to client — sanitize messages +- **Return typed results** — not void or any +- **If Convex needed**, use `fetchMutation` not direct db access + +## Output Format + +```markdown +## Summary +Created server action `[name]` for [purpose]. + +## Files Created +- `app/actions/[name].ts` + +## Schema +- `name: string` (required, min 2) +- `bio: string` (optional, max 500) + +## Returns +- `{ success: true, message: string }` on success +- `{ success: false, error: string }` on failure + +## Usage +- Form: `
` +- Programmatic: `startTransition(() => action(formData))` +``` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a0200f0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,137 @@ +--- +--- + +# .agent — Development Guidelines + +## Stack + +- Next.js (App Router, latest stable patch) +- React 19 +- TypeScript (strict) +- Bun (package manager/runtime) +- Convex +- shadcn/ui + Tailwind CSS (v4) +- Radix UI +- TanStack Query +- Clerk +- Zod +- Vitest + React Testing Library + +--- + +## Tooling & Commands + +- Dev: `bun run dev` +- Build: `bun run build` (runs lint, then `next build`) +- Start: `bun run start` +- Lint: `bun run lint` / `bun run lint:fix` +- Typecheck: `bun run typecheck` +- Tests: `bun run test` / `bun run test:watch` / `bun run test:coverage` + +--- + +## Defaults + +- **Server Components by default.** Add `'use client'` only at the leaves that need interactivity/hooks. +- **Convex is the source of truth** for server state; local state is for UI-only concerns. +- **Always leverage typescript**; avoid usage of `any` which defeats the point of using Typescript. +- **Ensure** new or updated code includes idiomatic Vitest + RTL tests. + +--- + +## Components & Composition + +- Prefer **small, focused components**. Split when a component mixes concerns or becomes hard to scan. +- **Presentational components:** props in → JSX out (no data fetching; minimal/no app logic). +- **Feature components:** coordinate data + state + composition. +- Prefer **composition** (children, slots) over giant prop APIs. + +--- + +## State & Side Effects + +- Keep state **closest to where it’s used**; lift only for shared ownership. +- Don’t mirror Convex query results into local state. Store **UI state** (selection, filters, draft text), not server data. +- Avoid `useEffect` for app data fetching. Use it for **browser-only side effects** (subscriptions, observers, localStorage). + +--- + +## Convex + Next.js (Best Practice Patterns) + +- **Client reactivity:** `useQuery` / `useMutation` in Client Components. +- **SSR + reactivity:** in a Server Component, `preloadQuery(...)` and pass the payload to a Client Component using `usePreloadedQuery(...)`. +- **Server-only (non-reactive) rendering:** `fetchQuery(...)` in Server Components. +- **Server Actions / Route Handlers:** call Convex via `fetchMutation` / `fetchAction` when you must mutate from the server boundary (e.g., form action, webhook, route handler). +- **Optimistic UI:** prefer Convex optimistic updates (`useMutation(...).withOptimisticUpdate(...)`) over duplicating server logic in ad-hoc local state. +- **Consistency:** avoid multiple `preloadQuery` calls on the same page; consolidate queries or design around a single preload. + +--- + +## Next.js App Router Conventions + +- Use `loading.tsx` and `error.tsx` for route-segment UX. +- Use `not-found.tsx` for 404 states. +- Prefer colocating components with the route/feature that owns them; extract to shared only when reused. + +--- + +## React 19 Usage + +- Prefer **Actions + `useActionState`** for form submission state when using form actions. +- Use `useOptimistic` for instant UI feedback when appropriate. +- Use `useFormStatus` inside design-system components that need form pending state. +- `use` can read a Promise/Context during render (only where Suspense semantics make sense). + +--- + +## TypeScript (Strict, Practical) + +- Let inference work; **export explicit prop types** for shared/public components. +- No `any`. Use `unknown` and narrow. +- Avoid `as` casts; prefer `satisfies`, generics, and runtime validation. +- `@ts-expect-error` allowed only with a comment explaining why + link/TODO to remove. + +--- + +## Code Style & Quality (ESLint) + +- No `any`, unsafe assignments/calls/returns, or floating promises. +- Keep modules under 400 lines (tests up to 600 lines). +- Complexity warning at 20, max depth 5, max params 8. +- `no-console` (allow `warn`/`error`), `no-debugger`. +- Prefer `const`, avoid `var`, favor optional chaining and nullish coalescing. + +--- + +## Imports & Naming + +- Use `@/` path aliases for app imports when possible. +- Keep files and folders in kebab-case. +- Use PascalCase for React components and TypeScript types. +- Prefix hooks with `use`. +- Use UPPER_SNAKE_CASE for module-level constants. + +--- + +## Error Handling + +- Prefer typed errors from `lib/errors` (e.g., `PollinationsApiError`, `getErrorMessage`, `isApiError`). +- Validate external data with Zod schemas in `lib/schemas`. +- Use toast helpers in `lib/errors/toast-errors` for user-facing failures. + +--- + +## Performance + +- Minimize client bundles: keep heavy logic/components server-side when possible. +- Use dynamic imports for non-critical client UI. +- Use `next/image` for images. +- Memoization is opt-in: use `memo`/`useMemo`/`useCallback` only when profiling shows value. + +--- + +## Styling (shadcn/ui + Tailwind) + +- Use shadcn/ui as the base; extend via composition + variants. +- Tailwind utilities in JSX; extract repetition into components, not `@apply`. +- Theme via CSS variables; keep design tokens centralized. diff --git a/LINT_STATUS.md b/LINT_STATUS.md new file mode 100644 index 0000000..2f863e0 --- /dev/null +++ b/LINT_STATUS.md @@ -0,0 +1,80 @@ +# ESLint Configuration & Fixes Summary + +## Configuration Changes + +### Strict Rules Added +- `@typescript-eslint/no-explicit-any`: error +- `@typescript-eslint/no-unsafe-assignment`: error +- `@typescript-eslint/no-unsafe-member-access`: error +- `@typescript-eslint/no-unsafe-call`: error +- `@typescript-eslint/no-unsafe-return`: error +- `@typescript-eslint/no-floating-promises`: error +- `@typescript-eslint/no-misused-promises`: error +- `@typescript-eslint/switch-exhaustiveness-check`: error +- `max-lines`: 400 (600 for tests) +- `complexity`: warn at 20 +- `max-depth`: warn at 5 +- `max-params`: error at 8 + +### Files Fixed (Production Code) + +#### API Routes +- ✅ `app/api/images/delete-bulk/route.ts` - Typed error handling (`unknown`) +- ✅ `app/api/images/delete/route.ts` - Typed error handling (`unknown`) + +#### Components +- ✅ `app/pricing/checkout-button.tsx` - Typed error, wrapped async onClick with `void` +- ✅ `components/ui/image-card.tsx` - Wrapped async onClick handlers with `void` +- ✅ `components/ui/masonry-grid.tsx` - Added proper type guards for image props +- ✅ `components/ui/media-player.tsx` - Wrapped async onClick with `void` + +## Remaining Issues + +### Test Files (1200+ errors) +Most errors are in test files due to: +- Mock functions returning `any` from vitest +- Type assertions needed for test contexts +- `@ts-expect-error` directives needing descriptions + +**Recommendation**: These are acceptable in tests. Consider adding per-file `eslint-disable` comments for specific rules in test files if needed, but the strict config encourages proper typing even in tests. + +### Convex Backend Files (200+ errors) +Issues in `convex/` directory: +- API response parsing without proper type guards +- Error handling with `any` types +- Files exceeding 400 lines (need splitting) +- Complex functions exceeding complexity limits + +**Files needing attention**: +- `convex/batchGeneration.ts` (449 lines, unsafe any) +- `convex/batchProcessor.ts` (unsafe member access on API responses) +- `convex/generatedImages.ts` (803 lines - needs splitting) +- `convex/lib/promptInference.ts` (unsafe any in AI response parsing) +- `convex/lib/providerHealth.ts` (complexity 25, unsafe any) + +### Large Files Needing Refactoring +- `components/ui/sidebar.tsx` (693 lines) +- `lib/config/models.ts` (520 lines) +- `lib/seo-config.ts` (933 lines) +- `lib/errors/pollinations-error.test.ts` (621 lines) +- `lib/schemas/pollinations.schema.test.ts` (464 lines) + +## Best Practices Applied + +1. **Error Handling**: Changed `catch (error)` to `catch (error: unknown)` for type safety +2. **Async Event Handlers**: Wrapped with `void` operator: `onClick={() => void asyncFn()}` +3. **Type Guards**: Added proper type guards instead of type assertions +4. **No Test Leniency**: Maintained strict rules for tests to encourage proper typing + +## Next Steps + +1. **Convex Files**: Add proper type definitions for API responses +2. **Large Files**: Split into smaller, focused modules +3. **Complex Functions**: Refactor to reduce cyclomatic complexity +4. **Test Files**: Add proper types for mocks or use `eslint-disable-next-line` with justification + +## Impact + +- Errors reduced from 2972 to 1448 (51% reduction) +- All production UI/API code now type-safe +- Remaining errors are in backend/test code that needs systematic refactoring diff --git a/build-errors.md b/build-errors.md new file mode 100644 index 0000000..5c0efba --- /dev/null +++ b/build-errors.md @@ -0,0 +1,291 @@ +21:30:01.482 52:60 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.482 104:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.482 121:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.482 7:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.482 8:9 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element +21:30:01.483 18:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.484 7:54 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.484 18:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.484 22:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.485 7:38 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.485 10:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.485 14:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.486 7:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.486 8:38 warning 'asChild' is defined but never used @typescript-eslint/no-unused-vars +21:30:01.486 8:49 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.486 9:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.487 10:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.487 11:38 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.487 12:44 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.487 13:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.487 14:49 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.487 15:58 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.488 20:38 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.488 7:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.488 10:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.489 7:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.489 7:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.489 8:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.489 9:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.490 14:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.490 17:23 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.490 20:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.491 16:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.492 7:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.492 8:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.492 9:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.493 10:37 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.493 7:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.494 7:30 warning Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element +21:30:01.494 7:30 warning img elements must have an alt prop, either with meaningful text, or an empty string for decorative images jsx-a11y/alt-text +21:30:01.494 12:22 warning 'totalSlides' is defined but never used @typescript-eslint/no-unused-vars +21:30:01.494 12:37 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.495 7:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.495 12:27 warning 'totalSlides' is defined but never used @typescript-eslint/no-unused-vars +21:30:01.495 12:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.496 7:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.496 51:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.496 63:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.496 96:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.496 97:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.496 147:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.496 148:41 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.497 165:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.497 166:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.497 183:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.497 184:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.497 207:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.497 208:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.497 228:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.497 229:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.498 244:55 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.498 269:36 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.498 270:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.498 6:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.498 7:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.499 11:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.499 37:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.499 46:71 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.499 54:79 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.499 64:79 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.499 77:84 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.499 84:84 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.499 91:84 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.500 161:91 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.500 180:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.500 6:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.500 7:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.501 11:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.501 33:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.501 6:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.501 7:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.501 8:32 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.502 9:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.502 25:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.502 74:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.502 88:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.502 118:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.502 133:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.502 134:78 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.502 141:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.503 142:78 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.503 152:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.503 153:70 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.503 157:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.503 158:70 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.503 165:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.503 183:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.503 200:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.504 201:69 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.504 210:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.504 211:76 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.504 226:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.504 227:77 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.504 244:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.504 246:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.505 262:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.505 264:38 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.505 287:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.505 310:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.505 312:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.505 32:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.506 65:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.506 66:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.506 3:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.506 4:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.506 5:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.507 6:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.507 30:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.507 31:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.507 48:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.507 49:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.507 58:44 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.507 59:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.508 69:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.508 71:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.508 80:56 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.508 82:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.508 99:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.508 112:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.508 127:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.509 146:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.509 154:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.509 17:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.511 30:52 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.511 30:76 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.511 42:11 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.511 73:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.511 92:74 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.511 92:98 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.511 110:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.512 11:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.512 22:54 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.512 55:71 error The `Function` type accepts any function-like value. +21:30:01.513 13:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.513 42:60 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.513 44:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.513 52:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.514 6:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.514 10:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.514 25:54 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.514 5:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.514 29:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.514 47:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.515 48:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.515 50:70 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.515 72:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.515 73:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.515 75:70 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.515 92:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.515 93:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.516 95:70 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.516 107:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.516 109:70 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.516 6:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.517 7:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.517 11:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.517 37:39 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.517 64:95 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.517 91:83 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.517 98:83 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.517 199:84 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.518 215:89 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.518 228:74 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.518 6:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.518 7:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.518 11:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.519 45:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.519 51:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.519 65:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.519 66:82 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.519 74:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.519 75:82 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.519 83:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.520 94:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.520 95:81 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.520 103:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.520 104:73 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.520 112:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.520 117:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.520 126:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.521 5:25 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.521 6:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.521 12:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.521 13:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.521 14:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.521 39:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.522 58:59 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.522 79:60 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.522 91:60 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.522 102:61 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.522 119:55 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.523 7:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.523 8:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.523 9:32 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.523 10:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.523 33:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.523 65:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.523 72:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.523 81:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.524 90:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.524 111:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.524 112:95 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.524 120:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.524 121:95 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.524 141:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.524 174:13 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.525 6:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.525 34:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.525 45:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.525 53:60 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.525 58:63 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.526 74:9 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.526 92:9 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.526 93:74 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.526 101:9 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.526 102:74 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.526 125:9 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.527 126:74 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.527 11:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.527 12:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.527 56:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.528 5:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.528 6:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.528 19:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.528 6:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.529 21:1 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.529 30:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.529 38:9 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.529 66:9 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.529 109:9 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.529 126:9 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.529 152:9 error Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment +21:30:01.530 17:22 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.530 18:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.530 19:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.531 32:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.532 39:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.532 40:32 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.532 49:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.532 49:66 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.532 65:70 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.532 79:70 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.533 80:26 warning 'e' is defined but never used @typescript-eslint/no-unused-vars +21:30:01.533 94:81 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.533 107:81 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.533 37:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.534 50:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.534 80:52 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.534 85:51 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.534 90:52 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.534 97:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.534 104:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.534 105:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.534 106:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.535 113:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.535 115:68 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.535 131:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.535 133:61 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.535 150:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.535 152:67 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.535 162:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.536 37:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.536 38:18 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.536 95:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.536 96:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.536 97:32 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.536 121:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.537 122:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.537 123:32 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.537 140:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.537 141:35 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.537 142:32 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.537 14:50 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.537 14:65 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.538 31:61 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.538 71:14 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.539 22:19 error The `Function` type accepts any function-like value. +21:30:01.539 28:41 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any +21:30:01.698 error: script "lint" exited with code 1 +21:30:01.700 error: script "build" exited with code 1 +21:30:01.766 Error: Command "npx convex deploy --cmd 'bun run build'" exited with 1 \ No newline at end of file diff --git a/components/debug/limit-tester.test.tsx b/components/debug/limit-tester.test.tsx new file mode 100644 index 0000000..18fb0dd --- /dev/null +++ b/components/debug/limit-tester.test.tsx @@ -0,0 +1,120 @@ +import { render, screen, fireEvent } from "@testing-library/react" +import { describe, it, expect, vi, beforeEach } from "vitest" +import { LimitTester } from "./limit-tester" + +// Mock Next.js Image +vi.mock("next/image", () => ({ + default: ({ src, alt, onLoadingComplete, ...props }: { + src: string + alt: string + onLoadingComplete?: (img: { naturalWidth: number; naturalHeight: number }) => void + [key: string]: unknown + }) => ( + {alt} { + onLoadingComplete?.({ naturalWidth: 1024, naturalHeight: 1024 }) + }} + /> + ), +})) + +// Mock Convex hooks +vi.mock("convex/react", () => ({ + useQuery: vi.fn(() => undefined), + useMutation: vi.fn(() => vi.fn()), +})) + +// Mock pollen auth +vi.mock("@/lib/pollen-auth", () => ({ + usePollenApiKey: vi.fn(() => "test-api-key"), + usePollenAuthActions: vi.fn(() => ({ authorize: vi.fn() })), +})) + +// Mock hooks +vi.mock("@/hooks", () => ({ + useRandomSeed: vi.fn(() => ({ generateSeed: vi.fn(() => 12345) })), +})) + +// Mock sonner toast +vi.mock("sonner", () => ({ + toast: { error: vi.fn(), success: vi.fn() }, +})) + +describe("LimitTester", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("renders the header correctly", () => { + render() + expect(screen.getByText("Model Limit Tester")).toBeInTheDocument() + }) + + it("renders model selector", () => { + render() + expect(screen.getByText("Model")).toBeInTheDocument() + }) + + it("renders prompt textarea", () => { + render() + expect(screen.getByText("Prompt")).toBeInTheDocument() + const textarea = screen.getByRole("textbox") + expect(textarea).toHaveValue("A glitch art masterpiece of a cyberpunk city, extremely detailed") + }) + + it("renders dimension inputs with default values", () => { + render() + expect(screen.getByText("Width (px)")).toBeInTheDocument() + expect(screen.getByText("Height (px)")).toBeInTheDocument() + + const inputs = screen.getAllByRole("spinbutton") + expect(inputs[0]).toHaveValue(1024) + expect(inputs[1]).toHaveValue(1024) + }) + + it("renders quick preset buttons", () => { + render() + expect(screen.getByText("Quick Presets")).toBeInTheDocument() + expect(screen.getByText("SQUARE (1:1)")).toBeInTheDocument() + expect(screen.getByText("TALL (Portrait)")).toBeInTheDocument() + expect(screen.getByText("WIDE (Landscape)")).toBeInTheDocument() + }) + + it("updates dimensions when preset is clicked", () => { + render() + + // Use a unique preset - 2048x2048 only exists in square presets + const preset2048 = screen.getByRole("button", { name: "2048x2048" }) + fireEvent.click(preset2048) + + const inputs = screen.getAllByRole("spinbutton") + expect(inputs[0]).toHaveValue(2048) + expect(inputs[1]).toHaveValue(2048) + }) + + it("displays megapixel calculation", () => { + render() + // 1024 * 1024 = 1,048,576 pixels = 1.05 MP + expect(screen.getByText(/Pixels:.*1\.05MP/)).toBeInTheDocument() + }) + + it("displays aspect ratio", () => { + render() + // 1024 / 1024 = 1.00 + expect(screen.getByText(/Ratio:.*1\.00/)).toBeInTheDocument() + }) + + it("renders test generation button", () => { + render() + expect(screen.getByRole("button", { name: "TEST GENERATION" })).toBeInTheDocument() + }) + + it("shows no results placeholder initially", () => { + render() + expect(screen.getByText("No results yet")).toBeInTheDocument() + }) +}) diff --git a/components/gallery/feed-client.test.tsx b/components/gallery/feed-client.test.tsx index 1aa381d..23d7182 100644 --- a/components/gallery/feed-client.test.tsx +++ b/components/gallery/feed-client.test.tsx @@ -335,7 +335,7 @@ describe("FeedClient", () => { it("does not call load more when already loading", async () => { // Make the server action slow mockLoadPublicFeedPage.mockImplementation( - () => new Promise((resolve) => setTimeout(() => resolve(mockSecondPage), 100)) + () => new Promise((resolve) => setTimeout(() => resolve(mockSecondPage), 500)) ); const user = userEvent.setup(); diff --git a/components/gallery/image-history.test.tsx b/components/gallery/image-history.test.tsx index f958e25..801864d 100644 --- a/components/gallery/image-history.test.tsx +++ b/components/gallery/image-history.test.tsx @@ -22,7 +22,6 @@ vi.mock("@/hooks/mutations/use-delete-image", () => ({ // Mock Next.js Image vi.mock("next/image", () => ({ default: ({ src, alt, ...props }: React.ImgHTMLAttributes & { fill?: boolean }) => { - // eslint-disable-next-line @next/next/no-img-element return {alt} }, diff --git a/components/images/image-lightbox.test.tsx b/components/images/image-lightbox.test.tsx index f595881..c845587 100644 --- a/components/images/image-lightbox.test.tsx +++ b/components/images/image-lightbox.test.tsx @@ -134,7 +134,6 @@ vi.mock("@/components/ui/media-player", () => ({ if (isVideo) { return