Add mounted state to prevent SSR rendering. Component now only renders after client-side mount. Fixes login blocking hydration error.
114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { usePushNotifications } from '@/hooks/usePushNotifications';
|
|
import { Bell, X } from 'lucide-react';
|
|
|
|
export default function NotificationPrompt() {
|
|
const { permission, isSubscribed, subscribe } = usePushNotifications();
|
|
const [showPrompt, setShowPrompt] = useState(false);
|
|
const [dismissed, setDismissed] = useState(false);
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!mounted) return;
|
|
|
|
// Check if we should show the prompt
|
|
const shouldShow =
|
|
'Notification' in window &&
|
|
'serviceWorker' in navigator &&
|
|
permission === 'default' && // Not yet asked
|
|
!isSubscribed && // Not subscribed
|
|
!dismissed && // Not dismissed this session
|
|
!localStorage.getItem('notification-prompt-dismissed'); // Not permanently dismissed
|
|
|
|
if (shouldShow) {
|
|
// Show prompt after a short delay (better UX)
|
|
const timer = setTimeout(() => {
|
|
setShowPrompt(true);
|
|
}, 3000); // 3 seconds after app loads
|
|
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [mounted, permission, isSubscribed, dismissed]);
|
|
|
|
const handleEnable = async () => {
|
|
const success = await subscribe();
|
|
if (success) {
|
|
setShowPrompt(false);
|
|
}
|
|
};
|
|
|
|
const handleDismiss = () => {
|
|
setShowPrompt(false);
|
|
setDismissed(true);
|
|
};
|
|
|
|
const handleDismissForever = () => {
|
|
if (typeof localStorage !== 'undefined') {
|
|
localStorage.setItem('notification-prompt-dismissed', 'true');
|
|
}
|
|
setShowPrompt(false);
|
|
setDismissed(true);
|
|
};
|
|
|
|
if (!mounted || !showPrompt) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="fixed bottom-4 right-4 left-4 sm:left-auto sm:w-96 z-50 animate-slide-up">
|
|
<div className="bg-white rounded-lg shadow-2xl border border-gray-200 p-4">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex-shrink-0 w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
|
<Bell className="w-5 h-5 text-blue-600" />
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-sm font-semibold text-gray-900 mb-1">
|
|
Stay Updated
|
|
</h3>
|
|
<p className="text-sm text-gray-600 mb-3">
|
|
Get notified when items are added, completed, or when you're invited to lists
|
|
</p>
|
|
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={handleEnable}
|
|
className="flex-1 px-3 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 transition-colors"
|
|
>
|
|
Enable Notifications
|
|
</button>
|
|
<button
|
|
onClick={handleDismiss}
|
|
className="px-3 py-2 text-gray-600 text-sm font-medium hover:text-gray-900 transition-colors"
|
|
>
|
|
Later
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleDismissForever}
|
|
className="text-xs text-gray-500 hover:text-gray-700 mt-2 underline"
|
|
>
|
|
Don't ask again
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleDismiss}
|
|
className="flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors"
|
|
aria-label="Close"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|