merge: for name and description of templates
This commit is contained in:
commit
353f8e41d1
11 changed files with 317 additions and 236 deletions
Binary file not shown.
|
|
@ -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/template-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,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client'
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Save, X, Code } from "lucide-react";
|
||||
import { ProcessedSlide } from "../../types";
|
||||
import Editor from 'react-simple-code-editor';
|
||||
|
|
@ -10,6 +9,7 @@ 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";
|
||||
|
||||
interface HtmlEditorProps {
|
||||
slide: ProcessedSlide;
|
||||
|
|
@ -42,58 +42,56 @@ export const HtmlEditor: React.FC<HtmlEditorProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Card className="border-2 border-purple-200 bg-purple-50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="w-5 h-5 text-purple-600" />
|
||||
<span className="text-purple-800">HTML Editor</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCancel}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-1 bg-purple-600 hover:bg-purple-700"
|
||||
>
|
||||
<Save size={14} />
|
||||
Save HTML
|
||||
</Button>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 ">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: htmlContent,
|
||||
}}
|
||||
/>
|
||||
<p className="text-base text-gray-800">Edit the HTML code to customize the slide layout.</p>
|
||||
{/* Render code editor */}
|
||||
<div className="container__content_area">
|
||||
<Sheet open={isHtmlEditMode} 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>
|
||||
|
||||
<Editor
|
||||
value={htmlContent}
|
||||
onValueChange={htmlContent => setHtmlContent(htmlContent)}
|
||||
highlight={htmlContent => highlight(htmlContent, languages.jsx!,'jsx')}
|
||||
padding={10}
|
||||
id="html-editor"
|
||||
name="html-editor"
|
||||
|
||||
className="container__editor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-4 px-2 overflow-y-auto h-[85%]">
|
||||
<div className="container__content_area">
|
||||
<Editor
|
||||
value={htmlContent}
|
||||
onValueChange={html => setHtmlContent(html)}
|
||||
highlight={code => highlight(code, languages.jsx!, 'jsx')}
|
||||
padding={10}
|
||||
id="html-editor"
|
||||
name="html-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"
|
||||
>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-1 bg-purple-600 hover:bg-purple-700"
|
||||
size="sm"
|
||||
>
|
||||
<Save size={14} />
|
||||
Save HTML
|
||||
</Button>
|
||||
</div>
|
||||
</SheetTitle>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,22 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
|
|||
});
|
||||
}
|
||||
}, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup]);
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
}, []);
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -175,7 +175,11 @@ const SidePanel = ({
|
|||
? "bg-[#5141e5] hover:bg-[#4638c7]"
|
||||
: "bg-white hover:bg-white"
|
||||
}`}
|
||||
onClick={() => setActive("grid")}
|
||||
onClick={() => {
|
||||
if (!isStreaming) {
|
||||
setActive("grid")
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LayoutList
|
||||
className={`${active === "grid" ? "text-white" : "text-black"
|
||||
|
|
@ -190,7 +194,11 @@ const SidePanel = ({
|
|||
? "bg-[#5141e5] hover:bg-[#4638c7]"
|
||||
: "bg-white hover:bg-white"
|
||||
}`}
|
||||
onClick={() => setActive("list")}
|
||||
onClick={() =>{
|
||||
if(!isStreaming){
|
||||
setActive("list")
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListTree
|
||||
className={`${active === "list" ? "text-white" : "text-black"
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
|
|||
}, [renderSlideContent, slide, isStreaming]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isStreaming || loading) {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
if (slide.layout_group.includes("custom")) {
|
||||
|
|
|
|||
|
|
@ -397,3 +397,4 @@ import { useDrawingCanvas } from "../../custom-template/hooks/useDrawingCanvas";
|
|||
};
|
||||
|
||||
export default GroupLayoutPreview;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,85 @@
|
|||
"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 [templateMeta, setTemplateMeta] = React.useState<{ name?: string; description?: string } | null>(null);
|
||||
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(
|
||||
|
|
@ -30,20 +93,15 @@ const GroupLayoutPreview = () => {
|
|||
}
|
||||
}, [slug]);
|
||||
|
||||
// Ensure fonts are injected if layoutsMap changes dynamically
|
||||
useEffect(() => {
|
||||
// Load template meta for custom groups
|
||||
if (slug.startsWith("custom-")) {
|
||||
const presentationId = slug.replace("custom-", "");
|
||||
fetch(`/api/v1/ppt/template-management/get-templates/${presentationId}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data?.template) {
|
||||
setTemplateMeta({ name: data.template.name, description: data.template.description });
|
||||
}
|
||||
})
|
||||
.catch(() => setTemplateMeta(null));
|
||||
}
|
||||
}, [slug]);
|
||||
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) {
|
||||
|
|
@ -65,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 */}
|
||||
|
|
@ -139,10 +254,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>
|
||||
|
|
@ -167,6 +294,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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,157 +0,0 @@
|
|||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface UseLayoutLoaderReturn {
|
||||
layoutGroups: LayoutGroup[]
|
||||
layouts: LayoutInfo[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
retry: () => void
|
||||
}
|
||||
|
||||
export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
||||
const [layoutGroups, setLayoutGroups] = useState<LayoutGroup[]>([])
|
||||
const [layouts, setLayouts] = useState<LayoutInfo[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const loadAllLayouts = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const response = await fetch('/api/templates')
|
||||
if (!response.ok) {
|
||||
toast.error('Error loading templates', {
|
||||
description: response.statusText,
|
||||
})
|
||||
return
|
||||
}
|
||||
const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json()
|
||||
const loadedGroups: LayoutGroup[] = []
|
||||
const allLayouts: LayoutInfo[] = []
|
||||
|
||||
for (const groupData of groupedLayoutsData) {
|
||||
const groupLayouts: LayoutInfo[] = []
|
||||
|
||||
const groupSettings: GroupSetting = groupData.settings ? groupData.settings : {
|
||||
description: `${groupData.groupName} presentation templates`,
|
||||
ordered: false,
|
||||
default: false
|
||||
}
|
||||
|
||||
for (const fileName of groupData.files) {
|
||||
try {
|
||||
const layoutName = fileName.replace('.tsx', '').replace('.ts', '')
|
||||
const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`)
|
||||
|
||||
if (!module.default) {
|
||||
toast.error(`${layoutName} has no default export`, {
|
||||
description: 'Please ensure the template file exports a default component',
|
||||
})
|
||||
console.warn(`${layoutName} has no default export`)
|
||||
throw new Error(`${layoutName} has no default export`)
|
||||
}
|
||||
|
||||
if (!module.Schema) {
|
||||
toast.error(`${layoutName} is missing required Schema export`, {
|
||||
description: 'Please ensure the template file exports a Schema',
|
||||
})
|
||||
console.error(`${layoutName} is missing required Schema export`)
|
||||
throw new Error(`${layoutName} is missing required Schema export`)
|
||||
}
|
||||
|
||||
const sampleData = module.Schema.parse({})
|
||||
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
|
||||
|
||||
const layoutInfo: LayoutInfo = {
|
||||
name: layoutName,
|
||||
component: module.default,
|
||||
schema: module.Schema,
|
||||
sampleData,
|
||||
fileName,
|
||||
groupName: groupData.groupName,
|
||||
layoutId
|
||||
}
|
||||
|
||||
groupLayouts.push(layoutInfo)
|
||||
allLayouts.push(layoutInfo)
|
||||
|
||||
} catch (importError) {
|
||||
console.error(`Failed to import ${fileName} from ${groupData.groupName}:`, importError)
|
||||
|
||||
try {
|
||||
const layoutName = fileName.replace('.tsx', '').replace('.ts', '')
|
||||
const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`)
|
||||
|
||||
if (module.default && module.Schema) {
|
||||
const sampleData = module.Schema.parse({})
|
||||
const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '')
|
||||
|
||||
const layoutInfo: LayoutInfo = {
|
||||
name: layoutName,
|
||||
component: module.default,
|
||||
schema: module.Schema,
|
||||
sampleData,
|
||||
fileName,
|
||||
groupName: groupData.groupName,
|
||||
layoutId
|
||||
}
|
||||
groupLayouts.push(layoutInfo)
|
||||
allLayouts.push(layoutInfo)
|
||||
} else {
|
||||
console.error(`${layoutName} is missing required exports (default component or Schema)`)
|
||||
}
|
||||
} catch (altError) {
|
||||
console.error(`Alternative import also failed for ${fileName} from ${groupData.groupName}:`, altError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groupLayouts.length > 0) {
|
||||
loadedGroups.push({
|
||||
groupName: groupData.groupName,
|
||||
layouts: groupLayouts,
|
||||
settings: groupSettings
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (allLayouts.length === 0) {
|
||||
toast.error('No valid templates found', {
|
||||
description: 'Make sure your template files export both a default component and a Schema.',
|
||||
})
|
||||
setError('No valid templates found. Make sure your template files export both a default component and a Schema.')
|
||||
} else {
|
||||
setLayoutGroups(loadedGroups)
|
||||
setLayouts(allLayouts)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading templates:', error)
|
||||
setError(error instanceof Error ? error.message : 'Failed to load templates')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const retry = () => {
|
||||
loadAllLayouts()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadAllLayouts()
|
||||
}, [])
|
||||
|
||||
return {
|
||||
layoutGroups,
|
||||
layouts,
|
||||
loading,
|
||||
error,
|
||||
retry
|
||||
}
|
||||
}
|
||||
|
|
@ -443,7 +443,7 @@ thead {
|
|||
|
||||
.container__content_area {
|
||||
tab-size: 4ch;
|
||||
max-height: 400px;
|
||||
/* max-height: 600px; */
|
||||
overflow: auto;
|
||||
margin: 1.67em 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue