diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx index cdc12568..ea71b7ff 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx @@ -20,9 +20,9 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(`${template.id}`); const handleOpen = useCallback(() => { if (template.id.startsWith('custom-')) { - router.push(`/template-preview/${template.id}`) + router.push(`/template-preview?slug=${template.id}`) } else { - router.push(`/template-preview/custom-${template.id}`) + router.push(`/template-preview?slug=custom-${template.id}`) } } , [router, template.id]); @@ -46,7 +46,7 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ [...Array(Math.min(4, template.layoutCount))].map((_, index) => (
@@ -172,7 +172,7 @@ const LayoutPreview = () => { } }, []); - const handleOpenPreview = useCallback((id: string) => router.push(`/template-preview/${id}`), [router]); + const handleOpenPreview = useCallback((id: string) => router.push(`/template-preview?slug=${id}`), [router]); @@ -193,7 +193,7 @@ const LayoutPreview = () => { return (
= ({ const id = await onSave(layoutName.trim(), description.trim()); if (id) { // Redirect to the new template preview page - router.push(`/template-preview/custom-${id}`); + router.push(`/template-preview?slug=custom-${id}`); } // Reset form after navigation decision setLayoutName(""); diff --git a/electron/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx b/electron/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx index 2cc41b71..56391b8d 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx @@ -47,7 +47,7 @@ const CustomTemplatePage = () => { trackEvent(MixpanelEvent.CustomTemplate_Save_Templates_API_Call); const id = await saveLayout(layoutName, description); if (id) { - router.push(`/template-preview/custom-${id}`); + router.push(`/template-preview?slug=custom-${id}`); } return id; }; @@ -94,7 +94,7 @@ const CustomTemplatePage = () => { } return ( -
+
{/* Header */} diff --git a/electron/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx b/electron/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx index 4e8895a1..57ef261a 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx @@ -36,9 +36,9 @@ const Header = () => { Create Template trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/template-preview" })} + onClick={() => trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/templates" })} className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none" role="menuitem" > diff --git a/electron/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/hooks/useTemplateLayoutsAutoSave.ts b/electron/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/hooks/useTemplateLayoutsAutoSave.ts deleted file mode 100644 index 117ea6b6..00000000 --- a/electron/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/hooks/useTemplateLayoutsAutoSave.ts +++ /dev/null @@ -1,185 +0,0 @@ -'use client'; - -import { useEffect, useRef, useCallback, useState } from 'react'; -import { getHeader } from '@/app/(presentation-generator)/services/api/header'; -import { ApiResponseHandler } from '@/app/(presentation-generator)/services/api/api-error-handler'; -import { ProcessedSlide } from '@/app/(presentation-generator)/custom-template/types'; -import { CustomTemplateLayout } from '@/app/hooks/useCustomTemplates'; -import { getApiUrl } from '@/utils/api'; - -interface LayoutPayload { - layout_id: string; - layout_code: string; - layout_name: string; -} - -/** Slide state for template preview: ProcessedSlide plus saved layout code and name */ -export type TemplatePreviewSlideState = ProcessedSlide & { - react?: string; - layout_name?: string; -}; - -interface UseTemplateLayoutsAutoSaveOptions { - templateId: string | null; - layouts: CustomTemplateLayout[]; - slideStates: TemplatePreviewSlideState[]; - debounceMs?: number; - enabled?: boolean; -} - -export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'; - -export const useTemplateLayoutsAutoSave = ({ - templateId, - layouts, - slideStates, - debounceMs = 2000, - enabled = true, -}: UseTemplateLayoutsAutoSaveOptions) => { - const saveTimeoutRef = useRef(null); - const lastSavedDataRef = useRef(''); - const [saveStatus, setSaveStatus] = useState('idle'); - const isSavingRef = useRef(false); - const [lastSavedAt, setLastSavedAt] = useState(null); - - // Build the payload for saving - const buildPayload = useCallback((): LayoutPayload[] => { - const payload: LayoutPayload[] = []; - - layouts.forEach((layout, index) => { - const slideState = slideStates[index]; - if (slideState?.react && layout.rawLayoutId) { - payload.push({ - layout_id: layout.rawLayoutId, - layout_code: slideState.react, - layout_name: slideState.layout_name || `Slide${index + 1}` - }); - } - }); - - return payload; - }, [layouts, slideStates]); - - // Save function - const saveLayouts = useCallback(async (payload: LayoutPayload[]) => { - if (!templateId || payload.length === 0 || isSavingRef.current) { - return false; - } - - const currentDataString = JSON.stringify(payload); - - // Skip if data hasn't changed since last save - if (currentDataString === lastSavedDataRef.current) { - return false; - } - - try { - isSavingRef.current = true; - setSaveStatus('saving'); - console.log('🔄 Auto-saving template layouts...'); - - const response = await fetch(getApiUrl('/api/v1/ppt/template/update'), { - method: 'PUT', - headers: getHeader(), - body: JSON.stringify({ - id: templateId, - - layouts: payload, - }), - }); - - await ApiResponseHandler.handleResponse(response, 'Failed to auto-save layouts'); - - // Update last saved data reference - lastSavedDataRef.current = currentDataString; - setLastSavedAt(new Date()); - setSaveStatus('saved'); - console.log('✅ Auto-save successful'); - - // Reset to idle after showing "saved" briefly - setTimeout(() => { - setSaveStatus('idle'); - }, 2000); - - return true; - } catch (error) { - console.error('❌ Auto-save failed:', error); - setSaveStatus('error'); - - // Reset to idle after showing error briefly - setTimeout(() => { - setSaveStatus('idle'); - }, 3000); - - return false; - } finally { - isSavingRef.current = false; - } - }, [templateId]); - - // Debounced save trigger - const debouncedSave = useCallback(() => { - if (!enabled || !templateId) return; - - // Clear existing timeout - if (saveTimeoutRef.current) { - clearTimeout(saveTimeoutRef.current); - } - - // Set new timeout - saveTimeoutRef.current = setTimeout(() => { - const payload = buildPayload(); - if (payload.length > 0) { - saveLayouts(payload); - } - }, debounceMs); - }, [enabled, templateId, buildPayload, saveLayouts, debounceMs]); - - // Watch for changes in slideStates - useEffect(() => { - if (!enabled || !templateId || slideStates.length === 0) return; - - // Check if any slide is still processing - const hasProcessingSlide = Array.from(slideStates.values()).some( - slide => slide.processing - ); - - if (hasProcessingSlide) return; - - debouncedSave(); - - // Cleanup timeout on unmount or when dependencies change - return () => { - if (saveTimeoutRef.current) { - clearTimeout(saveTimeoutRef.current); - } - }; - }, [slideStates, enabled, templateId, debouncedSave]); - - // Manual save function - const saveNow = useCallback(async () => { - // Clear any pending debounced save - if (saveTimeoutRef.current) { - clearTimeout(saveTimeoutRef.current); - } - - const payload = buildPayload(); - return saveLayouts(payload); - }, [buildPayload, saveLayouts]); - - // Cleanup on unmount - save any pending changes - useEffect(() => { - return () => { - if (saveTimeoutRef.current) { - clearTimeout(saveTimeoutRef.current); - } - }; - }, []); - - return { - saveStatus, - lastSavedAt, - saveNow, - }; -}; - diff --git a/electron/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx b/electron/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx deleted file mode 100644 index f812c885..00000000 --- a/electron/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import GroupLayoutPreview from './components/TemplatePreviewClient'; - -// Allow dynamic params for custom templates -export const dynamicParams = true; - -// Generate static params for built-in template groups -export async function generateStaticParams() { - // Pre-render built-in template routes at build time - // Custom templates (custom-*) will be generated on-demand - return [ - { slug: 'neo-general' }, - { slug: 'neo-standard' }, - { slug: 'neo-modern' }, - { slug: 'general' }, - { slug: 'modern' }, - { slug: 'standard' }, - ]; -} - -export default function GroupLayoutPreviewPage() { - return ; -} \ No newline at end of file diff --git a/electron/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/components/TemplatePreviewClient.tsx b/electron/servers/nextjs/app/(presentation-generator)/template-preview/components/TemplatePreviewClient.tsx similarity index 89% rename from electron/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/components/TemplatePreviewClient.tsx rename to electron/servers/nextjs/app/(presentation-generator)/template-preview/components/TemplatePreviewClient.tsx index 2fd7f07c..9518db74 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/components/TemplatePreviewClient.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/template-preview/components/TemplatePreviewClient.tsx @@ -1,35 +1,30 @@ "use client"; import React, { useEffect } from "react"; -import { useParams, usePathname, useRouter } from "next/navigation"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { ArrowLeft, Home, Loader2, Trash2 } from "lucide-react"; import { MixpanelEvent, trackEvent } from "@/utils/mixpanel"; -import TemplateService from "../../../services/api/template"; -import Header from "../../../dashboard/components/Header"; +import TemplateService from "../../services/api/template"; +import Header from "../../(dashboard)/dashboard/components/Header"; import { toast } from "sonner"; import { CustomTemplateLayout, useCustomTemplateDetails } from "@/app/hooks/useCustomTemplates"; import { templates as templateGroups, getTemplatesByTemplateName } from "@/app/presentation-templates"; const GroupLayoutPreview = () => { - const params = useParams(); + const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); - const templateParams = params.slug as string; + const templateParams = searchParams.get("slug") || ""; - // Check if this is a custom template const isCustom = templateParams.startsWith("custom-"); const customTemplateId = isCustom ? templateParams.split("custom-")[1] : null; - - // Fetch static templates if not custom const staticTemplates = !isCustom ? getTemplatesByTemplateName(templateParams) : []; - const staticGroup = !isCustom ? templateGroups.find((g: { id: string }) => g.id === templateParams) : null; - // Fetch custom template details if custom const { template: customTemplate, loading: customLoading, @@ -37,8 +32,6 @@ const GroupLayoutPreview = () => { fonts: customFonts, } = useCustomTemplateDetails({ id: templateParams?.split("custom-")[1] || "", name: "", description: "" }); - - useEffect(() => { const existingScript = document.querySelector('script[src*="tailwindcss.com"]'); if (!existingScript) { @@ -60,15 +53,13 @@ const GroupLayoutPreview = () => { const success = await TemplateService.deleteCustomTemplate(customTemplateId); if (success.success) { toast.success("Template deleted successfully"); - router.push("/template-preview"); + router.push("/templates"); } else { toast.error("Failed to delete template"); } }; - - // Loading state for custom templates - if (isCustom && (customLoading)) { + if (isCustom && customLoading) { return (
@@ -80,7 +71,6 @@ const GroupLayoutPreview = () => { ); } - // Error state if (isCustom && customError) { return (
@@ -88,7 +78,7 @@ const GroupLayoutPreview = () => {

Error loading template

{customError}

- @@ -97,10 +87,9 @@ const GroupLayoutPreview = () => { ); } - // Empty state if ( (!isCustom && (!staticGroup || staticTemplates.length === 0)) || - (isCustom && (!customTemplate)) + (isCustom && !customTemplate) ) { return (
@@ -109,7 +98,7 @@ const GroupLayoutPreview = () => {

Template not found

- @@ -118,7 +107,6 @@ const GroupLayoutPreview = () => { ); } - // Determine what to render const templateName = isCustom ? customTemplate?.template.name || "Custom Template" : staticGroup?.name || ""; const templateDescription = isCustom ? customTemplate?.template.description || "" @@ -127,13 +115,10 @@ const GroupLayoutPreview = () => { ? customTemplate?.layouts.length || 0 : staticTemplates.length; - console.log('compileLayout', customTemplate) - return (
- {/* Header */}
@@ -155,7 +140,7 @@ const GroupLayoutPreview = () => { size="sm" onClick={() => { trackEvent(MixpanelEvent.TemplatePreview_All_Groups_Button_Clicked, { pathname }); - router.push("/template-preview"); + router.push("/templates"); }} className="flex items-center gap-2" > @@ -166,7 +151,6 @@ const GroupLayoutPreview = () => { {isCustom && (
-
-
- {/* Layout Grid - Wrapped in SchemaHighlightProvider for custom templates */}
- {/* Static Templates */} {!isCustom && (
{staticTemplates.map((template: any, index: number) => { @@ -251,12 +232,8 @@ const GroupLayoutPreview = () => {
)} - {/* Custom Templates - with page-level schema editor */} {isCustom && ( - -
- {/* Slides List */} - +
{customTemplate && customTemplate.layouts.map((layout: CustomTemplateLayout, index: number) => { const LayoutComponent = layout.component; return ( @@ -280,7 +257,6 @@ const GroupLayoutPreview = () => { {templateParams}:{layout.layoutId} -
@@ -295,8 +271,6 @@ const GroupLayoutPreview = () => { ); })} - -
)} diff --git a/electron/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx b/electron/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx index b947735b..93a9fc84 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx @@ -1,222 +1,14 @@ "use client"; -import React, { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { Card } from "@/components/ui/card"; -import { ExternalLink, Loader2, Plus } from "lucide-react"; - -import { templates } from "@/app/presentation-templates"; -import type { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils"; -import { TemplateWithData } from "@/app/presentation-templates/utils"; -import { - useCustomTemplateSummaries, - useCustomTemplatePreview, - CustomTemplates, -} from "@/app/hooks/useCustomTemplates"; -import { CompiledLayout } from "@/app/hooks/compileLayout"; -import Header from "../(dashboard)/dashboard/components/Header"; - -// Component for rendering custom template card with lazy-loaded previews -const CustomTemplateCard = ({ template }: { template: CustomTemplates }) => { - const router = useRouter(); - const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(template.id); - - const handleNavigate = () => { - if (template.id.startsWith('custom-')) { - router.push(`/template-preview/${template.id}`); - } else { - router.push(`/template-preview/custom-${template.id}`); - } - } +import React, { Suspense } from "react"; +import { Loader2 } from "lucide-react"; +import GroupLayoutPreview from "./components/TemplatePreviewClient"; +const TemplatePreviewPage = () => { return ( - -
-
-

- {template.name} -

-
- - {totalLayouts} - - -
-
- - - - {/* Layout previews */} -
- {loading ? ( - // Loading placeholders - [...Array(Math.min(4, template.layoutCount))].map((_, index) => ( -
- -
- )) - ) : previewLayouts.length > 0 ? ( - // Actual layout previews - previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => { - const LayoutComponent = layout.component; - return ( -
-
-
- -
-
- ); - }) - ) : ( - // Empty state placeholders - [...Array(Math.min(4, template.layoutCount))].map((_, index) => ( -
- No preview -
- )) - )} -
- - -
- +
}> + + ); }; -const LayoutPreview = () => { - const router = useRouter(); - const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries(); - useEffect(() => { - const existingScript = document.querySelector('script[src*="tailwindcss.com"]'); - if (!existingScript) { - const script = document.createElement("script"); - script.src = "https://cdn.tailwindcss.com"; - script.async = true; - document.head.appendChild(script); - } - }, []); - - const totalStaticLayouts = templates.reduce((acc: number, g: TemplateLayoutsWithSettings) => acc + g.layouts.length, 0); - const totalCustomLayouts = customTemplates.reduce((acc: number, t: CustomTemplates) => acc + t.layoutCount, 0); - - return ( -
-
-
-
-

All Templates

-

- {totalStaticLayouts + totalCustomLayouts} layouts across{" "} - {templates.length + customTemplates.length} templates -

-
- - {/* Inbuilt Templates Section */} -
-

Inbuilt Templates

-
- {templates.map((template: TemplateLayoutsWithSettings) => { - const previewLayouts = template.layouts.slice(0, 4); - - return ( - router.push(`/template-preview/${template.id}`)} - > -
-
-

- {template.name} -

-
- - {template.layouts.length} - - -
-
- -

- {template.description} -

- -
- {previewLayouts.map((layout: TemplateWithData, index: number) => { - const LayoutComponent = layout.component; - return ( -
-
-
- -
-
- ); - })} -
-
- - ); - })} -
-
- - {/* Custom Templates Section */} -
-
- -

- My Custom Templates -

- - Create new template - -
- - {customLoading ? ( -
- - Loading custom templates... -
- ) : customTemplates.length === 0 ? ( - -

No custom templates yet.

-

- Custom templates you create will appear here. -

-
- ) : ( -
- {customTemplates.map((template: CustomTemplates) => ( - - ))} -
- )} -
-
-
- ); -}; - -export default LayoutPreview; +export default TemplatePreviewPage; diff --git a/electron/servers/nextjs/components/Header.tsx b/electron/servers/nextjs/components/Header.tsx index 947e51ab..7be5054e 100644 --- a/electron/servers/nextjs/components/Header.tsx +++ b/electron/servers/nextjs/components/Header.tsx @@ -6,7 +6,7 @@ import { Layout, Plus } from "lucide-react"; const Header: React.FC = () => { return ( -
+
@@ -18,7 +18,7 @@ const Header: React.FC = () => { Create Template - + Templates