diff --git a/frontend/src/components/StorageStatsCard.tsx b/frontend/src/components/StorageStatsCard.tsx deleted file mode 100644 index 31c991208..000000000 --- a/frontend/src/components/StorageStatsCard.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; -import { Card, Group, Text, Button, Progress } from "@mantine/core"; -import { useTranslation } from "react-i18next"; -import StorageIcon from "@mui/icons-material/Storage"; -import DeleteIcon from "@mui/icons-material/Delete"; -import { StorageStats } from "../services/fileStorage"; -import { formatFileSize } from "../utils/fileUtils"; -import { getStorageUsagePercent } from "../utils/storageUtils"; - -interface StorageStatsCardProps { - storageStats: StorageStats | null; - filesCount: number; - onClearAll: () => void; - onReloadFiles: () => void; -} - -const StorageStatsCard: React.FC = ({ - storageStats, - filesCount, - onClearAll, - onReloadFiles, -}) => { - const { t } = useTranslation(); - - if (!storageStats) return null; - - const storageUsagePercent = getStorageUsagePercent(storageStats); - - return ( - - - -
- - {t("fileManager.storage", "Storage")}: {formatFileSize(storageStats.used)} - {storageStats.quota && ` / ${formatFileSize(storageStats.quota)}`} - - {storageStats.quota && ( - 80 ? "red" : storageUsagePercent > 60 ? "yellow" : "blue"} - size="sm" - mt={4} - /> - )} - - {storageStats.fileCount} {t("fileManager.filesStored", "files stored")} - -
- - {filesCount > 0 && ( - - )} - - -
-
- ); -}; - -export default StorageStatsCard; \ No newline at end of file diff --git a/frontend/src/components/pageEditor/FileThumbnail.tsx b/frontend/src/components/pageEditor/FileThumbnail.tsx deleted file mode 100644 index be926f85d..000000000 --- a/frontend/src/components/pageEditor/FileThumbnail.tsx +++ /dev/null @@ -1,360 +0,0 @@ -import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react'; -import { ActionIcon, CheckboxIndicator } from '@mantine/core'; -import { useTranslation } from 'react-i18next'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; -import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined'; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; -import PushPinIcon from '@mui/icons-material/PushPin'; -import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined'; -import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; -import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; - -import styles from './PageEditor.module.css'; -import { useFileContext } from '../../contexts/FileContext'; -import { FileId } from '../../types/file'; - -interface FileItem { - id: FileId; - name: string; - pageCount: number; - thumbnail: string | null; - size: number; - modifiedAt?: number | string | Date; -} - -interface FileThumbnailProps { - file: FileItem; - index: number; - totalFiles: number; - selectedFiles: string[]; - selectionMode: boolean; - onToggleFile: (fileId: FileId) => void; - onDeleteFile: (fileId: FileId) => void; - onViewFile: (fileId: FileId) => void; - onSetStatus: (status: string) => void; - onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void; - onDownloadFile?: (fileId: FileId) => void; - toolMode?: boolean; - isSupported?: boolean; -} - -const FileThumbnail = ({ - file, - index, - selectedFiles, - onToggleFile, - onDeleteFile, - onSetStatus, - onReorderFiles, - onDownloadFile, - isSupported = true, -}: FileThumbnailProps) => { - const { t } = useTranslation(); - const { pinFile, unpinFile, isFilePinned, activeFiles } = useFileContext(); - - // ---- Drag state ---- - const [isDragging, setIsDragging] = useState(false); - const dragElementRef = useRef(null); - const [actionsWidth, setActionsWidth] = useState(undefined); - const [showActions, setShowActions] = useState(false); - - // Resolve the actual File object for pin/unpin operations - const actualFile = useMemo(() => { - return activeFiles.find(f => f.fileId === file.id); - }, [activeFiles, file.id]); - const isPinned = actualFile ? isFilePinned(actualFile) : false; - - const downloadSelectedFile = useCallback(() => { - // Prefer parent-provided handler if available - if (typeof onDownloadFile === 'function') { - onDownloadFile(file.id); - return; - } - - // Fallback: attempt to download using the File object if provided - const maybeFile = (file as unknown as { file?: File }).file; - if (maybeFile instanceof File) { - const link = document.createElement('a'); - link.href = URL.createObjectURL(maybeFile); - link.download = maybeFile.name || file.name || 'download'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(link.href); - return; - } - - // If we can't find a way to download, surface a status message - onSetStatus?.(typeof t === 'function' ? t('downloadUnavailable', 'Download unavailable for this item') : 'Download unavailable for this item'); - }, [file, onDownloadFile, onSetStatus, t]); - const handleRef = useRef(null); - - // ---- Selection ---- - const isSelected = selectedFiles.includes(file.id); - - // ---- Drag & drop wiring ---- - const fileElementRef = useCallback((element: HTMLDivElement | null) => { - if (!element) return; - - dragElementRef.current = element; - - const dragCleanup = draggable({ - element, - getInitialData: () => ({ - type: 'file', - fileId: file.id, - fileName: file.name, - selectedFiles: [file.id] // Always drag only this file, ignore selection state - }), - onDragStart: () => { - setIsDragging(true); - }, - onDrop: () => { - setIsDragging(false); - } - }); - - const dropCleanup = dropTargetForElements({ - element, - getData: () => ({ - type: 'file', - fileId: file.id - }), - canDrop: ({ source }) => { - const sourceData = source.data; - return sourceData.type === 'file' && sourceData.fileId !== file.id; - }, - onDrop: ({ source }) => { - const sourceData = source.data; - if (sourceData.type === 'file' && onReorderFiles) { - const sourceFileId = sourceData.fileId as FileId; - const selectedFileIds = sourceData.selectedFiles as FileId[]; - onReorderFiles(sourceFileId, file.id, selectedFileIds); - } - } - }); - - return () => { - dragCleanup(); - dropCleanup(); - }; - }, [file.id, file.name, selectedFiles, onReorderFiles]); - - // Update dropdown width on resize - useEffect(() => { - const update = () => { - if (dragElementRef.current) setActionsWidth(dragElementRef.current.offsetWidth); - }; - update(); - window.addEventListener('resize', update); - return () => window.removeEventListener('resize', update); - }, []); - - // Close the actions dropdown when hovering outside this file card (and its dropdown) - useEffect(() => { - if (!showActions) return; - - const isInsideCard = (target: EventTarget | null) => { - const container = dragElementRef.current; - if (!container) return false; - return target instanceof Node && container.contains(target); - }; - - const handleMouseMove = (e: MouseEvent) => { - if (!isInsideCard(e.target)) { - setShowActions(false); - } - }; - - const handleTouchStart = (e: TouchEvent) => { - // On touch devices, close if the touch target is outside the card - if (!isInsideCard(e.target)) { - setShowActions(false); - } - }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('touchstart', handleTouchStart, { passive: true }); - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('touchstart', handleTouchStart); - }; - }, [showActions]); - - // ---- Card interactions ---- - const handleCardClick = () => { - if (!isSupported) return; - onToggleFile(file.id); - }; - - - return ( -
- {/* Header bar */} -
- {/* Logo/checkbox area */} -
- {isSupported ? ( - onToggleFile(file.id)} - color="var(--checkbox-checked-bg)" - /> - ) : ( -
- - {t('unsupported', 'Unsupported')} - -
- )} -
- - {/* Centered index */} -
- {index + 1} -
- - {/* Kebab menu */} - { - e.stopPropagation(); - setShowActions((v) => !v); - }} - > - - -
- - {/* Actions overlay */} - {showActions && ( -
e.stopPropagation()} - > - - - - -
- - -
- )} - - {/* File content area */} -
- {/* Stacked file effect - multiple shadows to simulate pages */} -
- {file.thumbnail && ( - {file.name} { - // Hide broken image if blob URL was revoked - const img = e.target as HTMLImageElement; - img.style.display = 'none'; - }} - style={{ - maxWidth: '80%', - maxHeight: '80%', - objectFit: 'contain', - borderRadius: 0, - background: '#ffffff', - border: '1px solid var(--border-default)', - display: 'block', - marginLeft: 'auto', - marginRight: 'auto', - alignSelf: 'start' - }} - /> - )} -
- - {/* Pin indicator (bottom-left) */} - {isPinned && ( - - - - )} - - {/* Drag handle (span wrapper so we can attach a ref reliably) */} - - - -
-
- ); -}; - -export default React.memo(FileThumbnail); diff --git a/frontend/src/components/shared/AppConfigModal.tsx b/frontend/src/components/shared/AppConfigModal.tsx deleted file mode 100644 index a73e3ff73..000000000 --- a/frontend/src/components/shared/AppConfigModal.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from 'react'; -import { Modal, Button, Stack, Text, Code, ScrollArea, Group, Badge, Alert, Loader } from '@mantine/core'; -import { useAppConfig } from '../../hooks/useAppConfig'; - -interface AppConfigModalProps { - opened: boolean; - onClose: () => void; -} - -const AppConfigModal: React.FC = ({ opened, onClose }) => { - const { config, loading, error, refetch } = useAppConfig(); - - const renderConfigSection = (title: string, data: any) => { - if (!data || typeof data !== 'object') return null; - - return ( - - {title} - - {Object.entries(data).map(([key, value]) => ( - - - {key}: - - {typeof value === 'boolean' ? ( - - {value ? 'true' : 'false'} - - ) : typeof value === 'object' ? ( - {JSON.stringify(value, null, 2)} - ) : ( - String(value) || 'null' - )} - - ))} - - - ); - }; - - const basicConfig = config ? { - appName: config.appName, - appNameNavbar: config.appNameNavbar, - baseUrl: config.baseUrl, - contextPath: config.contextPath, - serverPort: config.serverPort, - } : null; - - const securityConfig = config ? { - enableLogin: config.enableLogin, - } : null; - - const systemConfig = config ? { - enableAlphaFunctionality: config.enableAlphaFunctionality, - enableAnalytics: config.enableAnalytics, - } : null; - - const premiumConfig = config ? { - premiumEnabled: config.premiumEnabled, - premiumKey: config.premiumKey ? '***hidden***' : null, - runningProOrHigher: config.runningProOrHigher, - runningEE: config.runningEE, - license: config.license, - } : null; - - const integrationConfig = config ? { - GoogleDriveEnabled: config.GoogleDriveEnabled, - SSOAutoLogin: config.SSOAutoLogin, - } : null; - - const legalConfig = config ? { - termsAndConditions: config.termsAndConditions, - privacyPolicy: config.privacyPolicy, - cookiePolicy: config.cookiePolicy, - impressum: config.impressum, - accessibilityStatement: config.accessibilityStatement, - } : null; - - return ( - - - - - This modal shows the current application configuration for testing purposes only. - - - - - {loading && ( - - - Loading configuration... - - )} - - {error && ( - - {error} - - )} - - {config && ( - - - {renderConfigSection('Basic Configuration', basicConfig)} - {renderConfigSection('Security Configuration', securityConfig)} - {renderConfigSection('System Configuration', systemConfig)} - {renderConfigSection('Premium/Enterprise Configuration', premiumConfig)} - {renderConfigSection('Integration Configuration', integrationConfig)} - {renderConfigSection('Legal Configuration', legalConfig)} - - {config.error && ( - - {config.error} - - )} - - - Raw Configuration - - {JSON.stringify(config, null, 2)} - - - - - )} - - - ); -}; - -export default AppConfigModal; \ No newline at end of file diff --git a/frontend/src/components/shared/FileCard.tsx b/frontend/src/components/shared/FileCard.tsx deleted file mode 100644 index 6c63af42e..000000000 --- a/frontend/src/components/shared/FileCard.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { useState } from "react"; -import { Card, Stack, Text, Group, Badge, Button, Box, Image, ThemeIcon, ActionIcon, Tooltip } from "@mantine/core"; -import { useTranslation } from "react-i18next"; -import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; -import StorageIcon from "@mui/icons-material/Storage"; -import VisibilityIcon from "@mui/icons-material/Visibility"; -import EditIcon from "@mui/icons-material/Edit"; - -import { StirlingFileStub } from "../../types/fileContext"; -import { getFileSize, getFileDate } from "../../utils/fileUtils"; -import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail"; - -interface FileCardProps { - file: File; - record?: StirlingFileStub; - onRemove: () => void; - onDoubleClick?: () => void; - onView?: () => void; - onEdit?: () => void; - isSelected?: boolean; - onSelect?: () => void; - isSupported?: boolean; // Whether the file format is supported by the current tool -} - -const FileCard = ({ file, record, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => { - const { t } = useTranslation(); - // Use record thumbnail if available, otherwise fall back to IndexedDB lookup - const fileMetadata = record ? { id: record.id, name: record.name, type: record.type, size: record.size, lastModified: record.lastModified } : null; - const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileMetadata); - const thumb = record?.thumbnailUrl || indexedDBThumb; - const [isHovered, setIsHovered] = useState(false); - - return ( - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - onClick={onSelect} - data-testid="file-card" - > - - - {/* Hover action buttons */} - {isHovered && (onView || onEdit) && ( -
e.stopPropagation()} - > - {onView && ( - - { - e.stopPropagation(); - onView(); - }} - > - - - - )} - {onEdit && ( - - { - e.stopPropagation(); - onEdit(); - }} - > - - - - )} -
- )} - {thumb ? ( - PDF thumbnail - ) : isGenerating ? ( -
-
- Generating... -
- ) : ( -
- 100 * 1024 * 1024 ? "orange" : "red"} - size={60} - radius="sm" - style={{ display: "flex", alignItems: "center", justifyContent: "center" }} - > - - - {file.size > 100 * 1024 * 1024 && ( - Large File - )} -
- )} - - - - {file.name} - - - - - {getFileSize(file)} - - - {getFileDate(file)} - - {record?.id && ( - } - > - DB - - )} - {!isSupported && ( - - {t("fileManager.unsupported", "Unsupported")} - - )} - - - - - - ); -}; - -export default FileCard; diff --git a/frontend/src/components/shared/FileGrid.tsx b/frontend/src/components/shared/FileGrid.tsx deleted file mode 100644 index 431c5bded..000000000 --- a/frontend/src/components/shared/FileGrid.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { useState } from "react"; -import { Box, Flex, Group, Text, Button, TextInput, Select } from "@mantine/core"; -import { useTranslation } from "react-i18next"; -import SearchIcon from "@mui/icons-material/Search"; -import SortIcon from "@mui/icons-material/Sort"; -import FileCard from "./FileCard"; -import { StirlingFileStub } from "../../types/fileContext"; -import { FileId } from "../../types/file"; - -interface FileGridProps { - files: Array<{ file: File; record?: StirlingFileStub }>; - onRemove?: (index: number) => void; - onDoubleClick?: (item: { file: File; record?: StirlingFileStub }) => void; - onView?: (item: { file: File; record?: StirlingFileStub }) => void; - onEdit?: (item: { file: File; record?: StirlingFileStub }) => void; - onSelect?: (fileId: FileId) => void; - selectedFiles?: FileId[]; - showSearch?: boolean; - showSort?: boolean; - maxDisplay?: number; // If set, shows only this many files with "Show All" option - onShowAll?: () => void; - showingAll?: boolean; - onDeleteAll?: () => void; - isFileSupported?: (fileName: string) => boolean; // Function to check if file is supported -} - -type SortOption = 'date' | 'name' | 'size'; - -const FileGrid = ({ - files, - onRemove, - onDoubleClick, - onView, - onEdit, - onSelect, - selectedFiles = [], - showSearch = false, - showSort = false, - maxDisplay, - onShowAll, - showingAll = false, - onDeleteAll, - isFileSupported -}: FileGridProps) => { - const { t } = useTranslation(); - const [searchTerm, setSearchTerm] = useState(""); - const [sortBy, setSortBy] = useState('date'); - - // Filter files based on search term - const filteredFiles = files.filter(item => - item.file.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - // Sort files - const sortedFiles = [...filteredFiles].sort((a, b) => { - switch (sortBy) { - case 'date': - return (b.file.lastModified || 0) - (a.file.lastModified || 0); - case 'name': - return a.file.name.localeCompare(b.file.name); - case 'size': - return (b.file.size || 0) - (a.file.size || 0); - default: - return 0; - } - }); - - // Apply max display limit if specified - const displayFiles = maxDisplay && !showingAll - ? sortedFiles.slice(0, maxDisplay) - : sortedFiles; - - const hasMoreFiles = maxDisplay && !showingAll && sortedFiles.length > maxDisplay; - - return ( - - {/* Search and Sort Controls */} - {(showSearch || showSort || onDeleteAll) && ( - - - {showSearch && ( - } - value={searchTerm} - onChange={(e) => setSearchTerm(e.currentTarget.value)} - style={{ flexGrow: 1, maxWidth: 300, minWidth: 200 }} - /> - )} - - {showSort && ( -