Files
app/src/admin/components/ProjectFileManager.tsx
BOHA 03285c6ff6 refactor(css): migrate Login + file icons off legacy vars, delete variables.css
Phase 4 (final) of the "fully MUI" cleanup — no custom stylesheets remain
in src/admin (only the third-party quill.snow.css + leaflet.css imports).

- Login.tsx: every var(--glass-*/bg-*/text-*/accent-*/orb-*/border-*/
  font-*/transition) in its sx/inline styles is now an MUI token
  (bgcolor/color/borderColor: "background.paper" / "text.secondary" /
  "divider" / "primary.main"), a channel-alpha (rgba(var(--mui-palette-
  primary-mainChannel) / a)) for tints, or theme.applyStyles("dark", …)
  for the glass-card shadow. The card sx is now a (theme)=>({}) callback.
- Restores the orb drift animation via the MUI keyframes() helper — the
  @keyframes float had been dropped in Phase 1 (the dead-CSS scan missed
  its use inside an sx animation string); recovered verbatim from git.
- ProjectFileManager: the fallback file-icon color uses
  var(--mui-palette-text-secondary).
- variables.css deleted; its import removed from AdminApp (last one).

tsc -b --noEmit + npm run build clean. applyStyles dark-scheme output and
the channel-alpha pattern were both verified live in Chrome via the Quill
editor, which uses the identical constructs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:45:36 +02:00

987 lines
27 KiB
TypeScript

