From a9af0b5246191222a944b84791ea120a5f04b666 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Sun, 2 Aug 2026 02:04:02 -0400 Subject: [PATCH] feat(auth): add frontend login page and guest landing gate --- src/app/(frontend)/layout.tsx | 45 +++++++---- src/app/(frontend)/page.tsx | 22 +----- src/app/login/layout.tsx | 9 +++ src/app/login/page.tsx | 30 ++++++++ src/components/frontend/LandingPage.tsx | 24 ++++++ src/components/frontend/auth/LoginForm.tsx | 86 ++++++++++++++++++++++ 6 files changed, 180 insertions(+), 36 deletions(-) create mode 100644 src/app/login/layout.tsx create mode 100644 src/app/login/page.tsx create mode 100644 src/components/frontend/LandingPage.tsx create mode 100644 src/components/frontend/auth/LoginForm.tsx diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 1836064..e806d50 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -8,6 +8,10 @@ import { headers as nextHeaders } from "next/headers"; import { SiteHeader } from "@/components/frontend/SiteHeader"; import { Metadata } from "next"; import { resolveLevel, type ResolvedLevel } from "@/utils/xp/resolveLevel"; +import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification"; +import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts"; +import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime"; +import { LandingPage } from "@/components/frontend/LandingPage"; export const metadata: Metadata = { description: "A gamified platform for Polaris Task Force, an Arma 3 unit.", @@ -31,7 +35,12 @@ export default async function RootLayout(props: { children: React.ReactNode }) { xp: 0, }; + let isLogistics = false; + if (user) { + isLogistics = await hasLogisticsQualification(payload, user).catch( + () => false, + ); const [profileRes, experienceRes] = await Promise.all([ payload.find({ collection: "profiles", @@ -61,21 +70,27 @@ export default async function RootLayout(props: { children: React.ReactNode }) { return ( - - -
- - {children} -
-
+ {user ? ( + + +
+ + {children} +
+
+ ) : ( + + )} + {user && } + {isLogistics && } ); diff --git a/src/app/(frontend)/page.tsx b/src/app/(frontend)/page.tsx index d77c42b..b069e5d 100644 --- a/src/app/(frontend)/page.tsx +++ b/src/app/(frontend)/page.tsx @@ -1,7 +1,6 @@ import config from "@payload-config"; import { getPayload } from "payload"; import { headers as nextHeaders } from "next/headers"; -import Link from "next/link"; import type { User, Rank, @@ -18,32 +17,13 @@ import { RecentEvents } from "@/components/frontend/dashboard/RecentEvents"; import { QuickStats } from "@/components/frontend/dashboard/QuickStats"; import { UpcomingMissions } from "@/components/frontend/dashboard/UpcomingMissions"; import { ActiveCampaigns } from "@/components/frontend/dashboard/ActiveCampaigns"; -import { ArrowRightIcon, ShieldIcon } from "lucide-react"; export default async function HomePage() { const payload = await getPayload({ config }); const headers = await nextHeaders(); const { user } = await payload.auth({ headers, canSetHeaders: false }); - if (!user) { - return ( -
- -
-

Welcome to Polaris Task Force

-

- Log in to access the operations dashboard. -

-
- - Go to login - -
- ); - } + if (!user) return null; const userId = user.id; diff --git a/src/app/login/layout.tsx b/src/app/login/layout.tsx new file mode 100644 index 0000000..20a6439 --- /dev/null +++ b/src/app/login/layout.tsx @@ -0,0 +1,9 @@ +import "@/app/(frontend)/styles.css"; + +export default function LoginLayout(props: { children: React.ReactNode }) { + return ( + + {props.children} + + ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..4199098 --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,30 @@ +import { getPayload } from "payload"; +import config from "@payload-config"; +import { headers as nextHeaders } from "next/headers"; +import { redirect } from "next/navigation"; +import { ShieldIcon } from "lucide-react"; +import { LoginForm } from "@/components/frontend/auth/LoginForm"; + +export default async function LoginPage() { + const payload = await getPayload({ config }); + const headers = await nextHeaders(); + const { user } = await payload.auth({ headers, canSetHeaders: false }); + + if (user) redirect("/"); + + return ( +
+
+
+ +
+

Polaris Task Force

+

+ Log in to access the operations dashboard, intelligence, and + logistics. +

+
+ +
+ ); +} diff --git a/src/components/frontend/LandingPage.tsx b/src/components/frontend/LandingPage.tsx new file mode 100644 index 0000000..b5219de --- /dev/null +++ b/src/components/frontend/LandingPage.tsx @@ -0,0 +1,24 @@ +import Link from "next/link"; +import { ShieldIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +export function LandingPage() { + return ( +
+
+
+ +
+

Polaris Task Force

+

+ Classified operations portal for Polaris Task Force, an Arma 3 unit. + Log in to access the operations dashboard, intelligence, and + logistics. +

+ +
+
+ ); +} diff --git a/src/components/frontend/auth/LoginForm.tsx b/src/components/frontend/auth/LoginForm.tsx new file mode 100644 index 0000000..42897f1 --- /dev/null +++ b/src/components/frontend/auth/LoginForm.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Loader2Icon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export function LoginForm() { + const router = useRouter(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + const res = await fetch("/api/users/login", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + + const body = await res.json().catch(() => null); + + if (!res.ok) { + setError( + body?.errors?.[0]?.message ?? "Invalid username or password.", + ); + return; + } + + router.push("/"); + router.refresh(); + } catch { + setError("Something went wrong. Please try again."); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ + setUsername(e.target.value)} + autoComplete="username" + autoFocus + required + /> +
+
+ + setPassword(e.target.value)} + autoComplete="current-password" + required + /> +
+ {error &&

{error}

} + +

+ Military personnel only. Authorized access required. +

+
+ ); +}