@nextsparkjs/ai-workflow 0.1.0-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/claude/_docs/workflows-optimizations.md +359 -0
- package/claude/agents/api-tester.md +634 -0
- package/claude/agents/architecture-supervisor.md +1351 -0
- package/claude/agents/backend-developer.md +997 -0
- package/claude/agents/backend-validator.md +417 -0
- package/claude/agents/bdd-docs-writer.md +737 -0
- package/claude/agents/block-developer.md +677 -0
- package/claude/agents/code-reviewer.md +1432 -0
- package/claude/agents/db-developer.md +721 -0
- package/claude/agents/db-validator.md +407 -0
- package/claude/agents/demo-video-generator.md +493 -0
- package/claude/agents/documentation-writer.md +1268 -0
- package/claude/agents/frontend-developer.md +1234 -0
- package/claude/agents/frontend-validator.md +777 -0
- package/claude/agents/functional-validator.md +630 -0
- package/claude/agents/mock-analyst.md +387 -0
- package/claude/agents/product-manager.md +963 -0
- package/claude/agents/qa-automation.md +1762 -0
- package/claude/agents/release-manager.md +634 -0
- package/claude/agents/selectors-translator.md +262 -0
- package/claude/agents/unit-test-writer.md +785 -0
- package/claude/agents/visual-comparator.md +329 -0
- package/claude/agents/workflow-maintainer.md +352 -0
- package/claude/commands/do/README.md +88 -0
- package/claude/commands/do/create-api.md +64 -0
- package/claude/commands/do/create-entity.md +66 -0
- package/claude/commands/do/create-migration.md +64 -0
- package/claude/commands/do/create-plugin.md +56 -0
- package/claude/commands/do/create-theme.md +70 -0
- package/claude/commands/do/mock-data.md +67 -0
- package/claude/commands/do/reset-db.md +71 -0
- package/claude/commands/do/setup-scheduled-action.md +75 -0
- package/claude/commands/do/sync-code-review.md +117 -0
- package/claude/commands/do/update-selectors.md +112 -0
- package/claude/commands/do/use-skills.md +90 -0
- package/claude/commands/do/validate-blocks.md +69 -0
- package/claude/commands/how-to/README.md +261 -0
- package/claude/commands/how-to/add-metadata.md +692 -0
- package/claude/commands/how-to/add-taxonomies.md +806 -0
- package/claude/commands/how-to/add-translations.md +571 -0
- package/claude/commands/how-to/create-api.md +577 -0
- package/claude/commands/how-to/create-block.md +575 -0
- package/claude/commands/how-to/create-child-entities.md +771 -0
- package/claude/commands/how-to/create-entity.md +597 -0
- package/claude/commands/how-to/create-migrations.md +605 -0
- package/claude/commands/how-to/create-plugin.md +654 -0
- package/claude/commands/how-to/customize-app.md +481 -0
- package/claude/commands/how-to/customize-dashboard.md +553 -0
- package/claude/commands/how-to/customize-theme.md +438 -0
- package/claude/commands/how-to/define-features-flows.md +632 -0
- package/claude/commands/how-to/deploy.md +507 -0
- package/claude/commands/how-to/handle-file-uploads.md +746 -0
- package/claude/commands/how-to/implement-search.md +1001 -0
- package/claude/commands/how-to/install-plugins.md +352 -0
- package/claude/commands/how-to/manage-test-coverage.md +984 -0
- package/claude/commands/how-to/run-tests.md +400 -0
- package/claude/commands/how-to/set-app-languages.md +601 -0
- package/claude/commands/how-to/set-plans-and-permissions.md +575 -0
- package/claude/commands/how-to/set-scheduled-actions.md +527 -0
- package/claude/commands/how-to/set-user-roles-and-permissions.md +550 -0
- package/claude/commands/how-to/setup-authentication.md +388 -0
- package/claude/commands/how-to/setup-claude-code.md +440 -0
- package/claude/commands/how-to/setup-database.md +274 -0
- package/claude/commands/how-to/setup-email-providers.md +598 -0
- package/claude/commands/how-to/setup-mobile-dev.md +627 -0
- package/claude/commands/how-to/start.md +500 -0
- package/claude/commands/how-to/use-devtools.md +639 -0
- package/claude/commands/how-to/use-superadmin.md +622 -0
- package/claude/commands/session/README.md +193 -0
- package/claude/commands/session/block-create.md +190 -0
- package/claude/commands/session/block-list.md +203 -0
- package/claude/commands/session/block-update.md +192 -0
- package/claude/commands/session/block-validate.md +218 -0
- package/claude/commands/session/changelog.md +115 -0
- package/claude/commands/session/close.md +225 -0
- package/claude/commands/session/commit.md +174 -0
- package/claude/commands/session/db-entity.md +206 -0
- package/claude/commands/session/db-fix.md +212 -0
- package/claude/commands/session/db-sample.md +206 -0
- package/claude/commands/session/demo.md +178 -0
- package/claude/commands/session/doc-bdd.md +207 -0
- package/claude/commands/session/doc-feature.md +218 -0
- package/claude/commands/session/doc-read.md +225 -0
- package/claude/commands/session/execute.md +204 -0
- package/claude/commands/session/explain.md +202 -0
- package/claude/commands/session/fix-bug.md +210 -0
- package/claude/commands/session/fix-build.md +182 -0
- package/claude/commands/session/fix-test.md +189 -0
- package/claude/commands/session/pending.md +232 -0
- package/claude/commands/session/refine.md +188 -0
- package/claude/commands/session/resume.md +192 -0
- package/claude/commands/session/review.md +192 -0
- package/claude/commands/session/scope-change.md +181 -0
- package/claude/commands/session/start-blocks.md +347 -0
- package/claude/commands/session/start.md +604 -0
- package/claude/commands/session/status.md +169 -0
- package/claude/commands/session/test-fix.md +221 -0
- package/claude/commands/session/test-run.md +203 -0
- package/claude/commands/session/test-write.md +242 -0
- package/claude/commands/session/validate.md +162 -0
- package/claude/config/context.json +40 -0
- package/claude/config/github.json +69 -0
- package/claude/config/github.schema.json +106 -0
- package/claude/config/team.json +46 -0
- package/claude/config/team.schema.json +106 -0
- package/claude/config/workspace.json +43 -0
- package/claude/config/workspace.schema.json +75 -0
- package/claude/skills/README.md +228 -0
- package/claude/skills/accessibility/SKILL.md +573 -0
- package/claude/skills/api-bypass-layers/SKILL.md +550 -0
- package/claude/skills/asana-integration/SKILL.md +499 -0
- package/claude/skills/better-auth/SKILL.md +666 -0
- package/claude/skills/billing-subscriptions/SKILL.md +660 -0
- package/claude/skills/block-decision-matrix/SKILL.md +359 -0
- package/claude/skills/clickup-integration/SKILL.md +434 -0
- package/claude/skills/core-theme-responsibilities/SKILL.md +485 -0
- package/claude/skills/create-plugin/SKILL.md +425 -0
- package/claude/skills/create-theme/SKILL.md +331 -0
- package/claude/skills/cypress-api/SKILL.md +511 -0
- package/claude/skills/cypress-api/scripts/generate-api-controller.py +329 -0
- package/claude/skills/cypress-api/scripts/generate-api-test.py +930 -0
- package/claude/skills/cypress-e2e/SKILL.md +526 -0
- package/claude/skills/cypress-e2e/scripts/extract-selectors.py +383 -0
- package/claude/skills/cypress-e2e/scripts/generate-uat-test.py +788 -0
- package/claude/skills/cypress-selectors/SKILL.md +309 -0
- package/claude/skills/cypress-selectors/scripts/extract-missing.py +243 -0
- package/claude/skills/cypress-selectors/scripts/generate-block-selectors.py +283 -0
- package/claude/skills/cypress-selectors/scripts/validate-selectors.py +145 -0
- package/claude/skills/database-migrations/SKILL.md +335 -0
- package/claude/skills/database-migrations/scripts/generate-sample-data.py +284 -0
- package/claude/skills/database-migrations/scripts/validate-migration.py +323 -0
- package/claude/skills/design-system/SKILL.md +682 -0
- package/claude/skills/documentation/SKILL.md +540 -0
- package/claude/skills/entity-api/SKILL.md +482 -0
- package/claude/skills/entity-system/SKILL.md +635 -0
- package/claude/skills/entity-system/scripts/generate-child-migration.py +298 -0
- package/claude/skills/entity-system/scripts/generate-metas-migration.py +233 -0
- package/claude/skills/entity-system/scripts/generate-migration.py +382 -0
- package/claude/skills/entity-system/scripts/generate-sample-data.py +418 -0
- package/claude/skills/entity-system/scripts/scaffold-entity.py +661 -0
- package/claude/skills/github/SKILL.md +467 -0
- package/claude/skills/i18n-nextintl/SKILL.md +302 -0
- package/claude/skills/i18n-nextintl/scripts/add-translation.py +243 -0
- package/claude/skills/i18n-nextintl/scripts/extract-hardcoded.py +246 -0
- package/claude/skills/i18n-nextintl/scripts/validate-translations.py +260 -0
- package/claude/skills/impact-analysis/SKILL.md +203 -0
- package/claude/skills/jest-unit/SKILL.md +306 -0
- package/claude/skills/jest-unit/references/component-testing.md +371 -0
- package/claude/skills/jest-unit/references/mocking-patterns.md +380 -0
- package/claude/skills/jest-unit/references/service-hook-testing.md +454 -0
- package/claude/skills/jira-integration/SKILL.md +539 -0
- package/claude/skills/media-library/SKILL.md +743 -0
- package/claude/skills/mock-analysis/SKILL.md +276 -0
- package/claude/skills/monorepo-architecture/SKILL.md +162 -0
- package/claude/skills/nextjs-api-development/SKILL.md +364 -0
- package/claude/skills/nextjs-api-development/scripts/generate-crud-tests.py +456 -0
- package/claude/skills/nextjs-api-development/scripts/scaffold-endpoint.py +481 -0
- package/claude/skills/nextjs-api-development/scripts/validate-api.py +283 -0
- package/claude/skills/notion-integration/SKILL.md +641 -0
- package/claude/skills/npm-development-workflow/SKILL.md +480 -0
- package/claude/skills/page-builder-blocks/SKILL.md +530 -0
- package/claude/skills/page-builder-blocks/scripts/scaffold-block.py +444 -0
- package/claude/skills/permissions-system/SKILL.md +619 -0
- package/claude/skills/plugins/SKILL.md +340 -0
- package/claude/skills/plugins/references/plugin-templates.md +414 -0
- package/claude/skills/plugins/references/plugin-testing.md +353 -0
- package/claude/skills/plugins/references/plugin-types.md +198 -0
- package/claude/skills/plugins/scripts/scaffold-plugin.py +443 -0
- package/claude/skills/pom-patterns/SKILL.md +452 -0
- package/claude/skills/pom-patterns/scripts/generate-pom.py +392 -0
- package/claude/skills/rate-limiting/SKILL.md +342 -0
- package/claude/skills/react-best-practices/AGENTS.md +2410 -0
- package/claude/skills/react-best-practices/README.md +123 -0
- package/claude/skills/react-best-practices/SKILL.md +125 -0
- package/claude/skills/react-best-practices/metadata.json +15 -0
- package/claude/skills/react-best-practices/rules/_sections.md +46 -0
- package/claude/skills/react-best-practices/rules/_template.md +28 -0
- package/claude/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/claude/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
- package/claude/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/claude/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/claude/skills/react-best-practices/rules/async-dependencies.md +36 -0
- package/claude/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/claude/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/claude/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/claude/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/claude/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/claude/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/claude/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/claude/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/claude/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/claude/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/claude/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/claude/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/claude/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/claude/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/claude/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/claude/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/claude/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/claude/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/claude/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/claude/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/claude/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/claude/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/claude/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/claude/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/claude/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/claude/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/claude/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/claude/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/claude/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/claude/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/claude/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/claude/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/claude/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/claude/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/claude/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/claude/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/claude/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/claude/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/claude/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/claude/skills/react-best-practices/rules/server-cache-react.md +76 -0
- package/claude/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/claude/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/claude/skills/react-patterns/SKILL.md +688 -0
- package/claude/skills/registry-system/SKILL.md +331 -0
- package/claude/skills/scheduled-actions/SKILL.md +671 -0
- package/claude/skills/scope-enforcement/SKILL.md +542 -0
- package/claude/skills/scope-enforcement/scripts/validate-scope.py +357 -0
- package/claude/skills/server-actions/SKILL.md +493 -0
- package/claude/skills/service-layer/SKILL.md +587 -0
- package/claude/skills/session-management/SKILL.md +266 -0
- package/claude/skills/session-management/scripts/create-session.py +166 -0
- package/claude/skills/session-management/scripts/iteration-close.sh +105 -0
- package/claude/skills/session-management/scripts/iteration-init.sh +180 -0
- package/claude/skills/session-management/scripts/session-archive.sh +87 -0
- package/claude/skills/session-management/scripts/session-close.sh +133 -0
- package/claude/skills/session-management/scripts/session-init.sh +225 -0
- package/claude/skills/session-management/scripts/session-list.sh +163 -0
- package/claude/skills/session-management/scripts/split-plan.sh +116 -0
- package/claude/skills/shadcn-components/SKILL.md +586 -0
- package/claude/skills/shadcn-theming/SKILL.md +446 -0
- package/claude/skills/suspense-loading/SKILL.md +280 -0
- package/claude/skills/tailwind-theming/SKILL.md +507 -0
- package/claude/skills/tanstack-query/SKILL.md +608 -0
- package/claude/skills/test-coverage/SKILL.md +239 -0
- package/claude/skills/web-design-guidelines/SKILL.md +39 -0
- package/claude/skills/zod-validation/SKILL.md +537 -0
- package/claude/templates/blocks/progress.md +86 -0
- package/claude/templates/iteration/changes.md +61 -0
- package/claude/templates/iteration/progress.md +55 -0
- package/claude/templates/log.md +31 -0
- package/claude/templates/story/context.md +77 -0
- package/claude/templates/story/pendings.md +37 -0
- package/claude/templates/story/plan.md +299 -0
- package/claude/templates/story/requirements.md +109 -0
- package/claude/templates/story/scope.json +10 -0
- package/claude/templates/story/tests.md +91 -0
- package/claude/templates/task/progress.md +58 -0
- package/claude/templates/task/requirements.md +54 -0
- package/claude/workflows/README.md +154 -0
- package/claude/workflows/blocks.md +614 -0
- package/claude/workflows/story.md +1207 -0
- package/claude/workflows/task.md +927 -0
- package/claude/workflows/tweak.md +527 -0
- package/cursor/.gitkeep +0 -0
- package/package.json +35 -0
- package/scripts/postinstall.mjs +198 -0
- package/scripts/setup.mjs +282 -0
- package/scripts/sync.mjs +209 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: media-library
|
|
3
|
+
description: |
|
|
4
|
+
WordPress-style media management system for this Next.js application.
|
|
5
|
+
Covers MediaService CRUD, file upload, tag system, duplicate detection,
|
|
6
|
+
MediaLibrary modal, MediaSelector form field, and block editor integration.
|
|
7
|
+
Use this skill when working with media uploads, browsing, or selection.
|
|
8
|
+
allowed-tools: Read, Glob, Grep, Bash
|
|
9
|
+
version: 1.0.0
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Media Library Skill
|
|
13
|
+
|
|
14
|
+
WordPress-style media management system with full CRUD API, reusable modal, form field component, and block editor integration.
|
|
15
|
+
|
|
16
|
+
## Architecture Overview
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
MEDIA LIBRARY SYSTEM:
|
|
20
|
+
|
|
21
|
+
Core Layer (packages/core/):
|
|
22
|
+
├── src/
|
|
23
|
+
│ ├── components/media/
|
|
24
|
+
│ │ ├── MediaLibrary.tsx # Main modal (browse, upload, select)
|
|
25
|
+
│ │ ├── MediaGrid.tsx # Grid view with thumbnails
|
|
26
|
+
│ │ ├── MediaList.tsx # List/table view
|
|
27
|
+
│ │ ├── MediaCard.tsx # Individual media card
|
|
28
|
+
│ │ ├── MediaToolbar.tsx # Search, filter, sort, view toggle
|
|
29
|
+
│ │ ├── MediaDetailPanel.tsx # Right sidebar detail/edit panel
|
|
30
|
+
│ │ ├── MediaUploadZone.tsx # Drag & drop upload area
|
|
31
|
+
│ │ ├── MediaSelector.tsx # Form field component for entities
|
|
32
|
+
│ │ ├── MediaTagFilter.tsx # Tag filter chips
|
|
33
|
+
│ │ └── index.ts # Re-exports
|
|
34
|
+
│ ├── hooks/
|
|
35
|
+
│ │ ├── useMedia.ts # TanStack Query hooks (CRUD + tags)
|
|
36
|
+
│ │ └── useMediaUpload.ts # Upload hook with progress tracking
|
|
37
|
+
│ ├── lib/
|
|
38
|
+
│ │ ├── media/
|
|
39
|
+
│ │ │ ├── types.ts # Media, MediaTag, MediaListOptions
|
|
40
|
+
│ │ │ ├── schemas.ts # Zod validation schemas
|
|
41
|
+
│ │ │ └── utils.ts # formatFileSize, getMediaType, etc.
|
|
42
|
+
│ │ └── services/
|
|
43
|
+
│ │ └── media.service.ts # MediaService (CRUD, tags, duplicates)
|
|
44
|
+
│ └── types/
|
|
45
|
+
│ └── blocks.ts # FieldType includes 'media-library'
|
|
46
|
+
│
|
|
47
|
+
├── migrations/
|
|
48
|
+
│ └── 021_media.sql # Media + media_tags + media_tag_relations
|
|
49
|
+
|
|
50
|
+
API Layer (apps/dev/app/api/v1/):
|
|
51
|
+
├── media/
|
|
52
|
+
│ ├── route.ts # GET (list), POST (create)
|
|
53
|
+
│ ├── upload/route.ts # POST (file upload)
|
|
54
|
+
│ ├── check-duplicates/route.ts # POST (hash check)
|
|
55
|
+
│ └── [id]/
|
|
56
|
+
│ ├── route.ts # GET, PATCH, DELETE
|
|
57
|
+
│ └── tags/route.ts # GET, POST, DELETE media tags
|
|
58
|
+
└── media-tags/
|
|
59
|
+
└── route.ts # GET all tags
|
|
60
|
+
|
|
61
|
+
Dashboard (apps/dev/app/dashboard/(main)/media/):
|
|
62
|
+
└── page.tsx # Dashboard media page
|
|
63
|
+
|
|
64
|
+
Flow:
|
|
65
|
+
Upload → API → MediaService → DB → TanStack Query Cache → UI
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> **Context-Aware Paths:** Core layer components and services are in `packages/core/`.
|
|
69
|
+
> API routes are in the app layer. Dashboard page is theme-provided.
|
|
70
|
+
> See `core-theme-responsibilities` skill for complete rules.
|
|
71
|
+
|
|
72
|
+
## When to Use This Skill
|
|
73
|
+
|
|
74
|
+
- Adding media upload functionality to a feature
|
|
75
|
+
- Integrating media selection into entity forms
|
|
76
|
+
- Adding `type: 'media-library'` fields to page builder blocks
|
|
77
|
+
- Working with the MediaLibrary modal or MediaSelector component
|
|
78
|
+
- Implementing media tag filtering or organization
|
|
79
|
+
- Debugging upload, duplicate detection, or media API issues
|
|
80
|
+
- Configuring upload size limits or accepted file types
|
|
81
|
+
|
|
82
|
+
## Media Type Definition
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface Media {
|
|
86
|
+
id: string
|
|
87
|
+
userId: string
|
|
88
|
+
teamId: string
|
|
89
|
+
filename: string
|
|
90
|
+
originalFilename: string
|
|
91
|
+
mimeType: string
|
|
92
|
+
fileSize: number
|
|
93
|
+
url: string
|
|
94
|
+
thumbnailUrl?: string
|
|
95
|
+
width?: number
|
|
96
|
+
height?: number
|
|
97
|
+
title?: string
|
|
98
|
+
alt?: string
|
|
99
|
+
caption?: string
|
|
100
|
+
hash?: string
|
|
101
|
+
status: 'active' | 'archived'
|
|
102
|
+
metadata?: Record<string, unknown>
|
|
103
|
+
createdAt: string
|
|
104
|
+
updatedAt: string
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface MediaTag {
|
|
108
|
+
id: string
|
|
109
|
+
name: string
|
|
110
|
+
slug: string
|
|
111
|
+
createdAt: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface MediaListOptions {
|
|
115
|
+
limit?: number // Default: 50
|
|
116
|
+
offset?: number // Default: 0
|
|
117
|
+
orderBy?: string // Default: 'createdAt'
|
|
118
|
+
orderDir?: 'asc' | 'desc' // Default: 'desc'
|
|
119
|
+
type?: 'all' | 'image' | 'video'
|
|
120
|
+
search?: string // Searches filename, title, alt
|
|
121
|
+
tagIds?: string[] // Filter by tag IDs
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## API Endpoints
|
|
126
|
+
|
|
127
|
+
| Method | Endpoint | Description | Auth Scope |
|
|
128
|
+
|--------|----------|-------------|------------|
|
|
129
|
+
| GET | `/api/v1/media` | List media (paginated, filterable) | `media:read` |
|
|
130
|
+
| POST | `/api/v1/media` | Create media record | `media:write` |
|
|
131
|
+
| POST | `/api/v1/media/upload` | Upload files | `media:write` |
|
|
132
|
+
| POST | `/api/v1/media/check-duplicates` | Check for duplicates by hash | `media:read` |
|
|
133
|
+
| GET | `/api/v1/media/[id]` | Get single media item | `media:read` |
|
|
134
|
+
| PATCH | `/api/v1/media/[id]` | Update media metadata | `media:write` |
|
|
135
|
+
| DELETE | `/api/v1/media/[id]` | Delete media item | `media:delete` |
|
|
136
|
+
| GET | `/api/v1/media/[id]/tags` | Get tags for a media item | `media:read` |
|
|
137
|
+
| POST | `/api/v1/media/[id]/tags` | Add tags to media | `media:write` |
|
|
138
|
+
| DELETE | `/api/v1/media/[id]/tags` | Remove tags from media | `media:write` |
|
|
139
|
+
| GET | `/api/v1/media-tags` | List all available tags | `media:read` |
|
|
140
|
+
|
|
141
|
+
All endpoints support dual authentication (session cookie + API key via `x-api-key` header).
|
|
142
|
+
|
|
143
|
+
### Query Parameters for GET /api/v1/media
|
|
144
|
+
|
|
145
|
+
| Param | Type | Default | Description |
|
|
146
|
+
|-------|------|---------|-------------|
|
|
147
|
+
| `limit` | number | 50 | Items per page |
|
|
148
|
+
| `offset` | number | 0 | Pagination offset |
|
|
149
|
+
| `orderBy` | string | `createdAt` | Sort field (`createdAt`, `filename`, `fileSize`, `mimeType`) |
|
|
150
|
+
| `orderDir` | string | `desc` | Sort direction (`asc`, `desc`) |
|
|
151
|
+
| `type` | string | `all` | Filter: `all`, `image`, `video` |
|
|
152
|
+
| `search` | string | - | Search in filename, title, alt |
|
|
153
|
+
| `tagIds` | string | - | Comma-separated tag IDs |
|
|
154
|
+
|
|
155
|
+
## MediaService
|
|
156
|
+
|
|
157
|
+
Static class following the service-layer pattern with RLS integration:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// core/lib/services/media.service.ts
|
|
161
|
+
|
|
162
|
+
export class MediaService {
|
|
163
|
+
// CRUD operations
|
|
164
|
+
static async getById(id: string, userId: string): Promise<Media | null>
|
|
165
|
+
static async list(userId: string, options?: MediaListOptions): Promise<ListResult<Media>>
|
|
166
|
+
static async create(userId: string, teamId: string, data: CreateMedia): Promise<Media>
|
|
167
|
+
static async update(id: string, userId: string, data: UpdateMedia): Promise<Media>
|
|
168
|
+
static async delete(id: string, userId: string): Promise<boolean>
|
|
169
|
+
|
|
170
|
+
// Tag operations
|
|
171
|
+
static async getTags(mediaId: string, userId: string): Promise<MediaTag[]>
|
|
172
|
+
static async addTag(mediaId: string, tagId: string, userId: string): Promise<void>
|
|
173
|
+
static async removeTag(mediaId: string, tagId: string, userId: string): Promise<void>
|
|
174
|
+
static async getAllTags(userId: string): Promise<MediaTag[]>
|
|
175
|
+
|
|
176
|
+
// Duplicate detection
|
|
177
|
+
static async checkDuplicates(hashes: string[], userId: string): Promise<Media[]>
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## TanStack Query Hooks
|
|
182
|
+
|
|
183
|
+
### Query Hooks
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import {
|
|
187
|
+
useMediaList,
|
|
188
|
+
useMediaItem,
|
|
189
|
+
useMediaTags,
|
|
190
|
+
useMediaItemTags,
|
|
191
|
+
} from '@/core/hooks/useMedia'
|
|
192
|
+
|
|
193
|
+
// List media with filters (paginated)
|
|
194
|
+
const { data, isLoading, error } = useMediaList({
|
|
195
|
+
limit: 50,
|
|
196
|
+
offset: 0,
|
|
197
|
+
type: 'image',
|
|
198
|
+
search: 'hero',
|
|
199
|
+
orderBy: 'createdAt',
|
|
200
|
+
orderDir: 'desc',
|
|
201
|
+
tagIds: ['tag-1', 'tag-2'],
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Single media item (enabled when id is truthy)
|
|
205
|
+
const { data: media } = useMediaItem(selectedId)
|
|
206
|
+
|
|
207
|
+
// All available tags
|
|
208
|
+
const { data: tags } = useMediaTags()
|
|
209
|
+
|
|
210
|
+
// Tags for a specific media item
|
|
211
|
+
const { data: mediaTags } = useMediaItemTags(mediaId)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Mutation Hooks
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import {
|
|
218
|
+
useCreateMedia,
|
|
219
|
+
useUpdateMedia,
|
|
220
|
+
useDeleteMedia,
|
|
221
|
+
useAddMediaTag,
|
|
222
|
+
useRemoveMediaTag,
|
|
223
|
+
} from '@/core/hooks/useMedia'
|
|
224
|
+
|
|
225
|
+
// Create media record
|
|
226
|
+
const createMutation = useCreateMedia()
|
|
227
|
+
await createMutation.mutateAsync({ filename, url, mimeType, fileSize })
|
|
228
|
+
|
|
229
|
+
// Update media metadata
|
|
230
|
+
const updateMutation = useUpdateMedia()
|
|
231
|
+
await updateMutation.mutateAsync({ id: 'media-123', title: 'New Title', alt: 'Alt text' })
|
|
232
|
+
|
|
233
|
+
// Delete media
|
|
234
|
+
const deleteMutation = useDeleteMedia()
|
|
235
|
+
await deleteMutation.mutateAsync('media-123')
|
|
236
|
+
|
|
237
|
+
// Tag management
|
|
238
|
+
const addTagMutation = useAddMediaTag()
|
|
239
|
+
await addTagMutation.mutateAsync({ mediaId: 'media-123', tagId: 'tag-456' })
|
|
240
|
+
|
|
241
|
+
const removeTagMutation = useRemoveMediaTag()
|
|
242
|
+
await removeTagMutation.mutateAsync({ mediaId: 'media-123', tagId: 'tag-456' })
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Upload Hook
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { useMediaUpload } from '@/core/hooks/useMediaUpload'
|
|
249
|
+
|
|
250
|
+
const { upload, progress, isUploading, error } = useMediaUpload()
|
|
251
|
+
|
|
252
|
+
// Upload a file with progress tracking
|
|
253
|
+
const media = await upload(file)
|
|
254
|
+
console.log(`Upload progress: ${progress}%`)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Query Key Conventions
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// Media query keys follow the standard pattern:
|
|
261
|
+
['media', filters] // Media list with filters
|
|
262
|
+
['media', id] // Single media item
|
|
263
|
+
['media-tags'] // All tags
|
|
264
|
+
['media', mediaId, 'tags'] // Tags for specific media
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Component Usage
|
|
268
|
+
|
|
269
|
+
### MediaLibrary Modal
|
|
270
|
+
|
|
271
|
+
Full-featured modal for browsing, uploading, and selecting media:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { MediaLibrary } from '@/core/components/media'
|
|
275
|
+
|
|
276
|
+
// Single selection mode
|
|
277
|
+
<MediaLibrary
|
|
278
|
+
isOpen={isModalOpen}
|
|
279
|
+
onClose={() => setIsModalOpen(false)}
|
|
280
|
+
onSelect={(media: Media) => {
|
|
281
|
+
setSelectedImage(media.url)
|
|
282
|
+
}}
|
|
283
|
+
mode="single"
|
|
284
|
+
allowedTypes={['image']}
|
|
285
|
+
/>
|
|
286
|
+
|
|
287
|
+
// Multiple selection mode
|
|
288
|
+
<MediaLibrary
|
|
289
|
+
isOpen={isModalOpen}
|
|
290
|
+
onClose={() => setIsModalOpen(false)}
|
|
291
|
+
onSelect={(mediaItems: Media[]) => {
|
|
292
|
+
setGalleryImages(mediaItems.map(m => m.url))
|
|
293
|
+
}}
|
|
294
|
+
mode="multiple"
|
|
295
|
+
allowedTypes={['image', 'video']}
|
|
296
|
+
maxSelections={10}
|
|
297
|
+
/>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**MediaLibrary Props:**
|
|
301
|
+
|
|
302
|
+
| Prop | Type | Required | Description |
|
|
303
|
+
|------|------|----------|-------------|
|
|
304
|
+
| `isOpen` | `boolean` | Yes | Controls modal visibility |
|
|
305
|
+
| `onClose` | `() => void` | Yes | Callback when modal closes |
|
|
306
|
+
| `onSelect` | `(media: Media \| Media[]) => void` | Yes | Callback with selected media |
|
|
307
|
+
| `mode` | `'single' \| 'multiple'` | No | Selection mode (default: `'single'`) |
|
|
308
|
+
| `allowedTypes` | `string[]` | No | Restrict to `['image']`, `['video']`, or both |
|
|
309
|
+
| `maxSelections` | `number` | No | Max items in multiple mode |
|
|
310
|
+
|
|
311
|
+
### MediaSelector Form Field
|
|
312
|
+
|
|
313
|
+
Drop-in form component for entity fields:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { MediaSelector } from '@/core/components/media'
|
|
317
|
+
|
|
318
|
+
// In an entity form
|
|
319
|
+
<MediaSelector
|
|
320
|
+
value={formData.featuredImageId}
|
|
321
|
+
onChange={(id) => setFormData({ ...formData, featuredImageId: id })}
|
|
322
|
+
type="image"
|
|
323
|
+
/>
|
|
324
|
+
|
|
325
|
+
// For video selection
|
|
326
|
+
<MediaSelector
|
|
327
|
+
value={formData.videoId}
|
|
328
|
+
onChange={(id) => setFormData({ ...formData, videoId: id })}
|
|
329
|
+
type="video"
|
|
330
|
+
/>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**MediaSelector Props:**
|
|
334
|
+
|
|
335
|
+
| Prop | Type | Required | Description |
|
|
336
|
+
|------|------|----------|-------------|
|
|
337
|
+
| `value` | `string \| null` | Yes | Current media ID |
|
|
338
|
+
| `onChange` | `(id: string \| null) => void` | Yes | Callback when selection changes |
|
|
339
|
+
| `type` | `'image' \| 'video'` | No | Restrict selectable media type |
|
|
340
|
+
|
|
341
|
+
## Block Editor Integration
|
|
342
|
+
|
|
343
|
+
### Field Type: 'media-library'
|
|
344
|
+
|
|
345
|
+
In block field definitions, use `type: 'media-library'` to open the full MediaLibrary modal instead of a simple file input:
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// contents/themes/{theme}/blocks/hero/fields.ts
|
|
349
|
+
import type { FieldDefinition } from '@/core/types/blocks'
|
|
350
|
+
|
|
351
|
+
const customDesignFields: FieldDefinition[] = [
|
|
352
|
+
{
|
|
353
|
+
name: 'backgroundImage',
|
|
354
|
+
label: 'Background Image',
|
|
355
|
+
type: 'media-library', // Opens MediaLibrary modal
|
|
356
|
+
tab: 'design',
|
|
357
|
+
},
|
|
358
|
+
]
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### How MediaLibraryField Works
|
|
362
|
+
|
|
363
|
+
The `MediaLibraryField` component renders based on state:
|
|
364
|
+
|
|
365
|
+
1. **Empty state** - Shows "Browse Media Library" prompt button
|
|
366
|
+
2. **On click** - Opens the full `MediaLibrary` modal
|
|
367
|
+
3. **After selection** - Shows image preview with hover overlay (Change / Remove buttons)
|
|
368
|
+
4. **Storage** - Stores the URL string (not media ID) in block data
|
|
369
|
+
|
|
370
|
+
### In Array Sub-Fields
|
|
371
|
+
|
|
372
|
+
Media library fields also work inside array (repeatable) items:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// Array field with media sub-field
|
|
376
|
+
{
|
|
377
|
+
name: 'slides',
|
|
378
|
+
label: 'Slides',
|
|
379
|
+
type: 'array',
|
|
380
|
+
tab: 'content',
|
|
381
|
+
itemFields: [
|
|
382
|
+
{ name: 'image', label: 'Slide Image', type: 'media-library', tab: 'content' },
|
|
383
|
+
{ name: 'title', label: 'Title', type: 'text', tab: 'content' },
|
|
384
|
+
{ name: 'description', label: 'Description', type: 'textarea', tab: 'content' },
|
|
385
|
+
],
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Block Schema with Media
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
// contents/themes/{theme}/blocks/hero/schema.ts
|
|
393
|
+
import { z } from 'zod'
|
|
394
|
+
import { baseBlockSchema } from '@/core/types/blocks'
|
|
395
|
+
|
|
396
|
+
export const schema = baseBlockSchema.merge(z.object({
|
|
397
|
+
backgroundImage: z.string().url().optional(), // URL from MediaLibrary
|
|
398
|
+
overlayOpacity: z.number().min(0).max(100).default(50),
|
|
399
|
+
}))
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Upload Configuration
|
|
403
|
+
|
|
404
|
+
### app.config.ts
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
// contents/themes/{theme}/config/app.config.ts
|
|
408
|
+
export const appConfig = {
|
|
409
|
+
// ... other config
|
|
410
|
+
|
|
411
|
+
media: {
|
|
412
|
+
maxSizeMB: 10, // General max size
|
|
413
|
+
maxSizeImageMB: 10, // Image-specific max
|
|
414
|
+
maxSizeVideoMB: 50, // Video-specific max
|
|
415
|
+
acceptedTypes: ['image/*', 'video/*'],
|
|
416
|
+
allowedMimeTypes: [
|
|
417
|
+
'image/jpeg',
|
|
418
|
+
'image/png',
|
|
419
|
+
'image/gif',
|
|
420
|
+
'image/webp',
|
|
421
|
+
'image/svg+xml',
|
|
422
|
+
'video/mp4',
|
|
423
|
+
'video/webm',
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Upload Flow
|
|
430
|
+
|
|
431
|
+
```
|
|
432
|
+
1. User drops file on MediaUploadZone (or clicks to browse)
|
|
433
|
+
2. Client-side validation: file size, MIME type
|
|
434
|
+
3. Client computes file hash (SHA-256)
|
|
435
|
+
4. POST /api/v1/media/check-duplicates with hash
|
|
436
|
+
5. If duplicate found: prompt user (skip/replace/upload anyway)
|
|
437
|
+
6. POST /api/v1/media/upload with FormData
|
|
438
|
+
7. Server validates, stores file, creates DB record
|
|
439
|
+
8. TanStack Query cache invalidated, UI updates
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
## Database Schema
|
|
443
|
+
|
|
444
|
+
### Migration: 021_media.sql
|
|
445
|
+
|
|
446
|
+
```sql
|
|
447
|
+
-- Media items table
|
|
448
|
+
CREATE TABLE IF NOT EXISTS "media" (
|
|
449
|
+
"id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
450
|
+
"userId" TEXT NOT NULL REFERENCES "user"(id),
|
|
451
|
+
"teamId" TEXT NOT NULL REFERENCES "team"(id),
|
|
452
|
+
"filename" VARCHAR(500) NOT NULL,
|
|
453
|
+
"originalFilename" VARCHAR(500) NOT NULL,
|
|
454
|
+
"mimeType" VARCHAR(100) NOT NULL,
|
|
455
|
+
"fileSize" INTEGER NOT NULL,
|
|
456
|
+
"url" TEXT NOT NULL,
|
|
457
|
+
"thumbnailUrl" TEXT,
|
|
458
|
+
"width" INTEGER,
|
|
459
|
+
"height" INTEGER,
|
|
460
|
+
"title" VARCHAR(500),
|
|
461
|
+
"alt" VARCHAR(500),
|
|
462
|
+
"caption" TEXT,
|
|
463
|
+
"hash" VARCHAR(128),
|
|
464
|
+
"status" VARCHAR(20) NOT NULL DEFAULT 'active',
|
|
465
|
+
"metadata" JSONB DEFAULT '{}'::jsonb,
|
|
466
|
+
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
467
|
+
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
-- Media tags table
|
|
471
|
+
CREATE TABLE IF NOT EXISTS "media_tags" (
|
|
472
|
+
"id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
473
|
+
"name" VARCHAR(100) NOT NULL,
|
|
474
|
+
"slug" VARCHAR(100) NOT NULL UNIQUE,
|
|
475
|
+
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
-- Many-to-many relation: media <-> tags
|
|
479
|
+
CREATE TABLE IF NOT EXISTS "media_tag_relations" (
|
|
480
|
+
"mediaId" TEXT NOT NULL REFERENCES "media"(id) ON DELETE CASCADE,
|
|
481
|
+
"tagId" TEXT NOT NULL REFERENCES "media_tags"(id) ON DELETE CASCADE,
|
|
482
|
+
PRIMARY KEY ("mediaId", "tagId")
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
-- Indexes
|
|
486
|
+
CREATE INDEX "idx_media_userId" ON "media"("userId");
|
|
487
|
+
CREATE INDEX "idx_media_teamId" ON "media"("teamId");
|
|
488
|
+
CREATE INDEX "idx_media_hash" ON "media"("hash");
|
|
489
|
+
CREATE INDEX "idx_media_status" ON "media"("status");
|
|
490
|
+
CREATE INDEX "idx_media_mimeType" ON "media"("mimeType");
|
|
491
|
+
CREATE INDEX "idx_media_tag_relations_tagId" ON "media_tag_relations"("tagId");
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
## Internationalization
|
|
495
|
+
|
|
496
|
+
### i18n Namespace: `media`
|
|
497
|
+
|
|
498
|
+
60+ translation keys in `en` and `es` locales.
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// Usage in components
|
|
502
|
+
const t = useTranslations('media')
|
|
503
|
+
|
|
504
|
+
t('title') // "Media Library"
|
|
505
|
+
t('upload') // "Upload"
|
|
506
|
+
t('dragDrop') // "Drag & drop files here"
|
|
507
|
+
t('noResults') // "No media found"
|
|
508
|
+
t('confirmDelete') // "Are you sure you want to delete this media?"
|
|
509
|
+
t('filters.allTypes') // "All Types"
|
|
510
|
+
t('filters.images') // "Images"
|
|
511
|
+
t('filters.videos') // "Videos"
|
|
512
|
+
t('detail.title') // "Title"
|
|
513
|
+
t('detail.alt') // "Alt Text"
|
|
514
|
+
t('detail.caption') // "Caption"
|
|
515
|
+
t('detail.fileSize') // "File Size"
|
|
516
|
+
t('detail.dimensions') // "Dimensions"
|
|
517
|
+
t('detail.uploadedAt') // "Uploaded At"
|
|
518
|
+
t('tags.addTag') // "Add Tag"
|
|
519
|
+
t('tags.removeTag') // "Remove Tag"
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Block Editor i18n Keys
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
// Namespace: admin.blockEditor.form
|
|
526
|
+
t('changeImage') // "Change"
|
|
527
|
+
t('removeImage') // "Remove"
|
|
528
|
+
t('browseMedia') // "Browse Media Library"
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
## Testing
|
|
532
|
+
|
|
533
|
+
### Cypress API Tests
|
|
534
|
+
|
|
535
|
+
File: `media-crud.cy.ts` (20+ test cases)
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
// Tests cover:
|
|
539
|
+
// - List media with pagination
|
|
540
|
+
// - List media with type filter (image, video)
|
|
541
|
+
// - List media with search
|
|
542
|
+
// - List media with tag filter
|
|
543
|
+
// - List media with sorting
|
|
544
|
+
// - Create media record
|
|
545
|
+
// - Upload file
|
|
546
|
+
// - Get single media
|
|
547
|
+
// - Update media metadata (title, alt, caption)
|
|
548
|
+
// - Delete media
|
|
549
|
+
// - Duplicate detection via hash
|
|
550
|
+
// - Add/remove tags
|
|
551
|
+
// - Permission checks (media:read, media:write, media:delete)
|
|
552
|
+
// - Dual auth (session + API key)
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Jest Unit Tests
|
|
556
|
+
|
|
557
|
+
```
|
|
558
|
+
media.service.test.ts # Service layer unit tests
|
|
559
|
+
schemas.test.ts # Zod schema validation tests
|
|
560
|
+
utils.test.ts # Utility function tests (formatFileSize, getMediaType, etc.)
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### data-cy Selectors
|
|
564
|
+
|
|
565
|
+
Defined in `media.selectors.ts` and `block-editor.selectors.ts`:
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// media.selectors.ts
|
|
569
|
+
export const MEDIA_SELECTORS = {
|
|
570
|
+
library: {
|
|
571
|
+
modal: 'media-library-modal',
|
|
572
|
+
grid: 'media-grid',
|
|
573
|
+
list: 'media-list',
|
|
574
|
+
card: 'media-card',
|
|
575
|
+
toolbar: 'media-toolbar',
|
|
576
|
+
searchInput: 'media-search-input',
|
|
577
|
+
typeFilter: 'media-type-filter',
|
|
578
|
+
sortSelect: 'media-sort-select',
|
|
579
|
+
viewToggle: 'media-view-toggle',
|
|
580
|
+
uploadZone: 'media-upload-zone',
|
|
581
|
+
detailPanel: 'media-detail-panel',
|
|
582
|
+
selectBtn: 'media-select-btn',
|
|
583
|
+
deleteBtn: 'media-delete-btn',
|
|
584
|
+
},
|
|
585
|
+
selector: {
|
|
586
|
+
container: 'media-selector',
|
|
587
|
+
preview: 'media-selector-preview',
|
|
588
|
+
browseBtn: 'media-selector-browse',
|
|
589
|
+
removeBtn: 'media-selector-remove',
|
|
590
|
+
},
|
|
591
|
+
tags: {
|
|
592
|
+
filter: 'media-tag-filter',
|
|
593
|
+
chip: 'media-tag-chip',
|
|
594
|
+
addBtn: 'media-tag-add',
|
|
595
|
+
removeBtn: 'media-tag-remove',
|
|
596
|
+
},
|
|
597
|
+
} as const
|
|
598
|
+
|
|
599
|
+
// block-editor.selectors.ts (additions)
|
|
600
|
+
export const BLOCK_EDITOR_SELECTORS = {
|
|
601
|
+
// ... existing selectors
|
|
602
|
+
mediaField: {
|
|
603
|
+
container: 'block-media-field',
|
|
604
|
+
preview: 'block-media-preview',
|
|
605
|
+
browseBtn: 'block-media-browse',
|
|
606
|
+
changeBtn: 'block-media-change',
|
|
607
|
+
removeBtn: 'block-media-remove',
|
|
608
|
+
},
|
|
609
|
+
} as const
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
## Key Files Reference
|
|
613
|
+
|
|
614
|
+
| File | Purpose |
|
|
615
|
+
|------|---------|
|
|
616
|
+
| `core/src/components/media/MediaLibrary.tsx` | Main modal component |
|
|
617
|
+
| `core/src/components/media/MediaSelector.tsx` | Form field component |
|
|
618
|
+
| `core/src/components/media/MediaUploadZone.tsx` | Drag & drop upload |
|
|
619
|
+
| `core/src/hooks/useMedia.ts` | TanStack Query hooks (CRUD + tags) |
|
|
620
|
+
| `core/src/hooks/useMediaUpload.ts` | Upload with progress |
|
|
621
|
+
| `core/src/lib/media/types.ts` | TypeScript type definitions |
|
|
622
|
+
| `core/src/lib/media/schemas.ts` | Zod validation schemas |
|
|
623
|
+
| `core/src/lib/media/utils.ts` | Utility functions |
|
|
624
|
+
| `core/src/lib/services/media.service.ts` | MediaService (CRUD, tags, duplicates) |
|
|
625
|
+
| `core/src/types/blocks.ts` | FieldType with `'media-library'` |
|
|
626
|
+
| `core/migrations/021_media.sql` | Database migration |
|
|
627
|
+
| `apps/dev/app/api/v1/media/route.ts` | List + create endpoints |
|
|
628
|
+
| `apps/dev/app/api/v1/media/upload/route.ts` | File upload endpoint |
|
|
629
|
+
| `apps/dev/app/api/v1/media/[id]/route.ts` | Single item CRUD |
|
|
630
|
+
| `apps/dev/app/api/v1/media/[id]/tags/route.ts` | Media tag management |
|
|
631
|
+
| `apps/dev/app/api/v1/media-tags/route.ts` | All tags endpoint |
|
|
632
|
+
| `apps/dev/app/dashboard/(main)/media/page.tsx` | Dashboard page |
|
|
633
|
+
|
|
634
|
+
## Anti-Patterns
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
// NEVER: Use type 'image' in block fields when you want the media library
|
|
638
|
+
{
|
|
639
|
+
name: 'backgroundImage',
|
|
640
|
+
type: 'image', // Opens basic file input
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// CORRECT: Use type 'media-library' for full media library experience
|
|
644
|
+
{
|
|
645
|
+
name: 'backgroundImage',
|
|
646
|
+
type: 'media-library', // Opens MediaLibrary modal
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// NEVER: Store media ID in block data (blocks store URLs)
|
|
650
|
+
onSelect={(media) => {
|
|
651
|
+
updateBlockData({ backgroundImage: media.id }) // WRONG
|
|
652
|
+
}}
|
|
653
|
+
|
|
654
|
+
// CORRECT: Store URL in block data
|
|
655
|
+
onSelect={(media) => {
|
|
656
|
+
updateBlockData({ backgroundImage: media.url }) // Correct
|
|
657
|
+
}}
|
|
658
|
+
|
|
659
|
+
// NEVER: Bypass MediaService for direct DB queries
|
|
660
|
+
const media = await queryWithRLS('SELECT * FROM "media" WHERE id = $1', [id], userId)
|
|
661
|
+
|
|
662
|
+
// CORRECT: Use MediaService
|
|
663
|
+
const media = await MediaService.getById(id, userId)
|
|
664
|
+
|
|
665
|
+
// NEVER: Skip duplicate check before upload
|
|
666
|
+
const result = await upload(file) // Might create duplicates
|
|
667
|
+
|
|
668
|
+
// CORRECT: Check for duplicates first (useMediaUpload handles this)
|
|
669
|
+
const { upload } = useMediaUpload() // Built-in duplicate detection
|
|
670
|
+
const result = await upload(file)
|
|
671
|
+
|
|
672
|
+
// NEVER: Hardcode file size limits
|
|
673
|
+
if (file.size > 10 * 1024 * 1024) { ... }
|
|
674
|
+
|
|
675
|
+
// CORRECT: Read from app.config.ts
|
|
676
|
+
import { appConfig } from '@/contents/themes/{theme}/config/app.config'
|
|
677
|
+
if (file.size > appConfig.media.maxSizeImageMB * 1024 * 1024) { ... }
|
|
678
|
+
|
|
679
|
+
// NEVER: Skip i18n for media UI strings
|
|
680
|
+
<Button>Upload</Button>
|
|
681
|
+
|
|
682
|
+
// CORRECT: Use translation keys
|
|
683
|
+
<Button>{t('upload')}</Button>
|
|
684
|
+
|
|
685
|
+
// NEVER: Forget data-cy selectors on interactive elements
|
|
686
|
+
<Button onClick={handleUpload}>Upload</Button>
|
|
687
|
+
|
|
688
|
+
// CORRECT: Include data-cy
|
|
689
|
+
<Button data-cy={sel(MEDIA_SELECTORS.library.uploadZone)} onClick={handleUpload}>
|
|
690
|
+
{t('upload')}
|
|
691
|
+
</Button>
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
## Checklist
|
|
695
|
+
|
|
696
|
+
### Adding Media Selection to an Entity
|
|
697
|
+
|
|
698
|
+
- [ ] Import `MediaSelector` from `@/core/components/media`
|
|
699
|
+
- [ ] Add media field to entity form with proper `value` and `onChange`
|
|
700
|
+
- [ ] Add corresponding database column (TEXT for media ID or URL)
|
|
701
|
+
- [ ] Add i18n keys for the field label and description
|
|
702
|
+
- [ ] Add `data-cy` selector for the media field
|
|
703
|
+
|
|
704
|
+
### Adding Media to a Block
|
|
705
|
+
|
|
706
|
+
- [ ] Use `type: 'media-library'` in field definition (not `type: 'image'`)
|
|
707
|
+
- [ ] Add `.url().optional()` Zod schema field for the URL
|
|
708
|
+
- [ ] Store URL string in block data (not media ID)
|
|
709
|
+
- [ ] Add i18n keys if custom labels needed
|
|
710
|
+
- [ ] Test in block editor: empty state, selection, change, remove
|
|
711
|
+
|
|
712
|
+
### Creating Custom Media Workflow
|
|
713
|
+
|
|
714
|
+
- [ ] Use TanStack Query hooks from `useMedia.ts` (not raw fetch)
|
|
715
|
+
- [ ] Handle loading states with proper skeleton/spinner
|
|
716
|
+
- [ ] Handle error states with user-friendly messages
|
|
717
|
+
- [ ] Invalidate media queries after mutations
|
|
718
|
+
- [ ] Respect upload limits from `app.config.ts`
|
|
719
|
+
- [ ] Include duplicate detection via hash check
|
|
720
|
+
- [ ] All UI text uses `media` i18n namespace
|
|
721
|
+
- [ ] All interactive elements have `data-cy` selectors
|
|
722
|
+
|
|
723
|
+
### API Integration
|
|
724
|
+
|
|
725
|
+
- [ ] Uses dual auth (session + API key)
|
|
726
|
+
- [ ] Validates input with Zod schemas
|
|
727
|
+
- [ ] Respects `media:read`, `media:write`, `media:delete` scopes
|
|
728
|
+
- [ ] Pagination parameters handled correctly
|
|
729
|
+
- [ ] File upload uses `FormData` (not JSON)
|
|
730
|
+
- [ ] Response follows standard `{ data, total, limit, offset }` format
|
|
731
|
+
|
|
732
|
+
## Related Skills
|
|
733
|
+
|
|
734
|
+
- `page-builder-blocks` - Block field type `'media-library'` integration
|
|
735
|
+
- `entity-system` - MediaSelector for entity forms
|
|
736
|
+
- `tanstack-query` - useMedia hooks follow TanStack Query patterns
|
|
737
|
+
- `cypress-api` - Media API test patterns
|
|
738
|
+
- `how-to:handle-file-uploads` - Upload component and API guide
|
|
739
|
+
- `service-layer` - MediaService follows static class pattern with RLS
|
|
740
|
+
- `i18n-nextintl` - `media` namespace translations
|
|
741
|
+
- `cypress-selectors` - `MEDIA_SELECTORS` and `BLOCK_EDITOR_SELECTORS`
|
|
742
|
+
- `zod-validation` - Media schema validation patterns
|
|
743
|
+
- `database-migrations` - `021_media.sql` migration structure
|