1
0
Fork 0

feat(preferences): add background texture toggle

This commit is contained in:
Jason Fraley 2026-08-28 01:20:12 -04:00
parent a6b06569da
commit 1f2f7b6269
10 changed files with 137 additions and 78 deletions

View file

@ -205,6 +205,7 @@ export async function updatePreferences(input: {
mutedTypes: string[]; mutedTypes: string[];
showCallsign: boolean; showCallsign: boolean;
showVersionOverlay: boolean; showVersionOverlay: boolean;
showBackgroundTexture: boolean;
leadOrActual: "lead" | "actual"; leadOrActual: "lead" | "actual";
discordEnabled: boolean; discordEnabled: boolean;
discordMutedTypes: string[]; discordMutedTypes: string[];
@ -221,7 +222,7 @@ export async function updatePreferences(input: {
(MUTEABLE_NOTIFICATION_VALUES as readonly string[]).includes(t), (MUTEABLE_NOTIFICATION_VALUES as readonly string[]).includes(t),
); );
await payload.update({ await payload.update({
collection: "users", collection: "users",
id: userId, id: userId,
data: { data: {
@ -230,6 +231,7 @@ export async function updatePreferences(input: {
display: { display: {
showCallsign: input.showCallsign, showCallsign: input.showCallsign,
showVersionOverlay: input.showVersionOverlay, showVersionOverlay: input.showVersionOverlay,
showBackgroundTexture: input.showBackgroundTexture,
leadOrActual: input.leadOrActual, leadOrActual: input.leadOrActual,
xpDisplayMini: input.xpDisplayMini, xpDisplayMini: input.xpDisplayMini,
}, },

View file

@ -81,24 +81,25 @@ export default async function AccountPage() {
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Preferences</CardTitle> <CardTitle>Preferences</CardTitle>
<CardDescription>Control in-app notifications and display options.</CardDescription> <CardDescription>Control in-app notifications and display options.</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<PreferencesForm <PreferencesForm
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}
showVersionOverlay={fullUser.preferences?.display?.showVersionOverlay ?? true} showVersionOverlay={fullUser.preferences?.display?.showVersionOverlay ?? true}
leadOrActual={fullUser.preferences?.display?.leadOrActual ?? "lead"} showBackgroundTexture={fullUser.preferences?.display?.showBackgroundTexture ?? true}
discordEnabled={discord?.enabled ?? false} leadOrActual={fullUser.preferences?.display?.leadOrActual ?? "lead"}
discordMutedTypes={discord?.mutedTypes ?? []} discordEnabled={discord?.enabled ?? false}
xpDisplayMini={fullUser.preferences?.display?.xpDisplayMini ?? false} discordMutedTypes={discord?.mutedTypes ?? []}
/> xpDisplayMini={fullUser.preferences?.display?.xpDisplayMini ?? false}
</CardContent> />
</Card> </CardContent>
</Card>
</div> </div>
</div> </div>
); );

View file

@ -133,7 +133,13 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
preferences={user.preferences} preferences={user.preferences}
className="border-r" className="border-r"
/> />
<SidebarInset> <SidebarInset
className={
user.preferences?.display?.showBackgroundTexture !== false
? "app-background-texture"
: undefined
}
>
<SiteHeader /> <SiteHeader />
{impersonating && <ImpersonationBanner />} {impersonating && <ImpersonationBanner />}
{children} {children}
@ -156,11 +162,7 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
{user && <GameTickRealtime />} {user && <GameTickRealtime />}
{isLogistics && <ShipmentToasts />} {isLogistics && <ShipmentToasts />}
<Toaster /> <Toaster />
<ShimLoader <ShimLoader shims={shimEntries} userId={user?.id ?? null} userRoles={user?.roles ?? []} />
shims={shimEntries}
userId={user?.id ?? null}
userRoles={user?.roles ?? []}
/>
</body> </body>
</html> </html>
); );

View file

@ -268,41 +268,47 @@ export const Users: CollectionConfig = {
}, },
], ],
}, },
{ {
name: "display", name: "display",
label: "Display", label: "Display",
type: "group", type: "group",
fields: [ fields: [
{ {
name: "showCallsign", name: "showCallsign",
label: "Show callsign (username) instead of display name in the sidebar", label: "Show callsign (username) instead of display name in the sidebar",
type: "checkbox", type: "checkbox",
defaultValue: false, defaultValue: false,
}, },
{ {
name: "leadOrActual", name: "leadOrActual",
label: 'Use "Lead" or "Actual" when reviewing the unit roster', label: 'Use "Lead" or "Actual" when reviewing the unit roster',
type: "select", type: "select",
options: [ options: [
{ label: "Lead", value: "lead" }, { label: "Lead", value: "lead" },
{ label: "Actual", value: "actual" }, { label: "Actual", value: "actual" },
], ],
defaultValue: "lead", defaultValue: "lead",
}, },
{ {
name: "showVersionOverlay", name: "showVersionOverlay",
label: "Show version overlay at top of page", label: "Show version overlay at top of page",
type: "checkbox", type: "checkbox",
defaultValue: true, defaultValue: true,
}, },
{ {
name: "xpDisplayMini", name: "xpDisplayMini",
label: "Use mini mode for XP display", label: "Use mini mode for XP display",
type: "checkbox", type: "checkbox",
defaultValue: false, defaultValue: false,
}, },
], {
}, name: "showBackgroundTexture",
label: "Show diagonal background texture",
type: "checkbox",
defaultValue: true,
},
],
},
{ {
name: "discord", name: "discord",
label: "Discord", label: "Discord",

View file

@ -17,6 +17,7 @@ interface PreferencesFormProps {
readonly mutedTypes: readonly string[]; readonly mutedTypes: readonly string[];
readonly showCallsign: boolean; readonly showCallsign: boolean;
readonly showVersionOverlay: boolean; readonly showVersionOverlay: boolean;
readonly showBackgroundTexture: boolean;
readonly leadOrActual: "lead" | "actual"; readonly leadOrActual: "lead" | "actual";
readonly discordEnabled: boolean; readonly discordEnabled: boolean;
readonly discordMutedTypes: readonly string[]; readonly discordMutedTypes: readonly string[];
@ -28,6 +29,7 @@ export function PreferencesForm({
mutedTypes: initialMutedTypes, mutedTypes: initialMutedTypes,
showCallsign: initialShowCallsign, showCallsign: initialShowCallsign,
showVersionOverlay: initialShowVersionOverlay, showVersionOverlay: initialShowVersionOverlay,
showBackgroundTexture: initialShowBackgroundTexture,
leadOrActual: initialLeadOrActual, leadOrActual: initialLeadOrActual,
discordEnabled: initialDiscordEnabled, discordEnabled: initialDiscordEnabled,
discordMutedTypes: initialDiscordMutedTypes, discordMutedTypes: initialDiscordMutedTypes,
@ -38,6 +40,7 @@ export function PreferencesForm({
const [mutedTypes, setMutedTypes] = useState<string[]>(() => [...initialMutedTypes]); const [mutedTypes, setMutedTypes] = useState<string[]>(() => [...initialMutedTypes]);
const [showCallsign, setShowCallsign] = useState(initialShowCallsign); const [showCallsign, setShowCallsign] = useState(initialShowCallsign);
const [showVersionOverlay, setShowVersionOverlay] = useState(initialShowVersionOverlay); const [showVersionOverlay, setShowVersionOverlay] = useState(initialShowVersionOverlay);
const [showBackgroundTexture, setShowBackgroundTexture] = useState(initialShowBackgroundTexture);
const [leadOrActual, setLeadOrActual] = useState<"lead" | "actual">(initialLeadOrActual); const [leadOrActual, setLeadOrActual] = useState<"lead" | "actual">(initialLeadOrActual);
const [discordEnabled, setDiscordEnabled] = useState(initialDiscordEnabled); const [discordEnabled, setDiscordEnabled] = useState(initialDiscordEnabled);
const [discordMutedTypes, setDiscordMutedTypes] = useState<string[]>(() => [ const [discordMutedTypes, setDiscordMutedTypes] = useState<string[]>(() => [
@ -72,6 +75,7 @@ export function PreferencesForm({
mutedTypes, mutedTypes,
showCallsign, showCallsign,
showVersionOverlay, showVersionOverlay,
showBackgroundTexture,
leadOrActual, leadOrActual,
discordEnabled, discordEnabled,
discordMutedTypes, discordMutedTypes,
@ -104,10 +108,7 @@ export function PreferencesForm({
<CardDescription>Choose what appears in the app shell and roster views.</CardDescription> <CardDescription>Choose what appears in the app shell and roster views.</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
<Label <Label htmlFor="showCallsign" className="flex items-center justify-between gap-4 text-sm">
htmlFor="showCallsign"
className="flex items-center justify-between gap-4 text-sm"
>
<span className="flex min-w-0 flex-col gap-1"> <span className="flex min-w-0 flex-col gap-1">
<span>Show callsign instead of display name</span> <span>Show callsign instead of display name</span>
<span className="text-sm font-normal text-muted-foreground"> <span className="text-sm font-normal text-muted-foreground">
@ -120,9 +121,12 @@ export function PreferencesForm({
onCheckedChange={setShowCallsign} onCheckedChange={setShowCallsign}
aria-label="Show callsign instead of display name" aria-label="Show callsign instead of display name"
/> />
</Label> </Label>
<Label htmlFor="xpDisplayMini" className="flex items-center justify-between gap-4 text-sm"> <Label
htmlFor="xpDisplayMini"
className="flex items-center justify-between gap-4 text-sm"
>
<span className="flex min-w-0 flex-col gap-1"> <span className="flex min-w-0 flex-col gap-1">
<span>Use mini mode for XP display</span> <span>Use mini mode for XP display</span>
<span className="text-sm font-normal text-muted-foreground"> <span className="text-sm font-normal text-muted-foreground">
@ -137,6 +141,24 @@ export function PreferencesForm({
/> />
</Label> </Label>
<Label
htmlFor="showBackgroundTexture"
className="flex items-center justify-between gap-4 text-sm"
>
<span className="flex min-w-0 flex-col gap-1">
<span>Show diagonal background texture</span>
<span className="text-sm font-normal text-muted-foreground">
Adds the subtle tactical line pattern behind page content.
</span>
</span>
<Switch
id="showBackgroundTexture"
checked={showBackgroundTexture}
onCheckedChange={setShowBackgroundTexture}
aria-label="Show diagonal background texture"
/>
</Label>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="text-sm font-medium">Roster leader label</span> <span className="text-sm font-medium">Roster leader label</span>

View file

@ -194,6 +194,7 @@ export function AppSidebar({
mutedTypes: preferences?.notifications?.mutedTypes ?? [], mutedTypes: preferences?.notifications?.mutedTypes ?? [],
showCallsign: preferences?.display?.showCallsign ?? false, showCallsign: preferences?.display?.showCallsign ?? false,
showVersionOverlay: preferences?.display?.showVersionOverlay ?? true, showVersionOverlay: preferences?.display?.showVersionOverlay ?? true,
showBackgroundTexture: preferences?.display?.showBackgroundTexture ?? true,
leadOrActual: preferences?.display?.leadOrActual ?? "lead", leadOrActual: preferences?.display?.leadOrActual ?? "lead",
discordEnabled: preferences?.discord?.enabled ?? false, discordEnabled: preferences?.discord?.enabled ?? false,
discordMutedTypes: preferences?.discord?.mutedTypes ?? [], discordMutedTypes: preferences?.discord?.mutedTypes ?? [],

View file

@ -0,0 +1,15 @@
import { MigrateDownArgs, MigrateUpArgs, sql } from "@payloadcms/db-postgres";
export async function up({ db }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "users"
ADD COLUMN "preferences_display_show_background_texture" boolean DEFAULT true;
`);
}
export async function down({ db }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "users"
DROP COLUMN "preferences_display_show_background_texture";
`);
}

View file

@ -1,39 +1,45 @@
import * as migration_20260820_004157 from './20260820_004157'; import * as migration_20260820_004157 from "./20260820_004157";
import * as migration_20260825_220000 from './20260825_220000'; import * as migration_20260825_220000 from "./20260825_220000";
import * as migration_20260826_050000_repair_shims_auxiliary_tables from './20260826_050000_repair_shims_auxiliary_tables'; import * as migration_20260826_050000_repair_shims_auxiliary_tables from "./20260826_050000_repair_shims_auxiliary_tables";
import * as migration_20260826_060000_add_game_servers_event_target from './20260826_060000_add_game_servers_event_target'; import * as migration_20260826_060000_add_game_servers_event_target from "./20260826_060000_add_game_servers_event_target";
import * as migration_20260827_005757 from './20260827_005757'; import * as migration_20260827_005757 from "./20260827_005757";
import * as migration_20260827_022334_add_eod_minesweeper_best_flags from './20260827_022334_add_eod_minesweeper_best_flags'; import * as migration_20260827_022334_add_eod_minesweeper_best_flags from "./20260827_022334_add_eod_minesweeper_best_flags";
import * as migration_20260828_011500_add_background_texture_preference from "./20260828_011500_add_background_texture_preference";
export const migrations = [ export const migrations = [
{ {
up: migration_20260820_004157.up, up: migration_20260820_004157.up,
down: migration_20260820_004157.down, down: migration_20260820_004157.down,
name: '20260820_004157', name: "20260820_004157",
}, },
{ {
up: migration_20260825_220000.up, up: migration_20260825_220000.up,
down: migration_20260825_220000.down, down: migration_20260825_220000.down,
name: '20260825_220000', name: "20260825_220000",
}, },
{ {
up: migration_20260826_050000_repair_shims_auxiliary_tables.up, up: migration_20260826_050000_repair_shims_auxiliary_tables.up,
down: migration_20260826_050000_repair_shims_auxiliary_tables.down, down: migration_20260826_050000_repair_shims_auxiliary_tables.down,
name: '20260826_050000_repair_shims_auxiliary_tables', name: "20260826_050000_repair_shims_auxiliary_tables",
}, },
{ {
up: migration_20260826_060000_add_game_servers_event_target.up, up: migration_20260826_060000_add_game_servers_event_target.up,
down: migration_20260826_060000_add_game_servers_event_target.down, down: migration_20260826_060000_add_game_servers_event_target.down,
name: '20260826_060000_add_game_servers_event_target', name: "20260826_060000_add_game_servers_event_target",
}, },
{ {
up: migration_20260827_005757.up, up: migration_20260827_005757.up,
down: migration_20260827_005757.down, down: migration_20260827_005757.down,
name: '20260827_005757', name: "20260827_005757",
}, },
{ {
up: migration_20260827_022334_add_eod_minesweeper_best_flags.up, up: migration_20260827_022334_add_eod_minesweeper_best_flags.up,
down: migration_20260827_022334_add_eod_minesweeper_best_flags.down, down: migration_20260827_022334_add_eod_minesweeper_best_flags.down,
name: '20260827_022334_add_eod_minesweeper_best_flags' name: "20260827_022334_add_eod_minesweeper_best_flags",
},
{
up: migration_20260828_011500_add_background_texture_preference.up,
down: migration_20260828_011500_add_background_texture_preference.down,
name: "20260828_011500_add_background_texture_preference",
}, },
]; ];

View file

@ -1174,6 +1174,7 @@ export interface User {
leadOrActual?: ('lead' | 'actual') | null; leadOrActual?: ('lead' | 'actual') | null;
showVersionOverlay?: boolean | null; showVersionOverlay?: boolean | null;
xpDisplayMini?: boolean | null; xpDisplayMini?: boolean | null;
showBackgroundTexture?: boolean | null;
}; };
discord?: { discord?: {
enabled?: boolean | null; enabled?: boolean | null;
@ -3366,6 +3367,7 @@ export interface UsersSelect<T extends boolean = true> {
leadOrActual?: T; leadOrActual?: T;
showVersionOverlay?: T; showVersionOverlay?: T;
xpDisplayMini?: T; xpDisplayMini?: T;
showBackgroundTexture?: T;
}; };
discord?: discord?:
| T | T

View file

@ -37,6 +37,7 @@ describe("PreferencesForm", () => {
mutedTypes: ["market:offer"], mutedTypes: ["market:offer"],
showCallsign: false, showCallsign: false,
showVersionOverlay: true, showVersionOverlay: true,
showBackgroundTexture: true,
leadOrActual: "lead", leadOrActual: "lead",
discordEnabled: false, discordEnabled: false,
discordMutedTypes: [], discordMutedTypes: [],
@ -62,6 +63,7 @@ describe("PreferencesForm", () => {
mutedTypes: ["market:offer"], mutedTypes: ["market:offer"],
showCallsign: true, showCallsign: true,
showVersionOverlay: true, showVersionOverlay: true,
showBackgroundTexture: true,
leadOrActual: "actual", leadOrActual: "actual",
discordEnabled: true, discordEnabled: true,
discordMutedTypes: ["market:offer"], discordMutedTypes: ["market:offer"],