feat: notificaciones toast con sileo (pnpm) + aviso en campana
This commit is contained in:
1 parent
73e6e96d73
commit
0a08fa5192
6 files changed
+301
-105
No files matched your search
@@ -1,8 +1,10 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Fira_Sans } from "next/font/google";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "sileo/styles.css";
|
||||
import "./globals.css";
|
||||
import { AuthProvider } from "@/lib/auth";
|
||||
import SileoToaster from "@/components/SileoToaster";
|
||||
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
@@ -34,7 +36,10 @@ export default function RootLayout({
|
||||
/>
|
||||
</head>
|
||||
<body style={{ fontFamily: "var(--font-fira-sans), sans-serif" }}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
<SileoToaster />
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
getNotifications,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
markAllNotificationsRead,
|
||||
type AppNotification,
|
||||
} from "@/lib/api";
|
||||
import { notify } from "@/lib/notify";
|
||||
import { formatDate } from "@/lib/format";
|
||||
|
||||
const POLL_MS = 60000;
|
||||
@@ -49,17 +50,57 @@ export default function NotificationBell() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [filter, setFilter] = useState("");
|
||||
const [markingAll, setMarkingAll] = useState(false);
|
||||
const seenIds = useRef<Set<number>>(new Set());
|
||||
const initialized = useRef(false);
|
||||
|
||||
const toastNew = useCallback(
|
||||
(list: AppNotification[]) => {
|
||||
const fresh = list.filter((n) => !n.isRead && !seenIds.current.has(n.id));
|
||||
// En la primera carga solo registra; evita una ráfaga de toasts históricos.
|
||||
if (!initialized.current) {
|
||||
initialized.current = true;
|
||||
} else if (fresh.length > 0) {
|
||||
// Un toast por notificación nueva (máx 3) o uno agrupado.
|
||||
if (fresh.length <= 3) {
|
||||
for (const n of fresh) {
|
||||
if (n.ticketId) {
|
||||
const ticketId = n.ticketId;
|
||||
notify.action(n.title, "Ver", () => router.push(`/tickets/${ticketId}`), n.body ?? undefined);
|
||||
} else {
|
||||
notify.info(n.title, n.body ?? undefined);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
notify.info(
|
||||
`${fresh.length} notificaciones nuevas`,
|
||||
fresh
|
||||
.slice(0, 3)
|
||||
.map((n) => n.title)
|
||||
.join(" · ")
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const n of list) seenIds.current.add(n.id);
|
||||
// Acota memoria del set.
|
||||
if (seenIds.current.size > 300) {
|
||||
const keep = new Set(list.map((n) => n.id));
|
||||
seenIds.current = keep;
|
||||
}
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const fetchAll = useCallback(() => {
|
||||
Promise.all([getNotifications(50), getUnreadCount()])
|
||||
.then(([list, { count }]) => {
|
||||
setItems(list);
|
||||
setUnread(count);
|
||||
toastNew(list);
|
||||
})
|
||||
.catch(() => {
|
||||
// Sin sesión o backend caído: se reintenta en el próximo ciclo.
|
||||
});
|
||||
}, []);
|
||||
}, [toastNew]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAll();
|
||||
@@ -103,7 +144,9 @@ export default function NotificationBell() {
|
||||
.catch(() => {});
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch(() => {
|
||||
notify.error("No se pudieron marcar como leídas");
|
||||
})
|
||||
.finally(() => setMarkingAll(false));
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Toaster } from "sileo";
|
||||
|
||||
export default function SileoToaster() {
|
||||
return <Toaster position="bottom-right" />;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { sileo, type SileoOptions } from "sileo";
|
||||
|
||||
// Wrapper fino sobre sileo para unificar copy en español y defaults.
|
||||
// Uso: notify.success("Ticket creado"), notify.error("No se pudo guardar", err)
|
||||
const DEFAULT_DURATION = 5000;
|
||||
|
||||
function base(title: string, description?: string): SileoOptions {
|
||||
return { title, description, duration: DEFAULT_DURATION };
|
||||
}
|
||||
|
||||
export const notify = {
|
||||
success(title: string, description?: string, opts?: Partial<SileoOptions>) {
|
||||
return sileo.success({ ...base(title, description), ...opts });
|
||||
},
|
||||
error(title: string, description?: string, opts?: Partial<SileoOptions>) {
|
||||
return sileo.error({ ...base(title, description), ...opts });
|
||||
},
|
||||
warning(title: string, description?: string, opts?: Partial<SileoOptions>) {
|
||||
return sileo.warning({ ...base(title, description), ...opts });
|
||||
},
|
||||
info(title: string, description?: string, opts?: Partial<SileoOptions>) {
|
||||
return sileo.info({ ...base(title, description), ...opts });
|
||||
},
|
||||
action(
|
||||
title: string,
|
||||
buttonTitle: string,
|
||||
onClick: () => void,
|
||||
description?: string,
|
||||
opts?: Partial<SileoOptions>
|
||||
) {
|
||||
return sileo.action({
|
||||
...base(title, description),
|
||||
button: { title: buttonTitle, onClick },
|
||||
...opts,
|
||||
});
|
||||
},
|
||||
promise<T>(
|
||||
promise: Promise<T> | (() => Promise<T>),
|
||||
messages: {
|
||||
loading: string;
|
||||
success: string | ((data: T) => string);
|
||||
error: string | ((err: unknown) => string);
|
||||
}
|
||||
) {
|
||||
const { loading, success, error } = messages;
|
||||
return sileo.promise(promise, {
|
||||
loading: { title: loading },
|
||||
success:
|
||||
typeof success === "function"
|
||||
? (data: T) => ({ title: (success as (d: T) => string)(data) })
|
||||
: { title: success },
|
||||
error:
|
||||
typeof error === "function"
|
||||
? (err: unknown) => ({ title: (error as (e: unknown) => string)(err) })
|
||||
: { title: error },
|
||||
});
|
||||
},
|
||||
dismiss: sileo.dismiss,
|
||||
clear: sileo.clear,
|
||||
};
|
||||
Reference in new issue
Block a user