feat(Nextjs): Custom Layout support for layout context & font loading in Custom layouts

This commit is contained in:
shiva raj badu 2025-08-02 14:33:42 +05:45
parent 6fc94648c4
commit 2622f2846b
No known key found for this signature in database
17 changed files with 1177 additions and 1786 deletions

View file

@ -1,74 +1,90 @@
import { Trash2 } from 'lucide-react';
import React from 'react'
import { useDispatch } from 'react-redux';
import { addNewSlide } from '@/store/slices/presentationGeneration';
import { Loader2 } from 'lucide-react';
import { useGroupLayoutLoader } from '@/app/layout-preview/hooks/useGroupLayoutLoader';
import { v4 as uuidv4 } from 'uuid';
import { toast } from 'sonner';
import { Trash2 } from "lucide-react";
import React from "react";
import { useDispatch } from "react-redux";
import { addNewSlide } from "@/store/slices/presentationGeneration";
import { Loader2 } from "lucide-react";
// import { useGroupLayoutLoader } from '@/app/layout-preview/hooks/useGroupLayoutLoader';
import { useLayout, FullDataInfo } from "../context/LayoutContext";
import { v4 as uuidv4 } from "uuid";
import { toast } from "sonner";
interface NewSlideProps {
setShowNewSlideSelection: (show: boolean) => void;
group: string;
index: number;
presentationId: string;
setShowNewSlideSelection: (show: boolean) => void;
group: string;
index: number;
presentationId: string;
}
const NewSlide = ({ setShowNewSlideSelection, group, index, presentationId }: NewSlideProps) => {
const dispatch = useDispatch();
const handleNewSlide = (sampleData: any, id: string) => {
try {
const newSlide = {
id: uuidv4(),
index: index,
content: sampleData,
layout_group: group,
layout: `${group}:${id}`,
presentation: presentationId
}
dispatch(addNewSlide({ slideData: newSlide, index }));
setShowNewSlideSelection(false);
} catch (error: any) {
console.error(error)
toast.error('Error adding new slide')
}
}
const { layoutGroup, loading } = useGroupLayoutLoader(group)
if (loading) {
return (
<div className='my-6 w-full bg-gray-50 p-8 max-w-[1280px]'>
<div className='flex justify-between items-center mb-8'>
<h2 className="text-2xl font-semibold">Select a Slide Layout</h2>
<Trash2 onClick={() => setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' />
</div>
<div className='flex items-center justify-center h-32'>
<Loader2 className="w-8 h-8 animate-spin text-gray-500" />
</div>
</div>
)
const NewSlide = ({
setShowNewSlideSelection,
group,
index,
presentationId,
}: NewSlideProps) => {
const dispatch = useDispatch();
const handleNewSlide = (sampleData: any, id: string) => {
try {
const newSlide = {
id: uuidv4(),
index: index,
content: sampleData,
layout_group: group,
layout: id,
presentation: presentationId,
};
dispatch(addNewSlide({ slideData: newSlide, index }));
setShowNewSlideSelection(false);
} catch (error: any) {
console.error(error);
toast.error("Error adding new slide");
}
};
const { getFullDataByGroup, loading } = useLayout();
const fullData = getFullDataByGroup(group);
if (loading) {
return (
<div className='my-6 w-full bg-gray-50 p-8 max-w-[1280px]'>
<div className='flex justify-between items-center mb-8'>
<h2 className="text-2xl font-semibold">Select a Slide Layout</h2>
<Trash2 onClick={() => setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' />
</div>
<div className='grid grid-cols-4 gap-4'>
{layoutGroup && layoutGroup?.layouts.map((layout: any, index: number) => {
const { component: LayoutComponent, sampleData, layoutId } = layout
return (
<div onClick={() => handleNewSlide(sampleData, layoutId)} key={`${layoutGroup?.groupName}-${index}`} className=" relative cursor-pointer overflow-hidden aspect-video">
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
)
})}
</div>
<div className="my-6 w-full bg-gray-50 p-8 max-w-[1280px]">
<div className="flex justify-between items-center mb-8">
<h2 className="text-2xl font-semibold">Select a Slide Layout</h2>
<Trash2
onClick={() => setShowNewSlideSelection(false)}
className="text-gray-500 text-2xl cursor-pointer"
/>
</div>
)
}
<div className="flex items-center justify-center h-32">
<Loader2 className="w-8 h-8 animate-spin text-gray-500" />
</div>
</div>
);
}
export default NewSlide
return (
<div className="my-6 w-full bg-gray-50 p-8 max-w-[1280px]">
<div className="flex justify-between items-center mb-8">
<h2 className="text-2xl font-semibold">Select a Slide Layout</h2>
<Trash2
onClick={() => setShowNewSlideSelection(false)}
className="text-gray-500 text-2xl cursor-pointer"
/>
</div>
<div className="grid grid-cols-4 gap-4">
{fullData.map((layout: FullDataInfo, index: number) => {
const { component: LayoutComponent, sampleData, layoutId } = layout;
return (
<div
onClick={() => handleNewSlide(sampleData, layoutId)}
key={`${group}-${index}`}
className=" relative cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
);
})}
</div>
</div>
);
};
export default NewSlide;

View file

@ -19,6 +19,15 @@ export interface LayoutInfo {
json_schema: any;
groupName: string;
}
export interface FullDataInfo {
name: string;
component: React.ComponentType<any>;
schema: any;
sampleData: any;
fileName: string;
groupName: string;
layoutId: string;
}
export interface GroupSetting {
description: string;
@ -39,6 +48,7 @@ export interface LayoutData {
fileMap: Map<string, { fileName: string; groupName: string }>;
groupedLayouts: Map<string, LayoutInfo[]>;
layoutSchema: LayoutInfo[];
fullDataByGroup: Map<string, FullDataInfo[]>;
}
export interface LayoutContextType {
@ -51,7 +61,7 @@ export interface LayoutContextType {
getGroupSetting: (groupName: string) => GroupSetting | null;
getAllGroups: () => string[];
getAllLayouts: () => LayoutInfo[];
getFullDataByGroup: (groupName: string) => FullDataInfo[];
loading: boolean;
error: string | null;
getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null;
@ -119,6 +129,7 @@ export const LayoutProvider: React.FC<{
const groupSettingsMap = new Map<string, GroupSetting>();
const fileMap = new Map<string, { fileName: string; groupName: string }>();
const groupedLayouts = new Map<string, LayoutInfo[]>();
const fullDataByGroup = new Map<string, FullDataInfo[]>();
// Start preloading process
setIsPreloading(true);
@ -130,6 +141,8 @@ export const LayoutProvider: React.FC<{
layoutsByGroup.set(groupData.groupName, new Set());
}
fullDataByGroup.set(groupData.groupName, []);
// group settings or default settings
const settings = groupData.settings || {
description: `${groupData.groupName} presentation layouts`,
@ -139,6 +152,7 @@ export const LayoutProvider: React.FC<{
groupSettingsMap.set(groupData.groupName, settings);
const groupLayouts: LayoutInfo[] = [];
const groupFullData: FullDataInfo[] = [];
for (const fileName of groupData.files) {
try {
@ -194,6 +208,18 @@ export const LayoutProvider: React.FC<{
groupName: groupData.groupName,
};
const sampleData = module.Schema.parse({});
const fullData: FullDataInfo = {
name: layoutName,
component: module.default,
schema: jsonSchema,
sampleData: sampleData,
fileName,
groupName: groupData.groupName,
layoutId: uniqueKey,
};
groupFullData.push(fullData);
layoutsById.set(uniqueKey, layout);
layoutsByGroup.get(groupData.groupName)!.add(uniqueKey);
fileMap.set(uniqueKey, {
@ -210,6 +236,7 @@ export const LayoutProvider: React.FC<{
}
}
fullDataByGroup.set(groupData.groupName, groupFullData);
// Cache grouped layouts
groupedLayouts.set(groupData.groupName, groupLayouts);
}
@ -224,6 +251,7 @@ export const LayoutProvider: React.FC<{
fileMap,
groupedLayouts,
layoutSchema: layouts,
fullDataByGroup,
};
};
@ -269,6 +297,10 @@ export const LayoutProvider: React.FC<{
customLayouts.groupedLayouts
),
layoutSchema: [...data.layoutSchema, ...customLayouts.layoutSchema],
fullDataByGroup: mergeMaps(
data.fullDataByGroup,
customLayouts.fullDataByGroup
),
};
setLayoutData(combinedData);
@ -301,7 +333,7 @@ export const LayoutProvider: React.FC<{
const groupSettingsMap = new Map<string, GroupSetting>();
const fileMap = new Map<string, { fileName: string; groupName: string }>();
const groupedLayouts = new Map<string, LayoutInfo[]>();
const fullDataByGroup = new Map<string, FullDataInfo[]>();
try {
const customGroupResponse = await fetch(
"/api/v1/ppt/layout-management/summary"
@ -312,6 +344,7 @@ export const LayoutProvider: React.FC<{
console.log("🔍 customGroup", customGroup);
for (const group of customGroup) {
const groupName = `custom-${group.presentation_id}`;
fullDataByGroup.set(groupName, []);
if (!layoutsByGroup.has(groupName)) {
layoutsByGroup.set(groupName, new Set());
}
@ -330,6 +363,7 @@ export const LayoutProvider: React.FC<{
groupSettingsMap.set(`custom-${presentationId}`, settings);
const groupLayouts: LayoutInfo[] = [];
const groupFullData: FullDataInfo[] = [];
for (const i of allLayout) {
/* ---------- 1. compile JSX to plain script ------------------ */
@ -383,6 +417,17 @@ export const LayoutProvider: React.FC<{
json_schema: jsonSchema,
groupName: groupName,
};
const sampleData = module.Schema.parse({});
const fullData: FullDataInfo = {
name: layoutName,
component: module.default,
schema: jsonSchema,
sampleData: sampleData,
fileName: i.layout_name,
groupName: groupName,
layoutId: uniqueKey,
};
groupFullData.push(fullData);
layoutsById.set(uniqueKey, layout);
layoutsByGroup.get(groupName)!.add(uniqueKey);
@ -395,6 +440,7 @@ export const LayoutProvider: React.FC<{
}
// Cache grouped layouts
groupedLayouts.set(groupName, groupLayouts);
fullDataByGroup.set(groupName, groupFullData);
}
} catch (err: any) {
console.error("Compilation error:", err);
@ -407,6 +453,7 @@ export const LayoutProvider: React.FC<{
fileMap,
groupedLayouts,
layoutSchema: layouts,
fullDataByGroup,
};
};
@ -489,6 +536,10 @@ export const LayoutProvider: React.FC<{
return layoutData?.layoutSchema || [];
};
const getFullDataByGroup = (groupName: string): FullDataInfo[] => {
return layoutData?.fullDataByGroup.get(groupName) || [];
};
// Load layouts on mount
useEffect(() => {
loadLayouts();
@ -501,7 +552,7 @@ export const LayoutProvider: React.FC<{
getGroupSetting,
getAllGroups,
getAllLayouts,
getFullDataByGroup,
loading,
error,
getLayout,

View file

@ -1,7 +1,8 @@
import { CheckCircle } from "lucide-react";
import React from "react";
import { LayoutGroup } from "../types/index";
import { useGroupLayoutLoader } from "@/app/layout-preview/hooks/useGroupLayoutLoader";
import { useLayout } from "../../context/LayoutContext";
// import { useGroupLayoutLoader } from "@/app/layout-preview/hooks/useGroupLayoutLoader";
interface GroupLayoutsProps {
group: LayoutGroup;
onSelectLayoutGroup: (group: LayoutGroup) => void;
@ -13,7 +14,8 @@ const GroupLayouts: React.FC<GroupLayoutsProps> = ({
onSelectLayoutGroup,
selectedLayoutGroup,
}) => {
const { layoutGroup } = useGroupLayoutLoader(group.id);
const { getFullDataByGroup } = useLayout();
const layoutGroup = getFullDataByGroup(group.id);
return (
<div
onClick={() => onSelectLayoutGroup(group)}
@ -39,11 +41,16 @@ const GroupLayouts: React.FC<GroupLayoutsProps> = ({
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2 mb-3 min-h-[300px]">
{layoutGroup &&
layoutGroup?.layouts.slice(0, 4).map((layout: any, index: number) => {
const { component: LayoutComponent, sampleData, layoutId } = layout;
layoutGroup?.slice(0, 4).map((layout: any, index: number) => {
const {
component: LayoutComponent,
sampleData,
layoutId,
groupName,
} = layout;
return (
<div
key={`${layoutGroup?.groupName}-${index}`}
key={`${groupName}-${index}`}
className=" relative cursor-pointer overflow-hidden aspect-video"
>
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
@ -56,7 +63,7 @@ const GroupLayouts: React.FC<GroupLayoutsProps> = ({
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>{layoutGroup?.layouts.length} layouts</span>
<span>{layoutGroup?.length} layouts</span>
<span
className={`px-2 py-1 rounded text-xs ${
group.ordered

View file

@ -1,540 +0,0 @@
"use client";
import React, { useRef, useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import {
X,
Pencil,
Eraser,
RotateCcw,
Download,
SendHorizontal,
} from "lucide-react";
import html2canvas from "html2canvas";
interface DrawingCanvasProps {
slideElement: HTMLElement | null;
onClose: () => void;
slideNumber: number;
onSlideUpdate: (updatedSlide: any) => void;
}
const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
slideElement,
onClose,
slideNumber,
onSlideUpdate,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const slideDisplayRef = useRef<HTMLDivElement>(null);
const slideContentRef = useRef<HTMLDivElement>(null);
const [strokeWidth, setStrokeWidth] = useState(3);
const [strokeColor, setStrokeColor] = useState("#000000");
const [eraserMode, setEraserMode] = useState(false);
const [isDrawing, setIsDrawing] = useState(false);
const [prompt, setPrompt] = useState("");
const [isUpdating, setIsUpdating] = useState(false);
const [slideHtml, setSlideHtml] = useState("");
const [canvasDimensions, setCanvasDimensions] = useState({
width: 800,
height: 600,
});
useEffect(() => {
if (slideElement && containerRef.current) {
console.log("slideElement", slideElement);
const rect = slideElement.getBoundingClientRect();
// Set canvas dimensions to match the slide element
setCanvasDimensions({
width: Math.max(rect.width, 800),
height: Math.max(rect.height, 600),
});
// Store the HTML once to prevent re-renders
setSlideHtml(slideElement.innerHTML);
}
}, [slideElement]);
// Apply optimizations once after slide content is rendered
useEffect(() => {
if (slideContentRef.current && slideHtml) {
const slideContent = slideContentRef.current;
// Apply styles to prevent interactions and flickering
slideContent.style.pointerEvents = "none";
slideContent.style.userSelect = "none";
slideContent.style.transform = "translateZ(0)";
slideContent.style.willChange = "auto";
slideContent.style.backfaceVisibility = "hidden";
// Target all interactive elements
const interactiveElements = slideContent.querySelectorAll(
"img, video, iframe, a, button, input, textarea, select"
);
interactiveElements.forEach((element) => {
const el = element as HTMLElement;
el.style.pointerEvents = "none";
el.style.userSelect = "none";
(el.style as any).webkitUserSelect = "none";
(el.style as any).webkitTouchCallout = "none";
(el.style as any).webkitUserDrag = "none";
el.style.transform = "translateZ(0)";
el.style.backfaceVisibility = "hidden";
if (element.tagName === "IMG") {
(element as HTMLImageElement).draggable = false;
}
// Remove any event listeners
el.onclick = null;
el.onmousedown = null;
el.onmouseup = null;
el.onmousemove = null;
});
}
}, [slideHtml]);
const getCanvasContext = () => {
const canvas = canvasRef.current;
if (!canvas) return null;
return canvas.getContext("2d");
};
const getMousePos = (e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) return { x: 0, y: 0 };
const rect = canvas.getBoundingClientRect();
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
};
const getTouchPos = (e: React.TouchEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current;
if (!canvas) return { x: 0, y: 0 };
const rect = canvas.getBoundingClientRect();
const touch = e.touches[0];
return {
x: touch.clientX - rect.left,
y: touch.clientY - rect.top,
};
};
const startDrawing = useCallback(
(pos: { x: number; y: number }) => {
const ctx = getCanvasContext();
if (!ctx) return;
setIsDrawing(true);
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
if (eraserMode) {
ctx.globalCompositeOperation = "destination-out";
ctx.lineWidth = strokeWidth * 2;
} else {
ctx.globalCompositeOperation = "source-over";
ctx.strokeStyle = strokeColor;
ctx.lineWidth = strokeWidth;
}
ctx.lineCap = "round";
ctx.lineJoin = "round";
},
[eraserMode, strokeColor, strokeWidth]
);
const draw = useCallback(
(pos: { x: number; y: number }) => {
if (!isDrawing) return;
const ctx = getCanvasContext();
if (!ctx) return;
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
},
[isDrawing]
);
const stopDrawing = useCallback(() => {
setIsDrawing(false);
}, []);
// Mouse events
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
e.preventDefault();
const pos = getMousePos(e);
startDrawing(pos);
};
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
e.preventDefault();
const pos = getMousePos(e);
draw(pos);
};
const handleMouseUp = (e: React.MouseEvent<HTMLCanvasElement>) => {
e.preventDefault();
stopDrawing();
};
// Touch events
const handleTouchStart = (e: React.TouchEvent<HTMLCanvasElement>) => {
e.preventDefault();
const pos = getTouchPos(e);
startDrawing(pos);
};
const handleTouchMove = (e: React.TouchEvent<HTMLCanvasElement>) => {
e.preventDefault();
const pos = getTouchPos(e);
draw(pos);
};
const handleTouchEnd = (e: React.TouchEvent<HTMLCanvasElement>) => {
e.preventDefault();
stopDrawing();
};
const handleClearCanvas = () => {
const canvas = canvasRef.current;
const ctx = getCanvasContext();
if (!canvas || !ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
};
// Convert data URL to blob for form data
const dataURLToBlob = (dataURL: string): Blob => {
const parts = dataURL.split(",");
const contentType = parts[0].match(/:(.*?);/)?.[1] || "image/png";
const raw = window.atob(parts[1]);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
};
const handleSave = async () => {
if (!slideElement || !canvasRef.current || !slideDisplayRef.current) return;
if (!prompt.trim()) {
alert("Please enter a prompt before saving.");
return;
}
setIsUpdating(true);
try {
// Take screenshot of the slide display area (slide only)
const slideOnly = await html2canvas(slideDisplayRef.current, {
backgroundColor: "#ffffff",
scale: 1,
logging: false,
useCORS: true,
ignoreElements: (element) => {
// Ignore the canvas element when taking screenshot of slide only
return element.tagName === "CANVAS";
},
});
// Take screenshot of the entire slide display area including canvas
const slideWithCanvas = await html2canvas(slideDisplayRef.current, {
backgroundColor: "#ffffff",
scale: 1,
logging: false,
useCORS: true,
});
// Get the current HTML content from the original slide element
const currentHtml = slideElement.innerHTML;
// Convert canvas images to blobs
const currentUiImageBlob = dataURLToBlob(
slideOnly.toDataURL("image/png")
);
const sketchImageBlob = dataURLToBlob(
slideWithCanvas.toDataURL("image/png")
);
// Prepare form data
const formData = new FormData();
formData.append(
"current_ui_image",
currentUiImageBlob,
`slide-${slideNumber}-current.png`
);
formData.append(
"sketch_image",
sketchImageBlob,
`slide-${slideNumber}-sketch.png`
);
formData.append("html", currentHtml);
formData.append("prompt", prompt);
// Call the API
const response = await fetch("/api/v1/ppt/html-edit/", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`API call failed: ${response.statusText}`);
}
const data = await response.json();
// Update the slide with new data
onSlideUpdate({
slide_number: slideNumber,
html: data.edited_html || currentHtml,
processed: true,
processing: false,
error: undefined,
});
// Close the drawing canvas
onClose();
} catch (error) {
console.error("Error updating slide:", error);
alert(
`Error updating slide: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
} finally {
setIsUpdating(false);
}
};
const handleEraserModeChange = (isEraser: boolean) => {
setEraserMode(isEraser);
};
const handleStrokeColorChange = (color: string) => {
setStrokeColor(color);
setEraserMode(false); // Switch back to draw mode when selecting color
};
const handleStrokeWidthChange = (width: number) => {
setStrokeWidth(width);
};
const colors = [
"#000000",
"#FF0000",
"#00FF00",
"#0000FF",
"#FFFF00",
"#FF00FF",
"#00FFFF",
"#FFA500",
];
const strokeWidths = [1, 3, 5, 8, 12];
return (
<div
ref={containerRef}
className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4"
onClick={(e) => {
if (e.target === e.currentTarget) {
onClose();
}
}}
>
<div className="relative bg-white rounded-lg shadow-xl max-w-[95vw] max-h-[95vh] overflow-hidden flex flex-col">
{/* Controls */}
<div className="flex items-center justify-between p-4 border-b bg-gray-50 flex-shrink-0">
<div className="flex items-center gap-4 flex-wrap">
<h3 className="text-lg font-semibold">Edit Slide {slideNumber}</h3>
{/* Drawing Tools */}
<div className="flex items-center gap-2">
<Button
variant={!eraserMode ? "default" : "outline"}
size="sm"
onClick={() => handleEraserModeChange(false)}
className="flex items-center gap-1"
>
<Pencil size={16} />
Draw
</Button>
<Button
variant={eraserMode ? "default" : "outline"}
size="sm"
onClick={() => handleEraserModeChange(true)}
className="flex items-center gap-1"
>
<Eraser size={16} />
Erase
</Button>
</div>
{/* Color Picker */}
{!eraserMode && (
<div className="flex items-center gap-1">
{colors.map((color) => (
<button
key={color}
className={`w-6 h-6 rounded-full border-2 ${
strokeColor === color
? "border-gray-800"
: "border-gray-300"
}`}
style={{ backgroundColor: color }}
onClick={() => handleStrokeColorChange(color)}
/>
))}
</div>
)}
{/* Stroke Width */}
<div className="flex items-center gap-1">
{strokeWidths.map((width) => (
<button
key={width}
className={`w-8 h-8 rounded border flex items-center justify-center ${
strokeWidth === width
? "bg-blue-100 border-blue-500"
: "border-gray-300"
}`}
onClick={() => handleStrokeWidthChange(width)}
>
<div
className="rounded-full bg-gray-800"
style={{
width: `${width + 2}px`,
height: `${width + 2}px`,
}}
/>
</button>
))}
</div>
<Button
variant="outline"
size="sm"
onClick={handleClearCanvas}
className="flex items-center gap-1"
>
<RotateCcw size={16} />
Clear
</Button>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Button
variant="outline"
onClick={onClose}
className="flex items-center gap-1"
>
<X size={16} />
Close
</Button>
</div>
</div>
{/* Prompt Section */}
<div className="p-4 border-b bg-gray-50 flex-shrink-0">
<div className="space-y-2">
<label
htmlFor="edit-prompt"
className="text-sm font-medium text-gray-700"
>
Describe the changes you want to make:
</label>
<div className="flex gap-2">
<Textarea
id="edit-prompt"
placeholder="Enter your prompt here... (e.g., 'Change the title color to blue', 'Add a border to the image', etc.)"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="flex-1 min-h-[80px] max-h-[80px] resize-none"
disabled={isUpdating}
/>
<Button
onClick={handleSave}
disabled={isUpdating || !prompt.trim()}
className="flex items-center gap-1 bg-green-600 hover:bg-green-700 px-6"
>
{isUpdating ? (
"Updating..."
) : (
<>
<SendHorizontal size={16} />
Update Slide
</>
)}
</Button>
</div>
</div>
</div>
{/* Canvas Area */}
<div className="flex-1 overflow-auto bg-gray-100 p-4">
<div
ref={slideDisplayRef}
className="relative mx-auto bg-white shadow-lg"
style={{
width: canvasDimensions.width,
height: canvasDimensions.height,
}}
>
{/* Slide Background - Static HTML content */}
<div
ref={slideContentRef}
className="absolute inset-0 z-10"
style={{
overflow: "hidden",
isolation: "isolate",
contain: "layout style paint",
}}
dangerouslySetInnerHTML={{
__html: slideHtml,
}}
/>
{/* Drawing Canvas */}
<canvas
ref={canvasRef}
width={canvasDimensions.width}
height={canvasDimensions.height}
style={{
position: "absolute",
top: 0,
left: 0,
zIndex: 20,
cursor: eraserMode ? "grab" : "crosshair",
pointerEvents: "auto",
touchAction: "none",
}}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
</div>
</div>
</div>
);
};
export default DrawingCanvas;

View file

@ -12,6 +12,7 @@ import {
SendHorizontal,
X,
Repeat2,
Trash,
} from "lucide-react";
import React, { useState, useEffect, useRef, useCallback } from "react";
import ToolTip from "@/components/ToolTip";
@ -66,6 +67,53 @@ const EachSlide = ({
}
}
}, [slide.processed, slide.html]);
// Load Google Fonts
useEffect(() => {
if (slide.fonts?.internally_supported_fonts) {
slide.fonts.internally_supported_fonts.forEach((font: any) => {
// Check if font link already exists
const existingFont = document.querySelector(
`link[href="${font.google_fonts_url}"]`
);
// Only add if font doesn't already exist
if (!existingFont) {
const link = document.createElement("link");
link.href = font.google_fonts_url;
link.rel = "stylesheet";
document.head.appendChild(link);
}
});
}
}, [slide.fonts]);
// Load uploaded fonts
useEffect(() => {
if (slide.uploaded_fonts && slide.uploaded_fonts.length > 0) {
slide.uploaded_fonts.forEach((fontUrl: string) => {
// Check if font style already exists
const existingStyle = document.querySelector(
`style[data-font-url="${fontUrl}"]`
);
if (!existingStyle) {
const style = document.createElement("style");
style.setAttribute("data-font-url", fontUrl);
// Extract font name from URL for font-family
const fontName =
fontUrl.split("/").pop()?.split(".")[0] || "CustomFont";
style.textContent = `
@font-face {
font-family: '${fontName}';
src: url('${fontUrl}') format('truetype');
font-display: swap;
}
`;
document.head.appendChild(style);
}
});
}
}, [slide.uploaded_fonts]);
// Set up canvas when entering edit mode
useEffect(() => {
@ -393,6 +441,10 @@ const EachSlide = ({
const strokeWidths = [1, 3, 5, 8, 12];
const handleDeleteSlide = () => {
setSlides((prevSlides) => prevSlides.filter((_, i) => i !== index));
};
return (
<Card
key={slide.slide_number}
@ -413,7 +465,7 @@ const EachSlide = ({
)}
</div>
{!isProcessingPptx && (
{!isProcessingPptx && slide.processed && (
<div className="flex gap-6">
{slide.processed && slide.html && !isEditMode && (
<div className=" ">
@ -429,14 +481,24 @@ const EachSlide = ({
</div>
)}
<div>
<ToolTip content="Retry fetch">
<ToolTip content="Re-Design this slide">
<button
onClick={() => retrySlide(index)}
disabled={slide.processing}
className="px-6 py-2 flex gap-2 text-sm items-center group-hover:scale-105 rounded-lg bg-[#5141e5] hover:shadow-md transition-all duration-300 cursor-pointer shadow-md"
>
<Repeat2 className="w-4 sm:w-5 h-4 sm:h-5 text-white" />
<span className="text-white">Retry Fetch</span>
<span className="text-white">Re-Design</span>
</button>
</ToolTip>
</div>
<div>
<ToolTip content="Delete Slide">
<button
onClick={handleDeleteSlide}
className="px-4 py-2 flex gap-2 text-sm items-center group-hover:scale-105 rounded-lg hover:shadow-md transition-all duration-300 cursor-pointer shadow-md"
>
<Trash className="w-4 sm:w-5 h-4 sm:h-5 text-red-500" />
</button>
</ToolTip>
</div>
@ -592,13 +654,6 @@ const EachSlide = ({
) : slide.processed && slide.html ? (
<div className="relative">
<div ref={slideDisplayRef} className="relative mx-auto w-full ">
{/* <div
ref={slideContentRef}
className="relative"
dangerouslySetInnerHTML={{
__html: slide.html,
}}
/> */}
<div ref={slideContentRef}>
<SlideContent slide={slide} />
</div>

View file

@ -0,0 +1,125 @@
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { CheckCircle, AlertCircle, X } from "lucide-react";
import { toast } from "sonner";
interface UploadedFont {
fontName: string;
fontUrl: string;
fontPath: string;
}
interface FontManagerProps {
slide: any;
onFontsUpdate: (updatedFonts: string[]) => void;
globalUploadedFonts: UploadedFont[];
}
const FontManager: React.FC<FontManagerProps> = ({
slide,
onFontsUpdate,
globalUploadedFonts,
}) => {
const [slideSpecificFonts, setSlideSpecificFonts] = useState<string[]>(
slide.uploaded_fonts || []
);
// Update slide-specific fonts when slide changes
useEffect(() => {
setSlideSpecificFonts(slide.uploaded_fonts || []);
}, [slide.uploaded_fonts]);
const addGlobalFontToSlide = (fontUrl: string) => {
if (!slideSpecificFonts.includes(fontUrl)) {
const updatedFonts = [...slideSpecificFonts, fontUrl];
setSlideSpecificFonts(updatedFonts);
onFontsUpdate(updatedFonts);
toast.success("Font added to slide");
}
};
const removeSlideFont = (fontUrl: string) => {
const updatedFonts = slideSpecificFonts.filter((url) => url !== fontUrl);
setSlideSpecificFonts(updatedFonts);
onFontsUpdate(updatedFonts);
toast.info("Font removed from slide");
};
const getFontNameFromUrl = (url: string) => {
return url.split("/").pop()?.split(".")[0] || "Custom Font";
};
if (!slide.fonts) {
return null;
}
const { internally_supported_fonts = [], not_supported_fonts = [] } =
slide.fonts;
// Get fonts that are globally uploaded but not in this slide
const availableGlobalFonts = globalUploadedFonts.filter(
(font) => !slideSpecificFonts.includes(font.fontUrl)
);
return (
<Card className="mt-4">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<span>Slide Font Management</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Supported Fonts */}
{internally_supported_fonts.length > 0 && (
<div>
<h4 className="text-sm font-medium text-green-700 mb-2 flex items-center gap-1">
<CheckCircle className="w-4 h-4" />
Supported Fonts ({internally_supported_fonts.length})
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{internally_supported_fonts.map((font: any, index: number) => (
<div
key={index}
className="p-2 bg-green-50 border border-green-200 rounded text-sm text-green-800"
>
{font.name}
</div>
))}
</div>
</div>
)}
{/* Unsupported Fonts - only show if they're not globally uploaded */}
{not_supported_fonts.filter(
(fontName: string) =>
!globalUploadedFonts.some((gf) => gf.fontName === fontName)
).length > 0 && (
<div>
<h4 className="text-sm font-medium text-orange-700 mb-2 flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
Missing Fonts (Upload in Global Font Manager above)
</h4>
<div className="space-y-2">
{not_supported_fonts
.filter(
(fontName: string) =>
!globalUploadedFonts.some((gf) => gf.fontName === fontName)
)
.map((fontName: string, index: number) => (
<div
key={index}
className="p-2 bg-orange-50 border border-orange-200 rounded text-sm text-orange-800"
>
{fontName} - Please upload in Global Font Manager
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
};
export default FontManager;

View file

@ -0,0 +1,237 @@
import React, { useState, useRef } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Upload,
CheckCircle,
AlertCircle,
X,
Loader2,
Type,
} from "lucide-react";
interface UploadedFont {
fontName: string;
fontUrl: string;
fontPath: string;
}
interface GlobalFontManagerProps {
slides: any[];
globalUploadedFonts: UploadedFont[];
uploadFont: (fontName: string, file: File) => Promise<string | null>;
removeFont: (fontUrl: string) => void;
getAllUnsupportedFonts: (slides: any[]) => string[];
}
const GlobalFontManager: React.FC<GlobalFontManagerProps> = ({
slides,
globalUploadedFonts,
uploadFont,
removeFont,
getAllUnsupportedFonts,
}) => {
const [uploadingFonts, setUploadingFonts] = useState<Set<string>>(new Set());
const fileInputRefs = useRef<{ [key: string]: HTMLInputElement | null }>({});
const allUnsupportedFonts = getAllUnsupportedFonts(slides);
// Filter out fonts that are already uploaded
const fontsNeedingUpload = allUnsupportedFonts.filter(
(fontName) =>
!globalUploadedFonts.some(
(uploadedFont) => uploadedFont.fontName === fontName
)
);
const handleFontUpload = async (fontName: string, file: File) => {
if (!file) return;
setUploadingFonts((prev) => new Set(prev).add(fontName));
try {
const fontUrl = await uploadFont(fontName, file);
if (fontUrl) {
// Clear the file input
if (fileInputRefs.current[fontName]) {
fileInputRefs.current[fontName]!.value = "";
}
}
} finally {
setUploadingFonts((prev) => {
const newSet = new Set(prev);
newSet.delete(fontName);
return newSet;
});
}
};
const handleFileInputChange = (
fontName: string,
event: React.ChangeEvent<HTMLInputElement>
) => {
const file = event.target.files?.[0];
if (file) {
handleFontUpload(fontName, file);
}
};
const getFontUsageCount = (fontName: string): number => {
return slides.filter((slide) =>
slide.fonts?.not_supported_fonts?.includes(fontName)
).length;
};
if (allUnsupportedFonts.length === 0 && globalUploadedFonts.length === 0) {
return null;
}
return (
<Card className="mb-6">
<CardHeader>
<CardTitle className="text-xl flex items-center gap-2">
<Type className="w-6 h-6" />
Global Font Management
</CardTitle>
<p className="text-sm text-gray-600">
Manage fonts across all slides. Upload fonts once and they'll be
available for all slides.
</p>
</CardHeader>
<CardContent className="space-y-6">
{/* Fonts Needing Upload */}
{fontsNeedingUpload.length > 0 && (
<div>
<h4 className="text-sm font-medium text-orange-700 mb-3 flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
Fonts Needing Upload ({fontsNeedingUpload.length})
</h4>
<div className="space-y-3">
{fontsNeedingUpload.map((fontName: string, index: number) => (
<div
key={index}
className="p-4 bg-orange-50 border border-orange-200 rounded-lg"
>
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium text-orange-800">
{fontName}
</span>
<p className="text-xs text-orange-600 mt-1">
Used in {getFontUsageCount(fontName)} slide
{getFontUsageCount(fontName) !== 1 ? "s" : ""}
</p>
</div>
<div className="flex items-center gap-2">
<input
ref={(el) => {
fileInputRefs.current[fontName] = el;
}}
type="file"
accept=".ttf,.otf,.woff,.woff2,.eot"
onChange={(e) => handleFileInputChange(fontName, e)}
className="hidden"
id={`global-font-upload-${index}`}
/>
<Button
size="sm"
variant="outline"
disabled={uploadingFonts.has(fontName)}
onClick={() => fileInputRefs.current[fontName]?.click()}
className="text-xs bg-blue-600 text-white hover:bg-blue-700 border-blue-600"
>
{uploadingFonts.has(fontName) ? (
<>
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
Uploading...
</>
) : (
<>
<Upload className="w-3 h-3 mr-1" />
Upload Font
</>
)}
</Button>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Successfully Uploaded Fonts */}
{globalUploadedFonts.length > 0 && (
<div>
<h4 className="text-sm font-medium text-green-700 mb-3 flex items-center gap-1">
<CheckCircle className="w-4 h-4" />
Uploaded Fonts ({globalUploadedFonts.length})
</h4>
<div className="space-y-2">
{globalUploadedFonts.map((font, index) => {
const usageCount = getFontUsageCount(font.fontName);
return (
<div
key={index}
className="p-3 bg-green-50 border border-green-200 rounded-lg flex items-center justify-between"
>
<div>
<span className="text-sm font-medium text-green-800">
{font.fontName}
</span>
<p className="text-xs text-green-600 mt-1">
{usageCount > 0 ? (
<>
Used in {usageCount} slide
{usageCount !== 1 ? "s" : ""}
</>
) : (
<>Available for use</>
)}
</p>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => removeFont(font.fontUrl)}
className="text-red-600 hover:text-red-700 hover:bg-red-50 p-1"
>
<X className="w-3 h-3" />
</Button>
</div>
);
})}
</div>
</div>
)}
{/* Summary */}
<div className="pt-4 border-t border-gray-200">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-center">
<div className="p-3 bg-gray-50 rounded-lg">
<p className="text-2xl font-bold text-gray-900">
{allUnsupportedFonts.length}
</p>
<p className="text-xs text-gray-600">Total Unique Fonts</p>
</div>
<div className="p-3 bg-green-50 rounded-lg">
<p className="text-2xl font-bold text-green-700">
{globalUploadedFonts.length}
</p>
<p className="text-xs text-green-600">Fonts Uploaded</p>
</div>
<div className="p-3 bg-orange-50 rounded-lg">
<p className="text-2xl font-bold text-orange-700">
{fontsNeedingUpload.length}
</p>
<p className="text-xs text-orange-600">Fonts Needed</p>
</div>
</div>
</div>
</CardContent>
</Card>
);
};
export default GlobalFontManager;

File diff suppressed because one or more lines are too long

View file

@ -9,23 +9,38 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress";
import { toast } from "sonner";
import { Upload, FileText, X, Loader2 } from "lucide-react";
import { ApiResponseHandler } from "@/app/(presentation-generator)/services/api/api-error-handler";
import { v4 as uuidv4 } from "uuid";
// Types
import EachSlide from "./components/EachSlide";
import GlobalFontManager from "./components/GlobalFontManager";
// Types
interface SlideData {
slide_number: number;
screenshot_url: string;
xml_content: string;
}
interface UploadedFont {
fontName: string;
fontUrl: string;
fontPath: string;
}
interface ProcessedSlide extends SlideData {
html?: string;
fonts: {
internally_supported_fonts: {
name: string;
google_fonts_url: string;
}[];
not_supported_fonts: string[];
};
uploaded_fonts?: string[];
processing?: boolean;
processed?: boolean;
error?: string;
@ -39,6 +54,142 @@ const CustomLayoutPage = () => {
const [slides, setSlides] = useState<ProcessedSlide[]>([]);
const [isSavingLayout, setIsSavingLayout] = useState(false);
const [isLayoutSaved, setIsLayoutSaved] = useState(false);
const [globalUploadedFonts, setGlobalUploadedFonts] = useState<
UploadedFont[]
>([]);
console.log(slides);
// Load uploaded fonts dynamically
useEffect(() => {
globalUploadedFonts.forEach((font) => {
// Check if font style already exists
const existingStyle = document.querySelector(
`style[data-font-url="${font.fontUrl}"]`
);
if (!existingStyle) {
const style = document.createElement("style");
style.setAttribute("data-font-url", font.fontUrl);
// Use the actual font name for font-family
style.textContent = `
@font-face {
font-family: '${font.fontName}';
src: url('${font.fontUrl}') format('truetype');
font-display: swap;
}
`;
document.head.appendChild(style);
}
});
}, [globalUploadedFonts]);
// Font management functions
const uploadFont = useCallback(
async (fontName: string, file: File): Promise<string | null> => {
// Check if font is already uploaded
const existingFont = globalUploadedFonts.find(
(f) => f.fontName === fontName
);
if (existingFont) {
toast.info(`Font "${fontName}" is already uploaded`);
return existingFont.fontUrl;
}
// Validate file type
const validExtensions = [".ttf", ".otf", ".woff", ".woff2", ".eot"];
const fileExtension = file.name
.toLowerCase()
.substring(file.name.lastIndexOf("."));
if (!validExtensions.includes(fileExtension)) {
toast.error(
"Invalid font file type. Please upload .ttf, .otf, .woff, .woff2, or .eot files"
);
return null;
}
// Validate file size (10MB limit)
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
toast.error("Font file size must be less than 10MB");
return null;
}
try {
const formData = new FormData();
formData.append("font_file", file);
const response = await fetch("/api/v1/ppt/fonts/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const data = await response.json();
if (data.success) {
const newFont: UploadedFont = {
fontName: data.font_name || fontName,
fontUrl: data.font_url,
fontPath: data.font_path,
};
setGlobalUploadedFonts((prev) => [...prev, newFont]);
toast.success(`Font "${fontName}" uploaded successfully`);
return newFont.fontUrl;
} else {
throw new Error(data.message || "Upload failed");
}
} catch (error) {
console.error("Error uploading font:", error);
toast.error(`Failed to upload font "${fontName}"`, {
description:
error instanceof Error
? error.message
: "An unexpected error occurred",
});
return null;
}
},
[globalUploadedFonts]
);
const removeGlobalFont = useCallback((fontUrl: string) => {
setGlobalUploadedFonts((prev) =>
prev.filter((font) => font.fontUrl !== fontUrl)
);
// Remove the style element for this font
const styleElement = document.querySelector(
`style[data-font-url="${fontUrl}"]`
);
if (styleElement) {
styleElement.remove();
}
toast.info("Font removed globally");
}, []);
const getAllUnsupportedFonts = useCallback(
(slides: ProcessedSlide[]): string[] => {
const allUnsupportedFonts = new Set<string>();
slides.forEach((slide) => {
if (slide.fonts?.not_supported_fonts) {
slide.fonts.not_supported_fonts.forEach((fontName: string) => {
allUnsupportedFonts.add(fontName);
});
}
});
return Array.from(allUnsupportedFonts);
},
[]
);
// Warning before page unload
useEffect(() => {
@ -66,6 +217,9 @@ const CustomLayoutPage = () => {
const reactComponents = [];
const presentationId = uuidv4();
// Get all uploaded font URLs
const globalFontUrls = globalUploadedFonts.map((font) => font.fontUrl);
for (let i = 0; i < slides.length; i++) {
const slide = slides[i];
@ -90,11 +244,21 @@ const CustomLayoutPage = () => {
`Failed to convert slide ${slide.slide_number} to React`
);
// Combine global fonts with slide-specific uploaded fonts
const slideFonts = [
...globalFontUrls,
...(slide.uploaded_fonts || []),
];
// Remove duplicates
const uniqueFonts = Array.from(new Set(slideFonts));
reactComponents.push({
presentation_id: presentationId,
layout_id: `${slide.slide_number}`,
layout_name: `Slide${slide.slide_number}`,
layout_code: data.react_component || data.component_code,
fonts: uniqueFonts,
});
// Update progress
@ -162,7 +326,7 @@ const CustomLayoutPage = () => {
} finally {
setIsSavingLayout(false);
}
}, [slides]);
}, [slides, globalUploadedFonts]);
// File upload handler
const handleFileSelect = useCallback(
@ -229,17 +393,6 @@ const CustomLayoutPage = () => {
console.log(`Successfully processed slide ${slide.slide_number}`);
// let data: any;
// if (slide.slide_number === 1) {
// data = firstSlide;
// } else if (slide.slide_number === 2) {
// data = slide2;
// } else if (slide.slide_number === 3) {
// data = slide3;
// } else if (slide.slide_number === 4) {
// data = slide4;
// }
// Update slide with success
setSlides((prev) => {
const newSlides = prev.map((s, i) =>
@ -249,6 +402,7 @@ const CustomLayoutPage = () => {
processing: false,
processed: true,
html: htmlData.html,
fonts: htmlData.fonts,
}
: s
);
@ -421,6 +575,17 @@ const CustomLayoutPage = () => {
</p>
</div>
{/* Global Font Management */}
{slides.length > 0 && (
<GlobalFontManager
slides={slides}
globalUploadedFonts={globalUploadedFonts}
uploadFont={uploadFont}
removeFont={removeGlobalFont}
getAllUnsupportedFonts={getAllUnsupportedFonts}
/>
)}
{/* Upload Section */}
<Card className="w-full">
<CardHeader>
@ -544,7 +709,7 @@ const CustomLayoutPage = () => {
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50">
<Button
onClick={saveLayout}
disabled={isSavingLayout}
disabled={isSavingLayout || isProcessingPptx}
className="bg-green-600 hover:bg-green-700 text-white shadow-lg hover:shadow-xl transition-all duration-200 px-10 py-3 text-lg"
size="lg"
>

View file

@ -1,335 +0,0 @@
import React, { useEffect, useState } from "react";
import * as Babel from "@babel/standalone";
import * as z from "zod";
// Clean JSX code without imports - they'll be available in the execution context
const jsxCode = `
const ImageSchema = z.object({
__image_url__: z.url().meta({
description: "URL to image",
}),
__image_prompt__: z.string().meta({
description: "Prompt used to generate the image",
}).min(10).max(50),
})
const layoutId = 'title-slide-with-decorative-elements'
const layoutName = 'TitleSlideWithDecorativeElements'
const layoutDescription = 'A title slide layout with company name, main title, subtitle, author text, and decorative curved shapes with images.'
const titleSlideWithDecorativeElementsSchema = z.object({
companyName: z.string().min(5).max(30).default('AROWWAI INDUSTRIES').meta({
description: "Company or organization name",
}),
mainTitle: z.string().min(5).max(50).default('STRATEGY DECK').meta({
description: "Main title of the presentation (can include line breaks)",
}),
subtitle: z.string().min(10).max(80).default('STRATEGIES FOR GROWTH AND INNOVATION').meta({
description: "Subtitle describing the presentation topic",
}),
authorText: z.string().min(5).max(30).default('BY GROUP 1').meta({
description: "Author or presenter information",
}),
logo: ImageSchema.default({
__image_url__: 'https://images.pexels.com/photos/31995895/pexels-photo-31995895/free-photo-of-turkish-coffee-with-scenic-bursa-view.jpeg',
__image_prompt__: 'Company logo or brand icon'
}).meta({
description: "Company logo or brand icon",
}),
leftDecorativeImage: ImageSchema.default({
__image_url__: 'https://images.pexels.com/photos/31995895/pexels-photo-31995895/free-photo-of-turkish-coffee-with-scenic-bursa-view.jpeg',
__image_prompt__: 'Turkish coffee with scenic Bursa view'
}).meta({
description: "Left decorative curved shape background image",
}),
rightDecorativeImage: ImageSchema.default({
__image_url__: 'https://images.pexels.com/photos/31995895/pexels-photo-31995895/free-photo-of-turkish-coffee-with-scenic-bursa-view.jpeg',
__image_prompt__: 'Turkish coffee with scenic Bursa view'
}).meta({
description: "Right decorative curved shape background image",
})
})
const Schema = titleSlideWithDecorativeElementsSchema
const TitleSlideWithDecorativeElementsLayout = ({ data: slideData }) => {
// Split main title by newlines for proper rendering
const titleLines = (slideData?.mainTitle || 'STRATEGY DECK').split('\\n')
return (
<>
{/* Import Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=League+Spartan:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Futura:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<div
className=" w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white relative z-20 mx-auto overflow-hidden"
style={{ backgroundColor: '#8d7b68' }}
>
{/* Bottom horizontal line */}
<div
className="absolute bottom-0 left-0 w-full h-0.5 bg-yellow-50"
style={{ backgroundColor: '#fdf7e4' }}
></div>
{/* Upper horizontal line */}
<div
className="absolute top-20 left-48 w-80 h-0.5 bg-yellow-50"
style={{ backgroundColor: '#fdf7e4' }}
></div>
{/* Right vertical line */}
<div
className="absolute top-0 right-8 w-0.5 h-full bg-yellow-50"
style={{ backgroundColor: '#fdf7e4' }}
></div>
{/* Upper left circular element */}
<div
className="absolute top-8 left-24 w-20 h-20 rounded-full"
style={{ backgroundColor: '#a4907c' }}
></div>
{/* Lower right circular element */}
<div
className="absolute bottom-20 right-28 w-48 h-48 rounded-full"
style={{ backgroundColor: '#a4907c' }}
></div>
{/* Left decorative curved shape */}
<div className="absolute top-20 left-0 w-64 h-80 overflow-hidden">
<svg viewBox="0 0 660 996" className="w-full h-full">
<path
d="M220.252 19.07C254 7.556 292.6 0 330.378 0C368.157 0 404.509 6.476 438.009 17.99C438.723 18.35 439.435 18.35 440.148 18.71C565.955 64.765 658.618 186.379 660.4 332.57L660.4 995.919L0 995.919L0 333.062C1.782 185.66 93.019 64.045 220.252 19.07Z"
fill="url(#leftImage)"
/>
<defs>
<pattern id="leftImage" patternUnits="objectBoundingBox" width="1" height="1">
<image
href={slideData?.leftDecorativeImage?.__image_url__ || 'https://images.pexels.com/photos/31995895/pexels-photo-31995895/free-photo-of-turkish-coffee-with-scenic-bursa-view.jpeg'}
x="0"
y="0"
width="1"
height="1"
preserveAspectRatio="xMidYMid slice"
/>
</pattern>
</defs>
</svg>
</div>
{/* Right decorative curved shape */}
<div className="absolute top-32 right-0 w-80 h-96 overflow-hidden">
<svg viewBox="0 0 660 996" className="w-full h-full">
<path
d="M220.252 19.07C254 7.556 292.6 0 330.378 0C368.157 0 404.509 6.476 438.009 17.99C438.723 18.35 439.435 18.35 440.148 18.71C565.955 64.765 658.618 186.379 660.4 332.57L660.4 995.919L0 995.919L0 333.062C1.782 185.66 93.019 64.045 220.252 19.07Z"
fill="url(#rightImage)"
/>
<defs>
<pattern id="rightImage" patternUnits="objectBoundingBox" width="1" height="1">
<image
href={slideData?.rightDecorativeImage?.__image_url__ || 'https://images.pexels.com/photos/31995895/pexels-photo-31995895/free-photo-of-turkish-coffee-with-scenic-bursa-view.jpeg'}
x="0"
y="0"
width="1"
height="1"
preserveAspectRatio="xMidYMid slice"
/>
</pattern>
</defs>
</svg>
</div>
{/* Small icon/logo near company name */}
<div className="absolute top-16 right-56 w-16 h-12 overflow-hidden">
<img
src={slideData?.logo?.__image_url__ || 'https://images.pexels.com/photos/31995895/pexels-photo-31995895/free-photo-of-turkish-coffee-with-scenic-bursa-view.jpeg'}
alt={slideData?.logo?.__image_prompt__ || 'Company logo'}
className="w-full h-full object-cover"
/>
</div>
{/* Company name */}
<div className="absolute top-14 right-8 text-right">
<h2
className="text-yellow-50 text-lg font-normal tracking-wider"
style={{ fontFamily: "'Futura', sans-serif", color: '#fdf7e4' }}
>
{slideData?.companyName || 'AROWWAI INDUSTRIES'}
</h2>
</div>
{/* Main title */}
<div className="absolute top-64 left-64 right-8 text-right">
<h1
className="text-yellow-50 text-8xl font-normal tracking-wider leading-tight"
style={{ fontFamily: "'League Spartan', sans-serif", color: '#fdf7e4' }}
>
{titleLines.map((line, index) => (
<React.Fragment key={index}>
{line}
{index < titleLines.length - 1 && <br />}
</React.Fragment>
))}
</h1>
</div>
{/* Subtitle */}
<div className="absolute bottom-24 left-64 right-8 text-right">
<h3
className="text-yellow-50 text-xl font-normal tracking-wide"
style={{ fontFamily: "'Futura', sans-serif", color: '#fdf7e4' }}
>
{slideData?.subtitle || 'STRATEGIES FOR GROWTH AND INNOVATION'}
</h3>
</div>
{/* Bottom left text */}
<div className="absolute bottom-4 left-8">
<p
className="text-yellow-50 text-2xl font-normal tracking-wide"
style={{ fontFamily: "'Futura', sans-serif", color: '#fdf7e4' }}
>
{slideData?.authorText || 'BY GROUP 1'}
</p>
</div>
</div>
</>
)
}
// Return the component
`;
interface Layout {
presentation_id: string;
layout_id: string;
layout_name: string;
layout_code: string;
}
const CustomLayouts = () => {
const [layouts, setLayouts] = useState<Layout[]>([]);
const [component, setComponent] = useState<React.ComponentType<{
data: any;
}> | null>(null);
useEffect(() => {
const fetchLayouts = async () => {
try {
const res = await fetch(
"/api/v1/ppt/layout-management/get-layouts/6038f1cb-80cb-448c-83cc-f6cb96081943"
);
const data = await res.json();
setLayouts(data.layouts);
} catch (error) {
console.error("Failed to fetch layouts:", error);
}
};
fetchLayouts();
}, []);
useEffect(() => {
if (layouts.length === 0) return;
try {
/* ---------- 1. compile JSX to plain script ------------------ */
const compiled = Babel.transform(jsxCode, {
presets: [
["react", { runtime: "classic" }],
["typescript", { isTSX: true, allExtensions: true }],
],
sourceType: "script",
}).code;
/* ---------- 2. wrap compiled code --------------------------- */
const factory = new Function(
"React",
"z",
`
${compiled}
/* everything declared in the string is in scope here */
return {
__esModule: true,
default: TitleSlideWithDecorativeElementsLayout,
layoutName,
layoutId,
layoutDescription,
Schema
};
`
);
/* ---------- 4. split result --------------------------------- */
const mod = factory(React, z);
console.log("generated", mod);
// default export (the component)
const DynamicComponent = mod.default;
// named exports (meta data)
console.log(mod.layoutName);
console.log(mod.layoutId);
console.log(mod.layoutDescription);
console.log(mod.Schema);
const jsonSchema = z.toJSONSchema(mod.Schema, {
override: (ctx) => {
delete ctx.jsonSchema.default;
},
});
console.log(jsonSchema);
// tell React to render the component only
setComponent(() => DynamicComponent);
} catch (err: any) {
console.error("Compilation error:", err);
setComponent(() => () => (
<div style={{ color: "red", padding: 20 }}>
<h3>Compilation Error</h3>
<pre>{err.message}</pre>
</div>
));
}
}, [layouts]);
z;
return (
<div>
<h1>Custom Layout Renderer</h1>
{component && (
<div style={{ marginBottom: "20px" }}>
<h2>Rendered Component:</h2>
{React.createElement(component, { data: {} })}
</div>
)}
{layouts.map((layout) => (
<div key={layout.layout_id} style={{ marginBottom: "20px" }}>
<h2>{layout.layout_name}</h2>
<details>
<summary>View Code</summary>
<pre
style={{
background: "#f5f5f5",
padding: "10px",
overflow: "auto",
}}
>
{layout.layout_code}
</pre>
</details>
</div>
))}
</div>
);
};
export default CustomLayouts;

View file

@ -1,124 +1,143 @@
'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, Wifi, WifiOff, RefreshCw } from 'lucide-react'
"use client";
import React, { useEffect } 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";
import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext";
const GroupLayoutPreview = () => {
const params = useParams()
const router = useRouter()
const slug = params.slug as string
const params = useParams();
const router = useRouter();
const slug = params.slug as string;
const { layoutGroup, loading, error, retry } = useGroupLayoutLoader(slug)
const { getFullDataByGroup, loading } = useLayout();
const layoutGroup = getFullDataByGroup(slug);
// Handle loading state
if (loading) {
return <LoadingStates type="loading" />
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
if (!existingScript) {
const script = document.createElement("script");
script.src = "https://cdn.tailwindcss.com";
script.async = true;
document.head.appendChild(script);
}
}, [slug]);
// Handle error state
if (error) {
return <LoadingStates type="error" message={error} onRetry={retry} />
}
// Handle loading state
if (loading) {
return <LoadingStates type="loading" />;
}
// Handle empty state
if (!layoutGroup || layoutGroup.layouts.length === 0) {
return <LoadingStates type="empty" onRetry={retry} />
}
// Handle empty state
if (!layoutGroup || layoutGroup.length === 0) {
return <LoadingStates type="empty" />;
}
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>
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: any, index: number) => {
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 aspect-video max-w-[1280px] w-full">
<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 className="text-center">
<h1 className="text-3xl font-bold text-gray-900 capitalize">
{layoutGroup[0].groupName} Layouts
</h1>
<p className="text-gray-600 mt-2">
{layoutGroup.length} layout{layoutGroup.length !== 1 ? "s" : ""} {" "}
{layoutGroup[0].groupName}
</p>
</div>
</div>
)
}
</header>
export default GroupLayoutPreview
{/* Layout Grid */}
<main className="max-w-7xl mx-auto px-6 py-8">
<div className="space-y-8">
{layoutGroup.map((layout: any, index: number) => {
const {
component: LayoutComponent,
sampleData,
name,
fileName,
} = layout;
return (
<Card
key={`${layoutGroup[0].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[0].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 aspect-video max-w-[1280px] w-full">
<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[0].groupName} {layoutGroup.length} components
</p>
</div>
</div>
</footer>
</div>
);
};
export default GroupLayoutPreview;

View file

@ -1,185 +1,174 @@
'use client'
import React from 'react'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Loader2, AlertCircle, RefreshCw, FileX, Layers } from 'lucide-react'
"use client";
import React from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Loader2, AlertCircle, RefreshCw, FileX, Layers } from "lucide-react";
interface LoadingStatesProps {
type: 'loading' | 'error' | 'empty'
message?: string
onRetry?: () => void
type: "loading" | "error" | "empty";
message?: string;
}
const LoadingStates: React.FC<LoadingStatesProps> = ({
type,
message,
onRetry
}) => {
if (type === 'loading') {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50 flex items-center justify-center">
<Card className="p-8 text-center shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<CardContent className="space-y-6">
<div className="relative">
<div className="w-16 h-16 mx-auto mb-4 relative">
<Loader2 className="w-16 h-16 text-blue-500 animate-spin" />
<div className="absolute inset-0 w-16 h-16 border-4 border-blue-100 rounded-full animate-pulse"></div>
</div>
</div>
<div className="space-y-2">
<h3 className="text-xl font-semibold text-gray-900">
Loading Layouts
</h3>
<p className="text-gray-600">
{message || 'Discovering and loading layout components...'}
</p>
</div>
{/* Loading animation dots */}
<div className="flex justify-center space-x-1">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
</div>
</CardContent>
</Card>
const LoadingStates: React.FC<LoadingStatesProps> = ({ type, message }) => {
if (type === "loading") {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50 flex items-center justify-center">
<Card className="p-8 text-center shadow-xl border-0 bg-white/80 backdrop-blur-sm">
<CardContent className="space-y-6">
<div className="relative">
<div className="w-16 h-16 mx-auto mb-4 relative">
<Loader2 className="w-16 h-16 text-blue-500 animate-spin" />
<div className="absolute inset-0 w-16 h-16 border-4 border-blue-100 rounded-full animate-pulse"></div>
</div>
</div>
)
}
if (type === 'error') {
return (
<div className="min-h-screen bg-gradient-to-br from-red-50 via-white to-orange-50 flex items-center justify-center">
<Card className="p-8 text-center shadow-xl border-0 bg-white/80 backdrop-blur-sm max-w-md">
<CardContent className="space-y-6">
<div className="w-16 h-16 mx-auto p-4 bg-red-100 rounded-full">
<AlertCircle className="w-8 h-8 text-red-500" />
</div>
<div className="space-y-2">
<h3 className="text-xl font-semibold text-gray-900">
Something went wrong
</h3>
<p className="text-gray-600 text-sm leading-relaxed">
{message || 'Failed to load layouts. Please check your layout files and try again.'}
</p>
</div>
{onRetry && (
<Button
onClick={onRetry}
className="mt-4 bg-red-500 hover:bg-red-600 text-white"
>
<RefreshCw className="w-4 h-4 mr-2" />
Try Again
</Button>
)}
</CardContent>
</Card>
<div className="space-y-2">
<h3 className="text-xl font-semibold text-gray-900">
Loading Layouts
</h3>
<p className="text-gray-600">
{message || "Discovering and loading layout components..."}
</p>
</div>
)
}
if (type === 'empty') {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-slate-50 flex items-center justify-center">
<Card className="p-8 text-center shadow-xl border-0 bg-white/80 backdrop-blur-sm max-w-md">
<CardContent className="space-y-6">
<div className="w-16 h-16 mx-auto p-4 bg-gray-100 rounded-full">
<FileX className="w-8 h-8 text-gray-400" />
</div>
<div className="space-y-2">
<h3 className="text-xl font-semibold text-gray-700">
No Layouts Found
</h3>
<p className="text-gray-500 text-sm leading-relaxed">
No valid layout files were discovered. Make sure your layout components export both a default component and a Schema.
</p>
</div>
<div className="bg-gray-50 p-4 rounded-lg text-left text-xs text-gray-600">
<p className="font-medium mb-2">Expected structure:</p>
<code className="block">
export default MyLayout<br />
export const Schema = z.object(...)
</code>
</div>
{onRetry && (
<Button
onClick={onRetry}
variant="outline"
className="mt-4"
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
)}
</CardContent>
</Card>
{/* Loading animation dots */}
<div className="flex justify-center space-x-1">
<div
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
style={{ animationDelay: "0ms" }}
></div>
<div
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
style={{ animationDelay: "150ms" }}
></div>
<div
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
style={{ animationDelay: "300ms" }}
></div>
</div>
)
}
</CardContent>
</Card>
</div>
);
}
return null
}
if (type === "error") {
return (
<div className="min-h-screen bg-gradient-to-br from-red-50 via-white to-orange-50 flex items-center justify-center">
<Card className="p-8 text-center shadow-xl border-0 bg-white/80 backdrop-blur-sm max-w-md">
<CardContent className="space-y-6">
<div className="w-16 h-16 mx-auto p-4 bg-red-100 rounded-full">
<AlertCircle className="w-8 h-8 text-red-500" />
</div>
<div className="space-y-2">
<h3 className="text-xl font-semibold text-gray-900">
Something went wrong
</h3>
<p className="text-gray-600 text-sm leading-relaxed">
{message ||
"Failed to load layouts. Please check your layout files and try again."}
</p>
</div>
</CardContent>
</Card>
</div>
);
}
if (type === "empty") {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-slate-50 flex items-center justify-center">
<Card className="p-8 text-center shadow-xl border-0 bg-white/80 backdrop-blur-sm max-w-md">
<CardContent className="space-y-6">
<div className="w-16 h-16 mx-auto p-4 bg-gray-100 rounded-full">
<FileX className="w-8 h-8 text-gray-400" />
</div>
<div className="space-y-2">
<h3 className="text-xl font-semibold text-gray-700">
No Layouts Found
</h3>
<p className="text-gray-500 text-sm leading-relaxed">
No valid layout files were discovered. Make sure your layout
components export both a default component and a Schema.
</p>
</div>
<div className="bg-gray-50 p-4 rounded-lg text-left text-xs text-gray-600">
<p className="font-medium mb-2">Expected structure:</p>
<code className="block">
export default MyLayout
<br />
export const Schema = z.object(...)
</code>
</div>
</CardContent>
</Card>
</div>
);
}
return null;
};
// Component for layout grid skeleton while loading
export const LayoutGridSkeleton: React.FC = () => {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
{/* Header Skeleton */}
<div className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-gray-200 rounded-lg animate-pulse"></div>
<div className="w-32 h-6 bg-gray-200 rounded animate-pulse"></div>
</div>
<div className="w-16 h-6 bg-gray-200 rounded animate-pulse"></div>
</div>
</div>
</div>
{/* Main Content Skeleton */}
<div className="max-w-7xl mx-auto p-6">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Skeleton */}
<div className="lg:col-span-1 space-y-4">
<Card className="p-4">
<div className="space-y-3">
<div className="w-24 h-4 bg-gray-200 rounded animate-pulse"></div>
<div className="space-y-2">
<div className="w-full h-8 bg-gray-200 rounded animate-pulse"></div>
<div className="w-full h-8 bg-gray-200 rounded animate-pulse"></div>
</div>
<div className="grid grid-cols-3 gap-2">
{[...Array(6)].map((_, i) => (
<div key={i} className="w-full h-12 bg-gray-200 rounded animate-pulse"></div>
))}
</div>
</div>
</Card>
</div>
{/* Main Display Skeleton */}
<div className="lg:col-span-3">
<Card className="p-6">
<div className="space-y-4">
<div className="w-full h-96 bg-gray-200 rounded-lg animate-pulse"></div>
<div className="space-y-2">
<div className="w-48 h-4 bg-gray-200 rounded animate-pulse"></div>
<div className="w-32 h-3 bg-gray-200 rounded animate-pulse"></div>
</div>
</div>
</Card>
</div>
</div>
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
{/* Header Skeleton */}
<div className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-gray-200 rounded-lg animate-pulse"></div>
<div className="w-32 h-6 bg-gray-200 rounded animate-pulse"></div>
</div>
<div className="w-16 h-6 bg-gray-200 rounded animate-pulse"></div>
</div>
</div>
)
}
</div>
export default LoadingStates
{/* Main Content Skeleton */}
<div className="max-w-7xl mx-auto p-6">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar Skeleton */}
<div className="lg:col-span-1 space-y-4">
<Card className="p-4">
<div className="space-y-3">
<div className="w-24 h-4 bg-gray-200 rounded animate-pulse"></div>
<div className="space-y-2">
<div className="w-full h-8 bg-gray-200 rounded animate-pulse"></div>
<div className="w-full h-8 bg-gray-200 rounded animate-pulse"></div>
</div>
<div className="grid grid-cols-3 gap-2">
{[...Array(6)].map((_, i) => (
<div
key={i}
className="w-full h-12 bg-gray-200 rounded animate-pulse"
></div>
))}
</div>
</div>
</Card>
</div>
{/* Main Display Skeleton */}
<div className="lg:col-span-3">
<Card className="p-6">
<div className="space-y-4">
<div className="w-full h-96 bg-gray-200 rounded-lg animate-pulse"></div>
<div className="space-y-2">
<div className="w-48 h-4 bg-gray-200 rounded animate-pulse"></div>
<div className="w-32 h-3 bg-gray-200 rounded animate-pulse"></div>
</div>
</div>
</Card>
</div>
</div>
</div>
</div>
);
};
export default LoadingStates;

View file

@ -1,16 +1,41 @@
"use client";
import React from "react";
import React, { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useLayoutLoader } from "./hooks/useLayoutLoader";
import { useLayout } from "../(presentation-generator)/context/LayoutContext";
import LoadingStates from "./components/LoadingStates";
import { Card } from "@/components/ui/card";
import { ExternalLink } from "lucide-react";
import CustomLayouts from "./CustomLayouts";
const LayoutPreview = () => {
const { layoutGroups, layouts, loading, error, retry } = useLayoutLoader();
const {
getAllGroups,
getLayoutsByGroup,
getGroupSetting,
getAllLayouts,
loading,
error,
} = useLayout();
const router = useRouter();
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
if (!existingScript) {
const script = document.createElement("script");
script.src = "https://cdn.tailwindcss.com";
script.async = true;
document.head.appendChild(script);
}
}, []);
// Transform context data to match expected format
const layoutGroups = getAllGroups().map((groupName) => ({
groupName,
layouts: getLayoutsByGroup(groupName),
settings: getGroupSetting(groupName) || { description: "", ordered: false },
}));
// Handle loading state
if (loading) {
return <LoadingStates type="loading" />;
@ -18,28 +43,28 @@ const LayoutPreview = () => {
// Handle error state
if (error) {
return <LoadingStates type="error" message={error} onRetry={retry} />;
return <LoadingStates type="error" message={error} />;
}
// Handle empty state
if (layoutGroups.length === 0 || layouts.length === 0) {
return <LoadingStates type="empty" onRetry={retry} />;
if (layoutGroups.length === 0) {
return <LoadingStates type="empty" />;
}
return (
<div className="min-h-screen bg-gray-50">
<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=" sticky top-0 z-30">
<div className="max-w-7xl mx-auto border-b px-6 py-6">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900">Layout Preview</h1>
<h1 className="text-3xl font-bold text-gray-900">All Templates</h1>
<p className="text-gray-600 mt-2">
{layoutGroups.length} groups {layouts.length} layouts
{layoutGroups.length} templates
</p>
</div>
</div>
{/* Group Navigation Cards */}
<div className="border-t bg-gray-50 min-h-screen flex justify-center items-center">
<div className=" h-full pt-16 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) => (
@ -86,21 +111,21 @@ const LayoutPreview = () => {
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
Custom Layouts
Create
</h3>
<div className="flex items-center gap-2">
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
Custom layouts for your presentations
Create a new custom layout
</p>
</div>
</Card>
</div>
</div>
</div>
<CustomLayouts />
{/* <CustomLayouts /> */}
</div>
</div>
);

View file

@ -1,305 +0,0 @@
/**
* Sample Data Generator Utility
*
* Generates realistic sample data from Zod schemas for layout previews.
* Provides context-aware data generation based on field names and types.
*/
export const generateSampleDataFromSchema = (schema: any, layoutName: string): any => {
if (!schema) return {}
try {
// Generate realistic sample data for all fields first
const generatedData = generateRealisticData(schema._def?.shape || schema.shape, layoutName)
// Merge generated data with defaults, giving priority to defaults
return generatedData
} catch (error) {
console.error(`Error generating sample data for ${layoutName}:`, error)
return {}
}
}
const generateRealisticData = (shape: any, layoutName: string): any => {
if (!shape) return {}
const data: any = {}
for (const [key, fieldSchema] of Object.entries(shape as any)) {
const field = fieldSchema as any
// Generate data for all fields (both required and optional)
// We'll let the defaults override this later if they exist
data[key] = generateFieldValue(key, field, layoutName)
}
return data
}
// const arrayMock = (length:number,element:any) => {
// return Array.from({length},()=>generateFieldValue(fieldName, element, layoutName))
// }
// const mockObject = (shapes:any) => {
// let obj:any = {}
// for(const [key,shape] of Object.entries(shapes)){
// const defaultValue = shape.def.defaultValue
// obj[key] = defaultValue ? defaultValue : generateFieldValue(key, shape, layoutName)
// }
// return obj
// }
// const generateMockValue = (fileType:string,format?:string)=>{
// switch(fileType){
// case 'number':
// return Math.floor(Math.random() * 100) + 1
// case 'string':
// return generateStringValue(fieldName, fieldSchema, layoutName)
// case 'boolean':
// return Math.random() > 0.5
// case 'object':
// return mockObject(fieldSchema.def.shape)
// case 'array':
// return arrayMock(fieldSchema.def.length,fieldSchema.def.element)
// }
// }
const generateFieldValue = (fieldName: string, fieldSchema: any, layoutName: string): any => {
console.log('BADU',fieldSchema,fieldName,layoutName)
const defaultValue = fieldSchema.def.defaultValue;
if(defaultValue){
console.log('DEFAULT VALUE',defaultValue)
return defaultValue;
}
if(fieldSchema.def.type ==='optional'){
return generateFieldValue(fieldName, fieldSchema.def.innerType, layoutName)
}
// Get the actual field type - handle optional fields properly
let actualFieldSchema = fieldSchema
let fieldType = fieldSchema._def?.typeName
// If this is an optional field (ZodOptional), get the inner type
if (fieldType === 'ZodOptional') {
actualFieldSchema = fieldSchema._def?.innerType
fieldType = actualFieldSchema?._def?.typeName
}
// For preview purposes, always generate data for optional fields
// (users want to see how the layout looks with content)
// Handle different field types
switch (fieldType) {
case 'ZodString':
return generateStringValue(fieldName, actualFieldSchema, layoutName)
case 'ZodArray':
return generateArrayValue(fieldName, actualFieldSchema, layoutName)
case 'ZodObject':
return generateObjectValue(fieldName, actualFieldSchema, layoutName)
case 'ZodEnum':
const options = actualFieldSchema._def?.values || []
return options[Math.floor(Math.random() * options.length)]
case 'ZodBoolean':
return Math.random() > 0.5
case 'ZodNumber':
return Math.floor(Math.random() * 100) + 1
default:
return generateStringValue(fieldName, actualFieldSchema, layoutName)
}
}
const generateStringValue = (fieldName: string, fieldSchema: any, layoutName: string): string => {
const lowerField = fieldName.toLowerCase()
// Handle URLs (images, logos, backgrounds, etc.)
if (lowerField.includes('url') || lowerField.includes('image') || lowerField.includes('logo')) {
if (lowerField.includes('logo')) {
return 'https://images.unsplash.com/photo-1611224923853-80b023f02d71?w=200&h=200&fit=crop'
}
if (lowerField.includes('background')) {
const backgrounds = [
'https://images.unsplash.com/photo-1557804506-669a67965ba0?w=1920&h=1080&fit=crop',
'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1920&h=1080&fit=crop',
'https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=1920&h=1080&fit=crop'
]
return backgrounds[Math.floor(Math.random() * backgrounds.length)]
}
// Regular images
const images = [
'https://images.unsplash.com/photo-1551434678-e076c223a692?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1504384308090-c894fdcc538d?w=800&h=600&fit=crop',
'https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=800&h=600&fit=crop'
]
return images[Math.floor(Math.random() * images.length)]
}
// Handle email
if (lowerField.includes('email')) {
const domains = ['example.com', 'company.com', 'business.org']
const names = ['contact', 'info', 'hello', 'support']
return `${names[Math.floor(Math.random() * names.length)]}@${domains[Math.floor(Math.random() * domains.length)]}`
}
// Handle phone
if (lowerField.includes('phone')) {
return `+1 (555) ${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 9000) + 1000}`
}
// Handle website
if (lowerField.includes('website')) {
const sites = ['https://example.com', 'https://company.com', 'https://business.org']
return sites[Math.floor(Math.random() * sites.length)]
}
// Handle LinkedIn
if (lowerField.includes('linkedin')) {
return 'https://linkedin.com/company/example'
}
// Handle specific field names
if (lowerField.includes('title')) {
const titles = [
'Welcome to Our Presentation',
'Key Business Insights',
'Product Overview',
'Market Analysis',
'Future Vision',
'Strategic Goals'
]
return titles[Math.floor(Math.random() * titles.length)]
}
if (lowerField.includes('subtitle')) {
const subtitles = [
'Driving innovation through technology',
'Transforming the way we work',
'Building solutions for tomorrow',
'Excellence in every detail',
'Your success is our mission'
]
return subtitles[Math.floor(Math.random() * subtitles.length)]
}
if (lowerField.includes('author') || lowerField.includes('name')) {
const names = ['Alex Johnson', 'Sarah Chen', 'Michael Rodriguez', 'Emily Davis', 'David Kim']
return names[Math.floor(Math.random() * names.length)]
}
if (lowerField.includes('organization') || lowerField.includes('company')) {
const orgs = ['Tech Innovations Inc.', 'Future Solutions Ltd.', 'Global Dynamics Corp.', 'NextGen Enterprises']
return orgs[Math.floor(Math.random() * orgs.length)]
}
if (lowerField.includes('date')) {
return new Date().toLocaleDateString()
}
if (lowerField.includes('content')) {
const contents = [
'Our innovative approach combines cutting-edge technology with proven methodologies to deliver exceptional results. We focus on scalability, reliability, and user experience.',
'Through strategic partnerships and continuous innovation, we\'ve established ourselves as leaders in the industry. Our solutions are designed to meet evolving market demands.',
'With over a decade of experience, our team brings deep expertise and fresh perspectives to every project. We\'re committed to exceeding expectations and driving growth.'
]
return contents[Math.floor(Math.random() * contents.length)]
}
if (lowerField.includes('caption')) {
const captions = [
'Innovative solutions driving business transformation',
'Real-time analytics and insights at your fingertips',
'Seamless integration with existing workflows',
'Empowering teams to achieve more'
]
return captions[Math.floor(Math.random() * captions.length)]
}
if (lowerField.includes('action') || lowerField.includes('cta')) {
const actions = [
'Get Started Today!',
'Schedule a Demo',
'Contact Our Team',
'Learn More',
'Try It Free'
]
return actions[Math.floor(Math.random() * actions.length)]
}
// Default text based on field length constraints
const minLength = fieldSchema._def?.checks?.find((c: any) => c.kind === 'min')?.value || 10
const maxLength = fieldSchema._def?.checks?.find((c: any) => c.kind === 'max')?.value || 100
if (maxLength <= 50) {
return 'Sample short text content'
} else if (maxLength <= 150) {
return 'This is sample medium-length text content for preview purposes'
} else {
return 'This is sample long-form text content that demonstrates how the layout will look with realistic data. It provides a good representation of the final presentation slide.'
}
}
const generateArrayValue = (fieldName: string, fieldSchema: any, layoutName: string): any[] => {
const itemSchema = fieldSchema._def?.type
const minItems = fieldSchema._def?.minLength?.value || 2
const maxItems = Math.min(fieldSchema._def?.maxLength?.value || 5, 6)
const itemCount = Math.floor(Math.random() * (maxItems - minItems + 1)) + minItems
const lowerField = fieldName.toLowerCase()
if (lowerField.includes('bullet') || lowerField.includes('point')) {
const bulletPoints = [
'Increased efficiency and productivity',
'Cost-effective solutions',
'Enhanced user experience',
'Scalable architecture',
'Real-time analytics',
'24/7 customer support',
'Seamless integration capabilities',
'Advanced security features'
]
return bulletPoints.slice(0, itemCount)
}
if (lowerField.includes('takeaway') || lowerField.includes('key')) {
const takeaways = [
'Strategic advantage through innovation',
'Proven ROI within 6 months',
'Comprehensive support included',
'Future-ready technology stack',
'Industry-leading performance'
]
return takeaways.slice(0, itemCount)
}
// Generate generic array items
const items = []
for (let i = 0; i < itemCount; i++) {
if (itemSchema) {
items.push(generateFieldValue(`${fieldName}Item`, itemSchema, layoutName))
} else {
items.push(`Sample item ${i + 1}`)
}
}
return items
}
const generateObjectValue = (fieldName: string, fieldSchema: any, layoutName: string): any => {
const shape = fieldSchema._def?.shape
if (!shape) return {}
const obj: any = {}
for (const [key, subSchema] of Object.entries(shape)) {
obj[key] = generateFieldValue(key, subSchema, layoutName)
}
return obj
}

View file

@ -2,6 +2,7 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View file

@ -1,95 +1,98 @@
import type { Config } from "tailwindcss";
import scrollbarHide from 'tailwind-scrollbar-hide'
import scrollbarHide from "tailwind-scrollbar-hide";
const config: Config = {
darkMode: ["class"],
content: [
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"!./app/privacy-policy/**/*.{js,ts,jsx,tsx,mdx}",
"./presentation-layouts/**/*.{js,ts,jsx,tsx,mdx}",
"!./app/privacy-policy/**/*.{js,ts,jsx,tsx,mdx}",
"./presentation-layouts/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
keyframes: {
'accordion-down': {
from: {
height: '0'
},
to: {
height: 'var(--radix-accordion-content-height)'
}
},
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: '0'
}
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
},
fontFamily: {
'instrument_sans':['var(--font-instrument-sans)'],
'inter':['var(--font-inter)'],
'roboto':['var(--font-roboto)'],
},
}
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
chart: {
"1": "hsl(var(--chart-1))",
"2": "hsl(var(--chart-2))",
"3": "hsl(var(--chart-3))",
"4": "hsl(var(--chart-4))",
"5": "hsl(var(--chart-5))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: {
height: "0",
},
to: {
height: "var(--radix-accordion-content-height)",
},
},
"accordion-up": {
from: {
height: "var(--radix-accordion-content-height)",
},
to: {
height: "0",
},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
fontFamily: {
instrument_sans: ["var(--font-instrument-sans)"],
inter: ["var(--font-inter)"],
roboto: ["var(--font-roboto)"],
},
},
},
plugins: [require("tailwindcss-animate"),scrollbarHide,require('@tailwindcss/typography')],
plugins: [
require("tailwindcss-animate"),
scrollbarHide,
require("@tailwindcss/typography"),
],
};
export default config;

File diff suppressed because one or more lines are too long