tahamajs/IE / Projects /front /src /components /authoring /FigureManagerPanel.tsx
tahamajs's picture
download
raw
6.4 kB
// src/components/authoring/FigureManagerPanel.tsx
import { useRef, useState } from "react";
import { Upload, Trash2, Image, GripVertical, ChevronUp, ChevronDown, Plus, Loader2, Check, ExternalLink } from "lucide-react";
interface FigureManagerPanelProps {
figures: any[];
loading: boolean;
uploadingIds: Set<string>;
uploadProgress: Record<string, number>;
addFigure: (file: File, caption?: string) => void;
removeFigure: (id: string) => void;
updateCaption: (id: string, caption: string) => void;
updatePlacement: (id: string, placement: string) => void;
reorderFigures: (ids: string[]) => void;
insertFigureAtCursor: (figure: any, editor: any) => void;
editor: any;
}
export function FigureManagerPanel({ figures, uploadingIds, uploadProgress, addFigure, removeFigure, updateCaption, updatePlacement, reorderFigures, insertFigureAtCursor, editor }: FigureManagerPanelProps) {
const fileRef = useRef<HTMLInputElement>(null);
const [editingCaptionId, setEditingCaptionId] = useState<string | null>(null);
const [captionInput, setCaptionInput] = useState("");
const handleFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files) {
for (const file of Array.from(files)) {
addFigure(file);
}
}
if (fileRef.current) fileRef.current.value = "";
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const files = e.dataTransfer.files;
for (const file of Array.from(files)) {
if (file.type.startsWith("image/")) {
addFigure(file);
}
}
};
return (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 h-full" onDragOver={e => e.preventDefault()} onDrop={handleDrop}>
{/* Upload zone */}
<div className="space-y-3">
<div className="border-2 border-dashed border-white/30 rounded-xl p-4 text-center cursor-pointer hover:border-primary/50 transition"
onClick={() => fileRef.current?.click()}>
<input ref={fileRef} type="file" multiple accept="image/*" className="hidden" onChange={handleFiles}/>
<Upload className="w-6 h-6 text-gray-400 mx-auto mb-1.5"/>
<p className="text-xs text-gray-500">Drop images or click</p>
</div>
<p className="text-[10px] text-gray-400 text-center">Supported: PNG, JPG, PDF, EPS</p>
</div>
{/* Figure grid */}
<div className="lg:col-span-3 overflow-y-auto">
{figures.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<Image className="w-8 h-8 mx-auto mb-2 opacity-30"/>
<p className="text-sm">No figures yet. Drag & drop to add.</p>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{figures.map((fig, idx) => (
<div key={fig.id} className={`relative group rounded-xl border overflow-hidden ${
fig.status === "error" ? "border-red-300 dark:border-red-800" :
uploadingIds.has(fig.id) ? "border-amber-300 dark:border-amber-800" :
"border-white/20"
}`}>
{/* Thumbnail */}
<div className="aspect-video bg-gray-100 dark:bg-gray-700 relative">
{fig.thumbnailUrl ? (
<img src={fig.thumbnailUrl} alt={fig.altText} className="w-full h-full object-cover"/>
) : (
<div className="flex items-center justify-center h-full"><Image className="w-8 h-8 text-gray-400"/></div>
)}
{/* Upload progress */}
{uploadingIds.has(fig.id) && (
<div className="absolute inset-0 bg-black/50 flex flex-col items-center justify-center">
<Loader2 className="w-6 h-6 animate-spin text-white mb-1"/>
<span className="text-xs text-white">{uploadProgress[fig.id] ?? 0}%</span>
</div>
)}
{/* Hover actions */}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition flex items-center justify-center gap-2">
<button onClick={() => insertFigureAtCursor(fig, editor)} className="p-1.5 bg-primary text-white rounded-lg text-xs" title="Insert at cursor">
<Plus className="w-4 h-4"/>
</button>
<button onClick={() => removeFigure(fig.id)} className="p-1.5 bg-red-500 text-white rounded-lg text-xs" title="Remove">
<Trash2 className="w-4 h-4"/>
</button>
</div>
</div>
{/* Info */}
<div className="p-2">
{editingCaptionId === fig.id ? (
<input type="text" value={captionInput} onChange={e => setCaptionInput(e.target.value)}
onKeyDown={e => { if (e.key === "Enter") { updateCaption(fig.id, captionInput); setEditingCaptionId(null); } }}
onBlur={() => { updateCaption(fig.id, captionInput); setEditingCaptionId(null); }}
className="w-full text-xs px-2 py-1 bg-white/10 rounded" autoFocus/>
) : (
<p className="text-xs text-gray-600 dark:text-gray-400 line-clamp-2 cursor-pointer"
onClick={() => { setEditingCaptionId(fig.id); setCaptionInput(fig.caption); }}>
{fig.caption || "Click to add caption"}
</p>
)}
<div className="flex items-center justify-between mt-1.5">
<span className="text-[9px] text-gray-500">Fig. {fig.order}</span>
<select aria-label="Select option" value={fig.placement} onChange={e => updatePlacement(fig.id, e.target.value)}
className="text-[9px] bg-white/10 rounded px-1.5 py-0.5">
<option value="htbp">htbp</option>
<option value="ht">ht</option>
<option value="t">t</option>
<option value="b">b</option>
<option value="p">p</option>
</select>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

Xet Storage Details

Size:
6.4 kB
·
Xet hash:
ef6c4a3b9d0ea458f994d684625234fe6e73110a7fc6c7591448e6451920a6b2

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.