All checks were successful
Deploy to Netlify / Deploy to Netlify (push) Successful in 1m48s
382 lines
13 KiB
TypeScript
382 lines
13 KiB
TypeScript
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 === currentSubscriptionId
|
||
);
|
||
useEffect(() => {
|
||
setEvents(currentSubscription?.events || []);
|
||
}, [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 l’application 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>Select Notification Events</CardTitle>
|
||
<CardDescription>
|
||
Choose which events you want to receive notifications for
|
||
</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.map((event) => event.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>
|
||
);
|
||
}
|