diff --git a/src/app/(frontend)/account/actions.ts b/src/app/(frontend)/account/actions.ts index f3bf76f..cbde0d1 100644 --- a/src/app/(frontend)/account/actions.ts +++ b/src/app/(frontend)/account/actions.ts @@ -203,6 +203,7 @@ export async function updatePreferences(input: { mutedAll: boolean; mutedTypes: string[]; showCallsign: boolean; + showVersionOverlay: boolean; leadOrActual: "lead" | "actual"; discordEnabled: boolean; discordMutedTypes: string[]; @@ -226,6 +227,7 @@ export async function updatePreferences(input: { notifications: { mutedAll: input.mutedAll, mutedTypes }, display: { showCallsign: input.showCallsign, + showVersionOverlay: input.showVersionOverlay, leadOrActual: input.leadOrActual, }, discord: { diff --git a/src/app/(frontend)/account/page.tsx b/src/app/(frontend)/account/page.tsx index cfdb19f..ff68df5 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} + showVersionOverlay={fullUser.preferences?.display?.showVersionOverlay ?? true} leadOrActual={fullUser.preferences?.display?.leadOrActual ?? "lead"} discordEnabled={discord?.enabled ?? false} discordMutedTypes={discord?.mutedTypes ?? []} diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index 239c7cf..95c3399 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -12,11 +12,13 @@ import { hasIntelligenceQualification } from "@/utils/access-control/hasIntellig import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification"; import { ShipmentToasts } from "@/components/frontend/logistics/ShipmentToasts"; import { GameTickRealtime } from "@/components/frontend/realtime/GameTickRealtime"; +import VersionOverlay from "@/components/static/VersionOverlay"; import { LandingPage } from "@/components/frontend/LandingPage"; import { Toaster } from "@/components/ui/sonner"; import { CommandPalette } from "@/components/command-palette/CommandPalette"; import { CommandPaletteProvider } from "@/components/command-palette/CommandPaletteContext"; import { KeyboardShortcutsProvider } from "@/components/command-palette/KeyboardShortcutsProvider"; +import { isSuperuser } from "@/utils/access-control/hasPermission"; // Skip static prerendering of this layout (and all pages under (frontend)/). // The layout calls `getPayload()` at render time to resolve the current user, @@ -33,9 +35,9 @@ export const metadata: Metadata = { { url: "/favicon.webp", type: "image/webp", - rel: "icon" - } - ] + rel: "icon", + }, + ], }; export default async function RootLayout(props: { children: React.ReactNode }) { @@ -129,6 +131,9 @@ export default async function RootLayout(props: { children: React.ReactNode }) { ) : ( )} + {(!user || user.preferences?.display?.showVersionOverlay !== false) && ( + + )} {user && } {isLogistics && } diff --git a/src/collections/users/Users.ts b/src/collections/users/Users.ts index 1a58a78..e32bc5c 100644 --- a/src/collections/users/Users.ts +++ b/src/collections/users/Users.ts @@ -275,7 +275,13 @@ export const Users: CollectionConfig = { { label: "Actual", value: "actual" }, ], defaultValue: "lead", - } + }, + { + name: "showVersionOverlay", + label: "Show version overlay at top of page", + type: "checkbox", + defaultValue: true, + }, ], }, { diff --git a/src/components/frontend/account/PreferencesForm.tsx b/src/components/frontend/account/PreferencesForm.tsx index 72b4115..b725171 100644 --- a/src/components/frontend/account/PreferencesForm.tsx +++ b/src/components/frontend/account/PreferencesForm.tsx @@ -1,77 +1,92 @@ "use client"; -import { useState } from "react"; +import { useState, type FormEvent } from "react"; 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"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; +import { MUTEABLE_NOTIFICATION_TYPES } from "@/lib/notifications/notificationTypes"; interface PreferencesFormProps { - mutedAll: boolean; - mutedTypes: string[]; - showCallsign: boolean; - leadOrActual: "lead" | "actual"; - discordEnabled: boolean; - discordMutedTypes: string[]; + readonly mutedAll: boolean; + readonly mutedTypes: readonly string[]; + readonly showCallsign: boolean; + readonly showVersionOverlay: boolean; + readonly leadOrActual: "lead" | "actual"; + readonly discordEnabled: boolean; + readonly discordMutedTypes: readonly string[]; } export function PreferencesForm({ mutedAll: initialMutedAll, mutedTypes: initialMutedTypes, showCallsign: initialShowCallsign, + showVersionOverlay: initialShowVersionOverlay, leadOrActual: initialLeadOrActual, discordEnabled: initialDiscordEnabled, discordMutedTypes: initialDiscordMutedTypes, }: PreferencesFormProps) { const router = useRouter(); const [mutedAll, setMutedAll] = useState(initialMutedAll); - const [mutedTypes, setMutedTypes] = useState(initialMutedTypes); + const [mutedTypes, setMutedTypes] = useState(() => [...initialMutedTypes]); const [showCallsign, setShowCallsign] = useState(initialShowCallsign); + const [showVersionOverlay, setShowVersionOverlay] = useState(initialShowVersionOverlay); const [leadOrActual, setLeadOrActual] = useState<"lead" | "actual">(initialLeadOrActual); const [discordEnabled, setDiscordEnabled] = useState(initialDiscordEnabled); - const [discordMutedTypes, setDiscordMutedTypes] = useState(initialDiscordMutedTypes); + const [discordMutedTypes, setDiscordMutedTypes] = useState(() => [ + ...initialDiscordMutedTypes, + ]); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [loading, setLoading] = useState(false); const toggleType = (value: string, checked: boolean) => { - setMutedTypes((prev) => - checked ? [...new Set([...prev, value])] : prev.filter((t) => t !== value), + setMutedTypes((previous) => + checked ? [...new Set([...previous, value])] : previous.filter((type) => type !== value), ); }; const toggleDiscordType = (value: string, checked: boolean) => { - setDiscordMutedTypes((prev) => - checked ? [...new Set([...prev, value])] : prev.filter((t) => t !== value), + setDiscordMutedTypes((previous) => + checked ? [...new Set([...previous, value])] : previous.filter((type) => type !== value), ); }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); setLoading(true); setError(null); setSuccess(null); try { - const res = await updatePreferences({ + const result = await updatePreferences({ mutedAll, mutedTypes, showCallsign, + showVersionOverlay, leadOrActual, discordEnabled, discordMutedTypes, }); - if (!res.success) { - setError(res.error ?? "Failed to save preferences."); + + if (!result.success) { + setError(result.error ?? "Could not save preferences."); return; } - setSuccess("Preferences saved."); + + setSuccess("Preferences updated."); router.refresh(); - } catch { - setError("Something went wrong. Please try again."); + } catch (caughtError) { + setError( + caughtError instanceof Error + ? caughtError.message + : "Could not save preferences. Please try again.", + ); } finally { setLoading(false); } @@ -79,101 +94,190 @@ export function PreferencesForm({ return (
-
-

Notifications

- -
- {MUTEABLE_NOTIFICATION_TYPES.map((t) => ( - - ))} -
-
- -
-

Display

- -
-
+ + -
-

Discord

- -
- {MUTEABLE_NOTIFICATION_TYPES.map((t) => ( - - ))} -
-
+ + + Notifications + Control the in-app alerts that appear in your inbox. + + + - {error &&

{error}

} - {success &&

{success}

} -
+
+ {MUTEABLE_NOTIFICATION_TYPES.map((notificationType) => { + const id = `notification-${notificationType.value.replace(":", "-")}`; + return ( + + ); + })} +
+ + + + + + Discord + Choose whether the bot can send you direct messages. + + + + +
+ {MUTEABLE_NOTIFICATION_TYPES.map((notificationType) => { + const id = `discord-${notificationType.value.replace(":", "-")}`; + return ( + + ); + })} +
+
+
+ + {error && ( +

+ {error} +

+ )} + {success && ( +

+ {success} +

+ )} +
diff --git a/src/components/static/VersionOverlay.tsx b/src/components/static/VersionOverlay.tsx index e83d7f2..24f3bfd 100644 --- a/src/components/static/VersionOverlay.tsx +++ b/src/components/static/VersionOverlay.tsx @@ -2,6 +2,9 @@ import { useEffect, useState } from "react"; import type { VersionInfo } from "@/lib/versionInfo"; +import { getPayload } from "payload"; +import config from "@payload-config"; +import { User } from "@/payload-types"; const EMPTY: VersionInfo = { version: null, @@ -30,7 +33,7 @@ function formatCommitDate(iso: string | null): string | null { }); } -const VersionOverlay = () => { +const VersionOverlay = ({ canSeeCommit }: { canSeeCommit: boolean }) => { const [info, setInfo] = useState(null); useEffect(() => { @@ -59,13 +62,15 @@ const VersionOverlay = () => { return (
-
- version {versionLabel} +
+ + version {versionLabel} + {showSecondary && ( - + {lastUpdated && <>last updated {lastUpdated}} - {lastUpdated && commitInfo &&
} - {commitInfo && <>{commitInfo}} + {canSeeCommit && lastUpdated && commitInfo &&
} + {canSeeCommit && commitInfo && {commitInfo}}
)}