/**
* @file Dashboard.jsx - Dashboard-Hauptkomponente
* @author Farah
* @description Diese Komponente stellt die Hauptseite des Dashboards dar, auf der Benutzer Ordner und Dateien verwalten können.
*
* @requires react
* @requires ../utils/fetchFoldersTree
* @requires react-router-dom
* @requires react-icons/fa6
* @requires sweetalert2
*/
import { useEffect, useState, useMemo } from "react";
import { fetchAndRenderFolderTree } from "../utils/fetchFoldersTree";
import { useNavigate, useLocation } from "react-router-dom";
import { FaFolder, FaPlus } from "react-icons/fa6";
import { BsThreeDotsVertical } from "react-icons/bs";
import CreateFolderForm from "../features/dashboard/CreateFolder";
import FolderElement from "../features/dashboard/FolderElement";
import { customFetch } from "../utils/helpers";
import { FaThList } from "react-icons/fa";
import axios from "axios";
import Swal from "sweetalert2";
import Breadcrumbs from "../components/ui/Breadcrumbs";
import File from "../components/File";
import { IoClose } from "react-icons/io5";
import prodconfig from "../production-config";
// this is the dashboard homepage
/**
* @component Dashboard
* @description Hauptkomponente für die Dashboard-Ansicht
* @returns {JSX.Element} Dashboard-Komponente mit Ordner- und Dateiverwaltung
*/
/**
* State-Definitionen
* @type {Object}
* @property {Array} folders - Liste aller Ordner
* @property {boolean} isLoading - Ladezustand
* @property {boolean} createNewFolder - Status des Ordner-Erstellungsdialogs
* @property {number|null} editingFolderId - ID des bearbeiteten Ordners
* @property {Array} results - Suchergebnisse
* @property {boolean} isCreating - Status der Ordnererstellung
* @property {boolean} isDeleting - Status des Löschvorgangs
* @property {boolean} isDownloading - Status des Downloads
* @property {Object} contextMenu - Position und Status des Kontextmenüs
* @property {boolean} isFileExplorerView - Aktuelle Ansichtsart
*/
function Dashboard() {
const [folders, setFolders] = useState([]);
const [isLoading, setLoading] = useState(false);
const [createNewFolder, setCreateNewFolder] = useState(false);
const navigate = useNavigate();
const [editingFolderId, setEditingFolderId] = useState(null); // To track which folder is being edited
const [newFolderName, setNewFolderName] = useState(""); // To store the new folder name
const [results, setResults] = useState([]);
const [isCreating, setIsCreating] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// boolean to track if a file is current downloading
const [isDownloading, setIsDownloading] = useState(false);
const [selectedDocToRename, setSelectedDocToRename] = useState({});
const [currentlyPreviewedFile, setCurrentlyPreviewedFile] = useState(null);
const [filePreviewContent, setFilePreviewContent] = useState("");
//view list
const [contextMenu, setContextMenu] = useState({
visible: false,
folderId: null,
x: 0,
y: 0,
});
const [isFileExplorerView, setIsFileExplorerView] = useState(
JSON.parse(localStorage.getItem("isFileExplorerView")) ?? true
);
useEffect(() => {
const fetchFolders = async () => {
try {
const folderTree = await fetchAndRenderFolderTree();
console.log("bb", folderTree);
if (folderTree) {
setFolders(folderTree.folderTree);
setLoading(false);
}
} catch (error) {
console.error("Fehler beim Abrufen der Ordnerstruktur:", error);
setLoading(false);
}
};
fetchFolders();
}, []);
/**
* @function handleCreateFolderSwal
* @async
* @param {number} id - Optional: ID des übergeordneten Ordners
* @description Öffnet einen SweetAlert2-Dialog zur Erstellung eines neuen Ordners
*/
const handleCreateFolderSwal = async (id) => {
const { value: folderName } = await Swal.fire({
title: "Neuen Ordner erstellen",
input: "text",
inputLabel: "Ordnername",
inputPlaceholder: "Gib den Namen des neuen Ordners ein",
showCancelButton: true,
confirmButtonText: "Erstellen",
cancelButtonText: "Abbrechen",
inputValidator: (value) => {
if (!value) {
return "Der Ordnername ist erforderlich!";
}
},
});
if (folderName) {
createFolder(folderName, id);
}
};
/**
* @function createFolder
* @async
* @param {string} folderName - Name des zu erstellenden Ordners
* @param {number} id - Optional: ID des übergeordneten Ordners
* @description Erstellt einen neuen Ordner über die API
*/
const createFolder = async (folderName, id) => {
setIsCreating(true);
try {
const response = await customFetch(
`${prodconfig.backendUrl}/folders/create`,
{
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ folderName, parentFolderId: id }),
}
);
const data = await response.json();
if (data.folderId) {
Swal.fire("Erfolg", "Ordner erfolgreich erstellt", "Erfolg");
window.location.reload();
} else {
Swal.fire(
"Fehler",
data?.message || "Fehler beim Erstellen des Ordners",
"fehler"
);
}
} catch (error) {
console.error("Fehler beim Erstellen des Ordners:", error);
Swal.fire(
"Fehler",
"Beim Erstellen des Ordners ist ein Fehler aufgetreten.",
"fehler"
);
} finally {
setIsCreating(false);
}
};
/**
* @function handleFileDownload
* @async
* @param {string} fileName - Name der herunterzuladenden Datei
* @description Lädt eine Datei über die API herunter
*/
const handleFileDownload = async (fileName) => {
try {
const response = await customFetch(
`${prodconfig.backendUrl}/docupload/download/${encodeURIComponent(
fileName
)}`,
{
method: "GET",
credentials: "include",
}
);
if (!response.ok) {
throw new Error("Fehler beim Herunterladen der Datei");
}
const blob = await response.blob(); // Retrieve file as blob
const url = window.URL.createObjectURL(blob); // Create a URL for the blob
// Create a temporary anchor element for downloading
const a = document.createElement("a");
a.href = url;
a.download = fileName; // Specify the file name for download
document.body.appendChild(a);
a.click(); // Programmatically click the anchor to trigger download
window.URL.revokeObjectURL(url); // Clean up the URL object
a.remove(); // Remove the temporary anchor element from the DOM
} catch (error) {
console.error("Download-Fehler:", error);
}
};
/**
* @function handleFileDelete
* @async
* @param {number} fileId - ID der zu löschenden Datei
* @description Löscht eine Datei nach Bestätigung
*/
const handleFileDelete = async (fileId) => {
if (
window.confirm("Bist du sicher, dass du diese Datei löschen möchtest?")
) {
// setIsDeleting(true);
try {
const response = await customFetch(
`${prodconfig.backendUrl}/docupload/delete/${fileId}`,
{ method: "DELETE", credentials: "include" }
);
if (!response.ok) throw new Error("Fehler beim Löschen der Datei");
const data = await response.json();
// setCurrentlyPreviewedFile(null);
// setFilePreviewContent(null);
const folderTree = await fetchAndRenderFolderTree();
// if (folderTree) {
// setFolders(folderTree.folderTree);
// setLoading(false);
// }
} catch (error) {
alert(
"Fehler beim Löschen der Datei. Bitte versuche es später erneut."
);
} finally {
// setIsDeleting(false);
}
}
};
// console.log(JSON.parse(localStorage.getItem("isFileExplorerView")));
/**
* @function handleFolderDelete
* @async
* @param {number} folderId - ID des zu löschenden Ordners
* @description Löscht einen Ordner und seinen Inhalt nach Bestätigung
*/
const handleFolderDelete = async (folderId) => {
if (confirm("Bist du sicher, dass du diesen Ordner löschen möchtest?")) {
setIsDeleting(true);
try {
const response = await customFetch(
`${prodconfig.backendUrl}/folders/${folderId}`,
{
method: "DELETE",
credentials: "include",
}
);
if (!response.ok) {
throw new Error("Fehler beim Löschen des Ordners");
}
const data = await response.json();
// Fetch and update the folder tree
const folderTree = await fetchAndRenderFolderTree();
if (folderTree) {
setFolders(folderTree.folderTree);
setLoading(false);
}
// Success message
await Swal.fire(
"Gelöscht!",
"Der Ordner wurde erfolgreich gelöscht.",
"success"
);
} catch (error) {
console.error("Fehler beim Löschen des Ordners:", error);
// Error message
await Swal.fire(
"Fehler",
"Fehler beim Löschen des Ordners. Bitte versuche es erneut.",
"error"
);
}
}
};
/**
* @function toggleView
* @description Wechselt zwischen Listen- und Kachelansicht
*/
const toggleView = () => {
setIsFileExplorerView((prev) => {
const newValue = !prev;
localStorage.setItem("isFileExplorerView", newValue);
return newValue;
});
};
// Function to handle right-click (context menu)
/**
* @function handleContextMenu
* @param {Event} event - Das auslösende Event
* @param {Object} folder - Der Ordner, für den das Kontextmenü geöffnet wird
* @description Verarbeitet Rechtsklick-Events für das Kontextmenü
*/
const handleContextMenu = (event, folder) => {
event.preventDefault(); // Prevent default right-click menu
const rect = event.currentTarget.getBoundingClientRect(); // Get the folder's position
setContextMenu({
visible: true,
folderId: folder.id,
x: rect.right, // Position the menu outside the card on the right
y: rect.top, // Align it vertically with the folder
});
};
// Function to close the context menu
const handleCloseContextMenu = () => {
setContextMenu({ visible: false, folderId: null, x: 0, y: 0 });
};
/**
* @function handleFilePreview
* @async
* @param {string} fileName - Name der anzuzeigenden Datei
* @description Zeigt eine Vorschau der ausgewählten Datei an
*/
const handleFilePreview = async (fileName) => {
// Überprüfen, ob die Vorschau gerade die Datei anzeigt, auf die geklickt wurde
if (currentlyPreviewedFile === fileName) {
// Vorschau ausblenden, wenn dieselbe Datei erneut geklickt wird
setCurrentlyPreviewedFile(null);
setFilePreviewContent(null);
return;
}
// Neue Datei wird angeklickt, also Vorschau aktualisieren
setCurrentlyPreviewedFile(fileName);
try {
const fileExtension = fileName.split(".").pop().toLowerCase();
if (["jpg", "jpeg", "png", "gif"].includes(fileExtension)) {
// Bildvorschau
setFilePreviewContent(
<img
src={`${prodconfig.backendUrl}/docupload/view/${encodeURIComponent(
fileName
)}`}
alt="Image Preview"
className="max-w-full mx-auto object-contain w-[500px] h-[300px]"
/>
);
} else if (["pdf"].includes(fileExtension)) {
// PDF-Vorschau
setFilePreviewContent(
<iframe
src={`${prodconfig.backendUrl}/docupload/view/${encodeURIComponent(
fileName
)}`}
frameBorder="0"
width="100%"
height="600px"
/>
);
} else if (fileExtension === "txt") {
// Textdatei-Vorschau
const response = await customFetch(
`${prodconfig.backendUrl}/docupload/view/${encodeURIComponent(
fileName
)}`,
{
credentials: "include",
}
);
const textContent = await response.text();
console.log(textContent);
setFilePreviewContent(
<div
dangerouslySetInnerHTML={{ __html: textContent }}
style={{
backgroundColor: "#f4f4f4",
padding: "10px",
border: "1px solid #ddd",
}}
/>
);
} else if (fileExtension === "docx") {
// DOCX-Vorschau
const response = await customFetch(
`${prodconfig.backendUrl}/docupload/view/${encodeURIComponent(
fileName
)}`,
{
credentials: "include",
}
);
const docxContent = await response.text(); // Der Server liefert HTML zurück
setFilePreviewContent(
<div
dangerouslySetInnerHTML={{ __html: docxContent }}
style={{
backgroundColor: "#f4f4f4",
padding: "10px",
border: "1px solid #ddd",
}}
/>
);
} else {
// Other file types
setFilePreviewContent(<p>File: {fileName}</p>);
}
} catch (error) {
console.error("Fehler beim Laden der Datei:", error);
}
};
/**
* @function useQuery
* @returns {URLSearchParams} Aktuelle URL-Suchparameter
* @description Custom Hook für URL-Suchparameter
*/
function useQuery() {
const { search } = useLocation();
return useMemo(() => new URLSearchParams(search), [search]);
}
let searchQuery = useQuery();
const searchQueryParam = searchQuery.get("search");
console.log("searchQuery", searchQuery.get("search"));
useEffect(() => {
const fetchSearchResults = async () => {
try {
const response = await customFetch(`${prodconfig.backendUrl}/search/`, {
method: "POST",
body: JSON.stringify({ query: searchQueryParam, limit: 10 }),
headers: { "Content-Type": "application/json" },
});
const data = await response.json();
setResults(data);
console.log(data);
} catch (error) {
setError(
"Beim Suchen ist ein Fehler aufgetreten. Bitte versuche es erneut."
);
console.error("Suchfehler:", error);
}
};
if (searchQueryParam) {
fetchSearchResults();
}
}, [searchQueryParam]);
console.log("results", results);
if (isLoading) return <div>Wird geladen...</div>;
return (
<div>
{searchQueryParam ? (
<div>
<h3 className="text-xl px-4 pt-2 pb-4 text-black">Suchergebnisse</h3>
{results.length > 0 ? (
<div className="overflow-x-auto">
<table className="w-full text-black">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-2 px-4 text-black">Name</th>
<th className="text-left py-2 px-4">Relevanz</th>
<th className="text-left py-2 px-4">Aktionen</th>
</tr>
</thead>
<tbody>
{results?.map((file, index) => (
<File
file={file}
results={results}
isDeleting={isDeleting}
isDownloading={isDownloading}
handleFilePreview={handleFilePreview}
handleFileDownload={handleFileDownload}
setSelectedDocToRename={setSelectedDocToRename}
handleFileDelete={handleFileDelete}
setResults={setResults}
searchQueryParam={searchQueryParam}
/>
))}
</tbody>
</table>
</div>
) : (
<h3 className="text-[1rem] text-black/50 flex items-center justify-center h-[70vh]">
Suche läuft ...
</h3>
)}
{currentlyPreviewedFile && (
<div className="my-4 bg-white p-4 rounded-xl shadow-md">
<button
className="ml-auto flex text-lg border-[1.5px] border-danger bg-danger text-white hover:bg-opacity-90 duration-200 transition-colors p-2 rounded-full mb-2"
onClick={() => {
setCurrentlyPreviewedFile(null);
setFilePreviewContent(null);
}}
>
<IoClose />
</button>
{filePreviewContent}
</div>
)}
</div>
) : (
<>
<div className="shadow-sm bg-white border-y border-slate-200 absolute left-0 top-0 right-0">
<div className="flex items-center gap-2 bg-gray-10 py-2 px-3">
<Breadcrumbs
isFileExplorerView={isFileExplorerView}
folders={folders}
toggleView={toggleView}
/>
</div>
</div>
<div className="mt-5"></div>
{!isFileExplorerView && (
<div className="flex items-center justify-between py-3">
<h3 className="text-lg text-black">Alle Ordner</h3>
</div>
)}
{!isFileExplorerView ? (
<div className="grid grid-cols-4 gap-2">
<div className="min-w-[300px] bg-muted/40 rounded-lg p-4 space-y-2 h-[calc(100vh-14rem)] grid2-scroll-y">
{/* Neuer Button zur Ordnererstellung */}
<div className="flex flex-col space-y-2">
<div className="flex justify-end mb-2">
<button
onClick={() => handleCreateFolderSwal(null)} // Erstellungsfunktion aufrufen
className="p-2 rounded-full bg-primary text-white hover:bg-primary-dark transition"
title="Neuen Ordner erstellen"
disabled={isCreating}
>
<FaPlus />
</button>
</div>
{folders.map((folder) => (
<div
key={folder.id}
className="flex items-center p-1.5 rounded-sm cursor-pointer hover:bg-black/10"
onClick={() => navigate(`/folders/${folder.id}`)}
>
<FaFolder
className="text-xl text-primary mr-4"
// onContextMenu={(e) => handleContextMenu(e, folder)} // Handle right-click
// folderId={folder.id}
/>
<span className="text-sm text-gray-800">
{folder.name}
</span>
</div>
))}
</div>
</div>
{/* <div className="border border-black/30 p-2 rounded-sm">ff</div>
<div className="border border-black/30 p-2 rounded-sm">ff</div> */}
</div>
) : (
<ul className="grid grid-cols-1 pl-0 xsm:grid-cols-3 gap-4 md:fap-7 md:grid-cols-5">
{folders.map((folder) => {
return (
<FolderElement
key={folder.id}
folderId={folder.id}
folderName={folder.name}
handleFolderDelete={handleFolderDelete}
/>
);
})}
<li
tabIndex={0}
onClick={() => setCreateNewFolder((p) => !p)}
className="bg-white border-lg p-4 flex flex-col gap-1 justify-center items-center rounded-lg hover:opacity-80 border border-transparent hover:border-primary focus:border-primary focus:outline-primary cursor-pointer duration-200 transition-opacity shadow-sm"
>
<FaPlus className="w-full text-primary text-5xl" />
<span>Neuer Ordner</span>
</li>
</ul>
)}
{createNewFolder && <CreateFolderForm parentFolderId={""} />}
{/* Folders Section ends */}
</>
)}
</div>
);
}
export default Dashboard;