UNPKG

@bigdigital/kiosk-content-sdk

Version:

A Firebase-powered Content Management System SDK optimized for kiosks with offline support, template management, and real-time connection monitoring

670 lines (557 loc) 20 kB
import { useState, useEffect } from "react"; import { KioskClient } from "./client"; import type { Content, Template, KioskConfig, CacheOptions, SyncOptions, TemplateValues } from "./types"; const CACHE_KEY_PREFIX = 'kiosk_content_cache_'; const CACHE_TIMESTAMP_KEY = 'kiosk_content_last_sync'; function getCacheKey(projectId: string) { return `${CACHE_KEY_PREFIX}${projectId}`; } // Helper function to find field by id, consistent with client.ts function findFieldById(fields: Template['fields'], fieldId: string) { return fields.find(field => field.id === fieldId); } function processTemplateContent(template: Template, content: Content): Content { if (!template) return content; console.log("Processing content with template:", { contentId: content.id, templateId: template.id, templateValues: content.templateValues }); // Start with existing template values if available const templateValues: TemplateValues = content.templateValues || { groups: {}, ungrouped: {} }; // Process fields in groups Object.entries(template.groups || {}).forEach(([_, group]) => { if (!group || !group.normalizedName) { console.error(`Group ${group?.name ?? 'unknown'} is missing normalizedName`); return; } const normalizedName = group.normalizedName; console.log(`Processing group ${group.name} -> ${normalizedName}`); // Initialize group if it doesn't exist if (!templateValues.groups[normalizedName]) { templateValues.groups[normalizedName] = {}; } // Process fields in this group (group.fieldIds || []).forEach(fieldId => { const field = findFieldById(template.fields, fieldId); if (field?.name) { // Only set default value if the field doesn't exist if (templateValues.groups[normalizedName][field.name] === undefined) { templateValues.groups[normalizedName][field.name] = field.defaultValue ?? null; } } }); }); // Process ungrouped fields (template.ungroupedFieldIds || []).forEach(fieldId => { const field = findFieldById(template.fields, fieldId); if (field?.name) { // Only set default value if the field doesn't exist if (templateValues.ungrouped[field.name] === undefined) { templateValues.ungrouped[field.name] = field.defaultValue ?? null; } } }); console.log("Processed template values:", templateValues); return { ...content, templateValues }; } function isOffline(): boolean { return !navigator.onLine; } function saveToCache(projectId: string, data: Content[]) { try { const cacheData = { timestamp: Date.now(), content: data }; localStorage.setItem(getCacheKey(projectId), JSON.stringify(cacheData)); localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString()); } catch (err) { console.warn('Failed to save to cache:', err); } } function getFromCache(projectId: string): { content: Content[]; timestamp: number } | null { try { const cached = localStorage.getItem(getCacheKey(projectId)); if (!cached) return null; const parsed = JSON.parse(cached); if (!parsed || !parsed.content || !Array.isArray(parsed.content)) { return null; } return { content: parsed.content, timestamp: parsed.timestamp || 0 }; } catch (err) { console.warn('Failed to read from cache:', err); return null; } } function getCacheTimestamp(): number { return parseInt(localStorage.getItem(CACHE_TIMESTAMP_KEY) || '0', 10); } function isCacheValid(timestamp: number, maxAge: number): boolean { return Date.now() - timestamp <= maxAge; } function isCacheStale(maxAge: number): boolean { const lastSync = getCacheTimestamp(); return Date.now() - lastSync > maxAge; } function filterPublishedProjectContent(content: Content[]): Content[] { return content.filter(item => item.status === "published" && item.projectIds && item.projectIds.length > 0 ); } export function useKioskContent(config: KioskConfig) { const [content, setContent] = useState<Content[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const [isSyncing, setIsSyncing] = useState(false); const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); useEffect(() => { const client = new KioskClient(config); let isMounted = true; async function fetchContentAndTemplates() { if (!isOnline && config.offlineSupport) { const cached = getFromCache(config.projectId); if (cached) { const templates = await client.getTemplates().catch(() => []); const processedContent = filterPublishedProjectContent(cached.content).map(item => { if (item.templateId) { const template = templates.find(t => t.id === item.templateId); if (template) { return processTemplateContent(template, item); } } return item; }); if (isMounted) { setContent(processedContent); setLoading(false); } return; } } try { setIsSyncing(true); const [contentData, templates] = await Promise.all([ client.getPublishedContent(), client.getTemplates() ]); if (isMounted) { console.log("Raw content data:", contentData); console.log("Available templates:", templates); const processedContent = filterPublishedProjectContent(contentData).map(item => { if (item.templateId) { const template = templates.find(t => t.id === item.templateId); if (template) { console.log("Processing content with template:", { contentId: item.id, templateId: template.id, beforeProcess: item.templateValues }); const processed = processTemplateContent(template, item); console.log("After processing:", processed.templateValues); return processed; } } return item; }); console.log("Final processed content:", processedContent); setContent(processedContent); if (config.offlineSupport) { saveToCache(config.projectId, processedContent); } } } catch (err) { console.error("Error fetching content:", err); if (isMounted) { if (config.offlineSupport) { const cached = getFromCache(config.projectId); if (cached) { setContent(filterPublishedProjectContent(cached.content)); } else { setError(err instanceof Error ? err : new Error('Failed to fetch content')); } } else { setError(err instanceof Error ? err : new Error('Failed to fetch content')); } } } finally { if (isMounted) { setIsSyncing(false); setLoading(false); } } } fetchContentAndTemplates(); let syncInterval: NodeJS.Timeout | undefined; if (config.syncInterval && isOnline) { syncInterval = setInterval(fetchContentAndTemplates, config.syncInterval); } return () => { isMounted = false; if (syncInterval) { clearInterval(syncInterval); } }; }, [config.projectId, config.syncInterval, config.offlineSupport, config.cacheMaxAge, isOnline]); return { content, loading, error, isSyncing, isOnline }; } export function useProjectContent(config: KioskConfig, projectId: string) { const [content, setContent] = useState<Content[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); useEffect(() => { const client = new KioskClient(config); let isMounted = true; async function fetchContent() { if (!isOnline && config.offlineSupport) { const cached = getFromCache(`${config.projectId}_project_${projectId}`); if (cached) { setContent(cached.content); setLoading(false); return; } } try { const data = await client.getContentByProject(projectId); if (isMounted) { const filteredContent = data.filter(item => filterPublishedProjectContent([item]).length > 0 && item.projectIds?.includes(projectId) ); setContent(filteredContent); if (config.offlineSupport) { saveToCache(`${config.projectId}_project_${projectId}`, filteredContent); } } } catch (err) { if (isMounted && config.offlineSupport) { const cached = getFromCache(`${config.projectId}_project_${projectId}`); if (cached) { setContent(cached.content); } else { setError(err instanceof Error ? err : new Error('Failed to fetch project content')); } } else if (isMounted) { setError(err instanceof Error ? err : new Error('Failed to fetch project content')); } } finally { if (isMounted) { setLoading(false); } } } if (!isOnline && config.offlineSupport) { const cached = getFromCache(`${config.projectId}_project_${projectId}`); if (cached) { setContent(cached.content); setLoading(false); } } if (isOnline) { fetchContent(); } return () => { isMounted = false; }; }, [config.projectId, projectId, config.offlineSupport, isOnline]); return { content, loading, error, isOnline }; } export function useContentWithTemplate(config: KioskConfig, contentId: string) { const [content, setContent] = useState<Content | null>(null); const [template, setTemplate] = useState<Template | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); useEffect(() => { const client = new KioskClient(config); let isMounted = true; async function fetchContent() { if (!isOnline && config.offlineSupport) { // Add offline cache handling here if needed. This hook doesn't currently use caching. } try { const { content: contentData, template: templateData } = await client.getContentWithTemplate(contentId); setContent(contentData); setTemplate(templateData); } catch (err) { setError(err instanceof Error ? err : new Error('Failed to fetch content with template')); } finally { setLoading(false); } } fetchContent(); }, [config.projectId, contentId, isOnline]); return { content, template, loading, error, isOnline }; } export function useTemplates(config: KioskConfig) { const [templates, setTemplates] = useState<Template[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); useEffect(() => { const client = new KioskClient(config); let isMounted = true; async function fetchTemplates() { if (!isOnline && config.offlineSupport) { // Add offline cache handling here if needed. This hook doesn't currently use caching. } try { const data = await client.getTemplates(); setTemplates(data); } catch (err) { setError(err instanceof Error ? err : new Error('Failed to fetch templates')); } finally { setLoading(false); } } fetchTemplates(); }, [config.projectId, isOnline]); return { templates, loading, error, isOnline }; } export function useTemplate(config: KioskConfig, templateId: string) { const [template, setTemplate] = useState<Template | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); useEffect(() => { const client = new KioskClient(config); let isMounted = true; async function fetchTemplate() { if (!isOnline && config.offlineSupport) { // Add offline cache handling here if needed. This hook doesn't currently use caching. } try { const data = await client.getTemplateById(templateId); setTemplate(data); } catch (err) { setError(err instanceof Error ? err : new Error('Failed to fetch template')); } finally { setLoading(false); } } fetchTemplate(); }, [config.projectId, templateId, isOnline]); return { template, loading, error, isOnline }; } export function useTemplateContent(config: KioskConfig, templateId: string) { const [content, setContent] = useState<Content[]>([]); const [template, setTemplate] = useState<Template | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); useEffect(() => { const client = new KioskClient(config); let isMounted = true; async function fetchTemplateContent() { if (!isOnline && config.offlineSupport) { // Add offline cache handling here if needed. This hook doesn't currently use caching. } try { const templateData = await client.getTemplateById(templateId); setTemplate(templateData); if (templateData) { const publishedContent = await client.getPublishedContent(); const templateContent = publishedContent.filter(c => c.templateId === templateId); setContent(templateContent); } } catch (err) { setError(err instanceof Error ? err : new Error('Failed to fetch template content')); } finally { setLoading(false); } } fetchTemplateContent(); }, [config.projectId, templateId, isOnline]); return { content, template, loading, error, isOnline }; } export function useOfflineContent(config: KioskConfig) { const [content, setContent] = useState<Content[]>([]); const [syncStatus, setSyncStatus] = useState<"synced" | "pending" | "failed">("pending"); const [lastSynced, setLastSynced] = useState<number | null>(null); const [error, setError] = useState<Error | null>(null); const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); useEffect(() => { const client = new KioskClient(config); let syncInterval: NodeJS.Timeout; async function syncContent() { if (!isOnline) return; setSyncStatus("pending"); try { const data = await client.getPublishedContent(); saveToCache(config.projectId, data); setContent(data); setLastSynced(Date.now()); setSyncStatus("synced"); } catch (err) { setSyncStatus("failed"); setError(err instanceof Error ? err : new Error('Content sync failed')); } } syncContent(); if (config.syncInterval) { syncInterval = setInterval(syncContent, config.syncInterval); } return () => { if (syncInterval) { clearInterval(syncInterval); } }; }, [config.projectId, config.syncInterval, isOnline]); return { content, syncStatus, lastSynced, error, isOnline }; } export function useContentSync(config: KioskConfig) { const [syncStatus, setSyncStatus] = useState<"idle" | "syncing" | "success" | "error">("idle"); const [progress, setProgress] = useState(0); const [error, setError] = useState<Error | null>(null); const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); const sync = async (options?: SyncOptions) => { if (!isOnline) { setError(new Error("Cannot sync while offline")); return; } setSyncStatus("syncing"); setProgress(0); setError(null); try { const client = new KioskClient(config); const progressInterval = setInterval(() => { setProgress(prev => Math.min(prev + 10, 90)); }, 500); await client.getPublishedContent(); clearInterval(progressInterval); setProgress(100); setSyncStatus("success"); } catch (err) { setSyncStatus("error"); setError(err instanceof Error ? err : new Error('Sync failed')); if (options?.onError) { options.onError(err instanceof Error ? err : new Error('Sync failed')); } } }; return { sync, syncStatus, progress, error, isOnline }; }