Fix bugs, convert thumbnail to png, add context menu
This commit is contained in:
parent
c545d4b17d
commit
eb8a302222
37 changed files with 1916 additions and 137 deletions
169
frontend/src/features/library/LibraryHeader.tsx
Normal file
169
frontend/src/features/library/LibraryHeader.tsx
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import { useLibrary } from './useLibrary'
|
||||
import { useAuth } from '../auth/useAuth'
|
||||
|
||||
export const LibraryHeader = ({ currentTab, onTabChange }: { currentTab: 'library' | 'trash', onTabChange: (tab: 'library' | 'trash') => void }) => {
|
||||
const { logout } = useAuth()
|
||||
const {
|
||||
searchQuery, setSearchQuery, fetchDocuments, fetchTrash,
|
||||
selectedIds, clearSelection, selectAll, bulkDelete, bulkRestore, emptyTrash, documents
|
||||
} = useLibrary()
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (currentTab === 'library') {
|
||||
fetchDocuments(searchQuery)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabChange = (tab: 'library' | 'trash') => {
|
||||
onTabChange(tab)
|
||||
if (tab === 'library') {
|
||||
fetchDocuments()
|
||||
} else {
|
||||
fetchTrash()
|
||||
}
|
||||
}
|
||||
|
||||
const isSelectionMode = selectedIds.size > 0
|
||||
const allSelected = documents.length > 0 && selectedIds.size === documents.length
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-40 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between gap-4">
|
||||
|
||||
{/* Left: Branding & Tabs */}
|
||||
<div className="flex items-center gap-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center shadow-lg shadow-indigo-500/30">
|
||||
<span className="text-white font-bold text-xl leading-none">P</span>
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white hidden sm:block">PaperJet</h1>
|
||||
</div>
|
||||
|
||||
<nav className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 p-1 rounded-lg">
|
||||
<button
|
||||
onClick={() => handleTabChange('library')}
|
||||
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${
|
||||
currentTab === 'library'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Library
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('trash')}
|
||||
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${
|
||||
currentTab === 'trash'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Trash
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Right: Search & Actions */}
|
||||
<div className="flex items-center gap-4 flex-1 justify-end">
|
||||
{isSelectionMode ? (
|
||||
<div className="flex items-center gap-3 bg-indigo-50 dark:bg-indigo-900/30 px-4 py-1.5 rounded-full border border-indigo-100 dark:border-indigo-800">
|
||||
<span className="text-sm font-medium text-indigo-700 dark:text-indigo-300">
|
||||
{selectedIds.size} selected
|
||||
</span>
|
||||
<div className="h-4 w-px bg-indigo-200 dark:bg-indigo-700"></div>
|
||||
|
||||
<button onClick={allSelected ? clearSelection : selectAll} className="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-200 font-medium">
|
||||
{allSelected ? 'Deselect All' : 'Select All'}
|
||||
</button>
|
||||
|
||||
{currentTab === 'library' ? (
|
||||
<button
|
||||
onClick={() => bulkDelete()}
|
||||
className="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 font-medium px-2 py-1 hover:bg-red-50 dark:hover:bg-red-900/30 rounded"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => bulkRestore()}
|
||||
className="text-sm text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300 font-medium px-2 py-1 hover:bg-green-50 dark:hover:bg-green-900/30 rounded"
|
||||
>
|
||||
Restore
|
||||
</button>
|
||||
<button
|
||||
onClick={() => bulkDelete(true)}
|
||||
className="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 font-medium px-2 py-1 hover:bg-red-50 dark:hover:bg-red-900/30 rounded"
|
||||
>
|
||||
Delete Forever
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{currentTab === 'library' && (
|
||||
<form onSubmit={handleSearch} className="relative hidden md:block max-w-md w-full">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search documents..."
|
||||
className="block w-full pl-10 pr-3 py-2 border border-gray-200 dark:border-gray-700 rounded-xl leading-5 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent sm:text-sm transition-all"
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{currentTab === 'trash' && (
|
||||
<button
|
||||
onClick={() => emptyTrash()}
|
||||
className="text-sm font-medium text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 transition-colors"
|
||||
>
|
||||
Empty Trash
|
||||
</button>
|
||||
)}
|
||||
|
||||
{currentTab === 'library' && (
|
||||
<label className="cursor-pointer inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-xl shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all active:scale-95">
|
||||
<span>Upload PDF</span>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
// We'll dispatch a custom event that UploadArea listens to, or just click its input
|
||||
// For simplicity, find the hidden file input in UploadArea and click it
|
||||
const uploader = document.querySelector('input[type="file"][multiple]') as HTMLInputElement
|
||||
if (uploader && uploader !== e.target) {
|
||||
uploader.files = e.target.files
|
||||
const event = new Event('change', { bubbles: true })
|
||||
uploader.dispatchEvent(event)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={logout}
|
||||
className="p-2 text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
|
||||
title="Logout"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue