@nextsparkjs/ai-workflow 0.1.0-beta.86
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 +86 -0
- package/claude/_docs/workflows-optimizations.md +359 -0
- package/claude/agents/api-tester.md +636 -0
- package/claude/agents/architecture-supervisor.md +1381 -0
- package/claude/agents/backend-developer.md +1021 -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 +1460 -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 +1291 -0
- package/claude/agents/frontend-developer.md +1259 -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 +971 -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 +373 -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 +681 -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 +512 -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 +455 -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/close.md +146 -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 +476 -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 +54 -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 +49 -0
- package/claude/config/workspace.schema.json +64 -0
- package/claude/scripts/.gitkeep +0 -0
- package/claude/sessions/.gitkeep +0 -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/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 +483 -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 +677 -0
- package/claude/skills/registry-system/SKILL.md +331 -0
- package/claude/skills/scheduled-actions/SKILL.md +431 -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 +479 -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 +34 -0
- package/scripts/setup.mjs +282 -0
- package/scripts/sync.mjs +209 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
# /how-to:handle-file-uploads
|
|
2
|
+
|
|
3
|
+
Interactive guide to implement file uploads and media management in NextSpark.
|
|
4
|
+
|
|
5
|
+
**Aliases:** `/how-to:media`, `/how-to:file-upload`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Required Skills
|
|
10
|
+
|
|
11
|
+
Before executing, these skills provide deeper context:
|
|
12
|
+
- `.claude/skills/entity-system/SKILL.md` - Entity field definitions
|
|
13
|
+
- `.claude/skills/zod-validation/SKILL.md` - Input validation patterns
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Syntax
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
/how-to:handle-file-uploads
|
|
21
|
+
/how-to:handle-file-uploads --component image
|
|
22
|
+
/how-to:handle-file-uploads --entity
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Behavior
|
|
28
|
+
|
|
29
|
+
Guides the user through implementing file uploads, using upload components, and adding file fields to entities.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Tutorial Structure
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
STEPS OVERVIEW (5 steps)
|
|
37
|
+
|
|
38
|
+
Step 1: Understanding the Media System
|
|
39
|
+
└── Vercel Blob, supported types, limits
|
|
40
|
+
|
|
41
|
+
Step 2: Configure Environment
|
|
42
|
+
└── BLOB_READ_WRITE_TOKEN setup
|
|
43
|
+
|
|
44
|
+
Step 3: Using Upload Components
|
|
45
|
+
└── FileUpload, ImageUpload, VideoUpload, AudioUpload
|
|
46
|
+
|
|
47
|
+
Step 4: Adding File Fields to Entities
|
|
48
|
+
└── Entity field types and configuration
|
|
49
|
+
|
|
50
|
+
Step 5: Custom Upload Handling
|
|
51
|
+
└── API endpoint, permissions, advanced usage
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Step 1: Understanding the Media System
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
60
|
+
📚 HOW TO: HANDLE FILE UPLOADS
|
|
61
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
62
|
+
|
|
63
|
+
STEP 1 OF 5: Understanding the Media System
|
|
64
|
+
|
|
65
|
+
NextSpark uses Vercel Blob for cloud file storage
|
|
66
|
+
with specialized components for different media types.
|
|
67
|
+
|
|
68
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**📋 Storage Architecture:**
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
┌─────────────────────────────────────────────┐
|
|
75
|
+
│ VERCEL BLOB STORAGE │
|
|
76
|
+
│ ───────────────────────────────────────── │
|
|
77
|
+
│ • Cloud-based file storage │
|
|
78
|
+
│ • Public URLs for uploaded files │
|
|
79
|
+
│ • Automatic CDN distribution │
|
|
80
|
+
│ • No server storage required │
|
|
81
|
+
└─────────────────────────────────────────────┘
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**📋 Supported File Types:**
|
|
85
|
+
|
|
86
|
+
| Category | MIME Types | Extensions |
|
|
87
|
+
|----------|------------|------------|
|
|
88
|
+
| Images | image/jpeg, image/png, image/gif, image/webp | .jpg, .png, .gif, .webp |
|
|
89
|
+
| Videos | video/mp4, video/mpeg, video/quicktime, video/webm | .mp4, .mpeg, .mov, .webm |
|
|
90
|
+
| Audio | audio/mpeg, audio/wav, audio/ogg, audio/m4a | .mp3, .wav, .ogg, .m4a |
|
|
91
|
+
| Files | Any | Any (configurable) |
|
|
92
|
+
|
|
93
|
+
**📋 Limits:**
|
|
94
|
+
|
|
95
|
+
| Limit | Value |
|
|
96
|
+
|-------|-------|
|
|
97
|
+
| Max file size | 10 MB per file |
|
|
98
|
+
| Max files per upload | 5 files (configurable) |
|
|
99
|
+
| Storage path | `uploads/temp/{timestamp}_{random}.{ext}` |
|
|
100
|
+
|
|
101
|
+
**📋 Available Components:**
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
packages/core/src/components/ui/
|
|
105
|
+
├── file-upload.tsx # Generic file upload
|
|
106
|
+
├── image-upload.tsx # Image-specific with preview
|
|
107
|
+
├── video-upload.tsx # Video with thumbnail generation
|
|
108
|
+
└── audio-upload.tsx # Audio with built-in player
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
113
|
+
|
|
114
|
+
What would you like to do?
|
|
115
|
+
|
|
116
|
+
[1] Continue to Step 2 (Configure Environment)
|
|
117
|
+
[2] Can I use S3 or Cloudinary instead?
|
|
118
|
+
[3] What are the storage costs?
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Step 2: Configure Environment
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
127
|
+
STEP 2 OF 5: Configure Environment
|
|
128
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
129
|
+
|
|
130
|
+
Set up Vercel Blob storage for your project.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**📋 Required Environment Variables:**
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# .env or .env.local
|
|
137
|
+
|
|
138
|
+
# Vercel Blob Storage Token (REQUIRED)
|
|
139
|
+
BLOB_READ_WRITE_TOKEN=vercel_blob_rw_xxxxxxxxxxxxx
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**📋 Getting a Blob Token:**
|
|
143
|
+
|
|
144
|
+
1. Go to your Vercel project dashboard
|
|
145
|
+
2. Navigate to **Storage** tab
|
|
146
|
+
3. Create a new **Blob** store
|
|
147
|
+
4. Copy the `BLOB_READ_WRITE_TOKEN`
|
|
148
|
+
5. Add to your `.env.local` file
|
|
149
|
+
|
|
150
|
+
**📋 Local Development:**
|
|
151
|
+
|
|
152
|
+
For local development, you still need a Vercel Blob token. The files are stored in Vercel's cloud even during development.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Verify your token is set
|
|
156
|
+
echo $BLOB_READ_WRITE_TOKEN
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**📋 Package Dependencies:**
|
|
160
|
+
|
|
161
|
+
The required package is already included:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"@vercel/blob": "^2.0.0"
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
171
|
+
|
|
172
|
+
What would you like to do?
|
|
173
|
+
|
|
174
|
+
[1] Continue to Step 3 (Upload Components)
|
|
175
|
+
[2] I don't use Vercel, what are my options?
|
|
176
|
+
[3] How do I test without a token?
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Step 3: Using Upload Components
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
185
|
+
STEP 3 OF 5: Using Upload Components
|
|
186
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
187
|
+
|
|
188
|
+
NextSpark provides specialized components for
|
|
189
|
+
different media types.
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**📋 1. FileUpload (Generic):**
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { FileUpload } from '@/core/components/ui/file-upload'
|
|
196
|
+
|
|
197
|
+
interface FileUploadProps {
|
|
198
|
+
value: UploadedFile[]
|
|
199
|
+
onChange: (files: UploadedFile[]) => void
|
|
200
|
+
maxFiles?: number // Default: 5
|
|
201
|
+
maxSize?: number // MB, Default: 10
|
|
202
|
+
acceptedTypes?: string[] // Default: ["*"]
|
|
203
|
+
disabled?: boolean
|
|
204
|
+
multiple?: boolean // Default: true
|
|
205
|
+
dragDrop?: boolean // Default: true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Usage
|
|
209
|
+
<FileUpload
|
|
210
|
+
value={files}
|
|
211
|
+
onChange={setFiles}
|
|
212
|
+
maxFiles={3}
|
|
213
|
+
maxSize={5}
|
|
214
|
+
acceptedTypes={['application/pdf', 'text/*']}
|
|
215
|
+
/>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**📋 2. ImageUpload (Images with Preview):**
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { ImageUpload } from '@/core/components/ui/image-upload'
|
|
222
|
+
|
|
223
|
+
interface ImageUploadProps {
|
|
224
|
+
value: UploadedImage[]
|
|
225
|
+
onChange: (images: UploadedImage[]) => void
|
|
226
|
+
maxImages?: number // Default: 5
|
|
227
|
+
maxSize?: number // MB, Default: 5
|
|
228
|
+
aspectRatio?: 'square' | 'landscape' | 'portrait' | 'free'
|
|
229
|
+
showPreview?: boolean // Default: true
|
|
230
|
+
multiple?: boolean
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Usage
|
|
234
|
+
<ImageUpload
|
|
235
|
+
value={images}
|
|
236
|
+
onChange={setImages}
|
|
237
|
+
maxImages={1}
|
|
238
|
+
aspectRatio="square"
|
|
239
|
+
/>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**📋 3. VideoUpload (Videos with Thumbnails):**
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { VideoUpload } from '@/core/components/ui/video-upload'
|
|
246
|
+
|
|
247
|
+
interface VideoUploadProps {
|
|
248
|
+
value: UploadedVideo[]
|
|
249
|
+
onChange: (videos: UploadedVideo[]) => void
|
|
250
|
+
maxVideos?: number // Default: 3
|
|
251
|
+
maxSize?: number // MB, Default: 100
|
|
252
|
+
acceptedFormats?: string[] // Default: ["mp4", "mov", "avi", "mkv", "webm"]
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Usage - auto-generates thumbnails!
|
|
256
|
+
<VideoUpload
|
|
257
|
+
value={videos}
|
|
258
|
+
onChange={setVideos}
|
|
259
|
+
maxVideos={1}
|
|
260
|
+
maxSize={50}
|
|
261
|
+
/>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**📋 4. AudioUpload (Audio with Player):**
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { AudioUpload } from '@/core/components/ui/audio-upload'
|
|
268
|
+
|
|
269
|
+
interface AudioUploadProps {
|
|
270
|
+
value: UploadedAudio[]
|
|
271
|
+
onChange: (audios: UploadedAudio[]) => void
|
|
272
|
+
maxAudios?: number // Default: 5
|
|
273
|
+
maxSize?: number // MB, Default: 50
|
|
274
|
+
acceptedFormats?: string[] // Default: ["mp3", "wav", "ogg", "m4a", "aac"]
|
|
275
|
+
showPlayer?: boolean // Default: true
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Usage - includes built-in audio player!
|
|
279
|
+
<AudioUpload
|
|
280
|
+
value={audioFiles}
|
|
281
|
+
onChange={setAudioFiles}
|
|
282
|
+
showPlayer={true}
|
|
283
|
+
/>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**📋 Uploaded File Types:**
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
interface UploadedFile {
|
|
290
|
+
id: string
|
|
291
|
+
name: string
|
|
292
|
+
size: number
|
|
293
|
+
type: string
|
|
294
|
+
url?: string
|
|
295
|
+
uploadProgress?: number
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface UploadedImage extends UploadedFile {
|
|
299
|
+
alt?: string
|
|
300
|
+
width?: number
|
|
301
|
+
height?: number
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
interface UploadedVideo extends UploadedFile {
|
|
305
|
+
duration?: number
|
|
306
|
+
thumbnail?: string // Auto-generated
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
interface UploadedAudio extends UploadedFile {
|
|
310
|
+
duration?: number
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
316
|
+
|
|
317
|
+
What would you like to do?
|
|
318
|
+
|
|
319
|
+
[1] Continue to Step 4 (Entity Fields)
|
|
320
|
+
[2] Show me a complete form example
|
|
321
|
+
[3] How do I customize the upload UI?
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Step 4: Adding File Fields to Entities
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
330
|
+
STEP 4 OF 5: Adding File Fields to Entities
|
|
331
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
332
|
+
|
|
333
|
+
Add file fields to your entities for automatic
|
|
334
|
+
form and display handling.
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**📋 Available Field Types:**
|
|
338
|
+
|
|
339
|
+
| Type | Component | Use Case |
|
|
340
|
+
|------|-----------|----------|
|
|
341
|
+
| `file` | FileUpload | Documents, PDFs, any files |
|
|
342
|
+
| `image` | ImageUpload | Photos, avatars, graphics |
|
|
343
|
+
| `video` | VideoUpload | Video content |
|
|
344
|
+
| `audio` | AudioUpload | Audio files, podcasts |
|
|
345
|
+
|
|
346
|
+
**📋 Entity Field Definition:**
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
// In your entity.fields.ts
|
|
350
|
+
|
|
351
|
+
// Image field example
|
|
352
|
+
{
|
|
353
|
+
name: 'featuredImage',
|
|
354
|
+
type: 'image',
|
|
355
|
+
required: false,
|
|
356
|
+
display: {
|
|
357
|
+
label: 'Featured Image',
|
|
358
|
+
description: 'Main image for the post',
|
|
359
|
+
placeholder: 'Upload an image...',
|
|
360
|
+
showInList: false, // Hide in table (shows count)
|
|
361
|
+
showInDetail: true,
|
|
362
|
+
showInForm: true,
|
|
363
|
+
order: 5,
|
|
364
|
+
},
|
|
365
|
+
api: {
|
|
366
|
+
searchable: false,
|
|
367
|
+
sortable: false,
|
|
368
|
+
readOnly: false,
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// File attachments example
|
|
373
|
+
{
|
|
374
|
+
name: 'attachments',
|
|
375
|
+
type: 'file',
|
|
376
|
+
required: false,
|
|
377
|
+
display: {
|
|
378
|
+
label: 'Attachments',
|
|
379
|
+
description: 'Upload project files (max 5)',
|
|
380
|
+
showInList: false,
|
|
381
|
+
showInDetail: true,
|
|
382
|
+
showInForm: true,
|
|
383
|
+
order: 10,
|
|
384
|
+
},
|
|
385
|
+
api: {
|
|
386
|
+
searchable: false,
|
|
387
|
+
sortable: false,
|
|
388
|
+
readOnly: false,
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**📋 Database Storage:**
|
|
394
|
+
|
|
395
|
+
Files are stored as JSONB arrays in the database:
|
|
396
|
+
|
|
397
|
+
```sql
|
|
398
|
+
-- In your migration
|
|
399
|
+
ALTER TABLE posts ADD COLUMN featured_image jsonb;
|
|
400
|
+
ALTER TABLE posts ADD COLUMN attachments jsonb;
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
```json
|
|
404
|
+
// Stored structure
|
|
405
|
+
[
|
|
406
|
+
{
|
|
407
|
+
"id": "1704067200000-0.456",
|
|
408
|
+
"name": "image.jpg",
|
|
409
|
+
"size": 245678,
|
|
410
|
+
"type": "image/jpeg",
|
|
411
|
+
"url": "https://xxxxx.public.blob.vercel-storage.com/uploads/temp/...",
|
|
412
|
+
"alt": "Description"
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**📋 EntityFieldRenderer Integration:**
|
|
418
|
+
|
|
419
|
+
The entity system automatically renders the correct component:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
// This happens automatically in EntityFieldRenderer.tsx
|
|
423
|
+
case 'image':
|
|
424
|
+
return (
|
|
425
|
+
<ImageUpload
|
|
426
|
+
value={Array.isArray(value) ? value : []}
|
|
427
|
+
onChange={onChange}
|
|
428
|
+
disabled={disabled}
|
|
429
|
+
multiple={true}
|
|
430
|
+
/>
|
|
431
|
+
)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**📋 Display in List/Detail Views:**
|
|
435
|
+
|
|
436
|
+
File fields show a count in list and detail views:
|
|
437
|
+
|
|
438
|
+
```
|
|
439
|
+
// In list view column
|
|
440
|
+
"2 archivos"
|
|
441
|
+
|
|
442
|
+
// In detail view
|
|
443
|
+
"3 archivos"
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
```
|
|
447
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
448
|
+
|
|
449
|
+
What would you like to do?
|
|
450
|
+
|
|
451
|
+
[1] Continue to Step 5 (Custom Upload Handling)
|
|
452
|
+
[2] How do I validate file types in schema?
|
|
453
|
+
[3] Can I have a single image instead of array?
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
## Step 5: Custom Upload Handling
|
|
459
|
+
|
|
460
|
+
```
|
|
461
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
462
|
+
STEP 5 OF 5: Custom Upload Handling
|
|
463
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
464
|
+
|
|
465
|
+
For advanced use cases, interact directly
|
|
466
|
+
with the upload API.
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**📋 Upload API Endpoint:**
|
|
470
|
+
|
|
471
|
+
```
|
|
472
|
+
POST /api/v1/media/upload
|
|
473
|
+
Content-Type: multipart/form-data
|
|
474
|
+
Authorization: Bearer {apiKey} OR Session Cookie
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**📋 Making Upload Requests:**
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
async function uploadFiles(files: File[]) {
|
|
481
|
+
const formData = new FormData()
|
|
482
|
+
|
|
483
|
+
files.forEach((file, index) => {
|
|
484
|
+
formData.append(`file${index}`, file)
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const response = await fetch('/api/v1/media/upload', {
|
|
488
|
+
method: 'POST',
|
|
489
|
+
body: formData,
|
|
490
|
+
// Note: Don't set Content-Type, browser sets it with boundary
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
const data = await response.json()
|
|
494
|
+
|
|
495
|
+
if (data.success) {
|
|
496
|
+
return data.urls // Array of uploaded file URLs
|
|
497
|
+
} else {
|
|
498
|
+
throw new Error(data.error)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**📋 API Response Format:**
|
|
504
|
+
|
|
505
|
+
```json
|
|
506
|
+
// Success
|
|
507
|
+
{
|
|
508
|
+
"message": "Files uploaded successfully",
|
|
509
|
+
"urls": [
|
|
510
|
+
"https://xxxxx.public.blob.vercel-storage.com/uploads/temp/1704067200000_abc123.jpg",
|
|
511
|
+
"https://xxxxx.public.blob.vercel-storage.com/uploads/temp/1704067200001_def456.pdf"
|
|
512
|
+
],
|
|
513
|
+
"count": 2
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Error
|
|
517
|
+
{
|
|
518
|
+
"success": false,
|
|
519
|
+
"error": "File type not allowed"
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**📋 Required Permissions:**
|
|
524
|
+
|
|
525
|
+
| Scope | Action |
|
|
526
|
+
|-------|--------|
|
|
527
|
+
| `media:write` | Upload files |
|
|
528
|
+
| `media:read` | List/retrieve files |
|
|
529
|
+
|
|
530
|
+
**📋 Deleting Files:**
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import { del } from '@vercel/blob'
|
|
534
|
+
|
|
535
|
+
// Delete a specific file by URL
|
|
536
|
+
await del(fileUrl)
|
|
537
|
+
|
|
538
|
+
// List files (for cleanup)
|
|
539
|
+
import { list } from '@vercel/blob'
|
|
540
|
+
const { blobs } = await list({ prefix: 'uploads/temp/' })
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**📋 Server-Side Validation:**
|
|
544
|
+
|
|
545
|
+
The upload endpoint validates:
|
|
546
|
+
1. File type (MIME type check)
|
|
547
|
+
2. File size (max 10MB)
|
|
548
|
+
3. File count (max per request)
|
|
549
|
+
4. Authentication (session or API key)
|
|
550
|
+
5. Permissions (media:write scope)
|
|
551
|
+
|
|
552
|
+
```
|
|
553
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
554
|
+
|
|
555
|
+
✅ TUTORIAL STORY!
|
|
556
|
+
|
|
557
|
+
You've learned:
|
|
558
|
+
• Media system architecture (Vercel Blob)
|
|
559
|
+
• Environment configuration
|
|
560
|
+
• Using upload components
|
|
561
|
+
• Adding file fields to entities
|
|
562
|
+
• Custom upload handling
|
|
563
|
+
|
|
564
|
+
📚 Related tutorials:
|
|
565
|
+
• /how-to:create-entity - Create entities with file fields
|
|
566
|
+
• /how-to:create-api - Custom API endpoints
|
|
567
|
+
|
|
568
|
+
🔙 Back to menu: /how-to:start
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
## Interactive Options
|
|
574
|
+
|
|
575
|
+
### "Can I use S3 or Cloudinary instead?"
|
|
576
|
+
|
|
577
|
+
```
|
|
578
|
+
📋 Alternative Storage Providers:
|
|
579
|
+
|
|
580
|
+
Currently, NextSpark uses Vercel Blob. To use S3 or Cloudinary:
|
|
581
|
+
|
|
582
|
+
1. Create a custom upload endpoint:
|
|
583
|
+
/app/api/v1/media/upload-s3/route.ts
|
|
584
|
+
|
|
585
|
+
2. Implement your provider:
|
|
586
|
+
|
|
587
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
|
|
588
|
+
|
|
589
|
+
const s3 = new S3Client({ region: 'us-east-1' })
|
|
590
|
+
|
|
591
|
+
export async function POST(request: Request) {
|
|
592
|
+
const formData = await request.formData()
|
|
593
|
+
const file = formData.get('file') as File
|
|
594
|
+
|
|
595
|
+
await s3.send(new PutObjectCommand({
|
|
596
|
+
Bucket: process.env.S3_BUCKET,
|
|
597
|
+
Key: `uploads/${file.name}`,
|
|
598
|
+
Body: Buffer.from(await file.arrayBuffer()),
|
|
599
|
+
ContentType: file.type,
|
|
600
|
+
}))
|
|
601
|
+
|
|
602
|
+
return Response.json({
|
|
603
|
+
url: `https://${process.env.S3_BUCKET}.s3.amazonaws.com/uploads/${file.name}`
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
3. Configure upload components to use your endpoint.
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### "Show me a complete form example"
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
'use client'
|
|
614
|
+
|
|
615
|
+
import { useState } from 'react'
|
|
616
|
+
import { ImageUpload } from '@/core/components/ui/image-upload'
|
|
617
|
+
import { FileUpload } from '@/core/components/ui/file-upload'
|
|
618
|
+
import { Button } from '@/core/components/ui/button'
|
|
619
|
+
|
|
620
|
+
export function ProductForm() {
|
|
621
|
+
const [images, setImages] = useState([])
|
|
622
|
+
const [documents, setDocuments] = useState([])
|
|
623
|
+
|
|
624
|
+
const handleSubmit = async (e) => {
|
|
625
|
+
e.preventDefault()
|
|
626
|
+
|
|
627
|
+
const productData = {
|
|
628
|
+
// ... other fields
|
|
629
|
+
images: images.map(img => ({
|
|
630
|
+
url: img.url,
|
|
631
|
+
alt: img.alt || img.name
|
|
632
|
+
})),
|
|
633
|
+
documents: documents.map(doc => ({
|
|
634
|
+
url: doc.url,
|
|
635
|
+
name: doc.name,
|
|
636
|
+
size: doc.size
|
|
637
|
+
}))
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
await saveProduct(productData)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<form onSubmit={handleSubmit}>
|
|
645
|
+
<div className="space-y-4">
|
|
646
|
+
<div>
|
|
647
|
+
<label>Product Images</label>
|
|
648
|
+
<ImageUpload
|
|
649
|
+
value={images}
|
|
650
|
+
onChange={setImages}
|
|
651
|
+
maxImages={5}
|
|
652
|
+
aspectRatio="square"
|
|
653
|
+
/>
|
|
654
|
+
</div>
|
|
655
|
+
|
|
656
|
+
<div>
|
|
657
|
+
<label>Documentation</label>
|
|
658
|
+
<FileUpload
|
|
659
|
+
value={documents}
|
|
660
|
+
onChange={setDocuments}
|
|
661
|
+
maxFiles={3}
|
|
662
|
+
acceptedTypes={['application/pdf']}
|
|
663
|
+
/>
|
|
664
|
+
</div>
|
|
665
|
+
|
|
666
|
+
<Button type="submit">Save Product</Button>
|
|
667
|
+
</div>
|
|
668
|
+
</form>
|
|
669
|
+
)
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Related Commands
|
|
676
|
+
|
|
677
|
+
| Command | Description |
|
|
678
|
+
|---------|-------------|
|
|
679
|
+
| `/how-to:create-entity` | Create entities with file fields |
|
|
680
|
+
| `/how-to:create-api` | Custom API endpoints |
|
|
681
|
+
| `/how-to:add-metadata` | Add metadata to uploaded files |
|