diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx index 08cbb7ea..d11c59ab 100644 --- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx +++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx @@ -9,29 +9,38 @@ interface LayoutInfo { name?: string; description?: string; json_schema: any; - group: string; + groupName: string; } interface GroupSetting { - id: string; - name: string; description: string; ordered: boolean; isDefault?: boolean; } interface GroupedLayoutsResponse { - group: string; + groupName: string; files: string[]; settings: GroupSetting | null; } +interface LayoutData { + layoutsById: Map; + layoutsByGroup: Map>; + groupSettings: Map; + fileMap: Map; + groupedLayouts: Map; + layoutSchema: LayoutInfo[]; +} + interface LayoutContextType { - layoutSchema: LayoutInfo[] | null; - groupSettings: Record; - idMapFileNames: Record; - idMapSchema: Record; - idMapGroups: Record; + getLayoutById: (layoutId: string) => LayoutInfo | null; + getLayoutByIdAndGroup: (layoutId: string, groupName: string) => LayoutInfo | null; + getLayoutsByGroup: (groupName: string) => LayoutInfo[]; + getGroupSetting: (groupName: string) => GroupSetting | null; + getAllGroups: () => string[]; + getAllLayouts: () => LayoutInfo[]; + loading: boolean; error: string | null; getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null; @@ -42,52 +51,56 @@ interface LayoutContextType { const LayoutContext = createContext(undefined); -// Global layout cache const layoutCache = new Map>(); +const createCacheKey = (groupName: string, fileName: string): string => `${groupName}/${fileName}`; + export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [layoutSchema, setLayoutSchema] = useState(null); - const [groupSettings, setGroupSettings] = useState>({}); - const [idMapFileNames, setIdMapFileNames] = useState>({}); - const [idMapSchema, setIdMapSchema] = useState>({}); - const [idMapGroups, setIdMapGroups] = useState>({}); + const [layoutData, setLayoutData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isPreloading, setIsPreloading] = useState(false); - const extractSchema = async (groupedLayoutsData: GroupedLayoutsResponse[]) => { + const buildData = async (groupedLayoutsData: GroupedLayoutsResponse[]) => { const layouts: LayoutInfo[] = []; - const idMapFileNames: Record = {}; - const idMapSchema: Record = {}; - const idMapGroups: Record = {}; - const groupSettings: Record = {}; + + const layoutsById = new Map(); + const layoutsByGroup = new Map>(); + const groupSettingsMap = new Map(); + const fileMap = new Map(); + const groupedLayouts = new Map(); + for (const groupData of groupedLayoutsData) { - // Store group settings - if (groupData.settings) { - groupSettings[groupData.group] = groupData.settings; - } else { - // Provide default settings if not available - groupSettings[groupData.group] = { - id: groupData.group, - name: groupData.group.charAt(0).toUpperCase() + groupData.group.slice(1), - description: `${groupData.group} presentation layouts`, - ordered: false, - isDefault: false - }; + + // Initialize group + if (!layoutsByGroup.has(groupData.groupName)) { + layoutsByGroup.set(groupData.groupName, new Set()); } + // group settings or default settings + const settings = groupData.settings || { + description: `${groupData.groupName} presentation layouts`, + ordered: false, + isDefault: false + }; + + groupSettingsMap.set(groupData.groupName, settings); + const groupLayouts: LayoutInfo[] = []; + for (const fileName of groupData.files) { try { const file = fileName.replace('.tsx', '').replace('.ts', ''); - const module = await import(`@/presentation-layouts/${groupData.group}/${file}`); + + const module = await import(`@/presentation-layouts/${groupData.groupName}/${file}`); + if (!module.default) { toast({ title: `${file} has no default export`, description: 'Please ensure the layout file exports a default component', }); - console.warn(`${file} has no default export`); + console.warn(`❌ ${file} has no default export`); continue; } @@ -96,83 +109,103 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) title: `${file} has no Schema export`, description: 'Please ensure the layout file exports a Schema', }); - console.warn(`${file} has no Schema export`); + console.warn(`❌ ${file} has no Schema export`); continue; } - const layoutId = module.layoutId || file.toLowerCase().replace(/layout$/, ''); + const originalLayoutId = module.layoutId || file.toLowerCase().replace(/layout$/, ''); + const uniqueKey = `${groupData.groupName}:${originalLayoutId}`; const layoutName = module.layoutName || file.replace(/([A-Z])/g, ' $1').trim(); const layoutDescription = module.layoutDescription || `${layoutName} layout for presentations`; + const jsonSchema = z.toJSONSchema(module.Schema, { override: (ctx) => { delete ctx.jsonSchema.default; }, }); - const layout = { - id: layoutId, + const layout: LayoutInfo = { + id: originalLayoutId, name: layoutName, description: layoutDescription, json_schema: jsonSchema, - group: groupData.group, + groupName: groupData.groupName, }; - idMapFileNames[layoutId] = fileName; - idMapSchema[layoutId] = module.Schema; - idMapGroups[layoutId] = groupData.group; + + layoutsById.set(uniqueKey, layout); + layoutsByGroup.get(groupData.groupName)!.add(originalLayoutId); + fileMap.set(uniqueKey, { fileName, groupName: groupData.groupName }); + groupLayouts.push(layout); layouts.push(layout); + + } catch (error) { - console.error(`Error extracting schema for ${fileName} from ${groupData.group}:`, error); + console.error(`💥 Error extracting schema for ${fileName} from ${groupData.groupName}:`, error); } } + + // Cache grouped layouts + groupedLayouts.set(groupData.groupName, groupLayouts); } - return { layouts, idMapFileNames, idMapSchema, idMapGroups, groupSettings }; + return { + layoutsById, + layoutsByGroup, + groupSettings: groupSettingsMap, + fileMap, + groupedLayouts, + layoutSchema: layouts + }; }; + const loadLayouts = async () => { try { setLoading(true); setError(null); + const layoutResponse = await fetch('/api/layouts'); + if (!layoutResponse.ok) { throw new Error(`Failed to fetch layouts: ${layoutResponse.statusText}`); } const groupedLayoutsData: GroupedLayoutsResponse[] = await layoutResponse.json(); - const response = await extractSchema(groupedLayoutsData); - setLayoutSchema(response?.layouts || []); - setGroupSettings(response?.groupSettings || {}); - setIdMapFileNames(response?.idMapFileNames || {}); - setIdMapSchema(response?.idMapSchema || {}); - setIdMapGroups(response?.idMapGroups || {}); + + if (!groupedLayoutsData || groupedLayoutsData.length === 0) { + console.warn('⚠️ API returned empty data'); + setError('No layout groups found'); + return; + } + + const data = await buildData(groupedLayoutsData); + setLayoutData(data); // Preload layouts after loading schema - await preloadLayouts(response?.idMapFileNames || {}, response?.idMapGroups || {}); + await preloadLayouts(data.fileMap); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Failed to load layouts'; setError(errorMessage); - console.error('Error loading layouts:', err); + console.error('💥 Error loading layouts:', err); } finally { setLoading(false); } }; - const preloadLayouts = async (fileNames: Record, groups: Record) => { + const preloadLayouts = async (fileMap: Map) => { setIsPreloading(true); - try { - const layoutPromises = Object.entries(fileNames).map(async ([layoutId, fileName]) => { - const cacheKey = `${groups[layoutId]}/${fileName}`; + const layoutPromises = Array.from(fileMap.entries()).map(async ([layoutId, { fileName, groupName }]) => { + const cacheKey = createCacheKey(groupName, fileName); if (!layoutCache.has(cacheKey)) { - const group = groups[layoutId]; const layoutName = fileName.replace('.tsx', '').replace('.ts', ''); const Layout = dynamic( - () => import(`@/presentation-layouts/${group}/${layoutName}`), + () => import(`@/presentation-layouts/${groupName}/${layoutName}`), { loading: () =>
, ssr: false, @@ -182,7 +215,6 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) layoutCache.set(cacheKey, Layout); } }); - await Promise.all(layoutPromises); } catch (error) { console.error('Error preloading layouts:', error); @@ -192,24 +224,37 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) }; const getLayout = (layoutId: string): React.ComponentType<{ data: any }> | null => { - const layoutName = idMapFileNames[layoutId]; - const group = idMapGroups[layoutId]; + if (!layoutData) return null; - if (!layoutName || !group) { + let fileInfo: { fileName: string; groupName: string } | undefined; + + // Search through all fileMap entries to find the layout + for (const [key, info] of Array.from(layoutData.fileMap.entries())) { + // Extract original layout ID from unique key (format: "groupName:layoutId") + const originalId = key.split(':')[1]; + if (originalId === layoutId) { + fileInfo = info; + break; + } + } + + if (!fileInfo) { + console.warn(`No file info found for layout: ${layoutId}`); return null; } - const cacheKey = `${group}/${layoutName}`; + const cacheKey = createCacheKey(fileInfo.groupName, fileInfo.fileName); // Return cached layout if available if (layoutCache.has(cacheKey)) { + console.log(` Returning cached layout: ${cacheKey}`); return layoutCache.get(cacheKey)!; } // Create and cache layout if not available - const file = layoutName.replace('.tsx', '').replace('.ts', ''); + const file = fileInfo.fileName.replace('.tsx', '').replace('.ts', ''); const Layout = dynamic( - () => import(`@/presentation-layouts/${group}/${file}`), + () => import(`@/presentation-layouts/${fileInfo.groupName}/${file}`), { loading: () =>
, ssr: false, @@ -220,17 +265,56 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) return Layout; }; + // Updated accessor methods to handle group-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) + for (const [key, layout] of Array.from(layoutData.layoutsById.entries())) { + const originalId = key.split(':')[1]; + if (originalId === layoutId) { + return layout; + } + } + return null; + }; + + const getLayoutByIdAndGroup = (layoutId: string, groupName: string): LayoutInfo | null => { + if (!layoutData) return null; + const uniqueKey = `${groupName}:${layoutId}`; + return layoutData.layoutsById.get(uniqueKey) || null; + }; + + const getLayoutsByGroup = (groupName: string): LayoutInfo[] => { + return layoutData?.groupedLayouts.get(groupName) || []; + }; + + const getGroupSetting = (groupName: string): GroupSetting | null => { + return layoutData?.groupSettings.get(groupName) || null; + }; + + const getAllGroups = (): string[] => { + return layoutData ? Array.from(layoutData.groupSettings.keys()) : []; + }; + + const getAllLayouts = (): LayoutInfo[] => { + return layoutData?.layoutSchema || []; + }; + // Load layouts on mount useEffect(() => { loadLayouts(); }, []); const contextValue: LayoutContextType = { - layoutSchema, - groupSettings, - idMapFileNames, - idMapSchema, - idMapGroups, + + getLayoutById, + getLayoutByIdAndGroup, + getLayoutsByGroup, + getGroupSetting, + getAllGroups, + getAllLayouts, + loading, error, getLayout, diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx index b536e5a8..5738207e 100644 --- a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx +++ b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx @@ -1,41 +1,32 @@ 'use client' import React, { useMemo } from 'react'; import { useLayout } from '../context/LayoutContext'; -import { useSelector } from 'react-redux'; -import { RootState } from '@/store/store'; - -interface LayoutInfo { - id: string; - name?: string; - description?: string; - json_schema: any; - group: string; -} export const useGroupLayouts = () => { - const { layoutSchema, getLayout, loading } = useLayout(); - const { presentationData } = useSelector((state: RootState) => state.presentationGeneration); + const { + getLayoutByIdAndGroup, + getLayoutsByGroup, + getLayout, + loading + } = useLayout(); - // Get the selected group name from presentation data - - - - // Get group-specific layout component with validation const getGroupLayout = useMemo(() => { return (layoutId: string, groupName: string) => { - // First check if the layout exists in the current group - - const groupLayout = layoutSchema?.filter(layout => layout.group === groupName) - if (groupLayout) { + const layout = getLayoutByIdAndGroup(layoutId, groupName); + if (layout) { return getLayout(layoutId); } - - // If layout not found in group, return null - console.warn(`Layout ${layoutId} not found in group ${groupName} `); + console.warn(`Layout ${layoutId} not found in group ${groupName}`); return null; }; - }, [getLayout]); + }, [getLayoutByIdAndGroup, getLayout]); + + const getGroupLayouts = useMemo(() => { + return (groupName: string) => { + return getLayoutsByGroup(groupName); + }; + }, [getLayoutsByGroup]); // Render slide content with group validation const renderSlideContent = useMemo(() => { @@ -56,6 +47,7 @@ export const useGroupLayouts = () => { return { getGroupLayout, + getGroupLayouts, renderSlideContent, loading }; diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx index 8adab136..17b6f00f 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx @@ -21,45 +21,40 @@ const LayoutSelection: React.FC = ({ selectedLayoutGroup, onSelectLayoutGroup }) => { - const { layoutSchema, groupSettings, getLayout, loading } = useLayout(); + const { + getLayoutsByGroup, + getGroupSetting, + getAllGroups, + getLayout, + loading + } = useLayout(); - // Convert layoutSchema to grouped format using actual group settings const layoutGroups: LayoutGroup[] = React.useMemo(() => { - if (!layoutSchema || layoutSchema.length === 0) return []; + const groups = getAllGroups(); - // Group layouts by their group property - const groupMap = new Map(); - layoutSchema.forEach(layout => { - const groupName = layout.group || 'default'; - if (!groupMap.has(groupName)) { - groupMap.set(groupName, []); - } - groupMap.get(groupName)?.push(layout); - }); + if (groups.length === 0) return []; - // Convert to LayoutGroup format using actual group settings - const groups: LayoutGroup[] = []; - groupMap.forEach((layouts, groupName) => { - const settings = groupSettings[groupName]; + const Groups: LayoutGroup[] = groups.map(groupName => { + const layouts = getLayoutsByGroup(groupName); + const settings = getGroupSetting(groupName); - const group: LayoutGroup = { - id: settings?.id || groupName, - name: settings?.name || groupName.charAt(0).toUpperCase() + groupName.slice(1), + return { + id: groupName, + name: groupName, description: settings?.description || `${groupName} presentation layouts`, ordered: settings?.ordered || false, isDefault: settings?.isDefault || false, - slides: layouts.map((layout: any) => layout.id) + slides: layouts.map(layout => layout.id) }; - groups.push(group); }); // Sort groups to put default first, then by name - return groups.sort((a, b) => { + return Groups.sort((a, b) => { if (a.isDefault && !b.isDefault) return -1; if (!a.isDefault && b.isDefault) return 1; return a.name.localeCompare(b.name); }); - }, [layoutSchema, groupSettings]); + }, [getAllGroups, getLayoutsByGroup, getGroupSetting]); // Auto-select first group when groups are loaded useEffect(() => { diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index 63decc9b..713efa84 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -32,7 +32,10 @@ interface LayoutGroup { const OutlinePage = () => { const dispatch = useDispatch(); const router = useRouter(); - const { layoutSchema } = useLayout(); + const { + getLayoutById, + loading: layoutLoading, + } = useLayout(); const { presentation_id, outlines } = useSelector( (state: RootState) => state.presentationGeneration @@ -192,10 +195,9 @@ const OutlinePage = () => { }); try { - // Collect the actual schemas for layouts in the selected group const groupLayoutSchemas = selectedLayoutGroup.slides .map(slideId => { - const layout = layoutSchema?.find(l => l.id === slideId); + const layout = getLayoutById(slideId); return layout ? { id: layout.id, name: layout.name, @@ -212,7 +214,6 @@ const OutlinePage = () => { slides: groupLayoutSchemas }; - console.log("layoutData", layoutData); const response = await PresentationGenerationApi.presentationPrepare({ presentation_id: presentation_id, outlines: outlines, diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx index 03dd67b8..cd4ba771 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx @@ -144,7 +144,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { try { const repairedJson = jsonrepair(accumulatedChunks); const partialData = JSON.parse(repairedJson); - console.log('partialData', partialData) + if (partialData.slides) { // Check if the length of slides has changed if ( diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx index 41f0ff80..235553ca 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx @@ -48,7 +48,6 @@ const SidePanel = ({ const { currentTheme, currentColors } = useSelector( (state: RootState) => state.theme ); - console.log('presentationData', presentationData) const dispatch = useDispatch(); // Use the centralized group layouts hook diff --git a/servers/nextjs/app/api/layouts/route.ts b/servers/nextjs/app/api/layouts/route.ts index 41ff89a3..583b70dd 100644 --- a/servers/nextjs/app/api/layouts/route.ts +++ b/servers/nextjs/app/api/layouts/route.ts @@ -3,8 +3,6 @@ import { promises as fs } from 'fs' import path from 'path' interface GroupSetting { - id: string; - name: string; description: string; ordered: boolean; isDefault?: boolean; @@ -23,7 +21,7 @@ export async function GET() { .filter(item => item.isDirectory()) .map(dir => dir.name) - const allLayouts: { group: string; files: string[]; settings: GroupSetting | null }[] = [] + const allLayouts: { groupName: string; files: string[]; settings: GroupSetting | null }[] = [] // Scan each group directory for layout files and settings for (const groupName of groupDirectories) { @@ -50,8 +48,6 @@ export async function GET() { console.warn(`No settings.json found for group ${groupName} or invalid JSON`) // Provide default settings if setting.json is missing or invalid settings = { - id: groupName, - name: groupName.charAt(0).toUpperCase() + groupName.slice(1), description: `${groupName} presentation layouts`, ordered: false, isDefault: false @@ -60,7 +56,7 @@ export async function GET() { if (layoutFiles.length > 0) { allLayouts.push({ - group: groupName, + groupName: groupName, files: layoutFiles, settings: settings }) @@ -70,6 +66,7 @@ export async function GET() { // Continue with other groups even if one fails } } + return NextResponse.json(allLayouts) } catch (error) { diff --git a/servers/nextjs/app/dashboard/components/PresentationCard.tsx b/servers/nextjs/app/dashboard/components/PresentationCard.tsx index 2a0b9761..17beb8aa 100644 --- a/servers/nextjs/app/dashboard/components/PresentationCard.tsx +++ b/servers/nextjs/app/dashboard/components/PresentationCard.tsx @@ -10,8 +10,6 @@ import { } from "@/components/ui/popover"; import { useRouter } from "next/navigation"; import { toast } from "@/hooks/use-toast"; -import { renderSlideContent } from "@/app/(presentation-generator)/components/slide_config"; -import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext"; import { useGroupLayouts } from "@/app/(presentation-generator)/hooks/useGroupLayouts"; export const PresentationCard = ({ @@ -25,7 +23,6 @@ export const PresentationCard = ({ created_at: string; slide: any }) => { - console.log('slide', slide) const router = useRouter(); const { renderSlideContent } = useGroupLayouts(); @@ -63,18 +60,7 @@ export const PresentationCard = ({ window.location.reload(); }; - // const LayoutComponent = useMemo(() => { - // const Layout = getLayout(slide.layout); - // if (!Layout) { - // return () =>
- // Layout not found - //
; - // } - // return Layout; - // }, [slide.layout, getLayout]); - // const slideContent = useMemo(() => { - // return ; - // }, [LayoutComponent, slide.content]); + diff --git a/servers/nextjs/app/layout-preview/[slug]/page.tsx b/servers/nextjs/app/layout-preview/[slug]/page.tsx new file mode 100644 index 00000000..d2c3e6fd --- /dev/null +++ b/servers/nextjs/app/layout-preview/[slug]/page.tsx @@ -0,0 +1,123 @@ +'use client' +import React from 'react' +import { useParams, useRouter } from 'next/navigation' +import { useGroupLayoutLoader } from '../hooks/useGroupLayoutLoader' +import LoadingStates from '../components/LoadingStates' +import { Card } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { ArrowLeft, Home } from 'lucide-react' + +const GroupLayoutPreview = () => { + const params = useParams() + const router = useRouter() + const slug = params.slug as string + + const { layoutGroup, loading, error, retry } = useGroupLayoutLoader(slug) + + // Handle loading state + if (loading) { + return + } + + // Handle error state + if (error) { + return + } + + // Handle empty state + if (!layoutGroup || layoutGroup.layouts.length === 0) { + return + } + + return ( +
+ {/* Header */} +
+
+ {/* Navigation */} +
+ + +
+ +
+

