![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/public_html/src/context/ |
import React, { createContext, useContext, useEffect, useState, ReactNode, useCallback } from 'react';
import { useRouter } from 'next/router';
import toast from 'react-hot-toast';
interface NotificationCampaign {
id: string;
type: 'banner' | 'toast' | 'modal' | 'exit-intent';
title: string;
message: string;
actionText?: string;
actionUrl?: string;
priority: 'low' | 'medium' | 'high' | 'urgent';
placement: 'top' | 'bottom' | 'center';
targetPages?: string[];
targetRegion?: string;
language: 'en' | 'fr' | 'both';
isActive: boolean;
expiresAt?: string;
showAfterSeconds?: number;
maxViews?: number;
createdAt: string;
}
interface VisitorBehavior {
sessionId: string;
timeOnSite: number;
pagesVisited: string[];
lastActivity: number;
hasSignedUp: boolean;
hasApplied: boolean;
notificationsShown: string[];
region?: string;
language: string;
}
interface PublicNotificationContextValue {
// Active campaigns
activeCampaigns: NotificationCampaign[];
// Visitor tracking
visitorBehavior: VisitorBehavior;
updateVisitorBehavior: (updates: Partial<VisitorBehavior>) => void;
// Notification controls
showNotification: (campaign: NotificationCampaign) => void;
dismissNotification: (campaignId: string) => void;
subscribeToNewsletter: (email: string) => Promise<boolean>;
// Engagement prompts
showEducationalPrompt: (topic: string) => void;
showNewsletterPrompt: () => void;
showGroupChatPrompt: () => void;
showExitIntent: () => void;
showTimeBasedPrompt: () => void;
// Banner state
activeBanner: NotificationCampaign | null;
dismissBanner: () => void;
// Analytics
trackNotificationView: (campaignId: string) => void;
trackNotificationClick: (campaignId: string) => void;
}
const PublicNotificationContext = createContext<PublicNotificationContextValue>({
activeCampaigns: [],
visitorBehavior: {
sessionId: '',
timeOnSite: 0,
pagesVisited: [],
lastActivity: Date.now(),
hasSignedUp: false,
hasApplied: false,
notificationsShown: [],
language: 'en'
},
updateVisitorBehavior: () => {},
showNotification: () => {},
dismissNotification: () => {},
subscribeToNewsletter: async () => false,
showEducationalPrompt: () => {},
showNewsletterPrompt: () => {},
showGroupChatPrompt: () => {},
showExitIntent: () => {},
showTimeBasedPrompt: () => {},
activeBanner: null,
dismissBanner: () => {},
trackNotificationView: () => {},
trackNotificationClick: () => {},
});
export const PublicNotificationProvider = ({ children }: { children: ReactNode }) => {
const router = useRouter();
const [activeCampaigns, setActiveCampaigns] = useState<NotificationCampaign[]>([]);
const [visitorBehavior, setVisitorBehavior] = useState<VisitorBehavior>({
sessionId: '',
timeOnSite: 0,
pagesVisited: [],
lastActivity: Date.now(),
hasSignedUp: false,
hasApplied: false,
notificationsShown: [],
language: (router.locale as 'en' | 'fr') || 'en'
});
const [activeBanner, setActiveBanner] = useState<NotificationCampaign | null>(null);
const [exitIntentShown, setExitIntentShown] = useState(false);
const [timeBasedShown, setTimeBasedShown] = useState(false);
// Generate or retrieve session ID
useEffect(() => {
if (typeof window !== 'undefined') {
let sessionId = sessionStorage.getItem('visitorSessionId');
if (!sessionId) {
sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
sessionStorage.setItem('visitorSessionId', sessionId);
}
setVisitorBehavior(prev => ({ ...prev, sessionId }));
}
}, []);
// Track time on site - OPTIMIZED to reduce re-renders
useEffect(() => {
const startTime = Date.now();
let lastUpdateTime = startTime;
// Update every 10 seconds instead of every 1 second to reduce re-renders
const interval = setInterval(() => {
const now = Date.now();
const timeOnSite = Math.floor((now - startTime) / 1000);
// Only update if we've crossed a meaningful threshold (10 seconds)
if (timeOnSite - Math.floor((lastUpdateTime - startTime) / 1000) >= 10) {
setVisitorBehavior(prev => ({
...prev,
timeOnSite,
lastActivity: now
}));
lastUpdateTime = now;
}
}, 10000); // Update every 10 seconds instead of 1 second
return () => clearInterval(interval);
}, []);
// Track page visits
useEffect(() => {
const currentPage = router.asPath;
setVisitorBehavior(prev => ({
...prev,
pagesVisited: Array.from(new Set([...prev.pagesVisited, currentPage]))
}));
}, [router.asPath]);
// Detect user region (basic implementation)
useEffect(() => {
const detectRegion = async () => {
try {
const response = await fetch('https://ipapi.co/json/');
const data = await response.json();
if (data.country_code === 'CA' && data.region === 'QC') {
setVisitorBehavior(prev => ({ ...prev, region: 'quebec' }));
} else if (data.country_code === 'CA') {
setVisitorBehavior(prev => ({ ...prev, region: 'canada' }));
} else {
setVisitorBehavior(prev => ({ ...prev, region: 'international' }));
}
} catch (error) {
console.log('Could not detect region');
}
};
detectRegion();
}, []);
// Fetch active campaigns
useEffect(() => {
const fetchCampaigns = async () => {
try {
const response = await fetch('/api/public/notifications/campaigns');
if (response.ok) {
const campaigns = await response.json();
setActiveCampaigns(campaigns);
// Find active banner
const banner = campaigns.find((c: NotificationCampaign) =>
c.type === 'banner' &&
c.isActive &&
(!c.expiresAt || new Date(c.expiresAt) > new Date()) &&
(!c.targetPages || c.targetPages.includes(router.asPath)) &&
(c.language === 'both' || c.language === visitorBehavior.language)
);
if (banner && typeof window !== 'undefined' && !localStorage.getItem(`banner_dismissed_${banner.id}`)) {
setActiveBanner(banner);
}
}
} catch (error) {
console.error('Failed to fetch notification campaigns:', error);
}
};
fetchCampaigns();
}, [router.asPath]);
// Exit intent detection
useEffect(() => {
const handleMouseLeave = (e: MouseEvent) => {
if (e.clientY <= 0 && !exitIntentShown && visitorBehavior.timeOnSite > 30) {
showExitIntent();
}
};
document.addEventListener('mouseleave', handleMouseLeave);
return () => document.removeEventListener('mouseleave', handleMouseLeave);
}, [exitIntentShown]);
// Time-based prompts - only if no other prompts shown recently
useEffect(() => {
if (typeof window !== 'undefined' &&
visitorBehavior.timeOnSite > 120 &&
!timeBasedShown &&
visitorBehavior.pagesVisited.length > 2 &&
!sessionStorage.getItem(`newsletter_prompt_${visitorBehavior.sessionId}`) &&
!sessionStorage.getItem(`exit_intent_${visitorBehavior.sessionId}`)) {
showTimeBasedPrompt();
}
}, [timeBasedShown]);
const updateVisitorBehavior = useCallback((updates: Partial<VisitorBehavior>) => {
setVisitorBehavior(prev => ({ ...prev, ...updates }));
}, []);
const showNotification = useCallback((campaign: NotificationCampaign) => {
// Check if already shown
if (visitorBehavior.notificationsShown.includes(campaign.id)) return;
// Check max views
if (typeof window !== 'undefined') {
const viewCount = parseInt(localStorage.getItem(`campaign_views_${campaign.id}`) || '0');
if (campaign.maxViews && viewCount >= campaign.maxViews) return;
}
switch (campaign.type) {
case 'toast':
toast((t) => (
<div className="flex items-center space-x-3 max-w-md">
<div className="flex-1">
<p className="font-medium text-gray-900">{campaign.title}</p>
<p className="text-sm text-gray-600">{campaign.message}</p>
</div>
{campaign.actionText && campaign.actionUrl && (
<button
onClick={() => {
toast.dismiss(t.id);
trackNotificationClick(campaign.id);
router.push(campaign.actionUrl!);
}}
className="bg-primary text-white px-3 py-1 rounded text-sm hover:bg-primary-dark"
>
{campaign.actionText}
</button>
)}
</div>
), {
duration: campaign.priority === 'urgent' ? 8000 : 5000,
position: campaign.placement === 'top' ? 'top-right' : 'bottom-right',
});
break;
case 'modal':
// Implement modal logic
break;
}
// Track view
trackNotificationView(campaign.id);
// Mark as shown
setVisitorBehavior(prev => ({
...prev,
notificationsShown: [...prev.notificationsShown, campaign.id]
}));
}, [router]);
const dismissNotification = useCallback((campaignId: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem(`notification_dismissed_${campaignId}`, 'true');
}
}, []);
const subscribeToNewsletter = useCallback(async (email: string): Promise<boolean> => {
try {
const response = await fetch('/api/public/newsletter/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
language: visitorBehavior.language,
source: 'website_notification'
}),
});
if (response.ok) {
setVisitorBehavior(prev => ({ ...prev, hasSignedUp: true }));
toast.success('✅ Successfully subscribed to case updates!', {
duration: 4000,
position: 'top-right',
});
return true;
}
return false;
} catch (error) {
toast.error('Failed to subscribe. Please try again.', {
duration: 4000,
position: 'top-right',
});
return false;
}
}, []);
const showEducationalPrompt = useCallback((topic: string) => {
const prompts = {
'class-action': {
en: {
title: '📚 New to Class Actions?',
message: 'Learn about your rights and how class actions work',
actionText: 'Learn More',
actionUrl: '/faq'
},
fr: {
title: '📚 Nouveau aux recours collectifs?',
message: 'Apprenez vos droits et comment fonctionnent les recours collectifs',
actionText: 'En savoir plus',
actionUrl: '/faq'
}
},
'rights': {
en: {
title: '⚖️ Know Your Rights',
message: 'Understanding your rights as a former detainee',
actionText: 'Download Guide',
actionUrl: '/resources'
},
fr: {
title: '⚖️ Connaissez vos droits',
message: 'Comprendre vos droits en tant qu\'ancien détenu',
actionText: 'Télécharger le guide',
actionUrl: '/resources'
}
}
};
const prompt = prompts[topic as keyof typeof prompts]?.[visitorBehavior.language as 'en' | 'fr'];
if (prompt) {
toast((t) => (
<div className="flex items-center space-x-3">
<div className="flex-1">
<p className="font-medium text-gray-900">{prompt.title}</p>
<p className="text-sm text-gray-600">{prompt.message}</p>
</div>
<button
onClick={() => {
toast.dismiss(t.id);
router.push(prompt.actionUrl);
}}
className="bg-primary text-white px-3 py-1 rounded text-sm hover:bg-primary-dark"
>
{prompt.actionText}
</button>
</div>
), {
duration: 6000,
position: 'top-right',
});
}
}, [router]);
const showNewsletterPrompt = useCallback(() => {
if (visitorBehavior.hasSignedUp) return;
// Check if already shown in this session to prevent duplicates
if (typeof window !== 'undefined') {
const newsletterShownKey = `newsletter_prompt_${visitorBehavior.sessionId}`;
if (sessionStorage.getItem(newsletterShownKey)) return;
// Mark as shown immediately to prevent duplicates
sessionStorage.setItem(newsletterShownKey, 'shown');
}
const message = visitorBehavior.language === 'fr'
? 'Restez informé des mises à jour importantes du dossier'
: 'Stay informed about important case updates';
const actionText = visitorBehavior.language === 'fr' ? 'S\'abonner' : 'Subscribe';
toast((t) => (
<div className="flex flex-col space-y-2 max-w-sm">
<p className="font-medium text-gray-900">📧 {message}</p>
<div className="flex space-x-2">
<input
type="email"
placeholder="email@example.com"
className="flex-1 px-2 py-1 border rounded text-sm"
id={`newsletter-input-${t.id}`}
onKeyDown={(e) => {
if (e.key === 'Enter') {
const email = (e.target as HTMLInputElement).value;
if (email) {
subscribeToNewsletter(email);
toast.dismiss(t.id);
}
}
}}
/>
<button
onClick={() => {
const input = document.getElementById(`newsletter-input-${t.id}`) as HTMLInputElement;
if (input?.value) {
subscribeToNewsletter(input.value);
toast.dismiss(t.id);
}
}}
className="bg-primary text-white px-3 py-1 rounded text-sm hover:bg-primary-dark"
>
{actionText}
</button>
</div>
</div>
), {
duration: 8000,
position: 'top-right',
});
}, [subscribeToNewsletter]);
const showGroupChatPrompt = useCallback(() => {
// Check if already shown in this session to prevent duplicates
if (typeof window !== 'undefined') {
const groupChatShownKey = `groupchat_prompt_${visitorBehavior.sessionId}`;
if (sessionStorage.getItem(groupChatShownKey)) return;
// Mark as shown immediately to prevent duplicates
sessionStorage.setItem(groupChatShownKey, 'shown');
}
const content = visitorBehavior.language === 'fr' ? {
title: '💬 Rejoignez la Communauté',
message: 'Connectez-vous avec d\'autres membres du recours collectif',
description: 'Partagez vos expériences, obtenez du soutien et restez informé avec la communauté',
actionText: 'Rejoindre le Chat',
benefits: [
'🤝 Soutien communautaire',
'📢 Mises à jour en temps réel',
'💪 Force collective'
]
} : {
title: '💬 Join the Community',
message: 'Connect with other class action members',
description: 'Share experiences, get support, and stay informed with the community',
actionText: 'Join Chat',
benefits: [
'🤝 Community support',
'📢 Real-time updates',
'💪 Collective strength'
]
};
toast((t) => (
<div className="flex flex-col space-y-3 max-w-sm">
<div className="flex items-center space-x-2">
<div className="text-2xl">💬</div>
<div>
<p className="font-bold text-gray-900">{content.title}</p>
<p className="text-sm text-gray-600">{content.message}</p>
</div>
</div>
<div className="text-xs text-gray-600">
{content.description}
</div>
<div className="grid grid-cols-1 gap-1 text-xs">
{content.benefits.map((benefit, index) => (
<div key={index} className="text-gray-700">{benefit}</div>
))}
</div>
<div className="flex space-x-2">
<button
onClick={() => {
toast.dismiss(t.id);
router.push('/group-chat');
}}
className="flex-1 bg-primary text-white px-3 py-2 rounded text-sm font-medium hover:bg-primary-dark transition-colors"
>
{content.actionText}
</button>
<button
onClick={() => toast.dismiss(t.id)}
className="px-3 py-2 text-xs text-gray-500 hover:text-gray-700"
>
{visitorBehavior.language === 'fr' ? 'Plus tard' : 'Later'}
</button>
</div>
</div>
), {
duration: 10000,
position: 'bottom-right',
});
}, [router]);
const showExitIntent = useCallback(() => {
setExitIntentShown(true);
// Mark exit intent as shown to prevent conflicts
if (typeof window !== 'undefined') {
sessionStorage.setItem(`exit_intent_${visitorBehavior.sessionId}`, 'shown');
}
const message = visitorBehavior.language === 'fr'
? 'Attendez! Vous pourriez être éligible à une compensation'
: 'Wait! You might be eligible for compensation';
const actionText = visitorBehavior.language === 'fr' ? 'Vérifier l\'éligibilité' : 'Check Eligibility';
toast((t) => (
<div className="flex items-center space-x-3">
<div className="text-2xl">⚖️</div>
<div className="flex-1">
<p className="font-bold text-gray-900">{message}</p>
<p className="text-sm text-gray-600">
{visitorBehavior.language === 'fr'
? 'Découvrez si vous pouvez vous joindre au recours collectif'
: 'Find out if you can join the class action'}
</p>
</div>
<button
onClick={() => {
toast.dismiss(t.id);
router.push('/register');
}}
className="bg-red-600 text-white px-4 py-2 rounded font-bold hover:bg-red-700"
>
{actionText}
</button>
</div>
), {
duration: 10000,
position: 'top-center',
style: {
maxWidth: '500px',
},
});
}, [router]);
const showTimeBasedPrompt = useCallback(() => {
setTimeBasedShown(true);
const prompts = [
{
en: {
title: '🕐 Still reading?',
message: 'Get personalized guidance about your case',
actionText: 'Free Consultation',
actionUrl: '/contact'
},
fr: {
title: '🕐 Toujours en train de lire?',
message: 'Obtenez des conseils personnalisés sur votre dossier',
actionText: 'Consultation gratuite',
actionUrl: '/contact'
}
},
{
en: {
title: '📞 Questions?',
message: 'Speak with a legal expert about your rights',
actionText: 'Contact Us',
actionUrl: '/contact'
},
fr: {
title: '📞 Des questions?',
message: 'Parlez à un expert juridique de vos droits',
actionText: 'Nous contacter',
actionUrl: '/contact'
}
}
];
const randomPrompt = prompts[Math.floor(Math.random() * prompts.length)];
const prompt = randomPrompt[visitorBehavior.language as 'en' | 'fr'];
toast((t) => (
<div className="flex items-center space-x-3">
<div className="flex-1">
<p className="font-medium text-gray-900">{prompt.title}</p>
<p className="text-sm text-gray-600">{prompt.message}</p>
</div>
<button
onClick={() => {
toast.dismiss(t.id);
router.push(prompt.actionUrl);
}}
className="bg-primary text-white px-3 py-1 rounded text-sm hover:bg-primary-dark"
>
{prompt.actionText}
</button>
</div>
), {
duration: 6000,
position: 'top-right',
});
}, [router]);
const dismissBanner = useCallback(() => {
if (activeBanner && typeof window !== 'undefined') {
localStorage.setItem(`banner_dismissed_${activeBanner.id}`, 'true');
setActiveBanner(null);
}
}, [activeBanner]);
const trackNotificationView = useCallback((campaignId: string) => {
if (typeof window !== 'undefined') {
const currentViews = parseInt(localStorage.getItem(`campaign_views_${campaignId}`) || '0');
localStorage.setItem(`campaign_views_${campaignId}`, (currentViews + 1).toString());
}
// Send analytics
fetch('/api/public/notifications/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
campaignId,
event: 'view',
sessionId: visitorBehavior.sessionId,
page: router.asPath
}),
}).catch(() => {}); // Silent fail
}, [visitorBehavior.sessionId, router.asPath]);
const trackNotificationClick = useCallback((campaignId: string) => {
// Send analytics
fetch('/api/public/notifications/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
campaignId,
event: 'click',
sessionId: visitorBehavior.sessionId,
page: router.asPath
}),
}).catch(() => {}); // Silent fail
}, [visitorBehavior.sessionId, router.asPath]);
return (
<PublicNotificationContext.Provider value={{
activeCampaigns,
visitorBehavior,
updateVisitorBehavior,
showNotification,
dismissNotification,
subscribeToNewsletter,
showEducationalPrompt,
showNewsletterPrompt,
showGroupChatPrompt,
showExitIntent,
showTimeBasedPrompt,
activeBanner,
dismissBanner,
trackNotificationView,
trackNotificationClick,
}}>
{children}
</PublicNotificationContext.Provider>
);
};
export const usePublicNotifications = () => useContext(PublicNotificationContext);