diff --git a/app/files/page.tsx b/app/files/page.tsx
new file mode 100644
index 0000000..4a9118b
--- /dev/null
+++ b/app/files/page.tsx
@@ -0,0 +1,164 @@
+"use client";
+
+import Spinner from "@/components/Spinner";
+import { Table, TableBody, TableCell, TableHeader, TableRow } from "@/components/ui/table";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { File, FileType } from "@/models/file";
+import { Response } from "@/models/response";
+import { useContext, useEffect, useState } from "react";
+import { toast } from "react-toastify";
+
+import { AuthContext } from "../Providers";
+
+type FileEntryProps = {
+ files: File[];
+};
+
+const FileEntry = ({ files }: FileEntryProps) => {
+ return (
+
+
+
+
+ Title
+ Description
+ Version
+ File
+
+
+
+ {files.length > 0 ? (
+ <>
+ {files.map((file) => (
+
+ {file.title}
+ {file.description}
+ {file.version}
+
+
+ Download
+
+
+
+ ))}
+ >
+ ): (
+
+ No files found
+
+ )}
+
+
+
+ );
+};
+
+export default function Files() {
+
+ const authContext = useContext(AuthContext);
+
+ const [loading, setLoading] = useState(true);
+
+ const [files, setFiles] = useState([] as File[]);
+
+ useEffect(() => {
+
+ document.title = "Files | Memphis ARTCC";
+
+ const fetchFiles = async () => {
+ const headers = new Map();
+ if (authContext?.isLoggedIn) {
+ headers.set("Authorization", `Bearer ${localStorage.getItem("accessToken")}`);
+ }
+ headers.set("Content-Type", "application/json");
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/files`, {
+ headers: Object.fromEntries(headers),
+ });
+ if (!response.ok) {
+ const error = await response.json() as Response;
+ throw new Error(`Error fetching roster:\n${error.message}`);
+ }
+ return await response.json() as Response;
+ };
+
+ fetchFiles()
+ .then((response) => {
+ setFiles(response.data);
+ setLoading(false);
+ })
+ .catch((error) => {
+ console.log(error);
+ toast.error(error);
+ setLoading(false);
+ });
+
+ }, [authContext]);
+
+ return (
+
+
+
+ {!loading ? (
+
+
+
+ Policies
+ SOP's
+ LOA's
+ References
+ Miscellaneous
+ Clients
+ {authContext?.isTrainingStaff() ? (
+ Training Staff
+ ) : <>>}
+ {authContext?.isStaff() ? (
+ Staff
+ ) : <>>}
+ {authContext?.isSeniorStaff() ? (
+ Senior Staff
+ ) : <>>}
+
+
+ file.type === FileType.POLICY)} />
+
+
+ file.type === FileType.SOP)} />
+
+
+ file.type === FileType.LOA)} />
+
+
+ file.type === FileType.REFERENCE)} />
+
+
+ file.type === FileType.MISC)} />
+
+
+ file.type === FileType.CLIENT)} />
+
+ {authContext?.isTrainingStaff() ? (
+
+ file.type === FileType.TRAINING_STAFF)} />
+
+ ) : <>>}
+ {authContext?.isStaff() ? (
+
+ file.type === FileType.STAFF)} />
+
+ ) : <>>}
+ {authContext?.isSeniorStaff() ? (
+
+ file.type === FileType.SENIOR_STAFF)} />
+
+ ) : <>>}
+
+
+ ) :
}
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/not-found.tsx b/app/not-found.tsx
index ddfeb4f..a48e778 100644
--- a/app/not-found.tsx
+++ b/app/not-found.tsx
@@ -17,8 +17,7 @@ export default function NotFound() {
404 - Page Not Found
-
+
Go Back
diff --git a/app/page.tsx b/app/page.tsx
index a3fcf27..a55eeea 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,8 +1,11 @@
"use client";
+import Spinner from "@/components/Spinner";
import { Token } from "@/models/auth/token";
+import { News } from "@/models/news";
import { Response } from "@/models/response";
import { Stats } from "@/models/stats";
+import { format } from "date-fns";
import { Trophy } from "lucide-react";
import { useRouter } from "next/navigation";
import { useContext, useEffect, useState } from "react";
@@ -18,8 +21,11 @@ export default function Home() {
const [isMember, setIsMember] = useState(false);
const [loadingTop, setLoadingTop] = useState(true);
+ const [loadingNews, setloadingNews] = useState(true);
+
const [top, setTop] = useState([] as Stats[]);
+ const [news, setNews] = useState([] as News[]);
useEffect(() => {
document.title = "Home | Memphis ARTCC";
@@ -27,6 +33,7 @@ export default function Home() {
setUser(auth.user);
setIsMember(auth.user.isMember);
}
+
const fetchTopControllers = async () => {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/stats/top`, {
headers: {
@@ -40,6 +47,20 @@ export default function Home() {
}
return await response.json() as Response;
};
+
+ const fetchNews = async () => {
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/news`, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (!response.ok) {
+ const error = await response.json() as Response;
+ throw new Error(`Error fetching news:\n${error.message}`);
+ }
+ return await response.json() as Response;
+ };
+
fetchTopControllers()
.then((response) => {
setTop(response.data);
@@ -50,6 +71,17 @@ export default function Home() {
toast.error(error);
setLoadingTop(false);
});
+
+ fetchNews()
+ .then((response) => {
+ setNews(response.data);
+ setloadingNews(false);
+ })
+ .catch((error) => {
+ console.log(error);
+ toast.error(error);
+ setloadingNews(false);
+ });
}, [auth]);
return (
@@ -70,7 +102,7 @@ export default function Home() {
If you would like to become a visitor, please click the button below.
- router.push("/visit")}>
Become a Visitor
@@ -82,57 +114,76 @@ export default function Home() {
News
- Testing
+ {!loadingNews ? (
+ <>
+ {news.length > 0 ? (
+ <>
+ {news.map((newsEntry) => (
+
+
+
+ {newsEntry.author}
+
+ {newsEntry.title}
+
+
+
{format(newsEntry.updated, "EEEE, MMMM do yyyy, h:mm a")}
+
+ ))}
+ >
+ ) : (
+
No news found
+ )}
+ >
+ ) :
}
Top Controllers
- {top.length > 0 ? (
-
-
-
- {top[1] ? (
- <>
- {top[1].firstName} {top[1].lastName}
- {top[1].totalHours} hrs
- >
- ) : (
- -
- )}
-
-
-
- {top[0] ? (
- <>
- {top[0].firstName} {top[0].lastName}
- {top[0].totalHours} hrs
- >
- ) : (
- -
- )}
-
-
-
- {top[2] ? (
- <>
- {top[2].firstName} {top[2].lastName}
- {top[2].totalHours} hrs
- >
- ) : (
- -
- )}
-
-
- ) : (
+ {!loadingTop ? (
<>
- {loadingTop ? (
-
Loading...
+ {top.length > 0 ? (
+
+
+
+ {top[1] ? (
+ <>
+ {top[1].firstName} {top[1].lastName}
+ {top[1].totalHours} hrs
+ >
+ ) : (
+ -
+ )}
+
+
+
+ {top[0] ? (
+ <>
+ {top[0].firstName} {top[0].lastName}
+ {top[0].totalHours} hrs
+ >
+ ) : (
+ -
+ )}
+
+
+
+ {top[2] ? (
+ <>
+ {top[2].firstName} {top[2].lastName}
+ {top[2].totalHours} hrs
+ >
+ ) : (
+ -
+ )}
+
+
) : (
No top controllers found
)}
>
- )}
+ ) :
}
);
diff --git a/app/staff/page.tsx b/app/staff/page.tsx
index b77c40e..5b77513 100644
--- a/app/staff/page.tsx
+++ b/app/staff/page.tsx
@@ -1,5 +1,6 @@
"use client";
+import Spinner from "@/components/Spinner";
import { Response } from "@/models/response";
import { Staff } from "@/models/staff";
import { useEffect, useState } from "react";
@@ -61,7 +62,7 @@ const StaffTeam = ({ name, team, email }: StaffTeamProps) => {
);
};
-export default function StaffingRequest() {
+export default function StaffPage() {
const [loading, setLoading] = useState(true);
const [staff, setStaff] = useState({} as Staff);
@@ -92,13 +93,6 @@ export default function StaffingRequest() {
});
}, []);
- if (loading) {
- return (
-
-
- );
- }
-
return (
@@ -107,69 +101,73 @@ export default function StaffingRequest() {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {!loading ? (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : }
);
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx
index e8ea771..0cfdee7 100644
--- a/components/Sidebar.tsx
+++ b/components/Sidebar.tsx
@@ -4,12 +4,13 @@ import { Event } from "@/models/event";
import { OnlineController } from "@/models/onlineController";
import { Response } from "@/models/response";
import { PlaneLanding, PlaneTakeoff } from "lucide-react";
+import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
import Spinner from "./Spinner";
-const Sideabr = () => {
+const Sideabar = () => {
const [onlineControllers, setOnlineControllers] = useState([] as OnlineController[]);
const [airports, setAirports] = useState([] as Airport[]);
@@ -37,7 +38,7 @@ const Sideabr = () => {
};
const fetchEvents = async () => {
- const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/events`);
+ const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/events?page=1&size=5`);
if (!response.ok) {
const error = await response.json() as Response;
throw new Error(`Error fetching events:\n${error.message}`);
@@ -148,6 +149,15 @@ const Sideabr = () => {
{events.map((event, index) => (
+
+
+
))}
@@ -160,4 +170,4 @@ const Sideabr = () => {
);
};
-export default Sideabr;
\ No newline at end of file
+export default Sideabar;
\ No newline at end of file
diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx
new file mode 100644
index 0000000..e305d18
--- /dev/null
+++ b/components/ui/tabs.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+import * as React from "react";
+
+const Tabs = TabsPrimitive.Root;
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsList.displayName = TabsPrimitive.List.displayName;
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsContent.displayName = TabsPrimitive.Content.displayName;
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/models/event.ts b/models/event.ts
index dca482c..d473bd8 100644
--- a/models/event.ts
+++ b/models/event.ts
@@ -1,12 +1,11 @@
import { EventPosition } from "./eventPosition";
-import { Upload } from "./upload";
export type Event = {
id: number;
- name: string;
+ title: string;
description: string;
host: string;
- banner: Upload;
+ bannerUrl?: string;
start: Date;
end: Date;
positions: EventPosition[];
diff --git a/models/file.ts b/models/file.ts
new file mode 100644
index 0000000..5911470
--- /dev/null
+++ b/models/file.ts
@@ -0,0 +1,23 @@
+
+export enum FileType {
+ POLICY,
+ SOP,
+ LOA,
+ REFERENCE,
+ MISC,
+ CLIENT,
+ TRAINING_STAFF,
+ STAFF,
+ SENIOR_STAFF
+}
+
+export type File = {
+ id: number;
+ title: string;
+ description: string;
+ version: string;
+ fileUrl?: string;
+ type: FileType;
+ created: Date;
+ updated: Date;
+};
\ No newline at end of file
diff --git a/models/news.ts b/models/news.ts
new file mode 100644
index 0000000..dbc592e
--- /dev/null
+++ b/models/news.ts
@@ -0,0 +1,8 @@
+export type News = {
+ id: number;
+ title: string;
+ content: string;
+ author: string;
+ created: Date;
+ updated: Date;
+};
\ No newline at end of file
diff --git a/next.config.ts b/next.config.ts
index 3d18932..52956c7 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -8,6 +8,10 @@ const nextConfig: NextConfig = {
protocol: "https",
hostname: "memphis.nyc3.cdn.digitaloceanspaces.com",
},
+ {
+ protocol: "https",
+ hostname: "memphis-artcc.nyc3.digitaloceanspaces.com"
+ }
],
},
};
diff --git a/package.json b/package.json
index 23b3ca0..837b764 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.4",
"bootstrap": "^5.3.3",
"class-variance-authority": "^0.7.1",
diff --git a/yarn.lock b/yarn.lock
index ccd6b8c..11c5164 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -778,6 +778,20 @@
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
+"@radix-ui/react-tabs@^1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz#c47c8202dc676dea47676215863d2ef9b141c17a"
+ integrity sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==
+ dependencies:
+ "@radix-ui/primitive" "1.1.1"
+ "@radix-ui/react-context" "1.1.1"
+ "@radix-ui/react-direction" "1.1.0"
+ "@radix-ui/react-id" "1.1.0"
+ "@radix-ui/react-presence" "1.1.2"
+ "@radix-ui/react-primitive" "2.0.2"
+ "@radix-ui/react-roving-focus" "1.1.2"
+ "@radix-ui/react-use-controllable-state" "1.1.0"
+
"@radix-ui/react-tooltip@^1.1.4":
version "1.1.8"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz#1aa2a575630fca2b2845b62f85056bb826bec456"