This commit is contained in:
114
frontend/src/components/data/DataView.tsx
Normal file
114
frontend/src/components/data/DataView.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { useSignal } from '@preact/signals';
|
||||
import { fetchJson } from '../../lib/api';
|
||||
import { getCachedData, setCachedData } from '../../lib/db';
|
||||
import { useToast } from '../../hooks/useToast';
|
||||
|
||||
type LoadState = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
export interface DataViewProps {
|
||||
endpoint: string;
|
||||
cacheKey: string;
|
||||
initialData: unknown[] | null;
|
||||
initialError?: string | null;
|
||||
}
|
||||
|
||||
export default function DataView({
|
||||
endpoint,
|
||||
cacheKey,
|
||||
initialData,
|
||||
initialError = null,
|
||||
}: DataViewProps) {
|
||||
const toast = useToast();
|
||||
const itemsSignal = useSignal<unknown[]>([]);
|
||||
const stateSignal = useSignal<LoadState>('idle');
|
||||
const sourceSignal = useSignal<'ssr' | 'network' | 'cache' | null>(null);
|
||||
const errorSignal = useSignal<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(initialData) && initialData.length) {
|
||||
itemsSignal.value = initialData;
|
||||
stateSignal.value = 'success';
|
||||
sourceSignal.value = 'ssr';
|
||||
} else if (initialError) {
|
||||
errorSignal.value = initialError;
|
||||
stateSignal.value = 'error';
|
||||
} else {
|
||||
stateSignal.value = 'loading';
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
// SSR gives first paint; hydration then refreshes client data and persists it offline.
|
||||
async function refreshData() {
|
||||
try {
|
||||
stateSignal.value = 'loading';
|
||||
const payload = await fetchJson<unknown[]>(endpoint, {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
timeoutMs: 12000,
|
||||
});
|
||||
|
||||
itemsSignal.value = Array.isArray(payload) ? payload : [];
|
||||
await setCachedData(cacheKey, itemsSignal.value);
|
||||
sourceSignal.value = 'network';
|
||||
errorSignal.value = null;
|
||||
stateSignal.value = 'success';
|
||||
toast.success('Podaci su uspješno osvježeni.');
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cached = await getCachedData<unknown[]>(cacheKey);
|
||||
if (cached?.payload) {
|
||||
itemsSignal.value = Array.isArray(cached.payload) ? cached.payload : [];
|
||||
sourceSignal.value = 'cache';
|
||||
errorSignal.value = null;
|
||||
stateSignal.value = 'success';
|
||||
toast.warning('Prikazani su lokalno spremljeni podaci (offline fallback).');
|
||||
return;
|
||||
}
|
||||
|
||||
errorSignal.value = error instanceof Error ? error.message : 'Data loading failed.';
|
||||
stateSignal.value = 'error';
|
||||
toast.error('Neuspješno učitavanje podataka i nema lokalnog cachea.');
|
||||
}
|
||||
}
|
||||
|
||||
void refreshData();
|
||||
return () => controller.abort();
|
||||
}, [endpoint, cacheKey, initialData, initialError]);
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-border-hairline bg-canvas-elevated p-5 shadow-sm">
|
||||
<header className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-text-main">DataView</h2>
|
||||
<span className="text-xs text-text-muted">
|
||||
Source: {sourceSignal.value || 'unknown'} • State: {stateSignal.value}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
{stateSignal.value === 'loading' && (
|
||||
<p className="text-sm text-text-muted">Učitavanje podataka...</p>
|
||||
)}
|
||||
|
||||
{stateSignal.value === 'error' && (
|
||||
<p className="rounded-lg border border-red-300 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
{errorSignal.value || 'Greška pri učitavanju.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{stateSignal.value === 'success' && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-muted">
|
||||
Ukupno zapisa: <strong className="text-text-main">{itemsSignal.value.length}</strong>
|
||||
</p>
|
||||
<pre className="max-h-96 overflow-auto rounded-lg border border-border-hairline bg-canvas-base p-3 text-xs text-text-main">
|
||||
{JSON.stringify(itemsSignal.value.slice(0, 20), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user