Landing zones

This commit is contained in:
Reece 2025-10-01 17:03:35 +01:00
parent 85dedf4b28
commit 700b341faa
8 changed files with 401 additions and 11 deletions

View File

@ -3131,6 +3131,9 @@
"addFiles": "Add Files",
"dragFilesInOrClick": "Drag files in or click \"Add Files\" to browse"
},
"fileEditor": {
"addFiles": "Add Files"
},
"fileManager": {
"title": "Upload PDF Files",
"subtitle": "Add files to your storage for easy access across tools",
@ -3166,7 +3169,6 @@
"googleDriveShort": "Drive",
"myFiles": "My Files",
"noRecentFiles": "No recent files found",
"dropFilesHint": "Drop files here to upload",
"googleDriveNotAvailable": "Google Drive integration not available",
"openFiles": "Open Files",
"openFile": "Open File",

View File

@ -20,7 +20,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
const [isDragging, setIsDragging] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const { loadRecentFiles, handleRemoveFile } = useFileManager();
const { loadRecentFiles, handleRemoveFile, loading } = useFileManager();
// File management handlers
const isFileSupported = useCallback((fileName: string) => {
@ -147,6 +147,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
onFileRemove={handleRemoveFileByIndex}
modalHeight={modalHeight}
refreshRecentFiles={refreshRecentFiles}
isLoading={loading}
>
{isMobile ? <MobileLayout /> : <DesktopLayout />}
</FileManagerProvider>

View File

@ -0,0 +1,179 @@
import React, { useRef, useState } from 'react';
import { Button, Group, useMantineColorScheme } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import AddIcon from '@mui/icons-material/Add';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
import LocalIcon from '../shared/LocalIcon';
import { BASE_PATH } from '../../constants/app';
import styles from './FileEditor.module.css';
interface AddFileCardProps {
onFileSelect: (files: File[]) => void;
accept?: string;
multiple?: boolean;
}
const AddFileCard = ({
onFileSelect,
accept = ".pdf,.zip",
multiple = true
}: AddFileCardProps) => {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const { openFilesModal } = useFilesModalContext();
const { colorScheme } = useMantineColorScheme();
const [isUploadHover, setIsUploadHover] = useState(false);
const handleNativeUploadClick = (e: React.MouseEvent) => {
e.stopPropagation();
fileInputRef.current?.click();
};
const handleOpenFilesModal = (e: React.MouseEvent) => {
e.stopPropagation();
openFilesModal();
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
if (files.length > 0) {
onFileSelect(files);
}
// Reset input so same files can be selected again
event.target.value = '';
};
// Prevent this card from interfering with drag and drop to parent Dropzone
const handleDragOver = (e: React.DragEvent) => {
// Don't prevent default - let the parent Dropzone handle it
// Don't stop propagation - let it bubble up
};
const handleDrop = (e: React.DragEvent) => {
// Don't prevent default or stop propagation
// Let the parent Dropzone handle the file drop
};
return (
<>
<input
ref={fileInputRef}
type="file"
accept={accept}
multiple={multiple}
onChange={handleFileChange}
style={{ display: 'none' }}
/>
<div
className={`${styles.addFileCard} w-[18rem] h-[22rem] select-none flex flex-col shadow-sm transition-all relative`}
tabIndex={0}
role="region"
aria-label={t('fileEditor.addFiles', 'Add files')}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{/* Header bar - matches FileEditorThumbnail structure */}
<div className={`${styles.header} ${styles.addFileHeader}`}>
<div className={styles.logoMark}>
<AddIcon sx={{ color: 'inherit', fontSize: '1.5rem' }} />
</div>
<div className={styles.headerIndex}>
{t('fileEditor.addFiles', 'Add Files')}
</div>
<div className={styles.kebab} />
</div>
{/* Main content area */}
<div className={styles.addFileContent}>
{/* Stirling PDF Branding */}
<Group gap="xs" align="center">
<img
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
alt="Stirling PDF"
style={{ height: '2.2rem', width: 'auto' }}
/>
</Group>
{/* Add Files + Native Upload Buttons - styled like LandingPage */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '0.6rem',
width: '100%',
marginTop: '0.8rem',
marginBottom: '0.8rem'
}}
onMouseLeave={() => setIsUploadHover(false)}
>
<Button
style={{
backgroundColor: 'var(--landing-button-bg)',
color: 'var(--landing-button-color)',
border: '1px solid var(--landing-button-border)',
borderRadius: '2rem',
height: '38px',
paddingLeft: isUploadHover ? 0 : '1rem',
paddingRight: isUploadHover ? 0 : '1rem',
width: isUploadHover ? '58px' : 'calc(100% - 58px - 0.6rem)',
minWidth: isUploadHover ? '58px' : undefined,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'width .5s ease, padding .5s ease'
}}
onClick={handleOpenFilesModal}
onMouseEnter={() => setIsUploadHover(false)}
>
<LocalIcon icon="add" width="1.5rem" height="1.5rem" className="text-[var(--accent-interactive)]" />
{!isUploadHover && (
<span>
{t('landing.addFiles', 'Add Files')}
</span>
)}
</Button>
<Button
aria-label="Upload"
style={{
backgroundColor: 'var(--landing-button-bg)',
color: 'var(--landing-button-color)',
border: '1px solid var(--landing-button-border)',
borderRadius: '1rem',
height: '38px',
width: isUploadHover ? 'calc(100% - 58px - 0.6rem)' : '58px',
minWidth: '58px',
paddingLeft: isUploadHover ? '1rem' : 0,
paddingRight: isUploadHover ? '1rem' : 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'width .5s ease, padding .5s ease'
}}
onClick={handleNativeUploadClick}
onMouseEnter={() => setIsUploadHover(true)}
>
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
{isUploadHover && (
<span style={{ marginLeft: '.5rem' }}>
{t('landing.uploadFromComputer', 'Upload from computer')}
</span>
)}
</Button>
</div>
{/* Instruction Text */}
<span
className="text-[var(--accent-interactive)]"
style={{ fontSize: '.8rem', textAlign: 'center', marginTop: '0.5rem' }}
>
{t('fileUpload.dropFilesHere', 'Drop files here or click the upload button')}
</span>
</div>
</div>
</>
);
};
export default AddFileCard;

View File

@ -304,4 +304,84 @@
/* Light mode selected header stroke override */
:global([data-mantine-color-scheme="light"]) .card[data-selected="true"] {
outline-color: #3B4B6E;
}
/* =========================
Add File Card Styles
========================= */
.addFileCard {
background: var(--file-card-bg);
border: 2px dashed var(--border-default);
border-radius: 0.0625rem;
cursor: pointer;
transition: all 0.18s ease;
max-width: 100%;
max-height: 100%;
overflow: hidden;
margin-left: 0.5rem;
margin-right: 0.5rem;
opacity: 0.7;
}
.addFileCard:hover {
opacity: 1;
border-color: var(--color-blue-500);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.addFileCard:focus {
outline: 2px solid var(--color-blue-500);
outline-offset: 2px;
}
.addFileHeader {
background: var(--bg-subtle);
color: var(--text-secondary);
border-bottom: 1px solid var(--border-default);
}
.addFileCard:hover .addFileHeader {
background: var(--color-blue-500);
color: white;
}
.addFileContent {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.5rem 1rem;
gap: 0.5rem;
}
.addFileIcon {
display: flex;
align-items: center;
justify-content: center;
width: 5rem;
height: 5rem;
border-radius: 50%;
background: var(--bg-subtle);
transition: background-color 0.18s ease;
}
.addFileCard:hover .addFileIcon {
background: var(--color-blue-50);
}
.addFileText {
font-weight: 500;
transition: color 0.18s ease;
}
.addFileCard:hover .addFileText {
color: var(--text-primary);
}
.addFileSubtext {
font-size: 0.875rem;
opacity: 0.8;
}

View File

@ -8,6 +8,7 @@ import { useNavigationActions } from '../../contexts/NavigationContext';
import { zipFileService } from '../../services/zipFileService';
import { detectFileExtension } from '../../utils/fileUtils';
import FileEditorThumbnail from './FileEditorThumbnail';
import AddFileCard from './AddFileCard';
import FilePickerModal from '../shared/FilePickerModal';
import SkeletonLoader from '../shared/SkeletonLoader';
import { FileId, StirlingFile } from '../../types/fileContext';
@ -405,6 +406,16 @@ const FileEditor = ({
pointerEvents: 'auto'
}}
>
{/* Add File Card - only show when files exist */}
{activeStirlingFileStubs.length > 0 && (
<AddFileCard
key="add-file-card"
onFileSelect={handleFileUpload}
accept=".pdf,.zip"
multiple={true}
/>
)}
{activeStirlingFileStubs.map((record, index) => {
return (
<FileEditorThumbnail

View File

@ -0,0 +1,115 @@
import React, { useState } from 'react';
import { Button, Group, Text, Stack, useMantineColorScheme } from '@mantine/core';
import HistoryIcon from '@mui/icons-material/History';
import { useTranslation } from 'react-i18next';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
import LocalIcon from '../shared/LocalIcon';
import { BASE_PATH } from '../../constants/app';
const EmptyFilesState: React.FC = () => {
const { t } = useTranslation();
const { colorScheme } = useMantineColorScheme();
const { onLocalFileClick } = useFileManagerContext();
const [isUploadHover, setIsUploadHover] = useState(false);
const handleUploadClick = () => {
onLocalFileClick();
};
return (
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem'
}}
>
{/* Container */}
<div
style={{
backgroundColor: 'transparent',
padding: '3rem 2rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1.5rem',
minWidth: '20rem',
maxWidth: '28rem',
width: '100%'
}}
>
{/* No Recent Files Message */}
<Stack align="center" gap="sm">
<HistoryIcon style={{ fontSize: '3rem', color: 'var(--mantine-color-gray-5)' }} />
<Text c="dimmed" ta="center" size="lg">
{t('fileManager.noRecentFiles', 'No recent files')}
</Text>
</Stack>
{/* Stirling PDF Logo */}
<Group gap="xs" align="center">
<img
src={colorScheme === 'dark' ? `${BASE_PATH}/branding/StirlingPDFLogoWhiteText.svg` : `${BASE_PATH}/branding/StirlingPDFLogoGreyText.svg`}
alt="Stirling PDF"
style={{ height: '2.2rem', width: 'auto' }}
/>
</Group>
{/* Upload Button */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
marginTop: '0.5rem',
marginBottom: '0.5rem'
}}
onMouseLeave={() => setIsUploadHover(false)}
>
<Button
aria-label="Upload"
style={{
backgroundColor: 'var(--bg-file-manager)',
color: 'var(--landing-button-color)',
border: '1px solid var(--landing-button-border)',
borderRadius: isUploadHover ? '2rem' : '1rem',
height: '38px',
width: isUploadHover ? '100%' : '58px',
minWidth: '58px',
paddingLeft: isUploadHover ? '1rem' : 0,
paddingRight: isUploadHover ? '1rem' : 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'width .5s ease, padding .5s ease, border-radius .5s ease'
}}
onClick={handleUploadClick}
onMouseEnter={() => setIsUploadHover(true)}
>
<LocalIcon icon="upload" width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
{isUploadHover && (
<span style={{ marginLeft: '.5rem' }}>
{t('landing.uploadFromComputer', 'Upload from computer')}
</span>
)}
</Button>
</div>
{/* Instruction Text */}
<span
className="text-[var(--accent-interactive)]"
style={{ fontSize: '.8rem', textAlign: 'center' }}
>
{t('fileUpload.dropFilesHere', 'Drop files here or click the upload button')}
</span>
</div>
</div>
);
};
export default EmptyFilesState;

View File

@ -1,10 +1,10 @@
import React from 'react';
import { Center, ScrollArea, Text, Stack } from '@mantine/core';
import CloudIcon from '@mui/icons-material/Cloud';
import HistoryIcon from '@mui/icons-material/History';
import { useTranslation } from 'react-i18next';
import FileListItem from './FileListItem';
import FileHistoryGroup from './FileHistoryGroup';
import EmptyFilesState from './EmptyFilesState';
import { useFileManagerContext } from '../../contexts/FileManagerContext';
interface FileListAreaProps {
@ -29,6 +29,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
onFileDoubleClick,
onDownloadSingle,
isFileSupported,
isLoading,
} = useFileManagerContext();
const { t } = useTranslation();
@ -43,15 +44,11 @@ const FileListArea: React.FC<FileListAreaProps> = ({
scrollbarSize={8}
>
<Stack gap={0}>
{recentFiles.length === 0 ? (
{recentFiles.length === 0 && !isLoading ? (
<EmptyFilesState />
) : recentFiles.length === 0 && isLoading ? (
<Center style={{ height: '12.5rem' }}>
<Stack align="center" gap="sm">
<HistoryIcon style={{ fontSize: '3rem', color: 'var(--mantine-color-gray-5)' }} />
<Text c="dimmed" ta="center">{t('fileManager.noRecentFiles', 'No recent files')}</Text>
<Text size="xs" c="dimmed" ta="center" style={{ opacity: 0.7 }}>
{t('fileManager.dropFilesHint', 'Drop files anywhere to upload')}
</Text>
</Stack>
<Text c="dimmed" ta="center">Loading files...</Text>
</Center>
) : (
filteredFiles.map((file, index) => {

View File

@ -18,6 +18,7 @@ interface FileManagerContextValue {
expandedFileIds: Set<FileId>;
fileGroups: Map<FileId, StirlingFileStub[]>;
loadedHistoryFiles: Map<FileId, StirlingFileStub[]>;
isLoading: boolean;
// Handlers
onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
@ -58,6 +59,7 @@ interface FileManagerProviderProps {
onFileRemove: (index: number) => void;
modalHeight: string;
refreshRecentFiles: () => Promise<void>;
isLoading: boolean;
}
export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
@ -71,6 +73,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
onFileRemove,
modalHeight,
refreshRecentFiles,
isLoading,
}) => {
const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent');
const [selectedFileIds, setSelectedFileIds] = useState<FileId[]>([]);
@ -574,6 +577,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
expandedFileIds,
fileGroups,
loadedHistoryFiles,
isLoading,
// Handlers
onSourceChange: handleSourceChange,
@ -607,6 +611,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
expandedFileIds,
fileGroups,
loadedHistoryFiles,
isLoading,
handleSourceChange,
handleLocalFileClick,
handleFileSelect,