- Fetch current user's leadOrActual preference in roster page via payload.auth() - Add RosterDisplayContext to avoid prop drilling through org chart components - MemberCard shows 'Lead' or 'Actual' based on user preference - Add preference radio buttons to PreferencesForm under Display section - Update updatePreferences server action to save leadOrActual - Add TODO for restyling radio buttons later
182 lines
6.2 KiB
TypeScript
182 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } 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";
|
|
|
|
interface PreferencesFormProps {
|
|
mutedAll: boolean;
|
|
mutedTypes: string[];
|
|
showCallsign: boolean;
|
|
leadOrActual: "lead" | "actual";
|
|
discordEnabled: boolean;
|
|
discordMutedTypes: string[];
|
|
}
|
|
|
|
export function PreferencesForm({
|
|
mutedAll: initialMutedAll,
|
|
mutedTypes: initialMutedTypes,
|
|
showCallsign: initialShowCallsign,
|
|
leadOrActual: initialLeadOrActual,
|
|
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 [leadOrActual, setLeadOrActual] = useState<"lead" | "actual">(initialLeadOrActual);
|
|
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);
|
|
|
|
const toggleType = (value: string, checked: boolean) => {
|
|
setMutedTypes((prev) =>
|
|
checked ? [...new Set([...prev, value])] : prev.filter((t) => t !== value),
|
|
);
|
|
};
|
|
|
|
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);
|
|
setError(null);
|
|
setSuccess(null);
|
|
|
|
try {
|
|
const res = await updatePreferences({
|
|
mutedAll,
|
|
mutedTypes,
|
|
showCallsign,
|
|
leadOrActual,
|
|
discordEnabled,
|
|
discordMutedTypes,
|
|
});
|
|
if (!res.success) {
|
|
setError(res.error ?? "Failed to save preferences.");
|
|
return;
|
|
}
|
|
setSuccess("Preferences saved.");
|
|
router.refresh();
|
|
} catch {
|
|
setError("Something went wrong. Please try again.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
|
<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)} />
|
|
Mute all in-app notifications
|
|
</label>
|
|
<div
|
|
className={
|
|
mutedAll
|
|
? "flex flex-col gap-3 pl-6 opacity-50 pointer-events-none"
|
|
: "flex flex-col gap-3 pl-6"
|
|
}
|
|
>
|
|
{MUTEABLE_NOTIFICATION_TYPES.map((t) => (
|
|
<label key={t.value} className="flex items-center gap-2 text-sm">
|
|
<Checkbox
|
|
checked={mutedTypes.includes(t.value)}
|
|
onCheckedChange={(c) => toggleType(t.value, c === true)}
|
|
/>
|
|
{t.label}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<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)} />
|
|
Show callsign (username) instead of display name
|
|
</label>
|
|
<div className="flex flex-col gap-2 pl-6">
|
|
<Label className="text-sm text-muted-foreground">
|
|
Roster leader label
|
|
</Label>
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input
|
|
type="radio"
|
|
name="leadOrActual"
|
|
value="lead"
|
|
checked={leadOrActual === "lead"}
|
|
onChange={() => setLeadOrActual("lead")}
|
|
className="accent-primary"
|
|
/>
|
|
Lead
|
|
</label>
|
|
<label className="flex items-center gap-2 text-sm">
|
|
<input
|
|
type="radio"
|
|
name="leadOrActual"
|
|
value="actual"
|
|
checked={leadOrActual === "actual"}
|
|
onChange={() => setLeadOrActual("actual")}
|
|
className="accent-primary"
|
|
/>
|
|
Actual
|
|
</label>
|
|
</div>
|
|
</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>}
|
|
{success && <p className="text-sm text-emerald-500">{success}</p>}
|
|
<div>
|
|
<Button type="submit" disabled={loading}>
|
|
{loading && <Loader2Icon className="size-4 animate-spin" />}
|
|
{loading ? "Saving..." : "Save preferences"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|