refactor(nextjs): use template keyword instead of group/group layouts

This commit is contained in:
shiva raj badu 2025-09-22 17:20:55 +05:45
parent df8167e5bb
commit c64cc86684
No known key found for this signature in database
18 changed files with 713 additions and 662 deletions

View file

@ -8,13 +8,13 @@ import { Trash2 } from 'lucide-react';
import { toast } from 'sonner';
interface NewSlideProps {
setShowNewSlideSelection: (show: boolean) => void;
group: string;
templateID: string;
index: number;
presentationId: string;
}
const NewSlide = ({
setShowNewSlideSelection,
group,
templateID,
index,
presentationId,
}: NewSlideProps) => {
@ -25,7 +25,7 @@ const NewSlide = ({
id: uuidv4(),
index: index,
content: sampleData,
layout_group: group,
layout_group: templateID,
layout: id,
presentation: presentationId,
};
@ -36,10 +36,8 @@ const NewSlide = ({
toast.error("Error adding new slide");
}
};
const { getFullDataByGroup, loading } = useLayout();
const fullData = getFullDataByGroup(group);
const { getFullDataByTemplateID, loading } = useLayout();
const fullData = getFullDataByTemplateID(templateID);
if (loading) {
return (
@ -73,7 +71,7 @@ const NewSlide = ({
return (
<div
onClick={() => handleNewSlide(sampleData, layoutId)}
key={`${group}-${index}`}
key={`${layoutId}-${index}`}
className=" relative cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />

View file

@ -9,7 +9,7 @@ import {
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Slide } from "../types/slide";
import { useGroupLayouts } from "../hooks/useGroupLayouts";
import { useTemplateLayouts } from "../hooks/useTemplateLayouts";
interface PresentationModeProps {
@ -33,7 +33,7 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
onSlideChange,
}) => {
const { renderSlideContent } = useGroupLayouts();
const { renderSlideContent } = useTemplateLayouts();
// Modify the handleKeyPress to prevent default behavior
const handleKeyPress = useCallback(
(event: KeyboardEvent) => {

View file

@ -11,14 +11,19 @@ import { toast } from "sonner";
import * as z from "zod";
import { useDispatch } from "react-redux";
import { setLayoutLoading } from "@/store/slices/presentationGeneration";
import * as Babel from "@babel/standalone";
import * as Recharts from "recharts";
import * as d3 from 'd3';
import { getHeader } from "../services/api/header";
export interface LayoutInfo {
id: string;
name?: string;
description?: string;
json_schema: any;
groupName: string;
templateID: string;
templateName?: string;
}
export interface FullDataInfo {
name: string;
@ -26,43 +31,41 @@ export interface FullDataInfo {
schema: any;
sampleData: any;
fileName: string;
groupName: string;
templateID: string;
layoutId: string;
}
export interface GroupSetting {
export interface TemplateSetting {
description: string;
ordered: boolean;
default?: boolean;
}
export interface GroupedLayoutsResponse {
groupName: string;
export interface TemplateResponse {
templateID: string;
templateName?: string;
files: string[];
settings: GroupSetting | null;
settings: TemplateSetting | null;
}
export interface LayoutData {
layoutsById: Map<string, LayoutInfo>;
layoutsByGroup: Map<string, Set<string>>;
groupSettings: Map<string, GroupSetting>;
fileMap: Map<string, { fileName: string; groupName: string }>;
groupedLayouts: Map<string, LayoutInfo[]>;
layoutsByTemplateID: Map<string, Set<string>>;
templateSettings: Map<string, TemplateSetting>;
fileMap: Map<string, { fileName: string; templateID: string }>;
templateLayouts: Map<string, LayoutInfo[]>;
layoutSchema: LayoutInfo[];
fullDataByGroup: Map<string, FullDataInfo[]>;
fullDataByTemplateID: Map<string, FullDataInfo[]>;
}
export interface LayoutContextType {
getLayoutById: (layoutId: string) => LayoutInfo | null;
getLayoutByIdAndGroup: (
layoutId: string,
groupName: string
) => LayoutInfo | null;
getLayoutsByGroup: (groupName: string) => LayoutInfo[];
getGroupSetting: (groupName: string) => GroupSetting | null;
getAllGroups: () => string[];
getLayoutsByTemplateID: (templateID: string) => LayoutInfo[];
getTemplateSetting: (templateID: string) => TemplateSetting | null;
getAllTemplateIDs: () => string[];
getAllLayouts: () => LayoutInfo[];
getFullDataByGroup: (groupName: string) => FullDataInfo[];
getFullDataByTemplateID: (templateID: string) => FullDataInfo[];
loading: boolean;
error: string | null;
getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null;
@ -76,8 +79,8 @@ const LayoutContext = createContext<LayoutContextType | undefined>(undefined);
const layoutCache = new Map<string, React.ComponentType<{ data: any }>>();
const createCacheKey = (groupName: string, fileName: string): string =>
`${groupName}/${fileName}`;
const createCacheKey = (templateID: string, fileName: string): string =>
`${templateID}/${fileName}`;
// Extract Babel compilation logic into a utility function
const compileCustomLayout = (layoutCode: string, React: any, z: any) => {
@ -102,11 +105,15 @@ const compileCustomLayout = (layoutCode: string, React: any, z: any) => {
"React",
"_z",
"Recharts",
`
const z = _z;
const useRef= React.useRef;
const useEffect= React.useEffect;
// Expose commonly used Recharts components to compiled layouts
const { ResponsiveContainer, LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, PieChart, Pie, Cell, AreaChart, Area, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ComposedChart, ScatterChart, Scatter, FunnelChart, Funnel, TreemapChart, Treemap, SankeyChart, Sankey, RadialBarChart, RadialBar, ReferenceLine, ReferenceDot, ReferenceArea, Brush, ErrorBar, LabelList, Label } = Recharts || {};
const { ResponsiveContainer, LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, PieChart, Pie, Cell, AreaChart, Area, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ComposedChart, ScatterChart, Scatter, FunnelChart, Funnel, TreemapChart, Treemap, SankeyChart, Sankey, RadialBarChart, RadialBar, ReferenceLine, ReferenceDot, ReferenceArea, Brush, ErrorBar, LabelList, Label } = Recharts || {};
${compiled}
/* everything declared in the string is in scope here */
@ -134,45 +141,46 @@ export const LayoutProvider: React.FC<{
const [customTemplateFonts, setCustomTemplateFonts] = useState<Map<string, string[]>>(new Map());
const dispatch = useDispatch();
const buildData = async (groupedLayoutsData: GroupedLayoutsResponse[]) => {
const buildData = async (templateData: TemplateResponse[]) => {
const layouts: LayoutInfo[] = [];
const layoutsById = new Map<string, LayoutInfo>();
const layoutsByGroup = new Map<string, Set<string>>();
const groupSettingsMap = new Map<string, GroupSetting>();
const fileMap = new Map<string, { fileName: string; groupName: string }>();
const groupedLayouts = new Map<string, LayoutInfo[]>();
const fullDataByGroup = new Map<string, FullDataInfo[]>();
const layoutsByTemplateID = new Map<string, Set<string>>();
const templateSettingsMap = new Map<string, TemplateSetting>();
const fileMap = new Map<string, { fileName: string; templateID: string }>();
const templateLayoutsCache = new Map<string, LayoutInfo[]>();
const fullDataByTemplateID = new Map<string, FullDataInfo[]>();
// Start preloading process
setIsPreloading(true);
try {
for (const groupData of groupedLayoutsData) {
// Initialize group
if (!layoutsByGroup.has(groupData.groupName)) {
layoutsByGroup.set(groupData.groupName, new Set());
for (const template of templateData) {
// Initialize template
if (!layoutsByTemplateID.has(template.templateID)) {
layoutsByTemplateID.set(template.templateID, new Set());
}
fullDataByGroup.set(groupData.groupName, []);
fullDataByTemplateID.set(template.templateID, []);
// group settings or default settings
const settings = groupData.settings || {
description: `${groupData.groupName} presentation layouts`,
// template settings or default settings
const settings = template.settings || {
templateName: template.templateName,
description: `${template.templateID} presentation layouts`,
ordered: false,
default: false,
};
groupSettingsMap.set(groupData.groupName, settings);
const groupLayouts: LayoutInfo[] = [];
const groupFullData: FullDataInfo[] = [];
templateSettingsMap.set(template.templateID, settings);
const templateLayouts: LayoutInfo[] = [];
const templateFullData: FullDataInfo[] = [];
for (const fileName of groupData.files) {
for (const fileName of template.files) {
try {
const file = fileName.replace(".tsx", "").replace(".ts", "");
const module = await import(
`@/presentation-templates/${groupData.groupName}/${file}`
`@/presentation-templates/${template.templateID}/${file}`
);
if (!module.default) {
@ -193,14 +201,14 @@ export const LayoutProvider: React.FC<{
}
// Cache the layout component immediately after import
const cacheKey = createCacheKey(groupData.groupName, fileName);
const cacheKey = createCacheKey(template.templateID, fileName);
if (!layoutCache.has(cacheKey)) {
layoutCache.set(cacheKey, module.default);
}
const originalLayoutId =
module.layoutId || file.toLowerCase().replace(/layout$/, "");
const uniqueKey = `${groupData.groupName}:${originalLayoutId}`;
const uniqueKey = `${template.templateID}:${originalLayoutId}`;
const layoutName =
module.layoutName || file.replace(/([A-Z])/g, " $1").trim();
const layoutDescription =
@ -218,7 +226,8 @@ export const LayoutProvider: React.FC<{
name: layoutName,
description: layoutDescription,
json_schema: jsonSchema,
groupName: groupData.groupName,
templateID: template.templateID,
templateName: template.templateName,
};
const sampleData = module.Schema.parse({});
@ -228,30 +237,30 @@ export const LayoutProvider: React.FC<{
schema: jsonSchema,
sampleData: sampleData,
fileName,
groupName: groupData.groupName,
templateID: template.templateID,
layoutId: uniqueKey,
};
groupFullData.push(fullData);
templateFullData.push(fullData);
layoutsById.set(uniqueKey, layout);
layoutsByGroup.get(groupData.groupName)!.add(uniqueKey);
layoutsByTemplateID.get(template.templateID)!.add(uniqueKey);
fileMap.set(uniqueKey, {
fileName,
groupName: groupData.groupName,
templateID: template.templateID,
});
groupLayouts.push(layout);
templateLayouts.push(layout);
layouts.push(layout);
} catch (error) {
console.error(
`💥 Error extracting schema for ${fileName} from ${groupData.groupName}:`,
`💥 Error extracting schema for ${fileName} from ${template.templateID}:`,
error
);
}
}
fullDataByGroup.set(groupData.groupName, groupFullData);
// Cache grouped layouts
groupedLayouts.set(groupData.groupName, groupLayouts);
fullDataByTemplateID.set(template.templateID, templateFullData);
// Cache template layouts
templateLayoutsCache.set(template.templateID, templateLayouts);
}
} catch (err: any) {
console.error("Compilation error:", err);
@ -259,12 +268,12 @@ export const LayoutProvider: React.FC<{
return {
layoutsById,
layoutsByGroup,
groupSettings: groupSettingsMap,
layoutsByTemplateID,
templateSettings: templateSettingsMap,
fileMap,
groupedLayouts,
templateLayoutsCache,
layoutSchema: layouts,
fullDataByGroup,
fullDataByTemplateID,
};
};
@ -274,44 +283,44 @@ export const LayoutProvider: React.FC<{
setError(null);
dispatch(setLayoutLoading(true));
const layoutResponse = await fetch("/api/templates");
const templateResponse = await fetch("/api/templates");
if (!layoutResponse.ok) {
if (!templateResponse.ok) {
throw new Error(
`Failed to fetch layouts: ${layoutResponse.statusText}`
`Failed to fetch layouts: ${templateResponse.statusText}`
);
}
const groupedLayoutsData: GroupedLayoutsResponse[] =
await layoutResponse.json();
const templateData: TemplateResponse[] =
await templateResponse.json();
if (!groupedLayoutsData || groupedLayoutsData.length === 0) {
setError("No layout groups found");
if (!templateData || templateData.length === 0) {
setError("No template found");
return;
}
const data = await buildData(groupedLayoutsData);
const data = await buildData(templateData);
const customLayouts = await LoadCustomLayouts();
setIsPreloading(false);
const combinedData = {
layoutsById: mergeMaps(data.layoutsById, customLayouts.layoutsById),
layoutsByGroup: mergeMaps(
data.layoutsByGroup,
customLayouts.layoutsByGroup
layoutsByTemplateID: mergeMaps(
data.layoutsByTemplateID,
customLayouts.layoutsByTemplateID
),
groupSettings: mergeMaps(
data.groupSettings,
customLayouts.groupSettings
templateSettings: mergeMaps(
data.templateSettings,
customLayouts.templateSettings
),
fileMap: mergeMaps(data.fileMap, customLayouts.fileMap),
groupedLayouts: mergeMaps(
data.groupedLayouts,
customLayouts.groupedLayouts
templateLayouts: mergeMaps(
data.templateLayoutsCache,
customLayouts.templateLayoutsCache
),
layoutSchema: [...data.layoutSchema, ...customLayouts.layoutSchema],
fullDataByGroup: mergeMaps(
data.fullDataByGroup,
customLayouts.fullDataByGroup
fullDataByTemplateID: mergeMaps(
data.fullDataByTemplateID,
customLayouts.fullDataByTemplateID
),
};
@ -338,30 +347,52 @@ export const LayoutProvider: React.FC<{
}
const LoadCustomLayouts = async () => {
const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
const layouts: LayoutInfo[] = [];
const layoutsById = new Map<string, LayoutInfo>();
const layoutsByGroup = new Map<string, Set<string>>();
const groupSettingsMap = new Map<string, GroupSetting>();
const fileMap = new Map<string, { fileName: string; groupName: string }>();
const groupedLayouts = new Map<string, LayoutInfo[]>();
const fullDataByGroup = new Map<string, FullDataInfo[]>();
const layoutsByTemplateID = new Map<string, Set<string>>();
const templateSettingsMap = new Map<string, TemplateSetting>();
const fileMap = new Map<string, { fileName: string; templateID: string }>();
const templateLayoutsCache = new Map<string, LayoutInfo[]>();
const fullDataByTemplateID = new Map<string, FullDataInfo[]>();
try {
const customGroupResponse = await fetch(
"/api/v1/ppt/template-management/summary"
const customTemplateResponse = await fetch(
`/api/v1/ppt/template-management/summary`,
{
headers: {
...getHeader(),
...(token ? { Authorization: `Bearer ${token}` } : {}),
}
}
);
const customGroupData = await customGroupResponse.json();
const customTemplateData = await customTemplateResponse.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, []);
if (!layoutsByGroup.has(groupName)) {
layoutsByGroup.set(groupName, new Set());
const customTemplates = customTemplateData.presentations || [];
for (const templateInfo of customTemplates) {
const pid =
(templateInfo && (templateInfo.presentation_id || templateInfo.presentation || templateInfo.id)) ||
"";
if (!pid) {
// skip invalid entries
continue;
}
const presentationId = group.presentation_id;
const templateID = `custom-${pid}`;
const templateName = templateInfo.template?.name || templateID;
fullDataByTemplateID.set(templateID, []);
if (!layoutsByTemplateID.has(templateID)) {
layoutsByTemplateID.set(templateID, new Set());
}
const presentationId = pid;
const customLayoutResponse = await fetch(
`/api/v1/ppt/template-management/get-templates/${presentationId}`
`/api/v1/ppt/template-management/get-templates/${presentationId}`,
{
headers: {
...getHeader(),
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
}
);
const customLayoutsData = await customLayoutResponse.json();
const allLayout = customLayoutsData.layouts;
@ -370,14 +401,15 @@ export const LayoutProvider: React.FC<{
const settings = {
templateName: templateName,
description: `Custom presentation layouts`,
ordered: false,
default: false,
};
groupSettingsMap.set(`custom-${presentationId}`, settings);
const groupLayouts: LayoutInfo[] = [];
const groupFullData: FullDataInfo[] = [];
templateSettingsMap.set(`custom-${presentationId}`, settings);
const templateLayouts: LayoutInfo[] = [];
const templateFullData: FullDataInfo[] = [];
// Helper to create an inline error component for this specific slide
const createErrorComponent = (title: string, message: string): React.ComponentType<{ data: any }> => {
@ -480,7 +512,8 @@ export const LayoutProvider: React.FC<{
name: layoutName,
description: layoutDescription,
json_schema: jsonSchema,
groupName: groupName,
templateID: templateID,
templateName: templateName,
};
fullData = {
@ -489,19 +522,19 @@ export const LayoutProvider: React.FC<{
schema: jsonSchema,
sampleData: sampleData,
fileName: i.layout_name,
groupName: groupName,
templateID: templateID,
layoutId: uniqueKey,
};
groupFullData.push(fullData);
templateFullData.push(fullData);
layoutsById.set(uniqueKey, layout);
layoutsByGroup.get(groupName)!.add(uniqueKey);
layoutsByTemplateID.get(templateID)!.add(uniqueKey);
fileMap.set(uniqueKey, {
fileName: i.layout_name,
groupName: groupName,
templateID: templateID,
});
groupLayouts.push(layout);
templateLayouts.push(layout);
layouts.push(layout);
} catch (e: any) {
// Handle compilation/runtime errors during transformation
@ -517,7 +550,8 @@ export const LayoutProvider: React.FC<{
name: layoutName,
description: `Failed to compile ${i.layout_name}`,
json_schema: {},
groupName: groupName,
templateID: templateID,
templateName: templateName,
};
const fullData: FullDataInfo = {
@ -526,25 +560,25 @@ export const LayoutProvider: React.FC<{
schema: {},
sampleData: {},
fileName: i.layout_name,
groupName: groupName,
templateID: templateID,
layoutId: uniqueKey,
};
groupFullData.push(fullData);
templateFullData.push(fullData);
layoutsById.set(uniqueKey, layout);
layoutsByGroup.get(groupName)!.add(uniqueKey);
layoutsByTemplateID.get(templateID)!.add(uniqueKey);
fileMap.set(uniqueKey, {
fileName: i.layout_name,
groupName: groupName,
templateID: templateID,
});
groupLayouts.push(layout);
templateLayouts.push(layout);
layouts.push(layout);
}
}
setCustomTemplateFonts(customFonts);
// Cache grouped layouts
groupedLayouts.set(groupName, groupLayouts);
fullDataByGroup.set(groupName, groupFullData);
// Cache template layouts
templateLayoutsCache.set(templateID, templateLayouts);
fullDataByTemplateID.set(templateID, templateFullData);
}
} catch (err: any) {
console.error("Compilation error:", err);
@ -553,12 +587,12 @@ export const LayoutProvider: React.FC<{
return {
layoutsById,
layoutsByGroup,
groupSettings: groupSettingsMap,
layoutsByTemplateID,
templateSettings: templateSettingsMap,
fileMap,
groupedLayouts,
templateLayoutsCache,
layoutSchema: layouts,
fullDataByGroup,
fullDataByTemplateID,
};
};
@ -567,7 +601,7 @@ export const LayoutProvider: React.FC<{
): React.ComponentType<{ data: any }> | null => {
if (!layoutData) return null;
let fileInfo: { fileName: string; groupName: string } | undefined;
let fileInfo: { fileName: string; templateID: string } | undefined;
// Search through all fileMap entries to find the layout
for (const [key, info] of Array.from(layoutData.fileMap.entries())) {
@ -582,7 +616,7 @@ export const LayoutProvider: React.FC<{
return null;
}
const cacheKey = createCacheKey(fileInfo.groupName, fileInfo.fileName);
const cacheKey = createCacheKey(fileInfo.templateID, fileInfo.fileName);
// Return cached layout if available
if (layoutCache.has(cacheKey)) {
@ -591,7 +625,7 @@ export const LayoutProvider: React.FC<{
// Create and cache layout if not available
const file = fileInfo.fileName.replace(".tsx", "").replace(".ts", "");
const Layout = dynamic(
() => import(`@/presentation-templates/${fileInfo.groupName}/${file}`),
() => import(`@/presentation-templates/${fileInfo.templateID}/${file}`),
{
loading: () => (
<div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />
@ -604,11 +638,11 @@ export const LayoutProvider: React.FC<{
return Layout;
};
// Updated accessor methods to handle group-specific lookups
// Updated accessor methods to handle templateID-specific lookups
const getLayoutById = (layoutId: string): LayoutInfo | null => {
if (!layoutData) return null;
// Search through all entries to find the layout (since we don't know the group)
// Search through all entries to find the layout (since we don't know the templateID)
for (const [key, layout] of Array.from(layoutData.layoutsById.entries())) {
if (key === layoutId) {
return layout;
@ -617,32 +651,26 @@ export const LayoutProvider: React.FC<{
return null;
};
const getLayoutByIdAndGroup = (
layoutId: string,
groupName: string
): LayoutInfo | null => {
if (!layoutData) return null;
return layoutData.layoutsById.get(layoutId) || null;
const getLayoutsByTemplateID = (templateID: string): LayoutInfo[] => {
return layoutData?.templateLayouts.get(templateID) || [];
};
const getLayoutsByGroup = (groupName: string): LayoutInfo[] => {
return layoutData?.groupedLayouts.get(groupName) || [];
const getTemplateSetting = (templateID: string): TemplateSetting | null => {
return layoutData?.templateSettings.get(templateID) || null;
};
const getGroupSetting = (groupName: string): GroupSetting | null => {
return layoutData?.groupSettings.get(groupName) || null;
};
const getAllGroups = (): string[] => {
return layoutData ? Array.from(layoutData.groupSettings.keys()) : [];
const getAllTemplateIDs = (): string[] => {
return layoutData ? Array.from(layoutData.templateSettings.keys()) : [];
};
const getAllLayouts = (): LayoutInfo[] => {
return layoutData?.layoutSchema || [];
};
const getFullDataByGroup = (groupName: string): FullDataInfo[] => {
return layoutData?.fullDataByGroup.get(groupName) || [];
const getFullDataByTemplateID = (templateID: string): FullDataInfo[] => {
return layoutData?.fullDataByTemplateID.get(templateID) || [];
};
const getCustomTemplateFonts = (presentationId: string): string[] | null => {
return customTemplateFonts.get(presentationId) || null;
@ -655,12 +683,11 @@ export const LayoutProvider: React.FC<{
const contextValue: LayoutContextType = {
getLayoutById,
getLayoutByIdAndGroup,
getLayoutsByGroup,
getGroupSetting,
getAllGroups,
getLayoutsByTemplateID,
getTemplateSetting,
getAllTemplateIDs,
getAllLayouts,
getFullDataByGroup,
getFullDataByTemplateID,
getCustomTemplateFonts,
loading,
error,

View file

@ -10,7 +10,7 @@ import {
} from "@/components/ui/popover";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { useGroupLayouts } from "@/app/(presentation-generator)/hooks/useGroupLayouts";
import { useTemplateLayouts } from "@/app/(presentation-generator)/hooks/useTemplateLayouts";
export const PresentationCard = ({
id,
@ -26,7 +26,7 @@ export const PresentationCard = ({
onDeleted?: (presentationId: string) => void;
}) => {
const router = useRouter();
const { renderSlideContent } = useGroupLayouts();
const { renderSlideContent } = useTemplateLayouts();

View file

@ -8,32 +8,28 @@ import TiptapTextReplacer from "../components/TiptapTextReplacer";
import { updateSlideContent } from "../../../store/slices/presentationGeneration";
import { Loader2 } from "lucide-react";
export const useGroupLayouts = () => {
export const useTemplateLayouts = () => {
const dispatch = useDispatch();
const { getLayoutByIdAndGroup, getLayoutsByGroup, getLayout, loading } =
const { getLayoutById, getLayout, loading } =
useLayout();
const getGroupLayout = useMemo(() => {
const getTemplateLayout = useMemo(() => {
return (layoutId: string, groupName: string) => {
const layout = getLayoutByIdAndGroup(layoutId, groupName);
const layout = getLayoutById(layoutId);
if (layout) {
return getLayout(layoutId);
}
return null;
};
}, [getLayoutByIdAndGroup, getLayout]);
}, [getLayoutById, getLayout]);
const getGroupLayouts = useMemo(() => {
return (groupName: string) => {
return getLayoutsByGroup(groupName);
};
}, [getLayoutsByGroup]);
// Render slide content with group validation, automatic Tiptap text editing, and editable images/icons
const renderSlideContent = useMemo(() => {
return (slide: any, isEditMode: boolean) => {
const Layout = getGroupLayout(slide.layout, slide.layout_group);
const Layout = getTemplateLayout(slide.layout, slide.layout_group);
if (loading) {
return (
<div className="flex flex-col items-center justify-center aspect-video h-full bg-gray-100 rounded-lg">
@ -92,11 +88,10 @@ export const useGroupLayouts = () => {
</SlideErrorBoundary>
);
};
}, [getGroupLayout, dispatch]);
}, [getTemplateLayout, dispatch]);
return {
getGroupLayout,
getGroupLayouts,
getTemplateLayout,
renderSlideContent,
loading,
};

View file

@ -2,19 +2,21 @@ import React from "react";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { Button } from "@/components/ui/button";
import { LoadingState, LayoutGroup } from "../types/index";
import { LoadingState, Template } from "../types/index";
interface GenerateButtonProps {
loadingState: LoadingState;
streamState: { isStreaming: boolean, isLoading: boolean };
selectedLayoutGroup: LayoutGroup | null;
selectedTemplate: Template | null;
onSubmit: () => void;
outlineCount: number;
}
const GenerateButton: React.FC<GenerateButtonProps> = ({
loadingState,
streamState,
selectedLayoutGroup,
selectedTemplate,
outlineCount,
onSubmit
}) => {
const pathname = usePathname();
@ -27,8 +29,8 @@ const GenerateButton: React.FC<GenerateButtonProps> = ({
const getButtonText = () => {
if (loadingState.isLoading) return loadingState.message;
if (streamState.isLoading || streamState.isStreaming) return "Loading...";
if (!selectedLayoutGroup) return "Select a Template";
return "Generate Presentation";
if (!selectedTemplate) return "Select a Template";
return `Generate Presentation (${outlineCount * 1} credits)`;
};
return (
@ -36,7 +38,7 @@ const GenerateButton: React.FC<GenerateButtonProps> = ({
disabled={isDisabled}
onClick={() => {
if (!streamState.isLoading && !streamState.isStreaming) {
if (!selectedLayoutGroup) {
if (!selectedTemplate) {
trackEvent(MixpanelEvent.Outline_Select_Template_Button_Clicked, { pathname });
} else {
trackEvent(MixpanelEvent.Outline_Generate_Presentation_Button_Clicked, { pathname });

View file

@ -1,244 +0,0 @@
"use client";
import React, { useEffect } from "react";
import { useLayout } from "../../context/LayoutContext";
import GroupLayouts from "./GroupLayouts";
import { LayoutGroup } from "../types/index";
interface LayoutSelectionProps {
selectedLayoutGroup: LayoutGroup | null;
onSelectLayoutGroup: (group: LayoutGroup) => void;
}
const LayoutSelection: React.FC<LayoutSelectionProps> = ({
selectedLayoutGroup,
onSelectLayoutGroup,
}) => {
const {
getLayoutsByGroup,
getGroupSetting,
getAllGroups,
getFullDataByGroup,
loading,
} = useLayout();
const [summaryMap, setSummaryMap] = React.useState<
Record<
string,
{ lastUpdatedAt?: number; name?: string; description?: string }
>
>({});
useEffect(() => {
// Fetch custom templates summary to get last_updated_at and template meta for sorting and display
fetch("/api/v1/ppt/template-management/summary")
.then((res) => res.json())
.then((data) => {
const map: Record<
string,
{ lastUpdatedAt?: number; name?: string; description?: string }
> = {};
if (data && Array.isArray(data.presentations)) {
for (const p of data.presentations) {
const slug = `custom-${p.presentation_id}`;
map[slug] = {
lastUpdatedAt: p.last_updated_at
? new Date(p.last_updated_at).getTime()
: 0,
name: p.template?.name,
description: p.template?.description,
};
}
}
setSummaryMap(map);
})
.catch(() => setSummaryMap({}));
}, []);
const layoutGroups: LayoutGroup[] = React.useMemo(() => {
const groups = getAllGroups();
if (groups.length === 0) return [];
const Groups: LayoutGroup[] = groups
.filter((groupName) => {
// Filter out groups that contain any errored layouts (from custom templates compile/parse errors)
const fullData = getFullDataByGroup(groupName);
const hasErroredLayouts = fullData.some(
(fd) =>
(fd as any)?.component?.displayName === "CustomTemplateErrorSlide"
);
return !hasErroredLayouts;
})
.map((groupName) => {
const settings = getGroupSetting(groupName);
const customMeta = summaryMap[groupName];
const isCustom = groupName.toLowerCase().startsWith("custom-");
return {
id: groupName,
name: isCustom && customMeta?.name ? customMeta.name : groupName,
description:
isCustom && customMeta?.description
? customMeta.description
: settings?.description || `${groupName} presentation templates`,
ordered: settings?.ordered || false,
default: settings?.default || false,
};
});
// Sort groups to put default first, then by name
return Groups.sort((a, b) => {
if (a.default && !b.default) return -1;
if (!a.default && b.default) return 1;
return a.name.localeCompare(b.name);
});
}, [
getAllGroups,
getLayoutsByGroup,
getGroupSetting,
getFullDataByGroup,
summaryMap,
]);
const inBuiltGroups = React.useMemo(
() => layoutGroups.filter((g) => !g.id.toLowerCase().startsWith("custom-")),
[layoutGroups]
);
const customGroups = React.useMemo(() => {
const unsorted = layoutGroups.filter((g) =>
g.id.toLowerCase().startsWith("custom-")
);
// Sort by last_updated_at desc using summaryMap keyed by slug id
return unsorted.sort(
(a, b) =>
(summaryMap[b.id]?.lastUpdatedAt || 0) -
(summaryMap[a.id]?.lastUpdatedAt || 0)
);
}, [layoutGroups, summaryMap]);
// Auto-select first group when groups are loaded
useEffect(() => {
if (layoutGroups.length > 0 && !selectedLayoutGroup) {
const defaultGroup =
layoutGroups.find((g) => g.default) || layoutGroups[0];
const slides = getLayoutsByGroup(defaultGroup.id);
onSelectLayoutGroup({
...defaultGroup,
slides: slides,
});
}
}, [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 (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className="p-4 rounded-lg border border-gray-200 bg-gray-50 animate-pulse"
>
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-3 bg-gray-200 rounded mb-3"></div>
<div className="grid grid-cols-3 gap-2 mb-3">
{[1, 2, 3].map((j) => (
<div
key={j}
className="aspect-video bg-gray-200 rounded"
></div>
))}
</div>
</div>
))}
</div>
</div>
);
}
if (layoutGroups.length === 0) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<h5 className="text-lg font-medium mb-2 text-gray-700">
No Templates Available
</h5>
<p className="text-gray-600 text-sm">
No presentation templates could be loaded. Please try refreshing the
page.
</p>
</div>
</div>
);
}
const handleLayoutGroupSelection = (group: LayoutGroup) => {
const slides = getLayoutsByGroup(group.id);
onSelectLayoutGroup({
...group,
slides: slides,
});
};
return (
<div className="space-y-8 mb-4">
{/* In Built Templates */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">
In Built Templates
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{inBuiltGroups.map((group) => (
<GroupLayouts
key={group.id}
group={group}
onSelectLayoutGroup={handleLayoutGroupSelection}
selectedLayoutGroup={selectedLayoutGroup}
/>
))}
</div>
</div>
{/* Custom AI Templates */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900">
Custom AI Templates
</h3>
</div>
{customGroups.length === 0 ? (
<div className="text-sm text-gray-600 py-2">
No custom templates. Create one from "Create Template" menu.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{customGroups.map((group) => (
<GroupLayouts
key={group.id}
group={group}
onSelectLayoutGroup={handleLayoutGroupSelection}
selectedLayoutGroup={selectedLayoutGroup}
/>
))}
</div>
)}
</div>
</div>
);
};
export default LayoutSelection;

View file

@ -7,14 +7,14 @@ import { useSelector } from "react-redux";
import { OverlayLoader } from "@/components/ui/overlay-loader";
import Wrapper from "@/components/Wrapper";
import OutlineContent from "./OutlineContent";
import LayoutSelection from "./LayoutSelection";
import EmptyStateView from "./EmptyStateView";
import GenerateButton from "./GenerateButton";
import { TABS, LayoutGroup } from "../types/index";
import { TABS, Template } from "../types/index";
import { useOutlineStreaming } from "../hooks/useOutlineStreaming";
import { useOutlineManagement } from "../hooks/useOutlineManagement";
import { usePresentationGeneration } from "../hooks/usePresentationGeneration";
import TemplateSelection from "./TemplateSelection";
const OutlinePage: React.FC = () => {
const { presentation_id, outlines } = useSelector(
@ -22,14 +22,14 @@ const OutlinePage: React.FC = () => {
);
const [activeTab, setActiveTab] = useState<string>(TABS.OUTLINE);
const [selectedLayoutGroup, setSelectedLayoutGroup] = useState<LayoutGroup | null>(null);
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null);
// Custom hooks
const streamState = useOutlineStreaming(presentation_id);
const { handleDragEnd, handleAddSlide } = useOutlineManagement(outlines);
const { loadingState, handleSubmit } = usePresentationGeneration(
presentation_id,
outlines,
selectedLayoutGroup,
selectedTemplate,
setActiveTab
);
if (!presentation_id) {
@ -54,8 +54,9 @@ const OutlinePage: React.FC = () => {
<TabsTrigger value={TABS.LAYOUTS}>Select Template</TabsTrigger>
</TabsList>
<div className="flex-grow w-full overflow-y-auto custom_scrollbar">
<TabsContent value={TABS.OUTLINE}>
<div className="flex-grow w-full mx-auto">
<TabsContent value={TABS.OUTLINE} className="h-[calc(100vh-16rem)] overflow-y-auto custom_scrollbar"
>
<div>
<OutlineContent
outlines={outlines}
@ -69,11 +70,11 @@ const OutlinePage: React.FC = () => {
</div>
</TabsContent>
<TabsContent value={TABS.LAYOUTS}>
<TabsContent value={TABS.LAYOUTS} className="h-[calc(100vh-16rem)] overflow-y-auto custom_scrollbar">
<div>
<LayoutSelection
selectedLayoutGroup={selectedLayoutGroup}
onSelectLayoutGroup={setSelectedLayoutGroup}
<TemplateSelection
selectedTemplate={selectedTemplate}
onSelectTemplate={setSelectedTemplate}
/>
</div>
</TabsContent>
@ -85,9 +86,10 @@ const OutlinePage: React.FC = () => {
<div className="py-4 border-t border-gray-200">
<div className="max-w-[1200px] mx-auto">
<GenerateButton
outlineCount={outlines.length}
loadingState={loadingState}
streamState={streamState}
selectedLayoutGroup={selectedLayoutGroup}
selectedTemplate={selectedTemplate}
onSubmit={handleSubmit}
/>
</div>

View file

@ -2,38 +2,37 @@ import { CheckCircle } from "lucide-react";
import React from "react";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { LayoutGroup } from "../types/index";
import { Template } from "../types/index";
import { useLayout } from "../../context/LayoutContext";
import { useFontLoader } from "../../hooks/useFontLoader";
interface GroupLayoutsProps {
group: LayoutGroup;
onSelectLayoutGroup: (group: LayoutGroup) => void;
selectedLayoutGroup: LayoutGroup | null;
interface TemplateLayoutsProps {
template: Template;
onSelectTemplate: (template: Template) => void;
selectedTemplate: Template | null;
}
const GroupLayouts: React.FC<GroupLayoutsProps> = ({
group,
onSelectLayoutGroup,
selectedLayoutGroup,
const TemplateLayouts: React.FC<TemplateLayoutsProps> = ({
template,
onSelectTemplate,
selectedTemplate,
}) => {
const { getFullDataByGroup,getCustomTemplateFonts } = useLayout();
const layoutGroup = getFullDataByGroup(group.id);
const fonts = getCustomTemplateFonts(group.id.split("custom-")[1]);
const { getFullDataByTemplateID, getCustomTemplateFonts } = useLayout();
const layoutTemplate = getFullDataByTemplateID(template.id);
const fonts = getCustomTemplateFonts(template.id.split("custom-")[1]);
useFontLoader(fonts || []);
const pathname = usePathname();
return (
<div
onClick={() => {
trackEvent(MixpanelEvent.Group_Layout_Selected_Clicked, { pathname });
onSelectLayoutGroup(group);
onSelectTemplate(template);
}}
className={`relative p-4 rounded-lg border cursor-pointer transition-all duration-200 ${
selectedLayoutGroup?.id === group.id
? "border-blue-500 bg-blue-50 shadow-md"
: "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm"
}`}
className={`relative p-4 rounded-lg border cursor-pointer transition-all duration-200 ${selectedTemplate?.id === template.id
? "border-blue-500 bg-blue-50 shadow-md"
: "border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm"
}`}
>
{selectedLayoutGroup?.id === group.id && (
{selectedTemplate?.id === template.id && (
<div className="absolute top-3 right-3">
<CheckCircle className="w-5 h-5 text-blue-500" />
</div>
@ -41,24 +40,24 @@ const GroupLayouts: React.FC<GroupLayoutsProps> = ({
<div className="mb-3 ">
<h6 className="text-base capitalize font-medium text-gray-900 mb-1">
{group.name}
{template.name}
</h6>
<p className="text-sm text-gray-600">{group.description}</p>
<p className="text-sm text-gray-600">{template.description}</p>
</div>
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2 mb-3 min-h-[300px]">
{layoutGroup &&
layoutGroup?.slice(0, 4).map((layout: any, index: number) => {
{layoutTemplate &&
layoutTemplate?.slice(0, 4).map((layout: any, index: number) => {
const {
component: LayoutComponent,
sampleData,
layoutId,
groupName,
templateID,
} = layout;
return (
<div
key={`${groupName}-${index}`}
key={`${templateID}-${index}`}
className=" relative cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
@ -71,19 +70,18 @@ const GroupLayouts: React.FC<GroupLayoutsProps> = ({
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>{layoutGroup?.length} layouts</span>
<span>{layoutTemplate?.length} layouts</span>
<span
className={`px-2 py-1 rounded text-xs ${
group.ordered
? "bg-gray-100 text-gray-700"
: "bg-blue-100 text-blue-700"
}`}
className={`px-2 py-1 rounded text-xs ${template.ordered
? "bg-gray-100 text-gray-700"
: "bg-blue-100 text-blue-700"
}`}
>
{group.ordered ? "Structured" : "Flexible"}
{template.ordered ? "Structured" : "Flexible"}
</span>
</div>
</div>
);
};
export default GroupLayouts;
export default TemplateLayouts;

View file

@ -0,0 +1,208 @@
"use client";
import React, { useEffect } from "react";
import { useLayout } from "../../context/LayoutContext";
import TemplateLayouts from "./TemplateLayouts";
import { Template } from "../types/index";
import { getHeader } from "../../services/api/header";
interface TemplateSelectionProps {
selectedTemplate: Template | null;
onSelectTemplate: (template: Template) => void;
}
const TemplateSelection: React.FC<TemplateSelectionProps> = ({
selectedTemplate,
onSelectTemplate
}) => {
const {
getLayoutsByTemplateID,
getTemplateSetting,
getAllTemplateIDs,
getFullDataByTemplateID,
loading
} = useLayout();
const [summaryMap, setSummaryMap] = React.useState<Record<string, { lastUpdatedAt?: number; name?: string; description?: string }>>({});
useEffect(() => {
// Fetch custom templates summary to get last_updated_at and template meta for sorting and display
fetch(`/api/v1/ppt/template-management/summary`, {
headers: getHeader(),
})
.then(res => res.json())
.then(data => {
const map: Record<string, { lastUpdatedAt?: number; name?: string; description?: string }> = {};
if (data && Array.isArray(data.presentations)) {
for (const p of data.presentations) {
const slug = `custom-${p.presentation}`;
map[slug] = {
lastUpdatedAt: p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0,
name: p.template?.name,
description: p.template?.description,
};
}
}
setSummaryMap(map);
})
.catch(() => setSummaryMap({}));
}, []);
const templates: Template[] = React.useMemo(() => {
const templates = getAllTemplateIDs();
if (templates.length === 0) return [];
const Templates: Template[] = templates
.filter((templateID: string) => {
// Filter out template that contain any errored layouts (from custom templates compile/parse errors)
const fullData = getFullDataByTemplateID(templateID);
const hasErroredLayouts = fullData.some((fd: any) => (fd as any)?.component?.displayName === "CustomTemplateErrorSlide");
return !hasErroredLayouts;
})
.map(templateID => {
const settings = getTemplateSetting(templateID);
const customMeta = summaryMap[templateID];
const isCustom = templateID.toLowerCase().startsWith("custom-");
return {
id: templateID,
name: isCustom && customMeta?.name ? customMeta.name : templateID,
description: (isCustom && customMeta?.description) ? customMeta.description : (settings?.description || `${templateID} presentation templates`),
ordered: settings?.ordered || false,
default: settings?.default || false,
};
});
// Sort templates to put default first, then by name
return Templates.sort((a, b) => {
if (a.default && !b.default) return -1;
if (!a.default && b.default) return 1;
return a.name.localeCompare(b.name);
});
}, [getAllTemplateIDs, getLayoutsByTemplateID, getTemplateSetting, getFullDataByTemplateID, summaryMap]);
const inBuiltTemplates = React.useMemo(
() => templates.filter(g => !g.id.toLowerCase().startsWith("custom-")),
[templates]
);
const customTemplates = React.useMemo(() => {
const unsorted = templates.filter(g => g.id.toLowerCase().startsWith("custom-"));
// Sort by last_updated_at desc using summaryMap keyed by template id
return unsorted.sort((a, b) => (summaryMap[b.id]?.lastUpdatedAt || 0) - (summaryMap[a.id]?.lastUpdatedAt || 0));
}, [templates, summaryMap]);
// Auto-select first template when templates are loaded
useEffect(() => {
if (templates.length > 0 && !selectedTemplate) {
const defaultTemplate = templates.find(g => g.default) || templates[0];
const slides = getLayoutsByTemplateID(defaultTemplate.id);
onSelectTemplate({
...defaultTemplate,
slides: slides,
});
}
}, [templates, selectedTemplate, onSelectTemplate]);
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 (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="p-4 rounded-lg border border-gray-200 bg-gray-50 animate-pulse">
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-3 bg-gray-200 rounded mb-3"></div>
<div className="grid grid-cols-3 gap-2 mb-3">
{[1, 2, 3].map((j) => (
<div key={j} className="aspect-video bg-gray-200 rounded"></div>
))}
</div>
</div>
))}
</div>
</div>
);
}
if (templates.length === 0) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<h5 className="text-lg font-medium mb-2 text-gray-700">
No Templates Available
</h5>
<p className="text-gray-600 text-sm">
No presentation templates could be loaded. Please try refreshing the page.
</p>
</div>
</div>
);
}
const handleTemplateSelection = (template: Template) => {
const slides = getLayoutsByTemplateID(template.id);
onSelectTemplate({
...template,
slides: slides,
});
}
return (
<div className="space-y-8 mb-4">
{/* In Built Templates */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">In Built Templates</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{inBuiltTemplates.map((template) => (
<TemplateLayouts
key={template.id}
template={template}
onSelectTemplate={onSelectTemplate}
selectedTemplate={selectedTemplate}
/>
))}
</div>
</div>
{/* Custom AI Templates */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900">Custom AI Templates</h3>
</div>
{customTemplates.length === 0 ? (
<div className="text-sm text-gray-600 py-2">
No custom templates. Create one from "All Templates" menu.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{customTemplates.map((template) => (
<TemplateLayouts
key={template.id}
template={template}
onSelectTemplate={handleTemplateSelection}
selectedTemplate={selectedTemplate}
/>
))}
</div>
)}
</div>
</div>
);
};
export default TemplateSelection;

View file

@ -1,4 +1,4 @@
export interface LayoutGroup {
export interface Template {
id: string;
name: string;
description: string;

View file

@ -8,18 +8,18 @@ import { Button } from "@/components/ui/button";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { AlertCircle } from "lucide-react";
import { useGroupLayouts } from "../hooks/useGroupLayouts";
import { setPresentationData } from "@/store/slices/presentationGeneration";
import { DashboardApi } from "../services/api/dashboard";
import { useLayout } from "../context/LayoutContext";
import { useFontLoader } from "../hooks/useFontLoader";
import { useTemplateLayouts } from "../hooks/useTemplateLayouts";
const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
const { renderSlideContent, loading } = useGroupLayouts();
const { renderSlideContent, loading } = useTemplateLayouts();
const pathname = usePathname();
const [contentLoading, setContentLoading] = useState(true);
const { getCustomTemplateFonts } = useLayout()

View file

@ -22,7 +22,7 @@ import {
import { setPresentationData } from "@/store/slices/presentationGeneration";
import { SortableSlide } from "./SortableSlide";
import { SortableListItem } from "./SortableListItem";
import { useGroupLayouts } from "../../hooks/useGroupLayouts";
import { useTemplateLayouts } from "../../hooks/useTemplateLayouts";
interface SidePanelProps {
selectedSlide: number;
@ -49,7 +49,7 @@ const SidePanel = ({
const dispatch = useDispatch();
// Use the centralized group layouts hook
const { renderSlideContent } = useGroupLayouts();
const { renderSlideContent } = useTemplateLayouts();
useEffect(() => {
if (window.innerWidth < 768) {
@ -194,8 +194,8 @@ const SidePanel = ({
? "bg-[#5141e5] hover:bg-[#4638c7]"
: "bg-white hover:bg-white"
}`}
onClick={() =>{
if(!isStreaming){
onClick={() => {
if (!isStreaming) {
setActive("list")
}
}}
@ -256,7 +256,7 @@ const SidePanel = ({
selectedSlide={selectedSlide}
onSlideClick={onSlideClick}
/>
))}
</div>
</SortableContext>

View file

@ -16,7 +16,7 @@ import {
deletePresentationSlide,
updateSlide,
} from "@/store/slices/presentationGeneration";
import { useGroupLayouts } from "../../hooks/useGroupLayouts";
import { useTemplateLayouts } from "../../hooks/useTemplateLayouts";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import NewSlide from "../../components/NewSlide";
@ -37,7 +37,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
);
// Use the centralized group layouts hook
const { renderSlideContent, loading } = useGroupLayouts();
const { renderSlideContent, loading } = useTemplateLayouts();
const pathname = usePathname();
const handleSubmit = async () => {
@ -176,7 +176,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
{showNewSlideSelection && !loading && (
<NewSlide
index={index}
group={`${slide.layout.split(":")[0]}`}
templateID={`${slide.layout.split(":")[0]}`}
setShowNewSlideSelection={setShowNewSlideSelection}
presentationId={presentationId}
/>

View file

@ -15,18 +15,24 @@ import "prismjs/components/prism-jsx";
import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { useFontLoader } from "../../hooks/useFontLoader";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { getHeader } from "../../services/api/header";
const GroupLayoutPreview = () => {
const params = useParams();
const router = useRouter();
const slug = params.slug as string;
const rawSlug = ((): string => {
const value: any = (params as any)?.slug;
if (typeof value === "string") return value;
if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") return value[0];
return "";
})();
const pathname = usePathname();
const { getFullDataByGroup, loading, refetch } = useLayout();
const layoutGroup = getFullDataByGroup(slug);
const { getFullDataByTemplateID, loading, refetch } = useLayout();
const layoutGroup = getFullDataByTemplateID(rawSlug);
const presentationId = slug.replace("custom-", "");
const isCustom = slug.includes("custom-");
const isCustom = rawSlug.startsWith("custom-");
const presentationId = isCustom && rawSlug.length > 7 ? rawSlug.slice(7) : "";
const [editorOpen, setEditorOpen] = useState(false);
const [currentCode, setCurrentCode] = useState("");
@ -37,13 +43,15 @@ const GroupLayoutPreview = () => {
const [layoutsMap, setLayoutsMap] = useState<Record<string, { layout_id: string; layout_name: string; layout_code: string; fonts?: string[] }>>({});
const [templateMeta, setTemplateMeta] = useState<{ name?: string; description?: string } | null>(null);
useEffect(() => {
const loadCustomLayouts = async () => {
if (!isCustom) return;
if (!isCustom || !presentationId) return;
try {
const res = await fetch(`/api/v1/ppt/template-management/get-templates/${presentationId}`);
const res = await fetch(`/api/v1/ppt/template-management/get-templates/${presentationId}`, {
headers: getHeader(),
});
if (!res.ok) return;
const data = await res.json();
const map: Record<string, { layout_id: string; layout_name: string; layout_code: string; fonts?: string[] }> = {};
@ -80,7 +88,7 @@ const GroupLayoutPreview = () => {
script.async = true;
document.head.appendChild(script);
}
}, [slug]);
}, [rawSlug]);
// Ensure fonts are injected if layoutsMap changes dynamically
useEffect(() => {
@ -102,12 +110,12 @@ const GroupLayoutPreview = () => {
return <LoadingStates type="empty" />;
}
const deleteLayouts = async () => {
const presentationId = slug.replace('custom-','');
refetch();
router.back();
const response = await fetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, {
method: "DELETE",
});
headers: getHeader(),
});
if (response.ok) {
router.push("/template-preview");
}
@ -148,7 +156,7 @@ const GroupLayoutPreview = () => {
};
const res = await fetch(`/api/v1/ppt/template-management/save-templates`, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: getHeader(),
body: JSON.stringify(payload),
});
if (!res.ok) return;
@ -198,24 +206,24 @@ const GroupLayoutPreview = () => {
className="flex items-center gap-2"
>
<Home className="w-4 h-4" />
All Groups
All Templates
</Button>
{slug.includes('custom-') && <button className=" border border-red-200 flex justify-center items-center gap-2 text-red-700 px-4 py-1 rounded-md" onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_API_Call);
deleteLayouts();
}}><Trash2 className="w-4 h-4" />Delete</button>}
{isCustom && <button className=" border border-red-200 flex justify-center items-center gap-2 text-red-700 px-4 py-1 rounded-md" onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_API_Call);
deleteLayouts();
}}><Trash2 className="w-4 h-4" />Delete</button>}
</div>
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900 capitalize">
{templateMeta?.name || layoutGroup[0].groupName} Layouts
{templateMeta?.name || layoutGroup[0].templateID} Layouts
</h1>
<p className="text-gray-600 mt-2">
{layoutGroup.length} layout{layoutGroup.length !== 1 ? "s" : ""} {templateMeta?.description || layoutGroup[0].groupName}
{layoutGroup.length} layout{layoutGroup.length !== 1 ? "s" : ""} {templateMeta?.description || layoutGroup[0].templateID}
</p>
</div>
</div>
</header>
@ -232,7 +240,7 @@ const GroupLayoutPreview = () => {
return (
<Card
key={`${layoutGroup[0].groupName}-${index}`}
key={`${layoutGroup[0].templateID}-${index}`}
className="overflow-hidden shadow-md hover:shadow-lg transition-shadow"
>
{/* Layout Header */}
@ -247,7 +255,7 @@ const GroupLayoutPreview = () => {
{fileName}
</span>
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{layoutGroup[0].groupName}
{layoutGroup[0].templateID}
</span>
</div>
</div>
@ -289,7 +297,7 @@ const GroupLayoutPreview = () => {
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="text-center text-gray-600">
<p>
{layoutGroup[0].groupName} {layoutGroup.length} components
{layoutGroup[0].templateID} {layoutGroup.length} components
</p>
</div>
</div>

View file

@ -3,17 +3,19 @@ import React, { useEffect, useState } from "react";
import { useRouter, usePathname } from "next/navigation";
import LoadingStates from "./components/LoadingStates";
import { Card } from "@/components/ui/card";
import { ExternalLink } from "lucide-react";
import { Copy, ExternalLink } from "lucide-react";
import Header from "@/app/(presentation-generator)/dashboard/components/Header";
import { useLayout } from "../context/LayoutContext";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { getHeader } from "../services/api/header";
import { toast } from "sonner";
const LayoutPreview = () => {
const {
getAllGroups,
getLayoutsByGroup,
getGroupSetting,
getFullDataByGroup,
getAllTemplateIDs,
getLayoutsByTemplateID,
getTemplateSetting,
getFullDataByTemplateID,
loading,
error,
} = useLayout();
@ -35,14 +37,16 @@ const LayoutPreview = () => {
}, []);
useEffect(() => {
// Fetch summary to map custom group slug to template meta and last updated time
fetch("/api/v1/ppt/template-management/summary")
// Fetch summary to map custom template slug to template meta and last updated time
fetch(`/api/v1/ppt/template-management/summary`, {
headers: getHeader(),
})
.then((res) => res.json())
.then((data) => {
const map: Record<string, { lastUpdatedAt?: number; name?: string; description?: string }> = {};
if (data && Array.isArray(data.presentations)) {
for (const p of data.presentations) {
const slug = `custom-${p.presentation_id}`;
const slug = `custom-${p.presentation}`;
map[slug] = {
lastUpdatedAt: p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0,
name: p.template?.name,
@ -56,22 +60,21 @@ const LayoutPreview = () => {
}, []);
// Transform context data to match expected format
const layoutGroups = getAllGroups().map((groupName) => ({
groupName,
layouts: getLayoutsByGroup(groupName),
settings: getGroupSetting(groupName) || { description: "", ordered: false },
const layoutTemplates = getAllTemplateIDs().map((templateID) => ({
templateID,
layouts: getLayoutsByTemplateID(templateID),
settings: getTemplateSetting(templateID) || { description: "", ordered: false },
}));
const inBuiltGroups = layoutGroups.filter(
(g) => !g.groupName.toLowerCase().startsWith("custom-")
const inBuiltTemplates = layoutTemplates.filter(
(g) => !g.templateID.toLowerCase().startsWith("custom-")
);
const customGroups = layoutGroups.filter((g) =>
g.groupName.toLowerCase().startsWith("custom-")
const customTemplates = layoutTemplates.filter((g) =>
g.templateID.toLowerCase().startsWith("custom-")
);
// Sort custom groups by last_updated_at desc using summaryMap
const customGroupsSorted = [...customGroups].sort(
(a, b) => (summaryMap[b.groupName]?.lastUpdatedAt || 0) - (summaryMap[a.groupName]?.lastUpdatedAt || 0)
// Sort custom templates by last_updated_at desc using summaryMap
const customTemplatesSorted = [...customTemplates].sort(
(a, b) => (summaryMap[b.templateID]?.lastUpdatedAt || 0) - (summaryMap[a.templateID]?.lastUpdatedAt || 0)
);
// Handle loading state
@ -85,7 +88,7 @@ const LayoutPreview = () => {
}
// Handle empty state
if (layoutGroups.length === 0) {
if (!loading && layoutTemplates.length === 0) {
return <LoadingStates type="empty" />;
}
@ -97,96 +100,37 @@ const LayoutPreview = () => {
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900">All Templates</h1>
<p className="text-gray-600 mt-2">
{layoutGroups.length} templates
{layoutTemplates.length} templates
</p>
</div>
</div>
{/* In Built Templates */}
<section className="h-full pt-16 flex justify-center items-center">
<div className="max-w-7xl mx-auto px-6 py-6 w-full">
<h2 className="text-xl font-semibold text-gray-900 mb-4">In Built Templates</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{inBuiltGroups.map((group) => {
const isCustom = group.groupName.toLowerCase().startsWith("custom-");
const meta = summaryMap[group.groupName];
const displayName = isCustom && meta?.name ? meta.name : group.groupName;
const displayDescription = isCustom && meta?.description ? meta.description : group.settings.description;
const layoutGroup = getFullDataByGroup(group.groupName);
return (
<Card
key={group.groupName}
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
onClick={() => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/template-preview/${group.groupName}` });
router.push(`/template-preview/${group.groupName}`)
}}
>
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
{displayName}
</h3>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
{group.layouts.length}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
{displayDescription}
</p>
<div className="grid grid-cols-2 gap-2 mb-3 ">
{layoutGroup &&
layoutGroup?.slice(0, 4).map((layout: any, index: number) => {
const {
component: LayoutComponent,
sampleData,
layoutId,
groupName,
} = layout;
return (
<div
key={`${groupName}-${index}`}
className=" relative border border-gray-200 cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
);
})}
</div>
</div>
</Card>
);
})}
</div>
</div>
</section>
{/* Custom Templates */}
<section className="h-full pt-8 pb-16 flex justify-center items-center">
<section className="h-full pt-8 pb-8 flex justify-center items-center">
<div className="max-w-7xl mx-auto px-6 py-6 w-full">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-gray-900">Custom AI Templates</h2>
<button className="text-sm text-gray-800 hover:text-blue-600 transition-colors flex items-center gap-2 group" onClick={() => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/custom-template` });
router.push(`/custom-template`)
}}>
Create Custom Template
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{customGroupsSorted.length > 0 ? (
customGroupsSorted.map((group) => {
const meta = summaryMap[group.groupName];
const displayName = meta?.name ? meta.name : group.groupName;
const displayDescription = meta?.description ? meta.description : group.settings.description;
{customTemplatesSorted.length > 0 ? (
customTemplatesSorted.map((template) => {
const meta = summaryMap[template.templateID];
const displayName = meta?.name ? meta.name : template.templateID;
const displayDescription = meta?.description ? meta.description : template.settings.description;
const layoutTemplate = getFullDataByTemplateID(template.templateID);
return (
<Card
key={group.groupName}
key={template.templateID}
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
onClick={() => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/template-preview/${group.groupName}` });
router.push(`/template-preview/${group.groupName}`)
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/template-preview/${template.templateID}` });
router.push(`/template-preview/${template.templateID}`)
}}
>
<div className="p-6">
@ -194,21 +138,45 @@ const LayoutPreview = () => {
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
{displayName}
</h3>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
{group.layouts.length}
{template.layouts.length}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
<div className="flex items-center justify-between">
<p className="text-xs text-gray-600 ">ID: {template.templateID}</p>
<Copy className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" onClick={() => {
navigator.clipboard.writeText(template.templateID);
toast.success("Copied to clipboard");
}} />
</div>
<p className="text-sm text-gray-600 my-4">
{displayDescription}
</p>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">
{group.layouts.length} layout
{group.layouts.length !== 1 ? "s" : ""}
</span>
<div className="grid grid-cols-2 gap-2 mb-3 min-h-[300px]">
{layoutTemplate &&
layoutTemplate?.slice(0, 4).map((layout: any, index: number) => {
const {
component: LayoutComponent,
sampleData,
layoutId,
templateID,
} = layout;
return (
<div
key={`${templateID}-${index}`}
className=" relative border border-gray-200 cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
);
})}
</div>
</div>
</Card>
@ -225,14 +193,14 @@ const LayoutPreview = () => {
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
Create
Create Custom Template
</h3>
<div className="flex items-center gap-2">
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
Create your first custom AI template
Create your first custom template
</p>
</div>
</Card>
@ -240,6 +208,73 @@ const LayoutPreview = () => {
</div>
</div>
</section>
{/* In Built Templates */}
<section className="h-full pt-8 flex justify-center items-center">
<div className="max-w-7xl mx-auto px-6 py-6 w-full">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Inbuilt Templates</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{inBuiltTemplates.map((template) => {
const isCustom = template.templateID.toLowerCase().startsWith("custom-");
const meta = summaryMap[template.templateID];
const displayName = isCustom && meta?.name ? meta.name : template.templateID;
const displayDescription = isCustom && meta?.description ? meta.description : template.settings.description;
const layoutTemplate = getFullDataByTemplateID(template.templateID);
return (
<Card
key={template.templateID}
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
onClick={() => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: `/template-preview/${template.templateID}` });
router.push(`/template-preview/${template.templateID}`)
}}
>
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
{displayName}
</h3>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
{template.layouts.length}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
{displayDescription}
</p>
<div className="grid grid-cols-2 gap-2 mb-3 min-h-[300px]">
{layoutTemplate &&
layoutTemplate?.slice(0, 4).map((layout: any, index: number) => {
const {
component: LayoutComponent,
sampleData,
layoutId,
templateID,
} = layout;
return (
<div
key={`${templateID}-${index}`}
className=" relative border border-gray-200 cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
);
})}
</div>
</div>
</Card>
);
})}
</div>
</div>
</section>
</div>
</div>
);

View file

@ -5,26 +5,28 @@ export interface LayoutInfo {
schema: any
sampleData: any
fileName: string
groupName: string
templateID: string
layoutId: string
}
export interface GroupSetting {
export interface TemplateSetting {
description: string;
ordered: boolean;
default?: boolean;
}
export interface LayoutGroup {
groupName: string
export interface TemplateResponse {
templateID: string
templateName?: string
layouts: LayoutInfo[]
settings: GroupSetting
settings: TemplateSetting | null
}
export interface GroupedLayoutsResponse {
groupName: string
files: string[]
settings: GroupSetting | null
export interface TemplateResponse {
templateName?: string
templateID: string
files: string[],
settings: TemplateSetting | null
}
export interface LoadingState {

View file

@ -1,57 +1,77 @@
import { NextResponse } from 'next/server'
import { promises as fs } from 'fs'
import path from 'path'
import { GroupSetting } from '@/app/(presentation-generator)/template-preview/types'
import { TemplateSetting } from '@/app/(presentation-generator)/template-preview/types'
export async function GET() {
try {
const layoutsDirectory = path.join(process.cwd(), 'presentation-templates')
const items = await fs.readdir(layoutsDirectory, { withFileTypes: true })
try {
// Get the path to the presentation-templates directory
const templatesDirectory = path.join(process.cwd(), 'presentation-templates')
// Read all directories in the presentation-templates directory
const items = await fs.readdir(templatesDirectory, { withFileTypes: true })
// Filter for directories (layout templates) and exclude files
const templateDirectories = items
.filter(item => item.isDirectory())
.map(dir => dir.name)
const allLayouts: {templateName: string, templateID: string; files: string[]; settings: TemplateSetting | null }[] = []
// Scan each template directory for layout files and settings
for (const templateName of templateDirectories) {
try {
const templatePath = path.join(templatesDirectory, templateName)
const templateFiles = await fs.readdir(templatePath)
// Filter for .tsx files and exclude any non-layout files
const layoutFiles = templateFiles.filter(file =>
file.endsWith('.tsx') &&
!file.startsWith('.') &&
!file.includes('.test.') &&
!file.includes('.spec.') &&
file !== 'settings.json'
)
// Read settings.json if it exists
let settings: TemplateSetting | null = null
const settingsPath = path.join(templatePath, 'settings.json')
try {
const settingsContent = await fs.readFile(settingsPath, 'utf-8')
settings = JSON.parse(settingsContent) as TemplateSetting
} catch (settingsError) {
console.warn(`No settings.json found for template ${templateName} or invalid JSON`)
// Provide default settings if settings.json is missing or invalid
settings = {
description: `${templateName} presentation layouts`,
ordered: false,
default: false
}
}
const groupDirectories = items.filter(item => item.isDirectory()).map(dir => dir.name)
const allLayouts: { groupName: string; files: string[]; settings: GroupSetting | null }[] = []
for (const groupName of groupDirectories) {
try {
const groupPath = path.join(layoutsDirectory, groupName)
const groupFiles = await fs.readdir(groupPath)
const layoutFiles = groupFiles.filter(file =>
file.endsWith('.tsx') &&
!file.startsWith('.') &&
!file.includes('.test.') &&
!file.includes('.spec.') &&
file !== 'settings.json'
if (layoutFiles.length > 0) {
allLayouts.push({
templateName: templateName,
templateID: templateName,
files: layoutFiles,
settings: settings
})
}
} catch (error) {
console.error(`Error reading template directory ${templateName}:`, error)
// Continue with other templates even if one fails
}
}
return NextResponse.json(allLayouts)
} catch (error) {
console.error('Error reading presentation-templates directory:', error)
return NextResponse.json(
{ error: 'Failed to read presentation-templates directory' },
{ status: 500 }
)
let settings: GroupSetting | null = null
const settingsPath = path.join(groupPath, 'settings.json')
try {
const settingsContent = await fs.readFile(settingsPath, 'utf-8')
settings = JSON.parse(settingsContent) as GroupSetting
} catch {
settings = {
description: `${groupName} presentation templates`,
ordered: false,
default: false,
}
}
if (layoutFiles.length > 0) {
allLayouts.push({ groupName, files: layoutFiles, settings })
}
} catch (error) {
console.error(`Error reading group directory ${groupName}:`, error)
}
}
return NextResponse.json(allLayouts)
} catch (error) {
console.error('Error reading presentation-templates directory:', error)
return NextResponse.json(
{ error: 'Failed to read presentation-templates directory' },
{ status: 500 }
)
}
}