Compare commits
@@ -59,9 +59,9 @@ export const NoticeTab = ({ isOffline }: { isOffline: boolean }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setNotice({ ...notice, isActive: !notice.isActive })}
|
onClick={() => setNotice({ ...notice, isActive: !notice.isActive })}
|
||||||
disabled={isOffline}
|
disabled={isOffline}
|
||||||
className={`relative inline-flex h-7 w-14 items-center rounded-full transition-colors focus:outline-none shadow-inner disabled:opacity-50 ${notice.isActive ? 'bg-moss' : 'bg-slate-teal/20'}`}
|
className={`relative inline-flex h-7 w-12 shrink-0 items-center rounded-full transition-colors focus:outline-none shadow-inner disabled:opacity-50 ${notice.isActive ? 'bg-moss' : 'bg-slate-teal/20'}`}
|
||||||
>
|
>
|
||||||
<span className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform shadow-sm ${notice.isActive ? 'translate-x-8' : 'translate-x-1'}`} />
|
<span className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform shadow-sm ${notice.isActive ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -105,7 +105,7 @@ export default function Admin() {
|
|||||||
type="password"
|
type="password"
|
||||||
ref={pinRef}
|
ref={pinRef}
|
||||||
placeholder="•••••"
|
placeholder="•••••"
|
||||||
className={`w-full bg-white border text-center text-2xl text-ebony font-mono p-3 rounded-xl focus:outline-none ${loginError ? 'border-emergency/50 bg-emergency/5' : 'border-slate-teal/20 focus:border-slate-teal'}`}
|
className={`w-full bg-white border text-center text-2xl text-ebony font-mono p-3 rounded-xl tracking-widest focus:outline-none ${loginError ? 'border-emergency/50 bg-emergency/5' : 'border-slate-teal/20 focus:border-slate-teal'}`}
|
||||||
onChange={() => setLoginError(false)}
|
onChange={() => setLoginError(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,7 +123,7 @@ export default function Admin() {
|
|||||||
|
|
||||||
const availableTabs = [];
|
const availableTabs = [];
|
||||||
if (currentUser.role === 'Admin') availableTabs.push({ id: 'setup', icon: CalendarRange, label: 'Perioder' });
|
if (currentUser.role === 'Admin') availableTabs.push({ id: 'setup', icon: CalendarRange, label: 'Perioder' });
|
||||||
if (currentUser.role === 'Admin') availableTabs.push({ id: 'notice', icon: BellRing, label: 'Notis' });
|
if (currentUser.role === 'Admin' || currentUser.role === 'Staff') availableTabs.push({ id: 'notice', icon: BellRing, label: 'Notis' });
|
||||||
if (currentUser.role === 'Admin' || currentUser.role === 'Staff') availableTabs.push({ id: 'today', icon: ClipboardCheck, label: 'Närvaro' });
|
if (currentUser.role === 'Admin' || currentUser.role === 'Staff') availableTabs.push({ id: 'today', icon: ClipboardCheck, label: 'Närvaro' });
|
||||||
availableTabs.push({ id: 'report', icon: UsersIcon, label: 'Rapport' });
|
availableTabs.push({ id: 'report', icon: UsersIcon, label: 'Rapport' });
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ export default function Admin() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'setup' && currentUser.role === 'Admin' && <SetupTab {...adminState} />}
|
{activeTab === 'setup' && currentUser.role === 'Admin' && <SetupTab {...adminState} />}
|
||||||
{activeTab === 'notice' && currentUser.role === 'Admin' && <NoticeTab isOffline={adminState.isOffline} />}
|
{activeTab === 'notice' && (currentUser.role === 'Admin' || currentUser.role === 'Staff') && <NoticeTab isOffline={adminState.isOffline} />}
|
||||||
{activeTab === 'today' && (currentUser.role === 'Admin' || currentUser.role === 'Staff') && (
|
{activeTab === 'today' && (currentUser.role === 'Admin' || currentUser.role === 'Staff') && (
|
||||||
<AttendanceTab
|
<AttendanceTab
|
||||||
periods={adminState.periods}
|
periods={adminState.periods}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// app/components/ui/EmergencyButton.tsx
|
||||||
|
|
||||||
|
import { ChevronRight, Siren } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
interface EmergencyButtonProps {
|
||||||
|
href: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmergencyButton({ href, title }: EmergencyButtonProps) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className="group flex items-center justify-between w-full bg-emergency text-eggshell px-5 py-4 rounded-2xl shadow-md hover:shadow-lg hover:brightness-110 active:scale-[0.98] transition-all duration-300 border border-emergency/50"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="bg-white/20 p-2 rounded-xl group-hover:bg-white/30 transition-colors">
|
||||||
|
{/* Ikonen pulserar mjukt för att direkt fånga uppmärksamheten */}
|
||||||
|
<Siren size={24} className="animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-base sm:text-lg font-black uppercase tracking-widest drop-shadow-sm">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<ChevronRight
|
||||||
|
size={24}
|
||||||
|
className="opacity-70 group-hover:opacity-100 group-hover:translate-x-1 transition-all"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -92,6 +92,10 @@
|
|||||||
{
|
{
|
||||||
"q": "Var parkerar man om det är fullt?",
|
"q": "Var parkerar man om det är fullt?",
|
||||||
"a": "Ransviks övre parkering."
|
"a": "Ransviks övre parkering."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": "Är det tillåtet att tälta här?",
|
||||||
|
"a": "Nej, förr fanns det en tältruta här men den ligger nu uppe vid stora parkeringen"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"type": "important",
|
"type": "warning",
|
||||||
"message": "Glöm inte minst 1 liter vatten, solkräm och myggmedel. Det förväntas bli mycket varmt idag!"
|
"message": "Glöm inte minst 1 liter vatten, solkräm och myggmedel. Det förväntas bli mycket varmt idag!"
|
||||||
}
|
}
|
||||||
+132
-12
@@ -1,34 +1,154 @@
|
|||||||
// app/emergency/page.tsx
|
// app/emergency/page.tsx
|
||||||
|
|
||||||
import { AlertTriangle, FileText, HeartPulse, MapPin, Phone } from 'lucide-react';
|
"use client";
|
||||||
|
|
||||||
|
import { AlertTriangle, CheckCircle, Crosshair, FileText, HeartPulse, Loader2, MapPin, PhoneCall, XCircle, Navigation } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { SafePhoneLink } from '../components/PhoneLinks';
|
import { SafePhoneLink } from '../components/PhoneLinks';
|
||||||
import { PageHeader } from '../components/ui/PageHeader';
|
import { PageHeader } from '../components/ui/PageHeader';
|
||||||
import { SectionCard } from '../components/ui/SectionCard';
|
import { SectionCard } from '../components/ui/SectionCard';
|
||||||
|
|
||||||
export default function Emergency() {
|
export default function Emergency() {
|
||||||
|
const [confirmCall, setConfirmCall] = useState(false);
|
||||||
|
const [location, setLocation] = useState<{ lat: number, lng: number, acc: number } | null>(null);
|
||||||
|
const [locLoading, setLocLoading] = useState(false);
|
||||||
|
const [locError, setLocError] = useState("");
|
||||||
|
|
||||||
|
const getLocation = () => {
|
||||||
|
setLocLoading(true);
|
||||||
|
setLocError("");
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
setLocError("Din enhet saknar stöd för GPS.");
|
||||||
|
setLocLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos) => {
|
||||||
|
setLocation({
|
||||||
|
lat: pos.coords.latitude,
|
||||||
|
lng: pos.coords.longitude,
|
||||||
|
acc: Math.round(pos.coords.accuracy) // Noggrannhet i meter
|
||||||
|
});
|
||||||
|
setLocLoading(false);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
setLocError("Kunde inte hämta plats. Kontrollera att GPS är aktiverat i telefonen.");
|
||||||
|
setLocLoading(false);
|
||||||
|
},
|
||||||
|
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in w-full mx-auto">
|
<div className="space-y-6 animate-fade-in w-full mx-auto">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Vid Nödsituation"
|
title="Vid Nödsituation"
|
||||||
icon={AlertTriangle}
|
icon={AlertTriangle}
|
||||||
description="Agera lugnt, stanna kvar på platsen och tillkalla hjälp."
|
description="Agera lugnt, stanna kvar på platsen och tillkalla hjälp."
|
||||||
variant="emergency" // <-- Sets the red colors and uppercase
|
variant="emergency"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 112 Card */}
|
|
||||||
<SectionCard
|
<SectionCard
|
||||||
variant="alert"
|
variant="alert"
|
||||||
title="Ring 112"
|
title="Ring 112"
|
||||||
icon={Phone}
|
icon={PhoneCall}
|
||||||
description="Vid olycka, brand eller livshotande tillstånd. Berätta vem du är och vad som har hänt."
|
description="Vid olycka, brand eller livshotande tillstånd. Berätta vem du är och vad som har hänt."
|
||||||
>
|
>
|
||||||
<div className="bg-white/60 p-4 rounded-xl border border-emergency/20">
|
<div className="space-y-4">
|
||||||
<h3 className="font-bold text-emergency flex items-center mb-1 text-xs uppercase tracking-wider">
|
|
||||||
<MapPin className="mr-2" size={14} /> Uppge din position
|
{/* SÄKER 112-KNAPP */}
|
||||||
</h3>
|
<div className="bg-white/40 p-2 rounded-2xl border border-emergency/20">
|
||||||
<p className="text-ebony font-medium text-sm">
|
{!confirmCall ? (
|
||||||
Använd appen <strong>112</strong> eller GPS. Säg att du befinner dig i Kullabergs Naturreservat. Var specifik (t.ex. "Nära fyren" eller "Vid Josefinelust").
|
<button
|
||||||
</p>
|
onClick={() => setConfirmCall(true)}
|
||||||
|
className="w-full flex items-center justify-center gap-3 bg-emergency text-white font-black uppercase tracking-widest text-lg py-4 rounded-xl shadow-sm hover:brightness-110 active:scale-[0.98] transition-all"
|
||||||
|
>
|
||||||
|
<PhoneCall size={24} className="animate-pulse" />
|
||||||
|
Ring 112
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 animate-fade-in">
|
||||||
|
<a
|
||||||
|
href="tel:112"
|
||||||
|
onClick={() => setTimeout(() => setConfirmCall(false), 2000)} // Återställ efter klick
|
||||||
|
className="flex-1 flex items-center justify-center gap-2 bg-emergency text-white font-black uppercase tracking-widest text-lg py-4 rounded-xl shadow-md hover:brightness-110 active:scale-[0.98] transition-all ring-4 ring-emergency/30"
|
||||||
|
>
|
||||||
|
<CheckCircle size={24} />
|
||||||
|
Ja, ring nu
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onClick={() => setConfirmCall(false)}
|
||||||
|
className="sm:w-1/3 flex items-center justify-center gap-2 bg-white text-ebony font-bold uppercase tracking-widest text-sm py-4 rounded-xl shadow-sm border border-emergency/20 hover:bg-eggshell transition-all"
|
||||||
|
>
|
||||||
|
<XCircle size={18} />
|
||||||
|
Avbryt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* GPS OCH POSITION */}
|
||||||
|
<div className="bg-white/60 p-4 md:p-5 rounded-2xl border border-emergency/20">
|
||||||
|
<h3 className="font-black text-emergency flex items-center mb-2 text-xs uppercase tracking-widest">
|
||||||
|
<MapPin className="mr-2" size={16} /> Uppge din position
|
||||||
|
</h3>
|
||||||
|
<p className="text-ebony font-medium text-sm leading-relaxed mb-4">
|
||||||
|
Säg att du befinner dig i Kullabergs Naturreservat. Var specifik (t.ex. "Nära fyren" eller "Vid Josefinelust"). Minns du närmsta räddningspunkt?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-white p-3 rounded-xl border border-emergency/10 shadow-sm">
|
||||||
|
{location ? (
|
||||||
|
<div className="space-y-1 animate-fade-in">
|
||||||
|
<p className="text-xs font-bold text-slate-teal uppercase tracking-widest">Dina koordinater (WGS84)</p>
|
||||||
|
<p className="font-mono text-lg font-black text-ebony tracking-tight">
|
||||||
|
{location.lat.toFixed(5)}, {location.lng.toFixed(5)}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] font-bold text-ebony/50 uppercase">
|
||||||
|
Noggrannhet: ca {location.acc} meter
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-3 mt-4 pt-3 border-t border-slate-teal/5">
|
||||||
|
<a
|
||||||
|
href={`https://maps.google.com/?q=${location.lat},${location.lng}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex items-center gap-1.5 bg-seafoam/10 text-slate-teal px-3 py-1.5 rounded-lg text-xs font-bold hover:bg-seafoam/20 transition-colors"
|
||||||
|
>
|
||||||
|
<MapPin size={14} />
|
||||||
|
Google Maps
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`http://maps.apple.com/?ll=${location.lat},${location.lng}&q=Min+Position`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex items-center gap-1.5 bg-seafoam/10 text-slate-teal px-3 py-1.5 rounded-lg text-xs font-bold hover:bg-seafoam/20 transition-colors"
|
||||||
|
>
|
||||||
|
<Navigation size={14} />
|
||||||
|
Apple Maps
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={getLocation}
|
||||||
|
disabled={locLoading}
|
||||||
|
className="w-full flex items-center justify-center gap-2 bg-emergency/10 text-emergency hover:bg-emergency/20 font-bold uppercase tracking-widest text-xs py-3 rounded-lg transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{locLoading ? <Loader2 size={16} className="animate-spin" /> : <Crosshair size={16} />}
|
||||||
|
{locLoading ? "Söker satelliter..." : "Hämta min exakta position"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{locError && (
|
||||||
|
<p className="text-xs font-bold text-emergency mt-3 animate-fade-in flex items-start gap-1.5">
|
||||||
|
<AlertTriangle size={14} className="shrink-0" />
|
||||||
|
{locError}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
@@ -45,7 +165,7 @@ export default function Emergency() {
|
|||||||
</li>
|
</li>
|
||||||
<li className="flex items-start">
|
<li className="flex items-start">
|
||||||
<span className="bg-seafoam text-eggshell font-bold px-2 py-0.5 rounded mr-3 text-xs shrink-0">2</span>
|
<span className="bg-seafoam text-eggshell font-bold px-2 py-0.5 rounded mr-3 text-xs shrink-0">2</span>
|
||||||
<span>Finns hjärtstartare? Ja, närmaste hjärtstartare finns inne på <strong className="text-ebony font-black">Naturum Kullaberg</strong> (vid fyren) under deras öppettider.</span>
|
<span>Finns hjärtstartare? Ja, närmaste hjärtstartare finns inne på <strong className="text-ebony font-black">Naturum Kullaberg</strong> (vid fyren).</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { readJsonFile } from './actions/jsonEditor';
|
import { readJsonFile } from './actions/jsonEditor';
|
||||||
import { SafePhoneLink } from './components/PhoneLinks';
|
import { SafePhoneLink } from './components/PhoneLinks';
|
||||||
import { ActionLinkCard } from './components/ui/ActionLinkCard';
|
import { ActionLinkCard } from './components/ui/ActionLinkCard';
|
||||||
|
import { EmergencyButton } from './components/ui/EmergencyButton';
|
||||||
import { SectionCard } from './components/ui/SectionCard';
|
import { SectionCard } from './components/ui/SectionCard';
|
||||||
|
|
||||||
const getNoticeConfig = (type: string) => {
|
const getNoticeConfig = (type: string) => {
|
||||||
@@ -65,6 +66,12 @@ export default function Home() {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* NÖDKNAPPEN - Ligger utanför griddet så den alltid är fullbredd och i fokus */}
|
||||||
|
<EmergencyButton
|
||||||
|
href="/emergency"
|
||||||
|
title="Nödsituation"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Quick Action Cards now use the glass style */}
|
{/* Quick Action Cards now use the glass style */}
|
||||||
<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
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ const withSerwist = withSerwistInit({
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
allowedDevOrigins: [
|
||||||
|
'10.10.0.121',
|
||||||
|
'localhost',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withSerwist(nextConfig);
|
export default withSerwist(nextConfig);
|
||||||
Generated
+567
-506
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user