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 ( +
+
+
+ Files +
+
+
+ {!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
-
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.
- @@ -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) => (
+ + {event.title} +
))}
@@ -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"