feat(users): add discordId and Discord notification preferences
This commit is contained in:
parent
bbfbd5966c
commit
83879687ae
5 changed files with 123 additions and 22 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 })),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue