diff --git a/src/app/(frontend)/dashboard/page.tsx b/src/app/(frontend)/dashboard/page.tsx
index 61c0faf..24119b8 100644
--- a/src/app/(frontend)/dashboard/page.tsx
+++ b/src/app/(frontend)/dashboard/page.tsx
@@ -1,21 +1,5 @@
-import { AppSidebar } from "@/components/frontend/blocks/AppSidebar"
-import {
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from "@/components/ui/breadcrumb"
-import { Separator } from "@/components/ui/separator"
-import {
- SidebarInset,
- SidebarProvider,
- SidebarTrigger,
-} from "@/components/ui/sidebar"
+import { redirect } from "next/navigation";
export default function DashboardPage() {
- return (
-
test
- )
+ redirect("/");
}
diff --git a/src/app/(frontend)/page.tsx b/src/app/(frontend)/page.tsx
index 07c06c8..d77c42b 100644
--- a/src/app/(frontend)/page.tsx
+++ b/src/app/(frontend)/page.tsx
@@ -1,21 +1,275 @@
-import React from "react";
-import "./styles.css";
-import { PayloadButtons } from "@/components/frontend/PayloadButtons";
-import { SiteHeader } from "@/components/frontend/SiteHeader";
+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,
+ Media,
+ Assignment,
+ Campaign,
+ Map,
+ GameStructure,
+} from "@/payload-types";
+import { resolveLevel } from "@/utils/xp/resolveLevel";
+import { ProfileSummary } from "@/components/frontend/dashboard/ProfileSummary";
+import { MissionBriefing } from "@/components/frontend/dashboard/MissionBriefing";
+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 headers = await getHeaders();
- // const payloadConfig = await config;
- // const payload = await getPayload({ config: payloadConfig });
- // const { user } = await payload.auth({ headers });
+ const payload = await getPayload({ config });
+ const headers = await nextHeaders();
+ const { user } = await payload.auth({ headers, canSetHeaders: false });
- // const fileURL = `webstorm://file/${fileURLToPath(import.meta.url)}`;
+ if (!user) {
+ return (
+
+
+
+
Welcome to Polaris Task Force
+
+ Log in to access the operations dashboard.
+
+
+
+ Go to login
+
+
+ );
+ }
+
+ const userId = user.id;
+
+ const [profileRes, experienceRes, assignmentRes] = await Promise.all([
+ payload.find({
+ collection: "profiles",
+ where: { user: { equals: userId } },
+ depth: 2,
+ select: {
+ user: true,
+ rank: true,
+ progression: { xp: true, awards: true },
+ createdAt: true,
+ },
+ }),
+ payload.find({
+ collection: "experience",
+ sort: "requiredPoints",
+ limit: 100,
+ select: { name: true, requiredPoints: true },
+ }),
+ payload.find({
+ collection: "assignments",
+ where: { members: { equals: userId } },
+ limit: 10,
+ select: { name: true, type: true, callsign: true },
+ }),
+ ]);
+
+ const profile = profileRes.docs[0];
+ const xp = profile?.progression?.xp ?? 0;
+ const xpLevel = resolveLevel(xp, experienceRes.docs);
+ const profileUser = profile?.user as User | undefined;
+ const rank = profile?.rank as Rank | undefined;
+ const assignments = assignmentRes.docs as Pick<
+ Assignment,
+ "name" | "type" | "callsign"
+ >[];
+
+ const rankData = rank
+ ? {
+ name: rank.name,
+ abbreviation: rank.abbreviation,
+ insigniaUrl:
+ rank.insignia && typeof rank.insignia === "object"
+ ? (rank.insignia as Media).url
+ : null,
+ }
+ : { name: "Unknown", abbreviation: "?" };
+
+ // Fetch upcoming missions (Scheduled or Ready) — up to 5 for the sidebar
+ const upcomingRes = await payload
+ .find({
+ collection: "missions",
+ where: {
+ "ownershipAndStatus.status": { in: ["Scheduled", "Ready"] },
+ },
+ sort: "classification.startDateTime",
+ limit: 5,
+ depth: 2,
+ })
+ .catch(() => ({ docs: [] }));
+
+ // If no upcoming, get most recently completed for the briefing card
+ let briefingMission = upcomingRes.docs[0] ?? null;
+ if (!briefingMission) {
+ const completedRes = await payload
+ .find({
+ collection: "missions",
+ where: {
+ "ownershipAndStatus.status": { equals: "Completed" },
+ },
+ sort: "-classification.startDateTime",
+ limit: 1,
+ depth: 2,
+ })
+ .catch(() => ({ docs: [] }));
+ briefingMission = completedRes.docs[0] ?? null;
+ }
+
+ const mission = briefingMission
+ ? {
+ id: briefingMission.id,
+ name: briefingMission.name,
+ codeName: briefingMission.codeName,
+ summary: briefingMission.summary,
+ operationType: briefingMission.operationType,
+ classification: {
+ missionType: briefingMission.classification.missionType,
+ startDateTime: briefingMission.classification.startDateTime,
+ estimatedDuration: briefingMission.classification.estimatedDuration,
+ map: briefingMission.classification.map as number | Map,
+ campaign: briefingMission.classification.campaign as number | Campaign,
+ },
+ ownershipAndStatus: {
+ status: briefingMission.ownershipAndStatus.status,
+ },
+ missionRoles: briefingMission.missionRoles
+ ? { maxPlayers: briefingMission.missionRoles.maxPlayers }
+ : null,
+ }
+ : null;
+
+ const upcomingMissions = upcomingRes.docs.map((m) => ({
+ id: m.id,
+ name: m.name,
+ codeName: m.codeName,
+ classification: {
+ startDateTime: m.classification.startDateTime,
+ map: m.classification.map as number | Map,
+ campaign: m.classification.campaign as number | Campaign,
+ },
+ ownershipAndStatus: {
+ status: m.ownershipAndStatus.status,
+ },
+ }));
+
+ // Fetch recent events
+ const eventsRes = await payload.find({
+ collection: "game-event-logs",
+ sort: "-timestamp",
+ limit: 8,
+ depth: 1,
+ });
+
+ const events = eventsRes.docs.map((e) => ({
+ id: e.id,
+ timestamp: e.timestamp,
+ type: e.type,
+ message: e.message,
+ system: e.system ?? true,
+ actor: e.actor as number | User | null | undefined,
+ structure: e.structure as
+ | number
+ | GameStructure
+ | null
+ | undefined,
+ }));
+
+ // Fetch active campaigns for the sidebar
+ const activeCampaignsRes = await payload
+ .find({
+ collection: "campaigns",
+ where: { status: { equals: "inProgress" } },
+ limit: 5,
+ sort: "-createdAt",
+ })
+ .catch(() => ({ docs: [] }));
+
+ const activeCampaigns = activeCampaignsRes.docs.map((c) => ({
+ id: c.id,
+ name: c.name,
+ summary: c.summary,
+ status: c.status,
+ }));
+
+ // Fetch counts
+ const [campaignsRes, missionsCountRes, structuresRes, factionsRes, profilesRes] =
+ await Promise.all([
+ payload.find({
+ collection: "campaigns",
+ where: { status: { equals: "inProgress" } },
+ limit: 0,
+ }),
+ payload.find({
+ collection: "missions",
+ limit: 0,
+ }).catch(() => ({ totalDocs: 0 })),
+ payload.find({
+ collection: "game-structures",
+ limit: 0,
+ }),
+ payload.find({
+ collection: "factions",
+ limit: 0,
+ }),
+ payload.find({
+ collection: "profiles",
+ limit: 0,
+ }),
+ ]);
return (
-
- {/*
*/}
+
+
+
+ Welcome back, {profileUser?.displayName ?? user.username}
+
+
+ Here's your operations dashboard.
+
+
- {/*
*/}
+
+ {/* ── Main Content (2/3) ──────────────────────────── */}
+
+
+ {profile && profileUser && (
+
+ )}
+
+
+
+
+
+
+
+
+ {/* ── Right Sidebar (1/3) ─────────────────────────── */}
+
+
);
}
diff --git a/src/components/frontend/dashboard/ActiveCampaigns.tsx b/src/components/frontend/dashboard/ActiveCampaigns.tsx
new file mode 100644
index 0000000..2c08909
--- /dev/null
+++ b/src/components/frontend/dashboard/ActiveCampaigns.tsx
@@ -0,0 +1,91 @@
+import Link from "next/link";
+import {
+ Item,
+ ItemContent,
+ ItemDescription,
+ ItemHeader,
+ ItemTitle,
+} from "@/components/ui/item";
+import { ChevronRightIcon, FlagIcon } from "lucide-react";
+
+interface CampaignEntry {
+ id: number;
+ name: string;
+ summary: string;
+ status: string;
+}
+
+interface ActiveCampaignsProps {
+ campaigns: CampaignEntry[];
+}
+
+const statusColors: Record
= {
+ inProgress: "bg-blue-500/20 text-blue-400",
+ concept: "bg-muted text-muted-foreground",
+};
+
+export function ActiveCampaigns({ campaigns }: ActiveCampaignsProps) {
+ if (campaigns.length === 0) {
+ return (
+ -
+
+
+
+ Active Campaigns
+
+
+
+
+ No active campaigns.
+
+
+
+ );
+ }
+
+ return (
+ -
+
+
+
+ Active Campaigns
+
+
+ View all
+
+
+
+
+ {campaigns.map((campaign) => (
+
+
+
+
+
+ {campaign.name}
+
+
+ {campaign.status === "inProgress" ? "Active" : campaign.status}
+
+
+
+ {campaign.summary}
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/frontend/dashboard/MissionBriefing.tsx b/src/components/frontend/dashboard/MissionBriefing.tsx
new file mode 100644
index 0000000..5c17b20
--- /dev/null
+++ b/src/components/frontend/dashboard/MissionBriefing.tsx
@@ -0,0 +1,160 @@
+import Link from "next/link";
+import type { Campaign, Map } from "@/payload-types";
+import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
+import {
+ Item,
+ ItemContent,
+ ItemDescription,
+ ItemHeader,
+ ItemTitle,
+} from "@/components/ui/item";
+import {
+ CalendarDaysIcon,
+ ClockIcon,
+ CrosshairIcon,
+ MapPinIcon,
+ UsersIcon,
+} from "lucide-react";
+
+interface MissionBriefingProps {
+ mission: {
+ id: number;
+ name: string;
+ codeName: string;
+ summary: string;
+ operationType: "main" | "side" | "external";
+ classification: {
+ missionType: string;
+ startDateTime: string;
+ estimatedDuration: number;
+ map: number | Map;
+ campaign: number | Campaign;
+ };
+ ownershipAndStatus: {
+ status: string;
+ };
+ missionRoles?: {
+ maxPlayers: number;
+ } | null;
+ } | null;
+}
+
+const statusColors: Record = {
+ Concept: "bg-muted text-muted-foreground",
+ Planning: "bg-muted text-muted-foreground",
+ Ready: "bg-blue-500/20 text-blue-400",
+ Scheduled: "bg-amber-500/20 text-amber-400",
+ Active: "bg-green-500/20 text-green-400",
+ Completed: "bg-emerald-500/20 text-emerald-400",
+ Cancelled: "bg-red-500/20 text-red-400",
+};
+
+export function MissionBriefing({ mission }: MissionBriefingProps) {
+ if (!mission) {
+ return (
+ -
+
+
+
+ Mission Briefing
+
+
+
+
+ No missions currently scheduled.
+
+
+
+ );
+ }
+
+ const map =
+ typeof mission.classification.map === "object"
+ ? (mission.classification.map as Map)
+ : null;
+ const campaign =
+ typeof mission.classification.campaign === "object"
+ ? (mission.classification.campaign as Campaign)
+ : null;
+
+ const startDate = new Date(mission.classification.startDateTime);
+ const dateStr = startDate.toLocaleDateString("en-US", {
+ weekday: "short",
+ month: "short",
+ day: "numeric",
+ });
+ const timeStr = startDate.toLocaleTimeString("en-US", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ const durationHours = Math.floor(mission.classification.estimatedDuration / 60);
+ const durationMins = mission.classification.estimatedDuration % 60;
+ const durationStr =
+ durationMins > 0
+ ? `${durationHours}h ${durationMins}m`
+ : `${durationHours}h`;
+
+ return (
+ -
+
+
+
+ Mission Briefing
+
+
+ View details
+
+
+
+
+
+
+
+ {mission.codeName}
+
+
+ {mission.ownershipAndStatus.status}
+
+
+
{mission.name}
+
+
+
+
+ {mission.summary}
+
+
+
+
+
+ {dateStr} at {timeStr}
+
+
+
+ {durationStr}
+
+ {map && (
+
+
+ {map.name}
+
+ )}
+ {mission.missionRoles?.maxPlayers && (
+
+
+ {mission.missionRoles.maxPlayers} slots
+
+ )}
+ {campaign && (
+ {campaign.name}
+ )}
+
+
+
+ );
+}
diff --git a/src/components/frontend/dashboard/ProfileSummary.tsx b/src/components/frontend/dashboard/ProfileSummary.tsx
new file mode 100644
index 0000000..507266f
--- /dev/null
+++ b/src/components/frontend/dashboard/ProfileSummary.tsx
@@ -0,0 +1,96 @@
+import Link from "next/link";
+import type { Rank, Media, Assignment } from "@/payload-types";
+import type { ResolvedLevel } from "@/utils/xp/resolveLevel";
+import {
+ Item,
+ ItemContent,
+ ItemDescription,
+ ItemHeader,
+ ItemTitle,
+} from "@/components/ui/item";
+import { BadgeCheckIcon, ShieldIcon, SparkleIcon, UserIcon } from "lucide-react";
+
+interface ProfileSummaryProps {
+ displayName: string;
+ username: string;
+ rank: Pick & { insigniaUrl?: string | null };
+ xpLevel: ResolvedLevel;
+ assignments: Pick[];
+}
+
+export function ProfileSummary({
+ displayName,
+ username,
+ rank,
+ xpLevel,
+ assignments,
+}: ProfileSummaryProps) {
+ return (
+ -
+
+
+
+ Service Summary
+
+
+ View full record
+
+
+
+
+ {rank.insigniaUrl && (
+

+ )}
+
+
+ {displayName}
+
+
+ {rank.name}
+ |
+
+ {xpLevel.levelName}
+
+
+
+
+
+
+
+
+ {xpLevel.xp.toLocaleString()} XP
+
+
+
+ {assignments.length > 0 && (
+
+ {assignments.map((a) => (
+
+ {a.callsign ? `[${a.callsign}] ` : ""}
+ {a.name}
+
+ ({a.type})
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/frontend/dashboard/QuickStats.tsx b/src/components/frontend/dashboard/QuickStats.tsx
new file mode 100644
index 0000000..e462ad8
--- /dev/null
+++ b/src/components/frontend/dashboard/QuickStats.tsx
@@ -0,0 +1,79 @@
+import {
+ Item,
+ ItemContent,
+ ItemDescription,
+ ItemMedia,
+ ItemTitle,
+} from "@/components/ui/item";
+import {
+ Building2Icon,
+ CrosshairIcon,
+ ShieldIcon,
+ UsersIcon,
+} from "lucide-react";
+
+interface QuickStatsProps {
+ activeCampaigns: number;
+ totalMissions: number;
+ totalStructures: number;
+ totalFactions: number;
+ totalMembers: number;
+}
+
+export function QuickStats({
+ activeCampaigns,
+ totalMissions,
+ totalStructures,
+ totalFactions,
+ totalMembers,
+}: QuickStatsProps) {
+ return (
+
+ -
+
+
+
+
+ Active Campaigns
+ {activeCampaigns}
+
+
+ -
+
+
+
+
+ Total Missions
+ {totalMissions}
+
+
+ -
+
+
+
+
+ Structures
+ {totalStructures}
+
+
+ -
+
+
+
+
+ Factions
+ {totalFactions}
+
+
+ -
+
+
+
+
+ Operators
+ {totalMembers}
+
+
+
+ );
+}
diff --git a/src/components/frontend/dashboard/RecentEvents.tsx b/src/components/frontend/dashboard/RecentEvents.tsx
new file mode 100644
index 0000000..d9dffdb
--- /dev/null
+++ b/src/components/frontend/dashboard/RecentEvents.tsx
@@ -0,0 +1,104 @@
+import type { User, GameStructure } from "@/payload-types";
+import {
+ Item,
+ ItemContent,
+ ItemDescription,
+ ItemHeader,
+ ItemTitle,
+} from "@/components/ui/item";
+import { ScrollTextIcon } from "lucide-react";
+
+interface EventEntry {
+ id: number;
+ timestamp: string;
+ type: string;
+ message: string;
+ system: boolean;
+ actor?: number | User | null;
+ structure?: number | GameStructure | null;
+}
+
+interface RecentEventsProps {
+ events: EventEntry[];
+ currentUserId?: number;
+}
+
+function timeAgo(dateStr: string): string {
+ const now = new Date();
+ const date = new Date(dateStr);
+ const diffMs = now.getTime() - date.getTime();
+ const diffMins = Math.floor(diffMs / 60000);
+ if (diffMins < 1) return "just now";
+ if (diffMins < 60) return `${diffMins}m ago`;
+ const diffHours = Math.floor(diffMins / 60);
+ if (diffHours < 24) return `${diffHours}h ago`;
+ const diffDays = Math.floor(diffHours / 24);
+ return `${diffDays}d ago`;
+}
+
+export function RecentEvents({ events, currentUserId }: RecentEventsProps) {
+ if (events.length === 0) {
+ return (
+ -
+
+
+
+ Recent Events
+
+
+
+
+ No recent events recorded.
+
+
+
+ );
+ }
+
+ return (
+ -
+
+
+
+ Recent Events
+
+
+
+
+ {events.map((event) => {
+ const actor =
+ typeof event.actor === "object" && event.actor !== null
+ ? (event.actor as User)
+ : null;
+ const actorName =
+ actor?.id === currentUserId
+ ? "You"
+ : actor?.displayName ?? actor?.username ?? null;
+
+ return (
+
+
+ {timeAgo(event.timestamp)}
+
+
+
+ {actorName && (
+ {actorName}
+ )}
+ {event.message}
+
+
+ {event.type}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/src/components/frontend/dashboard/UpcomingMissions.tsx b/src/components/frontend/dashboard/UpcomingMissions.tsx
new file mode 100644
index 0000000..bd4c20c
--- /dev/null
+++ b/src/components/frontend/dashboard/UpcomingMissions.tsx
@@ -0,0 +1,114 @@
+import Link from "next/link";
+import type { Campaign, Map } from "@/payload-types";
+import {
+ Item,
+ ItemContent,
+ ItemDescription,
+ ItemHeader,
+ ItemTitle,
+} from "@/components/ui/item";
+import { CalendarDaysIcon, ChevronRightIcon, CrosshairIcon } from "lucide-react";
+
+interface MissionEntry {
+ id: number;
+ name: string;
+ codeName: string;
+ classification: {
+ startDateTime: string;
+ map: number | Map;
+ campaign: number | Campaign;
+ };
+ ownershipAndStatus: {
+ status: string;
+ };
+}
+
+interface UpcomingMissionsProps {
+ missions: MissionEntry[];
+}
+
+export function UpcomingMissions({ missions }: UpcomingMissionsProps) {
+ if (missions.length === 0) {
+ return (
+ -
+
+
+
+ Upcoming Missions
+
+
+
+
+ No missions scheduled.
+
+
+
+ );
+ }
+
+ return (
+ -
+
+
+
+ Upcoming Missions
+
+
+ View all
+
+
+
+
+ {missions.map((mission) => {
+ const startDate = new Date(mission.classification.startDateTime);
+ const dateStr = startDate.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ });
+ const timeStr = startDate.toLocaleTimeString("en-US", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ const campaign =
+ typeof mission.classification.campaign === "object"
+ ? (mission.classification.campaign as Campaign)
+ : null;
+
+ return (
+
+
+
+ {startDate.toLocaleDateString("en-US", { weekday: "short" })}
+
+ {dateStr.split(" ")[1]}
+ {timeStr}
+
+
+
+ {mission.codeName}
+
+
+ {mission.name}
+
+ {campaign && (
+
+ {campaign.name}
+
+ )}
+
+
+
+ );
+ })}
+
+
+
+ );
+}