frontend/app/components/settings/notifications.tsx
Nathan Lamy aa88fbda44
All checks were successful
Deploy to Netlify / Deploy to Netlify (push) Successful in 1m26s
chore: fix notification events
2025-08-20 12:46:53 +02:00

390 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Switch } from "~/components/ui/switch";
import { Button } from "~/components/ui/button";
import { Badge } from "~/components/ui/badge";
import { Separator } from "~/components/ui/separator";
import {
Bell,
Smartphone,
Trash2,
Send,
AlertTriangle,
Loader,
} from "lucide-react";
import InstallApp, { isInstalled } from "./install-app";
import {
isNotificationEnabled,
registerNotification,
STORAGE_KEY,
unregisterNotification,
} from "~/lib/notification";
import {
testNotification,
unsubscribe,
updateSubscription,
useNotifications,
type Event,
} from "~/lib/api";
import { toast } from "sonner";
const EVENTS = [
{
id: "SYSTEM",
label: "Système",
description:
"Soyez informé des mises à jour et des nouveautés de Khollisé.",
},
{
id: "COLLE_ADDED",
label: "Colle ajoutée",
description:
"Recevez une notification lorsqu'une nouvelle colle est ajoutée par votre CDT.",
},
{
id: "COLLE_REMOVED",
label: "Colle supprimée",
description: "Recevez une notification lorsqu'une colle est supprimée.",
},
{
id: "COLLE_UPDATED",
label: "Colle modifiée",
description:
"Soyez averti lorsqu'un colleur modifie le contenu d'une colle.",
},
{
id: "GRADE_ADDED",
label: "Note ajoutée",
description: "Recevez une notification lorsqu'une colle est notée.",
},
{
id: "GRADE_UPDATED",
label: "Note modifiée",
description: "Soyez averti lorsqu'une note est modifiée.",
},
{
id: "ROOM_UPDATED",
label: "Salle modifiée",
description: "Soyez averti lorsqu'une colle change de salle.",
},
];
export default function NotificationSettings() {
const [pushEnabled, setPushEnabled] = useState(isNotificationEnabled());
const [isRegistering, setIsRegistering] = useState(false);
async function togglePushEnabled() {
setIsRegistering(true);
if (pushEnabled) {
// Unregister notifications
try {
await unregisterNotification();
setPushEnabled(false);
} catch (error) {
console.error("Failed to unregister notifications:", error);
} finally {
setIsRegistering(false);
}
} else {
// Register notifications
try {
const result = await registerNotification();
if ("error" in result && result.error) {
console.error(
"Failed to unregister previous notifications:",
result.error
);
} else {
setPushEnabled(true);
}
} catch (error) {
console.error("Failed to unregister previous notifications:", error);
} finally {
setIsRegistering(false);
}
}
await refetch();
}
const [events, setEvents] = useState<Event[]>([]);
// TODO: Loader and error handling
const { notifications, isError, isLoading, refetch } = useNotifications();
const toggleEvent = (eventId: string) => {
setEvents(
events.map((event) =>
event.id === eventId ? { ...event, enabled: !event.enabled } : event
)
);
};
const hasEvent = (eventId: string) => {
return events.some((event) => event.id === eventId && event.enabled);
};
const currentSubscriptionId = localStorage.getItem(STORAGE_KEY);
const currentSubscription = notifications.find(
(sub) => sub.id.toString() === currentSubscriptionId
);
useEffect(() => {
setEvents(currentSubscription?.events || []);
// Remove pushEnabled if no current subscription (i.e., user unsubscribed)
if (!isLoading && !isError && pushEnabled && !currentSubscription) {
setPushEnabled(false);
localStorage.removeItem(STORAGE_KEY);
}
}, [currentSubscription]);
async function handleDelete(subscriptionId: string) {
try {
await unsubscribe(subscriptionId);
if (subscriptionId === currentSubscriptionId) {
localStorage.removeItem(STORAGE_KEY);
}
await refetch();
toast.success("L'appareil a été désabonné des notifications.");
} catch (error) {
console.error("Failed to unsubscribe:", error);
toast.error("Échec de la désinscription de l'appareil.");
}
}
return (
<div className="max-w-4xl mx-auto space-y-6 sm:space-y-8">
<div className="space-y-2">
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight">
Paramètres de notifications
</h1>
<p className="text-sm sm:text-base text-muted-foreground">
Gérez vos préférences de notification et vos appareils abonnés.
</p>
</div>
{/* Enable Push Notifications Section */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Bell className="h-5 w-5" />
<CardTitle>Activer les notifications</CardTitle>
</div>
<CardDescription>
Autorisez cette application à vous envoyer des notifications push.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{isInstalled() ? (
pushEnabled ? (
<div className="flex items-start sm:items-center justify-between gap-4">
<div className="space-y-1 flex-1">
<p className="font-medium text-sm sm:text-base">
Notifications
</p>
<p className="text-xs sm:text-sm text-muted-foreground">
Recevez des notifications même lorsque lapplication est
fermée.
</p>
</div>
<Switch
checked={pushEnabled}
onCheckedChange={togglePushEnabled}
disabled={isRegistering}
className="flex-shrink-0"
/>
</div>
) : (
<Button
variant="outline"
className="w-full"
onClick={togglePushEnabled}
disabled={isRegistering}
>
{isRegistering ? (
<span className="flex items-center gap-2">
<Loader className="animate-spin h-4 w-4" />
Activation...
</span>
) : (
<span className="flex items-center gap-2">
<Bell className="h-4 w-4" />
Activer les notifications
</span>
)}
</Button>
)
) : (
<InstallApp />
)}
</CardContent>
</Card>
{/* Select Notification Events Section */}
{currentSubscription && (
<Card>
<CardHeader>
<CardTitle>Configurer vos notifications</CardTitle>
<CardDescription>
Choisissez les événements qui déclencheront une notification.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3 sm:space-y-4">
{EVENTS.map((event, index) => (
<div key={event.id}>
<div className="flex items-start sm:items-center justify-between py-2 gap-4">
<div className="space-y-1 flex-1 min-w-0">
<p className="font-medium text-sm sm:text-base">
{event.label}
</p>
<p className="text-xs sm:text-sm text-muted-foreground leading-relaxed">
{event.description}
</p>
</div>
<Switch
checked={hasEvent(event.id)}
onCheckedChange={() => toggleEvent(event.id)}
aria-label={`Toggle ${event.label} notifications`}
className="flex-shrink-0"
/>
</div>
{index < events.length - 1 && (
<Separator className="mt-3 sm:mt-4" />
)}
</div>
))}
<Button
variant="outline"
className="w-full mt-4"
onClick={() =>
updateSubscription(
currentSubscription.id,
events.filter((e) => e.enabled).map((e) => e.id)
)
.then(() => {
toast.success(
"Vos préférences de notification ont été mises à jour."
);
})
.catch((error) => {
console.error("Failed to update subscription:", error);
toast.error(
"Échec de la mise à jour des préférences de notification."
);
})
}
>
<Send className="h-4 w-4 mr-2" />
Enregistrer
</Button>
</div>
</CardContent>
</Card>
)}
{/* Manage Subscriptions Section */}
<Card>
<CardHeader>
<CardTitle>Manage Subscriptions</CardTitle>
<CardDescription>
View and manage devices that can receive notifications
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3 sm:space-y-4">
{notifications.map((subscription, index) => (
<div key={subscription.id}>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 sm:gap-4">
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className="flex-shrink-0">
<Smartphone className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground" />
</div>
<div className="space-y-1 flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<p className="font-medium text-sm sm:text-base truncate">
{subscription.device}
</p>
{subscription.enabled ? (
<Badge variant="secondary" className="text-xs">
Active
</Badge>
) : (
<Badge
variant="destructive"
className="flex items-center gap-1 text-xs"
>
<AlertTriangle className="h-3 w-3" />
Revoked
</Badge>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2 sm:gap-2 self-start sm:self-center">
<Button
variant="outline"
size="sm"
disabled={!subscription.enabled}
onClick={() => {
testNotification(subscription.id)
.then(() => {
toast.success(
"Vous allez recevoir une notification de test sur votre appareil..."
);
})
.catch((error) => {
console.error(
"Failed to send test notification:",
error
);
toast.error(
"Échec de l'envoi de la notification de test."
);
});
}}
className="flex items-center gap-1 bg-transparent text-xs sm:text-sm px-2 sm:px-3"
>
<Send className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden xs:inline">Test</span>
</Button>
<Button
variant="outline"
size="sm"
className="flex items-center gap-1 text-destructive hover:text-destructive bg-transparent text-xs sm:text-sm px-2 sm:px-3"
onClick={() => handleDelete(subscription.id)}
>
<Trash2 className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden xs:inline">Delete</span>
</Button>
</div>
</div>
{index < notifications.length - 1 && (
<Separator className="mt-3 sm:mt-4" />
)}
</div>
))}
{notifications.length === 0 && (
<div className="text-center py-6 sm:py-8 text-muted-foreground">
<Bell className="h-10 w-10 sm:h-12 sm:w-12 mx-auto mb-3 sm:mb-4 opacity-50" />
<p className="text-sm sm:text-base">
Aucun appareil n'est actuellement abonné aux notifications.
</p>
<p className="text-xs sm:text-sm">
Vous pouvez activer les notifications pour recevoir des
alertes sur vos appareils.
</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
);
}