import { useState, useRef } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import CircularProgress from "@mui/material/CircularProgress";
import { projectFilesOptions } from "../lib/queries/projects";
import { useAlert } from "../context/AlertContext";
import {
Card,
Button,
TextField,
ConfirmDialog,
EmptyState,
LoadingState,
DataTable,
type DataColumn,
} from "../ui";
import apiFetch from "../utils/api";
const API_BASE = "/api/admin";
interface ProjectFileManagerProps {
projectId: number;
projectNumber: string | null;
hasPermission: (perm: string) => boolean;
hasNasFolder: boolean;
}
interface FileItem {
name: string;
type: "file" | "folder";
size?: number;
size_formatted?: string;
modified?: string;
extension?: string;
item_count?: number;
is_symlink?: boolean;
link_target?: string;
}
function getFileIcon(type: string, extension?: string) {
if (type === "folder") {
return (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="#e6a817"
strokeWidth="1.5"
>
<path
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
fill="rgba(230, 168, 23, 0.15)"
/>
</svg>
);
}
const ext = (extension || "").toLowerCase();
const iconMap: Record<string, { color: string; path: string }> = {
pdf: {
color: "#e74c3c",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
doc: {
color: "#3498db",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
docx: {
color: "#3498db",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
xls: {
color: "#27ae60",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
xlsx: {
color: "#27ae60",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
ppt: {
color: "#e67e22",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
pptx: {
color: "#e67e22",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
jpg: {
color: "#3498db",
path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z",
},
jpeg: {
color: "#3498db",
path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z",
},
png: {
color: "#3498db",
path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z",
},
gif: {
color: "#3498db",
path: "M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z",
},
zip: {
color: "#e67e22",
path: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",
},
rar: {
color: "#e67e22",
path: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",
},
"7z": {
color: "#e67e22",
path: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z",
},
dwg: {
color: "#8e44ad",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
dxf: {
color: "#8e44ad",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
step: {
color: "#8e44ad",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
stp: {
color: "#8e44ad",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
},
};
const cfg = iconMap[ext] || {
color: "var(--mui-palette-text-secondary)",
path: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z",
};
return (
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke={cfg.color}
strokeWidth="1.5"
>
<path d={cfg.path} />
<polyline points="14 2 14 8 20 8" />
</svg>
);
}
function SymlinkBadge({ target }: { target?: string }) {
return (
<Box
component="span"
title={target || "Odkaz"}
sx={{
display: "inline-flex",
alignItems: "center",
ml: 0.5,
color: "text.secondary",
}}
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</Box>
);
}
function FileNameCell({
item,
onFolderClick,
}: {
item: FileItem;
onFolderClick: (name: string) => void;
}) {
if (item.type === "folder") {
return (
<Box
component="span"
sx={{ display: "inline-flex", alignItems: "center", gap: 0.5 }}
>
<Box
component="button"
type="button"
onClick={() => onFolderClick(item.name)}
sx={{
background: "none",
border: "none",
p: 0,
font: "inherit",
color: "primary.main",
cursor: "pointer",
textAlign: "left",
"&:hover": { textDecoration: "underline" },
}}
>
{item.name}
</Box>
{item.is_symlink && <SymlinkBadge target={item.link_target} />}
{item.item_count !== undefined && (
<Box
component="span"
sx={{
fontSize: ".7rem",
color: "text.secondary",
bgcolor: "action.hover",
borderRadius: 1,
px: 0.75,
}}
>
{item.item_count}
</Box>
)}
</Box>
);
}
return (
<Box
component="span"
sx={{ display: "inline-flex", alignItems: "center", gap: 0.5 }}
>
<Box component="span">{item.name}</Box>
{item.is_symlink && <SymlinkBadge target={item.link_target} />}
</Box>
);
}
export default function ProjectFileManager({
projectId,
projectNumber,
hasPermission,
hasNasFolder,
}: ProjectFileManagerProps) {
const alert = useAlert();
const queryClient = useQueryClient();
const fileInputRef = useRef<HTMLInputElement>(null);
const isCancelling = useRef(false);
const [currentPath, setCurrentPath] = useState("");
const [dragOver, setDragOver] = useState(false);
const [uploading, setUploading] = useState(false);
const [newFolderMode, setNewFolderMode] = useState(false);
const [newFolderName, setNewFolderName] = useState("");
const [creatingFolder, setCreatingFolder] = useState(false);
const [renamingItem, setRenamingItem] = useState<string | null>(null);
const [renameValue, setRenameValue] = useState("");
const [deleteTarget, setDeleteTarget] = useState<FileItem | null>(null);
const [deleting, setDeleting] = useState(false);
const canManage = hasPermission("projects.files");
const {
data: filesData,
isPending: filesLoading,
error: filesError,
} = useQuery(projectFilesOptions(projectId, currentPath));
const items = filesData?.items ?? [];
const breadcrumb = filesData?.breadcrumb ?? [""];
const fullPath = filesData?.full_path ?? "";
const errorMessage = filesError
? filesError.message || "Nepodařilo se načíst soubory"
: null;
const navigateTo = (path: string) => {
setNewFolderMode(false);
setRenamingItem(null);
setCurrentPath(path);
};
const handleBreadcrumbClick = (index: number) => {
if (index === 0) {
navigateTo("");
return;
}
const path = breadcrumb.slice(1, index + 1).join("/");
navigateTo(path);
};
const handleFolderClick = (folderName: string) => {
const path = currentPath ? `${currentPath}/${folderName}` : folderName;
navigateTo(path);
};
const handleUpload = async (files: FileList | null) => {
if (!files || files.length === 0) return;
setUploading(true);
let successCount = 0;
let errorMsg: string | null = null;
for (const file of Array.from(files)) {
const formData = new FormData();
formData.append("file", file);
const params = new URLSearchParams({
action: "upload",
project_id: String(projectId),
});
if (currentPath) {
params.set("path", currentPath);
}
try {
const res = await apiFetch(
`${API_BASE}/project-files/upload?${params}`,
{
method: "POST",
body: formData,
},
);
const data = await res.json();
if (data.success) {
successCount++;
} else {
errorMsg = data.error || "Chyba při nahrávání";
}
} catch {
errorMsg = "Chyba připojení";
}
}
setUploading(false);
if (successCount > 0) {
const msg =
successCount === 1
? "Soubor byl nahrán"
: `Nahráno ${successCount} souborů`;
alert.success(msg);
queryClient.invalidateQueries({
queryKey: ["projects", String(projectId), "files"],
});
}
if (errorMsg) {
alert.error(errorMsg);
}
};
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
handleUpload(e.target.files);
e.target.value = "";
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
if (!canManage) return;
handleUpload(e.dataTransfer.files);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
if (canManage) {
setDragOver(true);
}
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setDragOver(false);
};
const handleCreateFolder = async () => {
const name = newFolderName.trim();
if (!name) return;
setCreatingFolder(true);
try {
const params = new URLSearchParams({
action: "create_folder",
project_id: String(projectId),
});
const res = await apiFetch(`${API_BASE}/project-files?${params}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: currentPath, folder_name: name }),
});
const data = await res.json();
if (data.success) {
alert.success("Složka byla vytvořena");
setNewFolderMode(false);
setNewFolderName("");
queryClient.invalidateQueries({
queryKey: ["projects", String(projectId), "files"],
});
} else {
alert.error(data.error || "Nepodařilo se vytvořit složku");
}
} catch {
alert.error("Chyba připojení");
} finally {
setCreatingFolder(false);
}
};
const handleDownload = async (item: FileItem) => {
const filePath = currentPath ? `${currentPath}/${item.name}` : item.name;
const params = new URLSearchParams({
action: "download",
project_id: String(projectId),
path: filePath,
});
try {
const res = await apiFetch(`${API_BASE}/project-files?${params}`);
if (!res.ok) {
const err = await res.json().catch(() => null);
alert.error(err?.error || "Chyba při stahování");
return;
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = item.name;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
} catch {
alert.error("Chyba připojení");
}
};
const handleDelete = async () => {
if (!deleteTarget) return;
setDeleting(true);
const filePath = currentPath
? `${currentPath}/${deleteTarget.name}`
: deleteTarget.name;
try {
const params = new URLSearchParams({
project_id: String(projectId),
path: filePath,
});
const res = await apiFetch(`${API_BASE}/project-files?${params}`, {
method: "DELETE",
});
const data = await res.json();
if (data.success) {
alert.success(
deleteTarget.type === "folder"
? "Složka byla smazána"
: "Soubor byl smazán",
);
queryClient.invalidateQueries({
queryKey: ["projects", String(projectId), "files"],
});
} else {
alert.error(data.error || "Nepodařilo se smazat");
}
} catch {
alert.error("Chyba připojení");
} finally {
setDeleting(false);
setDeleteTarget(null);
}
};
const handleRename = async (item: FileItem) => {
const newName = renameValue.trim();
if (!newName || newName === item.name) {
setRenamingItem(null);
return;
}
const fromPath = currentPath ? `${currentPath}/${item.name}` : item.name;
const toPath = currentPath ? `${currentPath}/${newName}` : newName;
try {
const params = new URLSearchParams({
action: "move",
project_id: String(projectId),
});
const res = await apiFetch(`${API_BASE}/project-files?${params}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ from_path: fromPath, to_path: toPath }),
});
const data = await res.json();
if (data.success) {
alert.success("Přejmenováno");
queryClient.invalidateQueries({
queryKey: ["projects", String(projectId), "files"],
});
} else {
alert.error(data.error || "Nepodařilo se přejmenovat");
}
} catch {
alert.error("Chyba připojení");
} finally {
setRenamingItem(null);
}
};
const startRename = (item: FileItem) => {
setRenamingItem(item.name);
setRenameValue(item.name);
};
if (filesLoading && items.length === 0 && !errorMessage) {
return <LoadingState />;
}
if (errorMessage) {
return (
<Card>
<Typography variant="h6" sx={{ mb: 2 }}>
Soubory
</Typography>
<EmptyState
icon={
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
}
title={errorMessage}
/>
</Card>
);
}
const columns: DataColumn<FileItem>[] = [
{
key: "icon",
header: "",
width: 30,
align: "left",
render: (item) => (
<Box sx={{ textAlign: "center" }}>
{getFileIcon(item.type, item.extension)}
</Box>
),
},
{
key: "name",
header: "Název",
render: (item) =>
renamingItem === item.name ? (
<TextField
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
autoFocus
sx={{ maxWidth: 300 }}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleRename(item);
}
if (e.key === "Escape") {
e.preventDefault();
isCancelling.current = true;
setRenamingItem(null);
setRenameValue(item.name);
setTimeout(() => {
isCancelling.current = false;
}, 0);
}
}}
onBlur={() => {
if (isCancelling.current) {
return;
}
handleRename(item);
}}
/>
) : (
<FileNameCell item={item} onFolderClick={handleFolderClick} />
),
},
{
key: "size",
header: "Velikost",
width: 90,
render: (item) => (
<Typography variant="caption" color="text.secondary">
{item.type === "file" ? item.size_formatted : "—"}
</Typography>
),
},
{
key: "modified",
header: "Změněno",
width: 120,
render: (item) => (
<Typography variant="caption" color="text.secondary">
{item.modified || "—"}
</Typography>
),
},
...(canManage
? [
{
key: "actions",
header: "Akce",
width: 100,
align: "right" as const,
render: (item: FileItem) => (
<Box
sx={{
display: "flex",
gap: 0.5,
justifyContent: "flex-end",
}}
>
{item.type === "file" && (
<IconButton
size="small"
title="Stáhnout"
aria-label="Stáhnout"
onClick={() => handleDownload(item)}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
</IconButton>
)}
<IconButton
size="small"
title="Přejmenovat"
aria-label="Přejmenovat"
onClick={() => startRename(item)}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
</IconButton>
<IconButton
size="small"
color="error"
title="Smazat"
aria-label="Smazat"
onClick={() => setDeleteTarget(item)}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<path d="M10 11v6M14 11v6" />
</svg>
</IconButton>
</Box>
),
},
]
: []),
];
return (
<Card>
<Typography variant="h6" sx={{ mb: 2 }}>
Soubory
</Typography>
{/* Toolbar */}
<Box
sx={{
display: "flex",
alignItems: "center",
flexWrap: "wrap",
gap: 1,
mb: 2,
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
flexWrap: "wrap",
gap: 0.25,
}}
>
{breadcrumb.map((segment, i) => (
<Box
component="span"
key={i}
sx={{ display: "inline-flex", alignItems: "center" }}
>
{i > 0 && (
<Box component="span" sx={{ mx: 0.25, color: "text.disabled" }}>
/
</Box>
)}
<Box
component="button"
type="button"
onClick={() => handleBreadcrumbClick(i)}
sx={{
background: "none",
border: "none",
p: 0,
font: "inherit",
cursor: "pointer",
color:
i === breadcrumb.length - 1
? "text.primary"
: "primary.main",
fontWeight: i === breadcrumb.length - 1 ? 600 : 400,
"&:hover": { textDecoration: "underline" },
}}
>
{i === 0 ? projectNumber : segment}
</Box>
</Box>
))}
</Box>
{fullPath && (
<Typography
variant="caption"
color="text.secondary"
title={fullPath}
sx={{
maxWidth: 320,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{fullPath}
</Typography>
)}
{canManage && (
<Box sx={{ display: "flex", gap: 1, ml: "auto" }}>
<Button
size="small"
variant="outlined"
color="inherit"
onClick={() => {
setNewFolderMode(!newFolderMode);
setNewFolderName("");
}}
startIcon={
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
<line x1="12" y1="11" x2="12" y2="17" />
<line x1="9" y1="14" x2="15" y2="14" />
</svg>
}
>
Složka
</Button>
<Button
size="small"
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
startIcon={
uploading ? (
<CircularProgress size={14} color="inherit" />
) : (
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
)
}
>
{uploading ? "Nahrávání..." : "Nahrát"}
</Button>
<input
ref={fileInputRef}
type="file"
multiple
style={{ display: "none" }}
onChange={handleFileInputChange}
/>
</Box>
)}
</Box>
{/* New folder input */}
{newFolderMode && (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
mb: 2,
}}
>
<TextField
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
placeholder="Název složky..."
autoFocus
sx={{ maxWidth: 320 }}
onKeyDown={(e) => {
if (e.key === "Enter") handleCreateFolder();
if (e.key === "Escape") {
setNewFolderMode(false);
setNewFolderName("");
}
}}
/>
<Button
size="small"
onClick={handleCreateFolder}
disabled={creatingFolder || !newFolderName.trim()}
>
{creatingFolder ? (
<CircularProgress size={14} color="inherit" />
) : (
"Vytvořit"
)}
</Button>
<Button
size="small"
variant="outlined"
color="inherit"
onClick={() => {
setNewFolderMode(false);
setNewFolderName("");
}}
>
Zrušit
</Button>
</Box>
)}
{/* Drop zone + table */}
<Box
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
sx={{
position: "relative",
borderRadius: 2,
...(dragOver
? {
outline: "2px dashed",
outlineColor: "primary.main",
outlineOffset: -4,
}
: {}),
}}
>
{dragOver && (
<Box
sx={{
position: "absolute",
inset: 0,
zIndex: 2,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 1,
bgcolor: "action.hover",
color: "primary.main",
borderRadius: 2,
pointerEvents: "none",
}}
>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
<span>Přetáhněte soubory sem</span>
</Box>
)}
{items.length === 0 && !filesLoading ? (
<EmptyState
icon={
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
</svg>
}
title={
hasNasFolder
? "Složka je prázdná"
: "Složka projektu zatím neexistuje"
}
description={
canManage && !hasNasFolder
? "Nahrání souboru ji automaticky vytvoří"
: undefined
}
/>
) : (
<DataTable
columns={columns}
rows={items}
rowKey={(item) => item.name}
/>
)}
</Box>
<ConfirmDialog
isOpen={deleteTarget !== null}
onClose={() => setDeleteTarget(null)}
onConfirm={handleDelete}
title={
deleteTarget?.type === "folder" ? "Smazat složku" : "Smazat soubor"
}
message={`Opravdu chcete smazat "${deleteTarget?.name}"?${deleteTarget?.type === "folder" ? " Složka bude smazána včetně veškerého obsahu." : ""}`}
confirmText="Smazat"
cancelText="Zrušit"
confirmVariant="danger"
loading={deleting}
/>
</Card>
);
}