1
0
Fork 0

feat(users): add discordId and Discord notification preferences

This commit is contained in:
Jason Fraley 2026-08-12 12:38:43 -04:00
parent bbfbd5966c
commit 83879687ae
5 changed files with 123 additions and 22 deletions

View file

@ -195,6 +195,8 @@ export async function updatePreferences(input: {
mutedAll: boolean;
mutedTypes: string[];
showCallsign: boolean;
discordEnabled: boolean;
discordMutedTypes: string[];
}): Promise<ActionResult> {
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,

View file

@ -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 (
<div className="p-5 flex flex-col gap-6">
@ -82,9 +77,7 @@ export default async function AccountPage() {
</CardDescription>
</CardHeader>
<CardContent>
<DiscordUsernameRequestForm
currentDiscordUsername={fullUser.discordUsername ?? ""}
/>
<DiscordUsernameRequestForm currentDiscordUsername={fullUser.discordUsername ?? ""} />
</CardContent>
</Card>
@ -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 ?? []}
/>
</CardContent>
</Card>

View file

@ -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 })),
},
],
},
],
},
],

View file

@ -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<string[]>(initialMutedTypes);
const [showCallsign, setShowCallsign] = useState(initialShowCallsign);
const [discordEnabled, setDiscordEnabled] = useState(initialDiscordEnabled);
const [discordMutedTypes, setDiscordMutedTypes] = useState<string[]>(initialDiscordMutedTypes);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(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<HTMLFormElement>) => {
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({
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Notifications</h3>
<label className="flex items-center gap-2 text-sm">
<Checkbox
checked={mutedAll}
onCheckedChange={(c) => setMutedAll(c === true)}
/>
<Checkbox checked={mutedAll} onCheckedChange={(c) => setMutedAll(c === true)} />
Mute all in-app notifications
</label>
<div
@ -87,14 +102,39 @@ export function PreferencesForm({
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Display</h3>
<label className="flex items-center gap-2 text-sm">
<Checkbox
checked={showCallsign}
onCheckedChange={(c) => setShowCallsign(c === true)}
/>
<Checkbox checked={showCallsign} onCheckedChange={(c) => setShowCallsign(c === true)} />
Show callsign (username) instead of display name
</label>
</div>
<div className="flex flex-col gap-3">
<h3 className="text-sm font-medium">Discord</h3>
<label className="flex items-center gap-2 text-sm">
<Checkbox
checked={discordEnabled}
onCheckedChange={(c) => setDiscordEnabled(c === true)}
/>
Discord notifications (DM me about app events)
</label>
<div
className={
discordEnabled
? "flex flex-col gap-3 pl-6"
: "flex flex-col gap-3 pl-6 opacity-50 pointer-events-none"
}
>
{MUTEABLE_NOTIFICATION_TYPES.map((t) => (
<label key={t.value} className="flex items-center gap-2 text-sm">
<Checkbox
checked={discordMutedTypes.includes(t.value)}
onCheckedChange={(c) => toggleDiscordType(t.value, c === true)}
/>
Mute {t.label.toLowerCase()}
</label>
))}
</div>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
{success && <p className="text-sm text-emerald-500">{success}</p>}
<div>

View file

@ -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<T extends boolean = true> {
*/
export interface UsersSelect<T extends boolean = true> {
discordUsername?: T;
discordId?: T;
displayName?: T;
payloadDisplayName?: T;
steamId?: T;
@ -2520,6 +2546,12 @@ export interface UsersSelect<T extends boolean = true> {
| {
showCallsign?: T;
};
discord?:
| T
| {
enabled?: T;
mutedTypes?: T;
};
};
updatedAt?: T;
createdAt?: T;