Compare commits
@@ -59,9 +59,9 @@ export const NoticeTab = ({ isOffline }: { isOffline: boolean }) => {
|
||||
<button
|
||||
onClick={() => setNotice({ ...notice, isActive: !notice.isActive })}
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
||||
+3
-3
@@ -105,7 +105,7 @@ export default function Admin() {
|
||||
type="password"
|
||||
ref={pinRef}
|
||||
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)}
|
||||
/>
|
||||
</div>
|
||||
@@ -123,7 +123,7 @@ export default function Admin() {
|
||||
|
||||
const availableTabs = [];
|
||||
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' });
|
||||
availableTabs.push({ id: 'report', icon: UsersIcon, label: 'Rapport' });
|
||||
|
||||
@@ -166,7 +166,7 @@ export default function Admin() {
|
||||
)}
|
||||
|
||||
{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') && (
|
||||
<AttendanceTab
|
||||
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?",
|
||||
"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,
|
||||
"type": "important",
|
||||
"type": "warning",
|
||||
"message": "Glöm inte minst 1 liter vatten, solkräm och myggmedel. Det förväntas bli mycket varmt idag!"
|
||||
}
|
||||
+130
-10
@@ -1,34 +1,154 @@
|
||||
// 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 { PageHeader } from '../components/ui/PageHeader';
|
||||
import { SectionCard } from '../components/ui/SectionCard';
|
||||
|
||||
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 (
|
||||
<div className="space-y-6 animate-fade-in w-full mx-auto">
|
||||
<PageHeader
|
||||
title="Vid Nödsituation"
|
||||
icon={AlertTriangle}
|
||||
description="Agera lugnt, stanna kvar på platsen och tillkalla hjälp."
|
||||
variant="emergency" // <-- Sets the red colors and uppercase
|
||||
variant="emergency"
|
||||
/>
|
||||
|
||||
{/* 112 Card */}
|
||||
<SectionCard
|
||||
variant="alert"
|
||||
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."
|
||||
>
|
||||
<div className="bg-white/60 p-4 rounded-xl border border-emergency/20">
|
||||
<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
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* SÄKER 112-KNAPP */}
|
||||
<div className="bg-white/40 p-2 rounded-2xl border border-emergency/20">
|
||||
{!confirmCall ? (
|
||||
<button
|
||||
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">
|
||||
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").
|
||||
<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>
|
||||
</SectionCard>
|
||||
|
||||
@@ -45,7 +165,7 @@ export default function Emergency() {
|
||||
</li>
|
||||
<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>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>
|
||||
</ul>
|
||||
</SectionCard>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEffect, useState } from 'react';
|
||||
import { readJsonFile } from './actions/jsonEditor';
|
||||
import { SafePhoneLink } from './components/PhoneLinks';
|
||||
import { ActionLinkCard } from './components/ui/ActionLinkCard';
|
||||
import { EmergencyButton } from './components/ui/EmergencyButton';
|
||||
import { SectionCard } from './components/ui/SectionCard';
|
||||
|
||||
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 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ActionLinkCard
|
||||
|
||||
@@ -21,6 +21,10 @@ const withSerwist = withSerwistInit({
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
allowedDevOrigins: [
|
||||
'10.10.0.121',
|
||||
'localhost',
|
||||
],
|
||||
};
|
||||
|
||||
export default withSerwist(nextConfig);
|
||||
Generated
+567
-506
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user