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