diff --git a/apps/web/src/components/add-photos-fab.tsx b/apps/web/src/components/add-photos-fab.tsx new file mode 100644 index 0000000..b1770cd --- /dev/null +++ b/apps/web/src/components/add-photos-fab.tsx @@ -0,0 +1,34 @@ +import { ImagePlus } from "lucide-react"; + +import { useAddPhotos } from "@/hooks/use-add-photos"; +import { PHOTO_ACCEPT } from "@/lib/upload"; +import { cn } from "@/lib/utils"; + +/** Floating add-photos button — the primary add action on small screens. */ +export function AddPhotosFab({ className }: { className?: string }) { + const { inputRef, open, onChange } = useAddPhotos(); + + return ( + <> + + + + ); +} diff --git a/apps/web/src/components/empty-hero.tsx b/apps/web/src/components/empty-hero.tsx index a3f628c..d507540 100644 --- a/apps/web/src/components/empty-hero.tsx +++ b/apps/web/src/components/empty-hero.tsx @@ -1,11 +1,8 @@ -import { useRef } from "react"; import { ImagePlus } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { usePhotoStore } from "@/stores/photo-store"; - -const ACCEPT = - "image/jpeg,image/png,image/webp,image/heic,image/heif,.jpg,.jpeg,.png,.webp,.heic,.heif"; +import { useAddPhotos } from "@/hooks/use-add-photos"; +import { PHOTO_ACCEPT } from "@/lib/upload"; /** A few illustrative polaroids — pure CSS, no image assets, so it loads instantly. */ const SAMPLES = [ @@ -16,8 +13,7 @@ const SAMPLES = [ /** Shown in the centre column before any photos are added. */ export function EmptyHero() { - const addFiles = usePhotoStore((state) => state.addFiles); - const inputRef = useRef(null); + const { inputRef, open, onChange } = useAddPhotos(); return (
@@ -56,21 +52,12 @@ export function EmptyHero() { { - const files = Array.from(event.target.files ?? []); - if (files.length > 0) addFiles(files); - event.target.value = ""; - }} + onChange={onChange} /> - diff --git a/apps/web/src/components/photo-filmstrip.tsx b/apps/web/src/components/photo-filmstrip.tsx new file mode 100644 index 0000000..987291f --- /dev/null +++ b/apps/web/src/components/photo-filmstrip.tsx @@ -0,0 +1,143 @@ +import { + DndContext, + type DragEndEvent, + PointerSensor, + closestCenter, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + SortableContext, + horizontalListSortingStrategy, + useSortable, +} from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { ImagePlus, Loader2, X } from "lucide-react"; + +import { useAddPhotos } from "@/hooks/use-add-photos"; +import { type Photo } from "@/lib/photos"; +import { PHOTO_ACCEPT } from "@/lib/upload"; +import { cn } from "@/lib/utils"; +import { useEditorStore } from "@/stores/editor-store"; +import { usePhotoStore } from "@/stores/photo-store"; + +/** + * Mobile photo tray: a horizontal, sideways-scrolling row so any number of + * photos stays one thumbnail tall and never pushes the sheet down the page. + * Leads with a "+" tile; drag thumbnails to reorder, tap to select, × to remove. + */ +export function PhotoFilmstrip({ className }: { className?: string }) { + const photos = usePhotoStore((state) => state.photos); + const reorder = usePhotoStore((state) => state.reorder); + const { inputRef, open, onChange } = useAddPhotos(); + + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), + ); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (over && active.id !== over.id) { + reorder(String(active.id), String(over.id)); + } + }; + + return ( +
+ + + + + photo.id)} + strategy={horizontalListSortingStrategy} + > + {photos.map((photo) => ( + + ))} + + +
+ ); +} + +function FilmstripItem({ photo }: { photo: Photo }) { + const remove = usePhotoStore((state) => state.remove); + const selectedId = useEditorStore((state) => state.selectedId); + const select = useEditorStore((state) => state.select); + const selected = selectedId === photo.id; + + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: photo.id }); + + return ( +
+ + + {photo.enriching && ( + + + + )} + + +
+ ); +} diff --git a/apps/web/src/components/photo-sidebar.tsx b/apps/web/src/components/photo-sidebar.tsx index 0902ec6..1fe5ca9 100644 --- a/apps/web/src/components/photo-sidebar.tsx +++ b/apps/web/src/components/photo-sidebar.tsx @@ -1,25 +1,24 @@ -import { useCallback, useRef } from 'react' +import { useCallback } from 'react' import { ImagePlus } from 'lucide-react' import { PhotoStrip } from '@/components/photo-strip' import { Button } from '@/components/ui/button' +import { useAddPhotos } from '@/hooks/use-add-photos' import { useWindowFileDrop } from '@/hooks/use-window-file-drop' +import { PHOTO_ACCEPT } from '@/lib/upload' import { usePhotoStore } from '@/stores/photo-store' -const ACCEPT = - 'image/jpeg,image/png,image/webp,image/heic,image/heif,.jpg,.jpeg,.png,.webp,.heic,.heif' - /** * Left rail: the reorderable photo strip, with an "Add photos" button pinned * to the bottom. The button only appears once there's at least one photo — * before that, the centre empty-state hero carries the add action. - * Dragging a file anywhere over the app reveals a square drop pad here, with - * the strip blurred behind it — and a drop is accepted wherever it lands. + * Dragging a file anywhere over the app reveals a full-height drop pad here, + * with the strip blurred behind it — and a drop is accepted wherever it lands. */ export function PhotoSidebar() { const addFiles = usePhotoStore((state) => state.addFiles) const hasPhotos = usePhotoStore((state) => state.photos.length > 0) - const inputRef = useRef(null) + const { inputRef, open, onChange } = useAddPhotos() const onFiles = useCallback((files: File[]) => addFiles(files), [addFiles]) const dragging = useWindowFileDrop(onFiles) @@ -29,14 +28,10 @@ export function PhotoSidebar() { { - const files = Array.from(event.target.files ?? []) - if (files.length > 0) addFiles(files) - event.target.value = '' - }} + onChange={onChange} /> @@ -45,8 +40,9 @@ export function PhotoSidebar() {