// Dashboard — sidebar + topbar + content (grid/list) + detail panel.
const { useState: useStateD, useMemo, useEffect, useRef } = React;
/* ------------------------------------------------------------------ */
/* Thumb renderer */
/* ------------------------------------------------------------------ */
function Thumb({ item, large = false }) {
const [imgError, setImgError] = useStateD(false);
// Si hay thumbnail real en el servidor, usarlo
if (item.thumbnail_url && !imgError) {
return (
setImgError(true)}
/>
);
}
// Fallback generativo basado en tipo
const colors = {
image: { a: "#1B1A1A", b: "#00B147" },
pdf: { a: "#DC2626", b: "#FEF2F2" },
docx: { a: "#1D4ED8", b: "#EFF6FF" },
xlsx: { a: "#065F46", b: "#ECFDF5" },
pptx: { a: "#EA580C", b: "#FFF7ED" },
zip: { a: "#7C3AED", b: "#F5F3FF" },
video: { a: "#0E7490", b: "#ECFEFF" },
};
const c = colors[item.type] || { a: "#6B7280", b: "#F9FAFB" };
return (
);
}
const typeIcon = (t) => ({
pdf: "pdf", xlsx: "xlsx", docx: "docx", zip: "zip", image: "image",
}[t] || "document");
const fmtDate = (s) => {
const d = new Date(s);
if (isNaN(d)) return s;
return d.toLocaleDateString("es-MX", { day: "2-digit", month: "short", year: "numeric" });
};
const fmtSize = (bytes) => {
if (!bytes) return "—";
if (bytes < 1024) return bytes + " B";
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
return (bytes / 1073741824).toFixed(2) + " GB";
};
/* ------------------------------------------------------------------ */
/* Sidebar */
/* ------------------------------------------------------------------ */
function Sidebar({ sections, folders, currentUser, sectionId, setSection, folderId, setFolder, favorites, onLogout, onOpenAdmin }) {
return (
);
}
/* ------------------------------------------------------------------ */
/* Topbar */
/* ------------------------------------------------------------------ */
function Topbar({ query, setQuery, onUpload }) {
return (
);
}
/* ------------------------------------------------------------------ */
/* Card */
/* ------------------------------------------------------------------ */
function Card({ item, onOpen, onDownload }) {
return (
onOpen(item)}>
{item.type}
{item.name}
{fmtSize(item.size_bytes)}
{fmtDate(item.updated_at)}
v{item.version}
);
}
/* ------------------------------------------------------------------ */
/* Row (list view) */
/* ------------------------------------------------------------------ */
function Row({ item, folderName, onOpen, onDownload }) {
return (
onOpen(item)}>
{item.name}
{item.description}
{folderName}
{item.type}
{fmtDate(item.updated_at)}
{fmtSize(item.size_bytes)}
);
}
/* ------------------------------------------------------------------ */
/* Upload modal simplificado */
/* ------------------------------------------------------------------ */
function UploadModal({ sectionId, folderId, onClose, onSuccess }) {
const [file, setFile] = useStateD(null);
const [name, setName] = useStateD("");
const [desc, setDesc] = useStateD("");
const [tags, setTags] = useStateD("");
const [uploading, setUploading] = useStateD(false);
const [error, setError] = useStateD(null);
const handleSubmit = async (e) => {
e.preventDefault();
if (!file) { setError("Selecciona un archivo"); return; }
setUploading(true);
setError(null);
try {
const fd = new FormData();
fd.append("file", file);
fd.append("section_id", sectionId);
if (folderId) fd.append("folder_id", folderId);
fd.append("name", name || file.name);
fd.append("description", desc);
fd.append("tags", tags);
await API.Items.upload(fd);
onSuccess();
onClose();
} catch(err) {
setError(err.message || "Error al subir el archivo");
} finally {
setUploading(false);
}
};
return (
);
}
/* ------------------------------------------------------------------ */
/* Detail panel */
/* ------------------------------------------------------------------ */
function DetailPanel({ item, folderName, onClose, onDownload, favorites, onToggleFav }) {
if (!item) return null;
const isFav = favorites.includes(item.id);
return (
<>
>
);
}
/* ------------------------------------------------------------------ */
/* Main Dashboard */
/* ------------------------------------------------------------------ */
function Dashboard({ sections, allFolders, currentUser, onLogout, onOpenAdmin }) {
const [sectionId, setSection] = useStateD(sections[0]?.id || null);
const [folderId, setFolder] = useStateD(null);
const [query, setQuery] = useStateD("");
const [view, setView] = useStateD("grid");
const [typeFilter, setType] = useStateD("all");
const [sort, setSort] = useStateD("recent");
const [openItem, setOpenItem] = useStateD(null);
const [toast, setToast] = useStateD(null);
const [showUpload, setShowUpload] = useStateD(false);
// Items cargados del servidor
const [items, setItems] = useStateD([]);
const [loadingItems, setLoading] = useStateD(false);
const [totalItems, setTotal] = useStateD(0);
// Favoritos
const [favorites, setFavorites] = useStateD([]);
const debounceRef = useRef(null);
// Inicializar sectionId cuando sections lleguen
useEffect(() => {
if (!sectionId && sections.length > 0) {
setSection(sections[0].id);
}
}, [sections]);
// Cargar items al cambiar sección/carpeta (sin búsqueda activa)
useEffect(() => {
if (!sectionId) return;
if (query.trim().length > 1) return; // búsqueda activa maneja sus propios items
setLoading(true);
const params = { section_id: sectionId, per_page: 100, sort };
if (folderId) params.folder_id = folderId;
API.Items.list(params)
.then(function(res) {
setItems(res.data || []);
setTotal(res.total || 0);
// Extraer favoritos
const favIds = (res.data || []).filter(i => i.is_favorite).map(i => i.id);
setFavorites(function(prev) {
const merged = new Set([...prev, ...favIds]);
return Array.from(merged);
});
})
.catch(function() { setItems([]); })
.finally(function() { setLoading(false); });
}, [sectionId, folderId, sort]);
// Búsqueda con debounce
useEffect(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (!query.trim() || query.trim().length < 2) {
// Sin query: recargar items normales
if (!query.trim() && sectionId) {
setLoading(true);
const params = { section_id: sectionId, per_page: 100, sort };
if (folderId) params.folder_id = folderId;
API.Items.list(params)
.then(function(res) { setItems(res.data || []); setTotal(res.total || 0); })
.catch(function() {})
.finally(function() { setLoading(false); });
}
return;
}
debounceRef.current = setTimeout(function() {
setLoading(true);
const params = { q: query.trim(), per_page: 50 };
if (sectionId) params.section_id = sectionId;
API.Search.query(params)
.then(function(res) { setItems(res.data || []); setTotal(res.total || 0); })
.catch(function() {})
.finally(function() { setLoading(false); });
}, 350);
}, [query]);
// ESC para cerrar detail panel
useEffect(() => {
const h = (e) => { if (e.key === "Escape") setOpenItem(null); };
window.addEventListener("keydown", h);
return () => window.removeEventListener("keydown", h);
}, []);
// Filtros cliente (sobre items ya cargados)
const filtered = useMemo(() => {
let out = items;
if (typeFilter !== "all") out = out.filter(i => i.type === typeFilter);
if (sort === "name") out = [...out].sort((a,b) => a.name.localeCompare(b.name));
if (sort === "size") out = [...out].sort((a,b) => b.size_bytes - a.size_bytes);
return out;
}, [items, typeFilter, sort]);
// Tipos disponibles
const types = useMemo(() => {
const set = new Set(items.map(i => i.type));
return Array.from(set);
}, [items]);
const section = sections.find(s => s.id === sectionId);
const folders = allFolders[sectionId] || [];
const folder = folders.find(f => f.id === folderId);
const handleDownload = function(item) {
window.open(API.Items.downloadUrl(item.id), "_blank");
};
const handleToggleFav = async function(item) {
const isFav = favorites.includes(item.id);
try {
if (isFav) {
await API.Items.unfavorite(item.id);
setFavorites(function(prev) { return prev.filter(id => id !== item.id); });
} else {
await API.Items.favorite(item.id);
setFavorites(function(prev) { return [...prev, item.id]; });
}
} catch(e) {
setToast("Error al actualizar favorito");
setTimeout(() => setToast(null), 2000);
}
};
const handleUploadSuccess = function() {
// Recargar items
if (!sectionId) return;
const params = { section_id: sectionId, per_page: 100 };
if (folderId) params.folder_id = folderId;
API.Items.list(params).then(function(res) { setItems(res.data || []); }).catch(function() {});
setToast("Archivo subido correctamente");
setTimeout(() => setToast(null), 2000);
};
if (!section && sections.length > 0) {
return
Cargando sección…
;
}
return (
setShowUpload(true)}/>
{/* Crumbs */}
Biblioteca
{section?.label}
{folder && <>
{folder.name}
>}
{!folder && Todo}
{/* Page head */}
Sección
{folder ? folder.name : section?.label}
{section?.description}
Total
{totalItems.toLocaleString("es-MX")}
{/* Folder strip */}
{!folderId && folders.length > 0 && (
<>
Carpetas
{folders.length} carpetas
{folders.map(f => (
setFolder(f.id)}>
{f.name}
{(f.items_count || 0).toLocaleString("es-MX")} elementos
))}
+ Nueva carpeta
Expandir esta sección
>
)}
{/* Section label */}
{folder ? "Contenido" : "Archivos"}
{filtered.length} resultado{filtered.length===1?"":"s"}
{/* Toolbar */}
{types.map(tp => (
))}
{/* Loading */}
{loadingItems && (
Cargando archivos…
)}
{/* Empty */}
{!loadingItems && filtered.length === 0 && (
Sin resultados
Ajuste su búsqueda o limpie los filtros.
)}
{/* Grid */}
{!loadingItems && filtered.length > 0 && view === "grid" && (
{filtered.map(i => (
))}
)}
{/* List */}
{!loadingItems && filtered.length > 0 && view === "list" && (
Archivo
Carpeta
Tipo
Actualizado
Tamaño
{filtered.map(i => (
f.id===i.folder_id)?.name || "—"}
onOpen={setOpenItem}
onDownload={handleDownload}/>
))}
)}
{/* Detail */}
f.id === openItem.folder_id)?.name : null}
onClose={() => setOpenItem(null)}
onDownload={handleDownload}
favorites={favorites}
onToggleFav={handleToggleFav}
/>
{/* Upload modal */}
{showUpload && (
setShowUpload(false)}
onSuccess={handleUploadSuccess}
/>
)}
{/* Toast */}
{toast && (
{toast}
)}
);
}
window.Dashboard = Dashboard;