add key()

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-13 12:47:04 +08:00
parent e9b99f8243
commit bfc4b9fc67
15 changed files with 221 additions and 76 deletions
+80 -9
View File
@@ -1,7 +1,52 @@
import { createContext, useCallback, useContext, useRef, useState } from "react";
import { nanoid } from "nanoid";
import api from "@/utils/api.util";
import { useAuth } from "@/contexts/AuthContext";
import { toast } from "sonner";
// ─── Upload progress stream (Express -> Garage, real bytes) ───────────────────
//
// Native EventSource can't set the Authorization header this app authenticates
// with, so GET /admin/assets/upload-progress/:uploadId is consumed via a
// manually-parsed, authenticated fetch() stream instead of EventSource.
// Returns a stop() function. Failures here are swallowed on purpose — this is
// a best-effort visual on top of the real upload, never load-bearing for it.
function streamUploadProgress(uploadId, token, onProgress) {
const controller = new AbortController();
(async () => {
try {
const res = await fetch(`${import.meta.env.VITE_API_URL}/admin/assets/upload-progress/${uploadId}`, {
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
signal: controller.signal,
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const chunks = buffer.split("\n\n");
buffer = chunks.pop(); // keep the last, possibly-incomplete chunk for next read
for (const chunk of chunks) {
const line = chunk.split("\n").find((l) => l.startsWith("data: "));
if (!line) continue;
const data = JSON.parse(line.slice(6));
onProgress(data);
if (data.done) return;
}
}
} catch (err) {
if (err.name !== "AbortError") console.warn("[ASSET][UPLOAD PROGRESS STREAM]", err.message);
}
})();
return () => controller.abort();
}
const AssetsContext = createContext(null);
export function useAssets() {
@@ -31,6 +76,7 @@ const cacheKeyFor = (scope, { page, limit, filters, sort }) =>
`${scope}:${JSON.stringify({ page, limit, filters, sort })}`;
export function AssetsProvider({ children }) {
const { accessTokenRef } = useAuth();
const [assets, setAssets] = useState([]);
const [attributes, setAttributes] = useState([]);
const [pagination, setPagination] = useState(PAGINATION_INIT);
@@ -186,26 +232,51 @@ export function AssetsProvider({ children }) {
);
// ─── POST /api/admin/assets ───────────────────────────────────────────────
//
// onProgress?: ({ phase: 'uploading'|'storing'|'done'|'error', pct }) => void
// Two honest, sequential phases — not one blended/estimated number:
// "uploading" — browser -> this backend, real bytes sent (axios onUploadProgress).
// "storing" — this backend -> Garage, the actual S3 PUT, streamed live over
// SSE (see streamUploadProgress above / uploadProgress.service.js
// on the backend). Real numbers from the real transfer, both legs.
const uploadAsset = useCallback(
({ file, thumbnail, ...rest }) =>
({ file, thumbnail, onProgress, ...rest }) =>
request(async () => {
const uploadId = nanoid();
const form = new FormData();
form.append("file", file);
if (thumbnail) form.append("thumbnail", thumbnail);
form.append("uploadId", uploadId);
Object.entries(rest).forEach(([k, v]) => {
if (v !== undefined && v !== null) form.append(k, v);
});
const res = await api.post("/admin/assets", form);
const asset = res.data?.data?.data ?? null;
if (asset) {
setAssets((prev) => [asset, ...prev]);
invalidateListCache();
toast("Asset uploaded successfully.");
const stopStream = onProgress
? streamUploadProgress(uploadId, accessTokenRef.current, onProgress)
: null;
try {
const res = await api.post("/admin/assets", form, {
onUploadProgress: onProgress
? (evt) => {
const pct = evt.total ? Math.round((evt.loaded / evt.total) * 100) : 0;
onProgress({ phase: "uploading", pct });
}
: undefined,
});
const asset = res.data?.data?.data ?? null;
if (asset) {
setAssets((prev) => [asset, ...prev]);
invalidateListCache();
toast("Asset uploaded successfully.");
}
onProgress?.({ phase: "done", pct: 100 });
return res.data;
} finally {
stopStream?.();
}
return res.data;
}),
[request]
[request, accessTokenRef]
);
// ─── PATCH /api/admin/assets/:assetId ────────────────────────────────────