diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx index d1612d74..71fa5f87 100644 --- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx +++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx @@ -69,6 +69,7 @@ export interface LayoutContextType { isPreloading: boolean; cacheSize: number; refetch: () => Promise; + getCustomTemplateFonts: (presentationId: string) => string[] | null; } const LayoutContext = createContext(undefined); @@ -129,6 +130,7 @@ export const LayoutProvider: React.FC<{ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isPreloading, setIsPreloading] = useState(false); + const [customTemplateFonts, setCustomTemplateFonts] = useState>(new Map()); const dispatch = useDispatch(); const buildData = async (groupedLayoutsData: GroupedLayoutsResponse[]) => { @@ -336,7 +338,6 @@ export const LayoutProvider: React.FC<{ const LoadCustomLayouts = async () => { const layouts: LayoutInfo[] = []; - const layoutsById = new Map(); const layoutsByGroup = new Map>(); const groupSettingsMap = new Map(); @@ -348,9 +349,9 @@ export const LayoutProvider: React.FC<{ "/api/v1/ppt/layout-management/summary" ); const customGroupData = await customGroupResponse.json(); - + + const customFonts = new Map(); const customGroup = customGroupData.presentations; - for (const group of customGroup) { const groupName = `custom-${group.presentation_id}`; fullDataByGroup.set(groupName, []); @@ -363,6 +364,9 @@ export const LayoutProvider: React.FC<{ ); const customLayoutsData = await customLayoutResponse.json(); const allLayout = customLayoutsData.layouts; + + + const settings = { description: `Custom presentation layouts`, @@ -402,6 +406,8 @@ export const LayoutProvider: React.FC<{ layoutCache.set(cacheKey, module.default); } + customFonts.set(presentationId, i.fonts); + const originalLayoutId = module.layoutId || i.layout_name.toLowerCase().replace(/layout$/, ""); @@ -447,6 +453,7 @@ export const LayoutProvider: React.FC<{ groupLayouts.push(layout); layouts.push(layout); } + setCustomTemplateFonts(customFonts); // Cache grouped layouts groupedLayouts.set(groupName, groupLayouts); fullDataByGroup.set(groupName, groupFullData); @@ -454,6 +461,7 @@ export const LayoutProvider: React.FC<{ } catch (err: any) { console.error("Compilation error:", err); } + return { layoutsById, @@ -548,6 +556,9 @@ export const LayoutProvider: React.FC<{ const getFullDataByGroup = (groupName: string): FullDataInfo[] => { return layoutData?.fullDataByGroup.get(groupName) || []; }; + const getCustomTemplateFonts = (presentationId: string): string[] | null => { + return customTemplateFonts.get(presentationId) || null; + }; // Load layouts on mount useEffect(() => { @@ -562,6 +573,7 @@ export const LayoutProvider: React.FC<{ getAllGroups, getAllLayouts, getFullDataByGroup, + getCustomTemplateFonts, loading, error, getLayout, diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx index 860304bf..eadcaf93 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx @@ -13,8 +13,29 @@ const GroupLayouts: React.FC = ({ onSelectLayoutGroup, selectedLayoutGroup, }) => { - const { getFullDataByGroup } = useLayout(); + const { getFullDataByGroup,getCustomTemplateFonts } = useLayout(); const layoutGroup = getFullDataByGroup(group.id); + const fonts = getCustomTemplateFonts(group.id.split("custom-")[1]); + console.log("fonts here", fonts); +if(fonts){ + const injectFonts = (fontUrls: string[]) => { + fontUrls.forEach((fontUrl) => { + if (!fontUrl) return; + const existingStyle = document.querySelector(`style[data-font-url="${fontUrl}"]`); + if (existingStyle) return; + const fileName = fontUrl.split("/").pop() || "CustomFont"; + const baseName = fileName.replace(/\.[a-zA-Z0-9]+$/, ""); + const fontFamily = baseName.replace(/[^A-Za-z0-9_-]/g, "_"); + const ext = (fileName.split(".").pop() || "ttf").toLowerCase(); + const format = ext === "otf" ? "opentype" : ext === "woff" ? "woff" : ext === "woff2" ? "woff2" : "truetype"; + const style = document.createElement("style"); + style.setAttribute("data-font-url", fontUrl); + style.textContent = `@font-face { font-family: '${fontFamily}'; src: url('${fontUrl}') format('${format}'); font-display: swap; }`; + document.head.appendChild(style); + }); + }; + injectFonts(fonts); +} return (
onSelectLayoutGroup(group)} diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx index f80674ae..c10b5422 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx @@ -99,6 +99,7 @@ const LayoutSelection: React.FC = ({ } }, []); + if (loading) { return ( diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx index 6b634900..1b9f5d20 100644 --- a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx @@ -1,20 +1,86 @@ "use client"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useParams, useRouter } from "next/navigation"; // import { useGroupLayoutLoader } from '../hooks/useGroupLayoutLoader' import LoadingStates from "../components/LoadingStates"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, Home, Trash2 } from "lucide-react"; +import { ArrowLeft, Home, Trash2, Code, Save, X, Pencil } from "lucide-react"; import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext"; +import Editor from "react-simple-code-editor"; +import { highlight, languages } from "prismjs"; +import "prismjs/components/prism-clike"; +import "prismjs/components/prism-javascript"; +import "prismjs/components/prism-markup"; +import "prismjs/components/prism-jsx"; +import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet"; + const GroupLayoutPreview = () => { const params = useParams(); const router = useRouter(); const slug = params.slug as string; - const { getFullDataByGroup, loading,refetch } = useLayout(); + const { getFullDataByGroup, loading, refetch } = useLayout(); const layoutGroup = getFullDataByGroup(slug); + const presentationId = slug.replace("custom-", ""); + const isCustom = slug.includes("custom-"); + + const [editorOpen, setEditorOpen] = useState(false); + const [currentCode, setCurrentCode] = useState(""); + const [currentLayoutName, setCurrentLayoutName] = useState(""); + const [currentLayoutId, setCurrentLayoutId] = useState(""); + const [currentFonts, setCurrentFonts] = useState(undefined); + const [isSaving, setIsSaving] = useState(false); + const [layoutsMap, setLayoutsMap] = useState>({}); + + const injectFonts = (fontUrls: string[]) => { + fontUrls.forEach((fontUrl) => { + if (!fontUrl) return; + const existingStyle = document.querySelector(`style[data-font-url="${fontUrl}"]`); + if (existingStyle) return; + const fileName = fontUrl.split("/").pop() || "CustomFont"; + const baseName = fileName.replace(/\.[a-zA-Z0-9]+$/, ""); + const fontFamily = baseName.replace(/[^A-Za-z0-9_-]/g, "_"); + const ext = (fileName.split(".").pop() || "ttf").toLowerCase(); + const format = ext === "otf" ? "opentype" : ext === "woff" ? "woff" : ext === "woff2" ? "woff2" : "truetype"; + const style = document.createElement("style"); + style.setAttribute("data-font-url", fontUrl); + style.textContent = `@font-face { font-family: '${fontFamily}'; src: url('${fontUrl}') format('${format}'); font-display: swap; }`; + document.head.appendChild(style); + }); + }; + + useEffect(() => { + const loadCustomLayouts = async () => { + if (!isCustom) return; + try { + const res = await fetch(`/api/v1/ppt/layout-management/get-layouts/${presentationId}`); + if (!res.ok) return; + const data = await res.json(); + const map: Record = {}; + for (const l of data.layouts || []) { + map[l.layout_name] = { + layout_id: l.layout_id, + layout_name: l.layout_name, + layout_code: l.layout_code, + fonts: l.fonts, + }; + } + setLayoutsMap(map); + // Inject all fonts used by this custom group's layouts + // const allFonts: string[] = []; + // Object.values(map).forEach((entry) => { + // (entry.fonts || []).forEach((f) => allFonts.push(f)); + // }); + injectFonts(map[0].fonts || []); + } catch (e) { + // noop + } + }; + loadCustomLayouts(); + }, [isCustom, presentationId]); + useEffect(() => { const existingScript = document.querySelector( 'script[src*="tailwindcss.com"]' @@ -27,6 +93,16 @@ const GroupLayoutPreview = () => { } }, [slug]); + // Ensure fonts are injected if layoutsMap changes dynamically + useEffect(() => { + if (!isCustom) return; + const allFonts: string[] = []; + Object.values(layoutsMap).forEach((entry) => { + (entry.fonts || []).forEach((f) => allFonts.push(f)); + }); + if (allFonts.length) injectFonts(allFonts); + }, [layoutsMap, isCustom]); + // Handle loading state if (loading) { return ; @@ -47,6 +123,63 @@ const GroupLayoutPreview = () => { router.push("/layout-preview"); } } + + const openEditor = (layoutName: string) => { + const entry = layoutsMap[layoutName]; + if (!entry) return; + setCurrentLayoutName(entry.layout_name); + setCurrentLayoutId(entry.layout_id); + setCurrentCode(entry.layout_code || ""); + setCurrentFonts(entry.fonts); + // Make sure fonts for this layout are loaded before editing + injectFonts(entry.fonts || []); + setEditorOpen(true); + }; + + const handleCancel = () => { + // reset to original code + const entry = layoutsMap[currentLayoutName]; + if (entry) setCurrentCode(entry.layout_code || ""); + setEditorOpen(false); + }; + + const handleSave = async () => { + try { + setIsSaving(true); + const payload = { + layouts: [ + { + presentation_id: presentationId, + layout_id: currentLayoutId, + layout_name: currentLayoutName, + layout_code: currentCode, + fonts: currentFonts, + }, + ], + }; + const res = await fetch(`/api/v1/ppt/layout-management/save-layouts`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!res.ok) return; + // update cache map + setLayoutsMap((prev) => ({ + ...prev, + [currentLayoutName]: { + layout_id: currentLayoutId, + layout_name: currentLayoutName, + layout_code: currentCode, + fonts: currentFonts, + }, + })); + await refetch(); + setEditorOpen(false); + } finally { + setIsSaving(false); + } + }; + return (
{/* Header */} @@ -122,10 +255,22 @@ const GroupLayoutPreview = () => {
-
+
Layout #{index + 1}
+ {isCustom && ( + + )}
@@ -150,6 +295,61 @@ const GroupLayoutPreview = () => { + + {/* Right-side Sheet Editor */} + {isCustom && ( + { if (!open) handleCancel(); }}> + + + + + + HTML Editor + + + +
+
+ setCurrentCode(code)} + highlight={(code) => highlight(code, languages.jsx!, "jsx")} + padding={10} + id="layout-code-editor" + name="layout-code-editor" + className="container__editor" + /> +
+
+ + +
+
+ + +
+
+
+
+
+ )} ); };