refactor(Nextjs/layout): layout grouping

This commit is contained in:
shiva raj badu 2025-07-17 14:22:31 +05:45
parent 2cf23b2fbe
commit 512cde9f47
47 changed files with 1296 additions and 675 deletions

View file

@ -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<string, string>;
idMapSchema: Record<string, z.ZodSchema>;
idMapGroups: Record<string, string>;
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<LayoutInfo[] | null>(null);
const [idMapFileNames, setIdMapFileNames] = useState<Record<string, string>>({});
const [idMapSchema, setIdMapSchema] = useState<Record<string, z.ZodSchema>>({});
const [idMapGroups, setIdMapGroups] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isPreloading, setIsPreloading] = useState(false);
const extractSchema = async (layoutFiles: string[]) => {
const extractSchema = async (groupedLayoutsData: GroupedLayoutsResponse[]) => {
const layouts: LayoutInfo[] = [];
const idMapFileNames: Record<string, string> = {};
const idMapSchema: Record<string, z.ZodSchema> = {};
const idMapGroups: Record<string, string> = {};
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<string, string>) => {
const preloadLayouts = async (fileNames: Record<string, string>, groups: Record<string, string>) => {
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: () => <div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />,
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: () => <div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />,
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,

View file

@ -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));

View file

@ -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<LayoutSelectionProps> = ({
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<string, any[]>();
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<string, string> = {
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<LayoutSelectionProps> = ({
);
};
if (loading) {
return (
<div className="space-y-6">
<div className="mb-6">
<h5 className="text-lg font-medium mb-2">
Loading Layout Styles...
</h5>
<p className="text-gray-600 text-sm">
Please wait while we load the available presentation styles.
</p>
</div>
<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="mb-6">
<h5 className="text-lg font-medium mb-2">
No Layout Styles Available
</h5>
<p className="text-gray-600 text-sm">
No presentation layout styles could be loaded. Please try refreshing the page.
</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="mb-6">
@ -53,13 +164,13 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{LayoutGroups.map((group) => (
{layoutGroups.map((group) => (
<div
key={group.id}
onClick={() => 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 && (

View file

@ -9,7 +9,6 @@ import {
useSensors,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,

View file

@ -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<string>('outline');
const [selectedLayoutGroup, setSelectedLayoutGroup] = useState<LayoutGroup | null>(getDefaultLayoutGroup());
const [selectedLayoutGroup, setSelectedLayoutGroup] = useState<LayoutGroup | null>(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) {

View file

@ -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 {

View file

@ -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",
}

View file

@ -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<File[]>([]);
@ -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));

View file

@ -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<string, any>;
}
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<LayoutInfo | null> {
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<string[]> {
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<LayoutGroup[]> {
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<LayoutStructure[]> {
// 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<LayoutInfo | null> {
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<string[]> {
const layouts = await extractLayouts();
const ids: string[] = [];
layouts.forEach(group => {
group.slides.forEach(slide => {
ids.push(slide.id);
});
});
return ids;
}

View file

@ -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 }
)
}

View file

@ -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<LayoutGroup[]>([])
const [layouts, setLayouts] = useState<LayoutInfo[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(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,

View file

@ -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<Record<string, HTMLElement | null>>({})
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 <LoadingStates type="empty" onRetry={retry} />
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white z-20 backdrop-blur-sm border-b border-white/20 shadow-sm sticky top-0 ">
<div className="max-w-4xl mx-auto px-6 py-4">
<header className="bg-white shadow-sm border-b sticky top-0 z-30">
<div className="max-w-7xl mx-auto px-6 py-6">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900">Layout Preview</h1>
<p className="text-sm text-gray-600 mt-1">
{layouts.length} layout{layouts.length !== 1 ? 's' : ''} found
<h1 className="text-3xl font-bold text-gray-900">Layout Preview</h1>
<p className="text-gray-600 mt-2">
{layoutGroups.length} groups {layouts.length} layouts
</p>
</div>
</div>
{/* Group Navigation Tags */}
<div className="border-t bg-gray-50">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex flex-wrap gap-3 justify-center">
{layoutGroups.map((group) => (
<button
key={group.group}
onClick={() => scrollToSection(group.group)}
className="inline-flex items-center px-4 py-2 rounded-full text-sm font-medium bg-white border border-gray-200 text-gray-700 hover:bg-blue-50 hover:border-blue-300 hover:text-blue-700 transition-all duration-200 shadow-sm hover:shadow-md"
>
<span className="capitalize">{group.group}</span>
<span className="ml-2 px-2 py-0.5 bg-gray-100 text-gray-600 rounded-full text-xs">
{group.layouts.length}
</span>
</button>
))}
</div>
</div>
</div>
</header>
{/* Layouts List */}
<main className="max-w-7xl mx-auto space-y-8">
{layouts.map((layout, index) => {
const { component: LayoutComponent, sampleData, name, fileName } = layout
return (
<Card key={index} className="overflow-hidden py-0 shadow-lg border-0 bg-white">
{/* Layout Header */}
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900">{name}</h2>
<p className="text-sm text-gray-600 font-mono">{fileName}</p>
</div>
<div className="px-3 py-1 bg-blue-100 text-blue-700 rounded-md text-sm font-medium">
#{index + 1}
</div>
</div>
{/* Layout Groups */}
<main className="max-w-7xl mx-auto px-6 py-8">
<div className="space-y-12">
{layoutGroups.map((group) => (
<section
key={group.group}
ref={setSectionRef(group.group)}
className="space-y-6"
>
{/* Group Title */}
<div className="border-b border-gray-200 pb-4">
<h2 className="text-2xl font-bold text-gray-900 capitalize">
{group.group} Layouts
</h2>
<p className="text-gray-600 mt-1">
{group.layouts.length} layout{group.layouts.length !== 1 ? 's' : ''}
</p>
</div>
{/* Layout Content */}
{/* Group Layouts Grid */}
<div className="space-y-8">
{group.layouts.map((layout, index) => {
const { component: LayoutComponent, sampleData, name, fileName } = layout
<LayoutComponent data={sampleData} />
return (
<Card key={`${group.group}-${index}`} className="overflow-hidden shadow-md hover:shadow-lg transition-shadow">
{/* Layout Header */}
<div className="bg-white px-6 py-4 border-b">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-semibold text-gray-900">{name}</h3>
<div className="flex items-center gap-4 mt-1">
<span className="text-sm text-gray-500 font-mono">{fileName}</span>
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{group.group}
</span>
</div>
</div>
<div className="text-right">
<div className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-700">
Layout #{index + 1}
</div>
</div>
</div>
</div>
</Card>
)
})}
{/* Layout Content */}
<div className="bg-gray-50">
<LayoutComponent data={sampleData} />
</div>
</Card>
)
})}
</div>
</section>
))}
</div>
</main>
{/* Footer */}
<footer className="mt-16 bg-white/50 backdrop-blur-sm border-t border-white/20">
<div className="max-w-4xl mx-auto px-6 py-8">
<div className="text-center">
<p className="text-sm text-gray-600">
Layout Preview {layouts.length} component{layouts.length !== 1 ? 's' : ''} rendered
</p>
<footer className="bg-white border-t mt-16">
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="text-center text-gray-600">
<p>Layout Preview System {layoutGroups.length} groups {layouts.length} components</p>
</div>
</div>
</footer>

View file

@ -8,6 +8,17 @@ export interface LayoutInfo {
schema: any
sampleData: any
fileName: string
group: string
}
export interface LayoutGroup {
group: string
layouts: LayoutInfo[]
}
export interface GroupedLayoutsResponse {
group: string
files: string[]
}
export interface LoadingState {

View file

@ -1,151 +0,0 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema, IconSchema } from './defaultSchemes';
export const layoutId = 'process-slide'
export const layoutName = 'Process Slide'
export const layoutDescription = 'A professional slide featuring step-by-step processes with icons, titles, and descriptions.'
const processSlideSchema = z.object({
title: z.string().min(3).max(100).default('Our Process').meta({
description: "Main title of the slide",
}),
subtitle: z.string().min(10).max(200).optional().meta({
description: "Optional subtitle or description",
}),
steps: z.array(z.object({
icon: IconSchema.default({
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
__icon_query__: 'Default step icon'
}).meta({
description: "Icon for the step",
}),
title: z.string().min(2).max(50).meta({
description: "Title for the step",
}),
description: z.string().min(10).max(150).meta({
description: "Description of the step",
})
})).min(2).max(6).default([
{
icon: {
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
__icon_query__: 'Plan and strategy icon'
},
title: 'Plan & Strategy',
description: 'Define objectives, analyze requirements, and create a comprehensive roadmap'
},
{
icon: {
__icon_url__: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
__icon_query__: 'Execute and build icon'
},
title: 'Execute & Build',
description: 'Implement solutions with precision using cutting-edge technology and best practices'
},
{
icon: {
__icon_url__: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
__icon_query__: 'Launch and optimize icon'
},
title: 'Launch & Optimize',
description: 'Deploy the solution and continuously improve based on performance metrics'
}
]).meta({
description: "List of process steps (2-6 items)",
}),
backgroundImage: ImageSchema.optional().meta({
description: "Background image for the slide",
})
})
export const Schema = processSlideSchema
export type ProcessSlideData = z.infer<typeof processSlideSchema>
interface ProcessSlideLayoutProps {
data?: Partial<ProcessSlideData>
}
const ProcessSlideLayout: React.FC<ProcessSlideLayoutProps> = ({ data: slideData }) => {
return (
<div
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
style={slideData?.backgroundImage ? {
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.__image_url__})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
} : {}}
>
{/* Header section */}
<div className="text-center px-12 py-8 print:px-8 print:py-6 relative z-10">
<h1 className="text-4xl font-bold text-blue-600 mb-4 leading-tight print:text-3xl">
{slideData?.title || 'Our Process'}
</h1>
{slideData?.subtitle && (
<p className="text-lg text-gray-600 max-w-2xl mx-auto leading-relaxed print:text-base">
{slideData.subtitle}
</p>
)}
<div className="mt-4 w-24 h-1 bg-gradient-to-r from-blue-600 to-blue-800 mx-auto rounded-full"></div>
</div>
{/* Process steps section */}
<div className="flex-1 px-12 pb-8 print:px-8 print:pb-6 relative z-10">
<div className="flex justify-center items-center h-full">
<div className="flex flex-wrap justify-center items-center gap-8 max-w-6xl">
{slideData?.steps?.map((step, index) => (
<React.Fragment key={index}>
{/* Process Step */}
<div className="flex flex-col items-center text-center max-w-xs group">
{/* Step Number and Icon */}
<div className="relative mb-4">
<div className="w-20 h-20 bg-gradient-to-br from-blue-600 to-blue-800 rounded-full flex items-center justify-center shadow-2xl group-hover:scale-110 transition-transform duration-300 print:w-16 print:h-16">
<span className="text-white font-bold text-lg print:text-base">
{index + 1}
</span>
</div>
<div className="absolute -bottom-2 -right-2 w-8 h-8 bg-white rounded-full flex items-center justify-center shadow-lg print:w-6 print:h-6">
<img
src={step.icon?.__icon_url__ || ''}
alt={step.icon?.__icon_query__ || step.title}
className="w-6 h-6 object-cover rounded-full print:w-4 print:h-4"
/>
</div>
</div>
{/* Step Title */}
<h3 className="text-xl font-bold text-blue-600 mb-3 leading-tight print:text-lg">
{step.title}
</h3>
{/* Step Description */}
<p className="text-gray-700 leading-relaxed text-sm print:text-xs">
{step.description}
</p>
</div>
{/* Arrow between steps */}
{index < (slideData?.steps?.length || 0) - 1 && (
<div className="hidden lg:flex items-center">
<div className="w-12 h-0.5 bg-gradient-to-r from-blue-600 to-blue-800 relative print:w-8">
<div className="absolute right-0 top-1/2 transform -translate-y-1/2 w-0 h-0 border-l-4 border-l-blue-600 border-t-2 border-b-2 border-t-transparent border-b-transparent"></div>
</div>
</div>
)}
</React.Fragment>
))}
</div>
</div>
</div>
{/* Bottom decorative accent */}
<div className="absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-blue-600 to-blue-800"></div>
</div>
)
}
export default ProcessSlideLayout

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from './defaultSchemes';
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'type1-slide'
export const layoutName = 'Type1 Slide'

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from './defaultSchemes';
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'type3-slide'
export const layoutName = 'Type3 Slide'

View file

@ -56,7 +56,7 @@ const Type5SlideLayout: React.FC<Type5SlideLayoutProps> = ({ data: slideData })
>
<div className="flex flex-col lg:flex-row gap-4 sm:gap-18 md:gap-16 items-center w-full">
{/* Left section - Title and Description */}
<div className="lg:w-1/2 lg:space-y-8">
<div className="lg:w-1/2 lg:space-y-8 ">
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Key Points'}
</h1>

View file

@ -1,5 +1,6 @@
import React from 'react'
import * as z from "zod";
import { IconSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'type6-slide'
export const layoutName = 'Type6 Slide'
@ -16,29 +17,39 @@ const type6SlideSchema = z.object({
description: z.string().min(10).max(300).meta({
description: "Item description",
}),
icon: z.string().default('⭐').meta({
description: "Icon emoji for the item",
})
icon: IconSchema,
})).min(2).max(6).default([
{
heading: 'Professional Service',
description: 'High-quality professional services tailored to your specific needs and requirements',
icon: '🎯'
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css',
__icon_query__: 'Professional Service'
}
},
{
heading: 'Expert Consultation',
description: 'Expert advice and consultation from experienced professionals in the field',
icon: '💡'
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css',
__icon_query__: 'Expert Consultation'
}
},
{
heading: 'Quality Assurance',
description: 'Comprehensive quality assurance processes to ensure excellent results',
icon: '✅'
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css',
__icon_query__: 'Quality Assurance'
}
},
{
heading: 'Customer Support',
description: 'Dedicated customer support available to assist you throughout the process',
icon: '🤝'
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css',
__icon_query__: 'Customer Support'
}
}
]).meta({
description: "List of service items (2-6 items)",
@ -83,7 +94,7 @@ const Type6SlideLayout: React.FC<Type6SlideLayoutProps> = ({ data: slideData })
<div className="flex items-start gap-2 mg:gap-4">
<div className="flex-shrink-0 lg:w-16">
<div className="w-12 h-12 lg:w-16 lg:h-16 bg-blue-600 rounded-lg flex items-center justify-center text-white text-xl lg:text-2xl">
{item.icon}
<img src={item.icon.__icon_url__} className='w-full h-full object-contain' alt={item.icon.__icon_query__} />
</div>
</div>
<div>
@ -114,7 +125,7 @@ const Type6SlideLayout: React.FC<Type6SlideLayoutProps> = ({ data: slideData })
>
<div className="text-center mb-4">
<div className="w-16 h-16 lg:w-20 lg:h-20 bg-blue-600 rounded-lg flex items-center justify-center text-white text-2xl lg:text-3xl mx-auto mb-4">
{item.icon}
<img src={item.icon.__icon_url__} className='w-full h-full object-contain' alt={item.icon.__icon_query__} />
</div>
</div>
<div className="lg:space-y-4 mt-2 lg:mt-4">

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { IconSchema } from './defaultSchemes';
import { IconSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'type7-slide'
export const layoutName = 'Type7 Slide'

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { IconSchema } from './defaultSchemes';
import { IconSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'type8-slide'
export const layoutName = 'Type8 Slide'

View file

@ -0,0 +1,6 @@
{
"id": "default",
"name": "Default",
"description": "Default layout for presentations",
"ordered": true
}

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema, IconSchema } from './defaultSchemes';
import { ImageSchema, IconSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'card-slide'
export const layoutName = 'Card Slide'

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from './defaultSchemes';
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'comparison-slide'
export const layoutName = 'Comparison Slide'

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema, IconSchema } from './defaultSchemes';
import { ImageSchema, IconSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'timeline-slide'
export const layoutName = 'Timeline Slide'

View file

@ -0,0 +1,102 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'type2-timeline-slide'
export const layoutName = 'Type2 Timeline Slide'
export const layoutDescription = 'A timeline layout with title and content items arranged horizontally with numbered circles and connecting line.'
const type2TimelineSlideSchema = z.object({
title: z.string().min(3).max(100).default('Main Title').meta({
description: "Main title of the slide",
}),
items: z.array(z.object({
heading: z.string().min(2).max(100).meta({
description: "Item heading",
}),
description: z.string().min(10).max(300).meta({
description: "Item description",
})
})).min(2).max(4).default([
{
heading: 'First Point',
description: 'Description for the first key point that explains important details'
},
{
heading: 'Second Point',
description: 'Description for the second key point with relevant information'
},
{
heading: 'Third Point',
description: 'Description for the third key point highlighting crucial aspects'
}
]).meta({
description: "List of content items (2-4 items)",
})
})
export const Schema = type2TimelineSlideSchema
export type Type2TimelineSlideData = z.infer<typeof type2TimelineSlideSchema>
interface Type2TimelineSlideLayoutProps {
data?: Partial<Type2TimelineSlideData>
}
const Type2TimelineSlideLayout: React.FC<Type2TimelineSlideLayoutProps> = ({ data: slideData }) => {
const items = slideData?.items || []
const numberTranslations: string[] = ['01', '02', '03', '04', '05', '06']
const renderTimelineContent = () => {
return (
<div className="w-full flex flex-col relative mt-4 lg:mt-16">
{/* Timeline Header with Numbers and Line */}
<div className="relative flex justify-between w-[85%] mx-auto items-center mb-8 px-8">
{/* Horizontal Line */}
<div className="absolute top-1/2 w-[87%] left-1/2 -translate-x-1/2 h-[2px] bg-blue-600" />
{/* Timeline Numbers */}
{items.map((_, index) => (
<div
key={`timeline-${index}`}
className="relative z-10 w-12 h-12 rounded-full bg-blue-600 px-1 text-white flex items-center justify-center font-bold text-lg"
>
<span>{numberTranslations[index] || `0${index + 1}`}</span>
</div>
))}
</div>
{/* Timeline Content */}
<div className="flex justify-between gap-8">
{items.map((item, index) => (
<div key={index} className="flex-1 text-center relative">
<div className="space-y-4">
<h3 className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900 leading-tight">
{item.heading}
</h3>
<p className="text-sm sm:text-base lg:text-lg text-gray-700 leading-relaxed">
{item.description}
</p>
</div>
</div>
))}
</div>
</div>
)
}
return (
<div
className=" rounded-sm max-w-[1280px] w-full shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
>
<div className="text-center lg:pb-8 w-full">
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Main Title'}
</h1>
</div>
{renderTimelineContent()}
</div>
)
}
export default Type2TimelineSlideLayout

View file

@ -0,0 +1,102 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'type5-slide'
export const layoutName = 'Type5 Slide'
export const layoutDescription = 'A two-column layout with title and description on the left, and numbered items with large numerals on the right.'
const type5SlideSchema = z.object({
title: z.string().min(3).max(100).default('Key Points').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(500).default('Here is the main description that provides context and introduction to the numbered points on the right side.').meta({
description: "Main description text",
}),
items: z.array(z.object({
heading: z.string().min(2).max(100).meta({
description: "Item heading",
}),
description: z.string().min(10).max(300).meta({
description: "Item description",
})
})).min(2).max(3).default([
{
heading: 'First Key Point',
description: 'Detailed explanation of the first important point that supports the main topic'
},
{
heading: 'Second Key Point',
description: 'Detailed explanation of the second important point with relevant information'
},
{
heading: 'Third Key Point',
description: 'Detailed explanation of the third important point that concludes the discussion'
}
]).meta({
description: "List of numbered items (2-3 items)",
})
})
export const Schema = type5SlideSchema
export type Type5SlideData = z.infer<typeof type5SlideSchema>
interface Type5SlideLayoutProps {
data?: Partial<Type5SlideData>
}
const Type5SlideLayout: React.FC<Type5SlideLayoutProps> = ({ data: slideData }) => {
const items = slideData?.items || []
const numberTranslations: string[] = ['01', '02', '03', '04', '05', '06']
return (
<div
className="rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
>
<div className="flex flex-col lg:flex-row gap-4 sm:gap-18 md:gap-16 items-center w-full">
{/* Left section - Title and Description */}
<div className="lg:w-1/2 lg:space-y-8 ">
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Key Points'}
</h1>
<p className="text-base sm:text-lg lg:text-xl text-gray-700 leading-relaxed">
{slideData?.description || 'Here is the main description that provides context and introduction to the numbered points on the right side.'}
</p>
</div>
{/* Right section - Numbered items */}
<div className="lg:w-1/2 relative">
<div className="space-y-3 lg:space-y-6">
{items.map((item, index) => (
<div
key={index}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className="rounded-lg p-3 lg:p-6 relative"
>
<div className="flex gap-6">
<div className="text-[26px] lg:text-[32px] leading-[40px] px-1 font-bold mb-4 text-blue-600">
{numberTranslations[index] || `0${index + 1}`}
</div>
<div className="space-y-1">
<h3 className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900 leading-tight">
{item.heading}
</h3>
<p className="text-sm sm:text-base lg:text-lg text-gray-700 leading-relaxed">
{item.description}
</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}
export default Type5SlideLayout

View file

@ -0,0 +1,161 @@
import React from 'react'
import * as z from "zod";
import { IconSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'type6-slide'
export const layoutName = 'Type6 Slide'
export const layoutDescription = 'A centered title with a flexible grid of icon-based content items, adapting layout based on item count.'
const type6SlideSchema = z.object({
title: z.string().min(3).max(100).default('Our Services').meta({
description: "Main title of the slide",
}),
items: z.array(z.object({
heading: z.string().min(2).max(100).meta({
description: "Item heading",
}),
description: z.string().min(10).max(300).meta({
description: "Item description",
}),
icon: IconSchema,
})).min(2).max(6).default([
{
heading: 'Professional Service',
description: 'High-quality professional services tailored to your specific needs and requirements',
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css',
__icon_query__: 'Professional Service'
}
},
{
heading: 'Expert Consultation',
description: 'Expert advice and consultation from experienced professionals in the field',
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css',
__icon_query__: 'Expert Consultation'
}
},
{
heading: 'Quality Assurance',
description: 'Comprehensive quality assurance processes to ensure excellent results',
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css',
__icon_query__: 'Quality Assurance'
}
},
{
heading: 'Customer Support',
description: 'Dedicated customer support available to assist you throughout the process',
icon: {
__icon_url__: 'https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css',
__icon_query__: 'Customer Support'
}
}
]).meta({
description: "List of service items (2-6 items)",
})
})
export const Schema = type6SlideSchema
export type Type6SlideData = z.infer<typeof type6SlideSchema>
interface Type6SlideLayoutProps {
data?: Partial<Type6SlideData>
}
const Type6SlideLayout: React.FC<Type6SlideLayoutProps> = ({ data: slideData }) => {
const items = slideData?.items || []
const isGridLayout = items.length >= 4
const getGridCols = (length: number) => {
switch (length) {
case 1: return 'lg:grid-cols-1';
case 2: return 'lg:grid-cols-2';
case 3: return 'lg:grid-cols-3';
case 4: return 'lg:grid-cols-4';
case 5: return 'lg:grid-cols-5';
case 6: return 'lg:grid-cols-6';
default: return 'lg:grid-cols-1';
}
}
const renderGridContent = () => {
return (
<div className={`grid grid-cols-1 ${items.length > 4 ? 'md:grid-cols-3' : 'md:grid-cols-2'} gap-4 sm:gap-6 lg:gap-8 mt-4 lg:mt-12 w-full`}>
{items.map((item, index) => (
<div
key={index}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className="w-full rounded-lg p-3 lg:p-6 relative"
>
<div className="flex items-start gap-2 mg:gap-4">
<div className="flex-shrink-0 lg:w-16">
<div className="w-12 h-12 lg:w-16 lg:h-16 bg-blue-600 rounded-lg flex items-center justify-center text-white text-xl lg:text-2xl">
<img src={item.icon.__icon_url__} className='w-full h-full object-contain' alt={item.icon.__icon_query__} />
</div>
</div>
<div>
<h3 className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900 leading-tight mb-2">
{item.heading}
</h3>
<p className="text-sm sm:text-base lg:text-lg text-gray-700 leading-relaxed">
{item.description}
</p>
</div>
</div>
</div>
))}
</div>
)
}
const renderHorizontalContent = () => {
return (
<div className={`grid grid-cols-1 sm:grid-cols-2 ${getGridCols(items.length)} w-full gap-3 lg:gap-8 mt-4 lg:mt-12`}>
{items.map((item, index) => (
<div
key={index}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className="w-full rounded-lg p-3 lg:p-6 relative"
>
<div className="text-center mb-4">
<div className="w-16 h-16 lg:w-20 lg:h-20 bg-blue-600 rounded-lg flex items-center justify-center text-white text-2xl lg:text-3xl mx-auto mb-4">
<img src={item.icon.__icon_url__} className='w-full h-full object-contain' alt={item.icon.__icon_query__} />
</div>
</div>
<div className="lg:space-y-4 mt-2 lg:mt-4">
<h3 className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900 leading-tight text-center">
{item.heading}
</h3>
<p className="text-sm sm:text-base lg:text-lg text-gray-700 leading-relaxed text-center">
{item.description}
</p>
</div>
</div>
))}
</div>
)
}
return (
<div
className=" rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
>
<div className="text-center sm:pb-2 lg:pb-8 w-full">
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Our Services'}
</h1>
</div>
{isGridLayout ? renderGridContent() : renderHorizontalContent()}
</div>
)
}
export default Type6SlideLayout

View file

@ -0,0 +1,173 @@
import React from 'react'
import * as z from "zod";
import { IconSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'type7-slide'
export const layoutName = 'Type7 Slide'
export const layoutDescription = 'A centered title with a flexible grid of icon-based content items, adapting layout based on item count.'
const type7SlideSchema = z.object({
title: z.string().min(3).max(100).default('Our Services').meta({
description: "Main title of the slide",
}),
items: z.array(z.object({
heading: z.string().min(2).max(100).meta({
description: "Item heading",
}),
description: z.string().min(10).max(300).meta({
description: "Item description",
}),
icon: IconSchema.default({
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
__icon_query__: 'Default icon'
}).meta({
description: "Icon for the item",
})
})).min(2).max(6).default([
{
heading: 'Professional Service',
description: 'High-quality professional services tailored to your specific needs and requirements',
icon: {
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
__icon_query__: 'Professional service icon'
}
},
{
heading: 'Expert Consultation',
description: 'Expert advice and consultation from experienced professionals in the field',
icon: {
__icon_url__: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
__icon_query__: 'Expert consultation icon'
}
},
{
heading: 'Quality Assurance',
description: 'Comprehensive quality assurance processes to ensure excellent results',
icon: {
__icon_url__: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
__icon_query__: 'Quality assurance icon'
}
},
{
heading: 'Customer Support',
description: 'Dedicated customer support available to assist you throughout the process',
icon: {
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
__icon_query__: 'Customer support icon'
}
}
]).meta({
description: "List of service items (2-6 items)",
})
})
export const Schema = type7SlideSchema
export type Type7SlideData = z.infer<typeof type7SlideSchema>
interface Type7SlideLayoutProps {
data?: Partial<Type7SlideData>
}
const Type7SlideLayout: React.FC<Type7SlideLayoutProps> = ({ data: slideData }) => {
const items = slideData?.items || []
const isGridLayout = items.length >= 4
const getGridCols = (length: number) => {
switch (length) {
case 1: return 'lg:grid-cols-1';
case 2: return 'lg:grid-cols-2';
case 3: return 'lg:grid-cols-3';
case 4: return 'lg:grid-cols-4';
case 5: return 'lg:grid-cols-5';
case 6: return 'lg:grid-cols-6';
default: return 'lg:grid-cols-1';
}
}
const renderGridContent = () => {
return (
<div className={`grid grid-cols-1 ${items.length > 4 ? 'md:grid-cols-3' : 'md:grid-cols-2'} gap-4 sm:gap-6 lg:gap-8 mt-4 lg:mt-12 w-full`}>
{items.map((item, index) => (
<div
key={index}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className="w-full rounded-lg p-3 lg:p-6 relative"
>
<div className="flex items-start gap-2 mg:gap-4">
<div className="flex-shrink-0 lg:w-16">
<div className="w-12 h-12 lg:w-16 lg:h-16 bg-blue-600 rounded-lg flex items-center justify-center overflow-hidden">
<img
src={item.icon?.__icon_url__ || ''}
alt={item.icon?.__icon_query__ || item.heading}
className="w-full h-full object-cover"
/>
</div>
</div>
<div>
<h3 className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900 leading-tight mb-2">
{item.heading}
</h3>
<p className="text-sm sm:text-base lg:text-lg text-gray-700 leading-relaxed">
{item.description}
</p>
</div>
</div>
</div>
))}
</div>
)
}
const renderHorizontalContent = () => {
return (
<div className={`grid grid-cols-1 sm:grid-cols-2 ${getGridCols(items.length)} w-full gap-3 lg:gap-8 mt-4 lg:mt-12`}>
{items.map((item, index) => (
<div
key={index}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className="w-full rounded-lg p-3 lg:p-6 relative"
>
<div className="text-center mb-4">
<div className="w-16 h-16 lg:w-20 lg:h-20 bg-blue-600 rounded-lg flex items-center justify-center mx-auto mb-4 overflow-hidden">
<img
src={item.icon?.__icon_url__ || ''}
alt={item.icon?.__icon_query__ || item.heading}
className="w-full h-full object-cover"
/>
</div>
</div>
<div className="lg:space-y-4 mt-2 lg:mt-4">
<h3 className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900 leading-tight text-center">
{item.heading}
</h3>
<p className="text-sm sm:text-base lg:text-lg text-gray-700 leading-relaxed text-center">
{item.description}
</p>
</div>
</div>
))}
</div>
)
}
return (
<div
className=" rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
>
<div className="text-center sm:pb-2 lg:pb-8 w-full">
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Our Services'}
</h1>
</div>
{isGridLayout ? renderGridContent() : renderHorizontalContent()}
</div>
)
}
export default Type7SlideLayout

View file

@ -0,0 +1,167 @@
import React from 'react'
import * as z from "zod";
import { IconSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'type8-slide'
export const layoutName = 'Type8 Slide'
export const layoutDescription = 'A two-column layout with title and description on the left, and icon-based items on the right.'
const type8SlideSchema = z.object({
title: z.string().min(3).max(100).default('Key Features').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(500).default('Here is the main description that provides context and introduces the key features outlined on the right side.').meta({
description: "Main description text",
}),
items: z.array(z.object({
heading: z.string().min(2).max(100).meta({
description: "Item heading",
}),
description: z.string().min(10).max(300).meta({
description: "Item description",
}),
icon: IconSchema.default({
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
__icon_query__: 'Default icon'
}).meta({
description: "Icon for the item",
})
})).min(2).max(3).default([
{
heading: 'Advanced Features',
description: 'Cutting-edge functionality designed to enhance productivity and user experience',
icon: {
__icon_url__: 'https://cdn.pixabay.com/photo/2015/12/01/20/28/road-1072823_1280.jpg',
__icon_query__: 'Advanced features icon'
}
},
{
heading: 'Reliable Performance',
description: 'Consistent and dependable performance across all platforms and devices',
icon: {
__icon_url__: 'https://cdn.pixabay.com/photo/2016/02/19/11/19/office-1209640_1280.jpg',
__icon_query__: 'Reliable performance icon'
}
},
{
heading: 'Secure Environment',
description: 'Enterprise-grade security measures to protect your data and privacy',
icon: {
__icon_url__: 'https://cdn.pixabay.com/photo/2017/08/10/08/47/laptop-2619235_1280.jpg',
__icon_query__: 'Secure environment icon'
}
}
]).meta({
description: "List of featured items (2-3 items)",
})
})
export const Schema = type8SlideSchema
export type Type8SlideData = z.infer<typeof type8SlideSchema>
interface Type8SlideLayoutProps {
data?: Partial<Type8SlideData>
}
const Type8SlideLayout: React.FC<Type8SlideLayoutProps> = ({ data: slideData }) => {
const items = slideData?.items || []
const renderItems = () => {
if (items.length === 2) {
// Vertical stacked layout for 2 items
return (
<div className="space-y-4 lg:space-y-8">
{items.map((item, index) => (
<div
key={index}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className="rounded-lg p-3 lg:p-6 relative"
>
<div className="text-center mb-4">
<div className="w-16 h-16 lg:w-20 lg:h-20 bg-blue-600 rounded-lg flex items-center justify-center mx-auto mb-4 overflow-hidden">
<img
src={item.icon?.__icon_url__ || ''}
alt={item.icon?.__icon_query__ || item.heading}
className="w-full h-full object-cover"
/>
</div>
</div>
<div className="space-y-1 lg:space-y-3">
<h3 className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900 leading-tight text-center">
{item.heading}
</h3>
<p className="text-sm sm:text-base lg:text-lg text-gray-700 leading-relaxed text-center">
{item.description}
</p>
</div>
</div>
))}
</div>
)
} else {
// Horizontal layout with side icons for 3+ items
return (
<div className="space-y-4 lg:space-y-8">
{items.map((item, index) => (
<div
key={index}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className="rounded-lg p-3 lg:p-6 relative"
>
<div className="flex items-start gap-4">
<div className="w-[32px] md:w-[64px] h-[32px] md:h-[64px]">
<div className="w-full h-full bg-blue-600 rounded-lg flex items-center justify-center overflow-hidden">
<img
src={item.icon?.__icon_url__ || ''}
alt={item.icon?.__icon_query__ || item.heading}
className="w-full h-full object-cover"
/>
</div>
</div>
<div className="lg:space-y-3">
<h3 className="text-lg sm:text-xl lg:text-2xl font-bold text-gray-900 leading-tight">
{item.heading}
</h3>
<p className="text-sm sm:text-base lg:text-lg text-gray-700 leading-relaxed">
{item.description}
</p>
</div>
</div>
</div>
))}
</div>
)
}
}
return (
<div
className=" shadow-lg w-full max-w-[1280px] rounded-sm font-inter px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-8 lg:gap-16 items-center w-full">
{/* Left section - Title and Description */}
<div className="space-y-2 lg:space-y-6">
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Key Features'}
</h1>
<p className="text-base sm:text-lg lg:text-xl text-gray-700 leading-relaxed">
{slideData?.description || 'Here is the main description that provides context and introduces the key features outlined on the right side.'}
</p>
</div>
{/* Right section - Items */}
<div className="relative">
{renderItems()}
</div>
</div>
</div>
)
}
export default Type8SlideLayout

View file

@ -0,0 +1,6 @@
{
"id": "modern",
"name": "Modern",
"description": "Contemporary designs with clean lines and sophisticated layouts",
"ordered": true
}

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from './defaultSchemes';
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'bullet-point-slide'
export const layoutName = 'Bullet Point Slide'

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from './defaultSchemes';
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'content-slide'

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from './defaultSchemes';
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'first-slide'

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { IconSchema } from './defaultSchemes';
import { IconSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'icon-slide'
export const layoutName = 'Icon Slide'

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from './defaultSchemes';
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'image-slide'
export const layoutName = 'Image Slide'
@ -24,12 +24,6 @@ const imageSlideSchema = z.object({
}).meta({
description: "Main slide image",
}),
buttonText: z.string().max(50).default('Learn More').optional().meta({
description: "Optional button text",
}),
buttonUrl: z.string().url().optional().meta({
description: "Optional button URL",
})
})
export const Schema = imageSlideSchema
@ -73,14 +67,7 @@ const ImageSlideLayout: React.FC<ImageSlideLayoutProps> = ({ data: slideData })
{slideData?.description || 'Transform your ideas into reality with innovative solutions that drive success and growth.'}
</p>
{/* Button */}
{slideData?.buttonText && (
<div className="inline-flex">
<button className="px-8 py-3 bg-gradient-to-r from-blue-600 to-blue-800 text-white font-semibold rounded-lg shadow-lg print:shadow-md">
{slideData.buttonText}
</button>
</div>
)}
{/* Decorative line */}
<div className="mt-8 w-24 h-1 bg-gradient-to-r from-blue-600 to-blue-800 rounded-full"></div>

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from './defaultSchemes';
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'quote-slide'

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from './defaultSchemes';
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'statistics-slide'

View file

@ -1,6 +1,6 @@
import React from 'react'
import * as z from "zod";
import { ImageSchema } from './defaultSchemes';
import { ImageSchema } from '@/presentation-layouts/defaultSchemes';
export const layoutId = 'team-slide'
export const layoutName = 'Team Slide'

View file

@ -0,0 +1,71 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'type4-slide'
export const layoutName = 'Type4 Slide'
export const layoutDescription = 'A chart-focused layout with title, chart visualization, and description text.'
const type4SlideSchema = z.object({
title: z.string().min(3).max(100).default('Chart Analysis').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(500).default('This chart shows important data trends and insights that help understand the current situation and make informed decisions.').meta({
description: "Description text for the chart",
}),
chartData: z.any().optional().meta({
description: "Chart data object",
}),
isFullSizeChart: z.boolean().default(false).meta({
description: "Whether to display chart in full size mode",
})
})
export const Schema = type4SlideSchema
export type Type4SlideData = z.infer<typeof type4SlideSchema>
interface Type4SlideLayoutProps {
data?: Partial<Type4SlideData>
}
const Type4SlideLayout: React.FC<Type4SlideLayoutProps> = ({ data: slideData }) => {
const isFullSizeGraph = slideData?.isFullSizeChart || false
// Simple placeholder chart component
const ChartPlaceholder = () => (
<div className="w-full h-64 lg:h-80 bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg border-2 border-blue-200 flex items-center justify-center">
<div className="text-center">
<div className="text-blue-600 text-4xl mb-4">📊</div>
<p className="text-blue-700 font-semibold">Chart Component</p>
<p className="text-blue-600 text-sm mt-1">Data visualization will appear here</p>
</div>
</div>
)
return (
<div
className=" rounded-sm w-full max-w-[1280px] px-3 py-[10px] sm:px-12 lg:px-20 sm:py-[40px] lg:py-[86px] shadow-lg max-h-[720px] flex flex-col items-center justify-center aspect-video bg-white relative z-20 mx-auto"
>
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight mb-4 lg:mb-8">
{slideData?.title || 'Chart Analysis'}
</h1>
<div className={`flex w-full items-center ${isFullSizeGraph
? "flex-col mt-4 lg:mt-10 gap-2 sm:gap-4 md:gap-6 lg:gap-10"
: "mt-4 lg:mt-16 gap-4 sm:gap-8 md:gap-12 lg:gap-16"
}`}>
<div className="w-full">
<ChartPlaceholder />
</div>
<div className="w-full text-center">
<p className={`text-base sm:text-lg lg:text-xl text-gray-700 leading-relaxed ${isFullSizeGraph ? 'text-center' : ''}`}>
{slideData?.description || 'This chart shows important data trends and insights that help understand the current situation and make informed decisions.'}
</p>
</div>
</div>
</div>
)
}
export default Type4SlideLayout

View file

@ -0,0 +1,6 @@
{
"id": "professional",
"name": "Professional",
"description": "Clean, corporate designs perfect for business presentations",
"ordered": true
}