Files
site-builder/craft/src/hooks/useAssets.ts
T
shadowdaoandClaude Sonnet 5.5 9d425afa29 feat(editor): adopt backend final-fix contract (client_id, warnings, limits, head parity)
client_id per editor load (memory only), already_saved, save warnings via notices,
upload_max_bytes enforced client-side with the server error text on 413,
project_max_bytes warning, headCode in the Preview font scan, pinned backend heads.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:49:53 -07:00

102 lines
3.4 KiB
TypeScript

import { useState, useCallback } from 'react';
import { useEditorConfig } from '../state/EditorConfigContext';
import { AssetData } from '../types';
import { uploadSizeError, uploadFailureText } from '../utils/site-limits';
export function useAssets() {
const { whpConfig, isWHP } = useEditorConfig();
const [assets, setAssets] = useState<AssetData[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadAssets = useCallback(async () => {
if (!isWHP || !whpConfig) return;
setLoading(true);
setError(null);
try {
const resp = await fetch(
`${whpConfig.apiUrl}?action=list_assets&site_id=${whpConfig.siteId}`,
{ headers: { 'X-CSRF-Token': whpConfig.csrfToken } }
);
if (!resp.ok) throw new Error(`Failed to load assets: ${resp.status}`);
const data = await resp.json();
if (data.success && Array.isArray(data.assets)) {
setAssets(data.assets.map((item: any) => ({
name: item.name || '',
url: item.url || '',
type: item.type || 'file',
size: item.size,
modified: item.modified,
})));
}
} catch (e) {
const msg = e instanceof Error ? e.message : 'Failed to load assets';
setError(msg);
} finally {
setLoading(false);
}
}, [isWHP, whpConfig]);
const uploadAsset = useCallback(async (file: File): Promise<string | null> => {
if (!isWHP || !whpConfig) return null;
const tooBig = uploadSizeError(file);
if (tooBig) { setError(tooBig); return null; }
setLoading(true);
setError(null);
try {
const formData = new FormData();
formData.append('file', file);
const resp = await fetch(
`${whpConfig.apiUrl}?action=upload_asset&site_id=${whpConfig.siteId}`,
{
method: 'POST',
headers: { 'X-CSRF-Token': whpConfig.csrfToken },
body: formData,
}
);
let data: any = null;
try { data = await resp.json(); } catch { /* non-JSON body */ }
// The server's `error` is human text (the 413 size message included): show it as-is.
if (!resp.ok || !data?.success) throw new Error(uploadFailureText(data, resp.status));
const newAsset: AssetData = {
name: data.name || file.name,
url: data.url || '',
type: data.type || file.type || 'file',
size: file.size,
modified: Date.now(),
};
setAssets((prev) => [newAsset, ...prev]);
return newAsset.url;
} catch (e) {
const msg = e instanceof Error ? e.message : 'Upload failed';
setError(msg);
return null;
} finally {
setLoading(false);
}
}, [isWHP, whpConfig]);
const deleteAsset = useCallback(async (filename: string) => {
if (!isWHP || !whpConfig) return;
setError(null);
try {
const resp = await fetch(
`${whpConfig.apiUrl}?action=delete_asset&site_id=${whpConfig.siteId}&filename=${encodeURIComponent(filename)}`,
{
method: 'POST',
headers: { 'X-CSRF-Token': whpConfig.csrfToken },
}
);
if (!resp.ok) throw new Error(`Delete failed: ${resp.status}`);
setAssets((prev) => prev.filter((a) => a.name !== filename));
} catch (e) {
const msg = e instanceof Error ? e.message : 'Delete failed';
setError(msg);
}
}, [isWHP, whpConfig]);
return { assets, loading, error, loadAssets, uploadAsset, deleteAsset };
}