137 lines
5.7 KiB
TypeScript
137 lines
5.7 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { getNotificationPreferences, updateNotificationPreferences } from "@/client/api";
|
|
import type { NotificationPreferencesResponse } from "@/client/model";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Loader2, Smartphone, ShieldCheck, AlertCircle } from "lucide-react";
|
|
|
|
export function PushSettingsPage() {
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
const [pushEnabled, setPushEnabled] = useState(false);
|
|
const canPush = 'Notification' in window && 'serviceWorker' in navigator;
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
setLoading(true);
|
|
const res = await getNotificationPreferences();
|
|
const data = res.data.data as NotificationPreferencesResponse | undefined;
|
|
setPushEnabled(data?.push_enabled ?? false);
|
|
} catch {
|
|
setError("Failed to load notification settings");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
const handleTogglePush = async (checked: boolean) => {
|
|
if (checked && 'Notification' in window) {
|
|
const permission = await Notification.requestPermission();
|
|
if (permission !== 'granted') {
|
|
setError("Notification permission denied by browser");
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
setSaving(true);
|
|
setError(null);
|
|
await updateNotificationPreferences({
|
|
push_enabled: checked,
|
|
} as Partial<NotificationPreferencesResponse>);
|
|
setPushEnabled(checked);
|
|
setSuccess(true);
|
|
setTimeout(() => setSuccess(false), 3000);
|
|
} catch {
|
|
setError("Failed to update push settings");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 className="w-6 h-6 animate-spin" style={{ color: "var(--text-muted)" }} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="text-[20px] font-bold mb-1" style={{ color: "var(--text-primary)" }}>Push Notifications</h1>
|
|
<p className="text-[13px] mb-6" style={{ color: "var(--text-muted)" }}>
|
|
Receive real-time alerts even when the app is closed.
|
|
</p>
|
|
|
|
{!canPush && (
|
|
<div className="mb-6 p-4 rounded-lg flex items-start gap-3" style={{ backgroundColor: "var(--warning-alpha10)", border: "1px solid var(--warning)" }}>
|
|
<AlertCircle className="w-5 h-5 shrink-0 mt-0.5" style={{ color: "var(--warning)" }} />
|
|
<div className="text-sm" style={{ color: "var(--warning)" }}>
|
|
<p className="font-semibold mb-1">Push notifications are not supported</p>
|
|
<p>Your browser or environment doesn't support Web Push. Please use a modern desktop browser.</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between p-4 rounded-lg border" style={{ backgroundColor: "var(--surface-elevated)", borderColor: "var(--border-default)" }}>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full flex items-center justify-center" style={{ backgroundColor: "var(--accent-bg)" }}>
|
|
<Smartphone className="w-5 h-5" style={{ color: "var(--accent)" }} />
|
|
</div>
|
|
<div>
|
|
<Label className="text-sm font-semibold" style={{ color: "var(--text-primary)" }}>Enable Push Notifications</Label>
|
|
<p className="text-xs" style={{ color: "var(--text-muted)" }}>Get notified of mentions, issues, and system alerts.</p>
|
|
</div>
|
|
</div>
|
|
<Switch
|
|
checked={pushEnabled}
|
|
onCheckedChange={handleTogglePush}
|
|
disabled={saving || !canPush}
|
|
/>
|
|
</div>
|
|
|
|
{pushEnabled && (
|
|
<div className="p-4 rounded-lg border space-y-4" style={{ borderColor: "var(--border-default)" }}>
|
|
<h3 className="text-xs font-semibold uppercase" style={{ color: "var(--text-muted)" }}>Notification Filters</h3>
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span style={{ color: "var(--text-primary)" }}>Mentions & Replies</span>
|
|
<Switch checked={true} disabled />
|
|
</div>
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span style={{ color: "var(--text-primary)" }}>New Issuesss</span>
|
|
<Switch checked={true} disabled />
|
|
</div>
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span style={{ color: "var(--text-primary)" }}>System Updates</span>
|
|
<Switch checked={false} disabled />
|
|
</div>
|
|
<p className="text-[10px] italic" style={{ color: "var(--text-muted)" }}>
|
|
More granular controls coming soon.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mt-4 p-3 rounded text-xs flex items-center gap-2" style={{ backgroundColor: "var(--destructive-alpha10)", border: "1px solid var(--destructive)", color: "var(--destructive)" }}>
|
|
<AlertCircle className="w-4 h-4" />
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{success && (
|
|
<div className="mt-4 p-3 rounded text-xs flex items-center gap-2" style={{ backgroundColor: "var(--success-alpha10)", border: "1px solid var(--success)", color: "var(--success)" }}>
|
|
<ShieldCheck className="w-4 h-4" />
|
|
Settings saved successfully
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |