Dynamic data files

This commit is contained in:
2026-03-18 15:29:05 +01:00 Verified
parent d61f2af6d4
commit 2814a287fb
20 changed files with 272 additions and 186 deletions
+60 -38
View File
@@ -2,52 +2,69 @@
"use client";
import { BookCopy, CheckCircle, CloudDownload, Folder, Loader2, Map, MapPinned, MapPlus, WavesLadder } from 'lucide-react';
import { BookCopy, CheckCircle, CloudDownload, FileText, Folder, Loader2, Map, MapPinned, MapPlus, WavesLadder } from 'lucide-react';
import { useEffect, useState } from 'react';
import { DocumentCard, ExternalLinkCard, DocumentItem, LinkItem } from '../components/ui/Cards';
import { readJsonFile } from '../actions/jsonEditor';
import { DocumentCard, DocumentItem, ExternalLinkCard, LinkItem } from '../components/ui/Cards';
import { OfflineBadge } from '../components/ui/OfflineBadge';
import { PageHeader } from '../components/ui/PageHeader';
const IconMap: Record<string, any> = {
"Map": Map,
"MapPlus": MapPlus,
"WavesLadder": WavesLadder,
"MapPinned": MapPinned,
"BookCopy": BookCopy,
"FileText": FileText
};
export default function Documents() {
const [isHydrated, setIsHydrated] = useState(false);
const [isOffline, setIsOffline] = useState(false);
const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'done'>('idle');
const [cachedFiles, setCachedFiles] = useState<Set<string>>(new Set());
const [showNotification, setShowNotification] = useState(false);
const [docs, setDocs] = useState<DocumentItem[]>([]);
const [links, setLinks] = useState<LinkItem[]>([]);
const [isLoadingData, setIsLoadingData] = useState(true);
const docs: DocumentItem[] = [
{ title: 'Turistkarta', icon: <Map size={20} />, file: '/files/Turistkarta - Kullaberg.pdf', size: '2.51 MB' },
{ title: 'Orienteringskarta', icon: <MapPlus size={20} />, file: '/files/Orienteringskarta - Kullaberg.pdf', size: '3.74 MB' },
{ title: 'Badplatser att besöka', icon: <WavesLadder size={20} />, file: '/files/Badplatser på Kullaberg.pdf', size: '1.88 MB' },
{ title: 'Vandringsrutter', icon: <MapPinned size={20} />, file: '/files/Vandringsrutter.pdf', size: '4.42 MB' },
{ title: 'Underlag för guidediplomering', icon: <BookCopy size={20} />, file: '/files/Underlag för guidediplomering.pdf', size: '3.18 MB' },
{ title: 'Destinationskunskap Kullahalvön', icon: <BookCopy size={20} />, file: '/files/Destinationskunskap.pdf', size: '1.97 MB' }
];
const links: LinkItem[] = [
{ title: 'Kullabergs Naturreservat', url: 'https://www.kullabergsnatur.se/' },
{ title: 'Vandra på Kullahalvön', url: 'https://www.kullahalvon.com/upptacka--uppleva/friluftsliv--natur/vandra-pa-kullahalvon.html' },
{ title: 'Naturkartan', url: 'https://www.naturkartan.se/en/explore' },
{ title: 'Skåneleden', url: 'https://www.skaneleden.se/en' },
{ title: 'Väderprognos Mölle', url: 'https://www.smhi.se/vader/prognoser-och-varningar/vaderprognos/q/H%C3%B6gan%C3%A4s/M%C3%B6lle/2691501' }
];
const checkCacheStatus = async () => {
if (!('caches' in window)) return;
const checkCacheStatus = async (currentDocs: DocumentItem[]) => {
if (!('caches' in window) || currentDocs.length === 0) return;
const cached = new Set<string>();
try {
for (const doc of docs) {
for (const doc of currentDocs) {
const response = await caches.match(doc.file);
if (response) cached.add(doc.file);
}
setCachedFiles(cached);
if (cached.size === docs.length) setSyncStatus('done');
if (cached.size === currentDocs.length) setSyncStatus('done');
else setSyncStatus('idle');
} catch (error) {
console.error("Cache check failed", error);
}
};
const loadDynamicData = async () => {
if (navigator.onLine) {
const res = await readJsonFile('documents.json');
if (res.success && res.data) {
setDocs(res.data.docs || []);
setLinks(res.data.links || []);
localStorage.setItem('kullaberg_documents_cache', JSON.stringify(res.data));
checkCacheStatus(res.data.docs || []);
}
} else {
const cachedData = localStorage.getItem('kullaberg_documents_cache');
if (cachedData) {
const parsed = JSON.parse(cachedData);
setDocs(parsed.docs || []);
setLinks(parsed.links || []);
checkCacheStatus(parsed.docs || []);
}
}
setIsLoadingData(false);
};
useEffect(() => {
setIsHydrated(true);
setIsOffline(!navigator.onLine);
@@ -56,13 +73,13 @@ export default function Documents() {
window.addEventListener('online', handleStatus);
window.addEventListener('offline', handleStatus);
checkCacheStatus();
loadDynamicData();
const handleVisibility = () => {
if (document.visibilityState === 'visible') checkCacheStatus();
if (document.visibilityState === 'visible') checkCacheStatus(docs);
};
document.addEventListener('visibilitychange', handleVisibility);
window.addEventListener('focus', checkCacheStatus);
window.addEventListener('focus', () => checkCacheStatus(docs));
const conn = (navigator as any).connection;
if (conn && (conn.type === 'wifi' || conn.type === 'ethernet') && !conn.saveData) {
@@ -73,11 +90,12 @@ export default function Documents() {
window.removeEventListener('online', handleStatus);
window.removeEventListener('offline', handleStatus);
document.removeEventListener('visibilitychange', handleVisibility);
window.removeEventListener('focus', checkCacheStatus);
window.removeEventListener('focus', () => checkCacheStatus(docs));
};
}, []);
const handleSyncAll = async () => {
if (docs.length === 0) return;
setSyncStatus('syncing');
const updatedCache = new Set(cachedFiles);
@@ -96,13 +114,13 @@ export default function Documents() {
}
}
await checkCacheStatus();
await checkCacheStatus(docs);
setSyncStatus('done');
setShowNotification(true);
setTimeout(() => setShowNotification(false), 4000);
};
if (!isHydrated) return <div className="flex justify-center py-20"><Loader2 className="animate-spin text-slate-teal" size={40} /></div>;
if (!isHydrated || isLoadingData) return <div className="flex justify-center py-20"><Loader2 className="animate-spin text-slate-teal" size={40} /></div>;
return (
<div className="space-y-8 animate-fade-in w-full relative">
@@ -113,7 +131,7 @@ export default function Documents() {
actions={
<>
{isOffline && <OfflineBadge />}
{!isOffline && syncStatus !== 'done' && (
{!isOffline && syncStatus !== 'done' && docs.length > 0 && (
<button
onClick={handleSyncAll}
disabled={syncStatus === 'syncing'}
@@ -130,14 +148,18 @@ export default function Documents() {
<section>
<h2 className="text-sm font-black text-ebony/50 uppercase tracking-widest mb-4">Filer</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{docs.map((doc, idx) => (
<DocumentCard
key={idx}
doc={doc}
isOffline={isOffline}
isCached={cachedFiles.has(doc.file)}
/>
))}
{docs.map((doc, idx) => {
const IconComponent = IconMap[doc.icon as string] || FileText;
const renderDoc = { ...doc, icon: <IconComponent size={20} /> };
return (
<DocumentCard
key={idx}
doc={renderDoc}
isOffline={isOffline}
isCached={cachedFiles.has(doc.file)}
/>
);
})}
</div>
</section>