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;
|
mutedAll: boolean;
|
||||||
mutedTypes: string[];
|
mutedTypes: string[];
|
||||||
showCallsign: boolean;
|
showCallsign: boolean;
|
||||||
|
discordEnabled: boolean;
|
||||||
|
discordMutedTypes: string[];
|
||||||
}): Promise<ActionResult> {
|
}): Promise<ActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
|
|
@ -203,6 +205,9 @@ export async function updatePreferences(input: {
|
||||||
const mutedTypes = input.mutedTypes.filter((t): t is MuteableNotificationType =>
|
const mutedTypes = input.mutedTypes.filter((t): t is MuteableNotificationType =>
|
||||||
(MUTEABLE_NOTIFICATION_VALUES as readonly string[]).includes(t),
|
(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({
|
await payload.update({
|
||||||
collection: "users",
|
collection: "users",
|
||||||
|
|
@ -211,6 +216,10 @@ export async function updatePreferences(input: {
|
||||||
preferences: {
|
preferences: {
|
||||||
notifications: { mutedAll: input.mutedAll, mutedTypes },
|
notifications: { mutedAll: input.mutedAll, mutedTypes },
|
||||||
display: { showCallsign: input.showCallsign },
|
display: { showCallsign: input.showCallsign },
|
||||||
|
discord: {
|
||||||
|
enabled: input.discordEnabled,
|
||||||
|
mutedTypes: discordMutedTypes,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
overrideAccess: true,
|
overrideAccess: true,
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,7 @@ import { getPayload } from "payload";
|
||||||
import { headers as nextHeaders } from "next/headers";
|
import { headers as nextHeaders } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { UserCogIcon } from "lucide-react";
|
import { UserCogIcon } from "lucide-react";
|
||||||
import {
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { AccountProfileForm } from "@/components/frontend/account/AccountProfileForm";
|
import { AccountProfileForm } from "@/components/frontend/account/AccountProfileForm";
|
||||||
import { ChangePasswordForm } from "@/components/frontend/account/ChangePasswordForm";
|
import { ChangePasswordForm } from "@/components/frontend/account/ChangePasswordForm";
|
||||||
import { DiscordUsernameRequestForm } from "@/components/frontend/account/DiscordUsernameRequestForm";
|
import { DiscordUsernameRequestForm } from "@/components/frontend/account/DiscordUsernameRequestForm";
|
||||||
|
|
@ -38,6 +32,7 @@ export default async function AccountPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const notifications = fullUser.preferences?.notifications;
|
const notifications = fullUser.preferences?.notifications;
|
||||||
|
const discord = fullUser.preferences?.discord;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-5 flex flex-col gap-6">
|
<div className="p-5 flex flex-col gap-6">
|
||||||
|
|
@ -82,9 +77,7 @@ export default async function AccountPage() {
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<DiscordUsernameRequestForm
|
<DiscordUsernameRequestForm currentDiscordUsername={fullUser.discordUsername ?? ""} />
|
||||||
currentDiscordUsername={fullUser.discordUsername ?? ""}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -98,6 +91,8 @@ export default async function AccountPage() {
|
||||||
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}
|
||||||
|
discordEnabled={discord?.enabled ?? false}
|
||||||
|
discordMutedTypes={discord?.mutedTypes ?? []}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,7 @@ export const Users: CollectionConfig = {
|
||||||
overrideAccess: true,
|
overrideAccess: true,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
payload.logger.error(
|
payload.logger.error(`[Users] Failed to create profile for user ${userId}: ${e}`);
|
||||||
`[Users] Failed to create profile for user ${userId}: ${e}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
|
|
@ -102,6 +100,13 @@ export const Users: CollectionConfig = {
|
||||||
required: true,
|
required: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "discordId",
|
||||||
|
type: "text",
|
||||||
|
label: "Discord User ID",
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "displayName",
|
name: "displayName",
|
||||||
type: "text",
|
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;
|
mutedAll: boolean;
|
||||||
mutedTypes: string[];
|
mutedTypes: string[];
|
||||||
showCallsign: boolean;
|
showCallsign: boolean;
|
||||||
|
discordEnabled: boolean;
|
||||||
|
discordMutedTypes: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PreferencesForm({
|
export function PreferencesForm({
|
||||||
mutedAll: initialMutedAll,
|
mutedAll: initialMutedAll,
|
||||||
mutedTypes: initialMutedTypes,
|
mutedTypes: initialMutedTypes,
|
||||||
showCallsign: initialShowCallsign,
|
showCallsign: initialShowCallsign,
|
||||||
|
discordEnabled: initialDiscordEnabled,
|
||||||
|
discordMutedTypes: initialDiscordMutedTypes,
|
||||||
}: PreferencesFormProps) {
|
}: PreferencesFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [mutedAll, setMutedAll] = useState(initialMutedAll);
|
const [mutedAll, setMutedAll] = useState(initialMutedAll);
|
||||||
const [mutedTypes, setMutedTypes] = useState<string[]>(initialMutedTypes);
|
const [mutedTypes, setMutedTypes] = useState<string[]>(initialMutedTypes);
|
||||||
const [showCallsign, setShowCallsign] = useState(initialShowCallsign);
|
const [showCallsign, setShowCallsign] = useState(initialShowCallsign);
|
||||||
|
const [discordEnabled, setDiscordEnabled] = useState(initialDiscordEnabled);
|
||||||
|
const [discordMutedTypes, setDiscordMutedTypes] = useState<string[]>(initialDiscordMutedTypes);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
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>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -40,7 +52,13 @@ export function PreferencesForm({
|
||||||
setSuccess(null);
|
setSuccess(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await updatePreferences({ mutedAll, mutedTypes, showCallsign });
|
const res = await updatePreferences({
|
||||||
|
mutedAll,
|
||||||
|
mutedTypes,
|
||||||
|
showCallsign,
|
||||||
|
discordEnabled,
|
||||||
|
discordMutedTypes,
|
||||||
|
});
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
setError(res.error ?? "Failed to save preferences.");
|
setError(res.error ?? "Failed to save preferences.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -59,10 +77,7 @@ export function PreferencesForm({
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h3 className="text-sm font-medium">Notifications</h3>
|
<h3 className="text-sm font-medium">Notifications</h3>
|
||||||
<label className="flex items-center gap-2 text-sm">
|
<label className="flex items-center gap-2 text-sm">
|
||||||
<Checkbox
|
<Checkbox checked={mutedAll} onCheckedChange={(c) => setMutedAll(c === true)} />
|
||||||
checked={mutedAll}
|
|
||||||
onCheckedChange={(c) => setMutedAll(c === true)}
|
|
||||||
/>
|
|
||||||
Mute all in-app notifications
|
Mute all in-app notifications
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
|
|
@ -87,14 +102,39 @@ export function PreferencesForm({
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<h3 className="text-sm font-medium">Display</h3>
|
<h3 className="text-sm font-medium">Display</h3>
|
||||||
<label className="flex items-center gap-2 text-sm">
|
<label className="flex items-center gap-2 text-sm">
|
||||||
<Checkbox
|
<Checkbox checked={showCallsign} onCheckedChange={(c) => setShowCallsign(c === true)} />
|
||||||
checked={showCallsign}
|
|
||||||
onCheckedChange={(c) => setShowCallsign(c === true)}
|
|
||||||
/>
|
|
||||||
Show callsign (username) instead of display name
|
Show callsign (username) instead of display name
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>}
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||||
{success && <p className="text-sm text-emerald-500">{success}</p>}
|
{success && <p className="text-sm text-emerald-500">{success}</p>}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1212,6 +1212,7 @@ export interface GameEventLog {
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
discordUsername: string;
|
discordUsername: string;
|
||||||
|
discordId?: string | null;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
payloadDisplayName?: string | null;
|
payloadDisplayName?: string | null;
|
||||||
steamId: string;
|
steamId: string;
|
||||||
|
|
@ -1230,12 +1231,36 @@ export interface User {
|
||||||
| 'market:sold'
|
| 'market:sold'
|
||||||
| 'market:expired'
|
| 'market:expired'
|
||||||
| 'account:discord-request'
|
| 'account:discord-request'
|
||||||
|
| 'finance:deposit'
|
||||||
|
| 'finance:withdraw'
|
||||||
|
| 'finance:transfer'
|
||||||
|
| 'shipment:completed'
|
||||||
)[]
|
)[]
|
||||||
| null;
|
| null;
|
||||||
};
|
};
|
||||||
display?: {
|
display?: {
|
||||||
showCallsign?: boolean | null;
|
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;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
@ -2502,6 +2527,7 @@ export interface GameEventLogsSelect<T extends boolean = true> {
|
||||||
*/
|
*/
|
||||||
export interface UsersSelect<T extends boolean = true> {
|
export interface UsersSelect<T extends boolean = true> {
|
||||||
discordUsername?: T;
|
discordUsername?: T;
|
||||||
|
discordId?: T;
|
||||||
displayName?: T;
|
displayName?: T;
|
||||||
payloadDisplayName?: T;
|
payloadDisplayName?: T;
|
||||||
steamId?: T;
|
steamId?: T;
|
||||||
|
|
@ -2520,6 +2546,12 @@ export interface UsersSelect<T extends boolean = true> {
|
||||||
| {
|
| {
|
||||||
showCallsign?: T;
|
showCallsign?: T;
|
||||||
};
|
};
|
||||||
|
discord?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
enabled?: T;
|
||||||
|
mutedTypes?: T;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue