12 Commits
20 changed files with 305 additions and 126 deletions
+41
View File
@@ -0,0 +1,41 @@
name: Publish Docker image
on:
release:
types: [published]
jobs:
push_to_registry:
name: Build and Push Docker image
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: git.stws.cc
username: ${{ github.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: git.stws.cc/${{ github.repository }}
tags: |
type=ref,event=tag
type=raw,value=latest
- name: Build and push Docker images
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
-24
View File
@@ -1,24 +0,0 @@
stages:
- Build
"Build Docker":
stage: Build
image: docker:24.0.5
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin
script:
- echo "Building Docker-image..."
- >
docker build
--label "org.opencontainers.image.url=$CI_PROJECT_URL"
--label "org.opencontainers.image.source=$CI_PROJECT_URL"
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
-t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_COMMIT_TAG
+37
View File
@@ -0,0 +1,37 @@
// app/actions/files.ts
'use server';
import crypto from 'crypto';
import fs from 'fs';
import fsPromises from 'fs/promises';
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");
const generateFileHash = (filePath: string): Promise<string> => {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md5');
const stream = fs.createReadStream(filePath);
stream.on('error', (err) => reject(err));
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex').substring(0, 8)));
});
};
export async function getLocalFileMeta(fileUrl: string) {
try {
const cleanName = decodeURIComponent(fileUrl.split('/').pop() || '');
const fullPath = path.join(FILES_DIR, cleanName);
const stats = await fsPromises.stat(fullPath);
const sizeMb = (stats.size / (1024 * 1024)).toFixed(2) + ' MB';
const fileHash = await generateFileHash(fullPath);
return { size: sizeMb, version: fileHash };
} catch (error) {
console.error(`Kunde inte läsa filen (actions): ${fileUrl}`, error);
return { size: "Okänd", version: "v1" };
}
}
+54 -5
View File
@@ -19,27 +19,76 @@ export const NoticeTab = ({ isOffline }: { isOffline: boolean }) => {
const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle'); const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle');
useEffect(() => { useEffect(() => {
let isMounted = true;
const fetchNotice = async () => { const fetchNotice = async () => {
const res = await readJsonFile('notice.json'); const cached = localStorage.getItem('admin_notice_cache');
if (res.success && res.data) { if (cached) {
setNotice(res.data); setNotice(JSON.parse(cached));
}
setIsLoading(false); setIsLoading(false);
}
const fetchWithTimeout = new Promise<any>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Timeout')), 5000);
readJsonFile('notice.json').then(res => {
clearTimeout(timer);
resolve(res);
}).catch(err => {
clearTimeout(timer);
reject(err);
});
});
try {
if (navigator.onLine) {
const res = await fetchWithTimeout;
if (res.success && res.data && isMounted) {
setNotice(res.data);
localStorage.setItem('admin_notice_cache', JSON.stringify(res.data));
}
}
} catch (error) {
console.warn("Kunde inte hämta färsk notice.json (Liar-Fi), använder cache.");
} finally {
if (isMounted) setIsLoading(false);
}
}; };
fetchNotice(); fetchNotice();
return () => { isMounted = false; };
}, []); }, []);
const handleSave = async () => { const handleSave = async () => {
setIsSaving(true); setIsSaving(true);
setSaveStatus('idle'); setSaveStatus('idle');
const res = await writeJsonFile('notice.json', notice);
try {
const saveWithTimeout = new Promise<any>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Timeout')), 8000);
writeJsonFile('notice.json', notice).then(res => {
clearTimeout(timer);
resolve(res);
}).catch(err => {
clearTimeout(timer);
reject(err);
});
});
const res = await saveWithTimeout;
if (res.success) { if (res.success) {
setSaveStatus('success'); setSaveStatus('success');
localStorage.setItem('admin_notice_cache', JSON.stringify(notice));
setTimeout(() => setSaveStatus('idle'), 3000); setTimeout(() => setSaveStatus('idle'), 3000);
} else { } else {
setSaveStatus('error'); setSaveStatus('error');
} }
} catch (error) {
console.error("Liar-Fi: Sparande tog för lång tid", error);
setSaveStatus('error');
} finally {
setIsSaving(false); setIsSaving(false);
}
}; };
if (isLoading) return <div className="flex justify-center py-10"><Loader2 className="animate-spin text-slate-teal" size={32} /></div>; if (isLoading) return <div className="flex justify-center py-10"><Loader2 className="animate-spin text-slate-teal" size={32} /></div>;
+46 -27
View File
@@ -12,7 +12,7 @@ export const useAdminState = () => {
const [periods, setPeriods] = useState<Period[]>([]); const [periods, setPeriods] = useState<Period[]>([]);
const [attendance, setAttendance] = useState<AttendanceDataMap>({}); const [attendance, setAttendance] = useState<AttendanceDataMap>({});
const [scheduleData, setScheduleData] = useState<any[]>([]); const [scheduleData, setScheduleData] = useState<any[]>([]);
const [dailyLogs, setDailyLogs] = useState<Record<string, string>>({}); // NYTT const [dailyLogs, setDailyLogs] = useState<Record<string, string>>({});
const [activePeriodId, setActivePeriodId] = useState<string>(''); const [activePeriodId, setActivePeriodId] = useState<string>('');
const [isLoadingData, setIsLoadingData] = useState<boolean>(true); const [isLoadingData, setIsLoadingData] = useState<boolean>(true);
@@ -56,7 +56,6 @@ export const useAdminState = () => {
return nextAttendance; return nextAttendance;
}; };
// NYTT: Hantera offfline-kö för dagböcker
const applyQueueToLogs = async (baseLogs: Record<string, string>) => { const applyQueueToLogs = async (baseLogs: Record<string, string>) => {
const queue = await localforage.getItem<any[]>('sync-queue') || []; const queue = await localforage.getItem<any[]>('sync-queue') || [];
const nextLogs = { ...baseLogs }; const nextLogs = { ...baseLogs };
@@ -85,20 +84,57 @@ export const useAdminState = () => {
const refreshData = useCallback(async (isInitialLoad = false) => { const refreshData = useCallback(async (isInitialLoad = false) => {
if (!isInitialLoad) setIsSyncing(true); if (!isInitialLoad) setIsSyncing(true);
if (isInitialLoad) {
try {
const cachedSched = await localforage.getItem<any[]>('cachedScheduleData');
const cachedData = await localforage.getItem<any[]>('cachedAdminData');
const cachedLogs = await localforage.getItem<Record<string, string>>('cachedDailyLogs');
if (cachedData) {
const { loadedPeriods, loadedAttendance } = mapDbDataToUI(cachedData);
const finalAttendance = await applyQueueToAttendance(loadedAttendance);
const finalLogs = await applyQueueToLogs(cachedLogs || {});
if (cachedSched) setScheduleData(cachedSched);
setPeriods(loadedPeriods);
setAttendance(finalAttendance);
setDailyLogs(finalLogs);
if (loadedPeriods.length > 0) {
const today = toIsoDate(new Date());
const activePeriods = loadedPeriods.filter(p => today >= p.startDate && today <= p.endDate);
if (activePeriods.length > 0) setActivePeriodId(activePeriods[0].id);
else setActivePeriodId(loadedPeriods[0].id);
}
setIsLoadingData(false);
}
} catch (e) {
console.warn("Kunde inte ladda lokal cache", e);
}
}
const withTimeout = <T>(promise: Promise<T>, ms: number = 5000): Promise<T> => {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('NETWORK_TIMEOUT')), ms);
promise
.then(val => { clearTimeout(timer); resolve(val); })
.catch(err => { clearTimeout(timer); reject(err); });
});
};
try { try {
if (navigator.onLine) await flushOfflineQueue(); if (navigator.onLine) await flushOfflineQueue();
const schedRes = await readJsonFile('schedule.json'); const schedRes = await withTimeout(readJsonFile('schedule.json'));
if (schedRes.success && schedRes.data) { if (schedRes.success && schedRes.data) {
setScheduleData(schedRes.data); setScheduleData(schedRes.data);
await localforage.setItem('cachedScheduleData', schedRes.data); await localforage.setItem('cachedScheduleData', schedRes.data);
} }
const dbPeriods = await getAdminData(); const dbPeriods = await withTimeout(getAdminData());
await localforage.setItem('cachedAdminData', dbPeriods); await localforage.setItem('cachedAdminData', dbPeriods);
// NYTT: Ladda in loggarna const dbLogs = await withTimeout(getDailyLogsDb());
const dbLogs = await getDailyLogsDb();
const mappedLogs: Record<string, string> = {}; const mappedLogs: Record<string, string> = {};
dbLogs.forEach((l: { date: string | number; content: string; }) => mappedLogs[l.date] = l.content); dbLogs.forEach((l: { date: string | number; content: string; }) => mappedLogs[l.date] = l.content);
await localforage.setItem('cachedDailyLogs', mappedLogs); await localforage.setItem('cachedDailyLogs', mappedLogs);
@@ -113,35 +149,20 @@ export const useAdminState = () => {
setAttendance(finalAttendance); setAttendance(finalAttendance);
setDailyLogs(finalLogs); setDailyLogs(finalLogs);
if (isInitialLoad && loadedPeriods.length > 0) { if (isInitialLoad && periods.length === 0 && loadedPeriods.length > 0) {
const today = toIsoDate(new Date()); const today = toIsoDate(new Date());
const activePeriods = loadedPeriods.filter(p => today >= p.startDate && today <= p.endDate); const activePeriods = loadedPeriods.filter(p => today >= p.startDate && today <= p.endDate);
if (activePeriods.length > 0) setActivePeriodId(activePeriods[0].id); if (activePeriods.length > 0) setActivePeriodId(activePeriods[0].id);
else setActivePeriodId(loadedPeriods[0].id); else setActivePeriodId(loadedPeriods[0].id);
} }
} catch (error) { } catch (error) {
console.warn("Nätverkstimeout eller offline - faller tillbaka på cache.", error);
setIsOffline(true); setIsOffline(true);
const cachedSched = await localforage.getItem<any[]>('cachedScheduleData');
if (cachedSched) setScheduleData(cachedSched);
const cachedData = await localforage.getItem<any[]>('cachedAdminData');
if (cachedData) {
const { loadedPeriods, loadedAttendance } = mapDbDataToUI(cachedData);
const finalAttendance = await applyQueueToAttendance(loadedAttendance);
setPeriods(loadedPeriods);
setAttendance(finalAttendance);
const cachedLogs = await localforage.getItem<Record<string, string>>('cachedDailyLogs') || {};
const finalLogs = await applyQueueToLogs(cachedLogs);
setDailyLogs(finalLogs);
if (isInitialLoad && loadedPeriods.length > 0) setActivePeriodId(loadedPeriods[0].id);
}
} finally { } finally {
setIsLoadingData(false); setIsLoadingData(false);
setIsSyncing(false); setIsSyncing(false);
} }
}, [flushOfflineQueue]); }, [flushOfflineQueue, periods.length]);
useEffect(() => { refreshData(true); }, [refreshData]); useEffect(() => { refreshData(true); }, [refreshData]);
@@ -162,7 +183,6 @@ export const useAdminState = () => {
const addToOfflineQueue = async (action: any) => { const addToOfflineQueue = async (action: any) => {
const queue = await localforage.getItem<any[]>('sync-queue') || []; const queue = await localforage.getItem<any[]>('sync-queue') || [];
// Undvik dubbletter för just detta datum och denna åtgärd
let filteredQueue = queue; let filteredQueue = queue;
if (action.type === 'SET_DAILY_LOG') { if (action.type === 'SET_DAILY_LOG') {
filteredQueue = queue.filter(q => !(q.type === 'SET_DAILY_LOG' && q.payload.date === action.payload.date)); filteredQueue = queue.filter(q => !(q.type === 'SET_DAILY_LOG' && q.payload.date === action.payload.date));
@@ -242,7 +262,6 @@ export const useAdminState = () => {
else await removeAttendanceDb(date, youthId, shiftId).catch(async () => { await addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } }); }); else await removeAttendanceDb(date, youthId, shiftId).catch(async () => { await addToOfflineQueue({ type: 'REMOVE_ATTENDANCE', payload: { date, youthId, shiftId } }); });
}; };
// NYTT: Funktion för att spara dagbok lokalt och i databasen
const setDailyLog = async (date: string, content: string) => { const setDailyLog = async (date: string, content: string) => {
setDailyLogs(prev => ({ ...prev, [date]: content })); setDailyLogs(prev => ({ ...prev, [date]: content }));
@@ -257,7 +276,7 @@ export const useAdminState = () => {
return { return {
periods, attendance, activePeriodId, setActivePeriodId, isLoadingData, isOffline, isSyncing, scheduleData, periods, attendance, activePeriodId, setActivePeriodId, isLoadingData, isOffline, isSyncing, scheduleData,
dailyLogs, setDailyLog, // <-- Expotera här dailyLogs, setDailyLog,
createPeriod, deletePeriod, bulkAddYouth, removeYouth, createPeriod, deletePeriod, bulkAddYouth, removeYouth,
setManualAttendance, bulkSetManualAttendance, addPendingAttendance, removeAttendanceEntry, refreshData setManualAttendance, bulkSetManualAttendance, addPendingAttendance, removeAttendanceEntry, refreshData
}; };
+20 -6
View File
@@ -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.
+15 -23
View File
@@ -8,6 +8,7 @@ import { readJsonFile } from '../actions/jsonEditor';
import { DocumentCard, DocumentItem, ExternalLinkCard, LinkItem } from '../components/ui/Cards'; import { DocumentCard, DocumentItem, ExternalLinkCard, LinkItem } from '../components/ui/Cards';
import { OfflineBadge } from '../components/ui/OfflineBadge'; import { OfflineBadge } from '../components/ui/OfflineBadge';
import { PageHeader } from '../components/ui/PageHeader'; import { PageHeader } from '../components/ui/PageHeader';
import { getLocalFileMeta } from '../actions/files';
const IconMap: Record<string, any> = { const IconMap: Record<string, any> = {
"Map": Map, "Map": Map,
@@ -18,20 +19,6 @@ const IconMap: Record<string, any> = {
"FileText": FileText "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() { export default function Documents() {
const [isHydrated, setIsHydrated] = useState(false); const [isHydrated, setIsHydrated] = useState(false);
const [isOffline, setIsOffline] = useState(false); const [isOffline, setIsOffline] = useState(false);
@@ -62,22 +49,27 @@ export default function Documents() {
if (navigator.onLine) { if (navigator.onLine) {
const res = await readJsonFile('documents.json'); const res = await readJsonFile('documents.json');
if (res.success && res.data) { if (res.success && res.data) {
const docsWithSizes = await Promise.all( const docsWithMeta = await Promise.all(
(res.data.docs || []).map(async (doc: DocumentItem) => { (res.data.docs || []).map(async (doc: DocumentItem) => {
if (!doc.size || doc.size === "") { const meta = await getLocalFileMeta(doc.file);
const calculatedSize = await fetchFileSize(doc.file); const fileNameOnly = doc.file.split('/').pop();
return { ...doc, size: calculatedSize }; const versionedUrl = `/files/${fileNameOnly}?v=${meta.version}`;
}
return doc; return {
...doc,
size: (!doc.size || doc.size === "Okänd") ? meta.size : doc.size,
originalFile: doc.file,
file: versionedUrl
};
}) })
); );
setDocs(docsWithSizes); setDocs(docsWithMeta);
setLinks(res.data.links || []); setLinks(res.data.links || []);
const dataToCache = { ...res.data, docs: docsWithSizes }; const dataToCache = { ...res.data, docs: docsWithMeta };
localStorage.setItem('kullaberg_documents_cache', JSON.stringify(dataToCache)); localStorage.setItem('kullaberg_documents_cache', JSON.stringify(dataToCache));
checkCacheStatus(docsWithSizes); checkCacheStatus(docsWithMeta);
} }
} else { } else {
const cachedData = localStorage.getItem('kullaberg_documents_cache'); const cachedData = localStorage.getItem('kullaberg_documents_cache');
+42
View File
@@ -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 });
}
}
+36 -28
View File
@@ -26,20 +26,43 @@ export default function Home() {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
let isMounted = true;
const loadNotice = async () => { const loadNotice = async () => {
const cached = localStorage.getItem('kullaberg_notice_cache');
if (cached) {
setNotice(JSON.parse(cached));
setIsLoading(false);
}
const fetchWithTimeout = new Promise<any>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Timeout')), 5000);
readJsonFile('notice.json').then(res => {
clearTimeout(timer);
resolve(res);
}).catch(err => {
clearTimeout(timer);
reject(err);
});
});
try {
if (navigator.onLine) { if (navigator.onLine) {
const res = await readJsonFile('notice.json'); const res = await fetchWithTimeout;
if (res.success && res.data) { if (res.success && res.data && isMounted) {
setNotice(res.data); setNotice(res.data);
localStorage.setItem('kullaberg_notice_cache', JSON.stringify(res.data)); localStorage.setItem('kullaberg_notice_cache', JSON.stringify(res.data));
} }
} else {
const cached = localStorage.getItem('kullaberg_notice_cache');
if (cached) setNotice(JSON.parse(cached));
} }
setIsLoading(false); } catch (error) {
console.warn("Kunde inte hämta nytt meddelande (Liar-Fi), behåller cache.");
} finally {
if (isMounted) setIsLoading(false);
}
}; };
loadNotice(); loadNotice();
return () => { isMounted = false; };
}, []); }, []);
const nConfig = notice ? getNoticeConfig(notice.type) : null; const nConfig = notice ? getNoticeConfig(notice.type) : null;
@@ -66,32 +89,17 @@ export default function Home() {
) )
)} )}
{/* NÖDKNAPPEN - Ligger utanför griddet så den alltid är fullbredd och i fokus */} {/* NÖDKNAPPEN */}
<EmergencyButton <EmergencyButton href="/emergency" title="Nödsituation" />
href="/emergency"
title="Nödsituation"
/>
{/* Quick Action Cards now use the glass style */} {/* Quick Action Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<ActionLinkCard <ActionLinkCard href="/schedule" title="Ditt Schema" description="Kolla dina arbetspass och dagliga rundor snabbt." />
href="/schedule" <ActionLinkCard href="/faq" title="Vanliga Frågor" description="Snabba svar på turisternas vanligaste funderingar." />
title="Ditt Schema"
description="Kolla dina arbetspass och dagliga rundor snabbt."
/>
<ActionLinkCard
href="/faq"
title="Vanliga Frågor"
description="Snabba svar på turisternas vanligaste funderingar."
/>
</div> </div>
{/* Contact Card updated to 'glass' variant */} {/* Contact Card */}
<SectionCard <SectionCard title="Snabbkontakt" icon={PhoneCall}>
title="Snabbkontakt"
icon={PhoneCall}
>
<div className="space-y-4 pt-2"> <div className="space-y-4 pt-2">
<div className="flex flex-col md:flex-row md:justify-between md:items-center pb-4 border-b border-white gap-2"> <div className="flex flex-col md:flex-row md:justify-between md:items-center pb-4 border-b border-white gap-2">
<span className="text-sm font-bold text-ebony">William Söderberg</span> <span className="text-sm font-bold text-ebony">William Söderberg</span>
+1
View File
@@ -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',
], ],
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
// prisma.config.ts // prisma.config.ts
import "dotenv/config"; import "dotenv/config";
import { defineConfig, env } from "prisma/config"; import { defineConfig } from "prisma/config";
export default defineConfig({ export default defineConfig({
schema: "prisma/schema.prisma", schema: "prisma/schema.prisma",