lightswind
Version:
A collection of beautifully crafted React Components, Blocks & Templates for Modern Developers. Create stunning web applications effortlessly by using our 160+ professional and animated react components.
313 lines (309 loc) • 16.5 kB
JavaScript
// @ts-nocheck
"use client";
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useCallback, useEffect, useRef, useState } from "react";
import { Flipper, Flipped } from "react-flip-toolkit";
import { X } from "lucide-react";
import { cn } from "../../lib/utils";
import { v4 as uuidv4 } from "uuid";
const defaultMessages = [
"Just completed a task! ✅",
"New feature deployed 🚀",
"Check out our latest update 📱",
"Server responded with 200 OK ✨",
"Background job finished 🔄",
"Data synced successfully! 💾",
"User logged in successfully 👋",
"Payment processed 💳",
"Email sent successfully 📧",
"Backup completed 🛡️",
];
const Avatar = ({ user, showAvatar }) => {
if (!showAvatar)
return null;
return (_jsx("div", { className: "flex-shrink-0 w-10 h-10 rounded-full overflow-hidden bg-gradient-to-br from-primary/20 to-primary/40 flex items-center justify-center transition-all duration-300 hover:scale-110 backdrop-blur-sm", style: { backgroundColor: user.color }, children: user.avatarUrl ? (_jsx("img", { src: user.avatarUrl, alt: `${user.name} avatar`, className: "w-full h-full object-cover", loading: "lazy" })) : (_jsx("span", { className: "text-xs font-bold text-white drop-shadow-sm", children: user.initials || user.name.split(" ").map((n) => n[0]).join("").slice(0, 2).toUpperCase() })) }));
};
const Notification = ({ notification, showAvatars, showTimestamps, variant, onDismiss, onClick, allowDismiss }) => {
const getVariantStyles = () => {
switch (variant) {
case "minimal":
return "bg-background/95 border border-border/50 backdrop-blur-xl";
case "glass":
return "bg-background/30 backdrop-blur-2xl border border-white/20 dark:border-gray-800/20 shadow-2xl";
case "bordered":
return "bg-card/95 border-2 border-primary/30 backdrop-blur-lg shadow-xl";
default:
return "bg-background/30 backdrop-blur-2xl border border-white/20 shadow-2xl";
}
};
const getPriorityStyles = () => {
switch (notification.priority) {
case "high":
return "border-l-4 border-l-red-500 shadow-red-500/20 dark:border-l-red-500 dark:shadow-red-500/20";
case "medium":
return "border-l-4 border-l-yellow-500 shadow-yellow-500/20 dark:border-l-yellow-500 dark:shadow-yellow-500/20";
case "low":
return "border-l-4 border-l-blue-500 shadow-[0_4px_15px_color-mix(in_srgb,var(--primarylw)_20%,transparent)] dark:border-l-blue-500 dark:shadow-[0_4px_15px_color-mix(in_srgb,var(--primarylw)_20%,transparent)]";
default:
return "border-l-4 border-l-primary/50 shadow-primary/20 dark:border-l-primary/50 dark:shadow-primary/20";
}
};
return (_jsxs("div", { className: cn("group relative transition-all duration-500 ease-out transform hover:scale-[1.02] hover:-translate-y-1", "rounded-xl p-4 flex items-start gap-3 w-80 max-w-80 cursor-pointer", getVariantStyles(), getPriorityStyles(), notification.fadingOut && "animate-pulse"), onClick: onClick, children: [_jsx("div", { className: "absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 rounded-xl pointer-events-none" }), _jsx(Avatar, { user: notification.user, showAvatar: showAvatars }), _jsxs("div", { className: "flex-1 min-w-0 space-y-1", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h3", { className: "font-semibold text-sm text-foreground/90 truncate", children: notification.user.name }), showTimestamps && notification.timestamp && _jsx("span", { className: "text-xs text-muted-foreground/70 ", children: notification.timestamp })] }), _jsx("p", { className: "text-sm text-muted-foreground/80 line-clamp-2 leading-relaxed", children: notification.message })] }), allowDismiss && (_jsx("button", { onClick: (e) => {
e.stopPropagation();
onDismiss?.();
}, className: "flex-shrink-0 w-5 h-5 text-muted-foreground/50 hover:text-muted-foreground transition-all duration-200 hover:scale-110 opacity-0 group-hover:opacity-100", "aria-label": "dismiss notification", children: _jsx(X, { className: "w-4 h-4" }) }))] }));
};
async function fetchRandomUser(apiEndpoint) {
try {
const endpoint = apiEndpoint || "https://randomuser.me/api/";
const res = await fetch(endpoint);
const data = await res.json();
const user = data.results[0];
return {
avatarUrl: user.picture?.large,
name: `${user.name.first} ${user.name.last}`,
color: `hsl(${Math.floor(Math.random() * 360)}, 70%, 80%)`,
};
}
catch (error) {
const names = ["John Doe", "Jane Smith", "Alex Johnson", "Sarah Wilson", "Mike Brown"];
const randomName = names[Math.floor(Math.random() * names.length)];
return {
name: randomName,
color: `hsl(${Math.floor(Math.random() * 360)}, 70%, 80%)`,
};
}
}
function getRandomMessage(customMessages) {
const messages = customMessages || defaultMessages;
return messages[Math.floor(Math.random() * messages.length)];
}
async function generateNotification(customMessages, userApiEndpoint, fixedUser) {
const user = fixedUser || await fetchRandomUser(userApiEndpoint);
return {
id: uuidv4(),
user,
message: getRandomMessage(customMessages),
timestamp: new Date().toLocaleTimeString(),
priority: ["low", "medium", "high"][Math.floor(Math.random() * 3)],
};
}
export const AnimatedNotification = ({ maxNotifications = 3, autoInterval = 3500, autoGenerate = true, notifications = [], customMessages, animationDuration = 800, position = "center", width = 320, showAvatars = true, showTimestamps = true, className, onNotificationClick, onNotificationDismiss, allowDismiss = true, autoDismissTimeout = 3000, userApiEndpoint, variant = "glass", fixedUser, }) => {
const [notes, setNotes] = useState(notifications);
const intervalRef = useRef(null);
const dismissTimeouts = useRef(new Map());
const isGenerating = useRef(false);
// Helper: Clear a per-note timeout
const clearNoteTimeout = useCallback((id) => {
const t = dismissTimeouts.current.get(id);
if (t) {
window.clearTimeout(t);
dismissTimeouts.current.delete(id);
}
}, []);
// Dismiss with fade + removal after animationDuration
const dismissNotification = useCallback((id, isAutoRemoval = false) => {
setNotes((prev) => {
const note = prev.find((n) => n.id === id);
if (!note || note.fadingOut)
return prev;
// mark fading out
const updated = prev.map((n) => (n.id === id ? { ...n, fadingOut: true } : n));
// clear any auto timeout
clearNoteTimeout(id);
// remove after animation
window.setTimeout(() => {
setNotes((current) => {
const remaining = current.filter((n) => n.id !== id);
// only call onNotificationDismiss if the note actually existed before removal
if (note && onNotificationDismiss) {
// isAutoRemoval true indicates it was auto-dismissed; user can still be notified if needed
onNotificationDismiss(note);
}
return remaining;
});
}, animationDuration);
return updated;
});
}, [animationDuration, clearNoteTimeout, onNotificationDismiss]);
// Add a new note (from generator). Ensures we don't create concurrency issues.
const addGeneratedNote = useCallback(async () => {
if (!autoGenerate)
return;
if (isGenerating.current)
return;
isGenerating.current = true;
try {
const newNote = await generateNotification(customMessages, userApiEndpoint, fixedUser);
setNotes((prev) => {
// prune: if there are >= maxNotifications non-fading notes, dismiss the oldest non-fading
const nonFading = prev.filter((n) => !n.fadingOut);
if (nonFading.length >= maxNotifications) {
const oldest = nonFading[0];
// gracefully mark it for fading and schedule removal using existing dismissNotification
// call dismissNotification (which will clear its timeout and run onNotificationDismiss)
// but to avoid calling setNotes nested while in this setNotes, we will mark it locally:
const locallyUpdated = prev.map((n) => (n.id === oldest.id ? { ...n, fadingOut: true } : n));
// schedule actual removal outside this setNotes by using setTimeout
window.setTimeout(() => {
setNotes((current) => current.filter((n) => n.id !== oldest.id));
clearNoteTimeout(oldest.id);
onNotificationDismiss?.(oldest);
}, animationDuration);
// Also clear any auto timeout for that oldest note
const t = dismissTimeouts.current.get(oldest.id);
if (t) {
window.clearTimeout(t);
dismissTimeouts.current.delete(oldest.id);
}
// then append the new note
const appended = [...locallyUpdated, newNote];
// schedule auto-dismiss for newNote
if (autoDismissTimeout > 0) {
const timeoutId = window.setTimeout(() => dismissNotification(newNote.id, true), autoDismissTimeout);
dismissTimeouts.current.set(newNote.id, timeoutId);
}
return appended;
}
else {
const appended = [...prev, newNote];
if (autoDismissTimeout > 0) {
const timeoutId = window.setTimeout(() => dismissNotification(newNote.id, true), autoDismissTimeout);
dismissTimeouts.current.set(newNote.id, timeoutId);
}
return appended;
}
});
}
finally {
isGenerating.current = false;
}
}, [
autoGenerate,
customMessages,
userApiEndpoint,
maxNotifications,
animationDuration,
autoDismissTimeout,
dismissNotification,
onNotificationDismiss,
clearNoteTimeout,
fixedUser,
]);
// Start/stop the interval generator
useEffect(() => {
// If autoGenerate enabled, start interval. We'll call addGeneratedNote every autoInterval.
if (autoGenerate) {
// start with a small delay so UI can mount
intervalRef.current = window.setInterval(() => {
void addGeneratedNote();
}, autoInterval);
// do a first immediate call after small delay (not instant) so initial experience is consistent
const first = window.setTimeout(() => void addGeneratedNote(), 900);
return () => {
if (intervalRef.current) {
window.clearInterval(intervalRef.current);
intervalRef.current = null;
}
window.clearTimeout(first);
};
}
else {
// autoGenerate disabled -> ensure no interval running
if (intervalRef.current) {
window.clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoGenerate, autoInterval, addGeneratedNote]);
// Sync external notifications prop if the parent supplies them
useEffect(() => {
if (notifications && notifications.length > 0) {
// clear existing timers
dismissTimeouts.current.forEach((t) => window.clearTimeout(t));
dismissTimeouts.current.clear();
setNotes(notifications);
// set up auto dismiss for those if needed
if (autoDismissTimeout > 0) {
notifications.forEach((n) => {
const id = window.setTimeout(() => dismissNotification(n.id, true), autoDismissTimeout);
dismissTimeouts.current.set(n.id, id);
});
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [notifications]);
// cleanup all timers on unmount
useEffect(() => {
return () => {
if (intervalRef.current)
window.clearInterval(intervalRef.current);
dismissTimeouts.current.forEach((t) => window.clearTimeout(t));
dismissTimeouts.current.clear();
};
}, []);
// Manual dismiss wrapper so the caller's onNotificationDismiss isn't called twice
const handleManualDismiss = useCallback((note) => {
// clear timeout and fade out
clearNoteTimeout(note.id);
// mark fading and remove after animationDuration
setNotes((prev) => prev.map((n) => (n.id === note.id ? { ...n, fadingOut: true } : n)));
window.setTimeout(() => {
setNotes((current) => {
const filtered = current.filter((n) => n.id !== note.id);
onNotificationDismiss?.(note);
return filtered;
});
}, animationDuration);
}, [animationDuration, clearNoteTimeout, onNotificationDismiss]);
const getPositionStyles = () => {
switch (position) {
case "top-left":
return "fixed top-6 left-6 z-50";
case "top-right":
return "fixed top-6 right-6 z-50";
case "bottom-left":
return "fixed bottom-6 left-6 z-50";
case "bottom-right":
return "fixed bottom-6 right-6 z-50";
default:
return "flex items-center justify-center min-h-auto p-6";
}
};
return (_jsxs(_Fragment, { children: [_jsx("style", { dangerouslySetInnerHTML: {
__html: `
@keyframes notification-enter {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
filter: blur(4px);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px);
}
}
@keyframes notification-exit {
from {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0px);
}
to {
opacity: 0;
transform: translateY(-20px) scale(0.95);
filter: blur(4px);
}
}
.notification-enter {
animation: notification-enter var(--animation-duration) cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.notification-exit {
animation: notification-exit var(--animation-duration) cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
`,
} }), _jsx("div", { className: cn(getPositionStyles(), className), children: _jsx(Flipper, { flipKey: notes.map((n) => n.id).join(""), children: _jsx("div", { className: "flex flex-col gap-4 items-center", style: { width }, children: notes.map((note) => (_jsx(Flipped, { flipId: note.id, children: _jsx("div", { className: cn("notification-item", note.fadingOut ? "notification-exit" : "notification-enter"), style: { ["--animation-duration"]: `${animationDuration}ms` }, children: _jsx(Notification, { notification: note, showAvatars: showAvatars, showTimestamps: showTimestamps, variant: variant, allowDismiss: allowDismiss, onClick: () => onNotificationClick?.(note), onDismiss: () => handleManualDismiss(note) }) }) }, note.id))) }) }) })] }));
};
export default AnimatedNotification;