feat(Nextjs): Custom Layout support for layout context & font loading in Custom layouts
This commit is contained in:
parent
6fc94648c4
commit
2622f2846b
17 changed files with 1177 additions and 1786 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
125
servers/nextjs/app/custom-layout/components/FontManager.tsx
Normal file
125
servers/nextjs/app/custom-layout/components/FontManager.tsx
Normal 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;
|
||||
|
|
@ -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
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue