"use client"; import React from "react"; import { useState, useEffect, useCallback } from "react"; import { compileCustomLayout, CompiledLayout } from "./compileLayout"; import TemplateService from "../(presentation-generator)/services/api/template"; import { isJsonLayoutCode, parseLayoutSchema, ParsedLayout, LayoutSchema } from "./parseLayoutSchema"; import { SlideRenderer } from "../(presentation-generator)/components/SlideRenderer"; // Adapter: convert ParsedLayout (JSON schema) into a CompiledLayout-compatible object // so existing code that uses CompiledLayout can work with both formats. function parsedLayoutToCompiled(parsed: ParsedLayout): CompiledLayout { const schema: LayoutSchema = parsed.schema; // Create a React component that renders the JSON schema function JsonSlideComponent({ data }: { data: any }) { return React.createElement(SlideRenderer, { schema, content: data }); } JsonSlideComponent.displayName = parsed.layoutName; return { component: JsonSlideComponent, layoutId: parsed.layoutId, layoutName: parsed.layoutName, layoutDescription: parsed.layoutDescription, schema: null, sampleData: parsed.sampleData, schemaJSON: null, }; } export interface TemplateSummary { id: string; name: string; total_layouts: number; } export interface RawLayoutResponse { template: string; layout_id: string; layout_name: string; layout_code: string; fonts?: string[]; } export interface CustomTemplateDetailResponse { layouts: RawLayoutResponse[]; template: any; fonts?: string[]; } // Compiled layout with all metadata export interface CustomTemplateLayout extends CompiledLayout { templateId: string; rawLayoutId: string; rawLayoutName: string; layoutCode: string; fonts?: string[]; } export interface CustomTemplateDetail { layouts: CustomTemplateLayout[]; name: string; description: string; id: string; template: any; fonts?: string[]; } // Custom templates for the main page export interface CustomTemplates { id: string; name: string; layoutCount: number; isCustom: true; } // GLOBAL CACHE const customTemplateDetailsCache = new Map(); // GLOBAL IN-FLIGHT PROMISE TRACKER - prevents duplicate API calls for same ID const inFlightRequests = new Map>(); // GLOBAL CACHE: compiled first-slide previews (we only compile the first layout) const customTemplateFirstSlideCache = new Map(); // GLOBAL IN-FLIGHT PROMISE TRACKER - prevents duplicate preview calls for same ID const inFlightFirstSlideRequests = new Map>(); function normalizeCustomTemplateId(id: string): string { if (!id) return id; return id.startsWith("custom-") ? id.slice("custom-".length) : id; } /** * Fetch + compile ONLY the first layout for a custom template. * Accepts either a raw presentationId or a "custom-..." id. * Uses global cache + in-flight request deduplication. */ export async function getCustomTemplateFirstSlidePreview( presentationIdOrCustomId: string ): Promise { const presentationId = normalizeCustomTemplateId(presentationIdOrCustomId); if (!presentationId) return null; // Cache first if (customTemplateFirstSlideCache.has(presentationId)) { return customTemplateFirstSlideCache.get(presentationId) ?? null; } // In-flight dedupe const existing = inFlightFirstSlideRequests.get(presentationId); if (existing) return existing; const fetchPromise = (async (): Promise => { try { const data: CustomTemplateDetailResponse = await TemplateService.getCustomTemplateDetails(presentationId); const firstLayout = data?.layouts?.[0]; if (!firstLayout?.layout_code) { customTemplateFirstSlideCache.set(presentationId, null); return null; } let compiled: CompiledLayout | null = null; if (isJsonLayoutCode(firstLayout.layout_code)) { const parsed = parseLayoutSchema(firstLayout.layout_code); if (parsed) compiled = parsedLayoutToCompiled(parsed); } else { compiled = compileCustomLayout(firstLayout.layout_code); } customTemplateFirstSlideCache.set(presentationId, compiled); return compiled; } catch (err) { console.error("Error fetching first-slide preview:", err); // Don't cache errors; allow retry next time. return null; } finally { inFlightFirstSlideRequests.delete(presentationId); } })(); inFlightFirstSlideRequests.set(presentationId, fetchPromise); return fetchPromise; } /** * Standalone async function to fetch and compile custom template details * Can be called from hooks or regular async functions (like handleSubmit) * Uses global cache and in-flight request deduplication */ export async function getCustomTemplateDetails( templateId: string, name: string = "Custom Template", description: string = "User-created template" ): Promise { if (!templateId) { return null; } // Check cache first const cachedTemplate = customTemplateDetailsCache.get(templateId); if (cachedTemplate) { return cachedTemplate; } // Check if there's already an in-flight request for this ID const existingRequest = inFlightRequests.get(templateId); if (existingRequest) { return existingRequest; } // Create new request and track it const fetchPromise = (async (): Promise => { try { const data: CustomTemplateDetailResponse = await TemplateService.getCustomTemplateDetails(templateId); // Compile each layout const compiledLayouts: CustomTemplateLayout[] = []; for (const layout of data.layouts) { try { let compiled: CompiledLayout | null = null; if (isJsonLayoutCode(layout.layout_code)) { const parsed = parseLayoutSchema(layout.layout_code); if (parsed) compiled = parsedLayoutToCompiled(parsed); } else { compiled = compileCustomLayout(layout.layout_code); } if (compiled) { compiledLayouts.push({ ...compiled, templateId: layout.template, rawLayoutId: layout.layout_id, rawLayoutName: layout.layout_name, layoutCode: layout.layout_code, fonts: layout.fonts, }); } else { console.warn(`Failed to compile/parse layout: ${layout.layout_name}`); } } catch (compileError) { console.error(`Error compiling ${layout.layout_name}:`, compileError); } } const result: CustomTemplateDetail = { layouts: compiledLayouts, name, description, id: templateId, template: data.template ? data.template : null, fonts: data.fonts }; // Cache the result customTemplateDetailsCache.set(templateId, result); return result; } catch (err) { console.error("Error fetching template details:", err); throw err; } finally { // Clean up in-flight tracker inFlightRequests.delete(templateId); } })(); // Track this request inFlightRequests.set(templateId, fetchPromise); return fetchPromise; } /** * Hook to fetch custom template summaries */ export function useCustomTemplateSummaries() { const [templates, setTemplates] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const fetchTemplates = useCallback(async () => { try { setLoading(true); setError(null); const data = await TemplateService.getCustomTemplateSummaries(); // const mappedTemplates: CustomTemplates[] = data.filter(item => item.total_layouts && item.total_layouts > 0).map((item) => { // return { // id: item.id, // name: item.name || "Custom Template", // layoutCount: item.total_layouts, // isCustom: true as const, // } // }); const mappedTemplates: CustomTemplates[] = data.presentations .filter((item: any) => item.template != null) .map((item: any) => { return { id: item.template.id, name: item.template.name || "Custom Template", layoutCount: item.layout_count ?? 0, isCustom: true as const, } }); setTemplates(mappedTemplates); } catch (err) { console.error("Error fetching custom templates:", err); setError(err instanceof Error ? err.message : "Unknown error"); setTemplates([]); } finally { setLoading(false); } }, []); useEffect(() => { fetchTemplates(); }, [fetchTemplates]); return { templates, loading, error, refetch: fetchTemplates }; } /** * Hook to fetch and compile custom template layouts * Uses global cache and in-flight request deduplication to prevent duplicate API calls */ export function useCustomTemplateDetails(templateDetail: { id: string, name: string, description: string }) { const [template, setTemplate] = useState(() => { return templateDetail.id ? customTemplateDetailsCache.get(templateDetail.id) ?? null : null; }); const [fonts, setFonts] = useState([]); const [loading, setLoading] = useState(() => { return templateDetail.id ? !customTemplateDetailsCache.has(templateDetail.id) : false; }); const [error, setError] = useState(null); const fetchTemplateDetails = useCallback(async () => { if (!templateDetail.id) { return; } // Check cache first - instant return if cached const cachedTemplate = customTemplateDetailsCache.get(templateDetail.id); if (cachedTemplate) { setTemplate(cachedTemplate); setFonts(cachedTemplate?.fonts ?? []); setLoading(false); return; } // Check if there's already an in-flight request for this ID const existingRequest = inFlightRequests.get(templateDetail.id); if (existingRequest) { // Wait for the existing request instead of making a new one setLoading(true); try { const result = await existingRequest; if (result) { setTemplate(result); setFonts(result?.fonts ?? []); } } catch (err) { setError(err instanceof Error ? err.message : "Unknown error"); } finally { setLoading(false); } return; } // Create new request and track it setLoading(true); setError(null); const fetchPromise = (async (): Promise => { try { const data: CustomTemplateDetailResponse = await TemplateService.getCustomTemplateDetails(templateDetail.id); // Compile each layout const compiledLayouts: CustomTemplateLayout[] = []; for (const layout of data.layouts) { try { let compiled: CompiledLayout | null = null; if (isJsonLayoutCode(layout.layout_code)) { const parsed = parseLayoutSchema(layout.layout_code); if (parsed) compiled = parsedLayoutToCompiled(parsed); } else { compiled = compileCustomLayout(layout.layout_code); } if (compiled) { compiledLayouts.push({ ...compiled, templateId: layout.template, rawLayoutId: layout.layout_id, rawLayoutName: layout.layout_name, layoutCode: layout.layout_code, fonts: layout.fonts, layoutId: compiled?.layoutId ?? "", }); } else { console.warn(`Failed to compile/parse layout: ${layout.layout_name}`); } } catch (compileError) { console.error(`Error compiling ${layout.layout_name}:`, compileError); } } const result: CustomTemplateDetail = { layouts: compiledLayouts, name: templateDetail.name, description: templateDetail.description, id: templateDetail.id, template: data.template, fonts: data.fonts }; // Cache the result customTemplateDetailsCache.set(templateDetail.id, result); return result; } catch (err) { console.error("Error fetching template details:", err); throw err; } finally { // Clean up in-flight tracker inFlightRequests.delete(templateDetail.id); } })(); // Track this request inFlightRequests.set(templateDetail.id, fetchPromise); try { const result = await fetchPromise; if (result) { setTemplate(result); setFonts(result?.fonts ?? []); } } catch (err) { setError(err instanceof Error ? err.message : "Unknown error"); setTemplate(null); setFonts([]); } finally { setLoading(false); } }, [templateDetail.id, templateDetail.name, templateDetail.description]); useEffect(() => { if (templateDetail.id) { fetchTemplateDetails(); } }, [templateDetail.id, fetchTemplateDetails]); return { template, loading, error, refetch: fetchTemplateDetails, fonts }; } /** * Hook to fetch and compile preview layouts for a single template (first 4 layouts) */ export function useCustomTemplatePreview(presentationId: string) { const [previewLayouts, setPreviewLayouts] = useState([]); const [loading, setLoading] = useState(true); const [totalLayouts, setTotalLayouts] = useState(0); useEffect(() => { if (!presentationId) return; const fetchPreviews = async () => { try { setLoading(true); const data = await TemplateService.getCustomTemplateDetails(presentationId); setTotalLayouts(data.layouts.length); // Compile first 4 layouts for preview const compiled: CompiledLayout[] = []; const layoutsToPreview = data.layouts.slice(0, 4); for (const layout of layoutsToPreview) { try { if (isJsonLayoutCode(layout.layout_code)) { const parsed = parseLayoutSchema(layout.layout_code); if (parsed) { compiled.push(parsedLayoutToCompiled(parsed)); } } else { const result = compileCustomLayout(layout.layout_code); if (result) { compiled.push(result); } } } catch (e) { console.warn(`Failed to compile/parse preview: ${layout.layout_name}`); } } setPreviewLayouts(compiled); } catch (err) { console.error("Error fetching preview layouts:", err); } finally { setLoading(false); } }; fetchPreviews(); }, [presentationId]); return { previewLayouts, loading: loading, totalLayouts: totalLayouts }; } /** * Hook to fetch and compile preview for ONLY the first layout of a custom template. * Accepts either a raw presentationId or a "custom-..." id. */ export function useCustomTemplateFirstSlidePreview(presentationIdOrCustomId: string) { const [previewLayout, setPreviewLayout] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { if (!presentationIdOrCustomId) return; let cancelled = false; const run = async () => { try { setLoading(true); setError(null); const compiled = await getCustomTemplateFirstSlidePreview(presentationIdOrCustomId); if (cancelled) return; setPreviewLayout(compiled); } catch (e) { if (cancelled) return; setError(e instanceof Error ? e.message : "Unknown error"); setPreviewLayout(null); } finally { if (!cancelled) setLoading(false); } }; run(); return () => { cancelled = true; }; }, [presentationIdOrCustomId]); return { previewLayout, loading, error }; }