- **refactor(main):** migrate static HTML to React components
- **feat(ui):** implement `AcknowledgeButton` component for acknowledging images - **feat(stats):** add dashboard stats for total images, pending updates, and acknowledged status - **chore(deps):** introduce `bun` dependency management and add required libraries - **style(ui):** enhance UI with Tailwind-based components and modularity improvements - **chore:** add drag-and-drop tag assignment using `@dnd-kit/core`
This commit is contained in:
50
frontend/src/hooks/useTags.ts
Normal file
50
frontend/src/hooks/useTags.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import type { Tag } from '@/types/diun'
|
||||
|
||||
export function useTags() {
|
||||
const [tags, setTags] = useState<Tag[]>([])
|
||||
|
||||
const fetchTags = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/tags')
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const data: Tag[] = await res.json()
|
||||
setTags(data)
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch tags:', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchTags()
|
||||
}, [fetchTags])
|
||||
|
||||
const createTag = useCallback(async (name: string): Promise<Tag | null> => {
|
||||
try {
|
||||
const res = await fetch('/api/tags', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const tag: Tag = await res.json()
|
||||
setTags(prev => [...prev, tag].sort((a, b) => a.name.localeCompare(b.name)))
|
||||
return tag
|
||||
} catch (e) {
|
||||
console.error('Failed to create tag:', e)
|
||||
return null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const deleteTag = useCallback(async (id: number) => {
|
||||
try {
|
||||
const res = await fetch(`/api/tags/${id}`, { method: 'DELETE' })
|
||||
if (!res.ok && res.status !== 404) throw new Error(`HTTP ${res.status}`)
|
||||
setTags(prev => prev.filter(t => t.id !== id))
|
||||
} catch (e) {
|
||||
console.error('Failed to delete tag:', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { tags, createTag, deleteTag }
|
||||
}
|
||||
87
frontend/src/hooks/useUpdates.ts
Normal file
87
frontend/src/hooks/useUpdates.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import type { Tag, UpdatesMap } from '@/types/diun'
|
||||
|
||||
const POLL_INTERVAL = 5000
|
||||
|
||||
export function useUpdates() {
|
||||
const [updates, setUpdates] = useState<UpdatesMap>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null)
|
||||
const [secondsUntilRefresh, setSecondsUntilRefresh] = useState(POLL_INTERVAL / 1000)
|
||||
|
||||
const fetchUpdates = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/updates')
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
const data: UpdatesMap = await res.json()
|
||||
setUpdates(data)
|
||||
setError(null)
|
||||
setLastRefreshed(new Date())
|
||||
setSecondsUntilRefresh(POLL_INTERVAL / 1000)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to fetch updates')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchUpdates()
|
||||
const pollTimer = setInterval(fetchUpdates, POLL_INTERVAL)
|
||||
return () => clearInterval(pollTimer)
|
||||
}, [fetchUpdates])
|
||||
|
||||
useEffect(() => {
|
||||
const countdownTimer = setInterval(() => {
|
||||
setSecondsUntilRefresh(s => Math.max(0, s - 1))
|
||||
}, 1000)
|
||||
return () => clearInterval(countdownTimer)
|
||||
}, [])
|
||||
|
||||
const acknowledge = useCallback(async (image: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/updates/${encodeURIComponent(image)}`, {
|
||||
method: 'PATCH',
|
||||
})
|
||||
if (!res.ok && res.status !== 404) {
|
||||
throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
setUpdates(prev => {
|
||||
const entry = prev[image]
|
||||
if (!entry) return prev
|
||||
return { ...prev, [image]: { ...entry, acknowledged: true } }
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Acknowledge failed:', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const assignTag = useCallback(async (image: string, tag: Tag | null) => {
|
||||
// Optimistic update
|
||||
setUpdates(prev => {
|
||||
const entry = prev[image]
|
||||
if (!entry) return prev
|
||||
return { ...prev, [image]: { ...entry, tag } }
|
||||
})
|
||||
try {
|
||||
if (tag === null) {
|
||||
await fetch('/api/tag-assignments', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image }),
|
||||
})
|
||||
} else {
|
||||
await fetch('/api/tag-assignments', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image, tag_id: tag.id }),
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('assignTag failed:', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { updates, loading, error, lastRefreshed, secondsUntilRefresh, fetchUpdates, acknowledge, assignTag }
|
||||
}
|
||||
Reference in New Issue
Block a user