Feat(Nextjs): In Template Preview edit & Fonts loading in presentation

This commit is contained in:
shiva raj badu 2025-08-10 00:00:30 +05:45
parent e14c02ae31
commit 0eeeb435f1
No known key found for this signature in database
4 changed files with 242 additions and 8 deletions

View file

@ -69,6 +69,7 @@ export interface LayoutContextType {
isPreloading: boolean;
cacheSize: number;
refetch: () => Promise<void>;
getCustomTemplateFonts: (presentationId: string) => string[] | null;
}
const LayoutContext = createContext<LayoutContextType | undefined>(undefined);
@ -129,6 +130,7 @@ export const LayoutProvider: React.FC<{
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isPreloading, setIsPreloading] = useState(false);
const [customTemplateFonts, setCustomTemplateFonts] = useState<Map<string, string[]>>(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<string, LayoutInfo>();
const layoutsByGroup = new Map<string, Set<string>>();
const groupSettingsMap = new Map<string, GroupSetting>();
@ -348,9 +349,9 @@ export const LayoutProvider: React.FC<{
"/api/v1/ppt/layout-management/summary"
);
const customGroupData = await customGroupResponse.json();
const customFonts = new Map<string, string[]>();
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,

View file

@ -13,8 +13,29 @@ const GroupLayouts: React.FC<GroupLayoutsProps> = ({
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 (
<div
onClick={() => onSelectLayoutGroup(group)}

View file

@ -99,6 +99,7 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
}
}, []);
if (loading) {
return (

View file

@ -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<string[] | undefined>(undefined);
const [isSaving, setIsSaving] = useState(false);
const [layoutsMap, setLayoutsMap] = useState<Record<string, { layout_id: string; layout_name: string; layout_code: string; fonts?: string[] }>>({});
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<string, { layout_id: string; layout_name: string; layout_code: string; fonts?: string[] }> = {};
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 <LoadingStates type="loading" />;
@ -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 (
<div className="min-h-screen bg-gray-50">
{/* Header */}
@ -122,10 +255,22 @@ const GroupLayoutPreview = () => {
</span>
</div>
</div>
<div className="text-right">
<div className="flex items-center gap-2">
<div className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-700">
Layout #{index + 1}
</div>
{isCustom && (
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 bg-blue-50 border border-blue-400 text-blue-700"
onClick={() => openEditor(fileName)}
disabled={!layoutsMap[fileName]}
title={!layoutsMap[fileName] ? "Loading layout code..." : "Edit layout code"}
>
<Pencil className="w-4 h-4" /> Edit
</Button>
)}
</div>
</div>
</div>
@ -150,6 +295,61 @@ const GroupLayoutPreview = () => {
</div>
</div>
</footer>
{/* Right-side Sheet Editor */}
{isCustom && (
<Sheet open={editorOpen} onOpenChange={(open) => { if (!open) handleCancel(); }}>
<SheetContent side="right" className="w-full sm:max-w-[860px] p-0">
<SheetHeader className="px-6 py-4 border-b">
<SheetTitle className="flex items-center justify-between w-full">
<span className="flex items-center gap-2 text-purple-800">
<Code className="w-5 h-5 text-purple-600" />
HTML Editor
</span>
</SheetTitle>
</SheetHeader>
<div className="space-y-4 px-2 overflow-y-auto h-[85%]">
<div className="container__content_area">
<Editor
value={currentCode}
onValueChange={(code) => setCurrentCode(code)}
highlight={(code) => highlight(code, languages.jsx!, "jsx")}
padding={10}
id="layout-code-editor"
name="layout-code-editor"
className="container__editor"
/>
</div>
</div>
<SheetFooter className="px-6 py-4 border-b">
<SheetTitle className="flex items-center justify-between w-full">
<div></div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleCancel}
className="flex items-center gap-1"
disabled={isSaving}
>
<X size={14} />
Cancel
</Button>
<Button
onClick={handleSave}
className="flex items-center gap-1 bg-purple-600 hover:bg-purple-700"
size="sm"
disabled={isSaving}
>
<Save size={14} />
Save HTML
</Button>
</div>
</SheetTitle>
</SheetFooter>
</SheetContent>
</Sheet>
)}
</div>
);
};