From 4c3c3373d3c49665887bddce924ca6a60ac1a683 Mon Sep 17 00:00:00 2001
From: Z8MB1E
Date: Tue, 18 Aug 2026 02:20:58 -0400
Subject: [PATCH] feat(roster): implement leadOrActual display preference
- Fetch current user's leadOrActual preference in roster page via payload.auth()
- Add RosterDisplayContext to avoid prop drilling through org chart components
- MemberCard shows 'Lead' or 'Actual' based on user preference
- Add preference radio buttons to PreferencesForm under Display section
- Update updatePreferences server action to save leadOrActual
- Add TODO for restyling radio buttons later
---
TODO.md | 1 +
src/app/(frontend)/account/actions.ts | 6 +-
src/app/(frontend)/account/page.tsx | 1 +
src/app/(frontend)/roster/page.tsx | 9 ++-
.../frontend/account/PreferencesForm.tsx | 34 +++++++++
src/components/frontend/roster/RosterView.tsx | 76 +++++++++++--------
6 files changed, 92 insertions(+), 35 deletions(-)
diff --git a/TODO.md b/TODO.md
index 455e964..6468211 100644
--- a/TODO.md
+++ b/TODO.md
@@ -10,6 +10,7 @@
- [ ] Bidding and auctions – Allow sellers to list items as auctions with timed bidding instead of fixed-price buyouts.
- [ ] Marketplace fees and market price drift – Percentage listing/sale fees and price fluctuations that tend toward each asset's resting `baseBuyPrice`/`baseSellPrice` over time.
- [ ] Implement command palette – A feature that provides users with a list of available commands and allows them to execute them with a single keystroke or gesture. The command palette should be accessible from a designated keybind or gesture and should display a list of relevant commands based on the user's current context.
+- [ ] Restyle roster leader label radio buttons in PreferencesForm – The native HTML radio buttons work but look out of place; replace with shadcn RadioGroup or SegmentedControl for visual consistency.
## MVP – Priority Deadline by 8/15
diff --git a/src/app/(frontend)/account/actions.ts b/src/app/(frontend)/account/actions.ts
index b2317ad..3f8f5cf 100644
--- a/src/app/(frontend)/account/actions.ts
+++ b/src/app/(frontend)/account/actions.ts
@@ -195,6 +195,7 @@ export async function updatePreferences(input: {
mutedAll: boolean;
mutedTypes: string[];
showCallsign: boolean;
+ leadOrActual: "lead" | "actual";
discordEnabled: boolean;
discordMutedTypes: string[];
}): Promise {
@@ -215,7 +216,10 @@ export async function updatePreferences(input: {
data: {
preferences: {
notifications: { mutedAll: input.mutedAll, mutedTypes },
- display: { showCallsign: input.showCallsign },
+ display: {
+ showCallsign: input.showCallsign,
+ leadOrActual: input.leadOrActual,
+ },
discord: {
enabled: input.discordEnabled,
mutedTypes: discordMutedTypes,
diff --git a/src/app/(frontend)/account/page.tsx b/src/app/(frontend)/account/page.tsx
index 3416f7b..cfdb19f 100644
--- a/src/app/(frontend)/account/page.tsx
+++ b/src/app/(frontend)/account/page.tsx
@@ -91,6 +91,7 @@ export default async function AccountPage() {
mutedAll={notifications?.mutedAll ?? false}
mutedTypes={notifications?.mutedTypes ?? []}
showCallsign={fullUser.preferences?.display?.showCallsign ?? false}
+ leadOrActual={fullUser.preferences?.display?.leadOrActual ?? "lead"}
discordEnabled={discord?.enabled ?? false}
discordMutedTypes={discord?.mutedTypes ?? []}
/>
diff --git a/src/app/(frontend)/roster/page.tsx b/src/app/(frontend)/roster/page.tsx
index 596bf43..dfc1d50 100644
--- a/src/app/(frontend)/roster/page.tsx
+++ b/src/app/(frontend)/roster/page.tsx
@@ -1,5 +1,6 @@
import config from "@payload-config";
import { getPayload } from "payload";
+import { headers as nextHeaders } from "next/headers";
import type { Media, Rank, User } from "@/payload-types";
import { UsersIcon } from "lucide-react";
import { RosterView } from "@/components/frontend/roster/RosterView";
@@ -31,7 +32,9 @@ export type RosterAssignment = {
};
export default async function RosterPage() {
+ const headers = await nextHeaders();
const payload = await getPayload({ config });
+ const { user: currentUser } = await payload.auth({ headers, canSetHeaders: false });
const [profileRes, assignmentRes] = await Promise.all([
payload.find({
@@ -128,7 +131,11 @@ export default async function RosterPage() {
-
+
);
}
diff --git a/src/components/frontend/account/PreferencesForm.tsx b/src/components/frontend/account/PreferencesForm.tsx
index 6979866..72b4115 100644
--- a/src/components/frontend/account/PreferencesForm.tsx
+++ b/src/components/frontend/account/PreferencesForm.tsx
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { Loader2Icon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
+import { Label } from "@/components/ui/label";
import { MUTEABLE_NOTIFICATION_TYPES } from "@/lib/notifications/notificationTypes";
import { updatePreferences } from "@/app/(frontend)/account/actions";
@@ -12,6 +13,7 @@ interface PreferencesFormProps {
mutedAll: boolean;
mutedTypes: string[];
showCallsign: boolean;
+ leadOrActual: "lead" | "actual";
discordEnabled: boolean;
discordMutedTypes: string[];
}
@@ -20,6 +22,7 @@ export function PreferencesForm({
mutedAll: initialMutedAll,
mutedTypes: initialMutedTypes,
showCallsign: initialShowCallsign,
+ leadOrActual: initialLeadOrActual,
discordEnabled: initialDiscordEnabled,
discordMutedTypes: initialDiscordMutedTypes,
}: PreferencesFormProps) {
@@ -27,6 +30,7 @@ export function PreferencesForm({
const [mutedAll, setMutedAll] = useState(initialMutedAll);
const [mutedTypes, setMutedTypes] = useState(initialMutedTypes);
const [showCallsign, setShowCallsign] = useState(initialShowCallsign);
+ const [leadOrActual, setLeadOrActual] = useState<"lead" | "actual">(initialLeadOrActual);
const [discordEnabled, setDiscordEnabled] = useState(initialDiscordEnabled);
const [discordMutedTypes, setDiscordMutedTypes] = useState(initialDiscordMutedTypes);
const [error, setError] = useState(null);
@@ -56,6 +60,7 @@ export function PreferencesForm({
mutedAll,
mutedTypes,
showCallsign,
+ leadOrActual,
discordEnabled,
discordMutedTypes,
});
@@ -105,6 +110,35 @@ export function PreferencesForm({
setShowCallsign(c === true)} />
Show callsign (username) instead of display name
+
diff --git a/src/components/frontend/roster/RosterView.tsx b/src/components/frontend/roster/RosterView.tsx
index 3a2c5e5..33af640 100644
--- a/src/components/frontend/roster/RosterView.tsx
+++ b/src/components/frontend/roster/RosterView.tsx
@@ -1,11 +1,14 @@
"use client";
+import * as React from "react";
import Link from "next/link";
import { cn, pluralize } from "@/lib/utils";
import type { RosterAssignment, RosterMember } from "@/app/(frontend)/roster/page";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Building2Icon, CrownIcon, ListIcon, NetworkIcon, ShieldIcon } from "lucide-react";
+const RosterDisplayContext = React.createContext<"lead" | "actual">("lead");
+
const TYPE_ORDER: Record
= {
division: 0,
squad: 1,
@@ -346,6 +349,9 @@ function AlphabeticalView({ members }: { members: RosterMember[] }) {
// ─── Member Card ────────────────────────────────────────────────────
function MemberCard({ member }: { member: RosterMember }) {
+ const leadOrActual = React.useContext(RosterDisplayContext);
+ const leadLabel = leadOrActual === "actual" ? "Actual" : "Lead";
+
return (
{member.isLeader && (
- Lead
+ {leadLabel}
)}
{member.assignmentName && (
@@ -385,43 +391,47 @@ function MemberCard({ member }: { member: RosterMember }) {
export function RosterView({
members,
assignments,
+ leadOrActual = "lead",
}: {
members: RosterMember[];
assignments: RosterAssignment[];
+ leadOrActual?: "lead" | "actual";
}) {
return (
-
-
-
-
- Org Chart
-
-
-
- By Assignment
-
-
-
- By Rank
-
-
-
- A–Z
-
-
+
+
+
+
+
+ Org Chart
+
+
+
+ By Assignment
+
+
+
+ By Rank
+
+
+
+ A–Z
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}