import { useState } from 'react' import { X, Star, Info, ShoppingBasket, Trash2, Plus } from 'lucide-react' import clsx from 'clsx' import { useMutation, useQueryClient } from '@tanstack/react-query' import { usePhotoStore } from '../../store/photoStore' import { photos as photosApi, heaps as heapsApi, tags as tagsApi, } from '../../services/api' import { useActiveHeapMembers } from '../../hooks/useActiveHeapMembersQuery' import { HEAPS_QUERY_KEY } from '../../hooks/useHeapsQuery' import { useTagsQuery, TAGS_QUERY_KEY } from '../../hooks/useTagsQuery' import { toast } from '../ToastContainer' import { PhotoInfoPanel } from '../sidebar/PhotoInfoPanel' import { COLOR_LABEL_OPTIONS } from '../../constants/colorLabels' /** * Right-hand details panel. * - 1 photo selected → delegates to PhotoInfoPanel for the full editor. * - 2+ photos selected → renders a slim bulk-action panel that fans out * rating / color / discard / pick across the entire selection. */ export function RightSidebar() { const { selectedPhotos, activePhotoId, clearSelection } = usePhotoStore() const queryClient = useQueryClient() const invalidatePhotoQueries = () => { queryClient.invalidateQueries({ queryKey: ['photo'] }) queryClient.invalidateQueries({ queryKey: ['photos'] }) } const bulkRatingMutation = useMutation({ mutationFn: ({ ids, rating }: { ids: string[]; rating: number }) => photosApi.bulkSetRating(ids, rating), onSuccess: invalidatePhotoQueries, }) const bulkColorMutation = useMutation({ mutationFn: ({ ids, color }: { ids: string[]; color: string | null }) => photosApi.bulkSetColor(ids, color), onSuccess: invalidatePhotoQueries, }) const bulkDiscardMutation = useMutation({ mutationFn: (ids: string[]) => photosApi.bulkDiscard(ids), onSuccess: invalidatePhotoQueries, }) // Bulk tag mutations. Tag mutations also need to invalidate the tags // query so the FilterBar / sidebar tag counts stay fresh. const invalidateTagsAndPhotos = () => { invalidatePhotoQueries() queryClient.invalidateQueries({ queryKey: TAGS_QUERY_KEY }) } const bulkAddTagsMutation = useMutation({ mutationFn: ({ ids, tagIds }: { ids: string[]; tagIds: string[] }) => photosApi.bulkAddTags(ids, tagIds), onSuccess: (data) => { const added = data?.added ?? 0 toast.success( 'Tags added', `${added} new link${added === 1 ? '' : 's'}` ) invalidateTagsAndPhotos() }, onError: (e: any) => toast.error('Add tags failed', e?.message || 'Unknown error'), }) const bulkRemoveTagsMutation = useMutation({ mutationFn: ({ ids, tagIds }: { ids: string[]; tagIds: string[] }) => photosApi.bulkRemoveTags(ids, tagIds), onSuccess: (data) => { const removed = data?.removed ?? 0 toast.success( 'Tags removed', `${removed} link${removed === 1 ? '' : 's'} removed` ) invalidateTagsAndPhotos() }, onError: (e: any) => toast.error('Remove tags failed', e?.message || 'Unknown error'), }) // Idempotent create-and-attach: lets the user type a brand-new tag // name and apply it to the whole selection in one click. const createAndAttachMutation = useMutation({ mutationFn: async ({ name, ids }: { name: string; ids: string[] }) => { const created = await tagsApi.create(name) return photosApi.bulkAddTags(ids, [created.id]) }, onSuccess: () => { toast.success('Tag created and applied') invalidateTagsAndPhotos() }, onError: (e: any) => toast.error('Create tag failed', e?.message || 'Unknown error'), }) const { data: allTags = [] } = useTagsQuery() const [tagInput, setTagInput] = useState('') // Active heap membership for the bulk Pick toggle. const { activeHeap, memberIds: activeHeapMembers } = useActiveHeapMembers() const heapMutation = useMutation({ mutationFn: ({ ids, remove }: { ids: string[]; remove: boolean }) => { if (!activeHeap || ids.length === 0) return Promise.resolve(null) return remove ? heapsApi.removePhotos(activeHeap.id, ids) : heapsApi.addPhotos(activeHeap.id, ids) }, onMutate: ({ ids, remove }) => { if (!activeHeap || ids.length === 0) return { previous: undefined } const key = ['heap-photo-ids', activeHeap.id] as const const previous = queryClient.getQueryData(key) const set = new Set(previous ?? []) if (remove) ids.forEach((id) => set.delete(id)) else ids.forEach((id) => set.add(id)) queryClient.setQueryData(key, Array.from(set)) return { previous } }, onError: (e: any, _vars, ctx) => { if (activeHeap && ctx?.previous) { queryClient.setQueryData(['heap-photo-ids', activeHeap.id], ctx.previous) } toast.error('Heap update failed', e?.message || 'Unknown error') }, onSettled: () => { queryClient.invalidateQueries({ queryKey: HEAPS_QUERY_KEY }) if (activeHeap) { queryClient.invalidateQueries({ queryKey: ['heap-photo-ids', activeHeap.id], }) } }, }) if (selectedPhotos.length === 0) { return (

Select photos to view details

) } // ── Single-photo: full editor via PhotoInfoPanel ──────────────────── if (selectedPhotos.length === 1) { const id = activePhotoId ?? selectedPhotos[0] return (

