1
0
Fork 0

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
This commit is contained in:
Jason Fraley 2026-08-18 02:20:58 -04:00
parent feccbc0ced
commit 4c3c3373d3
6 changed files with 92 additions and 35 deletions

View file

@ -10,6 +10,7 @@
- [ ] Bidding and auctions – Allow sellers to list items as auctions with timed bidding instead of fixed-price buyouts. - [ ] 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. - [ ] 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. - [ ] 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 ## MVP – Priority Deadline by 8/15

View file

@ -195,6 +195,7 @@ export async function updatePreferences(input: {
mutedAll: boolean; mutedAll: boolean;
mutedTypes: string[]; mutedTypes: string[];
showCallsign: boolean; showCallsign: boolean;
leadOrActual: "lead" | "actual";
discordEnabled: boolean; discordEnabled: boolean;
discordMutedTypes: string[]; discordMutedTypes: string[];
}): Promise<ActionResult> { }): Promise<ActionResult> {
@ -215,7 +216,10 @@ export async function updatePreferences(input: {
data: { data: {
preferences: { preferences: {
notifications: { mutedAll: input.mutedAll, mutedTypes }, notifications: { mutedAll: input.mutedAll, mutedTypes },
display: { showCallsign: input.showCallsign }, display: {
showCallsign: input.showCallsign,
leadOrActual: input.leadOrActual,
},
discord: { discord: {
enabled: input.discordEnabled, enabled: input.discordEnabled,
mutedTypes: discordMutedTypes, mutedTypes: discordMutedTypes,

View file

@ -91,6 +91,7 @@ export default async function AccountPage() {
mutedAll={notifications?.mutedAll ?? false} mutedAll={notifications?.mutedAll ?? false}
mutedTypes={notifications?.mutedTypes ?? []} mutedTypes={notifications?.mutedTypes ?? []}
showCallsign={fullUser.preferences?.display?.showCallsign ?? false} showCallsign={fullUser.preferences?.display?.showCallsign ?? false}
leadOrActual={fullUser.preferences?.display?.leadOrActual ?? "lead"}
discordEnabled={discord?.enabled ?? false} discordEnabled={discord?.enabled ?? false}
discordMutedTypes={discord?.mutedTypes ?? []} discordMutedTypes={discord?.mutedTypes ?? []}
/> />

View file

@ -1,5 +1,6 @@
import config from "@payload-config"; import config from "@payload-config";
import { getPayload } from "payload"; import { getPayload } from "payload";
import { headers as nextHeaders } from "next/headers";
import type { Media, Rank, User } from "@/payload-types"; import type { Media, Rank, User } from "@/payload-types";
import { UsersIcon } from "lucide-react"; import { UsersIcon } from "lucide-react";
import { RosterView } from "@/components/frontend/roster/RosterView"; import { RosterView } from "@/components/frontend/roster/RosterView";
@ -31,7 +32,9 @@ export type RosterAssignment = {
}; };
export default async function RosterPage() { export default async function RosterPage() {
const headers = await nextHeaders();
const payload = await getPayload({ config }); const payload = await getPayload({ config });
const { user: currentUser } = await payload.auth({ headers, canSetHeaders: false });
const [profileRes, assignmentRes] = await Promise.all([ const [profileRes, assignmentRes] = await Promise.all([
payload.find({ payload.find({
@ -128,7 +131,11 @@ export default async function RosterPage() {
</p> </p>
</div> </div>
<RosterView members={members} assignments={assignments} /> <RosterView
members={members}
assignments={assignments}
leadOrActual={currentUser?.preferences?.display?.leadOrActual ?? "lead"}
/>
</div> </div>
); );
} }

View file

@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { Loader2Icon } from "lucide-react"; import { Loader2Icon } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { MUTEABLE_NOTIFICATION_TYPES } from "@/lib/notifications/notificationTypes"; import { MUTEABLE_NOTIFICATION_TYPES } from "@/lib/notifications/notificationTypes";
import { updatePreferences } from "@/app/(frontend)/account/actions"; import { updatePreferences } from "@/app/(frontend)/account/actions";
@ -12,6 +13,7 @@ interface PreferencesFormProps {
mutedAll: boolean; mutedAll: boolean;
mutedTypes: string[]; mutedTypes: string[];
showCallsign: boolean; showCallsign: boolean;
leadOrActual: "lead" | "actual";
discordEnabled: boolean; discordEnabled: boolean;
discordMutedTypes: string[]; discordMutedTypes: string[];
} }
@ -20,6 +22,7 @@ export function PreferencesForm({
mutedAll: initialMutedAll, mutedAll: initialMutedAll,
mutedTypes: initialMutedTypes, mutedTypes: initialMutedTypes,
showCallsign: initialShowCallsign, showCallsign: initialShowCallsign,
leadOrActual: initialLeadOrActual,
discordEnabled: initialDiscordEnabled, discordEnabled: initialDiscordEnabled,
discordMutedTypes: initialDiscordMutedTypes, discordMutedTypes: initialDiscordMutedTypes,
}: PreferencesFormProps) { }: PreferencesFormProps) {
@ -27,6 +30,7 @@ export function PreferencesForm({
const [mutedAll, setMutedAll] = useState(initialMutedAll); const [mutedAll, setMutedAll] = useState(initialMutedAll);
const [mutedTypes, setMutedTypes] = useState<string[]>(initialMutedTypes); const [mutedTypes, setMutedTypes] = useState<string[]>(initialMutedTypes);
const [showCallsign, setShowCallsign] = useState(initialShowCallsign); const [showCallsign, setShowCallsign] = useState(initialShowCallsign);
const [leadOrActual, setLeadOrActual] = useState<"lead" | "actual">(initialLeadOrActual);
const [discordEnabled, setDiscordEnabled] = useState(initialDiscordEnabled); const [discordEnabled, setDiscordEnabled] = useState(initialDiscordEnabled);
const [discordMutedTypes, setDiscordMutedTypes] = useState<string[]>(initialDiscordMutedTypes); const [discordMutedTypes, setDiscordMutedTypes] = useState<string[]>(initialDiscordMutedTypes);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -56,6 +60,7 @@ export function PreferencesForm({
mutedAll, mutedAll,
mutedTypes, mutedTypes,
showCallsign, showCallsign,
leadOrActual,
discordEnabled, discordEnabled,
discordMutedTypes, discordMutedTypes,
}); });
@ -105,6 +110,35 @@ export function PreferencesForm({
<Checkbox checked={showCallsign} onCheckedChange={(c) => setShowCallsign(c === true)} /> <Checkbox checked={showCallsign} onCheckedChange={(c) => setShowCallsign(c === true)} />
Show callsign (username) instead of display name Show callsign (username) instead of display name
</label> </label>
<div className="flex flex-col gap-2 pl-6">
<Label className="text-sm text-muted-foreground">
Roster leader label
</Label>
<div className="flex gap-4">
<label className="flex items-center gap-2 text-sm">
<input
type="radio"
name="leadOrActual"
value="lead"
checked={leadOrActual === "lead"}
onChange={() => setLeadOrActual("lead")}
className="accent-primary"
/>
Lead
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="radio"
name="leadOrActual"
value="actual"
checked={leadOrActual === "actual"}
onChange={() => setLeadOrActual("actual")}
className="accent-primary"
/>
Actual
</label>
</div>
</div>
</div> </div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">

View file

@ -1,11 +1,14 @@
"use client"; "use client";
import * as React from "react";
import Link from "next/link"; import Link from "next/link";
import { cn, pluralize } from "@/lib/utils"; import { cn, pluralize } from "@/lib/utils";
import type { RosterAssignment, RosterMember } from "@/app/(frontend)/roster/page"; import type { RosterAssignment, RosterMember } from "@/app/(frontend)/roster/page";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Building2Icon, CrownIcon, ListIcon, NetworkIcon, ShieldIcon } from "lucide-react"; import { Building2Icon, CrownIcon, ListIcon, NetworkIcon, ShieldIcon } from "lucide-react";
const RosterDisplayContext = React.createContext<"lead" | "actual">("lead");
const TYPE_ORDER: Record<string, number> = { const TYPE_ORDER: Record<string, number> = {
division: 0, division: 0,
squad: 1, squad: 1,
@ -346,6 +349,9 @@ function AlphabeticalView({ members }: { members: RosterMember[] }) {
// ─── Member Card ──────────────────────────────────────────────────── // ─── Member Card ────────────────────────────────────────────────────
function MemberCard({ member }: { member: RosterMember }) { function MemberCard({ member }: { member: RosterMember }) {
const leadOrActual = React.useContext(RosterDisplayContext);
const leadLabel = leadOrActual === "actual" ? "Actual" : "Lead";
return ( return (
<Link <Link
href={`/profile/${member.user.username}`} href={`/profile/${member.user.username}`}
@ -367,7 +373,7 @@ function MemberCard({ member }: { member: RosterMember }) {
<div className="flex items-center gap-1.5 shrink-0"> <div className="flex items-center gap-1.5 shrink-0">
{member.isLeader && ( {member.isLeader && (
<span className="text-[10px] uppercase tracking-wider text-amber-400 bg-amber-950/50 border border-amber-800/50 px-1.5 py-0.5 rounded font-semibold"> <span className="text-[10px] uppercase tracking-wider text-amber-400 bg-amber-950/50 border border-amber-800/50 px-1.5 py-0.5 rounded font-semibold">
Lead {leadLabel}
</span> </span>
)} )}
{member.assignmentName && ( {member.assignmentName && (
@ -385,43 +391,47 @@ function MemberCard({ member }: { member: RosterMember }) {
export function RosterView({ export function RosterView({
members, members,
assignments, assignments,
leadOrActual = "lead",
}: { }: {
members: RosterMember[]; members: RosterMember[];
assignments: RosterAssignment[]; assignments: RosterAssignment[];
leadOrActual?: "lead" | "actual";
}) { }) {
return ( return (
<Tabs defaultValue="org"> <RosterDisplayContext.Provider value={leadOrActual}>
<TabsList variant="line"> <Tabs defaultValue="org">
<TabsTrigger value="org"> <TabsList variant="line">
<NetworkIcon className="size-3" /> <TabsTrigger value="org">
Org Chart <NetworkIcon className="size-3" />
</TabsTrigger> Org Chart
<TabsTrigger value="assignment"> </TabsTrigger>
<Building2Icon className="size-3" /> <TabsTrigger value="assignment">
By Assignment <Building2Icon className="size-3" />
</TabsTrigger> By Assignment
<TabsTrigger value="rank"> </TabsTrigger>
<ShieldIcon className="size-3" /> <TabsTrigger value="rank">
By Rank <ShieldIcon className="size-3" />
</TabsTrigger> By Rank
<TabsTrigger value="alpha"> </TabsTrigger>
<ListIcon className="size-3" /> <TabsTrigger value="alpha">
A–Z <ListIcon className="size-3" />
</TabsTrigger> A–Z
</TabsList> </TabsTrigger>
</TabsList>
<TabsContent value="org"> <TabsContent value="org">
<OrgChartView assignments={assignments} members={members} /> <OrgChartView assignments={assignments} members={members} />
</TabsContent> </TabsContent>
<TabsContent value="assignment"> <TabsContent value="assignment">
<ByAssignmentView assignments={assignments} members={members} /> <ByAssignmentView assignments={assignments} members={members} />
</TabsContent> </TabsContent>
<TabsContent value="rank"> <TabsContent value="rank">
<ByRankView members={members} /> <ByRankView members={members} />
</TabsContent> </TabsContent>
<TabsContent value="alpha"> <TabsContent value="alpha">
<AlphabeticalView members={members} /> <AlphabeticalView members={members} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</RosterDisplayContext.Provider>
); );
} }