From 83879687ae1a7137ebed1868d8eb3a4fc87249f2 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Wed, 12 Aug 2026 12:38:43 -0400 Subject: [PATCH] feat(users): add discordId and Discord notification preferences --- src/app/(frontend)/account/actions.ts | 9 +++ src/app/(frontend)/account/page.tsx | 15 ++--- src/collections/users/Users.ts | 31 +++++++++- .../frontend/account/PreferencesForm.tsx | 58 ++++++++++++++++--- src/payload-types.ts | 32 ++++++++++ 5 files changed, 123 insertions(+), 22 deletions(-) diff --git a/src/app/(frontend)/account/actions.ts b/src/app/(frontend)/account/actions.ts index 523e8b7..b2317ad 100644 --- a/src/app/(frontend)/account/actions.ts +++ b/src/app/(frontend)/account/actions.ts @@ -195,6 +195,8 @@ export async function updatePreferences(input: { mutedAll: boolean; mutedTypes: string[]; showCallsign: boolean; + discordEnabled: boolean; + discordMutedTypes: string[]; }): Promise { try { const { payload, user } = await authenticate(); @@ -203,6 +205,9 @@ export async function updatePreferences(input: { const mutedTypes = input.mutedTypes.filter((t): t is MuteableNotificationType => (MUTEABLE_NOTIFICATION_VALUES as readonly string[]).includes(t), ); + const discordMutedTypes = input.discordMutedTypes.filter((t): t is MuteableNotificationType => + (MUTEABLE_NOTIFICATION_VALUES as readonly string[]).includes(t), + ); await payload.update({ collection: "users", @@ -211,6 +216,10 @@ export async function updatePreferences(input: { preferences: { notifications: { mutedAll: input.mutedAll, mutedTypes }, display: { showCallsign: input.showCallsign }, + discord: { + enabled: input.discordEnabled, + mutedTypes: discordMutedTypes, + }, }, }, overrideAccess: true, diff --git a/src/app/(frontend)/account/page.tsx b/src/app/(frontend)/account/page.tsx index 0ca778e..3416f7b 100644 --- a/src/app/(frontend)/account/page.tsx +++ b/src/app/(frontend)/account/page.tsx @@ -3,13 +3,7 @@ import { getPayload } from "payload"; import { headers as nextHeaders } from "next/headers"; import { redirect } from "next/navigation"; import { UserCogIcon } from "lucide-react"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { AccountProfileForm } from "@/components/frontend/account/AccountProfileForm"; import { ChangePasswordForm } from "@/components/frontend/account/ChangePasswordForm"; import { DiscordUsernameRequestForm } from "@/components/frontend/account/DiscordUsernameRequestForm"; @@ -38,6 +32,7 @@ export default async function AccountPage() { }); const notifications = fullUser.preferences?.notifications; + const discord = fullUser.preferences?.discord; return (
@@ -82,9 +77,7 @@ export default async function AccountPage() { - + @@ -98,6 +91,8 @@ export default async function AccountPage() { mutedAll={notifications?.mutedAll ?? false} mutedTypes={notifications?.mutedTypes ?? []} showCallsign={fullUser.preferences?.display?.showCallsign ?? false} + discordEnabled={discord?.enabled ?? false} + discordMutedTypes={discord?.mutedTypes ?? []} /> diff --git a/src/collections/users/Users.ts b/src/collections/users/Users.ts index c8be969..04a7b42 100644 --- a/src/collections/users/Users.ts +++ b/src/collections/users/Users.ts @@ -48,9 +48,7 @@ export const Users: CollectionConfig = { overrideAccess: true, }); } catch (e) { - payload.logger.error( - `[Users] Failed to create profile for user ${userId}: ${e}`, - ); + payload.logger.error(`[Users] Failed to create profile for user ${userId}: ${e}`); } return doc; @@ -102,6 +100,13 @@ export const Users: CollectionConfig = { required: true, unique: true, }, + { + name: "discordId", + type: "text", + label: "Discord User ID", + unique: true, + index: true, + }, { name: "displayName", type: "text", @@ -207,6 +212,26 @@ export const Users: CollectionConfig = { }, ], }, + { + name: "discord", + label: "Discord", + type: "group", + fields: [ + { + name: "enabled", + label: "Discord notifications", + type: "checkbox", + defaultValue: false, + }, + { + name: "mutedTypes", + label: "Muted notification types", + type: "select", + hasMany: true, + options: MUTEABLE_NOTIFICATION_TYPES.map((t) => ({ label: t.label, value: t.value })), + }, + ], + }, ], }, ], diff --git a/src/components/frontend/account/PreferencesForm.tsx b/src/components/frontend/account/PreferencesForm.tsx index 45cf3d2..6979866 100644 --- a/src/components/frontend/account/PreferencesForm.tsx +++ b/src/components/frontend/account/PreferencesForm.tsx @@ -12,17 +12,23 @@ interface PreferencesFormProps { mutedAll: boolean; mutedTypes: string[]; showCallsign: boolean; + discordEnabled: boolean; + discordMutedTypes: string[]; } export function PreferencesForm({ mutedAll: initialMutedAll, mutedTypes: initialMutedTypes, showCallsign: initialShowCallsign, + discordEnabled: initialDiscordEnabled, + discordMutedTypes: initialDiscordMutedTypes, }: PreferencesFormProps) { const router = useRouter(); const [mutedAll, setMutedAll] = useState(initialMutedAll); const [mutedTypes, setMutedTypes] = useState(initialMutedTypes); const [showCallsign, setShowCallsign] = useState(initialShowCallsign); + const [discordEnabled, setDiscordEnabled] = useState(initialDiscordEnabled); + const [discordMutedTypes, setDiscordMutedTypes] = useState(initialDiscordMutedTypes); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [loading, setLoading] = useState(false); @@ -33,6 +39,12 @@ export function PreferencesForm({ ); }; + const toggleDiscordType = (value: string, checked: boolean) => { + setDiscordMutedTypes((prev) => + checked ? [...new Set([...prev, value])] : prev.filter((t) => t !== value), + ); + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); @@ -40,7 +52,13 @@ export function PreferencesForm({ setSuccess(null); try { - const res = await updatePreferences({ mutedAll, mutedTypes, showCallsign }); + const res = await updatePreferences({ + mutedAll, + mutedTypes, + showCallsign, + discordEnabled, + discordMutedTypes, + }); if (!res.success) { setError(res.error ?? "Failed to save preferences."); return; @@ -59,10 +77,7 @@ export function PreferencesForm({

Notifications

Display

+
+

Discord

+ +
+ {MUTEABLE_NOTIFICATION_TYPES.map((t) => ( + + ))} +
+
+ {error &&

{error}

} {success &&

{success}

}
diff --git a/src/payload-types.ts b/src/payload-types.ts index fb20277..a2c7e95 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -1212,6 +1212,7 @@ export interface GameEventLog { export interface User { id: number; discordUsername: string; + discordId?: string | null; displayName: string; payloadDisplayName?: string | null; steamId: string; @@ -1230,12 +1231,36 @@ export interface User { | 'market:sold' | 'market:expired' | 'account:discord-request' + | 'finance:deposit' + | 'finance:withdraw' + | 'finance:transfer' + | 'shipment:completed' )[] | null; }; display?: { showCallsign?: boolean | null; }; + discord?: { + enabled?: boolean | null; + mutedTypes?: + | ( + | 'market:offer' + | 'market:counter' + | 'market:accept' + | 'market:reject' + | 'market:withdrawn' + | 'market:closed' + | 'market:sold' + | 'market:expired' + | 'account:discord-request' + | 'finance:deposit' + | 'finance:withdraw' + | 'finance:transfer' + | 'shipment:completed' + )[] + | null; + }; }; updatedAt: string; createdAt: string; @@ -2502,6 +2527,7 @@ export interface GameEventLogsSelect { */ export interface UsersSelect { discordUsername?: T; + discordId?: T; displayName?: T; payloadDisplayName?: T; steamId?: T; @@ -2520,6 +2546,12 @@ export interface UsersSelect { | { showCallsign?: T; }; + discord?: + | T + | { + enabled?: T; + mutedTypes?: T; + }; }; updatedAt?: T; createdAt?: T;