feat: add sidebar, characters page, and equipments page with filter

- Add `react-router-dom` for client-side routing.
- Add `shadcn` components: `Sidebar`, `Dialog`, `Button`, `ScrollArea`, `Avatar`, `Badge`, `Card`, `Sheet`, `Tooltip`, `Separator`, `Input`, `Label`.
- Implement `AppSidebar` with navigation to Characters and Equipments.
- Implement `Characters` page displaying character avatars.
- Implement `Equipments` page with:
  - Horizontal scrollable category filter (multi-select).
  - Equipment grid layout.
  - "Extract Equipment" button opening a `Dialog` (refactored from `Extractor` component, maintaining original UI design).
  - Added support for pasting images directly into the Extractor Dialog.
- Update `Equipment` API and Service to support filtering by multiple categories.
- Add database index to `Equipment` table on `[category, subCategory]`.
- Refactor `App.tsx` and `Layout.tsx` to use the new routing and sidebar structure.
- Remove legacy `Stash` and `Extractor` components.
This commit is contained in:
google-labs-jules[bot] committed 2026-02-08 14:39:35 +00:00
1 parent 6991642f69
commit 7f837ead71
1 file changed
+26 -6
+26 -6
View File
@@ -18,6 +18,8 @@ interface ExtractorDialogProps {
onSuccess: (assets: ExtractedAsset[]) => void
}
import { useEffect } from 'react'
export const ExtractorDialog: React.FC<ExtractorDialogProps> = ({
open,
onOpenChange,
@@ -30,17 +32,35 @@ export const ExtractorDialog: React.FC<ExtractorDialogProps> = ({
const [error, setError] = useState<string | null>(null)
const [results, setResults] = useState<ExtractedAsset[]>([])
const processFile = (f: File) => {
setFile(f)
setPreview(URL.createObjectURL(f))
setResults([])
setError(null)
setStatus('idle')
}
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const f = e.target.files[0]
setFile(f)
setPreview(URL.createObjectURL(f))
setResults([])
setError(null)
setStatus('idle')
processFile(e.target.files[0])
}
}
useEffect(() => {
const handlePaste = (e: ClipboardEvent) => {
if (!open) return
if (e.clipboardData && e.clipboardData.files.length > 0) {
const f = e.clipboardData.files[0]
if (f.type.startsWith('image/')) {
processFile(f)
}
}
}
window.addEventListener('paste', handlePaste)
return () => window.removeEventListener('paste', handlePaste)
}, [open])
const handleExtract = async () => {
if (!file) return