Add Craft.js site builder (v2) - complete rebuild from GrapesJS
Rebuilt the visual site builder from scratch using Craft.js, React 18, and TypeScript. The new editor renders directly in the DOM (no iframe), supports 40+ components, multi-page with shared header/footer, 16 templates, full-spectrum color/gradient controls, custom head code injection, save/publish workflow, and auto-save. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useEditorConfig } from '../state/EditorConfigContext';
|
||||
import { AssetData } from '../types';
|
||||
|
||||
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;
|
||||
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,
|
||||
}
|
||||
);
|
||||
if (!resp.ok) throw new Error(`Upload failed: ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
if (!data.success) throw new Error(data.error || 'Upload failed');
|
||||
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user