+ {layoutGroup.groupName} Layouts +

+

+ {layoutGroup.layouts.length} layout{layoutGroup.layouts.length !== 1 ? 's' : ''} • {layoutGroup.settings.description} +

+
+
+
+ + {/* Layout Grid */} +
+
+ {layoutGroup.layouts.map((layout, index) => { + const { component: LayoutComponent, sampleData, name, fileName } = layout + + return ( + + {/* Layout Header */} +
+
+
+

{name}

+
+ {fileName} + + {layoutGroup.groupName} + +
+
+
+
+ Layout #{index + 1} +
+
+
+
+ + {/* Layout Content */} +
+ +
+
+ ) + })} +
+
+ + {/* Footer */} +
+
+
+

{layoutGroup.groupName} • {layoutGroup.layouts.length} components

+
+
+
+
+ ) +} + +export default GroupLayoutPreview diff --git a/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts new file mode 100644 index 00000000..addae119 --- /dev/null +++ b/servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts @@ -0,0 +1,158 @@ +'use client' +import { useState, useEffect } from 'react' + +import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' +import { toast } from '@/hooks/use-toast' + +interface UseGroupLayoutLoaderReturn { + layoutGroup: LayoutGroup | null + loading: boolean + error: string | null + retry: () => void +} + +export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderReturn => { + const [layoutGroup, setLayoutGroup] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const loadGroupLayouts = async () => { + try { + setLoading(true) + setError(null) + setLayoutGroup(null) + + const response = await fetch('/api/layouts') + if (!response.ok) { + toast({ + title: 'Error loading layouts', + description: response.statusText, + }) + return + } + const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json() + + // Find the specific group by slug + const targetGroupData = groupedLayoutsData.find( + group => group.groupName.toLowerCase() === groupSlug.toLowerCase() + ) + + if (!targetGroupData) { + setError(`Group "${groupSlug}" not found`) + return + } + + const groupLayouts: LayoutInfo[] = [] + + // Use settings from setting.json or provide defaults + const groupSettings: GroupSetting = targetGroupData.settings ? targetGroupData.settings : { + description: `${targetGroupData.groupName} presentation layouts`, + ordered: false, + isDefault: false + } + + for (const fileName of targetGroupData.files) { + try { + const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`) + + if (!module.default) { + toast({ + title: `${layoutName} has no default export`, + description: 'Please ensure the layout file exports a default component', + }) + console.warn(`${layoutName} has no default export`) + continue + } + + if (!module.Schema) { + toast({ + title: `${layoutName} is missing required Schema export`, + description: 'Please ensure the layout file exports a Schema', + }) + console.error(`${layoutName} is missing required Schema export`) + continue + } + + // Use empty object to let schema apply its default values + const sampleData = module.Schema.parse({}) + + const layoutInfo: LayoutInfo = { + name: layoutName, + component: module.default, + schema: module.Schema, + sampleData, + fileName, + groupName: targetGroupData.groupName + } + + groupLayouts.push(layoutInfo) + + } catch (importError) { + console.error(`Failed to import ${fileName} from ${targetGroupData.groupName}:`, importError) + + // Try alternative import path + try { + const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`) + + if (module.default && module.Schema) { + const sampleData = module.Schema.parse({}) + const layoutInfo: LayoutInfo = { + name: layoutName, + component: module.default, + schema: module.Schema, + sampleData, + fileName, + groupName: targetGroupData.groupName + } + groupLayouts.push(layoutInfo) + } else { + console.error(`${layoutName} is missing required exports (default component or Schema)`) + } + } catch (altError) { + console.error(`Alternative import also failed for ${fileName} from ${targetGroupData.groupName}:`, altError) + } + } + } + + if (groupLayouts.length === 0) { + toast({ + title: 'No valid layouts found', + description: `No valid layouts found in "${groupSlug}" group.`, + }) + setError(`No valid layouts found in "${groupSlug}" group.`) + } else { + setLayoutGroup({ + groupName: targetGroupData.groupName, + layouts: groupLayouts, + settings: groupSettings + }) + setError(null) + } + + } catch (error) { + console.error('Error loading group layouts:', error) + setError(error instanceof Error ? error.message : 'Failed to load group layouts') + } finally { + setLoading(false) + } + } + + const retry = () => { + loadGroupLayouts() + } + + useEffect(() => { + if (groupSlug) { + loadGroupLayouts() + } + }, [groupSlug]) + + return { + layoutGroup, + loading, + error, + retry + } +} \ No newline at end of file diff --git a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts index c6c50863..7e45519d 100644 --- a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts @@ -27,12 +27,10 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { if (!response.ok) { toast({ title: 'Error loading layouts', - description: response.statusText, - + description: response.statusText, }) return } - const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json() const loadedGroups: LayoutGroup[] = [] const allLayouts: LayoutInfo[] = [] @@ -40,11 +38,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { for (const groupData of groupedLayoutsData) { const groupLayouts: LayoutInfo[] = [] - // Use settings from setting.json or provide defaults - const groupSettings: GroupSetting = groupData.settings || { - id: groupData.group, - name: groupData.group.charAt(0).toUpperCase() + groupData.group.slice(1), - description: `${groupData.group} presentation layouts`, + const groupSettings: GroupSetting = groupData.settings ? groupData.settings : { + description: `${groupData.groupName} presentation layouts`, ordered: false, isDefault: false } @@ -52,7 +47,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { for (const fileName of groupData.files) { try { const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/presentation-layouts/${groupData.group}/${layoutName}`) + const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) if (!module.default) { toast({ @@ -85,19 +80,19 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { schema: module.Schema, sampleData, fileName, - group: groupData.group + groupName: groupData.groupName } groupLayouts.push(layoutInfo) allLayouts.push(layoutInfo) } catch (importError) { - console.error(`Failed to import ${fileName} from ${groupData.group}:`, importError) + console.error(`Failed to import ${fileName} from ${groupData.groupName}:`, importError) // Try alternative import path try { const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/presentation-layouts/${groupData.group}/${layoutName}`) + const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) if (module.default && module.Schema) { // Use empty object to let schema apply its default values @@ -108,7 +103,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { schema: module.Schema, sampleData, fileName, - group: groupData.group + groupName: groupData.groupName } groupLayouts.push(layoutInfo) allLayouts.push(layoutInfo) @@ -116,27 +111,20 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { console.error(`${layoutName} is missing required exports (default component or Schema)`) } } catch (altError) { - console.error(`Alternative import also failed for ${fileName} from ${groupData.group}:`, altError) + console.error(`Alternative import also failed for ${fileName} from ${groupData.groupName}:`, altError) } } } if (groupLayouts.length > 0) { loadedGroups.push({ - group: groupData.group, + groupName: groupData.groupName, layouts: groupLayouts, settings: groupSettings }) } } - // Sort groups to put default first, then by name - loadedGroups.sort((a, b) => { - if (a.settings.isDefault && !b.settings.isDefault) return -1 - if (!a.settings.isDefault && b.settings.isDefault) return 1 - return a.settings.name.localeCompare(b.settings.name) - }) - if (allLayouts.length === 0) { toast({ title: 'No valid layouts found', diff --git a/servers/nextjs/app/layout-preview/page.tsx b/servers/nextjs/app/layout-preview/page.tsx index 433bc3db..91406dca 100644 --- a/servers/nextjs/app/layout-preview/page.tsx +++ b/servers/nextjs/app/layout-preview/page.tsx @@ -1,29 +1,15 @@ 'use client' -import React, { useRef } from 'react' +import React from 'react' +import { useRouter } from 'next/navigation' import { useLayoutLoader } from './hooks/useLayoutLoader' import LoadingStates from './components/LoadingStates' import { Card } from '@/components/ui/card' - +import { Button } from '@/components/ui/button' +import { ExternalLink } from 'lucide-react' const LayoutPreview = () => { const { layoutGroups, layouts, loading, error, retry } = useLayoutLoader() - const sectionRefs = useRef>({}) - - const scrollToSection = (groupName: string) => { - const element = sectionRefs.current[groupName] - if (element) { - const headerHeight = 140 // Account for sticky header + nav - const elementPosition = element.offsetTop - headerHeight - window.scrollTo({ - top: elementPosition, - behavior: 'smooth' - }) - } - } - - const setSectionRef = (groupName: string) => (el: HTMLElement | null) => { - sectionRefs.current[groupName] = el - } + const router = useRouter() // Handle loading state if (loading) { @@ -42,8 +28,7 @@ const LayoutPreview = () => { return (
- {/* Header */} -
+

