From 6988984cd7681d19639513d9f7cb4d761b693deb Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Sun, 12 Oct 2025 14:36:39 +0200 Subject: [PATCH] finished files page --- src/App.tsx | 2 - .../Connections/ConnectionEditModal.tsx | 2 +- .../Connections/connectionsInterfaces.ts | 2 +- .../Connections/connectionsLogic.tsx | 2 +- src/components/FilePreview/FilePreview.tsx | 2 +- .../ActionButtons/ActionButton.module.css | 5 + .../DeleteActionButton/DeleteActionButton.tsx | 7 +- .../EditActionButton/EditActionButton.tsx | 43 +- .../ViewActionButton/ViewActionButton.tsx | 10 - .../FormGenerator/FormGenerator.tsx | 39 +- src/components/Mitglieder/MitgliederTable.tsx | 4 +- src/components/Prompts/PromptsTable.tsx | 4 +- src/components/Prompts/promptsLogic.tsx | 2 +- .../Sidebar/SidebarStyles/Sidebar.module.css | 2 +- .../SidebarStyles/SidebarSubmenu.module.css | 7 + src/components/Sidebar/SidebarSubmenu.tsx | 9 +- src/components/Sidebar/sidebarTypes.ts | 1 + src/components/Workflows/WorkflowsTable.tsx | 2 +- src/components/Workflows/workflowsLogic.tsx | 2 +- src/components/ui/Button/ButtonTypes.ts | 1 - .../UploadButton/UploadButton.tsx | 12 +- .../ui/Button/UploadButton/index.ts | 2 + src/components/ui/Button/index.ts | 1 - .../DragDropOverlay.module.css | 165 +++++++ .../ui/DragDropOverlay/DragDropOverlay.tsx | 195 ++++++++ src/components/ui/DragDropOverlay/index.ts | 3 + .../MessageOverlay/MessageOverlay.module.css | 175 ++++++++ .../ui/MessageOverlay/MessageOverlay.tsx | 131 ++++++ src/components/ui/MessageOverlay/index.ts | 2 + .../{ => ui}/Popup/EditForm.module.css | 0 src/components/{ => ui}/Popup/EditForm.tsx | 0 .../{ => ui}/Popup/Popup.module.css | 0 src/components/{ => ui}/Popup/Popup.tsx | 0 .../{ => ui}/Popup/ViewForm.module.css | 0 src/components/{ => ui}/Popup/ViewForm.tsx | 0 src/components/{ => ui}/Popup/index.ts | 0 src/components/ui/UploadButton/index.ts | 3 - src/components/ui/index.ts | 6 +- src/core/PageManager/PageManager.tsx | 3 - src/core/PageManager/PageRenderer.tsx | 202 +++++---- src/core/PageManager/SidebarProvider.tsx | 24 +- .../{verwaltung.ts => administration.ts} | 32 +- .../PageManager/data/pages/example-page.ts | 256 ----------- .../data/pages/{dateien.ts => files.ts} | 141 +++--- src/core/PageManager/data/pages/index.ts | 18 +- src/core/PageManager/pageInterface.ts | 24 +- src/hooks/useFiles.ts | 419 +++++++----------- src/index.css | 5 +- src/locales/de.ts | 35 +- src/locales/en.ts | 35 +- src/locales/fr.ts | 35 +- src/main.tsx | 5 + src/pages/Home/Prompts.tsx | 6 +- src/{assets/styles => styles/assets}/bg.jpg | Bin src/styles/buttons.css | 12 +- .../PageManager => styles}/pages.module.css | 0 src/styles/themes.css | 64 --- src/{assets/styles => styles/themes}/dark.css | 3 + .../styles => styles/themes}/light.css | 3 + 59 files changed, 1263 insertions(+), 902 deletions(-) rename src/components/ui/{ => Button}/UploadButton/UploadButton.tsx (85%) create mode 100644 src/components/ui/Button/UploadButton/index.ts create mode 100644 src/components/ui/DragDropOverlay/DragDropOverlay.module.css create mode 100644 src/components/ui/DragDropOverlay/DragDropOverlay.tsx create mode 100644 src/components/ui/DragDropOverlay/index.ts create mode 100644 src/components/ui/MessageOverlay/MessageOverlay.module.css create mode 100644 src/components/ui/MessageOverlay/MessageOverlay.tsx create mode 100644 src/components/ui/MessageOverlay/index.ts rename src/components/{ => ui}/Popup/EditForm.module.css (100%) rename src/components/{ => ui}/Popup/EditForm.tsx (100%) rename src/components/{ => ui}/Popup/Popup.module.css (100%) rename src/components/{ => ui}/Popup/Popup.tsx (100%) rename src/components/{ => ui}/Popup/ViewForm.module.css (100%) rename src/components/{ => ui}/Popup/ViewForm.tsx (100%) rename src/components/{ => ui}/Popup/index.ts (100%) delete mode 100644 src/components/ui/UploadButton/index.ts rename src/core/PageManager/data/pages/{verwaltung.ts => administration.ts} (56%) delete mode 100644 src/core/PageManager/data/pages/example-page.ts rename src/core/PageManager/data/pages/{dateien.ts => files.ts} (67%) rename src/{assets/styles => styles/assets}/bg.jpg (100%) rename src/{core/PageManager => styles}/pages.module.css (100%) delete mode 100644 src/styles/themes.css rename src/{assets/styles => styles/themes}/dark.css (87%) rename src/{assets/styles => styles/themes}/light.css (94%) diff --git a/src/App.tsx b/src/App.tsx index 23ac220..c53dd79 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,8 +11,6 @@ import { AuthProvider } from './auth/authProvider'; import { ProtectedRoute } from './auth/ProtectedRoute'; import { LanguageProvider } from './contexts/LanguageContext'; import Home from './pages/Home/Home'; -// Import the global light theme CSS variables as default -import './assets/styles/light.css'; function App() { // Load saved theme preference and set app name on app mount diff --git a/src/components/Connections/ConnectionEditModal.tsx b/src/components/Connections/ConnectionEditModal.tsx index ca016dc..3d4581d 100644 --- a/src/components/Connections/ConnectionEditModal.tsx +++ b/src/components/Connections/ConnectionEditModal.tsx @@ -1,5 +1,5 @@ -import { Popup, EditForm } from '../Popup'; +import { Popup, EditForm } from '../ui/Popup'; import styles from './ConnectionEditModal.module.css'; import { ConnectionEditModalProps } from './connectionsInterfaces'; import { useLanguage } from '../../contexts/LanguageContext'; diff --git a/src/components/Connections/connectionsInterfaces.ts b/src/components/Connections/connectionsInterfaces.ts index 7d5c11b..dbfda22 100644 --- a/src/components/Connections/connectionsInterfaces.ts +++ b/src/components/Connections/connectionsInterfaces.ts @@ -1,5 +1,5 @@ import { ColumnConfig } from '../FormGenerator'; -import { EditFieldConfig } from '../Popup'; +import { EditFieldConfig } from '../ui/Popup'; // Import React for component types import React from 'react'; diff --git a/src/components/Connections/connectionsLogic.tsx b/src/components/Connections/connectionsLogic.tsx index 3f8ddda..03d62e5 100644 --- a/src/components/Connections/connectionsLogic.tsx +++ b/src/components/Connections/connectionsLogic.tsx @@ -7,7 +7,7 @@ import { MdModeEdit } from 'react-icons/md'; import { useConnections, useOAuthConnect, useDisconnect } from '../../hooks/useConnections'; import { useLanguage } from '../../contexts/LanguageContext'; import { ColumnConfig } from '../FormGenerator'; -import { EditFieldConfig } from '../Popup'; +import { EditFieldConfig } from '../ui/Popup'; import { Connection, CreateConnectionData, diff --git a/src/components/FilePreview/FilePreview.tsx b/src/components/FilePreview/FilePreview.tsx index 5e3f806..c49c6c8 100644 --- a/src/components/FilePreview/FilePreview.tsx +++ b/src/components/FilePreview/FilePreview.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { IoIosDownload, IoIosCopy } from 'react-icons/io'; -import { Popup, PopupAction } from '../Popup/Popup'; +import { Popup, PopupAction } from '../ui/Popup/Popup'; import { useLanguage } from '../../contexts/LanguageContext'; import { useFileOperations } from '../../hooks/useFiles'; import { diff --git a/src/components/FormGenerator/ActionButtons/ActionButton.module.css b/src/components/FormGenerator/ActionButtons/ActionButton.module.css index 2ec7cf7..ee9efca 100644 --- a/src/components/FormGenerator/ActionButtons/ActionButton.module.css +++ b/src/components/FormGenerator/ActionButtons/ActionButton.module.css @@ -67,6 +67,11 @@ animation: spin 1s linear infinite; } +/* Delete button loading state - no animation for user-friendly experience */ +.actionButton.delete.loading .actionIcon { + animation: none; +} + @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } diff --git a/src/components/FormGenerator/ActionButtons/DeleteActionButton/DeleteActionButton.tsx b/src/components/FormGenerator/ActionButtons/DeleteActionButton/DeleteActionButton.tsx index 1d84949..e505c67 100644 --- a/src/components/FormGenerator/ActionButtons/DeleteActionButton/DeleteActionButton.tsx +++ b/src/components/FormGenerator/ActionButtons/DeleteActionButton/DeleteActionButton.tsx @@ -146,6 +146,9 @@ export function DeleteActionButton({ // Use loading state from hookData if available const isDeletingFromHook = loadingState?.has((row as any)[idField]) || false; + + // Check if ANY deletion is in progress (not just this specific item) + const isAnyDeletionInProgress = loadingState && loadingState.size > 0; if (isConfirming) { return ( @@ -180,9 +183,9 @@ export function DeleteActionButton({ return ( + )} {selectedRows.size > 1 && onDeleteMultiple && ( - + )} @@ -768,20 +770,9 @@ export function FormGenerator>({ ? actionButton.title(row) : actionButton.title; const disabledResult = actionButton.disabled ? actionButton.disabled(row) : false; - const isDisabled = typeof disabledResult === 'boolean' ? disabledResult : disabledResult?.disabled || false; const isLoading = actionButton.loading ? actionButton.loading(row) : false; const isProcessing = actionButton.isProcessing ? actionButton.isProcessing(row) : false; - // Debug logging for disabled state - if (actionButton.type === 'edit' && import.meta.env.DEV) { - console.log('FormGenerator edit button:', { - hasDisabledFn: !!actionButton.disabled, - disabledFn: actionButton.disabled, - row, - disabledResult, - isDisabled - }); - } const baseProps = { row, @@ -796,12 +787,6 @@ export function FormGenerator>({ operationName: actionButton.operationName, loadingStateName: actionButton.loadingStateName }; - - // Debug logging for view buttons - if (actionButton.type === 'view' && import.meta.env.DEV) { - console.log('FormGenerator actionButton config:', actionButton); - console.log('FormGenerator baseProps:', baseProps); - } switch (actionButton.type) { case 'edit': diff --git a/src/components/Mitglieder/MitgliederTable.tsx b/src/components/Mitglieder/MitgliederTable.tsx index 6cdd1ca..15f523a 100644 --- a/src/components/Mitglieder/MitgliederTable.tsx +++ b/src/components/Mitglieder/MitgliederTable.tsx @@ -1,6 +1,6 @@ import { FormGenerator } from '../FormGenerator/FormGenerator'; -import { Popup } from '../Popup/Popup'; -import { EditForm, EditFieldConfig } from '../Popup/EditForm'; +import { Popup } from '../ui/Popup/Popup'; +import { EditForm, EditFieldConfig } from '../ui/Popup/EditForm'; import { useMitgliederLogic } from './mitgliederLogic'; import { MitgliederTableProps } from './mitgliederTypes'; import { useLanguage } from '../../contexts/LanguageContext'; diff --git a/src/components/Prompts/PromptsTable.tsx b/src/components/Prompts/PromptsTable.tsx index 2d318e9..a2b5304 100644 --- a/src/components/Prompts/PromptsTable.tsx +++ b/src/components/Prompts/PromptsTable.tsx @@ -1,8 +1,8 @@ import { FormGenerator } from '../FormGenerator/FormGenerator'; -import { Popup } from '../Popup/Popup'; -import { EditForm } from '../Popup/EditForm'; +import { Popup } from '../ui/Popup/Popup'; +import { EditForm } from '../ui/Popup/EditForm'; import { usePromptsLogic } from './promptsLogic'; import { PromptsTableProps, Prompt } from './promptsTypes'; import { useLanguage } from '../../contexts/LanguageContext'; diff --git a/src/components/Prompts/promptsLogic.tsx b/src/components/Prompts/promptsLogic.tsx index a61fe06..72f0913 100644 --- a/src/components/Prompts/promptsLogic.tsx +++ b/src/components/Prompts/promptsLogic.tsx @@ -4,7 +4,7 @@ import { MdModeEdit } from 'react-icons/md'; import { usePrompts, usePromptOperations, Prompt } from '../../hooks/usePrompts'; import { useLanguage } from '../../contexts/LanguageContext'; -import type { EditFieldConfig } from '../Popup/EditForm'; +import type { EditFieldConfig } from '../ui/Popup/EditForm'; // Helper function to determine if a prompt can be deleted const isPromptDeletable = (prompt: Prompt): boolean => { diff --git a/src/components/Sidebar/SidebarStyles/Sidebar.module.css b/src/components/Sidebar/SidebarStyles/Sidebar.module.css index eb9351e..5500fc7 100644 --- a/src/components/Sidebar/SidebarStyles/Sidebar.module.css +++ b/src/components/Sidebar/SidebarStyles/Sidebar.module.css @@ -2,7 +2,7 @@ .sidebarContainer { border-radius: 0px; background: var(--color-bg); - /*background-image: url('../../../assets/styles/bg.jpg'); + /*background-image: url('../../../styles/assets/bg.jpg'); background-size: cover; background-position: center; background-repeat: no-repeat; diff --git a/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css b/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css index af64e69..31269b2 100644 --- a/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css +++ b/src/components/Sidebar/SidebarStyles/SidebarSubmenu.module.css @@ -63,3 +63,10 @@ overflow: hidden; white-space: nowrap; } + +.submenuIcon { + width: 16px; + height: 16px; + color: #181818; + flex-shrink: 0; +} \ No newline at end of file diff --git a/src/components/Sidebar/SidebarSubmenu.tsx b/src/components/Sidebar/SidebarSubmenu.tsx index a68c05a..8a7e830 100644 --- a/src/components/Sidebar/SidebarSubmenu.tsx +++ b/src/components/Sidebar/SidebarSubmenu.tsx @@ -39,6 +39,8 @@ const SidebarSubmenu: React.FC = ({ item, isOpen }) => { return () => window.removeEventListener('resize', checkOverflow); }, [subitem.name]); + const SubIcon = subitem.icon as React.ComponentType>; + return (
  • = ({ item, isOpen }) => { } })} > -
    - {subitem.name} +
    + {SubIcon && } + + {subitem.name} +
    diff --git a/src/components/Sidebar/sidebarTypes.ts b/src/components/Sidebar/sidebarTypes.ts index 36b6139..b5466ac 100644 --- a/src/components/Sidebar/sidebarTypes.ts +++ b/src/components/Sidebar/sidebarTypes.ts @@ -15,6 +15,7 @@ export interface SidebarSubmenuItemData { id: string; name: string; link?: string; + icon?: React.ComponentType>; } // Sidebar state interface diff --git a/src/components/Workflows/WorkflowsTable.tsx b/src/components/Workflows/WorkflowsTable.tsx index 6cc55fb..ea50878 100644 --- a/src/components/Workflows/WorkflowsTable.tsx +++ b/src/components/Workflows/WorkflowsTable.tsx @@ -1,7 +1,7 @@ import { FormGenerator } from '../FormGenerator/FormGenerator'; -import { Popup, EditForm } from '../Popup'; +import { Popup, EditForm } from '../ui/Popup'; import { useWorkflowsLogic } from './workflowsLogic'; import { WorkflowsTableProps } from './workflowsTypes'; import styles from './WorkflowsTable.module.css'; diff --git a/src/components/Workflows/workflowsLogic.tsx b/src/components/Workflows/workflowsLogic.tsx index c3c94db..69f1d59 100644 --- a/src/components/Workflows/workflowsLogic.tsx +++ b/src/components/Workflows/workflowsLogic.tsx @@ -7,7 +7,7 @@ import { MdModeEdit } from 'react-icons/md'; import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows'; import { useApiRequest } from '../../hooks/useApi'; import { useLanguage } from '../../contexts/LanguageContext'; -import type { EditFieldConfig } from '../Popup/EditForm'; +import type { EditFieldConfig } from '../ui/Popup/EditForm'; import type { WorkflowsLogicReturn, diff --git a/src/components/ui/Button/ButtonTypes.ts b/src/components/ui/Button/ButtonTypes.ts index b940d10..266bb01 100644 --- a/src/components/ui/Button/ButtonTypes.ts +++ b/src/components/ui/Button/ButtonTypes.ts @@ -26,4 +26,3 @@ export interface UploadButtonProps extends BaseButtonProps { icon?: IconType; iconPosition?: 'left' | 'right'; } - diff --git a/src/components/ui/UploadButton/UploadButton.tsx b/src/components/ui/Button/UploadButton/UploadButton.tsx similarity index 85% rename from src/components/ui/UploadButton/UploadButton.tsx rename to src/components/ui/Button/UploadButton/UploadButton.tsx index 415070e..7345f05 100644 --- a/src/components/ui/UploadButton/UploadButton.tsx +++ b/src/components/ui/Button/UploadButton/UploadButton.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState } from 'react'; -import { UploadButtonProps } from '../Button/ButtonTypes'; -import Button from '../Button/Button'; +import { UploadButtonProps } from '../ButtonTypes'; +import Button from '../Button'; const UploadButton: React.FC = ({ onUpload, @@ -54,7 +54,6 @@ const UploadButton: React.FC = ({ }; const isDisabled = disabled || loading || isUploading; - const isButtonLoading = loading || isUploading; return ( <> @@ -63,12 +62,15 @@ const UploadButton: React.FC = ({ variant={variant} size={size} disabled={isDisabled} - loading={isButtonLoading} + loading={false} // We handle the loading state manually className={`uploadButton ${className}`} onClick={handleClick} - icon={icon} + icon={isUploading ? undefined : icon} // Hide original icon when uploading iconPosition={iconPosition} > + {isUploading && ( +
    + )} {children || (isUploading ? 'Uploading...' : 'Upload File')} diff --git a/src/components/ui/Button/UploadButton/index.ts b/src/components/ui/Button/UploadButton/index.ts new file mode 100644 index 0000000..458fbb3 --- /dev/null +++ b/src/components/ui/Button/UploadButton/index.ts @@ -0,0 +1,2 @@ +export { default as UploadButton } from './UploadButton'; +export type { UploadButtonProps } from '../ButtonTypes'; diff --git a/src/components/ui/Button/index.ts b/src/components/ui/Button/index.ts index 104c3a8..e283a04 100644 --- a/src/components/ui/Button/index.ts +++ b/src/components/ui/Button/index.ts @@ -1,3 +1,2 @@ export { default as Button } from './Button'; export * from './ButtonTypes'; - diff --git a/src/components/ui/DragDropOverlay/DragDropOverlay.module.css b/src/components/ui/DragDropOverlay/DragDropOverlay.module.css new file mode 100644 index 0000000..200e2dc --- /dev/null +++ b/src/components/ui/DragDropOverlay/DragDropOverlay.module.css @@ -0,0 +1,165 @@ +.dragDropContainer { + position: relative; + width: 100%; + height: 100%; +} + +.hiddenFileInput { + position: absolute; + top: -9999px; + left: -9999px; + opacity: 0; + pointer-events: none; +} + +.dragOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(var(--color-primary-rgb, 0, 123, 255), 0.1); + border: 2px dashed var(--color-primary, #007bff); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(2px); + animation: fadeIn 0.2s ease-in-out; +} + +.dragOverlayContent { + text-align: center; + color: var(--color-primary, #007bff); + padding: 2rem; +} + +.dragIcon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.8; +} + +.dragText { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.dragSubtext { + font-size: 1rem; + opacity: 0.8; + max-width: 300px; + line-height: 1.4; +} + +.processingOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(var(--color-bg-rgb, 255, 255, 255), 0.9); + display: flex; + align-items: center; + justify-content: center; + z-index: 1001; + backdrop-filter: blur(2px); + animation: fadeIn 0.2s ease-in-out; +} + +.processingContent { + text-align: center; + color: var(--color-text, #333); +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid var(--color-primary, #007bff); + border-top: 3px solid transparent; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 0 auto 1rem; +} + +.processingText { + font-size: 1.1rem; + font-weight: 500; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* Dark theme support */ +[data-theme="dark"] .dragOverlay { + background: rgba(var(--color-primary-rgb, 0, 123, 255), 0.15); + border-color: var(--color-primary, #007bff); +} + +[data-theme="dark"] .dragOverlayContent { + color: var(--color-primary, #007bff); +} + +[data-theme="dark"] .processingOverlay { + background: rgba(var(--color-bg-rgb, 0, 0, 0), 0.9); +} + +[data-theme="dark"] .processingContent { + color: var(--color-text, #fff); +} + +/* Responsive design */ +@media (max-width: 768px) { + .dragOverlayContent { + padding: 1.5rem; + } + + .dragIcon { + font-size: 2.5rem; + } + + .dragText { + font-size: 1.25rem; + } + + .dragSubtext { + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + .dragOverlayContent { + padding: 1rem; + } + + .dragIcon { + font-size: 2rem; + } + + .dragText { + font-size: 1.1rem; + } + + .dragSubtext { + font-size: 0.85rem; + } +} diff --git a/src/components/ui/DragDropOverlay/DragDropOverlay.tsx b/src/components/ui/DragDropOverlay/DragDropOverlay.tsx new file mode 100644 index 0000000..b6a1161 --- /dev/null +++ b/src/components/ui/DragDropOverlay/DragDropOverlay.tsx @@ -0,0 +1,195 @@ +import React, { useState, useCallback } from 'react'; +import { useLanguage } from '../../../contexts/LanguageContext'; +import styles from './DragDropOverlay.module.css'; +import { IoFolderOpen } from 'react-icons/io5'; + +export interface DragDropConfig { + enabled: boolean; + onDrop?: (files: File[]) => Promise | void; + accept?: string; // MIME types or file extensions + multiple?: boolean; + disabled?: boolean; + overlayText?: string; + overlaySubtext?: string; +} + +interface DragDropOverlayProps { + config: DragDropConfig; + children: React.ReactNode; + className?: string; +} + +export function DragDropOverlay({ + config, + children, + className = '' +}: DragDropOverlayProps) { + const { t } = useLanguage(); + const [isDragOver, setIsDragOver] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (config.disabled || !config.enabled) return; + + setIsDragOver(true); + }, [config.disabled, config.enabled]); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Only hide overlay if we're leaving the container entirely + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + setIsDragOver(false); + } + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleDrop = useCallback(async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (config.disabled || !config.enabled) return; + + setIsDragOver(false); + setIsProcessing(true); + + try { + const files = Array.from(e.dataTransfer.files); + + // Filter files by accept type if specified + let filteredFiles = files; + if (config.accept && config.accept !== '*/*') { + filteredFiles = files.filter(file => { + const acceptTypes = config.accept!.split(',').map(type => type.trim()); + return acceptTypes.some(acceptType => { + // Handle MIME types + if (acceptType.startsWith('.')) { + return file.name.toLowerCase().endsWith(acceptType.toLowerCase()); + } + // Handle MIME types like "image/*" or "application/pdf" + if (acceptType.includes('*')) { + const baseType = acceptType.split('/')[0]; + return file.type.startsWith(baseType); + } + return file.type === acceptType; + }); + }); + } + + console.log('🔍 DragDropOverlay file filtering:', { + originalFiles: files.length, + filteredFiles: filteredFiles.length, + accept: config.accept, + fileDetails: files.map(f => ({ name: f.name, type: f.type })) + }); + + // Respect multiple setting + const filesToProcess = config.multiple ? filteredFiles : filteredFiles.slice(0, 1); + + if (filesToProcess.length > 0 && config.onDrop) { + console.log('🎯 DragDropOverlay calling onDrop with files:', filesToProcess); + await config.onDrop(filesToProcess); + console.log('✅ DragDropOverlay onDrop completed'); + } else { + console.log('⚠️ DragDropOverlay: No files to process or no onDrop handler'); + console.log('Files to process:', filesToProcess.length); + console.log('Has onDrop:', !!config.onDrop); + } + } catch (error) { + console.error(t('dragdrop.overlay.error', 'Error processing files'), error); + } finally { + setIsProcessing(false); + } + }, [config, t]); + + const handleFileInputChange = useCallback(async (e: React.ChangeEvent) => { + if (config.disabled || !config.enabled) return; + + const files = Array.from(e.target.files || []); + console.log('🔍 DragDropOverlay file input:', { + fileCount: files.length, + accept: config.accept, + fileDetails: files.map(f => ({ name: f.name, type: f.type })) + }); + + if (files.length > 0 && config.onDrop) { + setIsProcessing(true); + try { + await config.onDrop(files); + } catch (error) { + console.error(t('dragdrop.overlay.error', 'Error processing files'), error); + } finally { + setIsProcessing(false); + // Reset input + e.target.value = ''; + } + } + }, [config, t]); + + if (!config.enabled) { + return <>{children}; + } + + return ( +
    + {children} + + {/* Hidden file input for click-to-upload */} + + + {/* Drag overlay */} + {isDragOver && ( +
    +
    +
    + +
    +
    + {config.overlayText || t('dragdrop.overlay.default_text', 'Drop files here')} +
    + {(config.overlaySubtext || !config.overlayText) && ( +
    + {config.overlaySubtext || t('dragdrop.overlay.default_subtext', 'You can also click the upload button')} +
    + )} +
    +
    + )} + + {/* Processing overlay */} + {isProcessing && ( +
    +
    +
    +
    + {t('dragdrop.overlay.processing', 'Processing files...')} +
    +
    +
    + )} +
    + ); +} + +export default DragDropOverlay; diff --git a/src/components/ui/DragDropOverlay/index.ts b/src/components/ui/DragDropOverlay/index.ts new file mode 100644 index 0000000..8b78f4b --- /dev/null +++ b/src/components/ui/DragDropOverlay/index.ts @@ -0,0 +1,3 @@ +export { DragDropOverlay } from './DragDropOverlay'; +export type { DragDropConfig } from './DragDropOverlay'; +export { DragDropOverlay as default } from './DragDropOverlay'; diff --git a/src/components/ui/MessageOverlay/MessageOverlay.module.css b/src/components/ui/MessageOverlay/MessageOverlay.module.css new file mode 100644 index 0000000..3b0499b --- /dev/null +++ b/src/components/ui/MessageOverlay/MessageOverlay.module.css @@ -0,0 +1,175 @@ +.messageOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100vw; + height: 100vh; + z-index: 9999; + backdrop-filter: blur(8px); + background-color: rgba(0, 0, 0, 0.1); + pointer-events: auto; + animation: fadeIn 0.3s ease-out forwards; +} + +.messageOverlay.closing { + animation: fadeOut 0.3s ease-out forwards; + pointer-events: none; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes slideIn { + from { + transform: translateY(-100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slideOut { + from { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY(-100%); + opacity: 0; + } +} + +.messageContainer { + display: flex; + justify-content: center; + align-items: flex-start; + padding: 30px; + pointer-events: none; +} + +.messageContent { + border-radius: var(--object-radius-large); + padding: 30px; + max-width: 500px; + width: 100%; + position: relative; + border: 2px solid; + pointer-events: auto; + animation: slideIn 0.4s ease-out forwards; +} + +.messageOverlay.closing .messageContent { + animation: slideOut 0.3s ease-out forwards; +} + +/* Warning Mode */ +.warningMode { + background-color: color-mix(in srgb, var(--color-red) 20%, transparent); + border-color: var(--color-red); + box-shadow: 0 8px 32px rgba(220, 38, 38, 0.4); +} + +/* Error Mode */ +.errorMode { + background-color: color-mix(in srgb, var(--color-red) 20%, transparent); + border-color: var(--color-red); + box-shadow: 0 8px 32px rgba(220, 38, 38, 0.4); +} + +/* Success Mode */ +.successMode { + background-color: color-mix(in srgb, var(--color-green, #16a34a) 90%, black); + border-color: var(--color-green, #16a34a); + box-shadow: 0 8px 32px rgba(22, 163, 74, 0.4); +} + +/* Info Mode */ +.infoMode { + background-color: color-mix(in srgb, var(--color-blue, #2563eb) 90%, black); + border-color: var(--color-blue, #2563eb); + box-shadow: 0 8px 32px rgba(37, 99, 235, 0.4); +} + +.messageHeader { + font-size: 18px; + font-weight: 600; + color: white; + line-height: 1.4; +} + +.messageText { + font-size: 16px; + color: rgba(255, 255, 255, 0.9); + line-height: 1.5; +} + +.closeButton { + position: absolute; + top: 12px; + right: 12px; + background: none; + border: none; + color: var(--color-red); + font-size: 24px; + font-weight: bold; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: background-color 0.2s ease, opacity 0.2s ease; + line-height: 1; +} + +.closeButton:hover { + background: rgba(255, 255, 255, 0.2); + opacity: 1; +} + +.closeButton:active { + background: rgba(255, 255, 255, 0.3); + opacity: 1; +} + +/* Responsive design */ +@media (max-width: 640px) { + .messageContainer { + padding: 15px; + } + + .messageContent { + padding: 20px; + } + + .messageHeader { + font-size: 16px; + } + + .messageText { + font-size: 14px; + } + + .closeButton { + top: 8px; + right: 8px; + font-size: 20px; + padding: 2px 6px; + } +} diff --git a/src/components/ui/MessageOverlay/MessageOverlay.tsx b/src/components/ui/MessageOverlay/MessageOverlay.tsx new file mode 100644 index 0000000..206ac36 --- /dev/null +++ b/src/components/ui/MessageOverlay/MessageOverlay.tsx @@ -0,0 +1,131 @@ +import React, { useEffect, useState, useRef } from 'react'; +import styles from './MessageOverlay.module.css'; + +export type MessageMode = 'warning' | 'error' | 'success' | 'info'; + +interface MessageOverlayProps { + header: string; + message: string; + isVisible: boolean; + mode?: MessageMode; + onClose?: () => void; + autoClose?: boolean; + autoCloseDelay?: number; +} + +const MessageOverlay: React.FC = ({ + header, + message, + isVisible, + mode = 'info', + onClose, + autoClose = true, + autoCloseDelay = 5000 +}) => { + const [isClosing, setIsClosing] = useState(false); + const autoCloseTimeoutRef = useRef(null); + const unmountTimeoutRef = useRef(null); + + // Handle auto-close + useEffect(() => { + if (autoCloseTimeoutRef.current) { + clearTimeout(autoCloseTimeoutRef.current); + } + + if (isVisible && autoClose && onClose) { + autoCloseTimeoutRef.current = window.setTimeout(() => { + setIsClosing(true); + // After animation completes, call onClose + unmountTimeoutRef.current = window.setTimeout(() => { + onClose(); + }, 700); // Match slideOut animation duration + }, autoCloseDelay); + } + + return () => { + if (autoCloseTimeoutRef.current) { + clearTimeout(autoCloseTimeoutRef.current); + } + }; + }, [isVisible, autoClose, autoCloseDelay, onClose]); + + // Handle manual close + const handleClose = () => { + setIsClosing(true); + setTimeout(() => { + if (onClose) { + onClose(); + } + }, 700); + }; + + // Cleanup on unmount + useEffect(() => { + return () => { + if (autoCloseTimeoutRef.current) { + clearTimeout(autoCloseTimeoutRef.current); + } + if (unmountTimeoutRef.current) { + clearTimeout(unmountTimeoutRef.current); + } + }; + }, []); + + if (!isVisible) { + return null; + } + + const getModeClass = () => { + switch (mode) { + case 'warning': + return styles.warningMode; + case 'error': + return styles.errorMode; + case 'success': + return styles.successMode; + case 'info': + default: + return styles.infoMode; + } + }; + + const getAriaLabel = () => { + switch (mode) { + case 'warning': + return 'Close warning'; + case 'error': + return 'Close error'; + case 'success': + return 'Close success message'; + case 'info': + default: + return 'Close message'; + } + }; + + return ( +
    +
    +
    +
    + {header} +
    +
    + {message} +
    + {onClose && ( + + )} +
    +
    +
    + ); +}; + +export default MessageOverlay; diff --git a/src/components/ui/MessageOverlay/index.ts b/src/components/ui/MessageOverlay/index.ts new file mode 100644 index 0000000..8c38fdc --- /dev/null +++ b/src/components/ui/MessageOverlay/index.ts @@ -0,0 +1,2 @@ +export { default } from './MessageOverlay'; +export type { MessageMode } from './MessageOverlay'; \ No newline at end of file diff --git a/src/components/Popup/EditForm.module.css b/src/components/ui/Popup/EditForm.module.css similarity index 100% rename from src/components/Popup/EditForm.module.css rename to src/components/ui/Popup/EditForm.module.css diff --git a/src/components/Popup/EditForm.tsx b/src/components/ui/Popup/EditForm.tsx similarity index 100% rename from src/components/Popup/EditForm.tsx rename to src/components/ui/Popup/EditForm.tsx diff --git a/src/components/Popup/Popup.module.css b/src/components/ui/Popup/Popup.module.css similarity index 100% rename from src/components/Popup/Popup.module.css rename to src/components/ui/Popup/Popup.module.css diff --git a/src/components/Popup/Popup.tsx b/src/components/ui/Popup/Popup.tsx similarity index 100% rename from src/components/Popup/Popup.tsx rename to src/components/ui/Popup/Popup.tsx diff --git a/src/components/Popup/ViewForm.module.css b/src/components/ui/Popup/ViewForm.module.css similarity index 100% rename from src/components/Popup/ViewForm.module.css rename to src/components/ui/Popup/ViewForm.module.css diff --git a/src/components/Popup/ViewForm.tsx b/src/components/ui/Popup/ViewForm.tsx similarity index 100% rename from src/components/Popup/ViewForm.tsx rename to src/components/ui/Popup/ViewForm.tsx diff --git a/src/components/Popup/index.ts b/src/components/ui/Popup/index.ts similarity index 100% rename from src/components/Popup/index.ts rename to src/components/ui/Popup/index.ts diff --git a/src/components/ui/UploadButton/index.ts b/src/components/ui/UploadButton/index.ts deleted file mode 100644 index ba325f2..0000000 --- a/src/components/ui/UploadButton/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as UploadButton } from './UploadButton'; -export type { UploadButtonProps } from '../Button/ButtonTypes'; - diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index a926731..782ab2b 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -1,3 +1,5 @@ export * from './Button'; -export * from './UploadButton'; - +export * from './Button/UploadButton'; +export { default as MessageOverlay } from './MessageOverlay'; +export type { MessageMode } from './MessageOverlay'; +export * from './DragDropOverlay'; diff --git a/src/core/PageManager/PageManager.tsx b/src/core/PageManager/PageManager.tsx index 4b5c832..a6df63f 100644 --- a/src/core/PageManager/PageManager.tsx +++ b/src/core/PageManager/PageManager.tsx @@ -3,7 +3,6 @@ import { useLocation } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { getPageDataByPath, GenericPageData, PageInstance } from './data'; import PageRenderer from './PageRenderer'; -import { useLanguage } from '../../contexts/LanguageContext'; interface PageManagerProps { loadingComponent: React.ComponentType; @@ -16,7 +15,6 @@ const PageManager: React.FC = ({ }) => { const location = useLocation(); const [pageInstances, setPageInstances] = useState>(new Map()); - const { currentLanguage } = useLanguage(); // Get current path const getCurrentPath = () => { @@ -100,7 +98,6 @@ const PageManager: React.FC = ({ ) : ( { console.log(`Button clicked: ${buttonId}`, button); // Add global button click handling here diff --git a/src/core/PageManager/PageRenderer.tsx b/src/core/PageManager/PageRenderer.tsx index 393e17e..5555437 100644 --- a/src/core/PageManager/PageRenderer.tsx +++ b/src/core/PageManager/PageRenderer.tsx @@ -2,19 +2,22 @@ import React from 'react'; import { GenericPageData, PageButton, PageContent, resolveLanguageText } from './pageInterface'; import { FormGenerator } from '../../components/FormGenerator'; import { Button, UploadButton } from '../../components/ui'; -import styles from './pages.module.css'; +import { DragDropOverlay } from '../../components/ui/DragDropOverlay'; +import { useLanguage } from '../../contexts/LanguageContext'; +import styles from '../../styles/pages.module.css'; interface PageRendererProps { pageData: GenericPageData; onButtonClick?: (buttonId: string, button: PageButton) => void; - language?: 'de' | 'en' | 'fr'; } const PageRenderer: React.FC = ({ pageData, - onButtonClick, - language = 'de' + onButtonClick }) => { + // Get translation function from language context + const { t } = useLanguage(); + // Call the hook at the top level to ensure it persists across renders // This is CRITICAL - hooks must be called in the same order on every render const tableContent = pageData.content?.find(content => content.type === 'table'); @@ -32,6 +35,12 @@ const PageRenderer: React.FC = ({ // Call the hook to get the current data // This will be called on every render, but it's the SAME hook instance const hookData = useTableData ? useTableData() : null; + + // Debug hook data + if (import.meta.env.DEV && hookData) { + console.log('🔍 PageRenderer hookData:', hookData); + console.log('🔍 PageRenderer has handleUpload:', !!hookData.handleUpload); + } // Handle button clicks const handleButtonClick = async (button: PageButton) => { @@ -67,13 +76,13 @@ const PageRenderer: React.FC = ({ return React.createElement( HeadingTag, { key: content.id, className: styles.contentHeading }, - resolveLanguageText(content.content, language) + resolveLanguageText(content.content, t) ); case 'paragraph': return (

    - {resolveLanguageText(content.content, language)} + {resolveLanguageText(content.content, t)}

    ); @@ -81,12 +90,12 @@ const PageRenderer: React.FC = ({ return (
    {content.content && ( -

    {resolveLanguageText(content.content, language)}

    +

    {resolveLanguageText(content.content, t)}

    )}
      {content.items?.map((item, index) => (
    • - {resolveLanguageText(item, language)} + {resolveLanguageText(item, t)}
    • ))}
    @@ -97,7 +106,7 @@ const PageRenderer: React.FC = ({ return (
                             
    -                            {resolveLanguageText(content.content, language)}
    +                            {resolveLanguageText(content.content, t)}
                             
                         
    ); @@ -142,7 +151,7 @@ const PageRenderer: React.FC = ({ // CRITICAL: Resolve LanguageText objects in column labels const resolvedColumns = columns.map(col => ({ ...col, - label: resolveLanguageText(col.label, language) + label: resolveLanguageText(col.label, t) })); // Convert action buttons to FormGenerator format @@ -152,7 +161,7 @@ const PageRenderer: React.FC = ({ type: action.type, onAction: action.onAction, // CRITICAL: Resolve LanguageText objects in action titles - title: resolveLanguageText(action.title, language), + title: resolveLanguageText(action.title, t), isProcessing: action.loading || (() => false), disabled: action.disabled || (() => false), // Preserve field mappings and operation names @@ -177,6 +186,8 @@ const PageRenderer: React.FC = ({ loading={showLoadingSpinner} actionButtons={formGeneratorActions} hookData={hookData} + onDelete={hookData.onDelete} + onDeleteMultiple={hookData.onDeleteMultiple} {...tableProps} />
    @@ -189,80 +200,121 @@ const PageRenderer: React.FC = ({ } }; + // Create enhanced drag drop config with hook data integration + const getDragDropConfig = () => { + if (!pageData.dragDropConfig) { + return { enabled: false, onDrop: () => {} }; + } + + console.log('🔍 DragDrop Debug - hookData:', hookData); + console.log('🔍 DragDrop Debug - has handleUpload:', !!hookData?.handleUpload); + + // If the page has drag drop config and hook data with handleUpload, integrate them + if (hookData?.handleUpload) { + return { + ...pageData.dragDropConfig, + onDrop: async (files: File[]) => { + console.log('🚀 DragDrop onDrop triggered with files:', files); + try { + // Process each file through the hook's handleUpload function + for (const file of files) { + if (hookData.handleUpload) { + console.log('📤 Uploading file:', file.name); + await hookData.handleUpload(file); + console.log('✅ File uploaded successfully:', file.name); + } + } + } catch (error) { + console.error('❌ Error uploading dropped files:', error); + } + } + }; + } + + console.log('⚠️ DragDrop Debug - No handleUpload found, using fallback config'); + // Fallback to the original config + return pageData.dragDropConfig; + }; + return ( -
    -
    - {/* Page Header */} -
    -
    -

    {resolveLanguageText(pageData.title, language)}

    - {pageData.subtitle && ( -

    {resolveLanguageText(pageData.subtitle, language)}

    + +
    +
    + {/* Page Header */} +
    +
    +

    {resolveLanguageText(pageData.title, t)}

    + {pageData.subtitle && ( +

    {resolveLanguageText(pageData.subtitle, t)}

    + )} +
    + + {/* Header Buttons */} + {pageData.headerButtons && pageData.headerButtons.length > 0 && ( +
    + {pageData.headerButtons.map((button) => { + // Check if this is an upload button + if (button.id === 'upload-file') { + const handleUpload = (hookData as any)?.handleUpload; + + if (handleUpload) { + return ( + + {resolveLanguageText(button.label, t)} + + ); + } + } + + // Regular button + return ( + + ); + })} +
    )}
    - {/* Header Buttons */} - {pageData.headerButtons && pageData.headerButtons.length > 0 && ( -
    - {pageData.headerButtons.map((button) => { - // Check if this is an upload button - if (button.id === 'upload-file') { - const handleUpload = (hookData as any)?.handleUpload; - - if (handleUpload) { - return ( - - {resolveLanguageText(button.label, language)} - - ); - } +
    + + {/* Page Content */} +
    +
    + {pageData.content?.map((content) => { + // Check privilege for content + if (content.privilegeChecker) { + // For now, we'll render content and let the privilege checker handle it + // In a real implementation, you might want to use a hook or context + return renderContent(content); } - - // Regular button - return ( - - ); + return renderContent(content); })}
    - )} -
    - -
    - - {/* Page Content */} -
    -
    - {pageData.content?.map((content) => { - // Check privilege for content - if (content.privilegeChecker) { - // For now, we'll render content and let the privilege checker handle it - // In a real implementation, you might want to use a hook or context - return renderContent(content); - } - return renderContent(content); - })}
    + + {/* Message Overlay Component */} + {hookData?.MessageOverlayComponent && }
    -
    + ); }; diff --git a/src/core/PageManager/SidebarProvider.tsx b/src/core/PageManager/SidebarProvider.tsx index 6a30b7d..31660d4 100644 --- a/src/core/PageManager/SidebarProvider.tsx +++ b/src/core/PageManager/SidebarProvider.tsx @@ -1,5 +1,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { allPageData, SidebarItem } from './data'; +import { useLanguage } from '../../contexts/LanguageContext'; +import { resolveLanguageText } from './pageInterface'; interface SidebarContextType { sidebarItems: SidebarItem[]; @@ -26,6 +28,9 @@ export const SidebarProvider: React.FC = ({ children }) => const [sidebarItems, setSidebarItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + + // Get translation function from language context + const { t } = useLanguage(); // Get sidebar items from page data const getSidebarItems = async (): Promise => { @@ -72,22 +77,23 @@ export const SidebarProvider: React.FC = ({ children }) => // Create expandable item with submenu items.push({ id: pageData.id, - name: pageData.name, + name: resolveLanguageText(pageData.name, t), link: `/${pageData.path}`, icon: pageData.icon, moduleEnabled: pageData.moduleEnabled ?? true, order: pageData.order || 0, submenu: subpages.map(subpage => ({ id: subpage.id, - name: subpage.name, - link: `/${subpage.path}` + name: resolveLanguageText(subpage.name, t), + link: `/${subpage.path}`, + icon: subpage.icon })) }); } else { // No subpages found, show as regular item items.push({ id: pageData.id, - name: pageData.name, + name: resolveLanguageText(pageData.name, t), link: `/${pageData.path}`, icon: pageData.icon, moduleEnabled: pageData.moduleEnabled ?? true, @@ -98,7 +104,7 @@ export const SidebarProvider: React.FC = ({ children }) => // No subpage privilege, show as regular non-expandable item items.push({ id: pageData.id, - name: pageData.name, + name: resolveLanguageText(pageData.name, t), link: `/${pageData.path}`, icon: pageData.icon, moduleEnabled: pageData.moduleEnabled ?? true, @@ -110,7 +116,7 @@ export const SidebarProvider: React.FC = ({ children }) => // Fallback to regular item on error items.push({ id: pageData.id, - name: pageData.name, + name: resolveLanguageText(pageData.name, t), link: `/${pageData.path}`, icon: pageData.icon, moduleEnabled: pageData.moduleEnabled ?? true, @@ -121,7 +127,7 @@ export const SidebarProvider: React.FC = ({ children }) => // Regular items without subpages items.push({ id: pageData.id, - name: pageData.name, + name: resolveLanguageText(pageData.name, t), link: `/${pageData.path}`, icon: pageData.icon, moduleEnabled: pageData.moduleEnabled ?? true, @@ -149,10 +155,10 @@ export const SidebarProvider: React.FC = ({ children }) => } }; - // Load sidebar items on mount + // Load sidebar items on mount and when language changes useEffect(() => { refreshSidebar(); - }, []); + }, [t]); const contextValue: SidebarContextType = { sidebarItems, diff --git a/src/core/PageManager/data/pages/verwaltung.ts b/src/core/PageManager/data/pages/administration.ts similarity index 56% rename from src/core/PageManager/data/pages/verwaltung.ts rename to src/core/PageManager/data/pages/administration.ts index 3d20717..1b2bda6 100644 --- a/src/core/PageManager/data/pages/verwaltung.ts +++ b/src/core/PageManager/data/pages/administration.ts @@ -2,45 +2,45 @@ import { GenericPageData } from '../../pageInterface'; import { FaCogs } from 'react-icons/fa'; import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; -export const verwaltungPageData: GenericPageData = { - id: 'verwaltung', - path: 'verwaltung', - name: 'Verwaltung', - description: 'Administration and management tools', +export const administrationPageData: GenericPageData = { + id: 'administration', + path: 'administration', + name: 'administration.title', + description: 'administration.description', // Visual icon: FaCogs, - title: 'Verwaltung', - subtitle: 'Administration and management tools', + title: 'administration.title', + subtitle: 'administration.subtitle', // Content sections content: [ { id: 'intro', type: 'heading', - content: 'Verwaltung', + content: 'administration.title', level: 2 }, { id: 'description', type: 'paragraph', - content: 'This section contains all administration and management tools for your workspace.' + content: 'administration.intro.description' }, { id: 'features', type: 'heading', - content: 'Available Tools', + content: 'administration.features.title', level: 3 }, { id: 'features-list', type: 'list', - content: 'Management tools include:', + content: 'administration.features.description', items: [ - 'File Management - Upload and organize documents', - 'User Management - Manage team members and permissions', - 'System Settings - Configure workspace settings', - 'Data Management - Handle data imports and exports' + 'administration.features.file_management', + 'administration.features.user_management', + 'administration.features.system_settings', + 'administration.features.data_management' ] } ], @@ -63,6 +63,6 @@ export const verwaltungPageData: GenericPageData = { // Lifecycle hooks onActivate: async () => { - if (import.meta.env.DEV) console.log('Verwaltung activated'); + if (import.meta.env.DEV) console.log('Administration activated'); } }; diff --git a/src/core/PageManager/data/pages/example-page.ts b/src/core/PageManager/data/pages/example-page.ts deleted file mode 100644 index 4c3a73f..0000000 --- a/src/core/PageManager/data/pages/example-page.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { GenericPageData } from '../../pageInterface'; -import { FaCog, FaPlus, FaEdit, FaTrash, FaDownload } from 'react-icons/fa'; -import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; - -// Example main page with subpages -export const examplePageData: GenericPageData = { - id: 'example-main', - path: 'example', - name: 'Example Page', - description: 'An example page showing the generic page system capabilities', - - // Visual - icon: FaCog, - title: 'Example Page', - subtitle: 'This demonstrates the generic page system', - - // Header buttons - headerButtons: [ - { - id: 'add-item', - label: 'Add Item', - variant: 'primary', - size: 'md', - icon: FaPlus, - onClick: () => { - console.log('Adding new item...'); - // Add your logic here - } - }, - { - id: 'edit-mode', - label: 'Edit Mode', - variant: 'secondary', - size: 'md', - icon: FaEdit, - onClick: () => { - console.log('Toggling edit mode...'); - // Add your logic here - } - }, - { - id: 'export-data', - label: 'Export Data', - variant: 'success', - size: 'md', - icon: FaDownload, - onClick: () => { - console.log('Exporting data...'); - // Add your logic here - } - }, - { - id: 'delete-all', - label: 'Delete All', - variant: 'danger', - size: 'md', - icon: FaTrash, - onClick: () => { - console.log('Deleting all items...'); - // Add your logic here - }, - privilegeChecker: privilegeCheckers.adminRole // Only admins can delete all - } - ], - - // Content sections - content: [ - { - id: 'intro', - type: 'heading', - content: 'Welcome to the Example Page', - level: 2 - }, - { - id: 'description', - type: 'paragraph', - content: 'This page demonstrates how to create rich, interactive pages using only data configuration. No React components needed!' - }, - { - id: 'features', - type: 'heading', - content: 'Features Demonstrated', - level: 3 - }, - { - id: 'features-list', - type: 'list', - content: 'This page shows:', - items: [ - 'Dynamic header with multiple action buttons', - 'Different button variants and sizes', - 'Privilege-based button visibility', - 'Rich content sections with headings and lists', - 'Code blocks and formatted text', - 'Subpage support for hierarchical navigation' - ] - }, - { - id: 'code-example', - type: 'heading', - content: 'Code Example', - level: 3 - }, - { - id: 'code-block', - type: 'code', - content: `// Example of how to create a new page -export const myPageData: GenericPageData = { - id: 'my-page', - path: 'my-page', - name: 'My Page', - title: 'My Custom Page', - content: [ - { - id: 'intro', - type: 'heading', - content: 'Hello World!', - level: 2 - } - ] -};`, - language: 'typescript' - }, - { - id: 'divider', - type: 'divider' - }, - { - id: 'subpages', - type: 'heading', - content: 'Subpages', - level: 3 - }, - { - id: 'subpages-text', - type: 'paragraph', - content: 'This page has subpages that demonstrate hierarchical navigation. Check the sidebar to see the submenu!' - } - ], - - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - - // Subpage support - hasSubpages: true, - subpagePrivilegeChecker: privilegeCheckers.viewerRole, - - // Page behavior - persistent: false, - preload: true, - moduleEnabled: true, - - // Sidebar - order: 10, - showInSidebar: true, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Example page activated'); - }, - onLoad: async () => { - if (import.meta.env.DEV) console.log('Example page loaded'); - } -}; - -// Example subpage 1 -export const exampleSubpage1Data: GenericPageData = { - id: 'example-sub1', - path: 'example/subpage1', - name: 'Subpage 1', - description: 'First subpage example', - - // Parent page - parentPath: 'example', - - // Visual - title: 'Subpage 1', - subtitle: 'This is the first subpage', - - // Content - content: [ - { - id: 'intro', - type: 'heading', - content: 'Subpage 1 Content', - level: 2 - }, - { - id: 'description', - type: 'paragraph', - content: 'This is content for the first subpage. Subpages can have their own content, buttons, and functionality.' - } - ], - - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - - // Page behavior - persistent: false, - preload: false, - moduleEnabled: true, - - // Sidebar - will be shown as subpage under Example - showInSidebar: false, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Example subpage 1 activated'); - } -}; - -// Example subpage 2 -export const exampleSubpage2Data: GenericPageData = { - id: 'example-sub2', - path: 'example/subpage2', - name: 'Subpage 2', - description: 'Second subpage example', - - // Parent page - parentPath: 'example', - - // Visual - title: 'Subpage 2', - subtitle: 'This is the second subpage', - - // Content - content: [ - { - id: 'intro', - type: 'heading', - content: 'Subpage 2 Content', - level: 2 - }, - { - id: 'description', - type: 'paragraph', - content: 'This is content for the second subpage. You can create as many subpages as needed!' - } - ], - - // Privilege system - privilegeChecker: privilegeCheckers.viewerRole, - - // Page behavior - persistent: false, - preload: false, - moduleEnabled: true, - - // Sidebar - will be shown as subpage under Example - showInSidebar: false, - - // Lifecycle hooks - onActivate: async () => { - if (import.meta.env.DEV) console.log('Example subpage 2 activated'); - } -}; diff --git a/src/core/PageManager/data/pages/dateien.ts b/src/core/PageManager/data/pages/files.ts similarity index 67% rename from src/core/PageManager/data/pages/dateien.ts rename to src/core/PageManager/data/pages/files.ts index c60825f..9561d21 100644 --- a/src/core/PageManager/data/pages/dateien.ts +++ b/src/core/PageManager/data/pages/files.ts @@ -1,5 +1,5 @@ import { useCallback } from 'react'; -import { GenericPageData, LanguageText } from '../../pageInterface'; +import { GenericPageData } from '../../pageInterface'; import { FaRegFileAlt, FaUpload } from 'react-icons/fa'; import { privilegeCheckers } from '../../../../utils/privilegeCheckers'; import { useUserFiles, useFileOperations } from '../../../../hooks/useFiles'; @@ -11,13 +11,15 @@ const createFilesHook = () => { const { handleFileDownload, handleFileDelete, + handleFileDeleteMultiple, handleFilePreview, handleFileUpdate, handleFileUpload: hookHandleFileUpload, downloadingFiles, deletingFiles, previewingFiles, - editingFiles + editingFiles, + MessageOverlayComponent } = useFileOperations(); // Upload function that can be called from header buttons @@ -40,6 +42,33 @@ const createFilesHook = () => { } }, [hookHandleFileUpload, refetch]); // Only recreate if dependencies change + // Handle multiple file deletion for FormGenerator + const handleDeleteMultiple = useCallback(async (selectedFiles: any[]) => { + const fileIds = selectedFiles.map(file => file.id); + const success = await handleFileDeleteMultiple(fileIds, (deletedIds) => { + // Optimistically remove files from UI + deletedIds.forEach(fileId => { + removeFileOptimistically(fileId); + }); + }); + + if (success) { + // Refetch to sync with backend + refetch(); + } + }, [handleFileDeleteMultiple, removeFileOptimistically, refetch]); + + // Handle single file deletion for FormGenerator + const handleDeleteSingle = useCallback(async (file: any) => { + const success = await handleFileDelete(file.id, () => { + removeFileOptimistically(file.id); + }); + + if (success) { + refetch(); + } + }, [handleFileDelete, removeFileOptimistically, refetch]); + return { data: files, loading, @@ -49,14 +78,20 @@ const createFilesHook = () => { // Operations handleDownload: handleFileDownload, handleDelete: handleFileDelete, + handleDeleteMultiple: handleFileDeleteMultiple, handlePreview: handleFilePreview, handleUpload: handleFileUpload, handleFileUpdate: handleFileUpdate, + // FormGenerator specific handlers + onDelete: handleDeleteSingle, + onDeleteMultiple: handleDeleteMultiple, // Loading states downloadingFiles, deletingFiles, previewingFiles, - editingFiles + editingFiles, + // Message overlay component + MessageOverlayComponent }; }; }; @@ -65,11 +100,7 @@ const createFilesHook = () => { const filesColumns = [ { key: 'file_name', - label: { - de: 'Dateiname', - en: 'Filename', - fr: 'Nom de fichier' - }, + label: 'files.column.filename', type: 'string', width: 300, minWidth: 200, @@ -80,11 +111,7 @@ const filesColumns = [ }, { key: 'mime_type', - label: { - de: 'Dateityp', - en: 'File Type', - fr: 'Type de fichier' - }, + label: 'files.column.mimetype', type: 'string', width: 200, minWidth: 150, @@ -95,11 +122,7 @@ const filesColumns = [ }, { key: 'size', - label: { - de: 'Dateigröße', - en: 'File Size', - fr: 'Taille du fichier' - }, + label: 'files.column.filesize', type: 'number', width: 140, minWidth: 120, @@ -109,11 +132,7 @@ const filesColumns = [ }, { key: 'created_at', - label: { - de: 'Erstellungsdatum', - en: 'Creation Date', - fr: 'Date de création' - }, + label: 'files.column.creationdate', type: 'date', width: 200, minWidth: 180, @@ -123,41 +142,25 @@ const filesColumns = [ } ]; -export const dateienPageData: GenericPageData = { - id: 'verwaltung-dateien', - path: 'verwaltung/dateien', - name: 'Dateien', - description: { - de: 'Dateiverwaltung und -organisation', - en: 'File management and organization', - fr: 'Gestion et organisation des fichiers' - }, +export const filesPageData: GenericPageData = { + id: 'administration-files', + path: 'administration/files', + name: 'files.title', + description: 'files.title', // Parent page - parentPath: 'verwaltung', + parentPath: 'administration', // Visual icon: FaRegFileAlt, - title: { - de: 'Dateien', - en: 'Files', - fr: 'Fichiers' - }, - subtitle: { - de: 'Verwalten Sie Ihre Dateien und Dokumente', - en: 'Manage your files and documents', - fr: 'Gérez vos fichiers et documents' - }, + title: 'files.title', + subtitle: 'files.title', // Header buttons headerButtons: [ { id: 'upload-file', - label: { - de: 'Datei hochladen', - en: 'Upload File', - fr: 'Télécharger un fichier' - }, + label: 'files.upload_button', icon: FaUpload, variant: 'primary', // onClick will be handled by PageRenderer to render UploadButton @@ -176,11 +179,7 @@ export const dateienPageData: GenericPageData = { actionButtons: [ { type: 'view', - title: { - de: 'Datei vorschauen', - en: 'Preview file', - fr: 'Aperçu du fichier' - }, + title: 'files.action.preview', idField: 'id', nameField: 'file_name', typeField: 'mime_type', @@ -189,11 +188,7 @@ export const dateienPageData: GenericPageData = { }, { type: 'edit', - title: { - de: 'Datei bearbeiten', - en: 'Edit file', - fr: 'Modifier le fichier' - }, + title: 'files.action.edit', idField: 'id', nameField: 'file_name', typeField: 'mime_type', @@ -207,22 +202,14 @@ export const dateienPageData: GenericPageData = { }, { type: 'download', - title: { - de: 'Datei herunterladen', - en: 'Download file', - fr: 'Télécharger le fichier' - }, + title: 'files.action.download', idField: 'id', operationName: 'handleDownload', loadingStateName: 'downloadingFiles' }, { type: 'delete', - title: { - de: 'Datei löschen', - en: 'Delete file', - fr: 'Supprimer le fichier' - }, + title: 'files.action.delete', idField: 'id', operationName: 'handleDelete', loadingStateName: 'deletingFiles' @@ -234,7 +221,7 @@ export const dateienPageData: GenericPageData = { resizable: true, pagination: true, pageSize: 10, - className: 'dateien-table' + className: 'files-table' } } ], @@ -247,17 +234,25 @@ export const dateienPageData: GenericPageData = { preload: false, moduleEnabled: true, - // Sidebar - will be shown as subpage under Verwaltung + // Sidebar - will be shown as subpage under Administration showInSidebar: false, + // Drag and drop configuration + dragDropConfig: { + enabled: true, + accept: '*/*', // Accept all file types + multiple: true, // Allow multiple files + // overlayText and overlaySubtext will use default translations from DragDropOverlay + }, + // Lifecycle hooks onActivate: async () => { - if (import.meta.env.DEV) console.log('Dateien activated'); + if (import.meta.env.DEV) console.log('Files activated'); }, onLoad: async () => { - if (import.meta.env.DEV) console.log('Dateien loaded - can initialize file lists here'); + if (import.meta.env.DEV) console.log('Files loaded - can initialize file lists here'); }, onUnload: async () => { - if (import.meta.env.DEV) console.log('Dateien unloaded - cleanup file references'); + if (import.meta.env.DEV) console.log('Files unloaded - cleanup file references'); } }; diff --git a/src/core/PageManager/data/pages/index.ts b/src/core/PageManager/data/pages/index.ts index 4409d52..555d6fb 100644 --- a/src/core/PageManager/data/pages/index.ts +++ b/src/core/PageManager/data/pages/index.ts @@ -1,26 +1,22 @@ // Export all page data export { dashboardPageData } from './dashboard'; -export { dateienPageData } from './dateien'; +export { filesPageData } from './files'; export { teamBereichPageData } from './team-bereich'; -export { examplePageData, exampleSubpage1Data, exampleSubpage2Data } from './example-page'; -export { verwaltungPageData } from './verwaltung'; +export { administrationPageData } from './administration'; // Import all page data import { dashboardPageData } from './dashboard'; -import { dateienPageData } from './dateien'; +import { administrationPageData } from './administration'; +import { filesPageData } from './files'; import { teamBereichPageData } from './team-bereich'; -import { examplePageData, exampleSubpage1Data, exampleSubpage2Data } from './example-page'; -import { verwaltungPageData } from './verwaltung'; // Array of all page data export const allPageData = [ dashboardPageData, - verwaltungPageData, - dateienPageData, + administrationPageData, + filesPageData, teamBereichPageData, - examplePageData, - exampleSubpage1Data, - exampleSubpage2Data + ]; // Helper function to get page data by path diff --git a/src/core/PageManager/pageInterface.ts b/src/core/PageManager/pageInterface.ts index a654ba4..eb1add7 100644 --- a/src/core/PageManager/pageInterface.ts +++ b/src/core/PageManager/pageInterface.ts @@ -1,5 +1,6 @@ import React from 'react'; import { IconType } from 'react-icons'; +import { DragDropConfig } from '../../components/ui/DragDropOverlay/DragDropOverlay'; // Generic privilege checker function type export type PrivilegeChecker = () => boolean | Promise; @@ -44,6 +45,11 @@ export interface GenericDataHook { handleDownload?: (fileId: string, fileName: string) => Promise; // For file download functionality handleDelete?: (fileId: string, onOptimisticDelete?: () => void) => Promise; // For file delete functionality handlePreview?: (fileId: string, fileName: string, mimeType?: string) => Promise; // For file preview functionality + // FormGenerator specific handlers + onDelete?: (row: any) => Promise; // For single item deletion + onDeleteMultiple?: (rows: any[]) => Promise; // For multiple item deletion + // Message overlay component + MessageOverlayComponent?: () => React.ReactElement; } // Action button configuration @@ -84,10 +90,18 @@ export interface LanguageText { } // Utility function to resolve language text -export const resolveLanguageText = (text: string | LanguageText | undefined, language: 'de' | 'en' | 'fr' = 'de'): string => { +export const resolveLanguageText = (text: string | LanguageText | undefined, t?: (key: string, fallback?: string) => string): string => { if (!text) return ''; - if (typeof text === 'string') return text; - return text[language] || text.de || ''; + if (typeof text === 'string') { + // Always use the translation function for strings (language keys) + if (t) { + return t(text); + } + return text; + } + // For LanguageText objects, we should convert them to language keys + // For now, fallback to the first available language + return text.de || text.en || text.fr || ''; }; // Generic page data interface @@ -135,6 +149,9 @@ export interface GenericPageData { // Custom component override (optional) customComponent?: React.ComponentType; + + // Drag and drop configuration + dragDropConfig?: DragDropConfig; } // Page data file structure @@ -159,6 +176,7 @@ export interface SidebarSubmenuItemData { id: string; name: string; link: string; + icon?: IconType; } // Page instance for PageManager diff --git a/src/hooks/useFiles.ts b/src/hooks/useFiles.ts index 023383a..bcd93e4 100644 --- a/src/hooks/useFiles.ts +++ b/src/hooks/useFiles.ts @@ -1,5 +1,8 @@ -import { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import api from '../api'; +import { MessageOverlay } from '../components/ui'; +import type { MessageMode } from '../components/ui'; +import { useLanguage } from '../contexts/LanguageContext'; // File interfaces - exactly matching backend FileItem model export interface FileInfo { @@ -29,51 +32,18 @@ export function useUserFiles() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - // Log hook state for debugging - console.log('🔄 useUserFiles hook', { filesCount: files.length, loading, isRefetching, hasError: !!error }); const fetchFiles = useCallback(async () => { try { setLoading(true); setError(null); - console.log('🔍 Fetching files from API...'); - console.log('🔍 Current auth authority:', localStorage.getItem('auth_authority')); - console.log('🔍 Has JWT token:', !!localStorage.getItem('auth_data')); - - console.log('🚀 Making API request to /api/files/list...'); // Debug: Check what auth headers are being sent const authData = localStorage.getItem('auth_data'); if (authData) { try { - const tokenData = JSON.parse(authData); - console.log('🔍 JWT token being sent:', { - hasTokenAccess: !!tokenData.tokenAccess, - tokenAccessStart: tokenData.tokenAccess?.substring(0, 50) + '...', - tokenType: tokenData.tokenType - }); - - // Decode JWT payload to see what's inside - if (tokenData.tokenAccess) { - try { - const jwtParts = tokenData.tokenAccess.split('.'); - if (jwtParts.length === 3) { - const payload = JSON.parse(atob(jwtParts[1])); - console.log('🔍 JWT payload contents:', { - sub: payload.sub, - userId: payload.userId, - authenticationAuthority: payload.authenticationAuthority, - exp: payload.exp, - expiredAt: new Date(payload.exp * 1000).toISOString(), - isExpired: payload.exp < Date.now() / 1000, - allKeys: Object.keys(payload) - }); - } - } catch (decodeError) { - console.error('❌ Failed to decode JWT payload:', decodeError); - } - } + JSON.parse(authData); } catch (e) { console.error('❌ Failed to parse auth_data:', e); } @@ -81,14 +51,10 @@ export function useUserFiles() { const response = await api.get('/api/files/list'); const data = response.data; - - console.log('✅ API request completed successfully!'); - - console.log('📥 Raw API response:', data); + // Ensure data is an array, handle null/undefined responses const fileList = Array.isArray(data) ? data : []; - console.log(`📋 Processing ${fileList.length} files from API`); // Filter out invalid files and map API response to our frontend model const validFiles = fileList.filter((apiFile: any): boolean => { @@ -114,30 +80,9 @@ export function useUserFiles() { apiFile.creationDate > 0 ); - if (!isValid) { - console.warn('❌ Filtering out invalid file record:', { - id: apiFile?.id, - mandateId: apiFile?.mandateId, - fileName: apiFile?.fileName, - mimeType: apiFile?.mimeType, - fileHash: apiFile?.fileHash, - fileSize: apiFile?.fileSize, - creationDate: apiFile?.creationDate, - creationDateType: typeof apiFile?.creationDate, - fullObject: apiFile - }); - } else { - console.log('✅ Valid file:', { - id: apiFile.id, - fileName: apiFile.fileName, - creationDate: apiFile.creationDate - }); - } - return isValid; }); - console.log(`✨ Filtered to ${validFiles.length} valid files`); if (validFiles.length !== fileList.length) { console.warn(`⚠️ Filtered out ${fileList.length - validFiles.length} invalid files`); } @@ -191,7 +136,6 @@ export function useUserFiles() { }; }); - console.log(`✅ Successfully processed ${mappedFiles.length} files from API`); setFiles(mappedFiles); } catch (error: any) { console.error('❌ Error fetching files:', error); @@ -208,7 +152,6 @@ export function useUserFiles() { // Provide informative placeholder when CORS blocks the request if (error.message === 'Keine Antwort vom Server erhalten' || error.message === 'Network Error') { - console.log('📝 CORS blocking files API - providing informative placeholder'); const corsPlaceholderFile: UserFile = { id: 'cors-info', file_name: 'Files Service Temporarily Unavailable', @@ -253,12 +196,10 @@ export function useUserFiles() { }; useEffect(() => { - console.log('🔄 useUserFiles useEffect triggered - fetching files on mount'); fetchFiles(); }, [fetchFiles]); // Depend on fetchFiles which is memoized with useCallback const refetch = useCallback(async () => { - console.log('🔄 Refetching files...'); setIsRefetching(true); try { await fetchFiles(); @@ -290,14 +231,20 @@ export function useFileOperations() { const [uploadError, setUploadError] = useState(null); const [previewingFiles, setPreviewingFiles] = useState>(new Set()); const [previewError, setPreviewError] = useState(null); + + // Warning state + const [showWarning, setShowWarning] = useState(false); + const [warningData, setWarningData] = useState<{ header: string; message: string; mode: MessageMode } | null>(null); + + // Language context + const { t } = useLanguage(); const handleFileDownload = async (fileId: string, fileName: string) => { setDownloadError(null); setDownloadingFiles(prev => new Set(prev).add(fileId)); try { - console.log(`📥 Starting download for file: ${fileName} (ID: ${fileId})`); - + // Try to get the file download const response = await api.get(`/api/files/${fileId}/download`, { responseType: 'blob', @@ -306,8 +253,7 @@ export function useFileOperations() { } }); const blob = response.data; - console.log(`✅ Download successful for: ${fileName}`, { size: blob.size, type: blob.type }); - + // Create a download link and trigger the download const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); @@ -350,12 +296,9 @@ export function useFileOperations() { } try { - console.log(`🗑️ Starting delete for file ID: ${fileId}`); await api.delete(`/api/files/${fileId}`); - - console.log(`✅ Delete successful for file ID: ${fileId}`); - + // Add a small delay to ensure backend has time to process await new Promise(resolve => setTimeout(resolve, 300)); return true; @@ -383,6 +326,56 @@ export function useFileOperations() { } }; + const handleFileDeleteMultiple = async (fileIds: string[], onOptimisticDelete?: (fileIds: string[]) => void) => { + setDeleteError(null); + setDeletingFiles(prev => { + const newSet = new Set(prev); + fileIds.forEach(id => newSet.add(id)); + return newSet; + }); + + // Optimistically remove from UI if callback provided + if (onOptimisticDelete) { + onOptimisticDelete(fileIds); + } + + try { + // Delete files one by one since there's no bulk delete endpoint + const deletePromises = fileIds.map(fileId => + api.delete(`/api/files/${fileId}`).catch(error => { + console.error(`❌ Delete failed for file ID ${fileId}:`, error); + // Return the error for this specific file + return { error, fileId }; + }) + ); + + const results = await Promise.all(deletePromises); + + // Check if any deletions failed + const failures = results.filter(result => result && 'error' in result); + + if (failures.length > 0) { + console.error(`❌ ${failures.length} out of ${fileIds.length} files failed to delete`); + // For now, we'll consider it successful if at least some files were deleted + // In a more robust implementation, you might want to handle partial failures differently + } + + // Add a small delay to ensure backend has time to process + await new Promise(resolve => setTimeout(resolve, 300)); + return true; + } catch (error: any) { + console.error(`❌ Bulk delete failed:`, error); + setDeleteError(error.message || 'Bulk delete failed'); + return false; + } finally { + setDeletingFiles(prev => { + const newSet = new Set(prev); + fileIds.forEach(id => newSet.delete(id)); + return newSet; + }); + } + }; + /** * File upload function - backend bug has been fixed! * @@ -396,12 +389,6 @@ export function useFileOperations() { setUploadingFile(true); try { - console.log('📤 Starting file upload...', { - fileName: file.name, - fileSize: file.size, - fileType: file.type, - workflowId: workflowId - }); // Validate file before upload if (!file || !file.name || file.name.trim() === '') { @@ -421,22 +408,6 @@ export function useFileOperations() { // FormData is now correctly configured for backend - console.log('📋 FormData prepared:', { - hasFile: formData.has('file'), - hasWorkflowId: formData.has('workflowId'), - workflowId: workflowId - }); - - // Log the actual file object being sent - console.log('📁 File object details:', { - name: file.name, - size: file.size, - type: file.type, - lastModified: file.lastModified, - constructor: file.constructor.name - }); - - console.log('🚀 Sending upload request...'); const response = await api.post('/api/files/upload', formData, { headers: { @@ -446,6 +417,46 @@ export function useFileOperations() { const fileData = response.data; console.log('✅ Upload successful:', fileData); + console.log('📤 Upload response details:', { + status: response.status, + statusText: response.statusText, + headers: response.headers, + data: response.data, + config: { + url: response.config.url, + method: response.config.method, + headers: response.config.headers + } + }); + + // Check if the response indicates a duplicate file + if (fileData && fileData.isDuplicate && fileData.message) { + const fileName = fileData.originalFileName || file.name; + const messageTemplate = t('warning.duplicate_file.message'); + const message = messageTemplate.replace('{fileName}', fileName); + + // Close any existing warning first + if (showWarning) { + setShowWarning(false); + // Wait a moment before showing the new warning + setTimeout(() => { + setWarningData({ + header: t('warning.duplicate_file.title'), + message: message, + mode: 'warning' + }); + setShowWarning(true); + }, 600); + } else { + setWarningData({ + header: t('warning.duplicate_file.title'), + message: message, + mode: 'warning' + }); + setShowWarning(true); + } + } + return { success: true, fileData }; } catch (error: any) { console.error('❌ Upload failed:', { @@ -482,12 +493,7 @@ export function useFileOperations() { setEditingFiles(prev => new Set(prev).add(fileId)); try { - console.log(`✏️ Starting update for file ID: ${fileId}`, { - fileId, - updateData, - url: `/api/files/${fileId}`, - method: 'put' - }); + // Use PUT request with complete file object // Always use current timestamp for creationDate to avoid validation issues @@ -504,16 +510,6 @@ export function useFileOperations() { creationDate: Math.floor(creationDate) // Ensure it's an integer }; - console.log('🔍 Sending complete file object with PUT:', { - completeFileObject, - originalFileData, - updateData, - creationDateType: typeof creationDate, - creationDateValue: creationDate, - creationDateFormatted: new Date(creationDate * 1000).toISOString(), - currentTime: new Date().toISOString(), - currentTimestamp: Math.floor(Date.now() / 1000) - }); const response = await api.put(`/api/files/${fileId}`, completeFileObject, { headers: { @@ -522,7 +518,6 @@ export function useFileOperations() { }); const updatedFile = response.data; - console.log(`✅ Update successful for file ID: ${fileId}`, updatedFile); return { success: true, fileData: updatedFile }; } catch (error: any) { console.error(`❌ Update failed for file ID ${fileId}:`, { @@ -565,11 +560,9 @@ export function useFileOperations() { setPreviewingFiles(prev => new Set(prev).add(fileId)); try { - console.log(`👁️ Starting preview for file: ${fileName} (ID: ${fileId})`, { mimeType }); // For PDF files, try JSON response first (API returns base64-encoded PDF) if (mimeType === 'application/pdf') { - console.log('📄 PDF file detected, trying JSON response with base64 content'); try { const response = await api.get(`/api/files/${fileId}/preview`, { @@ -580,56 +573,31 @@ export function useFileOperations() { }); const jsonResponse = response.data; - console.log('📄 PDF JSON response received:', { - hasContent: 'content' in jsonResponse, - hasMimeType: 'mimeType' in jsonResponse, - contentLength: jsonResponse.content?.length, - mimeType: jsonResponse.mimeType, - contentType: typeof jsonResponse.content, - contentStartsWith: jsonResponse.content?.substring(0, 10), - fullResponse: jsonResponse - }); + // Check if response has base64-encoded PDF content if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) { let content = jsonResponse.content; - const responseMimeType = jsonResponse.mimeType || 'application/pdf'; // The content field contains base64-encoded JSON, so decode it first if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) { - console.log('📄 Content appears to be base64-encoded, decoding first...'); try { const decodedJsonString = atob(content); - console.log('📄 Decoded JSON string:', { - length: decodedJsonString.length, - startsWith: decodedJsonString.substring(0, 20), - isJson: decodedJsonString.startsWith('{') - }); // Parse the decoded JSON string const nestedJson = JSON.parse(decodedJsonString); - console.log('📄 Parsed nested JSON:', { - hasContent: 'content' in nestedJson, - hasDocumentCount: 'documentCount' in nestedJson, - keys: Object.keys(nestedJson) - }); if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) { const innerContent = nestedJson.content; const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent); - console.log('📄 Extracted inner content:', { - innerContentLength: innerContent?.length, - innerContentPreview: innerContent?.substring(0, 100) + '...', - isBase64: isBase64 - }); + if (isBase64) { // It's base64-encoded PDF content content = innerContent; } else { // It's plain text content, not a PDF - console.log('📄 Inner content is plain text, not PDF. This appears to be a text file with PDF extension.'); // Return the text content for the FilePreview to handle as text return { success: true, @@ -646,38 +614,20 @@ export function useFileOperations() { } } - console.log('📄 Processing base64 PDF content:', { - contentLength: content?.length, - mimeType: responseMimeType, - contentPreview: content?.substring(0, 100) + '...', - firstChars: content?.substring(0, 20), - lastChars: content?.substring(content.length - 20), - isBase64: /^[A-Za-z0-9+/=]+$/.test(content) - }); + // Decode base64 content let decodedContent; try { decodedContent = atob(content); - console.log('📄 Base64 decode successful:', { - originalLength: content.length, - decodedLength: decodedContent.length, - firstBytes: decodedContent.substring(0, 10), - firstBytesHex: Array.from(decodedContent.substring(0, 10)).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' ') - }); + // Verify it's actually a PDF const isPDF = decodedContent.startsWith('%PDF'); - console.log('📄 PDF header verification:', { - isPDF: isPDF, - firstBytes: decodedContent.substring(0, 4), - firstBytesHex: Array.from(decodedContent.substring(0, 4)).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join(' '), - first20Chars: decodedContent.substring(0, 20) - }); + if (!isPDF) { console.warn('⚠️ Decoded content does not appear to be a valid PDF'); - console.log('📄 Full decoded content preview:', decodedContent.substring(0, 200)); } } catch (decodeError) { @@ -695,21 +645,14 @@ export function useFileOperations() { const blob = new Blob([uint8Array], { type: 'application/pdf' }); const url = window.URL.createObjectURL(blob); - console.log('🔗 Created PDF blob URL from base64:', url, { - blobSize: blob.size, - blobType: blob.type, - url: url, - uint8ArrayLength: uint8Array.length, - firstBytes: Array.from(uint8Array.slice(0, 4)).map(b => String.fromCharCode(b)).join(''), - firstBytesHex: Array.from(uint8Array.slice(0, 4)).map(b => b.toString(16).padStart(2, '0')).join(' ') - }); + return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent }; } else { throw new Error('No content field in PDF response'); } } catch (jsonError) { - console.log('📄 JSON PDF response failed, trying blob response...', jsonError); + // Fallback to blob response const response = await api.get(`/api/files/${fileId}/preview`, { @@ -720,11 +663,7 @@ export function useFileOperations() { }); const previewData = response.data; - console.log(`✅ PDF blob preview successful for: ${fileName}`, { - size: previewData.size, - type: previewData.type, - expectedType: 'application/pdf' - }); + const url = window.URL.createObjectURL(previewData); @@ -734,7 +673,7 @@ export function useFileOperations() { // For image files, try JSON response first (API returns base64-encoded images) if (mimeType?.startsWith('image/')) { - console.log('🖼️ Image file detected, trying JSON response with base64 content'); + try { const response = await api.get(`/api/files/${fileId}/preview`, { @@ -745,12 +684,7 @@ export function useFileOperations() { }); const jsonResponse = response.data; - console.log('🖼️ Image JSON response received:', { - hasContent: 'content' in jsonResponse, - hasMimeType: 'mimeType' in jsonResponse, - contentLength: jsonResponse.content?.length, - mimeType: jsonResponse.mimeType - }); + // Check if response has base64-encoded image content if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) { @@ -759,34 +693,22 @@ export function useFileOperations() { // The content field contains base64-encoded data, decode it first if (typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) { - console.log('🖼️ Content appears to be base64-encoded, decoding first...'); + try { const decodedString = atob(content); - console.log('🖼️ Decoded string:', { - length: decodedString.length, - startsWith: decodedString.substring(0, 20), - isJson: decodedString.startsWith('{'), - isImage: decodedString.startsWith('\x89PNG') || decodedString.startsWith('\xFF\xD8\xFF') - }); + // Check if it's JSON (nested structure) or direct image data if (decodedString.startsWith('{')) { // It's JSON, parse it const nestedJson = JSON.parse(decodedString); - console.log('🖼️ Parsed nested JSON:', { - hasContent: 'content' in nestedJson, - keys: Object.keys(nestedJson) - }); + if (nestedJson && typeof nestedJson === 'object' && 'content' in nestedJson) { const innerContent = nestedJson.content; const isBase64 = /^[A-Za-z0-9+/=]+$/.test(innerContent); - console.log('🖼️ Extracted inner content:', { - innerContentLength: innerContent?.length, - innerContentPreview: innerContent?.substring(0, 100) + '...', - isBase64: isBase64 - }); + if (isBase64) { // It's base64-encoded image content @@ -797,7 +719,6 @@ export function useFileOperations() { } } else if (decodedString.startsWith('\x89PNG') || decodedString.startsWith('\xFF\xD8\xFF') || decodedString.startsWith('GIF8') || decodedString.startsWith('RIFF')) { // It's direct image data, use it as is - console.log('🖼️ Direct image data detected, using as is'); content = btoa(decodedString); // Re-encode as base64 for processing } else { throw new Error('Decoded content is neither JSON nor image data'); @@ -808,22 +729,13 @@ export function useFileOperations() { } } - console.log('🖼️ Processing base64 image content:', { - contentLength: content?.length, - mimeType: responseMimeType, - contentPreview: content?.substring(0, 100) + '...', - isBase64: /^[A-Za-z0-9+/=]+$/.test(content) - }); + // Decode base64 content let decodedContent; try { decodedContent = atob(content); - console.log('🖼️ Base64 decode successful:', { - originalLength: content.length, - decodedLength: decodedContent.length, - firstBytes: decodedContent.substring(0, 10) - }); + // Verify it's actually an image by checking for common image headers const isJPEG = decodedContent.startsWith('\xFF\xD8\xFF'); @@ -831,13 +743,7 @@ export function useFileOperations() { const isGIF = decodedContent.startsWith('GIF8'); const isWebP = decodedContent.startsWith('RIFF') && decodedContent.includes('WEBP'); - console.log('🖼️ Image header verification:', { - isJPEG: isJPEG, - isPNG: isPNG, - isGIF: isGIF, - isWebP: isWebP, - firstBytes: decodedContent.substring(0, 4) - }); + if (!isJPEG && !isPNG && !isGIF && !isWebP) { console.warn('⚠️ Decoded content does not appear to be a valid image'); @@ -858,20 +764,14 @@ export function useFileOperations() { const blob = new Blob([uint8Array], { type: responseMimeType }); const url = window.URL.createObjectURL(blob); - console.log('🔗 Created image blob URL from base64:', url, { - blobSize: blob.size, - blobType: blob.type, - url: url, - uint8ArrayLength: uint8Array.length - }); + return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent }; } else { throw new Error('No content field in image response'); } } catch (jsonError) { - console.log('🖼️ JSON image response failed, trying blob response...', jsonError); - + // Fallback to blob response const response = await api.get(`/api/files/${fileId}/preview`, { responseType: 'blob', @@ -880,12 +780,7 @@ export function useFileOperations() { } }); const previewData = response.data; - - console.log(`✅ Image blob preview successful for: ${fileName}`, { - size: previewData.size, - type: previewData.type, - expectedType: mimeType - }); + const url = window.URL.createObjectURL(previewData); @@ -902,36 +797,23 @@ export function useFileOperations() { } }); const jsonResponse = response.data; - - console.log(`✅ JSON preview successful for: ${fileName}`, jsonResponse); - + // Check if response has content field (structured response) if (jsonResponse && typeof jsonResponse === 'object' && 'content' in jsonResponse) { const content = jsonResponse.content; const mimeType = jsonResponse.mimeType || 'text/plain'; - console.log('📄 Structured JSON response detected:', { - hasContent: !!content, - mimeType: mimeType, - contentLength: content?.length, - contentPreview: content?.substring(0, 100) + '...' - }); + // Check if content is base64 encoded (common pattern) let decodedContent = content; try { // Try to decode as base64 if it looks like base64 if (content && typeof content === 'string' && /^[A-Za-z0-9+/=]+$/.test(content)) { - console.log('📄 Content appears to be base64 encoded, attempting decode...'); decodedContent = atob(content); - console.log('📄 Base64 decode successful:', { - originalLength: content.length, - decodedLength: decodedContent.length, - decodedPreview: decodedContent.substring(0, 200) + '...' - }); + } } catch (decodeError) { - console.log('📄 Base64 decode failed, using original content:', decodeError); decodedContent = content; } @@ -939,36 +821,22 @@ export function useFileOperations() { const blob = new Blob([decodedContent], { type: mimeType }); const url = window.URL.createObjectURL(blob); - console.log('🔗 Created blob URL:', url); return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent }; } else if (jsonResponse && typeof jsonResponse === 'object' && 'result' in jsonResponse) { // Handle base64 encoded content in 'result' field - console.log('📄 Base64 encoded content detected in result field'); try { // Decode base64 content const decodedContent = atob(jsonResponse.result); const mimeType = jsonResponse.mimeType || 'application/json'; - console.log('📄 Decoded content:', { - length: decodedContent.length, - preview: decodedContent.substring(0, 200) + '...', - mimeType: mimeType, - originalResult: jsonResponse.result.substring(0, 100) + '...', - decodedFirstChars: decodedContent.substring(0, 50) - }); // Create a blob from the decoded content const blob = new Blob([decodedContent], { type: mimeType }); const url = window.URL.createObjectURL(blob); - console.log('🔗 Created blob URL for decoded content:', url); - console.log('🔍 Blob details:', { - size: blob.size, - type: blob.type, - url: url - }); + return { success: true, previewUrl: url, blob: blob, isJsonContent: true, decodedContent: decodedContent }; } catch (decodeError) { @@ -980,7 +848,6 @@ export function useFileOperations() { return { success: true, previewUrl: url, blob: blob, isJsonContent: true }; } } else { - console.log('📄 Raw JSON response, treating as content'); // If it's not structured JSON, treat as raw content const blob = new Blob([JSON.stringify(jsonResponse, null, 2)], { type: 'application/json' }); const url = window.URL.createObjectURL(blob); @@ -988,7 +855,6 @@ export function useFileOperations() { return { success: true, previewUrl: url, blob: blob, isJsonContent: true }; } } catch (jsonError) { - console.log('JSON preview failed, trying blob response...', jsonError); // Fallback to blob response for binary files const response = await api.get(`/api/files/${fileId}/preview`, { @@ -999,7 +865,6 @@ export function useFileOperations() { }); const previewData = response.data; - console.log(`✅ Blob preview successful for: ${fileName}`, { size: previewData.size, type: previewData.type }); // Create a blob URL for preview const url = window.URL.createObjectURL(previewData); @@ -1029,6 +894,15 @@ export function useFileOperations() { } }; + // Function to close warning + const closeWarning = useCallback(() => { + setShowWarning(false); + // Delay clearing the data to allow exit animation to complete (matches CSS transition) + setTimeout(() => { + setWarningData(null); + }, 700); + }, []); + return { downloadingFiles, deletingFiles, @@ -1041,9 +915,20 @@ export function useFileOperations() { previewError, handleFileDownload, handleFileDelete, + handleFileDeleteMultiple, handleFileUpload, handleFileUpdate, handleFilePreview, - isLoading + isLoading, + // Message overlay component + MessageOverlayComponent: () => React.createElement(MessageOverlay, { + header: warningData?.header || '', + message: warningData?.message || '', + isVisible: showWarning, + mode: warningData?.mode || 'info', + onClose: closeWarning, + autoClose: true, + autoCloseDelay: 5000 + }) }; } \ No newline at end of file diff --git a/src/index.css b/src/index.css index 537d219..34d6aee 100644 --- a/src/index.css +++ b/src/index.css @@ -15,7 +15,4 @@ html, body { width: 100vw; margin: 0; padding: 0; -} - -/* Import global button styles */ -@import './styles/buttons.css'; \ No newline at end of file +} \ No newline at end of file diff --git a/src/locales/de.ts b/src/locales/de.ts index a95c1e9..dc70078 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -497,11 +497,10 @@ export default { 'users.add.create': 'Benutzer erstellen', 'users.delete.title': 'Benutzer löschen', 'users.delete.message': 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?', - 'users.delete.confirm': 'Löschen', + 'users.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?', 'users.delete.warning': 'Diese Aktion kann nicht rückgängig gemacht werden.', 'users.action.edit': 'Bearbeiten', 'users.action.delete': 'Löschen', - 'users.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?', 'users.delete.confirmMultiple': 'Sind Sie sicher, dass Sie {count} Benutzer löschen möchten?', 'users.error.loading': 'Fehler beim Laden der Benutzer:', @@ -561,8 +560,6 @@ export default { 'speech.info.about_link': 'Mehr erfahren', 'speech.signup.button': 'Verbinden', - 'speech.signup.title': 'Mandat für Sprach Integration erstellen', - 'speech.signup.subtitle': 'Erstellen Sie Ihr Mandat für die Spitch.ai Integration', 'speech.signup.back': 'Zurück zur Sprach Integration', 'speech.signup.submit': 'Mandat erstellen', 'speech.signup.cancel': 'Abbrechen', @@ -665,4 +662,34 @@ export default { 'speech.settings.reset_success': 'Einstellungen wurden erfolgreich zurückgesetzt.', 'speech.settings.no_data': 'Keine Sprach-Integrations-Daten gefunden. Bitte melden Sie sich zuerst an, um auf die Einstellungen zuzugreifen.', 'speech.settings.sign_up_now': 'Jetzt anmelden', + + // Message Overlay Types + 'message.success.title': 'Erfolgreich', + 'message.success.upload': 'Datei erfolgreich hochgeladen!', + 'message.info.title': 'Information', + 'message.info.processing': 'Ihre Anfrage wird verarbeitet...', + 'message.error.title': 'Fehler', + 'message.error.upload_failed': 'Upload fehlgeschlagen. Bitte versuchen Sie es erneut.', + + // Warning Messages + 'warning.duplicate_file.title': 'Datei bereits vorhanden', + 'warning.duplicate_file.message': 'Die Datei "{fileName}" existiert bereits mit identischem Inhalt. Die vorhandene Datei wird wiederverwendet.', + + // Administration + 'administration.title': 'Verwaltung', + 'administration.description': 'Verwaltungs- und Management-Tools', + 'administration.subtitle': 'Verwaltungs- und Management-Tools', + 'administration.intro.description': 'Dieser Bereich enthält alle Verwaltungs- und Management-Tools für Ihren Arbeitsbereich.', + 'administration.features.title': 'Verfügbare Tools', + 'administration.features.description': 'Management-Tools umfassen:', + 'administration.features.file_management': 'Dateiverwaltung - Dokumente hochladen und organisieren', + 'administration.features.user_management': 'Benutzerverwaltung - Teammitglieder und Berechtigungen verwalten', + 'administration.features.system_settings': 'Systemeinstellungen - Arbeitsbereich-Einstellungen konfigurieren', + 'administration.features.data_management': 'Datenverwaltung - Datenimporte und -exporte verwalten', + + // Drag and Drop + 'dragdrop.overlay.default_text': 'Dateien hier ablegen', + 'dragdrop.overlay.default_subtext': 'Sie können auch auf den Upload-Button klicken', + 'dragdrop.overlay.processing': 'Dateien werden verarbeitet...', + 'dragdrop.overlay.error': 'Fehler beim Verarbeiten der Dateien', }; \ No newline at end of file diff --git a/src/locales/en.ts b/src/locales/en.ts index 0ac7899..51aa44a 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -497,11 +497,10 @@ export default { 'users.add.create': 'Create User', 'users.delete.title': 'Delete User', 'users.delete.message': 'Are you sure you want to delete this user?', - 'users.delete.confirm': 'Delete', + 'users.delete.confirm': 'Are you sure you want to delete "{name}"?', 'users.delete.warning': 'This action cannot be undone.', 'users.action.edit': 'Edit', 'users.action.delete': 'Delete', - 'users.delete.confirm': 'Are you sure you want to delete "{name}"?', 'users.delete.confirmMultiple': 'Are you sure you want to delete {count} users?', 'users.error.loading': 'Error loading users:', @@ -561,8 +560,6 @@ export default { 'speech.info.about_link': 'Learn more', 'speech.signup.button': 'Connect', - 'speech.signup.title': 'Create Mandate for Speech Integration', - 'speech.signup.subtitle': 'Create your mandate for Spitch.ai integration', 'speech.signup.back': 'Back to Speech Integration', 'speech.signup.submit': 'Create Mandate', 'speech.signup.cancel': 'Cancel', @@ -665,4 +662,34 @@ export default { 'speech.settings.reset_success': 'Settings have been reset successfully.', 'speech.settings.no_data': 'No speech integration data found. Please sign up first to access settings.', 'speech.settings.sign_up_now': 'Sign Up Now', + + // Message Overlay Types + 'message.success.title': 'Success', + 'message.success.upload': 'File uploaded successfully!', + 'message.info.title': 'Information', + 'message.info.processing': 'Processing your request...', + 'message.error.title': 'Error', + 'message.error.upload_failed': 'Upload failed. Please try again.', + + // Warning Messages + 'warning.duplicate_file.title': 'File Already Exists', + 'warning.duplicate_file.message': 'The file "{fileName}" already exists with identical content. The existing file will be reused.', + + // Administration + 'administration.title': 'Administration', + 'administration.description': 'Administration and management tools', + 'administration.subtitle': 'Administration and management tools', + 'administration.intro.description': 'This section contains all administration and management tools for your workspace.', + 'administration.features.title': 'Available Tools', + 'administration.features.description': 'Management tools include:', + 'administration.features.file_management': 'File Management - Upload and organize documents', + 'administration.features.user_management': 'User Management - Manage team members and permissions', + 'administration.features.system_settings': 'System Settings - Configure workspace settings', + 'administration.features.data_management': 'Data Management - Handle data imports and exports', + + // Drag and Drop + 'dragdrop.overlay.default_text': 'Drop files here', + 'dragdrop.overlay.default_subtext': 'You can also click the upload button', + 'dragdrop.overlay.processing': 'Processing files...', + 'dragdrop.overlay.error': 'Error processing files', }; \ No newline at end of file diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 5d278d1..fea35cb 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -497,11 +497,10 @@ export default { 'users.add.create': 'Créer l\'utilisateur', 'users.delete.title': 'Supprimer l\'utilisateur', 'users.delete.message': 'Êtes-vous sûr de vouloir supprimer cet utilisateur ?', - 'users.delete.confirm': 'Supprimer', + 'users.delete.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?', 'users.delete.warning': 'Cette action ne peut pas être annulée.', 'users.action.edit': 'Modifier', 'users.action.delete': 'Supprimer', - 'users.delete.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?', 'users.delete.confirmMultiple': 'Êtes-vous sûr de vouloir supprimer {count} utilisateurs ?', 'users.error.loading': 'Erreur lors du chargement des utilisateurs:', @@ -561,8 +560,6 @@ export default { 'speech.info.about_link': 'En savoir plus', 'speech.signup.button': 'Connecter', - 'speech.signup.title': 'Créer un Mandat pour l\'Intégration Vocale', - 'speech.signup.subtitle': 'Créez votre mandat pour l\'intégration Spitch.ai', 'speech.signup.back': 'Retour à l\'Intégration Vocale', 'speech.signup.submit': 'Créer le Mandat', 'speech.signup.cancel': 'Annuler', @@ -665,4 +662,34 @@ export default { 'speech.settings.reset_success': 'Les paramètres ont été réinitialisés avec succès.', 'speech.settings.no_data': 'Aucune donnée d\'intégration vocale trouvée. Veuillez d\'abord vous inscrire pour accéder aux paramètres.', 'speech.settings.sign_up_now': 'S\'inscrire Maintenant', + + // Message Overlay Types + 'message.success.title': 'Succès', + 'message.success.upload': 'Fichier téléchargé avec succès !', + 'message.info.title': 'Information', + 'message.info.processing': 'Traitement de votre demande...', + 'message.error.title': 'Erreur', + 'message.error.upload_failed': 'Échec du téléchargement. Veuillez réessayer.', + + // Warning Messages + 'warning.duplicate_file.title': 'Fichier Déjà Existant', + 'warning.duplicate_file.message': 'Le fichier "{fileName}" existe déjà avec un contenu identique. Le fichier existant sera réutilisé.', + + // Administration + 'administration.title': 'Administration', + 'administration.description': 'Outils d\'administration et de gestion', + 'administration.subtitle': 'Outils d\'administration et de gestion', + 'administration.intro.description': 'Cette section contient tous les outils d\'administration et de gestion pour votre espace de travail.', + 'administration.features.title': 'Outils Disponibles', + 'administration.features.description': 'Les outils de gestion incluent:', + 'administration.features.file_management': 'Gestion des Fichiers - Télécharger et organiser les documents', + 'administration.features.user_management': 'Gestion des Utilisateurs - Gérer les membres de l\'équipe et les permissions', + 'administration.features.system_settings': 'Paramètres Système - Configurer les paramètres de l\'espace de travail', + 'administration.features.data_management': 'Gestion des Données - Gérer les imports et exports de données', + + // Drag and Drop + 'dragdrop.overlay.default_text': 'Déposer les fichiers ici', + 'dragdrop.overlay.default_subtext': 'Vous pouvez aussi cliquer sur le bouton de téléchargement', + 'dragdrop.overlay.processing': 'Traitement des fichiers...', + 'dragdrop.overlay.error': 'Erreur lors du traitement des fichiers', }; \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 4aff025..ad8cb6c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,11 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App.tsx' +// Import all global styles +import './index.css' +import './styles/themes/light.css' +import './styles/buttons.css' + createRoot(document.getElementById('root')!).render( diff --git a/src/pages/Home/Prompts.tsx b/src/pages/Home/Prompts.tsx index e0c6a24..31c6ebd 100644 --- a/src/pages/Home/Prompts.tsx +++ b/src/pages/Home/Prompts.tsx @@ -3,10 +3,10 @@ import { useState } from 'react'; import { IoMdAdd } from 'react-icons/io'; import { PromptsTable } from '../../components/Prompts'; import { useLanguage } from '../../contexts/LanguageContext'; -import { Popup } from '../../components/Popup/Popup'; -import { EditForm } from '../../components/Popup/EditForm'; +import { Popup } from '../../components/ui/Popup/Popup'; +import { EditForm } from '../../components/ui/Popup/EditForm'; import { usePrompts, usePromptOperations } from '../../hooks/usePrompts'; -import type { EditFieldConfig } from '../../components/Popup/EditForm'; +import type { EditFieldConfig } from '../../components/ui/Popup/EditForm'; import sharedStyles from '../../core/PageManager/pages.module.css'; function Prompts() { diff --git a/src/assets/styles/bg.jpg b/src/styles/assets/bg.jpg similarity index 100% rename from src/assets/styles/bg.jpg rename to src/styles/assets/bg.jpg diff --git a/src/styles/buttons.css b/src/styles/buttons.css index be9836c..96b2e26 100644 --- a/src/styles/buttons.css +++ b/src/styles/buttons.css @@ -27,7 +27,7 @@ --button-warning-text: #212529; /* Button Sizes */ - --button-sm-padding: 6px 12px; + --button-sm-padding: 8px 12px; --button-sm-font-size: 12px; --button-sm-icon-size: 14px; @@ -202,6 +202,16 @@ display: none; } +/* Spinner Icon for Upload Button */ +.spinnerIcon { + width: 1em; + height: 1em; + border: 2px solid transparent; + border-top: 2px solid currentColor; + border-radius: 50%; + animation: spin 1s linear infinite; +} + /* Responsive Design */ @media (max-width: 768px) { .buttonSm { diff --git a/src/core/PageManager/pages.module.css b/src/styles/pages.module.css similarity index 100% rename from src/core/PageManager/pages.module.css rename to src/styles/pages.module.css diff --git a/src/styles/themes.css b/src/styles/themes.css deleted file mode 100644 index 6a82af1..0000000 --- a/src/styles/themes.css +++ /dev/null @@ -1,64 +0,0 @@ -/* Light Theme CSS Variables */ -:root { - --color-bg: #F8F9FA; /* war vorher surface */ - --color-surface: #EFEDE5; /* war vorher bg */ - --color-text: #3A3A3A; - - --color-primary: #C7C5B2; - --color-primary-hover: #D9D7C6; - --color-primary-disabled: #E3E2D8; - - --color-secondary: #F25843; - --color-secondary-hover: #FF6A55; - --color-secondary-disabled: #F5B0A4; - - --color-red: #dc3545; - --color-red-hover: #f5c6cb; - --color-red-disabled: #f8d7da; - - --color-secondary-red: #B94A55; - --color-secondary-red-hover: #D46872; - --color-secondary-red-disabled: #E8B7BA; - - --color-gray: #6F7373; - --color-gray-hover: #565A5A; - --color-gray-disabled: #B7BBBA; - - --color-medium-gray: #E0DDD3; - --color-medium-gray-hover: #D1CEC5; - --color-medium-gray-disabled: #E0DDD380; - - --color-highlight-gray: #F5F3ED; - --color-highlight-gray-hover: #E6E3DC; - --color-highlight-gray-disabled: #F5F3ED80; - - --font-family: "DM Sans", sans-serif; -} - -/* Dark Theme Overrides */ -.dark-theme { - --color-bg: #181818; /* war vorher surface */ - --color-surface: #1E1D1A; /* war vorher bg */ - --color-text: #E5E7EB; - - --color-primary: #C7C5B2; - --color-primary-hover: #E0DECC; - --color-primary-disabled: #59584F; - - --color-secondary: #F25843; - --color-secondary-hover: #FF715C; - --color-secondary-disabled: #6E3E36; - - --color-red: #dc3545; - --color-red-hover: #f5c6cb; - --color-red-disabled: #f8d7da; - - --color-secondary-red: #D65D6A; - --color-secondary-red-hover: #E17683; - --color-secondary-red-disabled: #70363C; - - --color-gray: #181818; - --color-gray-hover: #2E2E2E; - --color-gray-disabled: #505050; -} - diff --git a/src/assets/styles/dark.css b/src/styles/themes/dark.css similarity index 87% rename from src/assets/styles/dark.css rename to src/styles/themes/dark.css index 93df05f..51464d6 100644 --- a/src/assets/styles/dark.css +++ b/src/styles/themes/dark.css @@ -24,5 +24,8 @@ --color-gray-disabled: #505357; --font-family: "Trebuchet MS", sans-serif; + --object-radius-large: 30px; + --object-radius-medium: 15px; + --object-radius-small: 5px; } \ No newline at end of file diff --git a/src/assets/styles/light.css b/src/styles/themes/light.css similarity index 94% rename from src/assets/styles/light.css rename to src/styles/themes/light.css index 497bbdf..703d60e 100644 --- a/src/assets/styles/light.css +++ b/src/styles/themes/light.css @@ -32,6 +32,9 @@ --color-highlight-gray-disabled: #F5F3ED80; --font-family: "DM Sans", sans-serif; + --object-radius-large: 30px; + --object-radius-medium: 15px; + --object-radius-small: 5px; } /* Dark theme overrides */