bknd-file-upload
Use when uploading files to Bknd storage. Covers MediaApi SDK methods (upload, uploadToEntity), REST endpoints, React integration with file inputs, progress tracking with XHR, browser upload patterns, and entity field attachments.
What this skill does
# File Upload
Upload files to Bknd storage using the MediaApi SDK or REST endpoints.
## Prerequisites
- Media module enabled in Bknd config
- Storage adapter configured (S3, R2, Cloudinary, or local)
- `bknd` package installed
- For entity uploads: target entity has `media()` field defined
## When to Use UI Mode
- Admin panel > Media section > drag-and-drop upload
- Quick testing of storage configuration
- One-off file uploads without code
## When to Use Code Mode
- Programmatic uploads from frontend
- Upload with progress tracking
- Attach files to entity records
- Custom upload UI components
## Step-by-Step: UI Approach
### Step 1: Open Admin Panel
Navigate to `http://localhost:7654` (or your Bknd URL).
### Step 2: Go to Media Section
Click "Media" in sidebar to access file management.
### Step 3: Upload Files
- Drag files into the upload area, OR
- Click "Upload" button and select files
### Step 4: Verify Upload
Files appear in the list with name, size, type, and date.
## Step-by-Step: Code Approach
### Basic File Upload
```typescript
import { Api } from "bknd";
const api = new Api({ host: "http://localhost:7654" });
// From File object (browser)
const input = document.querySelector('input[type="file"]');
const file = input.files[0];
const { ok, data, error } = await api.media.upload(file);
if (ok) {
console.log("Uploaded:", data.name);
console.log("Size:", data.meta.size);
console.log("Type:", data.meta.type);
}
```
### Upload with Custom Filename
```typescript
const { ok, data } = await api.media.upload(file, {
filename: "custom-name.png",
});
```
### Upload from URL
```typescript
// Direct URL string
const { ok, data } = await api.media.upload("https://example.com/image.png");
// Or from fetch Response
const response = await fetch("https://example.com/image.png");
const { ok, data } = await api.media.upload(response);
```
### Upload to Entity Field
Attach file directly to an entity record:
```typescript
// Assumes "posts" entity has a media() field called "cover_image"
const { ok, data } = await api.media.uploadToEntity(
"posts", // entity name
123, // record ID
"cover_image", // field name
file,
{ overwrite: true } // replace existing file
);
if (ok) {
console.log("File attached:", data.result.cover_image);
}
```
## React Integration
### Simple File Input
```tsx
function FileUpload({ onUploaded }) {
const { api } = useApp(); // or your Api instance
const [uploading, setUploading] = useState(false);
const [error, setError] = useState(null);
const handleChange = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
setError(null);
const { ok, data, error } = await api.media.upload(file);
setUploading(false);
if (ok) {
onUploaded(data);
} else {
setError(error?.message || "Upload failed");
}
};
return (
<div>
<input
type="file"
onChange={handleChange}
disabled={uploading}
/>
{uploading && <span>Uploading...</span>}
{error && <span style={{ color: "red" }}>{error}</span>}
</div>
);
}
```
### Image Upload with Preview
```tsx
function ImageUpload({ value, onChange }) {
const { api } = useApp();
const [preview, setPreview] = useState(value);
const [uploading, setUploading] = useState(false);
const handleChange = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
// Show local preview immediately
const localUrl = URL.createObjectURL(file);
setPreview(localUrl);
setUploading(true);
const { ok, data } = await api.media.upload(file);
setUploading(false);
if (ok) {
// Clean up local preview
URL.revokeObjectURL(localUrl);
// Use server URL
onChange(data.name);
}
};
return (
<div>
{preview && (
<img
src={preview}
alt="Preview"
style={{ maxWidth: 200, opacity: uploading ? 0.5 : 1 }}
/>
)}
<input type="file" accept="image/*" onChange={handleChange} />
{uploading && <span>Uploading...</span>}
</div>
);
}
```
### Upload with Progress Tracking
For large files, use XHR for progress:
```tsx
function ProgressUpload({ onUploaded }) {
const { api } = useApp();
const [progress, setProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const uploadWithProgress = (file) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const url = api.media.getFileUploadUrl({ path: file.name });
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
setProgress(Math.round((e.loaded / e.total) * 100));
}
});
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(xhr.statusText));
}
});
xhr.addEventListener("error", () => reject(new Error("Upload failed")));
xhr.open("POST", url);
xhr.setRequestHeader("Content-Type", file.type);
// Add auth if using header transport
const token = api.getAuthState().token;
if (token) {
xhr.setRequestHeader("Authorization", `Bearer ${token}`);
}
xhr.send(file);
});
};
const handleChange = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
setProgress(0);
try {
const result = await uploadWithProgress(file);
onUploaded(result);
} catch (err) {
console.error("Upload failed:", err);
} finally {
setUploading(false);
}
};
return (
<div>
<input type="file" onChange={handleChange} disabled={uploading} />
{uploading && (
<div>
<progress value={progress} max={100} />
<span>{progress}%</span>
</div>
)}
</div>
);
}
```
### Avatar Upload Component
Complete avatar upload with entity attachment:
```tsx
function AvatarUpload({ userId, currentAvatar }) {
const { api } = useApp();
const [preview, setPreview] = useState(currentAvatar);
const [uploading, setUploading] = useState(false);
const handleChange = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
// Validate image
if (!file.type.startsWith("image/")) {
alert("Please select an image file");
return;
}
if (file.size > 5 * 1024 * 1024) {
alert("Image must be under 5MB");
return;
}
setPreview(URL.createObjectURL(file));
setUploading(true);
const { ok, data } = await api.media.uploadToEntity(
"users",
userId,
"avatar",
file,
{ overwrite: true }
);
setUploading(false);
if (ok) {
setPreview(data.result.avatar);
}
};
return (
<div>
<img
src={preview || "/default-avatar.png"}
alt="Avatar"
style={{
width: 100,
height: 100,
borderRadius: "50%",
opacity: uploading ? 0.5 : 1
}}
/>
<label>
<input
type="file"
accept="image/*"
onChange={handleChange}
disabled={uploading}
style={{ display: "none" }}
/>
<button type="button" disabled={uploading}>
{uploading ? "Uploading..." : "Change Avatar"}
</button>
</label>
</div>
);
}
```
## REST API
### Upload Endpoint
```bash
# Basic upload (filename in path)
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: image/png" \
--data-binary @image.png \
http://localhost:7654/api/media/upload/image.png
# Upload to entity field
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpg \
"http://localhost:7654/api/media/entity/users/123/avaRelated in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.