| import { useEffect, useState } from "react"; |
| import { FileText, Trash2, BarChart3, Database, AlertCircle } from "lucide-react"; |
| import type { EmbedStore } from "../editor/embeds/embed-store"; |
| import type { EmbedDataFileMeta } from "../editor/embeds/embed-data-store"; |
| import { UploadZone } from "./UploadZone"; |
| import { formatBytes } from "../utils/data-files"; |
| import type { UploadResult } from "../hooks/useEmbedData"; |
|
|
| interface FilesSidebarProps { |
| embedStore: EmbedStore | null; |
| currentSrc: string; |
| dataFiles: EmbedDataFileMeta[]; |
| selectedDataFile: string | null; |
| |
| |
| |
| |
| |
| onSelectChart?: (name: string) => void; |
| onSelectDataFile: (name: string | null) => void; |
| onUploadFiles: (files: FileList | File[]) => Promise<UploadResult[]>; |
| onRemoveDataFile: (name: string) => void; |
| } |
|
|
| export function FilesSidebar({ |
| embedStore, |
| currentSrc, |
| dataFiles, |
| selectedDataFile, |
| onSelectChart, |
| onSelectDataFile, |
| onUploadFiles, |
| onRemoveDataFile, |
| }: FilesSidebarProps) { |
| const [charts, setCharts] = useState<string[]>(() => |
| embedStore ? embedStore.keys() : [], |
| ); |
| const [uploadError, setUploadError] = useState<string | null>(null); |
|
|
| useEffect(() => { |
| if (!embedStore) return; |
| setCharts(embedStore.keys()); |
| return embedStore.observe(() => setCharts(embedStore.keys())); |
| }, [embedStore]); |
|
|
| const handleUpload = async (files: FileList | File[]) => { |
| setUploadError(null); |
| const results = await onUploadFiles(files); |
| const firstError = results.find((r) => !r.ok); |
| if (firstError?.error) setUploadError(firstError.error); |
| const firstOk = results.find((r) => r.ok && r.name); |
| if (firstOk?.name) onSelectDataFile(firstOk.name); |
| }; |
|
|
| return ( |
| <div className="es-files"> |
| <section className="es-files__section"> |
| <header className="es-files__section-title"> |
| <BarChart3 size={13} /> |
| <span>Charts</span> |
| <span className="es-files__count">{charts.length}</span> |
| </header> |
| <ul className="es-files__list"> |
| {charts.length === 0 && ( |
| <li className="es-files__empty">No charts yet</li> |
| )} |
| {charts.map((name) => { |
| const isCurrent = name === currentSrc; |
| const className = `es-files__item ${isCurrent ? "es-files__item--current" : ""}`; |
| const inner = ( |
| <> |
| <FileText size={12} /> |
| <span className="es-files__name">{name}</span> |
| {isCurrent && ( |
| <span className="es-files__badge">editing</span> |
| )} |
| </> |
| ); |
| if (!onSelectChart || isCurrent) { |
| return ( |
| <li |
| key={name} |
| className={className} |
| title={isCurrent ? "Current chart" : name} |
| > |
| {inner} |
| </li> |
| ); |
| } |
| return ( |
| <li key={name} className={className}> |
| <button |
| type="button" |
| className="es-files__item-main" |
| onClick={() => onSelectChart(name)} |
| title={`Switch to ${name}`} |
| > |
| {inner} |
| </button> |
| </li> |
| ); |
| })} |
| </ul> |
| </section> |
|
|
| <section className="es-files__section"> |
| <header className="es-files__section-title"> |
| <Database size={13} /> |
| <span>Data</span> |
| <span className="es-files__count">{dataFiles.length}</span> |
| </header> |
| |
| <ul className="es-files__list"> |
| {dataFiles.length === 0 && ( |
| <li className="es-files__empty">No data uploaded yet</li> |
| )} |
| {dataFiles.map((file) => { |
| const isSelected = file.name === selectedDataFile; |
| return ( |
| <li |
| key={file.name} |
| className={`es-files__item es-files__item--data ${isSelected ? "es-files__item--current" : ""}`} |
| > |
| <button |
| type="button" |
| className="es-files__item-main" |
| onClick={() => onSelectDataFile(isSelected ? null : file.name)} |
| title={file.name} |
| > |
| <FileText size={12} /> |
| <span className="es-files__name">{file.name}</span> |
| <span className="es-files__size">{formatBytes(file.size)}</span> |
| </button> |
| <button |
| type="button" |
| className="es-files__item-remove" |
| onClick={(e) => { |
| e.stopPropagation(); |
| if (isSelected) onSelectDataFile(null); |
| onRemoveDataFile(file.name); |
| }} |
| aria-label={`Remove ${file.name}`} |
| title="Remove file" |
| > |
| <Trash2 size={12} /> |
| </button> |
| </li> |
| ); |
| })} |
| </ul> |
| |
| <UploadZone onFiles={handleUpload} compact={dataFiles.length > 0} /> |
| {uploadError && ( |
| <div className="es-files__error"> |
| <AlertCircle size={12} /> |
| <span>{uploadError}</span> |
| </div> |
| )} |
| </section> |
| </div> |
| ); |
| } |
|
|