Optimized PDF's and better offline file handeling
This commit is contained in:
+130
-17
@@ -1,15 +1,23 @@
|
||||
// app/documents/page.tsx
|
||||
|
||||
import { BookCopy, Download, ExternalLink, Folder, Map, MapPinned, MapPlus, WavesLadder } from 'lucide-react';
|
||||
"use client";
|
||||
|
||||
import { BookCopy, CheckCircle, CloudDownload, Download, ExternalLink, Folder, Loader2, Map, MapPinned, MapPlus, WavesLadder, WifiOff, Archive } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
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 docs = [
|
||||
{ title: 'Turistkarta', icon: <Map size={24} />, file: '/files/Turistkarta - Kullaberg.pdf', size: '2.51 MB' },
|
||||
{ title: 'Orienteringskarta', icon: <MapPlus size={24} />, file: '/files/Orienteringskarta - Kullaberg.pdf', size: '7.23 MB' },
|
||||
{ title: 'Badplatser att besöka', icon: <WavesLadder size={24} />, file: '/files/Badplatser på Kullaberg.pdf', size: '7.07 MB' },
|
||||
{ title: 'Orienteringskarta', icon: <MapPlus size={24} />, file: '/files/Orienteringskarta - Kullaberg.pdf', size: '3.74 MB' },
|
||||
{ title: 'Badplatser att besöka', icon: <WavesLadder size={24} />, file: '/files/Badplatser på Kullaberg.pdf', size: '1.88 MB' },
|
||||
{ title: 'Vandringsrutter', icon: <MapPinned size={24} />, file: '/files/Vandringsrutter.pdf', size: '4.42 MB' },
|
||||
{ title: 'Underlag för guidediplomering', icon: <BookCopy size={24} />, file: '/files/Underlag för guidediplomering.pptx', size: '3.83 MB' },
|
||||
{ title: 'Destinationskunskap Kullahalvön', icon: <BookCopy size={24} />, file: '/files/Destinationskunskap 2026.pptx', size: '24.2 MB' }
|
||||
{ title: 'Underlag för guidediplomering', icon: <BookCopy size={24} />, file: '/files/Underlag för guidediplomering.pdf', size: '3.18 MB' },
|
||||
{ title: 'Destinationskunskap Kullahalvön', icon: <BookCopy size={24} />, file: '/files/Destinationskunskap.pdf', size: '1.97 MB' }
|
||||
];
|
||||
|
||||
const links = [
|
||||
@@ -20,8 +28,66 @@ export default function Documents() {
|
||||
{ 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 cached = new Set<string>();
|
||||
try {
|
||||
for (const doc of docs) {
|
||||
const response = await caches.match(doc.file);
|
||||
if (response) cached.add(doc.file);
|
||||
}
|
||||
setCachedFiles(cached);
|
||||
if (cached.size === docs.length) setSyncStatus('done');
|
||||
} catch (error) {
|
||||
console.error("Cache check failed", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsHydrated(true);
|
||||
setIsOffline(!navigator.onLine);
|
||||
|
||||
const handleStatus = () => setIsOffline(!navigator.onLine);
|
||||
window.addEventListener('online', handleStatus);
|
||||
window.addEventListener('offline', handleStatus);
|
||||
|
||||
checkCacheStatus();
|
||||
|
||||
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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSyncAll = async () => {
|
||||
setSyncStatus('syncing');
|
||||
for (const doc of docs) {
|
||||
try {
|
||||
await fetch(doc.file);
|
||||
} catch (error) {
|
||||
console.error("Kunde inte ladda ner:", doc.file);
|
||||
}
|
||||
}
|
||||
await checkCacheStatus();
|
||||
setSyncStatus('done');
|
||||
};
|
||||
|
||||
if (!isHydrated) {
|
||||
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">
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black text-slate-teal tracking-tight flex items-center">
|
||||
<Folder className="mr-3 text-seafoam" size={32} />
|
||||
@@ -30,40 +96,87 @@ export default function Documents() {
|
||||
<p className="text-moss font-medium mt-2 ml-11">Allt du behöver för att guida besökarna.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 items-start md:items-end">
|
||||
{isOffline && (
|
||||
<div className="bg-goldenrod/10 text-goldenrod border border-goldenrod/20 px-3 py-1.5 rounded-full flex items-center text-xs font-black uppercase tracking-tighter">
|
||||
<WifiOff size={14} className="mr-2" /> Offline-läge
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isOffline && syncStatus !== 'done' && (
|
||||
<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änglig offline'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{syncStatus === 'done' && (
|
||||
<div className="flex items-center gap-2 text-moss font-bold text-sm bg-moss/10 border border-moss/20 px-4 py-2 rounded-lg">
|
||||
<CheckCircle size={18} /> Redo för offline
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 className="text-sm font-black text-ebony/50 uppercase tracking-widest mb-4">Nedladdningar</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{docs.map((doc, idx) => (
|
||||
{docs.map((doc, idx) => {
|
||||
const isCached = cachedFiles.has(doc.file);
|
||||
|
||||
return (
|
||||
<a key={idx} href={doc.file} target="_blank" rel="noopener noreferrer"
|
||||
className="group flex items-center justify-between bg-white/60 backdrop-blur-sm p-5 rounded-2xl border border-white hover:-translate-y-1 hover:shadow-xl hover:shadow-moss/10 transition-all duration-300"
|
||||
className={`group flex items-center justify-between p-5 rounded-2xl border transition-all duration-300 ${isOffline && !isCached ? 'opacity-50 grayscale cursor-not-allowed pointer-events-none bg-white/20 border-transparent' : 'bg-white/60 border-white hover:-translate-y-1 hover:shadow-xl hover:shadow-moss/10 backdrop-blur-sm'}`}
|
||||
>
|
||||
<div className="flex items-center text-slate-teal">
|
||||
<div className="bg-linear-to-br from-seafoam to-slate-teal p-3 rounded-xl mr-4 text-eggshell shadow-inner shrink-0">
|
||||
{/* FIX: Keep the original icon, but change the background to green when cached */}
|
||||
<div className={`p-3 rounded-xl mr-4 text-eggshell shadow-inner shrink-0 transition-colors ${isCached ? 'bg-moss' : 'bg-linear-to-br from-seafoam to-slate-teal'}`}>
|
||||
{doc.icon}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-sm leading-tight">{doc.title}</span>
|
||||
<span className="text-[10px] font-bold text-moss bg-moss/10 px-2 py-0.5 rounded-md w-fit mt-1.5 uppercase tracking-wider border border-moss/10">
|
||||
<div className="flex flex-wrap items-center gap-2 mt-1.5">
|
||||
<span className="text-[10px] font-bold text-moss bg-moss/10 px-2 py-0.5 rounded-md uppercase tracking-wider border border-moss/10">
|
||||
{doc.size}
|
||||
</span>
|
||||
|
||||
{isCached && (
|
||||
<span className="flex items-center gap-1 text-[10px] font-bold text-moss bg-moss/10 px-2 py-0.5 rounded-md uppercase tracking-wider">
|
||||
<Archive size={12} /> Nerladdad
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Download size={20} className="text-moss group-hover:text-slate-teal transition-colors shrink-0 ml-2" />
|
||||
</div>
|
||||
|
||||
{/* FIX: Swap the Download icon for a Check icon when it is already saved on the phone */}
|
||||
{isCached ? (
|
||||
<CheckCircle size={20} className="text-moss shrink-0 ml-2" />
|
||||
) : (
|
||||
<Download size={20} className="text-slate-teal/50 group-hover:text-slate-teal transition-colors shrink-0 ml-2" />
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-sm font-black text-ebony/50 uppercase tracking-widest mb-4">Användbara Länkar</h2>
|
||||
<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-4">
|
||||
{links.map((link, idx) => (
|
||||
<a key={idx} href={link.url} target="_blank" rel="noopener noreferrer"
|
||||
className="group flex items-center justify-between bg-white/60 backdrop-blur-sm p-5 rounded-2xl border border-white hover:-translate-y-1 hover:shadow-xl hover:shadow-moss/10 transition-all duration-300"
|
||||
<a key={idx} href={isOffline ? '#' : link.url} target={isOffline ? '_self' : '_blank'} rel="noopener noreferrer"
|
||||
className={`group flex items-center justify-between p-5 rounded-2xl border transition-all duration-300 ${isOffline ? 'opacity-40 grayscale cursor-not-allowed pointer-events-none bg-white/20 border-transparent' : 'bg-white/60 border-white hover:-translate-y-1 hover:shadow-xl hover:shadow-moss/10 backdrop-blur-sm'}`}
|
||||
>
|
||||
<span className="font-bold text-slate-teal text-sm leading-tight pr-4">{link.title}</span>
|
||||
<div className="bg-eggshell rounded-full p-2 group-hover:bg-seafoam group-hover:text-eggshell transition-colors shrink-0">
|
||||
<ExternalLink size={16} className="text-slate-teal group-hover:text-eggshell" />
|
||||
<div className={`rounded-full p-2 transition-colors shrink-0 ${isOffline ? 'bg-ebony/5' : 'bg-eggshell group-hover:bg-seafoam'}`}>
|
||||
<ExternalLink size={16} className={isOffline ? 'text-ebony/40' : 'text-slate-teal group-hover:text-eggshell'} />
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
@@ -71,4 +184,4 @@ export default function Documents() {
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -21,19 +21,10 @@ const serwist = new Serwist({
|
||||
runtimeCaching: [
|
||||
{
|
||||
matcher: ({ url }) => url.pathname.startsWith("/files/"),
|
||||
handler: async ({ request, event }) => {
|
||||
const conn = navigator.connection;
|
||||
const isStrictWifi = conn && (conn.type === 'wifi' || conn.type === 'ethernet');
|
||||
const shouldCache = isStrictWifi && !conn?.saveData;
|
||||
if (shouldCache) {
|
||||
const cacheStrategy = new CacheFirst({
|
||||
handler: new CacheFirst({
|
||||
cacheName: "kullaberg-files-cache",
|
||||
plugins: [new ExpirationPlugin({ maxEntries: 20, maxAgeSeconds: 2592000 })],
|
||||
});
|
||||
return cacheStrategy.handle({ request, event });
|
||||
}
|
||||
return fetch(request);
|
||||
}
|
||||
})
|
||||
},
|
||||
...defaultCache,
|
||||
],
|
||||
|
||||
+3
-1
@@ -12,8 +12,10 @@ const withSerwist = withSerwistInit({
|
||||
cacheOnNavigation: true,
|
||||
additionalPrecacheEntries: [
|
||||
{ url: "/", revision: manualRevision },
|
||||
{ url: "/emergency", revision: manualRevision },
|
||||
{ url: "/schedule", revision: manualRevision },
|
||||
{ url: "/faq", revision: manualRevision },
|
||||
{ url: "/info", revision: manualRevision },
|
||||
{ url: "/emergency", revision: manualRevision },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user