UNPKG

@akson/cortex-landing-hooks

Version:

React hooks for landing pages - device detection, API calls, form submission, analytics, and performance

607 lines (602 loc) 18.7 kB
// src/data/useLeadData.ts import { useEffect, useState, useCallback } from "react"; var SimpleLeadManager = class { constructor() { this.data = /* @__PURE__ */ new Map(); this.listeners = []; if (typeof window !== "undefined") { try { const stored = localStorage.getItem("akson_lead_data"); if (stored) { const parsedData = JSON.parse(stored); Object.entries(parsedData).forEach(([phone, data]) => { this.data.set(phone, data); }); } } catch (error) { console.warn("Failed to load lead data from localStorage:", error); } } } initializeLead(phoneNumber) { if (!this.data.has(phoneNumber)) { const initialData = { phoneNumber, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() }; this.data.set(phoneNumber, initialData); this.persistToStorage(); } return this.data.get(phoneNumber); } updateLead(phoneNumber, updates, source = "user") { const existing = this.data.get(phoneNumber) || {}; const newData = { ...existing, ...updates, updatedAt: (/* @__PURE__ */ new Date()).toISOString(), lastUpdateSource: source }; this.data.set(phoneNumber, newData); this.persistToStorage(); this.listeners.forEach((listener) => { try { listener(phoneNumber, newData); } catch (error) { console.warn("Lead data listener error:", error); } }); } getLead(phoneNumber) { return this.data.get(phoneNumber) || null; } clearLead(phoneNumber) { this.data.delete(phoneNumber); this.persistToStorage(); this.listeners.forEach((listener) => { try { listener(phoneNumber, {}); } catch (error) { console.warn("Lead data listener error:", error); } }); } subscribe(listener) { this.listeners.push(listener); return () => { const index = this.listeners.indexOf(listener); if (index > -1) { this.listeners.splice(index, 1); } }; } persistToStorage() { if (typeof window !== "undefined") { try { const dataObj = Object.fromEntries(this.data.entries()); localStorage.setItem("akson_lead_data", JSON.stringify(dataObj)); } catch (error) { console.warn("Failed to persist lead data to localStorage:", error); } } } // Simulate API sync - in real implementation this would call an API async forceSyncToApi(phoneNumber) { const data = this.getLead(phoneNumber); if (!data) return false; try { console.log("[Lead Manager] Simulated API sync for:", phoneNumber, data); this.updateLead(phoneNumber, { lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(), syncStatus: "synced" }, "api"); return true; } catch (error) { console.error("Failed to sync lead data to API:", error); this.updateLead(phoneNumber, { syncStatus: "failed", lastSyncError: error instanceof Error ? error.message : "Unknown error" }, "api"); return false; } } }; var leadManager = null; function getLeadManager() { if (!leadManager && typeof window !== "undefined") { leadManager = new SimpleLeadManager(); } if (!leadManager) { leadManager = new SimpleLeadManager(); } return leadManager; } function useLeadData(phoneNumber) { const [leadData, setLeadData] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { if (!phoneNumber) { setLeadData(null); setIsLoading(false); return; } setIsLoading(true); const manager = getLeadManager(); const initialData = manager.initializeLead(phoneNumber); setLeadData(initialData); setIsLoading(false); const unsubscribe = manager.subscribe((phone, data) => { if (phone === phoneNumber) { setLeadData(data); } }); return unsubscribe; }, [phoneNumber]); const updateField = useCallback((fieldName, fieldValue, formData = {}) => { if (!phoneNumber) return; const updates = { ...formData, [fieldName]: fieldValue }; getLeadManager().updateLead(phoneNumber, updates, "user"); }, [phoneNumber]); const updateImmediate = useCallback(async (formData) => { if (!phoneNumber) return false; const manager = getLeadManager(); manager.updateLead(phoneNumber, formData, "user"); return manager.forceSyncToApi(phoneNumber); }, [phoneNumber]); const clearData = useCallback(() => { if (!phoneNumber) return; getLeadManager().clearLead(phoneNumber); }, [phoneNumber]); const hasData = Boolean(leadData && Object.keys(leadData).length > 3); return { leadData, updateField, updateImmediate, isLoading, clearData, hasData }; } // src/data/useDocumentUpload.ts import { useState as useState2, useCallback as useCallback2 } from "react"; function useDocumentUpload(options = {}) { const { uploadEndpoint = "/api/documents/upload", progressUpdateInterval = 200, onUploadStart, onUploadSuccess, onUploadError } = options; const [uploadProgress, setUploadProgress] = useState2({ isUploading: false, progress: 0 }); const uploadDocument = useCallback2(async (file, leadId, additionalData) => { setUploadProgress({ isUploading: true, progress: 0, error: void 0 }); onUploadStart?.(file); try { const formData = new FormData(); formData.append("file", file); if (leadId) { formData.append("leadId", leadId); } if (additionalData) { Object.entries(additionalData).forEach(([key, value]) => { if (value !== void 0 && value !== null) { formData.append(key, typeof value === "string" ? value : JSON.stringify(value)); } }); } const progressInterval = setInterval(() => { setUploadProgress((prev) => ({ ...prev, progress: Math.min(prev.progress + 10, 90) })); }, progressUpdateInterval); const response = await fetch(uploadEndpoint, { method: "POST", body: formData }); clearInterval(progressInterval); if (!response.ok) { throw new Error(`Upload failed: ${response.status} ${response.statusText}`); } const result = await response.json(); if (!result.success && result.error) { throw new Error(result.error); } setUploadProgress({ isUploading: false, progress: 100 }); const uploadResult = { success: true, ...result.data, fileName: file.name, fileSize: file.size, fileType: file.type }; onUploadSuccess?.(uploadResult); return uploadResult; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Upload failed"; setUploadProgress({ isUploading: false, progress: 0, error: errorMessage }); const uploadResult = { success: false, error: errorMessage }; onUploadError?.(errorMessage); return uploadResult; } }, [uploadEndpoint, progressUpdateInterval, onUploadStart, onUploadSuccess, onUploadError]); const resetUpload = useCallback2(() => { setUploadProgress({ isUploading: false, progress: 0, error: void 0 }); }, []); return { uploadProgress, uploadDocument, resetUpload }; } // src/data/useDashboardData.ts import { useCallback as useCallback3, useEffect as useEffect2, useState as useState3 } from "react"; function useDashboardData(options = {}) { const { endpoint = "/api/v1/analytics/dashboard", timeRange = "24h", refreshInterval = 5 * 60 * 1e3, // 5 minutes authToken, headers = {}, onError, onSuccess, transformData } = options; const [data, setData] = useState3(null); const [loading, setLoading] = useState3(true); const [error, setError] = useState3(null); const fetchDashboardData = useCallback3(async () => { try { setLoading(true); setError(null); const url = new URL(endpoint, window.location.origin); url.searchParams.set("timeRange", timeRange); const requestHeaders = { "Content-Type": "application/json", ...headers }; if (authToken) { requestHeaders.Authorization = `Bearer ${authToken}`; } else if (typeof window !== "undefined") { const storedToken = localStorage.getItem("auth_token"); if (storedToken) { requestHeaders.Authorization = `Bearer ${storedToken}`; } } const response = await fetch(url.toString(), { headers: requestHeaders }); if (!response.ok) { throw new Error(`Failed to fetch dashboard data: ${response.status} ${response.statusText}`); } const responseData = await response.json(); const rawData = responseData.data || responseData; const dashboardData = transformData ? transformData(rawData) : rawData; setData(dashboardData); onSuccess?.(dashboardData); } catch (err) { const errorObj = err; setError(errorObj); onError?.(errorObj); } finally { setLoading(false); } }, [endpoint, timeRange, authToken, headers, onError, onSuccess, transformData]); useEffect2(() => { fetchDashboardData(); if (refreshInterval > 0) { const interval = setInterval(fetchDashboardData, refreshInterval); return () => clearInterval(interval); } }, [fetchDashboardData, refreshInterval]); return { data, loading, error, refetch: fetchDashboardData }; } function createMockDashboardData() { return { traffic: { total: 12540, trend: 8.2, hourly: Array.from({ length: 24 }, (_, i) => ({ hour: i, value: Math.floor(Math.random() * 100) + 50 })), sources: [ { source: "Direct", visits: 4521, percentage: 36.1 }, { source: "Google", visits: 3762, percentage: 30 }, { source: "Social", visits: 2384, percentage: 19 }, { source: "Referral", visits: 1873, percentage: 14.9 } ] }, conversion: { rate: 3.24, trend: 12.5 }, funnel: { visitors: 12540, formStarted: 2856, formCompleted: 1947, quoteRequested: 1432, converted: 406 }, aov: { value: 189.5, trend: -2.1 }, whatsapp: { clickRate: 14.2, trend: 18.7, todayClicks: 89 }, devices: [ { deviceType: "Mobile", sessions: 7524, conversionRate: 2.8 }, { deviceType: "Desktop", sessions: 4320, conversionRate: 4.1 }, { deviceType: "Tablet", sessions: 696, conversionRate: 2.2 } ], channels: [ { channel: "Google Ads", visits: 3200, conversions: 128, cost: 850, revenue: 24320, roi: 2762 }, { channel: "Facebook", visits: 2100, conversions: 67, cost: 420, revenue: 12683, roi: 2921 }, { channel: "Email", visits: 890, conversions: 45, cost: 120, revenue: 8535, roi: 7013 } ] }; } // src/data/useRealtimeData.ts import { useCallback as useCallback4, useEffect as useEffect3, useRef, useState as useState4 } from "react"; function useRealtimeData(options) { const { table, filter, eventType = "*", onData, onError, onConnected, onDisconnected, enabled = true, reconnectInterval = 5e3 } = options; const [isConnected, setIsConnected] = useState4(false); const [lastUpdate, setLastUpdate] = useState4(null); const [error, setError] = useState4(null); const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); const connect = useCallback4(() => { if (wsRef.current?.readyState === WebSocket.OPEN) { return; } try { const wsUrl = `ws://localhost:3001/realtime?table=${table}&filter=${filter || ""}&events=${eventType}`; const ws = new WebSocket(wsUrl); ws.onopen = () => { console.log(`\u{1F4E1} Connected to realtime updates for table: ${table}`); setIsConnected(true); setError(null); onConnected?.(); }; ws.onmessage = (event) => { try { const payload = JSON.parse(event.data); setLastUpdate(/* @__PURE__ */ new Date()); onData?.(payload); } catch (parseError) { console.error("Failed to parse realtime message:", parseError); onError?.(parseError); } }; ws.onclose = (event) => { console.log(`\u{1F50C} Disconnected from realtime updates for table: ${table}`, event.reason); setIsConnected(false); onDisconnected?.(); if (enabled && event.code !== 1e3 && reconnectInterval > 0) { reconnectTimeoutRef.current = setTimeout(() => { console.log(`\u{1F504} Attempting to reconnect to ${table}...`); connect(); }, reconnectInterval); } }; ws.onerror = (event) => { console.error(`\u274C WebSocket error for table ${table}:`, event); const wsError = new Error(`WebSocket connection failed for table: ${table}`); setError(wsError); onError?.(wsError); }; wsRef.current = ws; } catch (connectionError) { console.error(`Failed to create WebSocket connection for ${table}:`, connectionError); const error2 = connectionError; setError(error2); onError?.(error2); } }, [table, filter, eventType, onData, onError, onConnected, onDisconnected, enabled, reconnectInterval]); const disconnect = useCallback4(() => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); reconnectTimeoutRef.current = null; } if (wsRef.current) { wsRef.current.close(1e3, "Manual disconnect"); wsRef.current = null; } setIsConnected(false); setError(null); }, []); const sendMessage = useCallback4((message) => { if (wsRef.current?.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify(message)); } else { console.warn("Cannot send message: WebSocket is not connected"); } }, []); useEffect3(() => { if (enabled) { connect(); } else { disconnect(); } return () => { disconnect(); }; }, [enabled, connect, disconnect]); return { isConnected, lastUpdate, error, connect, disconnect, sendMessage }; } function useLocalStorageSync(options) { const { key, syncInterval = 3e4, // 30 seconds syncEndpoint = "/api/sync", onSyncSuccess, onSyncError, transformBeforeSync } = options; const [lastSyncTime, setLastSyncTime] = useState4(null); const [isSyncing, setIsSyncing] = useState4(false); const intervalRef = useRef(null); const lastSyncDataRef = useRef(""); const saveToLocal = useCallback4((data) => { try { const dataWithTimestamp = { ...data, lastModified: (/* @__PURE__ */ new Date()).toISOString(), clientId: typeof window !== "undefined" ? window.crypto?.randomUUID?.() || Math.random().toString(36) : Math.random().toString(36) }; localStorage.setItem(key, JSON.stringify(dataWithTimestamp)); } catch (error) { console.error("Failed to save to localStorage:", error); } }, [key]); const getFromLocal = useCallback4(() => { try { if (typeof window === "undefined") return null; const item = localStorage.getItem(key); return item ? JSON.parse(item) : null; } catch (error) { console.error("Failed to read from localStorage:", error); return null; } }, [key]); const syncToRemote = useCallback4(async () => { if (isSyncing) return false; const localData = getFromLocal(); if (!localData) return false; const transformedData = transformBeforeSync ? transformBeforeSync(localData) : localData; const currentData = JSON.stringify(transformedData); if (currentData === lastSyncDataRef.current) return true; setIsSyncing(true); try { const response = await fetch(syncEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: currentData }); if (response.ok) { lastSyncDataRef.current = currentData; const syncTime = /* @__PURE__ */ new Date(); setLastSyncTime(syncTime); onSyncSuccess?.(localData); console.log("\u2705 Successfully synced to remote"); return true; } else { const error = new Error(`Sync failed: ${response.status} ${response.statusText}`); onSyncError?.(error); console.error("\u274C Failed to sync to remote:", error.message); return false; } } catch (error) { const syncError = error; onSyncError?.(syncError); console.error("\u274C Sync error:", syncError.message); return false; } finally { setIsSyncing(false); } }, [key, syncEndpoint, getFromLocal, transformBeforeSync, onSyncSuccess, onSyncError, isSyncing]); useEffect3(() => { if (syncInterval > 0) { intervalRef.current = setInterval(() => { syncToRemote(); }, syncInterval); } const handleVisibilityChange = () => { if (document.visibilityState === "visible") { syncToRemote(); } }; const handleBeforeUnload = () => { const localData = getFromLocal(); if (localData && navigator.sendBeacon) { const transformedData = transformBeforeSync ? transformBeforeSync(localData) : localData; navigator.sendBeacon( syncEndpoint, JSON.stringify(transformedData) ); } }; if (typeof document !== "undefined") { document.addEventListener("visibilitychange", handleVisibilityChange); window.addEventListener("beforeunload", handleBeforeUnload); } return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } if (typeof document !== "undefined") { document.removeEventListener("visibilitychange", handleVisibilityChange); window.removeEventListener("beforeunload", handleBeforeUnload); } }; }, [syncInterval, syncToRemote, getFromLocal, syncEndpoint, transformBeforeSync]); return { saveToLocal, getFromLocal, syncToRemote, lastSyncTime, isSyncing }; } export { useLeadData, useDocumentUpload, useDashboardData, createMockDashboardData, useRealtimeData, useLocalStorageSync };