mono/packages/ui/src/components/widgets/ImagePickerDialog.tsx

400 lines
14 KiB
TypeScript

import React, { useState, useEffect, useMemo, useRef } from 'react';
import { supabase } from '@/integrations/supabase/client'; // Still needed for collections (no API yet)
import { fetchPictures as fetchPicturesAPI } from '@/modules/posts/client-pictures';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { T, translate } from '@/i18n';
import { Search, Image as ImageIcon, Check, Tag, FolderOpen, X } from 'lucide-react';
import { useAuth } from '@/hooks/useAuth';
import MediaCard from '@/components/MediaCard';
import { MediaType } from '@/lib/mediaRegistry';
interface ImagePickerDialogProps {
isOpen: boolean;
onClose: () => void;
onSelect?: (pictureId: string) => void;
onSelectPicture?: (picture: Picture) => void;
onMultiSelect?: (pictureIds: string[]) => void;
onMultiSelectPictures?: (pictures: Picture[]) => void;
currentValue?: string | null;
currentValues?: string[];
multiple?: boolean;
}
interface Picture {
id: string;
title: string;
image_url: string;
thumbnail_url?: string;
type?: string;
user_id: string;
tags: string[] | null;
meta: any;
}
interface Collection {
id: string;
name: string;
slug: string;
}
export const ImagePickerDialog: React.FC<ImagePickerDialogProps> = ({
isOpen,
onClose,
onSelect,
onMultiSelect,
onMultiSelectPictures,
currentValue,
currentValues = [],
multiple = false,
onSelectPicture
}) => {
const { user } = useAuth();
const [pictures, setPictures] = useState<Picture[]>([]);
const [allTags, setAllTags] = useState<string[]>([]);
const [collections, setCollections] = useState<Collection[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [selectedCollections, setSelectedCollections] = useState<string[]>([]);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(currentValue || null);
const prevIsOpen = useRef(false);
// Initial data fetch - only runs when dialog opens
useEffect(() => {
if (isOpen) {
fetchPictures(); // Also extracts tags
fetchCollections();
}
}, [isOpen]);
// Sync props to local state - only when dialog first opens (not on every render)
useEffect(() => {
const justOpened = isOpen && !prevIsOpen.current;
prevIsOpen.current = isOpen;
if (justOpened) {
if (multiple) {
setSelectedIds(currentValues || []);
} else {
setSelectedId(currentValue || null);
}
}
}, [isOpen, currentValue, currentValues, multiple]);
const fetchPictures = async () => {
setLoading(true);
try {
// Fetch via API — returns all pictures, we filter client-side
const data = await fetchPicturesAPI({ limit: 100 });
// Filter to only selected pictures
const selected = (data || []).filter((p: any) => p.is_selected);
setPictures(selected);
// Extract unique tags from the same data
const tagsSet = new Set<string>();
selected.forEach((pic: any) => {
pic.tags?.forEach((tag: string) => tagsSet.add(tag));
});
setAllTags(Array.from(tagsSet).sort());
} catch (error) {
console.error('Error fetching pictures:', error);
} finally {
setLoading(false);
}
};
const fetchCollections = async () => {
if (!user) return;
try {
const { data, error } = await supabase
.from('collections')
.select('id, name, slug')
.eq('user_id', user.id)
.order('name');
if (error) throw error;
setCollections(data || []);
} catch (error) {
console.error('Error fetching collections:', error);
}
};
const filteredPictures = useMemo(() => {
return pictures.filter(pic => {
// Search filter
const matchesSearch = pic.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
pic.id.toLowerCase().includes(searchQuery.toLowerCase());
// Tag filter
const matchesTags = selectedTags.length === 0 ||
(pic.tags && selectedTags.some(tag => pic.tags?.includes(tag)));
return matchesSearch && matchesTags;
});
}, [pictures, searchQuery, selectedTags]);
// Further filter by collections if any selected
const [finalPictures, setFinalPictures] = useState<Picture[]>([]);
useEffect(() => {
const filterByCollections = async () => {
if (selectedCollections.length === 0) {
setFinalPictures(filteredPictures);
return;
}
try {
const { data, error } = await supabase
.from('collection_pictures')
.select('picture_id')
.in('collection_id', selectedCollections);
if (error) throw error;
const pictureIdsInCollections = new Set(data?.map(cp => cp.picture_id) || []);
setFinalPictures(filteredPictures.filter(pic => pictureIdsInCollections.has(pic.id)));
} catch (error) {
console.error('Error filtering by collections:', error);
setFinalPictures(filteredPictures);
}
};
filterByCollections();
}, [filteredPictures, selectedCollections]);
const handleSelect = () => {
if (multiple) {
if (onMultiSelect) {
onMultiSelect(selectedIds);
}
if (onMultiSelectPictures) {
const selectedPictures = pictures.filter(p => selectedIds.includes(p.id));
onMultiSelectPictures(selectedPictures);
}
} else {
if (selectedId) {
if (onSelect) onSelect(selectedId);
if (onSelectPicture) {
const pic = pictures.find(p => p.id === selectedId);
if (pic) onSelectPicture(pic);
}
}
}
};
const toggleSelection = (id: string) => {
if (multiple) {
setSelectedIds(prev =>
prev.includes(id)
? prev.filter(pid => pid !== id)
: [...prev, id]
);
} else {
setSelectedId(id);
}
};
console.log('selectedIds', selectedIds);
console.log('selectedId', selectedId);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle><T>Select Picture</T></DialogTitle>
<DialogDescription>
<T>Choose a picture from your published images</T>
</DialogDescription>
</DialogHeader>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={translate('Search by title or ID...')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
{/* Filter Buttons */}
<div className="space-y-3">
{/* Tags Filter */}
{allTags.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Tag className="h-4 w-4" />
<span><T>Tags</T></span>
{selectedTags.length > 0 && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={() => setSelectedTags([])}
>
<T>Clear</T>
</Button>
)}
</div>
<div className="flex flex-wrap gap-2">
{allTags.slice(0, 12).map(tag => (
<Badge
key={tag}
variant={selectedTags.includes(tag) ? "default" : "outline"}
className="cursor-pointer hover:shadow-md transition-shadow"
onClick={() => {
setSelectedTags(prev =>
prev.includes(tag)
? prev.filter(t => t !== tag)
: [...prev, tag]
);
}}
>
{tag}
{selectedTags.includes(tag) && (
<X className="ml-1 h-3 w-3" />
)}
</Badge>
))}
</div>
</div>
)}
{/* Collections Filter */}
{collections.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<FolderOpen className="h-4 w-4" />
<span><T>Collections</T></span>
{selectedCollections.length > 0 && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={() => setSelectedCollections([])}
>
<T>Clear</T>
</Button>
)}
</div>
<div className="flex flex-wrap gap-2">
{collections.map(collection => (
<Badge
key={collection.id}
variant={selectedCollections.includes(collection.id) ? "default" : "outline"}
className="cursor-pointer hover:shadow-md transition-shadow"
onClick={() => {
setSelectedCollections(prev =>
prev.includes(collection.id)
? prev.filter(c => c !== collection.id)
: [...prev, collection.id]
);
}}
>
{collection.name}
{selectedCollections.includes(collection.id) && (
<X className="ml-1 h-3 w-3" />
)}
</Badge>
))}
</div>
</div>
)}
</div>
{/* Pictures Grid */}
<div className="flex-1 overflow-y-auto min-h-0 -mx-6 px-6">
{loading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : finalPictures.length === 0 ? (
<div className="text-center py-12">
<ImageIcon className="h-12 w-12 mx-auto mb-4 text-muted-foreground opacity-50" />
<p className="text-muted-foreground">
<T>{searchQuery || selectedTags.length > 0 || selectedCollections.length > 0 ? 'No pictures found' : 'No pictures available'}</T>
</p>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-1">
{finalPictures.map((picture) => {
const isSelected = multiple
? selectedIds.includes(picture.id)
: selectedId === picture.id;
return (
<div
key={picture.id}
onClick={() => toggleSelection(picture.id)}
onDoubleClick={() => {
if (!multiple) {
setSelectedId(picture.id);
if (onSelect) onSelect(picture.id);
if (onSelectPicture) onSelectPicture(picture);
} else {
toggleSelection(picture.id);
}
}}
className={`relative cursor-pointer rounded-lg overflow-hidden border-2 transition-all group ${isSelected
? 'border-primary shadow-lg scale-[1.02]'
: 'border-transparent hover:border-primary/50'
}`}
>
<div className="aspect-square relative pointer-events-none">
<MediaCard
id={picture.id}
pictureId={picture.id}
url={picture.image_url}
thumbnailUrl={picture.thumbnail_url}
title={picture.title}
author={'Me'}
authorId={picture.user_id}
likes={0}
comments={0}
type={picture.type as MediaType}
meta={picture.meta}
showContent={false}
onClick={() => { }}
onLike={undefined}
onDelete={undefined}
onEdit={undefined}
/>
</div>
{/* Overlay Title */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-2 pointer-events-none">
<p className="text-white text-xs font-medium truncate">
{picture.title}
</p>
</div>
</div>
)
})}
</div>
)}
</div>
{/* Actions */}
<div className="flex justify-between items-center pt-4 border-t mt-auto">
<div className="text-sm text-muted-foreground">
{multiple && <span>{selectedIds.length} selected</span>}
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={onClose}>
<T>Cancel</T>
</Button>
<Button onClick={handleSelect} disabled={multiple ? selectedIds.length === 0 : !selectedId}>
<T>Select Picture{multiple ? 's' : ''}</T>
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};