more changes, pog

This commit is contained in:
Emma Ruby 2024-11-24 17:27:55 -06:00
parent 49be089aed
commit a550879c5e
17 changed files with 805 additions and 46 deletions

1
.env
View File

@ -1 +0,0 @@
NEXT_PUBLIC_BASE_URL=http://localhost:3000

View 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 });
}
}

View 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`)
}

View 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
View 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>
);
}

View File

@ -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">

BIN
bun.lockb Normal file

Binary file not shown.

View 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>
)
}

View 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>
);
}

View 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>
);
}

View File

@ -2,22 +2,39 @@
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 flex-col gap-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Plane className="h-8 w-8 text-blue-600 dark:text-blue-400" /> <Plane className="h-8 w-8 text-blue-600 dark:text-blue-400" />
<h1 className="text-3xl font-bold tracking-tight">CZQM Ops Support Tool</h1> <h1 className="text-3xl font-bold tracking-tight">
{title || "CZQM Ops Support Tool"}
</h1>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{currentTime && (
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
Last updated: {currentTime} Last updated: {currentTime}
</p> </p>
)}
<div className="flex gap-2">
<Button asChild variant="outline">
<Link href="/">METAR</Link>
</Button>
<Button asChild variant="outline">
<Link href="/controllers">Controllers</Link>
</Button>
</div>
{currentTime && (
<Button <Button
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
variant="outline" variant="outline"
@ -26,7 +43,12 @@ export function Header({ currentTime }: HeaderProps) {
<RefreshCcw className="h-4 w-4" /> <RefreshCcw className="h-4 w-4" />
Refresh Refresh
</Button> </Button>
)}
</div> </div>
</div> </div>
{subtitle && (
<p className="text-muted-foreground">{subtitle}</p>
)}
</div>
); );
} }

6
lib/supabase/client.ts Normal file
View 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
View 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
View 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
View File

@ -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",

View File

@ -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",

61
types/supabase.ts Normal file
View 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
}
}
}