mirror of
https://github.com/azaion/ui.git
synced 2026-04-22 15:26:34 +00:00
Refactor project structure and dependencies; rename package to azaion-ui, update version to 0.0.1, and remove unused files. Introduce new routing and authentication features in App component.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFlight } from '../../components/FlightContext'
|
||||
import { api } from '../../api/client'
|
||||
import { useDebounce } from '../../hooks/useDebounce'
|
||||
import ConfirmDialog from '../../components/ConfirmDialog'
|
||||
import type { Media, PaginatedResponse, AnnotationListItem } from '../../types'
|
||||
|
||||
interface Props {
|
||||
selectedMedia: Media | null
|
||||
onSelect: (m: Media) => void
|
||||
onAnnotationsLoaded: (anns: AnnotationListItem[]) => void
|
||||
}
|
||||
|
||||
export default function MediaList({ selectedMedia, onSelect, onAnnotationsLoaded }: Props) {
|
||||
const { t } = useTranslation()
|
||||
const { selectedFlight } = useFlight()
|
||||
const [media, setMedia] = useState<Media[]>([])
|
||||
const [filter, setFilter] = useState('')
|
||||
const debouncedFilter = useDebounce(filter, 300)
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
||||
const fetchMedia = useCallback(async () => {
|
||||
const params = new URLSearchParams({ pageSize: '1000' })
|
||||
if (selectedFlight) params.set('flightId', selectedFlight.id)
|
||||
if (debouncedFilter) params.set('name', debouncedFilter)
|
||||
try {
|
||||
const res = await api.get<PaginatedResponse<Media>>(`/api/annotations/media?${params}`)
|
||||
setMedia(res.items)
|
||||
} catch {}
|
||||
}, [selectedFlight, debouncedFilter])
|
||||
|
||||
useEffect(() => { fetchMedia() }, [fetchMedia])
|
||||
|
||||
const handleSelect = async (m: Media) => {
|
||||
onSelect(m)
|
||||
try {
|
||||
const res = await api.get<PaginatedResponse<AnnotationListItem>>(
|
||||
`/api/annotations/annotations?mediaId=${m.id}&pageSize=1000`
|
||||
)
|
||||
onAnnotationsLoaded(res.items)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteId) return
|
||||
await api.delete(`/api/annotations/media/${deleteId}`)
|
||||
setDeleteId(null)
|
||||
fetchMedia()
|
||||
}
|
||||
|
||||
const handleDrop = async (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragging(false)
|
||||
if (!selectedFlight || !e.dataTransfer.files.length) return
|
||||
const form = new FormData()
|
||||
form.append('waypointId', '')
|
||||
for (const file of e.dataTransfer.files) form.append('files', file)
|
||||
await api.upload('/api/annotations/media/batch', form)
|
||||
fetchMedia()
|
||||
}
|
||||
|
||||
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files?.length) return
|
||||
const form = new FormData()
|
||||
form.append('waypointId', '')
|
||||
for (const file of e.target.files) form.append('files', file)
|
||||
await api.upload('/api/annotations/media/batch', form)
|
||||
fetchMedia()
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex-1 flex flex-col overflow-hidden ${dragging ? 'ring-2 ring-az-orange ring-inset' : ''}`}
|
||||
onDragOver={e => { e.preventDefault(); setDragging(true) }}
|
||||
onDragLeave={() => setDragging(false)}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className="p-2 border-b border-az-border flex gap-1">
|
||||
<input
|
||||
value={filter}
|
||||
onChange={e => setFilter(e.target.value)}
|
||||
placeholder={t('annotations.mediaList')}
|
||||
className="flex-1 bg-az-bg border border-az-border rounded px-2 py-1 text-xs text-az-text outline-none"
|
||||
/>
|
||||
<label className="bg-az-orange text-white text-xs px-2 py-1 rounded cursor-pointer">
|
||||
↑
|
||||
<input type="file" multiple className="hidden" onChange={handleFileUpload} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{media.map(m => (
|
||||
<div
|
||||
key={m.id}
|
||||
onClick={() => handleSelect(m)}
|
||||
onContextMenu={e => { e.preventDefault(); setDeleteId(m.id) }}
|
||||
className={`px-2 py-1 cursor-pointer border-b border-az-border text-xs flex items-center gap-1.5 ${
|
||||
selectedMedia?.id === m.id ? 'bg-az-bg text-white' : ''
|
||||
} ${m.annotationCount > 0 ? 'bg-az-bg/50' : ''} text-az-text hover:bg-az-bg`}
|
||||
>
|
||||
<span className={`font-mono text-[10px] px-1 rounded ${m.mediaType === 2 ? 'bg-az-blue/20 text-az-blue' : 'bg-az-green/20 text-az-green'}`}>
|
||||
{m.mediaType === 2 ? 'V' : 'P'}
|
||||
</span>
|
||||
<span className="truncate flex-1">{m.name}</span>
|
||||
{m.duration && <span className="text-az-muted">{m.duration}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
open={!!deleteId}
|
||||
title={t('annotations.deleteMedia')}
|
||||
onConfirm={handleDelete}
|
||||
onCancel={() => setDeleteId(null)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user