Add logout functionality to map page header: - Import signOut from next-auth/react - Add Logout button next to Admin button - Use same red gradient styling as admin layout - Redirect to /login after logout 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
200 lines
8.2 KiB
TypeScript
200 lines
8.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import dynamic from "next/dynamic";
|
|
import Link from "next/link";
|
|
import { signOut } from "next-auth/react";
|
|
|
|
const MapView = dynamic(() => import("@/components/map/MapView"), {
|
|
ssr: false,
|
|
loading: () => (
|
|
<div className="h-full flex items-center justify-center">
|
|
Loading map...
|
|
</div>
|
|
),
|
|
});
|
|
|
|
const TIME_FILTERS = [
|
|
{ label: "1 Hour", value: 1 },
|
|
{ label: "3 Hours", value: 3 },
|
|
{ label: "6 Hours", value: 6 },
|
|
{ label: "12 Hours", value: 12 },
|
|
{ label: "24 Hours", value: 24 },
|
|
{ label: "All", value: 0 },
|
|
];
|
|
|
|
interface DeviceInfo {
|
|
id: string;
|
|
name: string;
|
|
color: string;
|
|
}
|
|
|
|
export default function MapPage() {
|
|
const [selectedDevice, setSelectedDevice] = useState<string>("all");
|
|
const [timeFilter, setTimeFilter] = useState<number>(1); // Default 1 hour
|
|
const [isPaused, setIsPaused] = useState(false);
|
|
const [devices, setDevices] = useState<DeviceInfo[]>([]);
|
|
|
|
// Custom range state
|
|
const [filterMode, setFilterMode] = useState<"quick" | "custom">("quick");
|
|
const [startTime, setStartTime] = useState<string>("");
|
|
const [endTime, setEndTime] = useState<string>("");
|
|
|
|
// Fetch user's devices from API
|
|
useEffect(() => {
|
|
const fetchDevices = async () => {
|
|
try {
|
|
const response = await fetch("/api/devices/public");
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setDevices(data.devices || []);
|
|
} else {
|
|
console.error("Failed to fetch devices:", response.status);
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to fetch devices:", err);
|
|
}
|
|
};
|
|
|
|
fetchDevices();
|
|
// Refresh devices every 30 seconds
|
|
const interval = setInterval(fetchDevices, 30000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="h-screen flex flex-col bg-gray-50">
|
|
{/* Modern SaaS Header - Compact */}
|
|
<div className="bg-white border-b border-gray-200 shadow-sm">
|
|
<div className="px-4 sm:px-6 py-3">
|
|
{/* Single row with title and controls */}
|
|
<div className="flex flex-col lg:flex-row gap-3 items-stretch lg:items-center">
|
|
{/* Title - left aligned */}
|
|
<h1 className="text-lg sm:text-xl font-semibold text-gray-900 lg:mr-4">Location Tracker</h1>
|
|
|
|
{/* Device Filter */}
|
|
<div className="flex items-center gap-2 bg-gray-50 px-3 py-2 rounded-lg border border-gray-200">
|
|
<label className="text-sm font-medium text-gray-700 whitespace-nowrap">Device</label>
|
|
<select
|
|
value={selectedDevice}
|
|
onChange={(e) => setSelectedDevice(e.target.value)}
|
|
className="flex-1 lg:w-44 px-2 py-1.5 text-sm bg-white border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
>
|
|
<option value="all">All My Devices</option>
|
|
{devices.map((device) => (
|
|
<option key={device.id} value={device.id}>
|
|
{device.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Time Filter */}
|
|
<div className="flex items-center gap-2 bg-gray-50 px-3 py-2 rounded-lg border border-gray-200">
|
|
<label className="text-sm font-medium text-gray-700 whitespace-nowrap">Time</label>
|
|
<select
|
|
value={timeFilter}
|
|
onChange={(e) => setTimeFilter(Number(e.target.value))}
|
|
className="flex-1 lg:w-32 px-2 py-1.5 text-sm bg-white border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
>
|
|
{TIME_FILTERS.map((filter) => (
|
|
<option key={filter.value} value={filter.value}>
|
|
{filter.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<button
|
|
onClick={() => setFilterMode(filterMode === "quick" ? "custom" : "quick")}
|
|
className="inline-flex items-center px-2.5 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 hover:border-gray-400 transition-all duration-200"
|
|
title="Toggle Custom Range"
|
|
>
|
|
📅
|
|
</button>
|
|
</div>
|
|
|
|
{/* Pause/Resume Button */}
|
|
<button
|
|
onClick={() => setIsPaused(!isPaused)}
|
|
className={`inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200 shadow-sm ${
|
|
isPaused
|
|
? "bg-green-600 hover:bg-green-700 text-white border border-green-600 hover:border-green-700"
|
|
: "bg-red-600 hover:bg-red-700 text-white border border-red-600 hover:border-red-700"
|
|
}`}
|
|
>
|
|
<span className="mr-1.5">{isPaused ? "▶" : "⏸"}</span>
|
|
{isPaused ? "Resume" : "Pause"}
|
|
</button>
|
|
|
|
{/* Spacer to push buttons to the right */}
|
|
<div className="hidden lg:block lg:flex-1"></div>
|
|
|
|
{/* Export, Admin and Logout Buttons */}
|
|
<div className="flex items-center gap-2">
|
|
<a
|
|
href="/export"
|
|
className="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 hover:border-gray-400 transition-all duration-200 shadow-sm"
|
|
>
|
|
<span className="mr-1.5">📥</span>
|
|
Export
|
|
</a>
|
|
<a
|
|
href="/admin"
|
|
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-blue-600 rounded-lg hover:bg-blue-700 hover:border-blue-700 transition-all duration-200 shadow-sm"
|
|
>
|
|
Admin
|
|
</a>
|
|
<button
|
|
onClick={async () => {
|
|
await signOut({ redirect: false });
|
|
window.location.href = '/login';
|
|
}}
|
|
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-red-600 to-red-700 border border-red-600 rounded-lg hover:from-red-700 hover:to-red-800 transition-all duration-200 shadow-md hover:shadow-lg"
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Custom Range - Modern Card Style */}
|
|
{filterMode === "custom" && (
|
|
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4">
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<label className="text-sm font-medium text-gray-700 whitespace-nowrap min-w-[60px]">From</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={startTime}
|
|
onChange={(e) => setStartTime(e.target.value)}
|
|
className="flex-1 px-3 py-2 text-sm bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<label className="text-sm font-medium text-gray-700 whitespace-nowrap min-w-[60px]">To</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={endTime}
|
|
onChange={(e) => setEndTime(e.target.value)}
|
|
className="flex-1 px-3 py-2 text-sm bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Map */}
|
|
<div className="flex-1">
|
|
<MapView
|
|
selectedDevice={selectedDevice}
|
|
timeFilter={timeFilter}
|
|
isPaused={isPaused}
|
|
filterMode={filterMode}
|
|
startTime={startTime}
|
|
endTime={endTime}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|