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,249 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useAssets } from '../../hooks/useAssets';
|
||||
|
||||
export const AssetsPanel: React.FC = () => {
|
||||
const { assets, loading, error, loadAssets, uploadAsset, deleteAsset } = useAssets();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [copiedUrl, setCopiedUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadAssets();
|
||||
}, [loadAssets]);
|
||||
|
||||
const handleFileSelect = useCallback(async (files: FileList | null) => {
|
||||
if (!files) return;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
await uploadAsset(files[i]);
|
||||
}
|
||||
}, [uploadAsset]);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
handleFileSelect(e.dataTransfer.files);
|
||||
}, [handleFileSelect]);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
}, []);
|
||||
|
||||
const copyUrl = useCallback((url: string) => {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
setCopiedUrl(url);
|
||||
setTimeout(() => setCopiedUrl(null), 2000);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isImage = (type: string) =>
|
||||
type.startsWith('image/') || /\.(jpg|jpeg|png|gif|svg|webp|ico)$/i.test(type);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{/* Upload button */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => handleFileSelect(e.target.files)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: '#fff',
|
||||
background: loading ? 'var(--color-bg-active)' : 'var(--color-accent)',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
transition: 'all var(--transition-fast)',
|
||||
}}
|
||||
>
|
||||
{loading ? 'Uploading...' : 'Upload File'}
|
||||
</button>
|
||||
|
||||
{/* Drop zone */}
|
||||
<div
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
style={{
|
||||
padding: 20,
|
||||
border: `2px dashed ${isDragOver ? 'var(--color-accent)' : 'var(--color-border)'}`,
|
||||
borderRadius: 'var(--radius-md)',
|
||||
background: isDragOver ? 'var(--color-accent-subtle)' : 'transparent',
|
||||
textAlign: 'center',
|
||||
color: isDragOver ? 'var(--color-accent)' : 'var(--color-text-dim)',
|
||||
fontSize: 11,
|
||||
transition: 'all var(--transition-fast)',
|
||||
}}
|
||||
>
|
||||
Drop files here to upload
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
padding: '6px 10px',
|
||||
fontSize: 11,
|
||||
color: 'var(--color-danger)',
|
||||
background: 'rgba(239, 68, 68, 0.1)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.2)',
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Asset grid */}
|
||||
{assets.length === 0 && !loading && (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: 20,
|
||||
color: 'var(--color-text-dim)',
|
||||
fontSize: 12,
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
>
|
||||
No assets uploaded yet
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
{assets.map((asset) => (
|
||||
<div
|
||||
key={asset.name}
|
||||
style={{
|
||||
position: 'relative',
|
||||
background: 'var(--color-bg-elevated)',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
transition: 'border-color var(--transition-fast)',
|
||||
}}
|
||||
onClick={() => copyUrl(asset.url)}
|
||||
title={`Click to copy URL: ${asset.url}`}
|
||||
>
|
||||
{/* Thumbnail */}
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'var(--color-bg-base)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{isImage(asset.type) ? (
|
||||
<img
|
||||
src={asset.url}
|
||||
alt={asset.name}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: 'var(--color-text-dim)',
|
||||
}}
|
||||
>
|
||||
📄
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div
|
||||
style={{
|
||||
padding: '4px 6px',
|
||||
fontSize: 10,
|
||||
color: 'var(--color-text-muted)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{copiedUrl === asset.url ? 'Copied!' : asset.name}
|
||||
</div>
|
||||
|
||||
{/* Delete button */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteAsset(asset.name);
|
||||
}}
|
||||
title="Delete asset"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
right: 4,
|
||||
width: 20,
|
||||
height: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 10,
|
||||
color: '#fff',
|
||||
background: 'rgba(0,0,0,0.6)',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
cursor: 'pointer',
|
||||
opacity: 0.7,
|
||||
transition: 'opacity var(--transition-fast)',
|
||||
}}
|
||||
onMouseEnter={(e) => { (e.target as HTMLElement).style.opacity = '1'; }}
|
||||
onMouseLeave={(e) => { (e.target as HTMLElement).style.opacity = '0.7'; }}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Loading indicator */}
|
||||
{loading && assets.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: 10,
|
||||
color: 'var(--color-text-dim)',
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user