Files
app/app/documents/page.tsx
T

217 lines
8.8 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';
const IconMap: Record<string, any> = {
"Map": Map,
"MapPlus": MapPlus,
"WavesLadder": WavesLadder,
"MapPinned": MapPinned,
"BookCopy": BookCopy,
"FileText": FileText
};
const fetchFileSize = async (url: string) => {
try {
const res = await fetch(url, { method: 'HEAD' });
const bytes = res.headers.get('content-length');
if (bytes) {
const mb = (parseInt(bytes) / (1024 * 1024)).toFixed(2);
return `${mb} MB`;
}
} catch (error) {
console.error("Kunde inte hämta filstorlek för", url);
}
return "Okänd";
};
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 docsWithSizes = await Promise.all(
(res.data.docs || []).map(async (doc: DocumentItem) => {
if (!doc.size || doc.size === "") {
const calculatedSize = await fetchFileSize(doc.file);
return { ...doc, size: calculatedSize };
}
return doc;
})
);
setDocs(docsWithSizes);
setLinks(res.data.links || []);
const dataToCache = { ...res.data, docs: docsWithSizes };
localStorage.setItem('kullaberg_documents_cache', JSON.stringify(dataToCache));
checkCacheStatus(docsWithSizes);
}
} 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>
);
}