Dynamic data files
This commit is contained in:
+60
-38
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user