209 lines
8.6 KiB
TypeScript
209 lines
8.6 KiB
TypeScript
// app/documents/page.tsx
|
|
|
|
"use client";
|
|
|
|
import { BookCopy, CheckCircle, CloudDownload, FileText, Folder, Loader2, Map, MapPinned, MapPlus, WavesLadder } from 'lucide-react';
|
|
import { useEffect, useState } from 'react';
|
|
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';
|
|
import { getLocalFileMeta } from '../actions/files';
|
|
|
|
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 checkCacheStatus = async (currentDocs: DocumentItem[]) => {
|
|
if (!('caches' in window) || currentDocs.length === 0) return;
|
|
const cached = new Set<string>();
|
|
try {
|
|
for (const doc of currentDocs) {
|
|
const response = await caches.match(doc.file);
|
|
if (response) cached.add(doc.file);
|
|
}
|
|
setCachedFiles(cached);
|
|
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) {
|
|
const docsWithMeta = await Promise.all(
|
|
(res.data.docs || []).map(async (doc: DocumentItem) => {
|
|
const meta = await getLocalFileMeta(doc.file);
|
|
const fileNameOnly = doc.file.split('/').pop();
|
|
const versionedUrl = `/files/${fileNameOnly}?v=${meta.version}`;
|
|
|
|
return {
|
|
...doc,
|
|
size: (!doc.size || doc.size === "Okänd") ? meta.size : doc.size,
|
|
originalFile: doc.file,
|
|
file: versionedUrl
|
|
};
|
|
})
|
|
);
|
|
|
|
setDocs(docsWithMeta);
|
|
setLinks(res.data.links || []);
|
|
const dataToCache = { ...res.data, docs: docsWithMeta };
|
|
localStorage.setItem('kullaberg_documents_cache', JSON.stringify(dataToCache));
|
|
|
|
checkCacheStatus(docsWithMeta);
|
|
}
|
|
} 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);
|
|
|
|
const handleStatus = () => setIsOffline(!navigator.onLine);
|
|
window.addEventListener('online', handleStatus);
|
|
window.addEventListener('offline', handleStatus);
|
|
|
|
loadDynamicData();
|
|
|
|
const handleVisibility = () => {
|
|
if (document.visibilityState === 'visible') checkCacheStatus(docs);
|
|
};
|
|
document.addEventListener('visibilitychange', handleVisibility);
|
|
window.addEventListener('focus', () => checkCacheStatus(docs));
|
|
|
|
const conn = (navigator as any).connection;
|
|
if (conn && (conn.type === 'wifi' || conn.type === 'ethernet') && !conn.saveData) {
|
|
handleSyncAll();
|
|
}
|
|
|
|
return () => {
|
|
window.removeEventListener('online', handleStatus);
|
|
window.removeEventListener('offline', handleStatus);
|
|
document.removeEventListener('visibilitychange', handleVisibility);
|
|
window.removeEventListener('focus', () => checkCacheStatus(docs));
|
|
};
|
|
}, []);
|
|
|
|
const handleSyncAll = async () => {
|
|
if (docs.length === 0) return;
|
|
setSyncStatus('syncing');
|
|
const updatedCache = new Set(cachedFiles);
|
|
|
|
for (const doc of docs) {
|
|
if (!updatedCache.has(doc.file)) {
|
|
try {
|
|
const response = await fetch(doc.file);
|
|
if (response.ok) {
|
|
await response.blob();
|
|
updatedCache.add(doc.file);
|
|
setCachedFiles(new Set(updatedCache));
|
|
}
|
|
} catch (error) {
|
|
console.error("Kunde inte ladda ner:", doc.file);
|
|
}
|
|
}
|
|
}
|
|
|
|
await checkCacheStatus(docs);
|
|
setSyncStatus('done');
|
|
setShowNotification(true);
|
|
setTimeout(() => setShowNotification(false), 4000);
|
|
};
|
|
|
|
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">
|
|
<PageHeader
|
|
title="Info & Dokument"
|
|
icon={Folder}
|
|
description="Allt du behöver för att guida besökarna."
|
|
actions={
|
|
<>
|
|
{isOffline && <OfflineBadge />}
|
|
{!isOffline && syncStatus !== 'done' && docs.length > 0 && (
|
|
<button
|
|
onClick={handleSyncAll}
|
|
disabled={syncStatus === 'syncing'}
|
|
className="flex items-center gap-2 bg-seafoam/10 text-slate-teal border border-seafoam/20 hover:bg-seafoam/20 px-4 py-2 rounded-lg font-bold text-sm transition-colors"
|
|
>
|
|
{syncStatus === 'syncing' ? <Loader2 size={18} className="animate-spin" /> : <CloudDownload size={18} />}
|
|
{syncStatus === 'syncing' ? 'Laddar ner filer...' : 'Gör tillgängliga offline'}
|
|
</button>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
|
|
<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) => {
|
|
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>
|
|
|
|
<section>
|
|
<h2 className="text-sm font-black text-ebony/50 uppercase tracking-widest mb-4">
|
|
{isOffline ? "Användbara Länkar (Kräver anslutning)" : "Användbara Länkar"}
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
{links.map((link, idx) => (
|
|
<ExternalLinkCard
|
|
key={idx}
|
|
link={link}
|
|
isOffline={isOffline}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{showNotification && (
|
|
<div className="fixed bottom-6 right-6 z-50 animate-fade-in flex items-center gap-3 bg-white text-moss font-bold text-sm border border-moss/20 shadow-xl px-4 py-3 rounded-xl">
|
|
<div className="bg-moss/10 p-1 rounded-full">
|
|
<CheckCircle size={20} className="text-moss" />
|
|
</div>
|
|
Alla filer är redo för offline
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |