refactor(nextjs): layout renderer & Preview by group slug
This commit is contained in:
parent
957ad959dd
commit
afffb374c4
26 changed files with 546 additions and 388 deletions
|
|
@ -9,29 +9,38 @@ interface LayoutInfo {
|
|||
name?: string;
|
||||
description?: string;
|
||||
json_schema: any;
|
||||
group: string;
|
||||
groupName: string;
|
||||
}
|
||||
|
||||
interface GroupSetting {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ordered: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
interface GroupedLayoutsResponse {
|
||||
group: string;
|
||||
groupName: string;
|
||||
files: string[];
|
||||
settings: GroupSetting | null;
|
||||
}
|
||||
|
||||
interface LayoutData {
|
||||
layoutsById: Map<string, LayoutInfo>;
|
||||
layoutsByGroup: Map<string, Set<string>>;
|
||||
groupSettings: Map<string, GroupSetting>;
|
||||
fileMap: Map<string, { fileName: string; groupName: string }>;
|
||||
groupedLayouts: Map<string, LayoutInfo[]>;
|
||||
layoutSchema: LayoutInfo[];
|
||||
}
|
||||
|
||||
interface LayoutContextType {
|
||||
layoutSchema: LayoutInfo[] | null;
|
||||
groupSettings: Record<string, GroupSetting>;
|
||||
idMapFileNames: Record<string, string>;
|
||||
idMapSchema: Record<string, z.ZodSchema>;
|
||||
idMapGroups: Record<string, string>;
|
||||
getLayoutById: (layoutId: string) => LayoutInfo | null;
|
||||
getLayoutByIdAndGroup: (layoutId: string, groupName: string) => LayoutInfo | null;
|
||||
getLayoutsByGroup: (groupName: string) => LayoutInfo[];
|
||||
getGroupSetting: (groupName: string) => GroupSetting | null;
|
||||
getAllGroups: () => string[];
|
||||
getAllLayouts: () => LayoutInfo[];
|
||||
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null;
|
||||
|
|
@ -42,52 +51,56 @@ interface LayoutContextType {
|
|||
|
||||
const LayoutContext = createContext<LayoutContextType | undefined>(undefined);
|
||||
|
||||
// Global layout cache
|
||||
const layoutCache = new Map<string, React.ComponentType<{ data: any }>>();
|
||||
|
||||
const createCacheKey = (groupName: string, fileName: string): string => `${groupName}/${fileName}`;
|
||||
|
||||
export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [layoutSchema, setLayoutSchema] = useState<LayoutInfo[] | null>(null);
|
||||
const [groupSettings, setGroupSettings] = useState<Record<string, GroupSetting>>({});
|
||||
const [idMapFileNames, setIdMapFileNames] = useState<Record<string, string>>({});
|
||||
const [idMapSchema, setIdMapSchema] = useState<Record<string, z.ZodSchema>>({});
|
||||
const [idMapGroups, setIdMapGroups] = useState<Record<string, string>>({});
|
||||
const [layoutData, setLayoutData] = useState<LayoutData | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPreloading, setIsPreloading] = useState(false);
|
||||
|
||||
const extractSchema = async (groupedLayoutsData: GroupedLayoutsResponse[]) => {
|
||||
const buildData = async (groupedLayoutsData: GroupedLayoutsResponse[]) => {
|
||||
const layouts: LayoutInfo[] = [];
|
||||
const idMapFileNames: Record<string, string> = {};
|
||||
const idMapSchema: Record<string, z.ZodSchema> = {};
|
||||
const idMapGroups: Record<string, string> = {};
|
||||
const groupSettings: Record<string, GroupSetting> = {};
|
||||
|
||||
const layoutsById = new Map<string, LayoutInfo>();
|
||||
const layoutsByGroup = new Map<string, Set<string>>();
|
||||
const groupSettingsMap = new Map<string, GroupSetting>();
|
||||
const fileMap = new Map<string, { fileName: string; groupName: string }>();
|
||||
const groupedLayouts = new Map<string, LayoutInfo[]>();
|
||||
|
||||
|
||||
for (const groupData of groupedLayoutsData) {
|
||||
// Store group settings
|
||||
if (groupData.settings) {
|
||||
groupSettings[groupData.group] = groupData.settings;
|
||||
} else {
|
||||
// Provide default settings if not available
|
||||
groupSettings[groupData.group] = {
|
||||
id: groupData.group,
|
||||
name: groupData.group.charAt(0).toUpperCase() + groupData.group.slice(1),
|
||||
description: `${groupData.group} presentation layouts`,
|
||||
ordered: false,
|
||||
isDefault: false
|
||||
};
|
||||
|
||||
// Initialize group
|
||||
if (!layoutsByGroup.has(groupData.groupName)) {
|
||||
layoutsByGroup.set(groupData.groupName, new Set());
|
||||
}
|
||||
|
||||
// group settings or default settings
|
||||
const settings = groupData.settings || {
|
||||
description: `${groupData.groupName} presentation layouts`,
|
||||
ordered: false,
|
||||
isDefault: false
|
||||
};
|
||||
|
||||
groupSettingsMap.set(groupData.groupName, settings);
|
||||
const groupLayouts: LayoutInfo[] = [];
|
||||
|
||||
for (const fileName of groupData.files) {
|
||||
try {
|
||||
const file = fileName.replace('.tsx', '').replace('.ts', '');
|
||||
const module = await import(`@/presentation-layouts/${groupData.group}/${file}`);
|
||||
|
||||
const module = await import(`@/presentation-layouts/${groupData.groupName}/${file}`);
|
||||
|
||||
|
||||
if (!module.default) {
|
||||
toast({
|
||||
title: `${file} has no default export`,
|
||||
description: 'Please ensure the layout file exports a default component',
|
||||
});
|
||||
console.warn(`${file} has no default export`);
|
||||
console.warn(`❌ ${file} has no default export`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -96,83 +109,103 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
|
|||
title: `${file} has no Schema export`,
|
||||
description: 'Please ensure the layout file exports a Schema',
|
||||
});
|
||||
console.warn(`${file} has no Schema export`);
|
||||
console.warn(`❌ ${file} has no Schema export`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const layoutId = module.layoutId || file.toLowerCase().replace(/layout$/, '');
|
||||
const originalLayoutId = module.layoutId || file.toLowerCase().replace(/layout$/, '');
|
||||
const uniqueKey = `${groupData.groupName}:${originalLayoutId}`;
|
||||
const layoutName = module.layoutName || file.replace(/([A-Z])/g, ' $1').trim();
|
||||
const layoutDescription = module.layoutDescription || `${layoutName} layout for presentations`;
|
||||
|
||||
|
||||
const jsonSchema = z.toJSONSchema(module.Schema, {
|
||||
override: (ctx) => {
|
||||
delete ctx.jsonSchema.default;
|
||||
},
|
||||
});
|
||||
|
||||
const layout = {
|
||||
id: layoutId,
|
||||
const layout: LayoutInfo = {
|
||||
id: originalLayoutId,
|
||||
name: layoutName,
|
||||
description: layoutDescription,
|
||||
json_schema: jsonSchema,
|
||||
group: groupData.group,
|
||||
groupName: groupData.groupName,
|
||||
};
|
||||
|
||||
idMapFileNames[layoutId] = fileName;
|
||||
idMapSchema[layoutId] = module.Schema;
|
||||
idMapGroups[layoutId] = groupData.group;
|
||||
|
||||
layoutsById.set(uniqueKey, layout);
|
||||
layoutsByGroup.get(groupData.groupName)!.add(originalLayoutId);
|
||||
fileMap.set(uniqueKey, { fileName, groupName: groupData.groupName });
|
||||
groupLayouts.push(layout);
|
||||
layouts.push(layout);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error extracting schema for ${fileName} from ${groupData.group}:`, error);
|
||||
console.error(`💥 Error extracting schema for ${fileName} from ${groupData.groupName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache grouped layouts
|
||||
groupedLayouts.set(groupData.groupName, groupLayouts);
|
||||
}
|
||||
|
||||
return { layouts, idMapFileNames, idMapSchema, idMapGroups, groupSettings };
|
||||
return {
|
||||
layoutsById,
|
||||
layoutsByGroup,
|
||||
groupSettings: groupSettingsMap,
|
||||
fileMap,
|
||||
groupedLayouts,
|
||||
layoutSchema: layouts
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const loadLayouts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
||||
const layoutResponse = await fetch('/api/layouts');
|
||||
|
||||
if (!layoutResponse.ok) {
|
||||
throw new Error(`Failed to fetch layouts: ${layoutResponse.statusText}`);
|
||||
}
|
||||
|
||||
const groupedLayoutsData: GroupedLayoutsResponse[] = await layoutResponse.json();
|
||||
const response = await extractSchema(groupedLayoutsData);
|
||||
|
||||
setLayoutSchema(response?.layouts || []);
|
||||
setGroupSettings(response?.groupSettings || {});
|
||||
setIdMapFileNames(response?.idMapFileNames || {});
|
||||
setIdMapSchema(response?.idMapSchema || {});
|
||||
setIdMapGroups(response?.idMapGroups || {});
|
||||
|
||||
if (!groupedLayoutsData || groupedLayoutsData.length === 0) {
|
||||
console.warn('⚠️ API returned empty data');
|
||||
setError('No layout groups found');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await buildData(groupedLayoutsData);
|
||||
setLayoutData(data);
|
||||
|
||||
// Preload layouts after loading schema
|
||||
await preloadLayouts(response?.idMapFileNames || {}, response?.idMapGroups || {});
|
||||
await preloadLayouts(data.fileMap);
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to load layouts';
|
||||
setError(errorMessage);
|
||||
console.error('Error loading layouts:', err);
|
||||
console.error('💥 Error loading layouts:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const preloadLayouts = async (fileNames: Record<string, string>, groups: Record<string, string>) => {
|
||||
const preloadLayouts = async (fileMap: Map<string, { fileName: string; groupName: string }>) => {
|
||||
setIsPreloading(true);
|
||||
|
||||
try {
|
||||
const layoutPromises = Object.entries(fileNames).map(async ([layoutId, fileName]) => {
|
||||
const cacheKey = `${groups[layoutId]}/${fileName}`;
|
||||
const layoutPromises = Array.from(fileMap.entries()).map(async ([layoutId, { fileName, groupName }]) => {
|
||||
const cacheKey = createCacheKey(groupName, fileName);
|
||||
if (!layoutCache.has(cacheKey)) {
|
||||
const group = groups[layoutId];
|
||||
const layoutName = fileName.replace('.tsx', '').replace('.ts', '');
|
||||
|
||||
const Layout = dynamic(
|
||||
() => import(`@/presentation-layouts/${group}/${layoutName}`),
|
||||
() => import(`@/presentation-layouts/${groupName}/${layoutName}`),
|
||||
{
|
||||
loading: () => <div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />,
|
||||
ssr: false,
|
||||
|
|
@ -182,7 +215,6 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
|
|||
layoutCache.set(cacheKey, Layout);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(layoutPromises);
|
||||
} catch (error) {
|
||||
console.error('Error preloading layouts:', error);
|
||||
|
|
@ -192,24 +224,37 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
|
|||
};
|
||||
|
||||
const getLayout = (layoutId: string): React.ComponentType<{ data: any }> | null => {
|
||||
const layoutName = idMapFileNames[layoutId];
|
||||
const group = idMapGroups[layoutId];
|
||||
if (!layoutData) return null;
|
||||
|
||||
if (!layoutName || !group) {
|
||||
let fileInfo: { fileName: string; groupName: string } | undefined;
|
||||
|
||||
// Search through all fileMap entries to find the layout
|
||||
for (const [key, info] of Array.from(layoutData.fileMap.entries())) {
|
||||
// Extract original layout ID from unique key (format: "groupName:layoutId")
|
||||
const originalId = key.split(':')[1];
|
||||
if (originalId === layoutId) {
|
||||
fileInfo = info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileInfo) {
|
||||
console.warn(`No file info found for layout: ${layoutId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = `${group}/${layoutName}`;
|
||||
const cacheKey = createCacheKey(fileInfo.groupName, fileInfo.fileName);
|
||||
|
||||
// Return cached layout if available
|
||||
if (layoutCache.has(cacheKey)) {
|
||||
console.log(` Returning cached layout: ${cacheKey}`);
|
||||
return layoutCache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
// Create and cache layout if not available
|
||||
const file = layoutName.replace('.tsx', '').replace('.ts', '');
|
||||
const file = fileInfo.fileName.replace('.tsx', '').replace('.ts', '');
|
||||
const Layout = dynamic(
|
||||
() => import(`@/presentation-layouts/${group}/${file}`),
|
||||
() => import(`@/presentation-layouts/${fileInfo.groupName}/${file}`),
|
||||
{
|
||||
loading: () => <div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />,
|
||||
ssr: false,
|
||||
|
|
@ -220,17 +265,56 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
|
|||
return Layout;
|
||||
};
|
||||
|
||||
// Updated accessor methods to handle group-specific lookups
|
||||
const getLayoutById = (layoutId: string): LayoutInfo | null => {
|
||||
if (!layoutData) return null;
|
||||
|
||||
// Search through all entries to find the layout (since we don't know the group)
|
||||
for (const [key, layout] of Array.from(layoutData.layoutsById.entries())) {
|
||||
const originalId = key.split(':')[1];
|
||||
if (originalId === layoutId) {
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getLayoutByIdAndGroup = (layoutId: string, groupName: string): LayoutInfo | null => {
|
||||
if (!layoutData) return null;
|
||||
const uniqueKey = `${groupName}:${layoutId}`;
|
||||
return layoutData.layoutsById.get(uniqueKey) || null;
|
||||
};
|
||||
|
||||
const getLayoutsByGroup = (groupName: string): LayoutInfo[] => {
|
||||
return layoutData?.groupedLayouts.get(groupName) || [];
|
||||
};
|
||||
|
||||
const getGroupSetting = (groupName: string): GroupSetting | null => {
|
||||
return layoutData?.groupSettings.get(groupName) || null;
|
||||
};
|
||||
|
||||
const getAllGroups = (): string[] => {
|
||||
return layoutData ? Array.from(layoutData.groupSettings.keys()) : [];
|
||||
};
|
||||
|
||||
const getAllLayouts = (): LayoutInfo[] => {
|
||||
return layoutData?.layoutSchema || [];
|
||||
};
|
||||
|
||||
// Load layouts on mount
|
||||
useEffect(() => {
|
||||
loadLayouts();
|
||||
}, []);
|
||||
|
||||
const contextValue: LayoutContextType = {
|
||||
layoutSchema,
|
||||
groupSettings,
|
||||
idMapFileNames,
|
||||
idMapSchema,
|
||||
idMapGroups,
|
||||
|
||||
getLayoutById,
|
||||
getLayoutByIdAndGroup,
|
||||
getLayoutsByGroup,
|
||||
getGroupSetting,
|
||||
getAllGroups,
|
||||
getAllLayouts,
|
||||
|
||||
loading,
|
||||
error,
|
||||
getLayout,
|
||||
|
|
|
|||
|
|
@ -1,41 +1,32 @@
|
|||
'use client'
|
||||
import React, { useMemo } from 'react';
|
||||
import { useLayout } from '../context/LayoutContext';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '@/store/store';
|
||||
|
||||
interface LayoutInfo {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
json_schema: any;
|
||||
group: string;
|
||||
}
|
||||
|
||||
export const useGroupLayouts = () => {
|
||||
const { layoutSchema, getLayout, loading } = useLayout();
|
||||
const { presentationData } = useSelector((state: RootState) => state.presentationGeneration);
|
||||
const {
|
||||
getLayoutByIdAndGroup,
|
||||
getLayoutsByGroup,
|
||||
getLayout,
|
||||
loading
|
||||
} = useLayout();
|
||||
|
||||
// Get the selected group name from presentation data
|
||||
|
||||
|
||||
|
||||
// Get group-specific layout component with validation
|
||||
const getGroupLayout = useMemo(() => {
|
||||
return (layoutId: string, groupName: string) => {
|
||||
// First check if the layout exists in the current group
|
||||
|
||||
|
||||
const groupLayout = layoutSchema?.filter(layout => layout.group === groupName)
|
||||
if (groupLayout) {
|
||||
const layout = getLayoutByIdAndGroup(layoutId, groupName);
|
||||
if (layout) {
|
||||
return getLayout(layoutId);
|
||||
}
|
||||
|
||||
// If layout not found in group, return null
|
||||
console.warn(`Layout ${layoutId} not found in group ${groupName} `);
|
||||
console.warn(`Layout ${layoutId} not found in group ${groupName}`);
|
||||
return null;
|
||||
};
|
||||
}, [getLayout]);
|
||||
}, [getLayoutByIdAndGroup, getLayout]);
|
||||
|
||||
const getGroupLayouts = useMemo(() => {
|
||||
return (groupName: string) => {
|
||||
return getLayoutsByGroup(groupName);
|
||||
};
|
||||
}, [getLayoutsByGroup]);
|
||||
|
||||
// Render slide content with group validation
|
||||
const renderSlideContent = useMemo(() => {
|
||||
|
|
@ -56,6 +47,7 @@ export const useGroupLayouts = () => {
|
|||
|
||||
return {
|
||||
getGroupLayout,
|
||||
getGroupLayouts,
|
||||
renderSlideContent,
|
||||
loading
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,45 +21,40 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
|
|||
selectedLayoutGroup,
|
||||
onSelectLayoutGroup
|
||||
}) => {
|
||||
const { layoutSchema, groupSettings, getLayout, loading } = useLayout();
|
||||
const {
|
||||
getLayoutsByGroup,
|
||||
getGroupSetting,
|
||||
getAllGroups,
|
||||
getLayout,
|
||||
loading
|
||||
} = useLayout();
|
||||
|
||||
// Convert layoutSchema to grouped format using actual group settings
|
||||
const layoutGroups: LayoutGroup[] = React.useMemo(() => {
|
||||
if (!layoutSchema || layoutSchema.length === 0) return [];
|
||||
const groups = getAllGroups();
|
||||
|
||||
// Group layouts by their group property
|
||||
const groupMap = new Map<string, any[]>();
|
||||
layoutSchema.forEach(layout => {
|
||||
const groupName = layout.group || 'default';
|
||||
if (!groupMap.has(groupName)) {
|
||||
groupMap.set(groupName, []);
|
||||
}
|
||||
groupMap.get(groupName)?.push(layout);
|
||||
});
|
||||
if (groups.length === 0) return [];
|
||||
|
||||
// Convert to LayoutGroup format using actual group settings
|
||||
const groups: LayoutGroup[] = [];
|
||||
groupMap.forEach((layouts, groupName) => {
|
||||
const settings = groupSettings[groupName];
|
||||
const Groups: LayoutGroup[] = groups.map(groupName => {
|
||||
const layouts = getLayoutsByGroup(groupName);
|
||||
const settings = getGroupSetting(groupName);
|
||||
|
||||
const group: LayoutGroup = {
|
||||
id: settings?.id || groupName,
|
||||
name: settings?.name || groupName.charAt(0).toUpperCase() + groupName.slice(1),
|
||||
return {
|
||||
id: groupName,
|
||||
name: groupName,
|
||||
description: settings?.description || `${groupName} presentation layouts`,
|
||||
ordered: settings?.ordered || false,
|
||||
isDefault: settings?.isDefault || false,
|
||||
slides: layouts.map((layout: any) => layout.id)
|
||||
slides: layouts.map(layout => layout.id)
|
||||
};
|
||||
groups.push(group);
|
||||
});
|
||||
|
||||
// Sort groups to put default first, then by name
|
||||
return groups.sort((a, b) => {
|
||||
return Groups.sort((a, b) => {
|
||||
if (a.isDefault && !b.isDefault) return -1;
|
||||
if (!a.isDefault && b.isDefault) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [layoutSchema, groupSettings]);
|
||||
}, [getAllGroups, getLayoutsByGroup, getGroupSetting]);
|
||||
|
||||
// Auto-select first group when groups are loaded
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@ interface LayoutGroup {
|
|||
const OutlinePage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const router = useRouter();
|
||||
const { layoutSchema } = useLayout();
|
||||
const {
|
||||
getLayoutById,
|
||||
loading: layoutLoading,
|
||||
} = useLayout();
|
||||
|
||||
const { presentation_id, outlines } = useSelector(
|
||||
(state: RootState) => state.presentationGeneration
|
||||
|
|
@ -192,10 +195,9 @@ const OutlinePage = () => {
|
|||
});
|
||||
|
||||
try {
|
||||
// Collect the actual schemas for layouts in the selected group
|
||||
const groupLayoutSchemas = selectedLayoutGroup.slides
|
||||
.map(slideId => {
|
||||
const layout = layoutSchema?.find(l => l.id === slideId);
|
||||
const layout = getLayoutById(slideId);
|
||||
return layout ? {
|
||||
id: layout.id,
|
||||
name: layout.name,
|
||||
|
|
@ -212,7 +214,6 @@ const OutlinePage = () => {
|
|||
slides: groupLayoutSchemas
|
||||
};
|
||||
|
||||
console.log("layoutData", layoutData);
|
||||
const response = await PresentationGenerationApi.presentationPrepare({
|
||||
presentation_id: presentation_id,
|
||||
outlines: outlines,
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
|
|||
try {
|
||||
const repairedJson = jsonrepair(accumulatedChunks);
|
||||
const partialData = JSON.parse(repairedJson);
|
||||
console.log('partialData', partialData)
|
||||
|
||||
if (partialData.slides) {
|
||||
// Check if the length of slides has changed
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ const SidePanel = ({
|
|||
const { currentTheme, currentColors } = useSelector(
|
||||
(state: RootState) => state.theme
|
||||
);
|
||||
console.log('presentationData', presentationData)
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Use the centralized group layouts hook
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import { promises as fs } from 'fs'
|
|||
import path from 'path'
|
||||
|
||||
interface GroupSetting {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ordered: boolean;
|
||||
isDefault?: boolean;
|
||||
|
|
@ -23,7 +21,7 @@ export async function GET() {
|
|||
.filter(item => item.isDirectory())
|
||||
.map(dir => dir.name)
|
||||
|
||||
const allLayouts: { group: string; files: string[]; settings: GroupSetting | null }[] = []
|
||||
const allLayouts: { groupName: string; files: string[]; settings: GroupSetting | null }[] = []
|
||||
|
||||
// Scan each group directory for layout files and settings
|
||||
for (const groupName of groupDirectories) {
|
||||
|
|
@ -50,8 +48,6 @@ export async function GET() {
|
|||
console.warn(`No settings.json found for group ${groupName} or invalid JSON`)
|
||||
// Provide default settings if setting.json is missing or invalid
|
||||
settings = {
|
||||
id: groupName,
|
||||
name: groupName.charAt(0).toUpperCase() + groupName.slice(1),
|
||||
description: `${groupName} presentation layouts`,
|
||||
ordered: false,
|
||||
isDefault: false
|
||||
|
|
@ -60,7 +56,7 @@ export async function GET() {
|
|||
|
||||
if (layoutFiles.length > 0) {
|
||||
allLayouts.push({
|
||||
group: groupName,
|
||||
groupName: groupName,
|
||||
files: layoutFiles,
|
||||
settings: settings
|
||||
})
|
||||
|
|
@ -70,6 +66,7 @@ export async function GET() {
|
|||
// Continue with other groups even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return NextResponse.json(allLayouts)
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import {
|
|||
} from "@/components/ui/popover";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { renderSlideContent } from "@/app/(presentation-generator)/components/slide_config";
|
||||
import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext";
|
||||
import { useGroupLayouts } from "@/app/(presentation-generator)/hooks/useGroupLayouts";
|
||||
|
||||
export const PresentationCard = ({
|
||||
|
|
@ -25,7 +23,6 @@ export const PresentationCard = ({
|
|||
created_at: string;
|
||||
slide: any
|
||||
}) => {
|
||||
console.log('slide', slide)
|
||||
const router = useRouter();
|
||||
const { renderSlideContent } = useGroupLayouts();
|
||||
|
||||
|
|
@ -63,18 +60,7 @@ export const PresentationCard = ({
|
|||
window.location.reload();
|
||||
};
|
||||
|
||||
// const LayoutComponent = useMemo(() => {
|
||||
// const Layout = getLayout(slide.layout);
|
||||
// if (!Layout) {
|
||||
// return () => <div className="flex flex-col items-center justify-center h-full">
|
||||
// Layout not found
|
||||
// </div>;
|
||||
// }
|
||||
// return Layout;
|
||||
// }, [slide.layout, getLayout]);
|
||||
// const slideContent = useMemo(() => {
|
||||
// return <LayoutComponent data={slide.content} />;
|
||||
// }, [LayoutComponent, slide.content]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
123
servers/nextjs/app/layout-preview/[slug]/page.tsx
Normal file
123
servers/nextjs/app/layout-preview/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useGroupLayoutLoader } from '../hooks/useGroupLayoutLoader'
|
||||
import LoadingStates from '../components/LoadingStates'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ArrowLeft, Home } from 'lucide-react'
|
||||
|
||||
const GroupLayoutPreview = () => {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const slug = params.slug as string
|
||||
|
||||
const { layoutGroup, loading, error, retry } = useGroupLayoutLoader(slug)
|
||||
|
||||
// Handle loading state
|
||||
if (loading) {
|
||||
return <LoadingStates type="loading" />
|
||||
}
|
||||
|
||||
// Handle error state
|
||||
if (error) {
|
||||
return <LoadingStates type="error" message={error} onRetry={retry} />
|
||||
}
|
||||
|
||||
// Handle empty state
|
||||
if (!layoutGroup || layoutGroup.layouts.length === 0) {
|
||||
return <LoadingStates type="empty" onRetry={retry} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b sticky top-0 z-30">
|
||||
<div className="max-w-7xl mx-auto px-6 py-6">
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.back()}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push('/layout-preview')}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
All Groups
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900 capitalize">
|
||||
{layoutGroup.groupName} Layouts
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
{layoutGroup.layouts.length} layout{layoutGroup.layouts.length !== 1 ? 's' : ''} • {layoutGroup.settings.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Layout Grid */}
|
||||
<main className="max-w-7xl mx-auto px-6 py-8">
|
||||
<div className="space-y-8">
|
||||
{layoutGroup.layouts.map((layout, index) => {
|
||||
const { component: LayoutComponent, sampleData, name, fileName } = layout
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={`${layoutGroup.groupName}-${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">
|
||||
{layoutGroup.groupName}
|
||||
</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>
|
||||
|
||||
{/* Layout Content */}
|
||||
<div className="bg-gray-50">
|
||||
<LayoutComponent data={sampleData} />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<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>{layoutGroup.groupName} • {layoutGroup.layouts.length} components</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GroupLayoutPreview
|
||||
158
servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts
Normal file
158
servers/nextjs/app/layout-preview/hooks/useGroupLayoutLoader.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
'use client'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
|
||||
interface UseGroupLayoutLoaderReturn {
|
||||
layoutGroup: LayoutGroup | null
|
||||
loading: boolean
|
||||
error: string | null
|
||||
retry: () => void
|
||||
}
|
||||
|
||||
export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderReturn => {
|
||||
const [layoutGroup, setLayoutGroup] = useState<LayoutGroup | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const loadGroupLayouts = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setLayoutGroup(null)
|
||||
|
||||
const response = await fetch('/api/layouts')
|
||||
if (!response.ok) {
|
||||
toast({
|
||||
title: 'Error loading layouts',
|
||||
description: response.statusText,
|
||||
})
|
||||
return
|
||||
}
|
||||
const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json()
|
||||
|
||||
// Find the specific group by slug
|
||||
const targetGroupData = groupedLayoutsData.find(
|
||||
group => group.groupName.toLowerCase() === groupSlug.toLowerCase()
|
||||
)
|
||||
|
||||
if (!targetGroupData) {
|
||||
setError(`Group "${groupSlug}" not found`)
|
||||
return
|
||||
}
|
||||
|
||||
const groupLayouts: LayoutInfo[] = []
|
||||
|
||||
// Use settings from setting.json or provide defaults
|
||||
const groupSettings: GroupSetting = targetGroupData.settings ? targetGroupData.settings : {
|
||||
description: `${targetGroupData.groupName} presentation layouts`,
|
||||
ordered: false,
|
||||
isDefault: false
|
||||
}
|
||||
|
||||
for (const fileName of targetGroupData.files) {
|
||||
try {
|
||||
const layoutName = fileName.replace('.tsx', '').replace('.ts', '')
|
||||
const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`)
|
||||
|
||||
if (!module.default) {
|
||||
toast({
|
||||
title: `${layoutName} has no default export`,
|
||||
description: 'Please ensure the layout file exports a default component',
|
||||
})
|
||||
console.warn(`${layoutName} has no default export`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!module.Schema) {
|
||||
toast({
|
||||
title: `${layoutName} is missing required Schema export`,
|
||||
description: 'Please ensure the layout file exports a Schema',
|
||||
})
|
||||
console.error(`${layoutName} is missing required Schema export`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Use empty object to let schema apply its default values
|
||||
const sampleData = module.Schema.parse({})
|
||||
|
||||
const layoutInfo: LayoutInfo = {
|
||||
name: layoutName,
|
||||
component: module.default,
|
||||
schema: module.Schema,
|
||||
sampleData,
|
||||
fileName,
|
||||
groupName: targetGroupData.groupName
|
||||
}
|
||||
|
||||
groupLayouts.push(layoutInfo)
|
||||
|
||||
} catch (importError) {
|
||||
console.error(`Failed to import ${fileName} from ${targetGroupData.groupName}:`, importError)
|
||||
|
||||
// Try alternative import path
|
||||
try {
|
||||
const layoutName = fileName.replace('.tsx', '').replace('.ts', '')
|
||||
const module = await import(`@/presentation-layouts/${targetGroupData.groupName}/${layoutName}`)
|
||||
|
||||
if (module.default && module.Schema) {
|
||||
const sampleData = module.Schema.parse({})
|
||||
const layoutInfo: LayoutInfo = {
|
||||
name: layoutName,
|
||||
component: module.default,
|
||||
schema: module.Schema,
|
||||
sampleData,
|
||||
fileName,
|
||||
groupName: targetGroupData.groupName
|
||||
}
|
||||
groupLayouts.push(layoutInfo)
|
||||
} else {
|
||||
console.error(`${layoutName} is missing required exports (default component or Schema)`)
|
||||
}
|
||||
} catch (altError) {
|
||||
console.error(`Alternative import also failed for ${fileName} from ${targetGroupData.groupName}:`, altError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groupLayouts.length === 0) {
|
||||
toast({
|
||||
title: 'No valid layouts found',
|
||||
description: `No valid layouts found in "${groupSlug}" group.`,
|
||||
})
|
||||
setError(`No valid layouts found in "${groupSlug}" group.`)
|
||||
} else {
|
||||
setLayoutGroup({
|
||||
groupName: targetGroupData.groupName,
|
||||
layouts: groupLayouts,
|
||||
settings: groupSettings
|
||||
})
|
||||
setError(null)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading group layouts:', error)
|
||||
setError(error instanceof Error ? error.message : 'Failed to load group layouts')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const retry = () => {
|
||||
loadGroupLayouts()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (groupSlug) {
|
||||
loadGroupLayouts()
|
||||
}
|
||||
}, [groupSlug])
|
||||
|
||||
return {
|
||||
layoutGroup,
|
||||
loading,
|
||||
error,
|
||||
retry
|
||||
}
|
||||
}
|
||||
|
|
@ -27,12 +27,10 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
if (!response.ok) {
|
||||
toast({
|
||||
title: 'Error loading layouts',
|
||||
description: response.statusText,
|
||||
|
||||
description: response.statusText,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json()
|
||||
const loadedGroups: LayoutGroup[] = []
|
||||
const allLayouts: LayoutInfo[] = []
|
||||
|
|
@ -40,11 +38,8 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
for (const groupData of groupedLayoutsData) {
|
||||
const groupLayouts: LayoutInfo[] = []
|
||||
|
||||
// Use settings from setting.json or provide defaults
|
||||
const groupSettings: GroupSetting = groupData.settings || {
|
||||
id: groupData.group,
|
||||
name: groupData.group.charAt(0).toUpperCase() + groupData.group.slice(1),
|
||||
description: `${groupData.group} presentation layouts`,
|
||||
const groupSettings: GroupSetting = groupData.settings ? groupData.settings : {
|
||||
description: `${groupData.groupName} presentation layouts`,
|
||||
ordered: false,
|
||||
isDefault: false
|
||||
}
|
||||
|
|
@ -52,7 +47,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
for (const fileName of groupData.files) {
|
||||
try {
|
||||
const layoutName = fileName.replace('.tsx', '').replace('.ts', '')
|
||||
const module = await import(`@/presentation-layouts/${groupData.group}/${layoutName}`)
|
||||
const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`)
|
||||
|
||||
if (!module.default) {
|
||||
toast({
|
||||
|
|
@ -85,19 +80,19 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
schema: module.Schema,
|
||||
sampleData,
|
||||
fileName,
|
||||
group: groupData.group
|
||||
groupName: groupData.groupName
|
||||
}
|
||||
|
||||
groupLayouts.push(layoutInfo)
|
||||
allLayouts.push(layoutInfo)
|
||||
|
||||
} catch (importError) {
|
||||
console.error(`Failed to import ${fileName} from ${groupData.group}:`, importError)
|
||||
console.error(`Failed to import ${fileName} from ${groupData.groupName}:`, importError)
|
||||
|
||||
// Try alternative import path
|
||||
try {
|
||||
const layoutName = fileName.replace('.tsx', '').replace('.ts', '')
|
||||
const module = await import(`@/presentation-layouts/${groupData.group}/${layoutName}`)
|
||||
const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`)
|
||||
|
||||
if (module.default && module.Schema) {
|
||||
// Use empty object to let schema apply its default values
|
||||
|
|
@ -108,7 +103,7 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
schema: module.Schema,
|
||||
sampleData,
|
||||
fileName,
|
||||
group: groupData.group
|
||||
groupName: groupData.groupName
|
||||
}
|
||||
groupLayouts.push(layoutInfo)
|
||||
allLayouts.push(layoutInfo)
|
||||
|
|
@ -116,27 +111,20 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
|
|||
console.error(`${layoutName} is missing required exports (default component or Schema)`)
|
||||
}
|
||||
} catch (altError) {
|
||||
console.error(`Alternative import also failed for ${fileName} from ${groupData.group}:`, altError)
|
||||
console.error(`Alternative import also failed for ${fileName} from ${groupData.groupName}:`, altError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groupLayouts.length > 0) {
|
||||
loadedGroups.push({
|
||||
group: groupData.group,
|
||||
groupName: groupData.groupName,
|
||||
layouts: groupLayouts,
|
||||
settings: groupSettings
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort groups to put default first, then by name
|
||||
loadedGroups.sort((a, b) => {
|
||||
if (a.settings.isDefault && !b.settings.isDefault) return -1
|
||||
if (!a.settings.isDefault && b.settings.isDefault) return 1
|
||||
return a.settings.name.localeCompare(b.settings.name)
|
||||
})
|
||||
|
||||
if (allLayouts.length === 0) {
|
||||
toast({
|
||||
title: 'No valid layouts found',
|
||||
|
|
|
|||
|
|
@ -1,29 +1,15 @@
|
|||
'use client'
|
||||
import React, { useRef } from 'react'
|
||||
import React from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useLayoutLoader } from './hooks/useLayoutLoader'
|
||||
import LoadingStates from './components/LoadingStates'
|
||||
import { Card } from '@/components/ui/card'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
|
||||
const LayoutPreview = () => {
|
||||
const { layoutGroups, layouts, loading, error, retry } = useLayoutLoader()
|
||||
const sectionRefs = useRef<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
|
||||
}
|
||||
const router = useRouter()
|
||||
|
||||
// Handle loading state
|
||||
if (loading) {
|
||||
|
|
@ -42,8 +28,7 @@ const LayoutPreview = () => {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm border-b sticky top-0 z-30">
|
||||
<div 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-3xl font-bold text-gray-900">Layout Preview</h1>
|
||||
|
|
@ -53,94 +38,51 @@ const LayoutPreview = () => {
|
|||
</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">
|
||||
{/* Group Navigation Cards */}
|
||||
<div className="border-t bg-gray-50 min-h-screen flex justify-center items-center">
|
||||
<div className="max-w-7xl mx-auto px-6 py-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{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"
|
||||
<Card
|
||||
key={group.groupName}
|
||||
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
|
||||
onClick={() => router.push(`/layout-preview/${group.groupName}`)}
|
||||
>
|
||||
<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 className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
|
||||
{group.groupName}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
|
||||
{group.layouts.length}
|
||||
</span>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
{group.settings.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">
|
||||
{group.layouts.length} layout{group.layouts.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
{group.settings.isDefault && (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</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>
|
||||
|
||||
{/* Group Layouts Grid */}
|
||||
<div className="space-y-8">
|
||||
{group.layouts.map((layout, index) => {
|
||||
const { component: LayoutComponent, sampleData, name, fileName } = layout
|
||||
|
||||
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>
|
||||
|
||||
{/* Layout Content */}
|
||||
<div className="bg-gray-50">
|
||||
<LayoutComponent data={sampleData} />
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
/**
|
||||
* Shared types for the Layout Preview system
|
||||
*/
|
||||
|
||||
export interface LayoutInfo {
|
||||
name: string
|
||||
|
|
@ -8,25 +5,23 @@ export interface LayoutInfo {
|
|||
schema: any
|
||||
sampleData: any
|
||||
fileName: string
|
||||
group: string
|
||||
groupName: string
|
||||
}
|
||||
|
||||
export interface GroupSetting {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ordered: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface LayoutGroup {
|
||||
group: string
|
||||
groupName: string
|
||||
layouts: LayoutInfo[]
|
||||
settings: GroupSetting
|
||||
}
|
||||
|
||||
export interface GroupedLayoutsResponse {
|
||||
group: string
|
||||
groupName: string
|
||||
files: string[]
|
||||
settings: GroupSetting | null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"id": "default",
|
||||
"name": "Default",
|
||||
"description": "Default layout for presentations",
|
||||
"ordered": false,
|
||||
"isDefault": true
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
export interface LayoutGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ordered: boolean;
|
||||
isDefault?: boolean;
|
||||
slides: string[];
|
||||
}
|
||||
|
||||
export const ProfessionalLayoutGroup: LayoutGroup = {
|
||||
id: 'professional',
|
||||
name: 'Professional',
|
||||
description: 'Clean, corporate designs perfect for business presentations',
|
||||
ordered: true,
|
||||
isDefault: true,
|
||||
slides: [
|
||||
'first-slide',
|
||||
'content-slide',
|
||||
'bullet-point-slide',
|
||||
'comparison-slide',
|
||||
'type4-slide',
|
||||
'statistics-slide',
|
||||
'team-slide',
|
||||
'quote-slide'
|
||||
]
|
||||
}
|
||||
|
||||
export const CreativeLayoutGroup: LayoutGroup = {
|
||||
id: 'creative',
|
||||
name: 'Creative',
|
||||
description: 'Vibrant, artistic layouts for innovative and creative presentations',
|
||||
ordered: false,
|
||||
slides: [
|
||||
'image-slide',
|
||||
'icon-slide',
|
||||
'card-slide',
|
||||
'type1-slide',
|
||||
'type2-slide',
|
||||
'type3-slide',
|
||||
'process-slide'
|
||||
]
|
||||
}
|
||||
|
||||
export const ModernLayoutGroup: LayoutGroup = {
|
||||
id: 'modern',
|
||||
name: 'Modern',
|
||||
description: 'Contemporary designs with clean lines and sophisticated layouts',
|
||||
ordered: true,
|
||||
slides: [
|
||||
'type5-slide',
|
||||
'type6-slide',
|
||||
'type7-slide',
|
||||
'type8-slide',
|
||||
'timeline-slide',
|
||||
'type2-timeline-slide',
|
||||
'number-box-slide'
|
||||
]
|
||||
}
|
||||
|
||||
export const MinimalLayoutGroup: LayoutGroup = {
|
||||
id: 'minimal',
|
||||
name: 'Minimal',
|
||||
description: 'Simple, focused layouts that emphasize content over decoration',
|
||||
ordered: false,
|
||||
slides: [
|
||||
'content-slide',
|
||||
'bullet-point-slide',
|
||||
'type2-numbered-slide',
|
||||
'quote-slide',
|
||||
'statistics-slide'
|
||||
]
|
||||
}
|
||||
|
||||
export const LayoutGroups = [
|
||||
ProfessionalLayoutGroup,
|
||||
CreativeLayoutGroup,
|
||||
ModernLayoutGroup,
|
||||
MinimalLayoutGroup
|
||||
];
|
||||
|
||||
export const getDefaultLayoutGroup = (): LayoutGroup => {
|
||||
return LayoutGroups.find(group => group.isDefault) || ProfessionalLayoutGroup;
|
||||
};
|
||||
|
||||
export const getAllLayouts = (): string[] => {
|
||||
const allLayouts = new Set<string>();
|
||||
LayoutGroups.forEach(group => {
|
||||
group.slides.forEach(slide => allLayouts.add(slide));
|
||||
});
|
||||
return Array.from(allLayouts);
|
||||
};
|
||||
|
||||
export const getGroupByLayoutId = (layoutId: string): LayoutGroup | undefined => {
|
||||
return LayoutGroups.find(group => group.slides.includes(layoutId));
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ const ComparisonSlideLayout: React.FC<ComparisonSlideLayoutProps> = ({ data: sli
|
|||
|
||||
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"
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-white 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',
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const NumberBoxSlideLayout: React.FC<NumberBoxSlideLayoutProps> = ({ data: slide
|
|||
// const data = numberBoxSlideSchema.parse(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">
|
||||
<div className="relative w-full aspect-[16/9] flex flex-col bg-white overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300">
|
||||
{/* Subtle background pattern */}
|
||||
<div className="absolute inset-0 opacity-[0.02] print:opacity-[0.01]">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-gray-200 to-transparent transform -rotate-45 scale-150"></div>
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const TimelineSlideLayout: React.FC<TimelineSlideLayoutProps> = ({ data: slideDa
|
|||
|
||||
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"
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-white 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',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"id": "modern",
|
||||
"name": "Modern",
|
||||
"description": "Contemporary designs with clean lines and sophisticated layouts",
|
||||
"ordered": false,
|
||||
"isDefault": false
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const BulletPointSlideLayout: React.FC<BulletPointSlideLayoutProps> = ({ data: s
|
|||
|
||||
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"
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-white overflow-hidden shadow-2xl border border-slate-200"
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const ContentSlideLayout: React.FC<ContentSlideLayoutProps> = ({ 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"
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-white overflow-hidden shadow-2xl border border-slate-200"
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const FirstSlideLayout: React.FC<FirstSlideLayoutProps> = ({ 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"
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-white overflow-hidden shadow-2xl border border-slate-200"
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ interface ImageSlideLayoutProps {
|
|||
const ImageSlideLayout: React.FC<ImageSlideLayoutProps> = ({ data: slideData }) => {
|
||||
|
||||
return (
|
||||
<div className="relative w-full aspect-[16/9] flex bg-gradient-to-br from-slate-50 to-slate-100 overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300">
|
||||
<div className="relative w-full aspect-[16/9] flex bg-white overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300">
|
||||
{/* Left panel - Image */}
|
||||
<div className="flex-1 relative">
|
||||
<img
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ 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"
|
||||
className="relative w-full aspect-[16/9] flex flex-col bg-white overflow-hidden shadow-2xl border border-slate-200"
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
|
|||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-[16/9] 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"
|
||||
className="relative w-full aspect-[16/9] bg-white overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
|
||||
style={slideData?.backgroundImage ? {
|
||||
backgroundImage: `url("${slideData.backgroundImage.__image_url__}")`,
|
||||
backgroundSize: 'cover',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"id": "professional",
|
||||
"name": "Professional",
|
||||
"description": "Clean, corporate designs perfect for business presentations",
|
||||
"ordered": false
|
||||
"ordered": false,
|
||||
"isDefault": false
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue