Compare commits
@@ -1,10 +1,14 @@
|
|||||||
// app/actions/files.ts
|
// app/actions/files.ts
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import crypto from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import fsPromises from 'fs/promises';
|
import fsPromises from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import crypto from 'crypto';
|
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
const DATA_DIR = isProd ? "/app/data" : path.join(process.cwd(), "app/data");
|
||||||
|
const FILES_DIR = path.join(DATA_DIR, "files");
|
||||||
|
|
||||||
const generateFileHash = (filePath: string): Promise<string> => {
|
const generateFileHash = (filePath: string): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -19,15 +23,15 @@ const generateFileHash = (filePath: string): Promise<string> => {
|
|||||||
|
|
||||||
export async function getLocalFileMeta(fileUrl: string) {
|
export async function getLocalFileMeta(fileUrl: string) {
|
||||||
try {
|
try {
|
||||||
const cleanPath = fileUrl.startsWith('/') ? fileUrl.substring(1) : fileUrl;
|
const cleanName = decodeURIComponent(fileUrl.split('/').pop() || '');
|
||||||
const fullPath = path.join(process.cwd(), 'public', cleanPath);
|
const fullPath = path.join(FILES_DIR, cleanName);
|
||||||
const stats = await fsPromises.stat(fullPath);
|
const stats = await fsPromises.stat(fullPath);
|
||||||
const sizeMb = (stats.size / (1024 * 1024)).toFixed(2) + ' MB';
|
const sizeMb = (stats.size / (1024 * 1024)).toFixed(2) + ' MB';
|
||||||
const fileHash = await generateFileHash(fullPath);
|
const fileHash = await generateFileHash(fullPath);
|
||||||
|
|
||||||
return { size: sizeMb, version: fileHash };
|
return { size: sizeMb, version: fileHash };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Kunde inte läsa filen: ${fileUrl}`, error);
|
console.error(`Kunde inte läsa filen (actions): ${fileUrl}`, error);
|
||||||
return { size: "Okänd", version: "v1" };
|
return { size: "Okänd", version: "v1" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,8 +30,8 @@ export function DocumentCard({ doc, isOffline, isCached }: DocumentCardProps) {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={`group flex items-center justify-between p-4 rounded-2xl border transition-colors ${isOffline && !isCached
|
className={`group flex items-center justify-between p-4 rounded-2xl border transition-colors ${isOffline && !isCached
|
||||||
? 'opacity-50 grayscale cursor-not-allowed pointer-events-none border-transparent bg-eggshell/50'
|
? 'opacity-50 grayscale cursor-not-allowed pointer-events-none border-transparent bg-eggshell/50'
|
||||||
: 'bg-white/50 hover:bg-white border-slate-teal/10'
|
: 'bg-white/50 hover:bg-white border-slate-teal/10'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center text-slate-teal">
|
<div className="flex items-center text-slate-teal">
|
||||||
@@ -74,8 +74,8 @@ export function ExternalLinkCard({ link, isOffline }: ExternalLinkCardProps) {
|
|||||||
target={isOffline ? '_self' : '_blank'}
|
target={isOffline ? '_self' : '_blank'}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={`group flex items-center justify-between p-4 rounded-2xl border transition-colors ${isOffline
|
className={`group flex items-center justify-between p-4 rounded-2xl border transition-colors ${isOffline
|
||||||
? 'opacity-40 grayscale cursor-not-allowed pointer-events-none bg-eggshell/50 border-transparent'
|
? 'opacity-40 grayscale cursor-not-allowed pointer-events-none bg-eggshell/50 border-transparent'
|
||||||
: 'bg-white/50 hover:bg-white border-slate-teal/10'
|
: 'bg-white/50 hover:bg-white border-slate-teal/10'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|||||||
+20
-6
@@ -3,32 +3,42 @@
|
|||||||
{
|
{
|
||||||
"title": "Turistkarta",
|
"title": "Turistkarta",
|
||||||
"icon": "Map",
|
"icon": "Map",
|
||||||
"file": "/files/Turistkarta - Kullaberg.pdf"
|
"file": "Turistkarta - Kullaberg.pdf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Orienteringskarta",
|
"title": "Orienteringskarta",
|
||||||
"icon": "MapPlus",
|
"icon": "MapPlus",
|
||||||
"file": "/files/Orienteringskarta - Kullaberg.pdf"
|
"file": "Orienteringskarta - Kullaberg.pdf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Badplatser att besöka",
|
"title": "Badplatser att besöka",
|
||||||
"icon": "WavesLadder",
|
"icon": "WavesLadder",
|
||||||
"file": "/files/Badplatser på Kullaberg.pdf"
|
"file": "Badplatser på Kullaberg.pdf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Vandringsrutter",
|
"title": "Vandringsrutter",
|
||||||
"icon": "MapPinned",
|
"icon": "MapPinned",
|
||||||
"file": "/files/Vandringsrutter.pdf"
|
"file": "Vandringsrutter.pdf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Underlag för guidediplomering",
|
"title": "Underlag för guidediplomering",
|
||||||
"icon": "BookCopy",
|
"icon": "BookCopy",
|
||||||
"file": "/files/Underlag för guidediplomering.pdf"
|
"file": "Underlag för guidediplomering.pdf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Destinationskunskap Kullahalvön",
|
"title": "Destinationskunskap Kullahalvön",
|
||||||
"icon": "BookCopy",
|
"icon": "BookCopy",
|
||||||
"file": "/files/Destinationskunskap.pdf"
|
"file": "Destinationskunskap.pdf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Tjänstgöringsrapport",
|
||||||
|
"icon": "BookCopy",
|
||||||
|
"file": "Tjänstgöringsrapport 2026.pdf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Information om ditt sommarjobb",
|
||||||
|
"icon": "BookCopy",
|
||||||
|
"file": "Information om ditt sommarjobb 2026.pdf"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [
|
"links": [
|
||||||
@@ -48,6 +58,10 @@
|
|||||||
"title": "Skåneleden",
|
"title": "Skåneleden",
|
||||||
"url": "https://www.skaneleden.se/en"
|
"url": "https://www.skaneleden.se/en"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "RSNV Brandriskprognos",
|
||||||
|
"url": "https://rsnv.se/brandriskprognos/"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Väderprognos Mölle",
|
"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"
|
"url": "https://www.smhi.se/vader/prognoser-och-varningar/vaderprognos/q/H%C3%B6gan%C3%A4s/M%C3%B6lle/2691501"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
-22
@@ -19,27 +19,6 @@ const IconMap: Record<string, any> = {
|
|||||||
"FileText": FileText
|
"FileText": FileText
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFileMeta = async (url: string) => {
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, { method: 'HEAD', cache: 'no-store' });
|
|
||||||
const bytes = res.headers.get('content-length');
|
|
||||||
const lastModified = res.headers.get('last-modified');
|
|
||||||
const etag = res.headers.get('etag');
|
|
||||||
|
|
||||||
let sizeStr = "Okänd";
|
|
||||||
if (bytes) {
|
|
||||||
const mb = (parseInt(bytes) / (1024 * 1024)).toFixed(2);
|
|
||||||
sizeStr = `${mb} MB`;
|
|
||||||
}
|
|
||||||
const version = etag ? etag.replace(/"/g, '') :
|
|
||||||
(lastModified ? new Date(lastModified).getTime().toString() : 'v1');
|
|
||||||
|
|
||||||
return { size: sizeStr, version };
|
|
||||||
} catch (error) {
|
|
||||||
return { size: "Okänd", version: "v1" };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Documents() {
|
export default function Documents() {
|
||||||
const [isHydrated, setIsHydrated] = useState(false);
|
const [isHydrated, setIsHydrated] = useState(false);
|
||||||
const [isOffline, setIsOffline] = useState(false);
|
const [isOffline, setIsOffline] = useState(false);
|
||||||
@@ -73,7 +52,8 @@ export default function Documents() {
|
|||||||
const docsWithMeta = await Promise.all(
|
const docsWithMeta = await Promise.all(
|
||||||
(res.data.docs || []).map(async (doc: DocumentItem) => {
|
(res.data.docs || []).map(async (doc: DocumentItem) => {
|
||||||
const meta = await getLocalFileMeta(doc.file);
|
const meta = await getLocalFileMeta(doc.file);
|
||||||
const versionedUrl = `${doc.file}?v=${meta.version}`;
|
const fileNameOnly = doc.file.split('/').pop();
|
||||||
|
const versionedUrl = `/files/${fileNameOnly}?v=${meta.version}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...doc,
|
...doc,
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// app/files/[...slug]/route.ts
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const isProd = process.env.NODE_ENV === "production";
|
||||||
|
const DATA_DIR = isProd ? "/app/data" : path.join(process.cwd(), "app/data");
|
||||||
|
const FILES_DIR = path.join(DATA_DIR, "files");
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ slug: string[] }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const resolvedParams = await params;
|
||||||
|
const filename = decodeURIComponent(resolvedParams.slug[resolvedParams.slug.length - 1]);
|
||||||
|
const filePath = path.join(FILES_DIR, filename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
console.error(`404 - Filen finns inte på disk: ${filePath}`);
|
||||||
|
return new NextResponse('Filen hittades inte', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileBuffer = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
let contentType = 'application/pdf';
|
||||||
|
if (filename.toLowerCase().endsWith('.png')) contentType = 'image/png';
|
||||||
|
else if (filename.toLowerCase().endsWith('.jpg') || filename.toLowerCase().endsWith('.jpeg')) contentType = 'image/jpeg';
|
||||||
|
else if (filename.toLowerCase().endsWith('.doc') || filename.toLowerCase().endsWith('.docx')) contentType = 'application/msword';
|
||||||
|
|
||||||
|
return new NextResponse(fileBuffer, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'Content-Disposition': `inline; filename="${filename}"`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Fel vid fildelning:", error);
|
||||||
|
return new NextResponse('Ett internt serverfel uppstod', { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ const nextConfig: NextConfig = {
|
|||||||
output: "standalone",
|
output: "standalone",
|
||||||
allowedDevOrigins: [
|
allowedDevOrigins: [
|
||||||
'10.10.0.121',
|
'10.10.0.121',
|
||||||
|
'10.11.0.122',
|
||||||
'localhost',
|
'localhost',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user