diff --git a/.env b/.env deleted file mode 100644 index 4ce83b9..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -NEXT_PUBLIC_BASE_URL=http://localhost:3000 diff --git a/app/api/cron/update-controllers/route.ts b/app/api/cron/update-controllers/route.ts new file mode 100644 index 0000000..75ed94d --- /dev/null +++ b/app/api/cron/update-controllers/route.ts @@ -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 }); + } +} \ No newline at end of file diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts new file mode 100644 index 0000000..e2e56f2 --- /dev/null +++ b/app/auth/callback/route.ts @@ -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`) +} \ No newline at end of file diff --git a/app/controllers/[cid]/page.tsx b/app/controllers/[cid]/page.tsx new file mode 100644 index 0000000..7117854 --- /dev/null +++ b/app/controllers/[cid]/page.tsx @@ -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([]); + 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 ( +
+
+
+
+ + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/controllers/page.tsx b/app/controllers/page.tsx new file mode 100644 index 0000000..13832c5 --- /dev/null +++ b/app/controllers/page.tsx @@ -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([]); + 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 ( +
+
+
+
+ + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 3f6bf82..1924ecc 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,4 +1,4 @@ -"use client"; // Mark this component as client-side +"use client"; import { useEffect, useState } from "react"; import { Card } from "@/components/ui/card"; @@ -7,35 +7,27 @@ import { CallsignSearch } from "@/components/callsign-search"; import { Header } from "@/components/header"; 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() { - const [metarData, setMetarData] = useState(null); + const [metarData, setMetarData] = useState([]); const [loading, setLoading] = useState(true); const currentTime = getUTCtime(); useEffect(() => { async function loadMetarData() { try { - const data = await fetchMetarData(); - setMetarData(data); // Update the state with fetched data + const response = await fetch('/api/metar'); + if (!response.ok) throw new Error('Failed to fetch METAR data'); + const data = await response.json(); + setMetarData(data); } catch (error) { console.error("Error fetching METAR data:", error); - setMetarData(null); // Optionally handle error case } finally { - setLoading(false); // Set loading to false after fetching + setLoading(false); } } - loadMetarData(); // Call the fetch function when the component mounts - }, []); // Empty dependency array means this effect runs once after mount + loadMetarData(); + }, []); return (
@@ -44,13 +36,7 @@ export default function Home() {
- {loading ? ( -

Loading METAR data...

- ) : metarData ? ( - - ) : ( -

Error loading METAR data.

- )} +
@@ -63,4 +49,4 @@ export default function Home() {
); -} +} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 0000000..070e31b Binary files /dev/null and b/bun.lockb differ diff --git a/components/auth-button.tsx b/components/auth-button.tsx new file mode 100644 index 0000000..f5204c7 --- /dev/null +++ b/components/auth-button.tsx @@ -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 ( + + + + + + Account + + + + Sign Out + + + + ) + } + + return ( + + ) +} \ No newline at end of file diff --git a/components/controller-profile.tsx b/components/controller-profile.tsx new file mode 100644 index 0000000..a29be36 --- /dev/null +++ b/components/controller-profile.tsx @@ -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 ; + } + + 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 ( +
+
+ +
+ +

Total Time

+
+

+ {totalHours}h {totalMinutes}m +

+
+ + +
+ +

Total Sessions

+
+

{sessions.length}

+
+ + +
+ +

Positions

+
+
+ {positions.map(position => ( + + {position} + + ))} +
+
+
+ +
+

Session History

+ + + + Date + Callsign + Position + Airport + Frequency + Duration + + + + {sessions.map((session) => ( + + + {format(new Date(session.logon_time), "MMM d, yyyy")} + + {session.callsign} + {session.facility_type} + {session.airport} + {session.frequency} + + {formatSessionDuration(session.logon_time, session.last_seen)} + + + ))} + +
+
+
+ ); +} + +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 ( +
+
+ + + +
+
+ + {Array.from({ length: 5 }).map((_, i) => ( +
+ + + + + + +
+ ))} +
+
+ ); +} \ No newline at end of file diff --git a/components/controller-table.tsx b/components/controller-table.tsx new file mode 100644 index 0000000..0b7b613 --- /dev/null +++ b/components/controller-table.tsx @@ -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 ; + } + + return ( + + + + Name + CID + Current Position + Airport + Frequency + Last Seen + Session Duration + + + + {data.map((controller) => ( + handleControllerClick(controller.cid)} + > + {controller.name} + {controller.cid} + {controller.callsign} + {controller.airport} + {controller.frequency} + + {formatDistanceToNow(new Date(controller.last_seen), { addSuffix: true })} + + + {formatSessionDuration(controller.logon_time, controller.last_seen)} + + + ))} + +
+ ); +} + +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 ( +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ + + + + + + +
+ ))} +
+ ); +} \ No newline at end of file diff --git a/components/header.tsx b/components/header.tsx index 1cc8076..5917e60 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -2,31 +2,53 @@ import { RefreshCcw, Plane } from "lucide-react"; import { Button } from "@/components/ui/button"; +import Link from "next/link"; interface HeaderProps { - currentTime: string; + title?: string; + subtitle?: string; + currentTime?: string; } -export function Header({ currentTime }: HeaderProps) { +export function Header({ title, subtitle, currentTime }: HeaderProps) { return ( -
-
- -

CZQM Ops Support Tool

-
-
-

- Last updated: {currentTime} -

- +
+
+
+ +

+ {title || "CZQM Ops Support Tool"} +

+
+
+ {currentTime && ( +

+ Last updated: {currentTime} +

+ )} +
+ + +
+ {currentTime && ( + + )} +
+ {subtitle && ( +

{subtitle}

+ )}
); } \ No newline at end of file diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts new file mode 100644 index 0000000..e3968ac --- /dev/null +++ b/lib/supabase/client.ts @@ -0,0 +1,6 @@ +import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' +import type { Database } from '@/types/supabase' + +export const createClient = () => { + return createClientComponentClient() +} \ No newline at end of file diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts new file mode 100644 index 0000000..4906c7d --- /dev/null +++ b/lib/supabase/server.ts @@ -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({ cookies: () => cookieStore }) +} \ No newline at end of file diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..356a4c6 --- /dev/null +++ b/middleware.ts @@ -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).*)', + ], +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cd47866..bab91e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,8 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@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/react": "18.2.22", "@types/react-dom": "18.2.7", @@ -1845,6 +1847,96 @@ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz", "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": { "version": "0.5.2", "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", "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": { "version": "15.7.13", "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", "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": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", @@ -4896,6 +5001,14 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6100,6 +6213,11 @@ "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": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -6619,6 +6737,11 @@ "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": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -6896,6 +7019,20 @@ "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": { "version": "2.0.2", "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", "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": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", diff --git a/package.json b/package.json index a0806d1..eff36f8 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@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/react": "18.2.22", "@types/react-dom": "18.2.7", @@ -61,6 +63,7 @@ "react-resizable-panels": "^2.1.3", "recharts": "^2.12.7", "sonner": "^1.5.0", + "supabase": "^1.223.10", "tailwind-merge": "^2.5.2", "tailwindcss": "3.3.3", "tailwindcss-animate": "^1.0.7", @@ -68,4 +71,4 @@ "vaul": "^0.9.9", "zod": "^3.23.8" } -} +} \ No newline at end of file diff --git a/types/supabase.ts b/types/supabase.ts new file mode 100644 index 0000000..71ecb59 --- /dev/null +++ b/types/supabase.ts @@ -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 + } + } +} \ No newline at end of file