Photo Details

) } // ── Multi-photo: bulk action panel ────────────────────────────────── const allMembers = selectedPhotos.every((id) => activeHeapMembers.has(id)) return (

{selectedPhotos.length} Photos Selected

Rating, color, and flag apply to all {selectedPhotos.length} selected.

{/* Bulk rating */}
{[1, 2, 3, 4, 5].map((value) => ( ))}
{/* Bulk color */}
{COLOR_LABEL_OPTIONS.map(({ value, className }) => (
{/* Bulk flag */}
{/* Bulk tags. Click an existing tag chip to apply it to the * whole selection; long-press / X icon to remove. The text * input adds an existing tag if it matches a name, or creates * a new tag and applies it. */}
bulkAddTagsMutation.mutate({ ids: selectedPhotos, tagIds: [tagId] }) } onRemove={(tagId) => bulkRemoveTagsMutation.mutate({ ids: selectedPhotos, tagIds: [tagId], }) } onCreate={(name) => { createAndAttachMutation.mutate({ name, ids: selectedPhotos }) setTagInput('') }} />
) } interface BulkTagsEditorProps { allTags: { id: string; name: string; color: string | null }[] tagInput: string onTagInputChange: (value: string) => void disabled: boolean onApply: (tagId: string) => void onRemove: (tagId: string) => void onCreate: (name: string) => void } /** * Compact bulk tag editor for the multi-select right sidebar. Unlike the * single-photo TagsEditor we don't show "current tags" — there's no clean * single-photo notion of that across an arbitrary selection. Instead the * user picks an existing tag (apply to all) or types a new one (create * and apply to all). */ function BulkTagsEditor({ allTags, tagInput, onTagInputChange, disabled, onApply, onRemove, onCreate, }: BulkTagsEditorProps) { const trimmed = tagInput.trim() const lower = trimmed.toLowerCase() const filtered = trimmed ? allTags.filter((t) => t.name.toLowerCase().includes(lower)) : allTags const exactMatch = trimmed ? allTags.find((t) => t.name.toLowerCase() === lower) : null const handleSubmit = () => { if (!trimmed || disabled) return if (exactMatch) { onApply(exactMatch.id) onTagInputChange('') } else { onCreate(trimmed) } } return (
onTagInputChange(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault() handleSubmit() } else if (e.key === 'Escape') { onTagInputChange('') } }} placeholder="Filter or create…" disabled={disabled} className="w-full rounded border border-border bg-bg px-2 py-1 text-xs text-text placeholder-text-faint focus:border-primary focus:outline-none disabled:opacity-50" /> {trimmed && !exactMatch && ( )} {filtered.length > 0 ? (
{filtered.map((tag) => ( ))}
) : (
No tags match
)}
) }