@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Private Media Edit Modal
|
||||
*
|
||||
* Edit metadata for a single media item
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
X,
|
||||
Save,
|
||||
Calendar,
|
||||
Tag,
|
||||
FileText,
|
||||
Loader2,
|
||||
Image as ImageIcon,
|
||||
Video,
|
||||
} from 'lucide-react'
|
||||
import { api } from '../../lib/api'
|
||||
import PersonSelector from './PersonSelector'
|
||||
|
||||
interface MediaItem {
|
||||
id: number
|
||||
filename: string
|
||||
description?: string | null
|
||||
file_type: 'image' | 'video' | 'other'
|
||||
person_id?: number | null
|
||||
media_date: string
|
||||
tags: Array<{ id: number; name: string; color: string }>
|
||||
thumbnail_url: string
|
||||
}
|
||||
|
||||
interface EditModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
item: MediaItem | null
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export function PrivateMediaEditModal({ open, onClose, item, onSuccess }: EditModalProps) {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const [personId, setPersonId] = useState<number | undefined>()
|
||||
const [selectedTags, setSelectedTags] = useState<number[]>([])
|
||||
const [mediaDate, setMediaDate] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
|
||||
// Load item data when it changes
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
setPersonId(item.person_id ?? undefined)
|
||||
setSelectedTags(item.tags.map(t => t.id))
|
||||
setMediaDate(item.media_date)
|
||||
setDescription(item.description || '')
|
||||
}
|
||||
}, [item])
|
||||
|
||||
const { data: tags = [] } = useQuery({
|
||||
queryKey: ['private-gallery-tags'],
|
||||
queryFn: () => api.privateGallery.getTags(),
|
||||
enabled: open,
|
||||
})
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: {
|
||||
description?: string
|
||||
person_id?: number
|
||||
media_date?: string
|
||||
tag_ids?: number[]
|
||||
}) => api.privateGallery.updateMedia(item!.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['private-gallery-media'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['private-gallery-albums'] })
|
||||
handleClose()
|
||||
onSuccess?.()
|
||||
},
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
setPersonId(undefined)
|
||||
setSelectedTags([])
|
||||
setMediaDate('')
|
||||
setDescription('')
|
||||
updateMutation.reset()
|
||||
onClose()
|
||||
}
|
||||
|
||||
const toggleTag = (tagId: number) => {
|
||||
setSelectedTags(prev =>
|
||||
prev.includes(tagId)
|
||||
? prev.filter(id => id !== tagId)
|
||||
: [...prev, tagId]
|
||||
)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (selectedTags.length === 0) {
|
||||
alert('Please select at least one tag')
|
||||
return
|
||||
}
|
||||
|
||||
updateMutation.mutate({
|
||||
description: description || undefined,
|
||||
person_id: personId || 0, // 0 means clear
|
||||
media_date: mediaDate,
|
||||
tag_ids: selectedTags,
|
||||
})
|
||||
}
|
||||
|
||||
if (!open || !item) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="w-full max-w-lg bg-card rounded-xl shadow-2xl border border-border max-h-[90vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 sm:px-6 py-4 border-b border-border">
|
||||
<h2 className="text-lg font-semibold">Edit Media</h2>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="p-2 rounded-lg hover:bg-secondary transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 sm:p-6 space-y-6">
|
||||
{/* Preview */}
|
||||
<div className="flex items-center gap-4 p-4 rounded-lg bg-secondary/50">
|
||||
<div className="w-16 h-16 rounded-lg overflow-hidden bg-secondary flex-shrink-0">
|
||||
{item.file_type === 'image' ? (
|
||||
<img
|
||||
src={item.thumbnail_url}
|
||||
alt={item.filename}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : item.file_type === 'video' ? (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<Video className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<ImageIcon className="w-6 h-6 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium truncate">{item.filename}</p>
|
||||
<p className="text-sm text-muted-foreground capitalize">{item.file_type}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Person Selector */}
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium mb-2">
|
||||
Person
|
||||
</label>
|
||||
<PersonSelector
|
||||
value={personId}
|
||||
onChange={(id) => setPersonId(id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium mb-2">
|
||||
<Tag className="w-4 h-4" />
|
||||
<span className="text-red-500">*</span>
|
||||
Tags
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map(tag => (
|
||||
<button
|
||||
key={tag.id}
|
||||
type="button"
|
||||
onClick={() => toggleTag(tag.id)}
|
||||
className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${
|
||||
selectedTags.includes(tag.id)
|
||||
? 'text-white'
|
||||
: 'bg-secondary text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
style={selectedTags.includes(tag.id) ? { backgroundColor: tag.color } : undefined}
|
||||
>
|
||||
{tag.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium mb-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={mediaDate}
|
||||
onChange={(e) => setMediaDate(e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium mb-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-4 py-2.5 rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary resize-none"
|
||||
placeholder="Add a description..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex flex-col-reverse sm:flex-row sm:items-center sm:justify-end gap-3 px-4 sm:px-6 py-4 border-t border-border">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="w-full sm:w-auto px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={updateMutation.isPending || selectedTags.length === 0}
|
||||
className="w-full sm:w-auto px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
{updateMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-4 h-4" />
|
||||
Save Changes
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PrivateMediaEditModal
|
||||
Reference in New Issue
Block a user