@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
JavaScript
// 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
};