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 (
+
+ );
+}