252 lines
8.0 KiB
TypeScript
252 lines
8.0 KiB
TypeScript
/**
|
|
* 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
|