refactor(nextjs): use template keyword instead of group/group layouts
This commit is contained in:
parent
df8167e5bb
commit
c64cc86684
18 changed files with 713 additions and 662 deletions
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
export interface LayoutGroup {
|
||||
export interface Template {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue