import { useEffect } from 'react' const VISITOR_STORAGE_KEY = 'academy.analytics.visitor-id' const VISITOR_COOKIE_NAME = 'academy_visitor_id' const ONCE_PREFIX = 'academy.analytics.once:' function getCsrfToken() { if (typeof document === 'undefined') { return '' } return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '' } function getCookieValue(name) { if (typeof document === 'undefined') { return '' } const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)) return match ? decodeURIComponent(match[1]) : '' } function generateVisitorId() { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID() } return `academy-${Date.now()}-${Math.random().toString(16).slice(2)}` } function ensureVisitorId() { if (typeof window === 'undefined') { return null } let visitorId = '' try { visitorId = window.localStorage.getItem(VISITOR_STORAGE_KEY) || '' } catch { visitorId = '' } if (!visitorId) { visitorId = getCookieValue(VISITOR_COOKIE_NAME) } if (!visitorId) { visitorId = generateVisitorId() } try { window.localStorage.setItem(VISITOR_STORAGE_KEY, visitorId) } catch { // Ignore storage failures and continue. } if (typeof document !== 'undefined') { document.cookie = `${VISITOR_COOKIE_NAME}=${encodeURIComponent(visitorId)}; path=/; max-age=31536000; SameSite=Lax` } return visitorId } function buildPayload(payload = {}) { return { ...payload, visitor_id: payload.visitor_id || ensureVisitorId(), url: payload.url || (typeof window !== 'undefined' ? window.location.href : null), _token: payload._token || getCsrfToken(), } } function markOnce(onceKey) { if (!onceKey || typeof window === 'undefined') { return false } const storageKey = `${ONCE_PREFIX}${onceKey}` try { if (window.sessionStorage.getItem(storageKey)) { return true } window.sessionStorage.setItem(storageKey, '1') } catch { return false } return false } export async function postAcademyAction(url, payload = {}) { if (!url || typeof window === 'undefined') { return null } const response = await fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': getCsrfToken(), }, credentials: 'same-origin', body: JSON.stringify(buildPayload(payload)), }).catch(() => null) if (!response?.ok) { return null } const responseContentType = response.headers.get('content-type') || '' if (!responseContentType.includes('application/json')) { return null } return response.json().catch(() => null) } export function trackAcademyEvent(eventType, contentType, contentId, metadata = {}, options = {}) { if (!eventType || !options?.url || typeof window === 'undefined') { return Promise.resolve(false) } if (options.onceKey && markOnce(options.onceKey)) { return Promise.resolve(false) } const payload = buildPayload({ event_type: eventType, content_type: contentType || null, content_id: contentId || null, metadata, route_name: options.pageName || null, }) const body = JSON.stringify(payload) if (options.useBeacon !== false && typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') { try { const blob = new Blob([body], { type: 'application/json' }) const queued = navigator.sendBeacon(options.url, blob) if (queued) { return Promise.resolve(true) } } catch { // Fall back to fetch. } } return fetch(options.url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': getCsrfToken(), }, credentials: 'same-origin', keepalive: options.keepalive === true, body, }).then(() => true).catch(() => false) } export function normalizeAcademySearchQuery(query = '') { const normalizedWhitespace = String(query).trim().toLowerCase().replace(/\s+/g, ' ') return normalizedWhitespace.replace(/[^a-z0-9\s\-_]+/g, '').trim() } export function trackAcademySearchResultClick(analytics, search, result) { if (!analytics?.eventUrl || !search?.query || !result?.contentType || !result?.contentId) { return } void trackAcademyEvent('academy_search_result_click', result.contentType, result.contentId, { query: search.query, normalized_query: search.normalizedQuery || normalizeAcademySearchQuery(search.query), results_count: Number(search.resultsCount || 0), position: result.position || null, source: result.source || 'academy_search_results', filters: search.filters || {}, }, { url: analytics.eventUrl, pageName: analytics.pageName, keepalive: true, }) } function contentViewEventType(contentType) { if (contentType === 'academy_lesson') return 'academy_lesson_view' if (contentType === 'academy_course') return 'academy_course_view' if (contentType === 'academy_prompt_pack') return 'academy_prompt_pack_view' if (contentType === 'academy_challenge') return 'academy_challenge_view' return 'academy_content_view' } export function trackUpgradeClick(analytics, metadata = {}) { if (!analytics?.eventUrl) { return } void trackAcademyEvent('academy_upgrade_click', analytics?.contentType || 'academy_upgrade', analytics?.contentId || null, metadata, { url: analytics.eventUrl, pageName: analytics.pageName, useBeacon: false, }) } export function useAcademyPageAnalytics(analytics) { useEffect(() => { if (!analytics?.enabled || !analytics?.eventUrl || typeof window === 'undefined') { return undefined } const baseKey = `${analytics.pageName || window.location.pathname}:${analytics.contentType || 'page'}:${analytics.contentId || 'none'}` void trackAcademyEvent('academy_page_view', analytics.contentType || null, analytics.contentId || null, { page_name: analytics.pageName, }, { url: analytics.eventUrl, pageName: analytics.pageName, onceKey: `${baseKey}:page-view`, }) if (analytics.contentType || analytics.contentId) { void trackAcademyEvent(contentViewEventType(analytics.contentType), analytics.contentType || null, analytics.contentId || null, { page_name: analytics.pageName, }, { url: analytics.eventUrl, pageName: analytics.pageName, onceKey: `${baseKey}:content-view`, }) } if (analytics.isPremium && analytics.isLocked) { void trackAcademyEvent('academy_premium_preview_view', analytics.contentType || null, analytics.contentId || null, { page_name: analytics.pageName, }, { url: analytics.eventUrl, pageName: analytics.pageName, onceKey: `${baseKey}:premium-preview`, }) } const engagedTimer = window.setTimeout(() => { void trackAcademyEvent('academy_engaged_view', analytics.contentType || null, analytics.contentId || null, { page_name: analytics.pageName, engaged_seconds: 15, }, { url: analytics.eventUrl, pageName: analytics.pageName, onceKey: `${baseKey}:engaged`, }) }, 15000) const sentMilestones = new Set() const onScroll = () => { const doc = document.documentElement const scrollable = Math.max(1, doc.scrollHeight - window.innerHeight) const percent = Math.min(100, Math.round((window.scrollY / scrollable) * 100)) ;[ { threshold: 50, eventType: 'academy_scroll_50' }, { threshold: 75, eventType: 'academy_scroll_75' }, { threshold: 100, eventType: 'academy_scroll_100' }, ].forEach((milestone) => { if (percent < milestone.threshold || sentMilestones.has(milestone.threshold)) { return } sentMilestones.add(milestone.threshold) void trackAcademyEvent(milestone.eventType, analytics.contentType || null, analytics.contentId || null, { page_name: analytics.pageName, scroll_percent: milestone.threshold, }, { url: analytics.eventUrl, pageName: analytics.pageName, onceKey: `${baseKey}:scroll-${milestone.threshold}`, }) }) } window.addEventListener('scroll', onScroll, { passive: true }) return () => { window.clearTimeout(engagedTimer) window.removeEventListener('scroll', onScroll) } }, [analytics?.contentId, analytics?.contentType, analytics?.enabled, analytics?.eventUrl, analytics?.isLocked, analytics?.isPremium, analytics?.pageName]) }