more changes, pog
This commit is contained in:
parent
49be089aed
commit
a550879c5e
76
app/api/cron/update-controllers/route.ts
Normal file
76
app/api/cron/update-controllers/route.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { createClient } from '@supabase/supabase-js'
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
const facilityTypes = {
|
||||||
|
0: "OBS",
|
||||||
|
1: "FSS",
|
||||||
|
2: "DEL",
|
||||||
|
3: "GND",
|
||||||
|
4: "TWR",
|
||||||
|
5: "APP",
|
||||||
|
6: "CTR",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Define FIR coverage
|
||||||
|
const CZQM_AIRPORTS = ["CYHZ", "CYFC", "CYQM", "CYSJ", "CYZX", "CYYG", "CYYT", "CYQX", "CYYR", "LFVP", "CYQI", "CYAY", "CYDF", "CYJT"];
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Fetch current VATSIM data
|
||||||
|
const response = await fetch('https://data.vatsim.net/v3/vatsim-data.json');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch VATSIM data');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_SECRET_SUPABASE_KEY)
|
||||||
|
|
||||||
|
// Filter controllers in CZQM airspace
|
||||||
|
const czqmControllers = data.controllers.filter((controller: any) => {
|
||||||
|
const callsign = controller.callsign;
|
||||||
|
// console.log(callsign)
|
||||||
|
return callsign.startsWith('CZQM_') ||
|
||||||
|
callsign.startsWith('CZQX_') ||
|
||||||
|
CZQM_AIRPORTS.some(airport => callsign.startsWith(airport));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process each controller
|
||||||
|
for (const controller of czqmControllers) {
|
||||||
|
const facilityType = facilityTypes[controller.facility as keyof typeof facilityTypes];
|
||||||
|
const airport = CZQM_AIRPORTS.find(ap => controller.callsign.startsWith(ap)) || 'CZQM';
|
||||||
|
|
||||||
|
// Insert or update controller session
|
||||||
|
const { error } = await supabase
|
||||||
|
.from('controller_sessions')
|
||||||
|
.upsert({
|
||||||
|
cid: controller.cid,
|
||||||
|
name: controller.name,
|
||||||
|
callsign: controller.callsign,
|
||||||
|
facility_type: facilityType,
|
||||||
|
frequency: controller.frequency,
|
||||||
|
airport: airport,
|
||||||
|
last_seen: new Date().toISOString(),
|
||||||
|
logon_time: controller.logon_time,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error updating controller session:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up old sessions (controllers who have logged off)
|
||||||
|
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
||||||
|
const { error: cleanupError } = await supabase
|
||||||
|
.from('controller_sessions')
|
||||||
|
.delete()
|
||||||
|
.lt('last_seen', fiveMinutesAgo);
|
||||||
|
|
||||||
|
if (cleanupError) {
|
||||||
|
console.error('Error cleaning up old sessions:', cleanupError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in controller update:', error);
|
||||||
|
return NextResponse.json({ error: 'Failed to update controller data' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
18
app/auth/callback/route.ts
Normal file
18
app/auth/callback/route.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { createClient } from '@/lib/supabase/server'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const { searchParams, origin } = new URL(request.url)
|
||||||
|
const code = searchParams.get('code')
|
||||||
|
const next = searchParams.get('next') ?? '/'
|
||||||
|
|
||||||
|
if (code) {
|
||||||
|
const supabase = createClient()
|
||||||
|
const { error } = await supabase.auth.exchangeCodeForSession(code)
|
||||||
|
if (!error) {
|
||||||
|
return NextResponse.redirect(`${origin}${next}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.redirect(`${origin}/auth/auth-code-error`)
|
||||||
|
}
|
55
app/controllers/[cid]/page.tsx
Normal file
55
app/controllers/[cid]/page.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import { ControllerProfile } from "@/components/controller-profile";
|
||||||
|
import { createClient } from "@/lib/supabase/client";
|
||||||
|
|
||||||
|
export default function ControllerProfilePage() {
|
||||||
|
const params = useParams();
|
||||||
|
const [sessions, setSessions] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadControllerData() {
|
||||||
|
try {
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('controller_sessions')
|
||||||
|
.select('*')
|
||||||
|
.eq('cid', params.cid)
|
||||||
|
.order('last_seen', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
setSessions(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching controller data:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadControllerData();
|
||||||
|
}, [params.cid]);
|
||||||
|
|
||||||
|
const controllerName = sessions[0]?.name || params.cid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<Header
|
||||||
|
title={`Controller Profile: ${controllerName}`}
|
||||||
|
subtitle={`CID: ${params.cid}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<ControllerProfile sessions={sessions} loading={loading} />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
61
app/controllers/page.tsx
Normal file
61
app/controllers/page.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Header } from "@/components/header";
|
||||||
|
import { ControllerTable } from "@/components/controller-table";
|
||||||
|
import { createClient } from "@/lib/supabase/client";
|
||||||
|
|
||||||
|
export default function ControllersPage() {
|
||||||
|
const [controllers, setControllers] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadControllers() {
|
||||||
|
try {
|
||||||
|
// Get unique controllers with their latest session
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('controller_sessions')
|
||||||
|
.select('*')
|
||||||
|
.order('last_seen', { ascending: false });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Group by CID and take the most recent session
|
||||||
|
const uniqueControllers = data.reduce((acc: any[], curr) => {
|
||||||
|
const existingIndex = acc.findIndex(c => c.cid === curr.cid);
|
||||||
|
if (existingIndex === -1) {
|
||||||
|
acc.push(curr);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
setControllers(uniqueControllers);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching controller data:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadControllers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="flex flex-col gap-8">
|
||||||
|
<Header
|
||||||
|
title="Controller History"
|
||||||
|
subtitle="View recent controller activity"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<ControllerTable data={controllers} loading={loading} />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
36
app/page.tsx
36
app/page.tsx
@ -1,4 +1,4 @@
|
|||||||
"use client"; // Mark this component as client-side
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
@ -7,35 +7,27 @@ import { CallsignSearch } from "@/components/callsign-search";
|
|||||||
import { Header } from "@/components/header";
|
import { Header } from "@/components/header";
|
||||||
import { getUTCtime } from "@/lib/utils/metar";
|
import { getUTCtime } from "@/lib/utils/metar";
|
||||||
|
|
||||||
// Function to fetch METAR data from the API
|
|
||||||
async function fetchMetarData() {
|
|
||||||
const response = await fetch('/api/metar');
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch METAR data');
|
|
||||||
}
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [metarData, setMetarData] = useState(null);
|
const [metarData, setMetarData] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const currentTime = getUTCtime();
|
const currentTime = getUTCtime();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadMetarData() {
|
async function loadMetarData() {
|
||||||
try {
|
try {
|
||||||
const data = await fetchMetarData();
|
const response = await fetch('/api/metar');
|
||||||
setMetarData(data); // Update the state with fetched data
|
if (!response.ok) throw new Error('Failed to fetch METAR data');
|
||||||
|
const data = await response.json();
|
||||||
|
setMetarData(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching METAR data:", error);
|
console.error("Error fetching METAR data:", error);
|
||||||
setMetarData(null); // Optionally handle error case
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false); // Set loading to false after fetching
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMetarData(); // Call the fetch function when the component mounts
|
loadMetarData();
|
||||||
}, []); // Empty dependency array means this effect runs once after mount
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
|
||||||
@ -44,13 +36,7 @@ export default function Home() {
|
|||||||
<Header currentTime={currentTime} />
|
<Header currentTime={currentTime} />
|
||||||
|
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-hidden">
|
||||||
{loading ? (
|
<MetarTable data={metarData} loading={loading} />
|
||||||
<p>Loading METAR data...</p>
|
|
||||||
) : metarData ? (
|
|
||||||
<MetarTable data={metarData} loading={false} />
|
|
||||||
) : (
|
|
||||||
<p className="text-red-500">Error loading METAR data.</p>
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
@ -63,4 +49,4 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
59
components/auth-button.tsx
Normal file
59
components/auth-button.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createClient } from '@/lib/supabase/client'
|
||||||
|
import { Button } from './ui/button'
|
||||||
|
import { LogIn, LogOut, User } from 'lucide-react'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
|
||||||
|
export function AuthButton({ user }: { user: any }) {
|
||||||
|
const supabase = createClient()
|
||||||
|
|
||||||
|
const handleSignIn = async () => {
|
||||||
|
await supabase.auth.signInWithOAuth({
|
||||||
|
provider: 'vatsim',
|
||||||
|
options: {
|
||||||
|
redirectTo: `${location.origin}/auth/callback`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSignOut = async () => {
|
||||||
|
await supabase.auth.signOut()
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className="gap-2">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
{user.user_metadata?.full_name || 'User'}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Account</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={handleSignOut}>
|
||||||
|
<LogOut className="h-4 w-4 mr-2" />
|
||||||
|
Sign Out
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={handleSignIn} className="gap-2">
|
||||||
|
<LogIn className="h-4 w-4" />
|
||||||
|
Sign in with VATSIM
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
139
components/controller-profile.tsx
Normal file
139
components/controller-profile.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { formatDistanceToNow, format } from "date-fns";
|
||||||
|
import { Clock, Calendar, Radio } from "lucide-react";
|
||||||
|
|
||||||
|
interface ControllerProfileProps {
|
||||||
|
sessions: any[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ControllerProfile({ sessions, loading }: ControllerProfileProps) {
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalTime = sessions.reduce((acc, session) => {
|
||||||
|
const duration = new Date(session.last_seen).getTime() - new Date(session.logon_time).getTime();
|
||||||
|
return acc + duration;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const totalHours = Math.floor(totalTime / (1000 * 60 * 60));
|
||||||
|
const totalMinutes = Math.floor((totalTime % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
|
||||||
|
// Get unique positions
|
||||||
|
const positions = [...new Set(sessions.map(s => s.facility_type))];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 p-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<Clock className="h-5 w-5 text-blue-500" />
|
||||||
|
<h3 className="text-lg font-semibold">Total Time</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold">
|
||||||
|
{totalHours}h {totalMinutes}m
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<Calendar className="h-5 w-5 text-blue-500" />
|
||||||
|
<h3 className="text-lg font-semibold">Total Sessions</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold">{sessions.length}</p>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="p-6">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<Radio className="h-5 w-5 text-blue-500" />
|
||||||
|
<h3 className="text-lg font-semibold">Positions</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{positions.map(position => (
|
||||||
|
<span
|
||||||
|
key={position}
|
||||||
|
className="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||||
|
>
|
||||||
|
{position}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Session History</h3>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Date</TableHead>
|
||||||
|
<TableHead>Callsign</TableHead>
|
||||||
|
<TableHead>Position</TableHead>
|
||||||
|
<TableHead>Airport</TableHead>
|
||||||
|
<TableHead>Frequency</TableHead>
|
||||||
|
<TableHead>Duration</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{sessions.map((session) => (
|
||||||
|
<TableRow key={`${session.callsign}-${session.last_seen}`}>
|
||||||
|
<TableCell>
|
||||||
|
{format(new Date(session.logon_time), "MMM d, yyyy")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{session.callsign}</TableCell>
|
||||||
|
<TableCell>{session.facility_type}</TableCell>
|
||||||
|
<TableCell>{session.airport}</TableCell>
|
||||||
|
<TableCell>{session.frequency}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{formatSessionDuration(session.logon_time, session.last_seen)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSessionDuration(start: string, end: string) {
|
||||||
|
const duration = new Date(end).getTime() - new Date(start).getTime();
|
||||||
|
const hours = Math.floor(duration / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 p-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<Skeleton className="h-[140px] rounded-lg" />
|
||||||
|
<Skeleton className="h-[140px] rounded-lg" />
|
||||||
|
<Skeleton className="h-[140px] rounded-lg" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3 mt-8">
|
||||||
|
<Skeleton className="h-8 w-[200px]" />
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<div key={i} className="flex gap-4">
|
||||||
|
<Skeleton className="h-4 w-[120px]" />
|
||||||
|
<Skeleton className="h-4 w-[120px]" />
|
||||||
|
<Skeleton className="h-4 w-[100px]" />
|
||||||
|
<Skeleton className="h-4 w-[80px]" />
|
||||||
|
<Skeleton className="h-4 w-[100px]" />
|
||||||
|
<Skeleton className="h-4 w-[100px]" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
91
components/controller-table.tsx
Normal file
91
components/controller-table.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
|
||||||
|
interface ControllerTableProps {
|
||||||
|
data: any[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ControllerTable({ data, loading }: ControllerTableProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleControllerClick = (cid: string) => {
|
||||||
|
router.push(`/controllers/${cid}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>CID</TableHead>
|
||||||
|
<TableHead>Current Position</TableHead>
|
||||||
|
<TableHead>Airport</TableHead>
|
||||||
|
<TableHead>Frequency</TableHead>
|
||||||
|
<TableHead>Last Seen</TableHead>
|
||||||
|
<TableHead>Session Duration</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.map((controller) => (
|
||||||
|
<TableRow
|
||||||
|
key={controller.cid}
|
||||||
|
className="cursor-pointer hover:bg-muted/50"
|
||||||
|
onClick={() => handleControllerClick(controller.cid)}
|
||||||
|
>
|
||||||
|
<TableCell className="font-medium">{controller.name}</TableCell>
|
||||||
|
<TableCell>{controller.cid}</TableCell>
|
||||||
|
<TableCell>{controller.callsign}</TableCell>
|
||||||
|
<TableCell>{controller.airport}</TableCell>
|
||||||
|
<TableCell>{controller.frequency}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{formatDistanceToNow(new Date(controller.last_seen), { addSuffix: true })}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{formatSessionDuration(controller.logon_time, controller.last_seen)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSessionDuration(start: string, end: string) {
|
||||||
|
const duration = new Date(end).getTime() - new Date(start).getTime();
|
||||||
|
const hours = Math.floor(duration / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((duration % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
return `${hours}h ${minutes}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3 p-4">
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<div key={i} className="flex gap-4">
|
||||||
|
<Skeleton className="h-4 w-[150px]" />
|
||||||
|
<Skeleton className="h-4 w-[100px]" />
|
||||||
|
<Skeleton className="h-4 w-[120px]" />
|
||||||
|
<Skeleton className="h-4 w-[80px]" />
|
||||||
|
<Skeleton className="h-4 w-[100px]" />
|
||||||
|
<Skeleton className="h-4 w-[120px]" />
|
||||||
|
<Skeleton className="h-4 w-[100px]" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -2,31 +2,53 @@
|
|||||||
|
|
||||||
import { RefreshCcw, Plane } from "lucide-react";
|
import { RefreshCcw, Plane } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
currentTime: string;
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
currentTime?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ currentTime }: HeaderProps) {
|
export function Header({ title, subtitle, currentTime }: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between">
|
||||||
<Plane className="h-8 w-8 text-blue-600 dark:text-blue-400" />
|
<div className="flex items-center gap-3">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">CZQM Ops Support Tool</h1>
|
<Plane className="h-8 w-8 text-blue-600 dark:text-blue-400" />
|
||||||
</div>
|
<h1 className="text-3xl font-bold tracking-tight">
|
||||||
<div className="flex items-center gap-4">
|
{title || "CZQM Ops Support Tool"}
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
</h1>
|
||||||
Last updated: {currentTime}
|
</div>
|
||||||
</p>
|
<div className="flex items-center gap-4">
|
||||||
<Button
|
{currentTime && (
|
||||||
onClick={() => window.location.reload()}
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
variant="outline"
|
Last updated: {currentTime}
|
||||||
className="gap-2"
|
</p>
|
||||||
>
|
)}
|
||||||
<RefreshCcw className="h-4 w-4" />
|
<div className="flex gap-2">
|
||||||
Refresh
|
<Button asChild variant="outline">
|
||||||
</Button>
|
<Link href="/">METAR</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<Link href="/controllers">Controllers</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{currentTime && (
|
||||||
|
<Button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
variant="outline"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<RefreshCcw className="h-4 w-4" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-muted-foreground">{subtitle}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
6
lib/supabase/client.ts
Normal file
6
lib/supabase/client.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||||
|
import type { Database } from '@/types/supabase'
|
||||||
|
|
||||||
|
export const createClient = () => {
|
||||||
|
return createClientComponentClient<Database>()
|
||||||
|
}
|
8
lib/supabase/server.ts
Normal file
8
lib/supabase/server.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
|
import { Database } from '@/types/supabase'
|
||||||
|
|
||||||
|
export const createClient = () => {
|
||||||
|
const cookieStore = cookies()
|
||||||
|
return createServerComponentClient<Database>({ cookies: () => cookieStore })
|
||||||
|
}
|
18
middleware.ts
Normal file
18
middleware.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import type { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
export async function middleware(request: NextRequest) {
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
*/
|
||||||
|
'/((?!_next/static|_next/image|favicon.ico).*)',
|
||||||
|
],
|
||||||
|
}
|
157
package-lock.json
generated
157
package-lock.json
generated
@ -37,6 +37,8 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
|
"@supabase/auth-helpers-nextjs": "^0.9.0",
|
||||||
|
"@supabase/supabase-js": "^2.39.0",
|
||||||
"@types/node": "20.6.2",
|
"@types/node": "20.6.2",
|
||||||
"@types/react": "18.2.22",
|
"@types/react": "18.2.22",
|
||||||
"@types/react-dom": "18.2.7",
|
"@types/react-dom": "18.2.7",
|
||||||
@ -1845,6 +1847,96 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz",
|
||||||
"integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA=="
|
"integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@supabase/auth-helpers-nextjs": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-nextjs/-/auth-helpers-nextjs-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-V+UKFngSCkzAucX3Zi5D4TRiJZUUx0RDme7W217nIkwhCTvJY7Ih2L1cgnAMihQost2YYgTzJ7DrUzz4mm8i8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/auth-helpers-shared": "0.6.3",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@supabase/supabase-js": "^2.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/auth-helpers-shared": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/auth-helpers-shared/-/auth-helpers-shared-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-xYQRLFeFkL4ZfwC7p9VKcarshj3FB2QJMgJPydvOY7J5czJe6xSG5/wM1z63RmAzGbCkKg+dzpq61oeSyWiGBQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"jose": "^4.14.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@supabase/supabase-js": "^2.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/auth-js": {
|
||||||
|
"version": "2.65.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.1.tgz",
|
||||||
|
"integrity": "sha512-IA7i2Xq2SWNCNMKxwmPlHafBQda0qtnFr8QnyyBr+KaSxoXXqEzFCnQ1dGTy6bsZjVBgXu++o3qrDypTspaAPw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/functions-js": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-sOLXy+mWRyu4LLv1onYydq+10mNRQ4rzqQxNhbrKLTLTcdcmS9hbWif0bGz/NavmiQfPs4ZcmQJp4WqOXlR4AQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/node-fetch": {
|
||||||
|
"version": "2.6.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||||
|
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/postgrest-js": {
|
||||||
|
"version": "1.16.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.16.3.tgz",
|
||||||
|
"integrity": "sha512-HI6dsbW68AKlOPofUjDTaosiDBCtW4XAm0D18pPwxoW3zKOE2Ru13Z69Wuys9fd6iTpfDViNco5sgrtnP0666A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/realtime-js": {
|
||||||
|
"version": "2.10.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.7.tgz",
|
||||||
|
"integrity": "sha512-OLI0hiSAqQSqRpGMTUwoIWo51eUivSYlaNBgxsXZE7PSoWh12wPRdVt0psUMaUzEonSB85K21wGc7W5jHnT6uA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14",
|
||||||
|
"@types/phoenix": "^1.5.4",
|
||||||
|
"@types/ws": "^8.5.10",
|
||||||
|
"ws": "^8.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/storage-js": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/supabase-js": {
|
||||||
|
"version": "2.46.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.46.1.tgz",
|
||||||
|
"integrity": "sha512-HiBpd8stf7M6+tlr+/82L8b2QmCjAD8ex9YdSAKU+whB/SHXXJdus1dGlqiH9Umy9ePUuxaYmVkGd9BcvBnNvg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/auth-js": "2.65.1",
|
||||||
|
"@supabase/functions-js": "2.4.3",
|
||||||
|
"@supabase/node-fetch": "2.6.15",
|
||||||
|
"@supabase/postgrest-js": "1.16.3",
|
||||||
|
"@supabase/realtime-js": "2.10.7",
|
||||||
|
"@supabase/storage-js": "2.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
|
||||||
@ -1917,6 +2009,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz",
|
||||||
"integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw=="
|
"integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/phoenix": {
|
||||||
|
"version": "1.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz",
|
||||||
|
"integrity": "sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w=="
|
||||||
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.13",
|
"version": "15.7.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||||
@ -1945,6 +2042,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz",
|
||||||
"integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw=="
|
"integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.5.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||||
|
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
|
||||||
@ -4896,6 +5001,14 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "4.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||||
|
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@ -6100,6 +6213,11 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
|
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
|
||||||
|
},
|
||||||
"node_modules/set-function-length": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
@ -6619,6 +6737,11 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
|
||||||
@ -6896,6 +7019,20 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@ -7085,6 +7222,26 @@
|
|||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||||
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
|
||||||
|
@ -38,6 +38,8 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
|
"@supabase/auth-helpers-nextjs": "^0.9.0",
|
||||||
|
"@supabase/supabase-js": "^2.39.0",
|
||||||
"@types/node": "20.6.2",
|
"@types/node": "20.6.2",
|
||||||
"@types/react": "18.2.22",
|
"@types/react": "18.2.22",
|
||||||
"@types/react-dom": "18.2.7",
|
"@types/react-dom": "18.2.7",
|
||||||
@ -61,6 +63,7 @@
|
|||||||
"react-resizable-panels": "^2.1.3",
|
"react-resizable-panels": "^2.1.3",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^1.5.0",
|
||||||
|
"supabase": "^1.223.10",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss": "3.3.3",
|
"tailwindcss": "3.3.3",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@ -68,4 +71,4 @@
|
|||||||
"vaul": "^0.9.9",
|
"vaul": "^0.9.9",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
61
types/supabase.ts
Normal file
61
types/supabase.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
export type Json =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| { [key: string]: Json | undefined }
|
||||||
|
| Json[]
|
||||||
|
|
||||||
|
export interface Database {
|
||||||
|
public: {
|
||||||
|
Tables: {
|
||||||
|
controller_sessions: {
|
||||||
|
Row: {
|
||||||
|
id: number
|
||||||
|
cid: string
|
||||||
|
name: string
|
||||||
|
callsign: string
|
||||||
|
facility_type: string
|
||||||
|
frequency: string
|
||||||
|
airport: string
|
||||||
|
last_seen: string
|
||||||
|
logon_time: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
id?: number
|
||||||
|
cid: string
|
||||||
|
name: string
|
||||||
|
callsign: string
|
||||||
|
facility_type: string
|
||||||
|
frequency: string
|
||||||
|
airport: string
|
||||||
|
last_seen: string
|
||||||
|
logon_time: string
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
id?: number
|
||||||
|
cid?: string
|
||||||
|
name?: string
|
||||||
|
callsign?: string
|
||||||
|
facility_type?: string
|
||||||
|
frequency?: string
|
||||||
|
airport?: string
|
||||||
|
last_seen?: string
|
||||||
|
logon_time?: string
|
||||||
|
created_at?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Views: {
|
||||||
|
[_ in never]: never
|
||||||
|
}
|
||||||
|
Functions: {
|
||||||
|
[_ in never]: never
|
||||||
|
}
|
||||||
|
Enums: {
|
||||||
|
[_ in never]: never
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user