Optimized PDF's and better offline file handeling
This commit is contained in:
+147
-34
@@ -1,15 +1,23 @@
|
|||||||
// app/documents/page.tsx
|
// 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() {
|
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 = [
|
const docs = [
|
||||||
{ title: 'Turistkarta', icon: <Map size={24} />, file: '/files/Turistkarta - Kullaberg.pdf', size: '2.51 MB' },
|
{ 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: '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: '7.07 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: '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: '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 2026.pptx', size: '24.2 MB' }
|
{ title: 'Destinationskunskap Kullahalvön', icon: <BookCopy size={24} />, file: '/files/Destinationskunskap.pdf', size: '1.97 MB' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
@@ -20,50 +28,155 @@ 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' }
|
{ 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 (
|
return (
|
||||||
<div className="space-y-8 animate-fade-in w-full">
|
<div className="space-y-8 animate-fade-in w-full">
|
||||||
<div>
|
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
|
||||||
<h1 className="text-3xl font-black text-slate-teal tracking-tight flex items-center">
|
<div>
|
||||||
<Folder className="mr-3 text-seafoam" size={32} />
|
<h1 className="text-3xl font-black text-slate-teal tracking-tight flex items-center">
|
||||||
Info & Dokument
|
<Folder className="mr-3 text-seafoam" size={32} />
|
||||||
</h1>
|
Info & Dokument
|
||||||
<p className="text-moss font-medium mt-2 ml-11">Allt du behöver för att guida besökarna.</p>
|
</h1>
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-sm font-black text-ebony/50 uppercase tracking-widest mb-4">Nedladdningar</h2>
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{docs.map((doc, idx) => (
|
{docs.map((doc, idx) => {
|
||||||
<a key={idx} href={doc.file} target="_blank" rel="noopener noreferrer"
|
const isCached = cachedFiles.has(doc.file);
|
||||||
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"
|
|
||||||
>
|
return (
|
||||||
<div className="flex items-center text-slate-teal">
|
<a key={idx} href={doc.file} target="_blank" rel="noopener noreferrer"
|
||||||
<div className="bg-linear-to-br from-seafoam to-slate-teal p-3 rounded-xl mr-4 text-eggshell shadow-inner shrink-0">
|
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'}`}
|
||||||
{doc.icon}
|
>
|
||||||
|
<div className="flex items-center text-slate-teal">
|
||||||
|
{/* 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>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-bold text-sm leading-tight">{doc.title}</span>
|
{/* FIX: Swap the Download icon for a Check icon when it is already saved on the phone */}
|
||||||
<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">
|
{isCached ? (
|
||||||
{doc.size}
|
<CheckCircle size={20} className="text-moss shrink-0 ml-2" />
|
||||||
</span>
|
) : (
|
||||||
</div>
|
<Download size={20} className="text-slate-teal/50 group-hover:text-slate-teal transition-colors shrink-0 ml-2" />
|
||||||
</div>
|
)}
|
||||||
<Download size={20} className="text-moss group-hover:text-slate-teal transition-colors shrink-0 ml-2" />
|
</a>
|
||||||
</a>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{links.map((link, idx) => (
|
{links.map((link, idx) => (
|
||||||
<a key={idx} href={link.url} target="_blank" rel="noopener noreferrer"
|
<a key={idx} href={isOffline ? '#' : link.url} target={isOffline ? '_self' : '_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 ? '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>
|
<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">
|
<div className={`rounded-full p-2 transition-colors shrink-0 ${isOffline ? 'bg-ebony/5' : 'bg-eggshell group-hover:bg-seafoam'}`}>
|
||||||
<ExternalLink size={16} className="text-slate-teal group-hover:text-eggshell" />
|
<ExternalLink size={16} className={isOffline ? 'text-ebony/40' : 'text-slate-teal group-hover:text-eggshell'} />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
@@ -71,4 +184,4 @@ export default function Documents() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
@@ -21,19 +21,10 @@ const serwist = new Serwist({
|
|||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
matcher: ({ url }) => url.pathname.startsWith("/files/"),
|
matcher: ({ url }) => url.pathname.startsWith("/files/"),
|
||||||
handler: async ({ request, event }) => {
|
handler: new CacheFirst({
|
||||||
const conn = navigator.connection;
|
cacheName: "kullaberg-files-cache",
|
||||||
const isStrictWifi = conn && (conn.type === 'wifi' || conn.type === 'ethernet');
|
plugins: [new ExpirationPlugin({ maxEntries: 20, maxAgeSeconds: 2592000 })],
|
||||||
const shouldCache = isStrictWifi && !conn?.saveData;
|
})
|
||||||
if (shouldCache) {
|
|
||||||
const cacheStrategy = new CacheFirst({
|
|
||||||
cacheName: "kullaberg-files-cache",
|
|
||||||
plugins: [new ExpirationPlugin({ maxEntries: 20, maxAgeSeconds: 2592000 })],
|
|
||||||
});
|
|
||||||
return cacheStrategy.handle({ request, event });
|
|
||||||
}
|
|
||||||
return fetch(request);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
...defaultCache,
|
...defaultCache,
|
||||||
],
|
],
|
||||||
|
|||||||
+3
-1
@@ -12,8 +12,10 @@ const withSerwist = withSerwistInit({
|
|||||||
cacheOnNavigation: true,
|
cacheOnNavigation: true,
|
||||||
additionalPrecacheEntries: [
|
additionalPrecacheEntries: [
|
||||||
{ url: "/", revision: manualRevision },
|
{ url: "/", revision: manualRevision },
|
||||||
{ url: "/emergency", revision: manualRevision },
|
{ url: "/schedule", revision: manualRevision },
|
||||||
{ url: "/faq", 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