Layout Preview

@@ -53,94 +38,51 @@ const LayoutPreview = () => {
- {/* Group Navigation Tags */} -
-
-
+ {/* Group Navigation Cards */} +
+
+
{layoutGroups.map((group) => ( - +
+
+

+ {group.groupName} +

+
+ + {group.layouts.length} + + +
+
+

+ {group.settings.description} +

+
+ + {group.layouts.length} layout{group.layouts.length !== 1 ? 's' : ''} + + {group.settings.isDefault && ( + + Default + + )} +
+
+ ))}
-
+
- {/* Layout Groups */} -
-
- {layoutGroups.map((group) => ( -
- {/* Group Title */} -
-

- {group.group} Layouts -

-

- {group.layouts.length} layout{group.layouts.length !== 1 ? 's' : ''} -

-
- {/* Group Layouts Grid */} -
- {group.layouts.map((layout, index) => { - const { component: LayoutComponent, sampleData, name, fileName } = layout - return ( - - {/* Layout Header */} -
-
-
-

{name}

-
- {fileName} - - {group.group} - -
-
-
-
- Layout #{index + 1} -
-
-
-
- - {/* Layout Content */} -
- -
-
- ) - })} -
-
- ))} -
-
- - {/* Footer */} -
-
-
-

Layout Preview System • {layoutGroups.length} groups • {layouts.length} components

-
-
-
) } diff --git a/servers/nextjs/app/layout-preview/types/index.ts b/servers/nextjs/app/layout-preview/types/index.ts index 42bed124..668bf428 100644 --- a/servers/nextjs/app/layout-preview/types/index.ts +++ b/servers/nextjs/app/layout-preview/types/index.ts @@ -1,6 +1,3 @@ -/** - * Shared types for the Layout Preview system - */ export interface LayoutInfo { name: string @@ -8,25 +5,23 @@ export interface LayoutInfo { schema: any sampleData: any fileName: string - group: string + groupName: string } export interface GroupSetting { - id: string; - name: string; description: string; ordered: boolean; isDefault?: boolean; } export interface LayoutGroup { - group: string + groupName: string layouts: LayoutInfo[] settings: GroupSetting } export interface GroupedLayoutsResponse { - group: string + groupName: string files: string[] settings: GroupSetting | null } diff --git a/servers/nextjs/presentation-layouts/default/setting.json b/servers/nextjs/presentation-layouts/default/setting.json index ac83599c..d6a15682 100644 --- a/servers/nextjs/presentation-layouts/default/setting.json +++ b/servers/nextjs/presentation-layouts/default/setting.json @@ -1,6 +1,4 @@ { - "id": "default", - "name": "Default", "description": "Default layout for presentations", "ordered": false, "isDefault": true diff --git a/servers/nextjs/presentation-layouts/layoutGroup.ts b/servers/nextjs/presentation-layouts/layoutGroup.ts deleted file mode 100644 index 67044221..00000000 --- a/servers/nextjs/presentation-layouts/layoutGroup.ts +++ /dev/null @@ -1,97 +0,0 @@ -export interface LayoutGroup { - id: string; - name: string; - description: string; - ordered: boolean; - isDefault?: boolean; - slides: string[]; -} - -export const ProfessionalLayoutGroup: LayoutGroup = { - id: 'professional', - name: 'Professional', - description: 'Clean, corporate designs perfect for business presentations', - ordered: true, - isDefault: true, - slides: [ - 'first-slide', - 'content-slide', - 'bullet-point-slide', - 'comparison-slide', - 'type4-slide', - 'statistics-slide', - 'team-slide', - 'quote-slide' - ] -} - -export const CreativeLayoutGroup: LayoutGroup = { - id: 'creative', - name: 'Creative', - description: 'Vibrant, artistic layouts for innovative and creative presentations', - ordered: false, - slides: [ - 'image-slide', - 'icon-slide', - 'card-slide', - 'type1-slide', - 'type2-slide', - 'type3-slide', - 'process-slide' - ] -} - -export const ModernLayoutGroup: LayoutGroup = { - id: 'modern', - name: 'Modern', - description: 'Contemporary designs with clean lines and sophisticated layouts', - ordered: true, - slides: [ - 'type5-slide', - 'type6-slide', - 'type7-slide', - 'type8-slide', - 'timeline-slide', - 'type2-timeline-slide', - 'number-box-slide' - ] -} - -export const MinimalLayoutGroup: LayoutGroup = { - id: 'minimal', - name: 'Minimal', - description: 'Simple, focused layouts that emphasize content over decoration', - ordered: false, - slides: [ - 'content-slide', - 'bullet-point-slide', - 'type2-numbered-slide', - 'quote-slide', - 'statistics-slide' - ] -} - -export const LayoutGroups = [ - ProfessionalLayoutGroup, - CreativeLayoutGroup, - ModernLayoutGroup, - MinimalLayoutGroup -]; - -export const getDefaultLayoutGroup = (): LayoutGroup => { - return LayoutGroups.find(group => group.isDefault) || ProfessionalLayoutGroup; -}; - -export const getAllLayouts = (): string[] => { - const allLayouts = new Set(); - LayoutGroups.forEach(group => { - group.slides.forEach(slide => allLayouts.add(slide)); - }); - return Array.from(allLayouts); -}; - -export const getGroupByLayoutId = (layoutId: string): LayoutGroup | undefined => { - return LayoutGroups.find(group => group.slides.includes(layoutId)); -}; - - diff --git a/servers/nextjs/presentation-layouts/modern/ComparisonSlideLayout.tsx b/servers/nextjs/presentation-layouts/modern/ComparisonSlideLayout.tsx index c4da0650..851f401a 100644 --- a/servers/nextjs/presentation-layouts/modern/ComparisonSlideLayout.tsx +++ b/servers/nextjs/presentation-layouts/modern/ComparisonSlideLayout.tsx @@ -74,7 +74,7 @@ const ComparisonSlideLayout: React.FC = ({ data: sli return (
= ({ data: slide // const data = numberBoxSlideSchema.parse(slideData || {}) return ( -
+
{/* Subtle background pattern */}
diff --git a/servers/nextjs/presentation-layouts/modern/TimelineSlideLayout.tsx b/servers/nextjs/presentation-layouts/modern/TimelineSlideLayout.tsx index 5c333280..8d91d939 100644 --- a/servers/nextjs/presentation-layouts/modern/TimelineSlideLayout.tsx +++ b/servers/nextjs/presentation-layouts/modern/TimelineSlideLayout.tsx @@ -86,7 +86,7 @@ const TimelineSlideLayout: React.FC = ({ data: slideDa return (
= ({ data: s return (
= ({ data: slideData return (
= ({ data: slideData }) return (
= ({ data: slideData }) => { return ( -
+
{/* Left panel - Image */}
= ({ data: slideData }) return (
= ({ data: slideData }) => return (