diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx index d806faa7..5506aef2 100644 --- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx +++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx @@ -9,12 +9,19 @@ interface LayoutInfo { name?: string; description?: string; json_schema: any; + group: string; +} + +interface GroupedLayoutsResponse { + group: string; + files: string[]; } interface LayoutContextType { layoutSchema: LayoutInfo[] | null; idMapFileNames: Record; idMapSchema: Record; + idMapGroups: Record; loading: boolean; error: string | null; getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null; @@ -32,91 +39,92 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) const [layoutSchema, setLayoutSchema] = useState(null); const [idMapFileNames, setIdMapFileNames] = useState>({}); const [idMapSchema, setIdMapSchema] = useState>({}); + const [idMapGroups, setIdMapGroups] = useState>({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isPreloading, setIsPreloading] = useState(false); - const extractSchema = async (layoutFiles: string[]) => { + const extractSchema = async (groupedLayoutsData: GroupedLayoutsResponse[]) => { const layouts: LayoutInfo[] = []; const idMapFileNames: Record = {}; const idMapSchema: Record = {}; + const idMapGroups: Record = {}; - for (const fileName of layoutFiles) { - try { - const file = fileName.replace('.tsx', '').replace('.ts', ''); - const module = await import(`@/components/layouts/${file}`); + for (const groupData of groupedLayoutsData) { + for (const fileName of groupData.files) { + try { + const file = fileName.replace('.tsx', '').replace('.ts', ''); + const module = await import(`@/presentation-layouts/${groupData.group}/${file}`); - if (!module.default) { - toast({ - title: `${file} has no default export`, - description: 'Please ensure the layout file exports a default component', + 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`); + continue; + } + + if (!module.Schema) { + toast({ + title: `${file} has no Schema export`, + description: 'Please ensure the layout file exports a Schema', + }); + console.warn(`${file} has no Schema export`); + continue; + } + + const layoutId = module.layoutId || file.toLowerCase().replace(/layout$/, ''); + 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; + }, }); - console.warn(`${file} has no default export`); - continue; + + const layout = { + id: layoutId, + name: layoutName, + description: layoutDescription, + json_schema: jsonSchema, + group: groupData.group, + }; + + idMapFileNames[layoutId] = fileName; + idMapSchema[layoutId] = module.Schema; + idMapGroups[layoutId] = groupData.group; + layouts.push(layout); + } catch (error) { + console.error(`Error extracting schema for ${fileName} from ${groupData.group}:`, error); } - - if (!module.Schema) { - toast({ - title: `${file} has no Schema export`, - description: 'Please ensure the layout file exports a Schema', - }); - console.warn(`${file} has no Schema export`); - continue; - } - - const layoutId = module.layoutId; - if (!layoutId) { - toast({ - title: `${file} has no layoutId`, - description: 'Please ensure the layout file exports a layoutId', - }); - console.warn(`${file} has no layoutId`); - continue; - } - - const layoutName = module.layoutName; - const layoutDescription = module.layoutDescription; - const jsonSchema = z.toJSONSchema(module.Schema, { - override: (ctx) => { - delete ctx.jsonSchema.default; - }, - }); - - const layout = { - id: layoutId, - name: layoutName, - description: layoutDescription, - json_schema: jsonSchema, - }; - - idMapFileNames[layoutId] = fileName; - idMapSchema[layoutId] = module.Schema; - layouts.push(layout); - } catch (error) { - console.error(`Error extracting schema for ${fileName}:`, error); } } - return { layouts, idMapFileNames, idMapSchema }; + return { layouts, idMapFileNames, idMapSchema, idMapGroups }; }; const loadLayouts = async () => { - if (layoutSchema) return; // Already loaded - try { setLoading(true); setError(null); const layoutResponse = await fetch('/api/layouts'); - const layoutFiles = await layoutResponse.json(); - const response = await extractSchema(layoutFiles); + 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 || []); setIdMapFileNames(response?.idMapFileNames || {}); setIdMapSchema(response?.idMapSchema || {}); + setIdMapGroups(response?.idMapGroups || {}); // Preload layouts after loading schema - await preloadLayouts(response?.idMapFileNames || {}); + await preloadLayouts(response?.idMapFileNames || {}, response?.idMapGroups || {}); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Failed to load layouts'; setError(errorMessage); @@ -126,21 +134,25 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) } }; - const preloadLayouts = async (fileNames: Record) => { + const preloadLayouts = async (fileNames: Record, groups: Record) => { setIsPreloading(true); try { - const layoutPromises = Object.values(fileNames).map(async (layoutName) => { - if (!layoutCache.has(layoutName)) { + const layoutPromises = Object.entries(fileNames).map(async ([layoutId, fileName]) => { + const cacheKey = `${groups[layoutId]}/${fileName}`; + if (!layoutCache.has(cacheKey)) { + const group = groups[layoutId]; + const layoutName = fileName.replace('.tsx', '').replace('.ts', ''); + const Layout = dynamic( - () => import(`@/components/layouts/${layoutName}`), + () => import(`@/presentation-layouts/${group}/${layoutName}`), { loading: () =>
, ssr: false, } ) as React.ComponentType<{ data: any }>; - layoutCache.set(layoutName, Layout); + layoutCache.set(cacheKey, Layout); } }); @@ -154,25 +166,30 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) const getLayout = (layoutId: string): React.ComponentType<{ data: any }> | null => { const layoutName = idMapFileNames[layoutId]; - if (!layoutName) { + const group = idMapGroups[layoutId]; + + if (!layoutName || !group) { return null; } + const cacheKey = `${group}/${layoutName}`; + // Return cached layout if available - if (layoutCache.has(layoutName)) { - return layoutCache.get(layoutName)!; + if (layoutCache.has(cacheKey)) { + return layoutCache.get(cacheKey)!; } // Create and cache layout if not available + const file = layoutName.replace('.tsx', '').replace('.ts', ''); const Layout = dynamic( - () => import(`@/components/layouts/${layoutName}`), + () => import(`@/presentation-layouts/${group}/${file}`), { loading: () =>
, ssr: false, } ) as React.ComponentType<{ data: any }>; - layoutCache.set(layoutName, Layout); + layoutCache.set(cacheKey, Layout); return Layout; }; @@ -185,6 +202,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) layoutSchema, idMapFileNames, idMapSchema, + idMapGroups, loading, error, getLayout, diff --git a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx index 14efe8ee..0480be69 100644 --- a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx @@ -70,7 +70,6 @@ const DocumentsPreviewPage: React.FC = () => { duration: 10, progress: false, }); - const { layoutSchema } = useLayout(); // Memoized computed values const fileItems: FileItem[] = useMemo(() => { @@ -137,16 +136,6 @@ const DocumentsPreviewPage: React.FC = () => { const handleCreatePresentation = async () => { try { - if (!layoutSchema) { - toast({ - title: "Error", - description: "No layout schema found", - variant: "destructive", - }); - return; - } - - setShowLoading({ message: "Generating presentation outline...", @@ -161,11 +150,7 @@ const DocumentsPreviewPage: React.FC = () => { n_slides: config?.slides ? parseInt(config.slides) : null, file_paths: documentPaths, language: config?.language ?? "", - layout: { - name: 'Professional', - ordered: false, - slides: layoutSchema - } + }); dispatch(setPresentationId(createResponse.id)); diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx index b676e1f5..a6644a4c 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx @@ -1,9 +1,17 @@ "use client"; -import React from "react"; -import { LayoutGroups, LayoutGroup } from "@/components/layouts/layoutGroup"; +import React, { useEffect } from "react"; import { useLayout } from "../../context/LayoutContext"; import { CheckCircle } from "lucide-react"; +interface LayoutGroup { + id: string; + name: string; + description: string; + ordered: boolean; + isDefault?: boolean; + slides: string[]; +} + interface LayoutSelectionProps { selectedLayoutGroup: LayoutGroup | null; onSelectLayoutGroup: (group: LayoutGroup) => void; @@ -13,7 +21,67 @@ const LayoutSelection: React.FC = ({ selectedLayoutGroup, onSelectLayoutGroup }) => { - const { getLayout } = useLayout(); + const { layoutSchema, getLayout, loading } = useLayout(); + + // Create layout groups from the loaded layout schema + const layoutGroups: LayoutGroup[] = React.useMemo(() => { + if (!layoutSchema || layoutSchema.length === 0) return []; + + // 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); + }); + + // Convert to LayoutGroup format + const groups: LayoutGroup[] = []; + groupMap.forEach((layouts, groupName) => { + const group: LayoutGroup = { + id: groupName, + name: groupName.charAt(0).toUpperCase() + groupName.slice(1), + description: getGroupDescription(groupName), + ordered: getGroupOrdered(groupName), + isDefault: groupName === 'professional', + slides: layouts.map(layout => layout.id) + }; + groups.push(group); + }); + + // Sort groups to put default first + return groups.sort((a, b) => { + if (a.isDefault) return -1; + if (b.isDefault) return 1; + return a.name.localeCompare(b.name); + }); + }, [layoutSchema]); + + // Auto-select first group when groups are loaded + useEffect(() => { + if (layoutGroups.length > 0 && !selectedLayoutGroup) { + const defaultGroup = layoutGroups.find(g => g.isDefault) || layoutGroups[0]; + onSelectLayoutGroup(defaultGroup); + } + }, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup]); + + const getGroupDescription = (groupName: string): string => { + const descriptions: Record = { + professional: 'Clean, corporate designs perfect for business presentations', + modern: 'Contemporary designs with clean lines and sophisticated layouts', + default: 'Standard layouts suitable for general presentations', + creative: 'Vibrant, artistic layouts for innovative presentations', + minimal: 'Simple, focused layouts that emphasize content' + }; + return descriptions[groupName] || `${groupName} presentation layouts`; + }; + + const getGroupOrdered = (groupName: string): boolean => { + const orderedGroups = ['professional', 'modern']; + return orderedGroups.includes(groupName); + }; const renderLayoutPreview = (layoutId: string) => { const Layout = getLayout(layoutId); @@ -41,6 +109,49 @@ const LayoutSelection: React.FC = ({ ); }; + if (loading) { + return ( +
+
+
+ Loading Layout Styles... +
+

+ Please wait while we load the available presentation styles. +

+
+
+ {[1, 2, 3, 4].map((i) => ( +
+
+
+
+ {[1, 2, 3].map((j) => ( +
+ ))} +
+
+ ))} +
+
+ ); + } + + if (layoutGroups.length === 0) { + return ( +
+
+
+ No Layout Styles Available +
+

+ No presentation layout styles could be loaded. Please try refreshing the page. +

+
+
+ ); + } + return (
@@ -53,13 +164,13 @@ const LayoutSelection: React.FC = ({
- {LayoutGroups.map((group) => ( + {layoutGroups.map((group) => (
onSelectLayoutGroup(group)} - className={`relative p-4 rounded-lg border cursor-pointer ${selectedLayoutGroup?.id === group.id - ? 'border-blue-500 bg-blue-50' - : 'border-gray-200 bg-white' + 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' }`} > {selectedLayoutGroup?.id === group.id && ( diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx index 12250105..02a48c6f 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx @@ -9,7 +9,6 @@ import { useSensors, } from "@dnd-kit/core"; import { - arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index e2eaf9fd..5cd82e70 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -16,10 +16,18 @@ import { import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; import { jsonrepair } from "jsonrepair"; -import { LayoutGroup, getDefaultLayoutGroup } from "@/components/layouts/layoutGroup"; import OutlineContent from "./OutlineContent"; import LayoutSelection from "./LayoutSelection"; +interface LayoutGroup { + id: string; + name: string; + description: string; + ordered: boolean; + isDefault?: boolean; + slides: string[]; +} + const OutlinePage = () => { const dispatch = useDispatch(); const router = useRouter(); @@ -29,7 +37,7 @@ const OutlinePage = () => { ); const [activeTab, setActiveTab] = useState('outline'); - const [selectedLayoutGroup, setSelectedLayoutGroup] = useState(getDefaultLayoutGroup()); + const [selectedLayoutGroup, setSelectedLayoutGroup] = useState(null); const [loadingState, setLoadingState] = useState({ message: "", isLoading: false, @@ -182,10 +190,17 @@ const OutlinePage = () => { }); try { + // Prepare layout data in the expected format + const layoutData = { + name: selectedLayoutGroup.name, + ordered: selectedLayoutGroup.ordered, + slides: selectedLayoutGroup.slides + }; + const response = await PresentationGenerationApi.presentationPrepare({ presentation_id: presentation_id, outlines: outlines, - layoutGroup: selectedLayoutGroup, + layout: layoutData, }); if (response) { diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx index 6063eb4b..d297bde7 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState, useMemo } from "react"; +import React, { useEffect, useState, useMemo } from "react"; import { Slide } from "../../types/slide"; import { Loader2, PlusIcon, Trash2, WandSparkles } from "lucide-react"; import { diff --git a/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts b/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts index 73fef649..8cb23bd3 100644 --- a/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts +++ b/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts @@ -408,14 +408,13 @@ export class PresentationGenerationApi { n_slides, file_paths, language, - layout }: { prompt: string; n_slides: number | null; file_paths?: string[]; language: string | null; - layout: any; + }) { try { const response = await fetch( @@ -428,8 +427,6 @@ export class PresentationGenerationApi { n_slides, file_paths, language, - layout - }), cache: "no-cache", } diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx index 0f99f20d..35da0e61 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx @@ -40,7 +40,7 @@ const UploadPage = () => { const router = useRouter(); const dispatch = useDispatch(); const { toast } = useToast(); - const { layoutSchema, loading: layoutsLoading, error: layoutsError } = useLayout(); + // State management const [files, setFiles] = useState([]); @@ -88,23 +88,7 @@ const UploadPage = () => { return false; } - if (layoutsError) { - toast({ - title: "Layouts Error", - description: "Failed to load presentation layouts. Please try again.", - variant: "destructive", - }); - return false; - } - if (!layoutSchema || layoutSchema.length === 0) { - toast({ - title: "Layouts Not Available", - description: "Presentation layouts are still loading. Please wait.", - variant: "destructive", - }); - return false; - } return true; }; @@ -181,11 +165,7 @@ const UploadPage = () => { n_slides: config?.slides ? parseInt(config.slides) : null, file_paths: [], language: config?.language ?? "", - layout: { - name: 'Professional', - ordered: false, - slides: layoutSchema - } + }); dispatch(setPresentationId(createResponse.id)); diff --git a/servers/nextjs/app/(presentation-generator)/utils/layoutsExtractor.ts b/servers/nextjs/app/(presentation-generator)/utils/layoutsExtractor.ts deleted file mode 100644 index 6432e1ac..00000000 --- a/servers/nextjs/app/(presentation-generator)/utils/layoutsExtractor.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { zodToJsonSchema } from 'zod-to-json-schema'; -import fs from 'fs'; -import * as path from 'path'; - -interface LayoutInfo { - id: string; - name: string; - description: string; - json_schema: Record; -} - -interface LayoutGroup { - id: string; - ordered: boolean; - slides: string[]; -} - -interface LayoutStructure { - name: string; - ordered: boolean; - slides: LayoutInfo[]; -} - -// Cache for layouts to avoid repeated file system operations -let layoutsCache: LayoutStructure[] | null = null; - -/** - * Dynamically imports a layout file and extracts its schema and metadata - */ -async function extractLayoutFromFile(filePath: string, fileName: string): Promise { - try { - // Import the layout module dynamically - const module = await import(filePath); - - // Check if the module has a Schema export - if (!module.Schema) { - console.warn(`No Schema export found in ${fileName}`); - return null; - } - - // Extract layout metadata (optional) - const layoutId = module.layoutId || fileName.replace(/\.tsx?$/, '').toLowerCase().replace(/layout$/, ''); - const layoutName = module.layoutName || fileName.replace(/\.tsx?$/, '').replace(/([A-Z])/g, ' $1').trim(); - const layoutDescription = module.layoutDescription || `${layoutName} layout for presentations`; - - // Convert Zod schema to JSON schema - const jsonSchema = zodToJsonSchema(module.Schema, { - name: `${layoutId}Schema`, - $refStrategy: 'none' - }); - - return { - id: layoutId, - name: layoutName, - description: layoutDescription, - json_schema: jsonSchema - }; - } catch (error: unknown) { - console.error(`Error extracting layout from ${fileName}:`, error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - throw new Error(`Failed to extract schema from ${fileName}: ${errorMessage}`); - } -} - -/** - * Gets all layout files from the layouts directory - */ -async function getLayoutFiles(): Promise { - const layoutsDirectory = path.join(process.cwd(), 'components', 'layouts') - - if (! fs.existsSync(layoutsDirectory)) { - throw new Error(`Layouts directory not found at ${layoutsDirectory}`); - } - - const files = fs.readdirSync(layoutsDirectory) - - // Filter for TypeScript/TSX files, excluding layoutGroup.ts - return files.filter(file => - (file.endsWith('.ts') || file.endsWith('.tsx')) && - file !== 'layoutGroup.ts' && - !file.startsWith('.') - ); -} - -/** - * Extracts layout groups from layoutGroup.ts file - */ -async function extractLayoutGroups(): Promise { - try { - const layoutGroupPath = path.join(process.cwd(), 'components', 'layouts', 'layoutGroup.ts'); - - if (!fs.existsSync(layoutGroupPath)) { - throw new Error('layoutGroup.ts file not found in layouts directory'); - } - - const module = await import(layoutGroupPath); - - // Extract all exported layout groups - const layoutGroups: LayoutGroup[] = []; - - Object.keys(module).forEach(key => { - const exportedItem = module[key]; - - // Check if it's a layout group object - if (exportedItem && - typeof exportedItem === 'object' && - exportedItem.id && - Array.isArray(exportedItem.slides)) { - - layoutGroups.push({ - id: exportedItem.id, - ordered: exportedItem.ordered || false, - slides: exportedItem.slides - }); - } - }); - - if (layoutGroups.length === 0) { - throw new Error('No valid layout groups found in layoutGroup.ts'); - } - - return layoutGroups; - } catch (error) { - console.error('Error extracting layout groups:', error); - throw error; - } -} - -/** - * Maps layout information to layout groups - */ -function mapLayoutsToGroups( - layoutInfos: LayoutInfo[], - layoutGroups: LayoutGroup[] -): LayoutStructure[] { - return layoutGroups.map(group => { - const groupSlides: LayoutInfo[] = []; - - // Map slides in the group to their layout info - group.slides.forEach(slideId => { - const layoutInfo = layoutInfos.find(layout => - layout.id === slideId || - layout.id.replace('-', '') === slideId.replace('-', '') || - layout.id.toLowerCase() === slideId.toLowerCase() - ); - - if (layoutInfo) { - groupSlides.push(layoutInfo); - } else { - console.warn(`Layout info not found for slide ID: ${slideId}`); - } - }); - - return { - name: group.id, - ordered: group.ordered, - slides: groupSlides - }; - }); -} - -/** - * Main function to extract all layouts dynamically - */ -export async function extractLayouts(): Promise { - // Return cached layouts if available - if (layoutsCache) { - return layoutsCache; - } - - try { - // Get all layout files - const layoutFiles = await getLayoutFiles(); - - if (layoutFiles.length === 0) { - throw new Error('No layout files found in the layouts directory'); - } - - // Extract layout information from each file - const layoutPromises = layoutFiles.map(async (fileName) => { - const filePath = path.join(process.cwd(), 'components', 'layouts', fileName); - return extractLayoutFromFile(filePath, fileName); - }); - - const layoutResults = await Promise.all(layoutPromises); - - // Filter out null results (files without valid schemas) - const validLayouts = layoutResults.filter((layout): layout is LayoutInfo => layout !== null); - - if (validLayouts.length === 0) { - throw new Error('No valid schemas found in any layout files'); - } - - // Extract layout groups - const layoutGroups = await extractLayoutGroups(); - - // Map layouts to groups - const mappedLayouts = mapLayoutsToGroups(validLayouts, layoutGroups); - - // Cache the results - layoutsCache = mappedLayouts; - - return mappedLayouts; - } catch (error) { - console.error('Error extracting layouts:', error); - throw error; - } -} - -/** - * Clears the layouts cache (useful for development) - */ -export function clearLayoutsCache(): void { - layoutsCache = null; -} - -/** - * Gets a specific layout by ID - */ -export async function getLayoutById(layoutId: string): Promise { - const layouts = await extractLayouts(); - - for (const group of layouts) { - const layout = group.slides.find(slide => slide.id === layoutId); - if (layout) { - return layout; - } - } - - return null; -} - -/** - * Gets all available layout IDs - */ -export async function getAllLayoutIds(): Promise { - const layouts = await extractLayouts(); - const ids: string[] = []; - - layouts.forEach(group => { - group.slides.forEach(slide => { - ids.push(slide.id); - }); - }); - - return ids; -} \ No newline at end of file diff --git a/servers/nextjs/app/api/layouts/route.ts b/servers/nextjs/app/api/layouts/route.ts index 8ab38ce6..9cb0372b 100644 --- a/servers/nextjs/app/api/layouts/route.ts +++ b/servers/nextjs/app/api/layouts/route.ts @@ -4,25 +4,51 @@ import path from 'path' export async function GET() { try { - // Get the path to the layouts directory - const layoutsDirectory = path.join(process.cwd(), 'components', 'layouts') + // Get the path to the presentation-layouts directory + const layoutsDirectory = path.join(process.cwd(), 'presentation-layouts') - // Read all files in the layouts directory - const files = await fs.readdir(layoutsDirectory) + // Read all directories in the presentation-layouts directory + const items = await fs.readdir(layoutsDirectory, { withFileTypes: true }) - // Filter for .tsx files and exclude any non-layout files - const layoutFiles = files.filter(file => - file.endsWith('.tsx') && - !file.startsWith('.') && - !file.includes('.test.') && - !file.includes('.spec.') - ) + // Filter for directories (layout groups) and exclude files + const groupDirectories = items + .filter(item => item.isDirectory()) + .map(dir => dir.name) - return NextResponse.json(layoutFiles) + const allLayouts: { group: string; files: string[] }[] = [] + + // Scan each group directory for layout files + for (const groupName of groupDirectories) { + try { + const groupPath = path.join(layoutsDirectory, groupName) + const groupFiles = await fs.readdir(groupPath) + + // Filter for .tsx files and exclude any non-layout files + const layoutFiles = groupFiles.filter(file => + file.endsWith('.tsx') && + !file.startsWith('.') && + !file.includes('.test.') && + !file.includes('.spec.') && + file !== 'setting.json' + ) + + if (layoutFiles.length > 0) { + allLayouts.push({ + group: groupName, + files: layoutFiles + }) + } + } catch (error) { + console.error(`Error reading group directory ${groupName}:`, error) + // Continue with other groups even if one fails + } + } + + return NextResponse.json(allLayouts) } catch (error) { - console.error('Error reading layouts directory:', error) + console.error('Error reading presentation-layouts directory:', error) return NextResponse.json( - { error: 'Failed to read layouts directory' }, + { error: 'Failed to read presentation-layouts directory' }, { status: 500 } ) } diff --git a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts index 57c45f9d..85e561a9 100644 --- a/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts +++ b/servers/nextjs/app/layout-preview/hooks/useLayoutLoader.ts @@ -1,10 +1,11 @@ 'use client' import { useState, useEffect } from 'react' -import { LayoutInfo } from '../types' +import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse } from '../types' import { toast } from '@/hooks/use-toast' interface UseLayoutLoaderReturn { + layoutGroups: LayoutGroup[] layouts: LayoutInfo[] loading: boolean error: string | null @@ -12,6 +13,7 @@ interface UseLayoutLoaderReturn { } export const useLayoutLoader = (): UseLayoutLoaderReturn => { + const [layoutGroups, setLayoutGroups] = useState([]) const [layouts, setLayouts] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -31,75 +33,94 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { return } - const layoutFiles: string[] = await response.json() - const loadedLayouts: LayoutInfo[] = [] + const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json() + const loadedGroups: LayoutGroup[] = [] + const allLayouts: LayoutInfo[] = [] - for (const fileName of layoutFiles) { - try { - const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/components/layouts/${layoutName}`) + for (const groupData of groupedLayoutsData) { + const groupLayouts: LayoutInfo[] = [] - 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 - // User will need to provide actual data when using the layouts - const sampleData = module.Schema.parse({}) - - loadedLayouts.push({ - name: layoutName, - component: module.default, - schema: module.Schema, - sampleData, - fileName - }) - - } catch (importError) { - console.error(`Failed to import ${fileName}:`, importError) - - // Try alternative import path + for (const fileName of groupData.files) { try { const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/components/layouts/${layoutName}`) + const module = await import(`@/presentation-layouts/${groupData.group}/${layoutName}`) - if (module.default && module.Schema) { - // Use empty object to let schema apply its default values - const sampleData = module.Schema.parse({}) - loadedLayouts.push({ - name: layoutName, - component: module.default, - schema: module.Schema, - sampleData, - fileName + if (!module.default) { + toast({ + title: `${layoutName} has no default export`, + description: 'Please ensure the layout file exports a default component', + }) - } else { - console.error(`${layoutName} is missing required exports (default component or Schema)`) + 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 + // User will need to provide actual data when using the layouts + const sampleData = module.Schema.parse({}) + + const layoutInfo: LayoutInfo = { + name: layoutName, + component: module.default, + schema: module.Schema, + sampleData, + fileName, + group: groupData.group + } + + groupLayouts.push(layoutInfo) + allLayouts.push(layoutInfo) + + } catch (importError) { + console.error(`Failed to import ${fileName} from ${groupData.group}:`, importError) + + // Try alternative import path + try { + const layoutName = fileName.replace('.tsx', '').replace('.ts', '') + const module = await import(`@/presentation-layouts/${groupData.group}/${layoutName}`) + + if (module.default && module.Schema) { + // 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, + group: groupData.group + } + groupLayouts.push(layoutInfo) + allLayouts.push(layoutInfo) + } else { + console.error(`${layoutName} is missing required exports (default component or Schema)`) + } + } catch (altError) { + console.error(`Alternative import also failed for ${fileName} from ${groupData.group}:`, altError) } - } catch (altError) { - console.error(`Alternative import also failed for ${fileName}:`, altError) } } + + if (groupLayouts.length > 0) { + loadedGroups.push({ + group: groupData.group, + layouts: groupLayouts + }) + } } - if (loadedLayouts.length === 0) { + if (allLayouts.length === 0) { toast({ title: 'No valid layouts found', description: 'Make sure your layout files export both a default component and a Schema.', @@ -107,7 +128,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { }) setError('No valid layouts found. Make sure your layout files export both a default component and a Schema.') } else { - setLayouts(loadedLayouts) + setLayoutGroups(loadedGroups) + setLayouts(allLayouts) setError(null) } @@ -128,6 +150,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => { }, []) return { + layoutGroups, layouts, loading, error, diff --git a/servers/nextjs/app/layout-preview/page.tsx b/servers/nextjs/app/layout-preview/page.tsx index 10df0d96..433bc3db 100644 --- a/servers/nextjs/app/layout-preview/page.tsx +++ b/servers/nextjs/app/layout-preview/page.tsx @@ -1,16 +1,29 @@ 'use client' -import React from 'react' +import React, { useRef } from 'react' import { useLayoutLoader } from './hooks/useLayoutLoader' import LoadingStates from './components/LoadingStates' import { Card } from '@/components/ui/card' -/** - * Layout Preview Page - * - * Simple vertical display of all layout components with their sample data. - */ + const LayoutPreview = () => { - const { layouts, loading, error, retry } = useLayoutLoader() + 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 + } // Handle loading state if (loading) { @@ -23,59 +36,108 @@ const LayoutPreview = () => { } // Handle empty state - if (layouts.length === 0) { + if (layoutGroups.length === 0 || layouts.length === 0) { return } return ( -
+
{/* Header */} -
-
+
+
-

Layout Preview

-

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

Layout Preview

+

+ {layoutGroups.length} groups • {layouts.length} layouts

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

{name}

-

{fileName}

-
-
- #{index + 1} -
-
+ {/* Layout Groups */} +
+
+ {layoutGroups.map((group) => ( +
+ {/* Group Title */} +
+

+ {group.group} Layouts +

+

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

- {/* Layout Content */} + {/* 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 */} -