- **refactor(main):** migrate static HTML to React components
Some checks failed
CI / build-test (push) Successful in 1m25s
CI / docker (push) Failing after 1s

- **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:
2026-02-25 20:37:15 +01:00
parent 54478dcd4f
commit 6094edc5c8
40 changed files with 2395 additions and 99 deletions

View 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 }
}

View 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 }
}