refactor (Nextjs): Unused files and function removed

This commit is contained in:
shiva raj badu 2025-07-19 22:59:02 +05:45
parent 1c4dca4677
commit d4813b02a6
No known key found for this signature in database
44 changed files with 28 additions and 3810 deletions

View file

@ -1,291 +0,0 @@
import React, { useState, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
setThemeColors,
setTheme,
setLoadingState,
} from "../store/themeSlice";
import { ThemeType } from "../upload/type";
import { useThemeService, ThemeColors } from "../services/themeService";
import { PresentationGenerationApi } from "../services/api/presentation-generation";
interface CustomThemeSettingsProps {
onClose?: () => void;
presentationId: string;
}
const CustomThemeSettings = ({
onClose,
presentationId,
}: CustomThemeSettingsProps) => {
const dispatch = useDispatch();
const [draftColors, setDraftColors] = useState<ThemeColors>({
background: "#63ceff",
slideBg: "#F4F4F4",
slideTitle: "#1A1A1A",
slideHeading: "#2D2D2D",
slideDescription: "#4A4A4A",
slideBox: "#d8c6c6",
iconBg: "#281810",
chartColors: ["#281810", "#4A3728", "#665E57", "#665E57", "#665E57"],
fontFamily: "var(--font-inter)",
});
const themeService = useThemeService();
// Refs for tracking drag state and RAF
const isDragging = useRef(false);
const rafId = useRef<number>();
const currentKey = useRef<string>();
const currentValue = useRef<string>();
const updateDraftColor = (key: string, value: string) => {
if (rafId.current) {
cancelAnimationFrame(rafId.current);
}
rafId.current = requestAnimationFrame(() => {
setDraftColors((prev) => ({
...prev,
[key]: value,
}));
});
};
const handleColorPickerChange = (key: string, value: string) => {
if (isDragging.current) {
// Update refs for current values
currentKey.current = key;
currentValue.current = value;
// Update preview immediately
const previewElement = document.getElementById(`preview-${key}`);
if (previewElement) {
previewElement.style.backgroundColor = value;
}
} else {
// For non-drag changes (like text input), update immediately
updateDraftColor(key, value);
}
};
const handleColorPickerMouseDown = () => {
isDragging.current = true;
};
const handleColorPickerMouseUp = () => {
isDragging.current = false;
// Apply the final color value
if (currentKey.current && currentValue.current) {
updateDraftColor(currentKey.current, currentValue.current);
}
};
const handleTextInputChange = (key: string, value: string) => {
updateDraftColor(key, value);
};
const handleSave = async () => {
try {
// Update UI immediately
const themeType = "custom" as ThemeType;
dispatch(setTheme(themeType));
dispatch(
setThemeColors({
...draftColors,
theme: themeType,
})
);
// Set CSS variables
const root = document.documentElement;
root.style.setProperty("--custom-slide-bg", draftColors.slideBg);
root.style.setProperty("--custom-slide-title", draftColors.slideTitle);
root.style.setProperty(
"--custom-slide-heading",
draftColors.slideHeading
);
root.style.setProperty(
"--custom-slide-description",
draftColors.slideDescription
);
root.style.setProperty("--custom-slide-box", draftColors.slideBox);
root.style.setProperty("--custom-icon-bg", draftColors.iconBg);
// Save to file and API
await Promise.all([
PresentationGenerationApi.setThemeColors(presentationId, {
name: themeType,
colors: {
...draftColors,
},
}),
themeService.saveTheme({
name: "custom",
colors: {
...draftColors,
theme: themeType,
},
}),
]);
onClose?.();
} catch (error) {
console.error("Failed to save custom theme:", error);
}
};
// Load saved theme
React.useEffect(() => {
const loadSavedCustomTheme = async () => {
try {
dispatch(setLoadingState(true));
const savedTheme = await themeService.getTheme();
if (savedTheme) {
setDraftColors(savedTheme.colors);
}
} catch (error) {
console.error("Failed to load theme preferences:", error);
} finally {
dispatch(setLoadingState(false));
}
};
loadSavedCustomTheme();
}, []);
// Cleanup RAF on unmount
React.useEffect(() => {
return () => {
if (rafId.current) {
cancelAnimationFrame(rafId.current);
}
};
}, []);
const colorInputs = [
{ key: "background", label: "Background Color", icon: "🎨" },
{ key: "slideBg", label: "Slide Background Color", icon: "🎨" },
{ key: "slideTitle", label: "Title Color", icon: "📝" },
{ key: "slideHeading", label: "Heading Color", icon: "🔤" },
{ key: "slideDescription", label: "Description Color", icon: "📄" },
{ key: "slideBox", label: "Box Color", icon: "📦" },
{ key: "iconBg", label: "Icon Background Color", icon: "📦" },
];
return (
<div className="">
<div className="h-[60vh] font-inter overflow-y-auto custom_scrollbar pr-2 pb-2">
{/* Live Preview */}
<div className=" w-full space-y-2">
<h3 className="text-xs font-medium text-gray-500">Live Preview</h3>
<div
style={{
backgroundColor: draftColors.background,
}}
className="p-3 rounded-lg"
>
<div
className="w-full h-28 rounded-lg shadow-sm transition-all overflow-hidden"
style={{
backgroundColor: draftColors.slideBg,
padding: "0.75rem",
}}
>
<div
style={{ color: draftColors.slideTitle }}
className="text-base font-bold mb-2"
>
Sample Title
</div>
<div
style={{ color: draftColors.slideHeading }}
className="text-sm mb-1"
>
Heading
</div>
<div
style={{ color: draftColors.slideDescription }}
className="text-xs"
>
Description text
</div>
<div
style={{ backgroundColor: draftColors.slideBox }}
className="mt-2 p-1.5 rounded w-20"
></div>
</div>
</div>
</div>
<div className="w-full grid grid-cols-1 gap-4">
{colorInputs.map(({ key, label, icon }) => (
<div
key={key}
className="flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="relative group">
<Input
type="color"
id={key}
value={draftColors[key as keyof typeof draftColors]}
onChange={(e) => handleColorPickerChange(key, e.target.value)}
onMouseDown={() => handleColorPickerMouseDown()}
onMouseUp={() => handleColorPickerMouseUp()}
onTouchStart={() => handleColorPickerMouseDown()}
onTouchEnd={() => handleColorPickerMouseUp()}
className="w-12 h-12 p-1 cursor-pointer border rounded-lg transition-all hover:border-[#5146E5] focus:border-[#5146E5]"
/>
<div
id={`preview-${key}`}
className="absolute top-0 left-0 w-full h-full rounded-lg pointer-events-none"
style={{
backgroundColor: draftColors[
key as keyof typeof draftColors
] as string,
}}
/>
</div>
<div className="flex-1 space-y-1">
<Label
htmlFor={key}
className="text-sm font-medium text-gray-700 flex items-center gap-2"
>
<span>{icon}</span>
{label}
</Label>
<Input
type="text"
value={draftColors[key as keyof typeof draftColors]}
onChange={(e) => handleTextInputChange(key, e.target.value)}
className="h-8 font-mono text-sm"
placeholder="#000000"
/>
</div>
</div>
))}
</div>
</div>
<div className="mt-6 pt-4 font-roboto border-t flex justify-end gap-2">
<Button
variant="outline"
onClick={onClose}
className="px-4 h-9 text-sm"
>
Cancel
</Button>
<Button
onClick={handleSave}
className="bg-[#5146E5] hover:bg-[#4338ca] text-white px-4 h-9 text-sm"
>
Save Theme
</Button>
</div>
</div>
);
};
export default CustomThemeSettings;

View file

@ -1,111 +0,0 @@
import React, { useEffect, useRef } from "react";
import { useSelector } from "react-redux";
import TipTapEditor from "./Tiptap";
import { RootState } from "@/store/store";
import Typewriter from "./TypeWriter";
interface EditableTextProps {
slideIndex: number;
bodyIdx?: number;
elementId: string;
type:
| "title"
| "heading"
| "description-body"
| "description"
| "heading-description"
| "info-heading"
| "info-description";
content: string;
isAlingCenter?: boolean;
}
const EditableText = ({
slideIndex,
elementId,
type,
content,
bodyIdx = 0,
isAlingCenter = false,
}: EditableTextProps) => {
const { isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
const elementRef = useRef<HTMLDivElement>(null);
// Add useEffect to initialize content
useEffect(() => {
if (elementRef.current) {
const displayContent = content || getPlaceholder();
elementRef.current.textContent = displayContent;
// Add placeholder styling if needed
if (!content) {
elementRef.current.classList.add("text-gray-400");
}
}
}, [content]);
const getPlaceholder = () => {
switch (type) {
case "title":
return "Enter title";
case "heading":
return "Enter heading";
case "description-body":
return "Enter description";
case "description":
return "Enter description";
case "heading-description":
return "Enter description";
case "info-heading":
return "Enter heading";
case "info-description":
return "Enter description";
default:
return "Enter text";
}
};
const getTextStyle = () => {
const baseStyle = "outline-none transition-all duration-200";
switch (type) {
case "title":
return `${baseStyle} text-[40px] slide-title leading-[48px] font-bold`;
case "heading":
return `${baseStyle} text-[24px] slide-heading leading-[32px] font-bold`;
case "description":
case "description-body":
case "heading-description":
return `${baseStyle} text-[20px] slide-description leading-[24px] font-normal`;
default:
return `${baseStyle} text-[20px] slide-description leading-[24px] font-normal`;
}
};
return (
<>
{isStreaming ? (
<div
className={`w-full min-w-[60px] font-inter ${getTextStyle()} ${isAlingCenter ? "text-center " : ""
}`}
>
<Typewriter text={content ? content.replace(/\*\*/g, "") : ""} speed={20} />
</div>
) : (
<TipTapEditor
key={content}
bodyIdx={bodyIdx}
isAlingCenter={isAlingCenter}
slideIndex={slideIndex}
elementId={elementId}
type={type}
content={content}
/>
)}
</>
);
};
export default React.memo(EditableText);

View file

@ -1,43 +0,0 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreHorizontal } from "lucide-react";
import React from "react";
interface ElementMenuProps {
index: number;
handleDeleteItem: (index: number) => void;
}
const ElementMenu = ({ index, handleDeleteItem }: ElementMenuProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="absolute hidden md:block top-0 left-1/2 -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
data-index={index}
>
<MoreHorizontal className="w-4 h-4 text-black" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[180px] p-2">
<DropdownMenuItem
onClick={() => handleDeleteItem(index)}
className="px-3 py-2 cursor-pointer"
>
Delete Item {index + 1}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
// Prevent unnecessary re-renders
export default React.memo(ElementMenu, (prevProps, nextProps) => {
return (
prevProps.index === nextProps.index && prevProps.index === nextProps.index
);
});

View file

@ -5,7 +5,7 @@ import Link from "next/link";
import { RootState } from "@/store/store";
import { useSelector } from "react-redux";
const UserAccount = () => {
const HeaderNav = () => {
const canChangeKeys = useSelector((state: RootState) => state.userConfig.can_change_keys);
@ -39,4 +39,4 @@ const UserAccount = () => {
);
};
export default UserAccount;
export default HeaderNav;

View file

@ -15,7 +15,7 @@ import { useGroupLayouts } from "../hooks/useGroupLayouts";
interface PresentationModeProps {
slides: Slide[];
currentSlide: number;
currentTheme: string;
isFullscreen: boolean;
onFullscreenToggle: () => void;
onExit: () => void;
@ -26,7 +26,7 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
slides,
currentSlide,
currentTheme,
isFullscreen,
onFullscreenToggle,
onExit,
@ -190,10 +190,9 @@ const PresentationMode: React.FC<PresentationModeProps> = ({
<div className="flex-1 flex items-center justify-center p-8">
<div
className={`w-full max-w-[1280px] scale-110 aspect-video slide-theme slide-container border rounded-sm font-inter shadow-lg bg-white`}
data-theme={currentTheme}
>
{slides[currentSlide] &&
renderSlideContent(slides[currentSlide])}
renderSlideContent(slides[currentSlide], false)}
</div>
</div>
</div>

View file

@ -1,91 +0,0 @@
"use client";
import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown";
import Underline from "@tiptap/extension-underline";
import {
Bold,
Italic,
Underline as UnderlinedIcon,
Strikethrough,
Code,
} from "lucide-react";
const TipTapEditor = ({
content,
}: {
content: string;
}) => {
const editor = useEditor({
extensions: [StarterKit, Markdown, Underline],
content: content,
editorProps: {
attributes: {
class: "outline-none transition-all duration-200",
},
},
immediatelyRender: false,
});
return (
<div className="relative">
<BubbleMenu editor={editor} tippyOptions={{ duration: 100 }}>
<div className="flex bg-white rounded-lg shadow-lg p-2 gap-1 border-r pr-2">
<button
onClick={() => editor?.chain().focus().toggleBold().run()}
className={`p-1 rounded hover:bg-gray-100 ${editor?.isActive("bold") ? "bg-gray-200" : ""
}`}
>
<Bold className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleItalic().run()}
className={`p-1 rounded hover:bg-gray-100 ${editor?.isActive("italic") ? "bg-gray-200" : ""
}`}
>
<Italic className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleUnderline().run()}
className={`p-1 rounded hover:bg-gray-100 ${editor?.isActive("underline") ? "bg-gray-200" : ""
}`}
>
<UnderlinedIcon className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleStrike().run()}
className={`p-1 rounded hover:bg-gray-100 ${editor?.isActive("strike") ? "bg-gray-200" : ""
}`}
>
<Strikethrough className="h-4 w-4" />
</button>
<button
onClick={() => editor?.chain().focus().toggleCode().run()}
className={`p-1 rounded hover:bg-gray-100 ${editor?.isActive("codeBlock") ? "bg-gray-200" : ""
}`}
>
<Code className="h-4 w-4" />
</button>
</div>
</BubbleMenu>
<EditorContent
className={`min-w-[100px] w-full max-md:pointer-events-none ${editor?.getText() ? "" : `hover:outline hover:outline-gray-400`
} `}
onBlur={() => {
const markdown = editor?.storage.markdown.getMarkdown();
console.log("🔍 markdown", markdown);
}}
editor={editor}
/>
</div>
);
};
export default TipTapEditor;

View file

@ -1,14 +0,0 @@
import { useTypewriter } from "@/hooks/useTypeWriter";
const Typewriter = ({ text, speed }: { text: string; speed: number }) => {
const { displayText, isCursorVisible } = useTypewriter(text, speed, true);
return (
<p>
{displayText}
{isCursorVisible && <span className="slide-title">|</span>}
</p>
);
};
export default Typewriter;

View file

@ -1,104 +0,0 @@
import React, { useState, useEffect } from "react";
import ChartEditor from "../ChartEditor";
import { StoreChartData } from "../../utils/chartDataTransforms";
import { useDispatch, useSelector } from "react-redux";
import {
ChartSettings,
updateSlideChart,
updateSlideChartSettings,
} from "@/store/slices/presentationGeneration";
import { renderChart } from "../slide_config";
import { RootState } from "@/store/store";
interface AllChartProps {
chartData: StoreChartData;
slideIndex: number;
}
const AllChart = ({
chartData: initialChartData,
slideIndex,
}: AllChartProps) => {
const dispatch = useDispatch();
const [localChartData, setLocalChartData] =
useState<StoreChartData>(initialChartData);
const [isEditorOpen, setIsEditorOpen] = useState(false);
const { currentColors } = useSelector((state: RootState) => state.theme);
// Use chart settings from the Redux store
const chartSettings = useSelector((state: RootState) => {
const slide =
state.presentationGeneration?.presentationData?.slides[slideIndex];
const style = slide?.content.graph.style || {};
return Object.keys(
style === null || style === undefined ? {} : (style as ChartSettings)
).length > 0
? (style as ChartSettings)
: {
showLegend: false,
showGrid: false,
showAxisLabel: true,
showDataLabel: true,
dataLabel: {
dataLabelPosition:
slide?.content.graph.type === "pie"
? ("Outside" as const)
: ("Inside" as const),
dataLabelAlignment: "Center" as const,
},
};
});
useEffect(() => {
setLocalChartData(initialChartData);
}, [initialChartData]);
const handleChartClick = () => {
setIsEditorOpen(true);
};
const onChartDataChange = (newData: StoreChartData) => {
dispatch(updateSlideChart({ index: slideIndex, chart: newData }));
setLocalChartData(newData);
};
const onChartSettingsChange = (newSettings: ChartSettings) => {
dispatch(
updateSlideChartSettings({
index: slideIndex,
chartSettings: newSettings,
})
);
};
return (
<>
<div
onClick={handleChartClick}
data-slide-element
data-element-type="graph"
data-graph-type={localChartData && localChartData.type}
data-element-id={`slide-group-${slideIndex}-graph`}
className="w-full h-full min-h-[200px] lg:min-h-[300px] max-md:pointer-events-none cursor-pointer hover:opacity-90 transition-opacity relative"
>
{renderChart(localChartData, false, currentColors ?? [], chartSettings)}
{/* <img src={`/Banner.png`} alt={localChartData.type} className="w-full h-full object-cover" /> */}
</div>
{localChartData && (
<ChartEditor
chartSettings={chartSettings}
setChartSettings={onChartSettingsChange}
isOpen={isEditorOpen}
onClose={() => setIsEditorOpen(false)}
chartData={localChartData}
onChartDataChange={onChartDataChange}
/>
)}
</>
);
};
export default AllChart;

View file

@ -1,82 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import ImageEditor from "../ImageEditor";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import SlideFooter from "./SlideFooter";
interface Type1LayoutProps {
title: string;
description: string;
slideId: string | null;
images: string[];
slideIndex: number;
image_prompts?: string[] | null;
properties?: null | any;
}
const Type1Layout = ({
title,
description,
images,
slideId,
slideIndex,
image_prompts,
properties,
}: Type1LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
return (
<div
className="slide-container w-full rounded-sm max-w-[1280px] shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] max-h-[720px] flex items-center aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-id={slideId}
data-slide-index={slideIndex}
data-slide-type="1"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3 sm:gap-8 md:gap-12 lg:gap-16 w-full">
<div className=" flex flex-col w-full items-start justify-center space-y-1 md:space-y-2 lg:space-y-6">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description-body`}
type="description-body"
content={description}
/>
</div>
<ImageEditor
elementId={`slide-${slideIndex}-image`}
slideIndex={slideIndex}
initialImage={images[0]}
title={title}
promptContent={image_prompts?.[0]}
properties={properties}
/>
{/* {imagePosition === 'left' ? (
<>
<ImageSection />
<ContentSection />
</>
) : (
<>
<ContentSection />
<ImageSection />
</>
)} */}
</div>
<SlideFooter />
</div>
);
};
export default Type1Layout;

View file

@ -1,358 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { MoreVertical, Plus } from "lucide-react";
import ElementMenu from "../ElementMenu";
import { useSelector } from "react-redux";
import { numberTranslations } from "../../utils/others";
import { RootState } from "@/store/store";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type2LayoutProps {
title: string;
body: Array<{
heading: string;
description: string;
}>;
slideId: string | null;
slideIndex: number;
language: string;
design_index: number;
}
const Type2Layout = ({
title,
body,
slideId,
slideIndex,
design_index,
language,
}: Type2LayoutProps) => {
const { currentColors } = useSelector(
(state: RootState) => state.theme
);
const { handleAddItem, handleDeleteItem, handleVariantChange } =
useSlideOperations(slideIndex);
const onAddItem = () => {
if (body.length < 4) {
handleAddItem({ item: { heading: "", description: "" } });
}
};
const onDeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
}
};
const VariantMenu = () => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="absolute top-0 -left-7 hidden md:block p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50">
<MoreVertical className="w-4 h-4 text-black" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[180px] p-2">
<DropdownMenuItem
onClick={() => handleVariantChange({ variant: 1 })}
className={`px-3 py-2 cursor-pointer ${design_index === 1 ? "bg-blue-50" : ""
}`}
>
Default Layout
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleVariantChange({ variant: 2 })}
className={`px-3 py-2 cursor-pointer ${design_index === 2 ? "bg-blue-50" : ""
}`}
>
Numbered Layout
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleVariantChange({ variant: 3 })}
className={`px-3 py-2 cursor-pointer ${design_index === 3 ? "bg-blue-50" : ""
}`}
>
Timeline Layout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
const isGridLayout = body.length >= 4;
const renderContent = () => {
if (design_index === 3) {
return (
<div className="w-full flex flex-col relative group mt-4 lg:mt-16">
<div className="absolute -inset-[2px] border-2 border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
{/* Three Dots Icon */}
<VariantMenu />
{/* Plus Icon */}
<button
onClick={onAddItem}
className="absolute top-1/2 -right-4 -translate-y-1/2 p-1 rounded-md bg-white shadow-sm opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
{/* Timeline Header with Numbers and Line */}
<div className="relative flex justify-between w-[85%] mx-auto items-center mb-8 px-8">
{/* Horizontal Line */}
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="line"
data-element-id={`slide-${slideIndex}-horizontal-line`}
className="absolute top-1/2 w-[87%] left-1/2 -translate-x-1/2 h-[2px] "
style={{
backgroundColor: currentColors.iconBg,
}}
/>
{/* Timeline Numbers */}
{body.map((_, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="filledbox"
data-element-id={`slide-${slideIndex}-timeline-number-${index}`}
key={`timeline-${index}`}
className="relative z-10 w-12 h-12 rounded-full px-1 text-white flex items-center justify-center font-bold text-lg"
style={{
backgroundColor: currentColors.iconBg,
}}
>
<span
data-slide-element
data-slide-index={slideIndex}
data-element-type="text"
data-element-id={`slide-${slideIndex}-timeline-number-text-${index}`}
>
{numberTranslations[language][index || 0]}
</span>
</div>
))}
</div>
{/* Timeline Content */}
<div className="flex justify-between gap-8">
{body.map((item, index) => (
<div
key={`${body.length}-${index}`}
className="flex-1 text-center relative"
>
<ElementMenu index={index} handleDeleteItem={onDeleteItem} />
<div className="space-y-4">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
/>
</div>
</div>
))}
</div>
</div>
);
}
if (isGridLayout) {
return (
<div
className={`grid grid-cols-1 lg:grid-cols-2 relative group ${design_index === 2 ? "gap-4 lg:gap-8" : "gap-6 md:gap-12"
} mt-4 lg:mt-12`}
>
{/* Hover Border and Icons for entire layout */}
<div className="absolute -inset-[2px] border-2 border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
{/* Three Dots Icon */}
<VariantMenu />
{body.map((item, index) => (
<div
key={index}
data-slide-element
data-slide-index={slideIndex}
data-element-type={design_index === 2 ? "slide-box" : ""}
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow:
design_index === 2
? "0 2px 10px 0 rgba(43, 43, 43, 0.2)"
: "",
}}
className={`w-full relative group ${design_index === 2
? "slide-box shadow-lg rounded-lg p-3 lg:p-6"
: ""
}`}
>
<div className="flex gap-3 ">
{design_index === 2 && (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="text"
data-element-id={`slide-${slideIndex}-item-${index}-number`}
className=" text-[32px] leading-[40px] px-1 font-bold mb-4"
style={{
color: currentColors.iconBg,
}}
>
{
numberTranslations[
language as keyof typeof numberTranslations
][index]
}
</div>
)}
<ElementMenu index={index} handleDeleteItem={onDeleteItem} />
<div className="space-y-2">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
bodyIdx={index}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
bodyIdx={index}
/>
</div>
</div>
</div>
))}
</div>
);
}
// Horizontal layout for 2-3 items
return (
<div
className={`flex flex-col lg:flex-row mt-4 lg:mt-12 w-full relative group ${design_index === 2 ? "gap-4 lg:gap-8" : "gap-12"
}`}
>
{/* Hover Border and Icons for entire layout */}
<div className="absolute -inset-[2px] hidden lg:block border-2 border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
{/* Three Dots Icon */}
<VariantMenu />
{/* Plus Icon */}
{body.length < 4 && (
<button
onClick={onAddItem}
className="absolute top-1/2 -right-4 hidden lg:block -translate-y-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
)}
{body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
key={`${body.length}-${index}`}
style={{
boxShadow:
design_index === 2 ? "0 2px 10px 0 rgba(43, 43, 43, 0.2)" : "",
}}
className={`w-full relative ${design_index === 2
? "slide-box shadow-lg rounded-lg p-3 lg:p-6"
: ""
}`}
>
<ElementMenu index={index} handleDeleteItem={onDeleteItem} />
{design_index === 2 && (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="text"
data-element-id={`slide-${slideIndex}-item-${index}-number`}
className=" text-[32px] leading-[40px] font-semibold lg:mb-4"
style={{
color: currentColors.iconBg,
}}
>
{
numberTranslations[
language as keyof typeof numberTranslations
][index]
}
</div>
)}
<div className="space-y-2 lg:space-y-4">
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
/>
</div>
</div>
))}
</div>
);
};
return (
<div
className="slide-container rounded-sm max-w-[1280px]w-full shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-element-type="slide-container"
data-slide-type="2"
data-element-id={`slide-${slideIndex}-container`}
data-design-index={design_index}
>
<div className="text-center lg:pb-8 w-full">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
isAlingCenter={true}
content={title}
/>
</div>
{renderContent()}
<SlideFooter />
</div>
);
};
export default Type2Layout;

View file

@ -1,147 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import ImageEditor from "../ImageEditor";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store/store";
import ElementMenu from "../ElementMenu";
import { Plus } from "lucide-react";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type4LayoutProps {
title: string;
body: Array<{
heading: string;
description: string;
}>;
slideId: string | null;
images: string[];
slideIndex: number;
image_prompts?: string[] | null;
properties?: null | any;
}
const Type4Layout = ({
title,
body,
slideId,
images,
slideIndex,
image_prompts,
properties,
}: Type4LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
const {
handleAddItem,
handleDeleteItem,
handleImageChange,
handleDeleteImage,
} = useSlideOperations(slideIndex);
const AddItem = () => {
if (body.length < 3) {
handleImageChange({ imageUrl: "", imageIndex: slideIndex });
handleAddItem({
item: { heading: "Enter Heading", description: "Enter Description" },
});
}
};
const DeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
handleDeleteImage({ imageIndex: index });
}
};
const getGridCols = (length: number) => {
switch (length) {
case 1: return 'lg:grid-cols-1';
case 2: return 'lg:grid-cols-2';
case 3: return 'lg:grid-cols-3';
case 4: return 'lg:grid-cols-4';
// Add more cases as needed
default: return 'lg:grid-cols-1';
}
}
return (
<div
className="slide-container shadow-lg rounded-sm w-full max-w-[1280px] px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] font-inter flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-slide-type="4"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="text-center mb-4 lg:mb-16 w-full">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
isAlingCenter={true}
content={title}
/>
</div>
<div
className={`grid grid-cols-1 lg:grid-cols-2 ${getGridCols(body.length)} gap-3 lg:gap-6 w-full relative group`}
>
<div className="absolute -inset-[2px] border-2 border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute -bottom-4 left-1/2 -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-black" />
</button>
{body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
key={index}
className="flex slide-box flex-col w-full rounded-lg overflow-hidden relative group"
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<ImageEditor
elementId={`slide-${slideIndex}-item-${index}-image`}
slideIndex={slideIndex}
initialImage={images[index]}
className="max-md:h-[140px] max-lg:h-[180px] h-48 w-full rounded-t-lg rounded-b-none"
title={item.heading}
promptContent={image_prompts?.[index]}
imageIdx={index}
properties={properties}
/>
<div className="space-y-2 p-3 lg:p-6">
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
/>
</div>
</div>
))}
</div>
<SlideFooter />
</div>
);
};
export default Type4Layout;

View file

@ -1,73 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import { RootState } from "@/store/store";
import { useSelector } from "react-redux";
import AllChart from "./AllChart";
import SlideFooter from "./SlideFooter";
interface Type5LayoutProps {
title: string;
description: string;
slideId: string | null;
chartComponent?: React.ReactNode;
graphData?: any;
slideIndex: number;
isFullSizeGraph?: boolean;
}
const Type5Layout = ({
title,
description,
slideId,
chartComponent,
graphData,
slideIndex,
isFullSizeGraph = false,
}: Type5LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
return (
<div
className="slide-container font-inter rounded-sm w-full max-w-[1280px] px-3 py-[10px] sm:px-12 lg:px-20 sm:py-[40px] lg:py-[86px] shadow-lg max-h-[720px] flex flex-col items-center justify-center aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-slide-type="5"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
isAlingCenter={false}
/>
<div
className={`flex w-full items-center ${isFullSizeGraph
? " flex-col mt-4 lg:mt-10 gap-2 sm:gap-4 md:gap-6 lg:gap-10"
: "mt-4 lg:mt-16 gap-4 sm:gap-8 md:gap-12 lg:gap-16 "
} `}
>
<div className={` w-full`}>
<AllChart chartData={graphData} slideIndex={slideIndex} />
</div>
<div className={` w-full text-center`}>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description-body`}
type="description-body"
isAlingCenter={isFullSizeGraph}
content={description}
/>
</div>
</div>
<SlideFooter />
</div>
);
};
export default Type5Layout;

View file

@ -1,139 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import { Plus } from "lucide-react";
import ElementMenu from "../ElementMenu";
import { useDispatch, useSelector } from "react-redux";
import { numberTranslations } from "../../utils/others";
import { RootState } from "@/store/store";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type6LayoutProps {
title: string;
description: string;
body: Array<{
heading: string;
description: string;
}>;
slideId: string | null;
slideIndex: number;
language: string;
}
const Type6Layout = ({
title,
description,
body,
slideId,
slideIndex,
language,
}: Type6LayoutProps) => {
const dispatch = useDispatch();
const { currentColors } = useSelector((state: RootState) => state.theme);
const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex);
const AddItem = () => {
if (body.length < 3) {
handleAddItem({ item: { heading: "", description: "" } });
}
};
const DeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
}
};
return (
<div
className="slide-container rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-container"
data-slide-type="6"
data-element-id={`slide-${slideIndex}-container`}
data-slide-id={slideId}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="flex flex-col lg:flex-row gap-4 sm:gap-18 d:gap-16 items-center w-full">
{/* Left section - Description */}
<div className="lg:w-1/2 lg:space-y-8">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description`}
type="description"
content={description}
/>
</div>
{/* Right section - Numbered items */}
<div className="lg:w-1/2 relative group">
<div className="absolute -inset-[2px] border-2 hidden md:block border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute -bottom-4 left-1/2 hidden md:block -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-black" />
</button>
<div className="space-y-3 lg:space-y-6">
{body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
key={`${body.length}-${index}`}
className="slide-box rounded-lg p-3 lg:p-6 relative group"
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<div className="flex gap-6">
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="text"
data-element-id={`slide-${slideIndex}-item-${index}-number`}
className=" text-[26px] lg:text-[32px] leading-[40px] px-1 font-bold mb-4"
style={{
color: currentColors.iconBg,
}}
>
{numberTranslations[language][index || 0]}
</div>
<div className="space-y-1">
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
<SlideFooter />
</div>
);
};
export default Type6Layout;

View file

@ -1,209 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import IconsEditor from "../IconsEditor";
import { Plus } from "lucide-react";
import ElementMenu from "../ElementMenu";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type2LayoutProps {
title: string;
body: Array<{
heading: string;
description: string;
}>;
icons: string[];
slideIndex: number;
slideId: string | null;
icon_queries?: Array<{ queries: string[] }> | null;
}
const Type7Layout = ({
title,
body,
icons,
slideIndex,
slideId,
icon_queries,
}: Type2LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex);
const AddItem = () => {
if (body.length < 4) {
handleAddItem({ item: { heading: "", description: "" } });
}
};
const DeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
}
};
const getGridCols = (length: number) => {
switch (length) {
case 1: return 'lg:grid-cols-1';
case 2: return 'lg:grid-cols-2';
case 3: return 'lg:grid-cols-3';
case 4: return 'lg:grid-cols-4';
case 5: return 'lg:grid-cols-5';
case 6: return 'lg:grid-cols-6';
case 7: return 'lg:grid-cols-7';
// Add more cases as needed
default: return 'lg:grid-cols-1';
}
}
const isGridLayout = body.length >= 4;
const renderContent = () => {
if (isGridLayout) {
return (
<div
className={`grid grid-cols-1 ${body.length > 4 ? 'md:grid-cols-3' : 'md:grid-cols-2'} gap-4 sm:gap-6 lg:gap-8 mt-4 lg:mt-12 w-full relative group`}
>
<div className="absolute hidden lg:block -inset-[2px] border-2 border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute hidden lg:block -bottom-4 left-1/2 -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
{body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
key={`${body.length}-${index}`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className={` w-full slide-box rounded-lg p-3 lg:p-6 relative group`}
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<div className="flex items-start gap-2 mg:gap-4">
<div className="flex-shrink-0 lg:w-16">
<IconsEditor
hasBg={true}
backgroundColor={currentColors.iconBg}
icon={icons[index]}
index={index}
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-icon-${index}`}
icon_prompt={icon_queries?.[index]?.queries || []}
/>
</div>
<div>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
bodyIdx={index}
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
bodyIdx={index}
content={item.description}
/>
</div>
</div>
</div>
))}
</div>
);
}
// Horizontal layout for 2-3 items
return (
<div
className={`grid grid-cols-1 sm:grid-cols-2 ${getGridCols(body.length)} w-full gap-3 lg:gap-8 mt-4 lg:mt-12 relative group`}
>
<div className="absolute -inset-[2px] border-2 hidden lg:block border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute -bottom-4 hidden lg:block left-1/2 -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
{body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
key={`${body.length}-${index}`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
className={`w-full slide-box rounded-lg p-3 lg:p-6 relative group`}
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<IconsEditor
hasBg={true}
backgroundColor={currentColors.iconBg}
icon={icons[index]}
index={index}
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-icon-${index}`}
icon_prompt={icon_queries?.[index]?.queries || []}
/>
<div className="lg:space-y-4 mt-2 lg:mt-4">
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
content={item.description}
/>
</div>
</div>
))}
</div>
);
};
return (
<div
className="slide-container rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-slide-type="7"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
data-slide-id={slideId}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="text-center sm:pb-2 lg:pb-8 w-full">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
isAlingCenter={true}
content={title}
/>
</div>
{renderContent()}
<SlideFooter />
</div>
);
};
export default Type7Layout;

View file

@ -1,185 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import IconsEditor from "../IconsEditor";
import { Plus } from "lucide-react";
import ElementMenu from "../ElementMenu";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type6LayoutProps {
title: string;
description: string;
body: Array<{
heading: string;
description: string;
}>;
icons: string[];
slideId: string | null;
slideIndex: number;
icon_queries?: Array<{ queries: string[] }> | null;
}
const Type8Layout = ({
title,
description,
body,
icons,
slideIndex,
slideId,
icon_queries,
}: Type6LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex);
const AddItem = () => {
if (body.length < 3) {
handleAddItem({ item: { heading: "", description: "" } });
}
};
const DeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
}
};
return (
<div
className="slide-container shadow-lg w-full max-w-[1280px] rounded-sm font-inter px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-slide-type="8"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-8 lg:gap-16 items-center w-full">
{/* Left section - Description */}
<div className="space-y-2 lg:space-y-6">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description`}
type="description"
content={description}
/>
</div>
{/* Right section - Numbered items */}
<div className=" relative group ">
<div className="absolute -inset-[2px] hidden md:block border-2 border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute -bottom-4 left-1/2 hidden md:block -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
<div className="space-y-4 lg:space-y-8">
{body && body.length > 0 && body.length === 2
? body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
key={`${body.length}-${index}`}
className="slide-box rounded-lg p-3 lg:p-6 relative group"
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<IconsEditor
icon={icons[index]}
index={index}
backgroundColor={currentColors.iconBg}
hasBg={true}
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-icon-${index}`}
icon_prompt={icon_queries?.[index]?.queries || []}
/>
<div className="space-y-1 lg:space-y-3 lg:mt-3">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
bodyIdx={index}
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
bodyIdx={index}
content={item.description}
/>
</div>
</div>
))
: body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
key={`${body.length}-${index}`}
className="slide-box rounded-lg p-3 lg:p-6 relative group"
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<div className="flex items-start gap-4">
<div className="w-[32px] md:w-[64px] h-[32px] md:h-[64px]">
<IconsEditor
className="rounded-lg"
icon={icons[index]}
index={index}
backgroundColor={currentColors.iconBg}
hasBg={true}
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-icon-${index}`}
icon_prompt={icon_queries?.[index]?.queries || []}
/>
</div>
<div className="lg:space-y-3 ">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
bodyIdx={index}
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
bodyIdx={index}
content={item.description}
/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
<SlideFooter />
</div>
);
};
export default Type8Layout;

View file

@ -1,139 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import { Plus } from "lucide-react";
import ElementMenu from "../ElementMenu";
import { useSelector } from "react-redux";
import { numberTranslations } from "../../utils/others";
import { RootState } from "@/store/store";
import AllChart from "./AllChart";
import { useSlideOperations } from "../../hooks/use-slide-operations";
import SlideFooter from "./SlideFooter";
interface Type9LayoutProps {
title: string;
body: Array<{
heading: string;
description: string;
}>;
graphData?: any;
slideId: string | null;
language: string;
slideIndex: number;
}
const Type9Layout = ({
title,
body,
graphData,
slideId,
slideIndex,
language,
}: Type9LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
const { handleAddItem, handleDeleteItem } = useSlideOperations(slideIndex);
const AddItem = () => {
if (body.length < 3) {
handleAddItem({ item: { heading: "", description: "" } });
}
};
const DeleteItem = (index: number) => {
if (body.length > 2) {
handleDeleteItem({ itemIndex: index });
}
};
return (
<div
className="slide-container rounded-sm w-full max-w-[1280px] font-inter px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] shadow-lg flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative mx-auto"
data-slide-element
data-slide-index={slideIndex}
data-slide-type="9"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
data-slide-id={slideId}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="grid grid-cols-1 lg:grid-cols-2 w-full items-center gap-2 sm:gap-4 md:gap-8 lg:gap-16">
{/* Left section - Chart */}
<div className="space-y-2 lg:space-y-14">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
/>
<div className=" flex items-center justify-center">
<div className="w-full">
<AllChart chartData={graphData} slideIndex={slideIndex} />
</div>
</div>
</div>
{/* Right section - Numbered items */}
<div className=" relative group">
<div className="absolute -inset-[2px] border-2 hidden lg:block border-transparent group-hover:border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button
onClick={AddItem}
className="absolute left-1/2 hidden lg:block -bottom-4 -translate-x-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
<div className="space-y-4 lg:space-y-8">
{body.length > 0 &&
body.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: "0 2px 10px 0 rgba(43, 43, 43, 0.2)",
}}
key={`${body.length}-${index}`}
className="slide-box relative rounded-lg p-3 lg:p-6"
>
<ElementMenu index={index} handleDeleteItem={DeleteItem} />
<div className="flex gap-3 lg:gap-6 ">
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="text"
data-element-id={`slide-${slideIndex}-item-${index}-number`}
className=" text-[32px] leading-[40px] px-1 font-bold mb-4"
style={{
color: currentColors.iconBg,
}}
>
{numberTranslations[language][index || 0]}
</div>
<div className="lg:space-y-2 ">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-heading`}
type="heading"
bodyIdx={index}
content={item.heading}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-item-${index}-description`}
type="heading-description"
bodyIdx={index}
content={item.description}
/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
<SlideFooter />
</div>
);
};
export default Type9Layout;

View file

@ -13,7 +13,6 @@
"use client";
import styles from "../styles/main.module.css";
import { useEffect, useState, useRef, useMemo } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { OverlayLoader } from "@/components/ui/overlay-loader";
@ -29,7 +28,6 @@ import { getIconFromFile } from "../../utils/others";
import { ChevronRight, PanelRightOpen, X } from "lucide-react";
import ToolTip from "@/components/ToolTip";
import Header from "@/app/dashboard/components/Header";
import { useLayout } from "../../context/LayoutContext";
// Types
interface LoadingState {
@ -214,7 +212,7 @@ const DocumentsPreviewPage: React.FC = () => {
if (!isOpen) return null;
return (
<div className={`${styles.sidebar} fixed xl:relative w-full z-50 xl:z-auto
<div className={`border-r border-gray-200 fixed xl:relative w-full z-50 xl:z-auto
transition-all duration-300 ease-in-out max-w-[200px] md:max-w-[300px] h-[85vh] rounded-md p-5`}>
<X
onClick={() => setIsOpen(false)}
@ -230,7 +228,7 @@ const DocumentsPreviewPage: React.FC = () => {
<div
key={key}
onClick={() => updateSelectedDocument(key)}
className={`${selectedDocument === key ? styles.selected_border : ""
className={`${selectedDocument === key ? 'border border-blue-500' : ""
} flex p-2 rounded-sm gap-2 items-center cursor-pointer`}
>
<img
@ -251,7 +249,7 @@ const DocumentsPreviewPage: React.FC = () => {
};
return (
<div className={`${styles.wrapper} min-h-screen flex flex-col w-full`}>
<div className={`bg-white/90 min-h-screen flex flex-col w-full`}>
<OverlayLoader
show={showLoading.show}
text={showLoading.message}

View file

@ -1,39 +0,0 @@
.wrapper {
background: rgba(233, 232, 248, 1);
}
.sidebar {
border-right: 1px solid rgba(221, 220, 237, 1);
background: white;
}
.report_icon_box {
background: rgba(234, 241, 255, 1);
}
.selected_border {
border: 1px solid rgba(0, 0, 255, 1);
}
.unselected_border {
border: 1px solid rgba(237, 237, 237, 1);
}
.custom_scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom_scrollbar::-webkit-scrollbar-thumb {
background-color: #4A90E2;
border-radius: 6px;
cursor: pointer;
}
.custom_scrollbar::-webkit-scrollbar-track {
background-color: #F0F0F0;
}
.custom_scrollbar::-webkit-scrollbar-corner {
background-color: transparent;
}

View file

@ -1,56 +0,0 @@
import { useDispatch } from "react-redux";
import {
addSlideBodyItem,
deleteSlideBodyItem,
updateSlideVariant,
updateSlideImage,
deleteSlideImage,
// Import other slide operation actions as needed
} from "@/store/slices/presentationGeneration";
export const useSlideOperations = (slideIndex: number) => {
const dispatch = useDispatch();
const handleAddItem = ({ item }: { item: any }) => {
dispatch(addSlideBodyItem({ index: slideIndex, item }));
};
const handleDeleteItem = ({ itemIndex }: { itemIndex: number }) => {
dispatch(deleteSlideBodyItem({ index: slideIndex, itemIdx: itemIndex }));
};
const handleVariantChange = ({ variant }: { variant: number }) => {
dispatch(updateSlideVariant({ index: slideIndex, variant }));
};
const handleImageChange = ({
imageUrl,
imageIndex,
}: {
imageUrl: string;
imageIndex: number;
}) => {
dispatch(
updateSlideImage({
index: slideIndex,
imageIdx: imageIndex,
image: imageUrl,
})
);
};
const handleDeleteImage = ({ imageIndex }: { imageIndex: number }) => {
dispatch(deleteSlideImage({ index: slideIndex, imageIdx: imageIndex }));
};
// Add other common slide operations here
return {
handleAddItem,
handleDeleteItem,
handleVariantChange,
handleImageChange,
handleDeleteImage,
};
};

View file

@ -1,12 +1,9 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Menu,
Palette,
SquareArrowOutUpRight,
Play,
Loader2,
ExternalLink,
} from "lucide-react";
import React, { useState } from "react";
import Wrapper from "@/components/Wrapper";
@ -16,38 +13,21 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import UserAccount from "../../components/UserAccount";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { OverlayLoader } from "@/components/ui/overlay-loader";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { useDispatch, useSelector } from "react-redux";
import Link from "next/link";
import { ThemeType } from "@/app/(presentation-generator)/upload/type";
import {
setTheme,
setThemeColors,
defaultColors,
serverColors,
} from "../../store/themeSlice";
import CustomThemeSettings from "../../components/CustomThemeSettings";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RootState } from "@/store/store";
import { toast } from "@/hooks/use-toast";
import ThemeSelector from "./ThemeSelector";
import Modal from "./Modal";
import Announcement from "@/components/Announcement";
import { getFontLink, getStaticFileUrl } from "../../utils/others";
import { getStaticFileUrl } from "../../utils/others";
import { PptxPresentationModel } from "@/types/pptx_models";
import HeaderNav from "../../components/HeaderNab";
const Header = ({
@ -61,66 +41,13 @@ const Header = ({
const [showLoader, setShowLoader] = useState(false);
const router = useRouter();
const [showCustomThemeModal, setShowCustomThemeModal] = useState(false);
const [showDownloadModal, setShowDownloadModal] = useState(false);
const [downloadPath, setDownloadPath] = useState("");
const { currentTheme, currentColors } = useSelector(
(state: RootState) => state.theme
);
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
const dispatch = useDispatch();
const handleThemeSelect = async (value: string) => {
if (isStreaming) return;
if (value === "custom") {
setShowCustomThemeModal(true);
return;
} else {
const themeType = value as ThemeType;
const themeColors = serverColors[themeType] || defaultColors[themeType];
if (themeColors) {
try {
// Update UI
dispatch(setTheme(themeType));
dispatch(setThemeColors({ ...themeColors, theme: themeType }));
// Set CSS variables
const root = document.documentElement;
root.style.setProperty(
`--${themeType}-slide-bg`,
themeColors.slideBg
);
root.style.setProperty(
`--${themeType}-slide-title`,
themeColors.slideTitle
);
root.style.setProperty(
`--${themeType}-slide-heading`,
themeColors.slideHeading
);
root.style.setProperty(
`--${themeType}-slide-description`,
themeColors.slideDescription
);
root.style.setProperty(
`--${themeType}-slide-box`,
themeColors.slideBox
);
} catch (error) {
console.error("Failed to update theme:", error);
toast({
title: "Error updating theme",
description:
"Failed to update the presentation theme. Please try again.",
variant: "destructive",
});
}
}
}
};
const get_presentation_pptx_model = async (id: string): Promise<PptxPresentationModel> => {
const response = await fetch(`/api/presentation_to_pptx_model?id=${id}`);
@ -212,15 +139,8 @@ const Header = ({
<img src="/pptx.svg" alt="pptx export" width={30} height={30} />
Export as PPTX
</Button>
{/* <div className={`w-full ${mobile ? "bg-white py-2 rounded-lg" : ""}`}>
<JSPowerPointExtractor />
</div> */}
<p className={`text-sm pt-3 border-t border-gray-300 ${mobile ? "border-none text-white font-semibold" : ""}`}>
Font Used:
<a className={`text-blue-500 flex items-center gap-1 ${mobile ? "mt-2 py-2 px-4 bg-white rounded-lg w-fit" : ""}`} href={getFontLink(currentColors.fontFamily).link || ''} target="_blank" rel="noopener noreferrer">
{getFontLink(currentColors.fontFamily).name || ''} <ExternalLink className="w-4 h-4" />
</a>
</p>
</div>
);
@ -282,68 +202,16 @@ const Header = ({
{isStreaming && (
<Loader2 className="animate-spin text-white font-bold w-6 h-6" />
)}
<Select value={currentTheme} onValueChange={handleThemeSelect}>
<SelectTrigger className="w-[160px] bg-[#6358fd] text-white border-none hover:bg-[#5146E5] transition-colors">
<div className="flex items-center gap-2">
<Palette className="w-4 h-4" />
<span>Change Theme</span>
</div>
</SelectTrigger>
<SelectContent className="w-[300px] p-0">
<ThemeSelector
onSelect={handleThemeSelect}
selectedTheme={currentTheme}
/>
</SelectContent>
</Select>
{/* Custom Theme Modal */}
<Modal
isOpen={showCustomThemeModal}
onClose={() => setShowCustomThemeModal(false)}
title="Custom Theme Colors"
>
<CustomThemeSettings
onClose={() => setShowCustomThemeModal(false)}
presentationId={presentation_id}
/>
</Modal>
<MenuItems mobile={false} />
<UserAccount />
<HeaderNav />
</div>
{/* Mobile Menu */}
<div className="lg:hidden flex items-center gap-4">
<UserAccount />
<Sheet>
<SheetTrigger asChild>
<button className="text-white">
<Menu className="h-6 w-6" />
</button>
</SheetTrigger>
<SheetContent side="right" className="bg-[#5146E5] border-none p-4">
<div className="flex flex-col gap-6 mt-10">
<Select onValueChange={handleThemeSelect}>
<SelectTrigger className="w-full bg-[#6358fd] flex justify-center gap-2 text-white border-none">
<Palette className="w-4 h-4 mr-2" />
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">Light Theme</SelectItem>
<SelectItem value="dark">Dark Theme</SelectItem>
<SelectItem value="royal_blue">Royal Blue Theme</SelectItem>
<SelectItem value="cream">Cream Theme</SelectItem>
<SelectItem value="dark_pink">Dark Pink Theme</SelectItem>
<SelectItem value="light_red">Light Red Theme</SelectItem>
<SelectItem value="faint_yellow">
Faint Yellow Theme
</SelectItem>
<SelectItem value="custom">Custom Theme</SelectItem>
</SelectContent>
</Select>
<MenuItems mobile={true} />
</div>
</SheetContent>
</Sheet>
<HeaderNav />
</div>
</Wrapper>
{/* Download Modal */}

View file

@ -6,7 +6,6 @@ import { Skeleton } from "@/components/ui/skeleton";
import PresentationMode from "../../components/PresentationMode";
import SidePanel from "../components/SidePanel";
import SlideContent from "../components/SlideContent";
import LoadingState from "../../components/LoadingState";
import Header from "../components/Header";
import { Button } from "@/components/ui/button";
import { AlertCircle, Loader2 } from "lucide-react";
@ -18,6 +17,7 @@ import {
useAutoSave
} from "../hooks";
import { PresentationPageProps } from "../types";
import LoadingState from "./LoadingState";
const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id }) => {
// State management
@ -27,10 +27,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
const [error, setError] = useState(false);
const [isMobilePanelOpen, setIsMobilePanelOpen] = useState(false);
// Redux state
const { currentTheme, currentColors } = useSelector(
(state: RootState) => state.theme
);
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
@ -84,7 +81,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
<PresentationMode
slides={presentationData?.slides!}
currentSlide={currentSlide}
currentTheme={currentTheme}
isFullscreen={isFullscreen}
onFullscreenToggle={toggleFullscreen}
onExit={handlePresentExit}
@ -130,7 +127,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
<div
style={{
background: currentColors.background,
background: '#c8c7c9',
}}
className="flex flex-1 relative pt-6"
>

View file

@ -45,9 +45,7 @@ const SidePanel = ({
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
const { currentTheme, currentColors } = useSelector(
(state: RootState) => state.theme
);
const dispatch = useDispatch();
// Use the centralized group layouts hook
@ -159,16 +157,10 @@ const SidePanel = ({
`}
>
<div
data-theme={currentTheme}
style={{
backgroundColor: currentColors.slideBg,
}}
className="min-w-[300px] max-w-[300px] h-[calc(100vh-120px)] rounded-[20px] hide-scrollbar overflow-hidden slide-theme shadow-xl"
className="min-w-[300px] bg-white max-w-[300px] h-[calc(100vh-120px)] rounded-[20px] hide-scrollbar overflow-hidden slide-theme shadow-xl"
>
<div
style={{
backgroundColor: currentColors.slideBg,
}}
className="sticky top-0 z-40 px-6 py-4"
>
<div className="flex items-center justify-between gap-4">

View file

@ -14,8 +14,8 @@ import ToolTip from "@/components/ToolTip";
import { RootState } from "@/store/store";
import { useDispatch, useSelector } from "react-redux";
import { deletePresentationSlide, updateSlide } from "@/store/slices/presentationGeneration";
import NewSlide from "../../components/slide_layouts/NewSlide";
import { useGroupLayouts } from "../../hooks/useGroupLayouts";
import NewSlide from "../../components/NewSlide";
interface SlideContentProps {
slide: any;

View file

@ -1,155 +0,0 @@
import React from "react";
const ThemeSelector = ({
onSelect,
selectedTheme,
}: {
onSelect: (theme: string) => void;
selectedTheme: string;
}) => {
return (
<div className="grid grid-cols-2 gap-3 p-3">
<button
onClick={() => onSelect("light")}
className="group focus:outline-none"
>
<ThemePreview
theme="Light"
color="#F5F5F5"
isSelected={selectedTheme === "light"}
/>
</button>
<button
onClick={() => onSelect("dark")}
className="group focus:outline-none"
>
<ThemePreview
theme="Dark"
color="#1E1E1E"
isSelected={selectedTheme === "dark"}
/>
</button>
<button
onClick={() => onSelect("cream")}
className="group focus:outline-none"
>
<ThemePreview
theme="Cream"
color="#F9F6F0"
isSelected={selectedTheme === "cream"}
/>
</button>
<button
onClick={() => onSelect("royal_blue")}
className="group focus:outline-none"
>
<ThemePreview
theme="Royal Blue"
color="#091433"
isSelected={selectedTheme === "royal_blue"}
/>
</button>
<button
onClick={() => onSelect("faint_yellow")}
className="group focus:outline-none"
>
<ThemePreview
theme="Faint Yellow"
color="#F8F4E8"
isSelected={selectedTheme === "faint_yellow"}
/>
</button>
<button
onClick={() => onSelect("light_red")}
className="group focus:outline-none"
>
<ThemePreview
theme="Light Red"
color="#FFFAFA"
isSelected={selectedTheme === "light_red"}
/>
</button>
<button
onClick={() => onSelect("dark_pink")}
className="group focus:outline-none"
>
<ThemePreview
theme="Dark Pink"
color="#F9E8FF"
isSelected={selectedTheme === "dark_pink"}
/>
</button>
<button
onClick={() => onSelect("custom")}
className="group focus:outline-none"
>
<div className="flex flex-col items-center gap-1 w-full">
<div
className={`w-full h-16 rounded-lg shadow-sm transition-all p-2 flex flex-col justify-between
bg-gradient-to-r from-purple-500 via-pink-500 to-orange-500
${
selectedTheme === "custom"
? "ring-2 ring-[#5146E5] scale-95"
: "hover:scale-105"
}`}
>
<div className="w-12 h-1.5 rounded bg-white/20"></div>
<div className="space-y-1">
<div className="w-16 h-1.5 rounded bg-white/30"></div>
<div className="w-12 h-1.5 rounded bg-white/20"></div>
</div>
</div>
<span
className={`text-xs font-medium ${
selectedTheme === "custom" ? "text-[#5146E5]" : "text-gray-600"
}`}
>
Custom
</span>
</div>
</button>
</div>
);
};
export default ThemeSelector;
const ThemePreview = ({
theme,
color,
isSelected,
}: {
theme: string;
color: string;
isSelected: boolean;
}) => (
<div
className={`flex flex-col items-center gap-1 w-full ${
isSelected ? "scale-95" : ""
}`}
>
<div
className={`w-full h-16 rounded-t-lg rounded-r-lg border shadow-sm transition-all p-2 flex flex-col justify-between
${
isSelected
? "ring-2 ring-[#5146E5] scale-95"
: "hover:scale-105"
}`}
style={{ backgroundColor: color }}
>
<div className="w-12 h-1.5 rounded bg-white/20"></div>
<div className="space-y-1">
<div className="w-16 h-1.5 rounded bg-white/30"></div>
<div className="w-12 h-1.5 rounded bg-white/20"></div>
</div>
</div>
<span
className={`text-xs font-medium ${
isSelected ? "text-[#5146E5]" : "text-gray-600"
}`}
>
{theme}
</span>
</div>
);

View file

@ -1,164 +0,0 @@
import { ThemeType } from "@/app/(presentation-generator)/upload/type";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export const defaultColors = {
light: {
background: "#c8c7c9",
slideBg: "#F2F2F2",
slideTitle: "#000000",
slideHeading: "#1a1a1a",
slideDescription: "#333333",
slideBox: "#ffffff",
iconBg: "#1F1F2D",
chartColors: ["#1F1F2D", "#3F3F5D", "#62628E", "#8F8FB2", "#C0C0D3"],
fontFamily: "var(--font-inter)",
},
dark: {
background: "#000000",
slideBg: "#1E1E1E",
slideTitle: "#ffffff",
slideHeading: "#f5f5f5",
slideDescription: "#e0e0e0",
slideBox: "#2d2d2d",
iconBg: "#5E8CF0",
chartColors: ["#5E8CF0", "#8800ff", "#b200ff", "#d700ff", "#ef00ff"],
fontFamily: "var(--font-inter)",
},
faint_yellow: {
background: "#d9cebc",
slideBg: "#F8F4E8",
slideTitle: "#2C1810",
slideHeading: "#4A3728",
slideDescription: "#665E57",
slideBox: "#FFFFFF",
iconBg: "#281810",
chartColors: ["#281810", "#4A3728", "#665E57", "#665E57", "#665E57"],
fontFamily: "var(--font-inter)",
},
custom: {
background: "#63ceff",
slideBg: "#F4F4F4",
slideTitle: "#1A1A1A",
slideHeading: "#2D2D2D",
slideDescription: "#4A4A4A",
slideBox: "#d8c6c6",
iconBg: "#281810",
chartColors: ["#281810", "#4A3728", "#665E57", "#665E57", "#665E57"],
fontFamily: "var(--font-inter)",
},
cream: {
background: "#DDCFBB",
slideBg: "#F9F6F0",
slideTitle: "#484237",
slideHeading: "#484237",
slideDescription: "#595F6C",
slideBox: "#EEE9DD",
iconBg: "#A6825B",
chartColors: ["#765939", "#A6825B", "#B89B7C", "#CAB49D", "#DBCDBD"],
fontFamily: "var(--font-fraunces)",
},
royal_blue: {
background: "#010103",
slideBg: "#091433",
slideTitle: "#ffffff",
slideHeading: "#ffffff",
slideDescription: "#E6E6E6",
slideBox: "#29136C",
iconBg: "#5E8CF0",
chartColors: ["#5E8CF0", "#496CEB", "#f051b5", "#F7A8FF", "#FCD8FF"],
fontFamily: "var(--font-instrument-sans)",
},
light_red: {
background: "#F8E9E8",
slideBg: "#FFFAFA",
slideTitle: "#181D27",
slideHeading: "#252B37",
slideDescription: "#595F6C",
slideBox: "#F3E8E8",
iconBg: "#F0695F",
chartColors: [
"#F0695F",
"#450808",
"#8F1010",
"#C1392F",
"#EC5555",
"#F49E9E",
],
fontFamily: "var(--font-montserrat)",
},
dark_pink: {
background: "#F3AEED",
slideBg: "#F9E8FF",
slideTitle: "#261827",
slideHeading: "#252B37",
slideDescription: "#6A596C",
slideBox: "#F0D4F7",
iconBg: "#D02CE5",
chartColors: ["#D02CE5", "#B414C9", "#6E1886", "#A724CC", "#C65FE3"],
fontFamily: "var(--font-inria-serif)",
},
};
// Store the server-provided colors
export const serverColors: { [key in ThemeType]?: ThemeColors } = {};
export interface ThemeColors {
background: string;
slideBg: string;
slideTitle: string;
slideHeading: string;
slideDescription: string;
slideBox: string;
iconBg: string;
chartColors: string[];
fontFamily: string;
}
interface ThemeState {
currentTheme: ThemeType;
currentColors: ThemeColors;
isLoading: boolean;
}
const initialState: ThemeState = {
currentTheme: ThemeType.Light,
currentColors: defaultColors.light,
isLoading: false,
};
const themeSlice = createSlice({
name: "theme",
initialState,
reducers: {
setTheme: (state, action: PayloadAction<ThemeType>) => {
state.currentTheme = action.payload;
// Use server colors if available, otherwise fall back to default
state.currentColors =
serverColors[action.payload] || defaultColors[action.payload];
},
setThemeColors: (
state,
action: PayloadAction<Partial<ThemeColors> & { theme: ThemeType }>
) => {
const newColors = { ...state.currentColors, ...action.payload };
state.currentColors = newColors;
state.currentTheme = action.payload.theme;
// Store the colors for this theme
serverColors[action.payload.theme] = newColors;
},
setLoadingState: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
loadSavedTheme: (state, action: PayloadAction<any>) => {
if (action.payload.name === "custom") {
state.currentTheme = ThemeType.Custom;
state.currentColors = action.payload.colors;
serverColors.custom = action.payload.colors;
}
},
},
});
export const { setTheme, setThemeColors, setLoadingState, loadSavedTheme } =
themeSlice.actions;
export default themeSlice.reducer;

View file

@ -1,148 +0,0 @@
/* Light Theme */
.slide-theme[data-theme="light"] {
--slide-bg: var(--light-slide-bg);
--slide-title: var(--light-slide-title);
--slide-heading: var(--light-slide-heading);
--slide-description: var(--light-slide-description);
--slide-box: var(--light-slide-box);
}
/* Dark Theme */
.slide-theme[data-theme="dark"] {
--slide-bg: var(--dark-slide-bg);
--slide-title: var(--dark-slide-title);
--slide-heading: var(--dark-slide-heading);
--slide-description: var(--dark-slide-description);
--slide-box: var(--dark-slide-box);
}
/* Classic Theme */
.slide-theme[data-theme="faint_yellow"] {
--slide-bg: var(--faint_yellow-slide-bg);
--slide-title: var(--faint_yellow-slide-title);
--slide-heading: var(--faint_yellow-slide-heading);
--slide-description: var(--faint_yellow-slide-description);
--slide-box: var(--faint_yellow-slide-box);
}
/* Custom Theme */
.slide-theme[data-theme="custom"] {
--slide-bg: var(--custom-slide-bg);
--slide-title: var(--custom-slide-title);
--slide-heading: var(--custom-slide-heading);
--slide-description: var(--custom-slide-description);
--slide-box: var(--custom-slide-box);
}
/* Royal Blue Theme */
.slide-theme[data-theme="royal_blue"] {
--slide-bg: var(--royal_blue-slide-bg);
--slide-title: var(--royal_blue-slide-title);
--slide-heading: var(--royal_blue-slide-heading);
--slide-description: var(--royal_blue-slide-description);
--slide-box: var(--royal_blue-slide-box);
}
/* Light Red Theme */
.slide-theme[data-theme="light_red"] {
--slide-bg: var(--light_red-slide-bg);
--slide-title: var(--light_red-slide-title);
--slide-heading: var(--light_red-slide-heading);
--slide-description: var(--light_red-slide-description);
--slide-box: var(--light_red-slide-box);
}
/* Dark Pink Theme */
.slide-theme[data-theme="dark_pink"] {
--slide-bg: var(--dark_pink-slide-bg);
--slide-title: var(--dark_pink-slide-title);
--slide-heading: var(--dark_pink-slide-heading);
--slide-description: var(--dark_pink-slide-description);
--slide-box: var(--dark_pink-slide-box);
}
/* Cream Theme */
.slide-theme[data-theme="cream"] {
--slide-bg: var(--cream-slide-bg);
--slide-title: var(--cream-slide-title);
--slide-heading: var(--cream-slide-heading);
--slide-description: var(--cream-slide-description);
--slide-box: var(--cream-slide-box);
}
/* Apply theme styles - Make sure these selectors are more specific */
.slide-theme .slide-container {
background-color: var(--slide-bg);
}
.slide-theme .slide-title {
color: var(--slide-title);
}
.slide-theme .slide-heading {
color: var(--slide-heading);
}
.slide-theme .slide-description {
color: var(--slide-description);
}
.slide-theme .slide-box {
background-color: var(--slide-box);
}
/* Add to your existing CSS */
.slide-theme {
transition: background-color 0.3s ease, color 0.3s ease;
}
.slide-theme .border-color{
border-color: var(--slide-box);
}
@keyframes progress {
0% { width: 5%; }
20% { width: 25%; }
50% { width: 45%; }
75% { width: 75%; }
90% { width: 85%; }
100% { width: 90%; }
}
@keyframes fade-in {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
.animate-progress {
animation: progress 20s ease-out forwards;
}
.animate-fade-in {
animation: fade-in 0.5s ease-out forwards;
}
@keyframes border-dance {
0% {
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
}
100% {
background-position: 100% 0, 100% 100%, 0 100%, 0 0;
}
}
.animate-border {
background-image:
linear-gradient(90deg, #5141e5 50%, transparent 50%), /* top */
linear-gradient(90deg, #5141e5 50%, transparent 50%), /* right */
linear-gradient(90deg, #5141e5 50%, transparent 50%), /* bottom */
linear-gradient(90deg, #5141e5 50%, transparent 50%); /* left */
background-repeat: repeat-x, repeat-y, repeat-x, repeat-y;
background-size: 15px 2px, 2px 15px, 15px 2px, 2px 15px;
background-position: 0 0, 100% 0, 100% 100%, 0 100%;
animation: border-dance 6s infinite linear;
}

View file

@ -1,184 +0,0 @@
"use client";
import React, { useEffect, useState } from "react";
import { Card } from "@/components/ui/card";
import { defaultColors, setTheme, ThemeColors } from "../store/themeSlice";
import Header from "@/app/dashboard/components/Header";
import Wrapper from "@/components/Wrapper";
import { useDispatch } from "react-redux";
import { useRouter } from "next/navigation";
import { ThemeType } from "../upload/type";
import { Button } from "@/components/ui/button";
import { toast } from "@/hooks/use-toast";
interface ThemeCardProps {
name: string;
font: string;
colors: ThemeColors;
selected: boolean;
onClick: () => void;
}
const ThemeCard = ({
name,
font,
colors,
selected,
onClick,
}: ThemeCardProps) => {
return (
<div
className="cursor-pointer group"
style={{ fontFamily: font }}
onClick={onClick}
>
<Card
className={` p-3 md:p-6 h-[120px] md:h-[210px] transition-all duration-200 border-2 ${selected ? " border-4 border-blue-400" : "hover:border-primary"
}`}
style={{ background: colors.slideBg }}
>
<div
className="rounded-lg p-6 h-full"
style={{ background: colors.slideBox }}
>
<div className="flex justify-start items-center h-full">
<div className="space-y-3">
<h3
style={{ color: colors.slideTitle }}
className="text-xl font-semibold"
>
{name}
</h3>
<p
style={{ color: colors.slideDescription }}
className="text-sm text-muted-foreground"
>
This is the body paragraph
</p>
</div>
</div>
</div>
</Card>
</div>
);
};
const ThemePage = () => {
const themes = [
{
name: "Dark Theme",
colors: defaultColors.dark,
type: "dark",
font: "var(--font-inter)",
},
{
name: "Royal Blue Theme",
colors: defaultColors.royal_blue,
type: "royal_blue",
font: "var(--font-instrument-sans)",
},
{
name: "Creme Theme",
colors: defaultColors.cream,
type: "cream",
font: "var(--font-fraunces)",
},
{
name: "Light Red Theme",
colors: defaultColors.light_red,
type: "light_red",
font: "var(--font-montserrat)",
},
{
name: "Dark Pink Theme",
colors: defaultColors.dark_pink,
type: "dark_pink",
font: "var(--font-inria-serif)",
},
{
name: "Light Theme",
colors: defaultColors.light,
type: "light",
font: "var(--font-inter)",
},
{
name: "Faint Yellow Theme",
colors: defaultColors.faint_yellow,
type: "faint_yellow",
font: "var(--font-inter)",
},
];
const dispatch = useDispatch();
const router = useRouter();
const [selectedTheme, setSelectedTheme] = useState<ThemeType | null>(null);
const handleThemeClick = async (theme: ThemeColors, type: string) => {
setSelectedTheme(type as ThemeType);
};
const handleSubmit = () => {
if (!selectedTheme) {
toast({
title: "Please select a theme",
variant: "destructive",
});
return;
}
dispatch(setTheme(selectedTheme as ThemeType));
router.push("/outline");
};
return (
<div>
<Header />
<Wrapper className="py-8 md:w-[90%] xl:w-[70%]">
<h1 className="text-3xl font-bold mb-8">Select a Theme</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 pb-16">
{themes.map((theme, index) => (
<ThemeCard
key={index}
name={theme.name}
font={theme.font}
colors={theme.colors}
selected={selectedTheme === theme.type}
onClick={() => handleThemeClick(theme.colors, theme.type)}
/>
))}
</div>
<Button
onClick={handleSubmit}
className="bg-[#5146E5] fixed bottom-4 left-0 right-0 max-w-[1100px] mx-auto w-full rounded-[32px] text-base sm:text-lg py-4 sm:py-6 transition-all duration-300 font-switzer font-semibold hover:bg-[#5146E5]/80 text-white mt-4"
>
<svg
xmlns="http://www.w3.org/2000/svg"
height={35}
width={35}
viewBox="0 0 25 25"
fill="none"
>
<g clipPath="url(#clip0_1960_939)">
<path
d="M21.217 9.57008L21.463 9.00408C21.8955 8.0028 22.6876 7.2 23.683 6.75408L24.442 6.41508C24.5341 6.37272 24.6121 6.30485 24.6668 6.21951C24.7214 6.13417 24.7505 6.03494 24.7505 5.93358C24.7505 5.83222 24.7214 5.73299 24.6668 5.64765C24.6121 5.56231 24.5341 5.49444 24.442 5.45208L23.725 5.13308C22.7046 4.67446 21.8989 3.84196 21.474 2.80708L21.221 2.19608C21.1838 2.10144 21.119 2.02018 21.035 1.96291C20.951 1.90563 20.8517 1.875 20.75 1.875C20.6483 1.875 20.549 1.90563 20.465 1.96291C20.381 2.02018 20.3162 2.10144 20.279 2.19608L20.026 2.80608C19.6015 3.84116 18.7962 4.67401 17.776 5.13308L17.058 5.45308C16.9662 5.49556 16.8885 5.56342 16.834 5.64865C16.7795 5.73389 16.7506 5.83293 16.7506 5.93408C16.7506 6.03523 16.7795 6.13428 16.834 6.21951C16.8885 6.30474 16.9662 6.3726 17.058 6.41508L17.818 6.75308C18.8132 7.19945 19.6049 8.00261 20.037 9.00408L20.283 9.57008C20.463 9.98408 21.036 9.98408 21.217 9.57008ZM6.55 16.8761H8.704L9.304 15.3761H12.196L12.796 16.8761H14.95L11.75 8.87608H9.75L6.55 16.8761ZM10.75 11.7611L11.396 13.3761H10.104L10.75 11.7611ZM15.75 16.8761V8.87608H17.75V16.8761H15.75ZM3.75 3.87608C3.48478 3.87608 3.23043 3.98144 3.04289 4.16897C2.85536 4.35651 2.75 4.61086 2.75 4.87608V20.8761C2.75 21.1413 2.85536 21.3957 3.04289 21.5832C3.23043 21.7707 3.48478 21.8761 3.75 21.8761H21.75C22.0152 21.8761 22.2696 21.7707 22.4571 21.5832C22.6446 21.3957 22.75 21.1413 22.75 20.8761V11.8761H20.75V19.8761H4.75V5.87608H14.75V3.87608H3.75Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_1960_939">
<rect
width="30"
height="30"
fill="white"
transform="translate(0.75 0.876953)"
/>
</clipPath>
</defs>
</svg>
Generate Outline
</Button>
</Wrapper>
</div>
);
};
export default ThemePage;

View file

@ -1,24 +0,0 @@
import { Skeleton } from '@/components/ui/skeleton'
import React from 'react'
const loading = () => {
return (
<div className='h-screen'>
<Skeleton className='h-24 w-full' />
<div className='max-w-7xl mx-auto'>
<Skeleton className='h-10 mt-10 w-60' />
<div className=' mt-10 mx-auto grid grid-cols-2 gap-6'>
{
Array.from({ length: 6 }).map((_, index) => (
<Skeleton key={index} className='h-[210px] w-full' />
))
}
</div>
</div>
</div>
)
}
export default loading

View file

@ -1,16 +0,0 @@
import React from 'react'
import ThemePage from './ThemePage'
import { Metadata } from 'next'
export const metadata: Metadata = {
title: "Theme Selection | Presenton.ai",
description: "Select a Presenton theme which will be suitable for your presentation",
}
const page = () => {
return (
<ThemePage />
)
}
export default page

View file

@ -1,151 +0,0 @@
import { randomChartGenerator } from "@/lib/utils";
import { Slide } from "../types/slide";
import { generateRandomId } from "./others";
const randomGraph = (presentation_id: string) => {
const randomData = randomChartGenerator();
return {
id: generateRandomId(),
name: "Sales Performance",
type: "bar",
presentation: presentation_id,
postfix: "",
data: randomData,
};
};
export const getEmptySlideContent = (
type: number,
index: number,
presentation_id: string
): Slide => {
const baseSlide: Slide = {
id: generateRandomId(),
type,
index,
design_index: 1,
properties: null,
images: [],
icons: [],
graph_id: null,
presentation: presentation_id,
content: {
title: "",
body: "",
infographics: [],
},
};
const graph = randomGraph(presentation_id);
switch (type) {
case 1:
return {
...baseSlide,
images: [""],
content: {
title: "New Title",
body: "Add your description here",
image_prompts: [""],
},
};
case 2:
return {
...baseSlide,
content: {
title: "New Title",
body: [
{ heading: "First Point", description: "Add description" },
{ heading: "Second Point", description: "Add description" },
],
},
};
case 4:
return {
...baseSlide,
images: ["", "", ""],
content: {
title: "New Title",
body: [
{ heading: "First Item", description: "Add description" },
{ heading: "Second Item", description: "Add description" },
{ heading: "Third Item", description: "Add description" },
],
image_prompts: ["", "", ""],
},
};
case 5:
return {
...baseSlide,
graph_id: graph.id,
content: {
graph: graph,
title: "New Title",
body: "Add your description here",
},
};
case 6:
return {
...baseSlide,
content: {
title: "New Title",
description: "Add your description here",
body: [
{ heading: "First Point", description: "Add description" },
{ heading: "Second Point", description: "Add description" },
],
},
};
case 7:
return {
...baseSlide,
icons: ["", "", ""],
content: {
title: "New Title",
body: [
{ heading: "First Point", description: "Add description" },
{ heading: "Second Point", description: "Add description" },
],
icon_queries: [
{
queries: [""],
},
],
},
};
case 8:
return {
...baseSlide,
icons: ["", "", ""],
content: {
title: "New Title",
description: "Add your description here",
body: [
{ heading: "First Point", description: "Add description" },
{ heading: "Second Point", description: "Add description" },
],
icon_queries: [
{
queries: [""],
},
],
},
};
case 9:
return {
...baseSlide,
graph_id: graph.id,
content: {
graph: graph,
title: "New Subheading",
body: [
{ heading: "First Point", description: "Add description" },
{ heading: "Second Point", description: "Add description" },
],
},
};
default:
return baseSlide;
}
};

View file

@ -1,46 +0,0 @@
import { formatLargeNumber } from "@/lib/utils";
import { Chart } from "@/store/slices/presentationGeneration";
export const formatTooltipValue = (localChartData: Chart, value: number) => {
const formattedValue = formatLargeNumber(value);
if (localChartData.postfix) {
return `${formattedValue}${localChartData.postfix}`;
}
return formattedValue;
};
export const transformedData = (localChartData: Chart) => {
if (!localChartData) return [];
if (!localChartData.data || localChartData.data.categories.length === 0)
return [];
if (localChartData && localChartData.type === "pie") {
return localChartData.data.categories.map((category, index) => ({
name: category,
value: localChartData.data.series[0].data[index],
actualValue: localChartData.data.series[0].data[index],
seriesName: localChartData.data.series[0].name || "Series 1",
}));
} else {
return localChartData.data.categories.map((category, index) => {
const dataPoint: any = { name: category };
localChartData.data.series.forEach((serie) => {
const seriesName = serie.name || "Series";
dataPoint[seriesName] = serie.data[index];
dataPoint[`${seriesName}Value`] = serie.data[index];
});
return dataPoint;
});
}
};
export const formatYAxisTick = (value: number) => {
if (value >= 1_000_000_000_000) {
return `${(value / 1_000_000_000_000).toFixed(0)}T`;
} else if (value >= 1_000_000_000) {
return `${(value / 1_000_000_000).toFixed(0)}B`;
} else if (value >= 1_000_000) {
return `${(value / 1_000_000).toFixed(0)}M`;
} else if (value >= 1_000) {
return `${(value / 1_000).toFixed(0)}k`;
}
return value.toString();
};

View file

@ -1,18 +0,0 @@
// Store Chart Data Type
export interface StoreChartData {
id: string;
name: string;
type: 'bar' | 'line' | 'pie';
style: {
};
presentation: string;
postfix: string;
data: {
categories: string[];
series: Array<{
name?: string;
data: number[];
}>;
};
}

View file

@ -1,285 +0,0 @@
import * as z from 'zod';
import fs from 'fs';
import * as path from 'path';
interface LayoutInfo {
id: string;
name: string;
description: string;
json_schema: Record<string, any>;
group: string;
}
interface LayoutGroup {
id: string;
ordered: boolean;
slides: string[];
}
interface LayoutStructure {
name: string;
ordered: boolean;
slides: LayoutInfo[];
}
interface GroupedLayoutsResponse {
group: string;
files: string[];
}
// Cache for layouts to avoid repeated file system operations
let layoutsCache: LayoutStructure[] | null = null;
/**
* Dynamically imports a layout file and extracts its schema and metadata
*/
async function extractLayoutFromFile(filePath: string, fileName: string, groupName: string): Promise<LayoutInfo | null> {
try {
// Import the layout module dynamically
const module = await import(filePath);
// Check if the module has a Schema export
if (!module.Schema) {
console.warn(`No Schema export found in ${fileName}`);
return null;
}
// Extract layout metadata (optional)
const layoutId = module.layoutId || fileName.replace(/\.tsx?$/, '').toLowerCase().replace(/layout$/, '');
const layoutName = module.layoutName || fileName.replace(/\.tsx?$/, '').replace(/([A-Z])/g, ' $1').trim();
const layoutDescription = module.layoutDescription || `${layoutName} layout for presentations`;
// Convert Zod schema to JSON schema
const jsonSchema = z.toJSONSchema(module.Schema, {
override: (ctx) => {
delete ctx.jsonSchema.default;
},
});
return {
id: layoutId,
name: layoutName,
description: layoutDescription,
json_schema: jsonSchema,
group: groupName
};
} catch (error: unknown) {
console.error(`Error extracting layout from ${fileName}:`, error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Failed to extract schema from ${fileName}: ${errorMessage}`);
}
}
/**
* Gets all layout files from the grouped presentation-layouts directory
*/
async function getGroupedLayoutFiles(): Promise<GroupedLayoutsResponse[]> {
const layoutsDirectory = path.join(process.cwd(), 'presentation-layouts');
if (!fs.existsSync(layoutsDirectory)) {
throw new Error(`Layouts directory not found at ${layoutsDirectory}`);
}
const items = fs.readdirSync(layoutsDirectory, { withFileTypes: true });
const groupDirectories = items
.filter(item => item.isDirectory())
.map(dir => dir.name);
const allLayouts: GroupedLayoutsResponse[] = [];
for (const groupName of groupDirectories) {
try {
const groupPath = path.join(layoutsDirectory, groupName);
const groupFiles = fs.readdirSync(groupPath);
// Filter for TypeScript/TSX files, excluding setting.json and other non-layout files
const layoutFiles = groupFiles.filter(file =>
(file.endsWith('.ts') || file.endsWith('.tsx')) &&
file !== 'setting.json' &&
!file.startsWith('.') &&
!file.includes('.test.') &&
!file.includes('.spec.')
);
if (layoutFiles.length > 0) {
allLayouts.push({
group: groupName,
files: layoutFiles
});
}
} catch (error) {
console.error(`Error reading group directory ${groupName}:`, error);
// Continue with other groups even if one fails
}
}
return allLayouts;
}
/**
* Extracts layout groups from layoutGroup.ts file in presentation-layouts directory
*/
async function extractLayoutGroups(): Promise<LayoutGroup[]> {
try {
const layoutGroupPath = path.join(process.cwd(), 'presentation-layouts', 'layoutGroup.ts');
if (!fs.existsSync(layoutGroupPath)) {
throw new Error('layoutGroup.ts file not found in presentation-layouts directory');
}
const module = await import(layoutGroupPath);
// Extract all exported layout groups
const layoutGroups: LayoutGroup[] = [];
Object.keys(module).forEach(key => {
const exportedItem = module[key];
// Check if it's a layout group object
if (exportedItem &&
typeof exportedItem === 'object' &&
exportedItem.id &&
Array.isArray(exportedItem.slides)) {
layoutGroups.push({
id: exportedItem.id,
ordered: exportedItem.ordered || false,
slides: exportedItem.slides
});
}
});
if (layoutGroups.length === 0) {
throw new Error('No valid layout groups found in layoutGroup.ts');
}
return layoutGroups;
} catch (error) {
console.error('Error extracting layout groups:', error);
throw error;
}
}
/**
* Maps layout information to layout groups
*/
function mapLayoutsToGroups(
layoutInfos: LayoutInfo[],
layoutGroups: LayoutGroup[]
): LayoutStructure[] {
return layoutGroups.map(group => {
const groupSlides: LayoutInfo[] = [];
// Map slides in the group to their layout info
group.slides.forEach(slideId => {
const layoutInfo = layoutInfos.find(layout =>
layout.id === slideId ||
layout.id.replace('-', '') === slideId.replace('-', '') ||
layout.id.toLowerCase() === slideId.toLowerCase()
);
if (layoutInfo) {
groupSlides.push(layoutInfo);
} else {
console.warn(`Layout info not found for slide ID: ${slideId}`);
}
});
return {
name: group.id,
ordered: group.ordered,
slides: groupSlides
};
});
}
/**
* Main function to extract all layouts dynamically from grouped structure
*/
export async function extractLayouts(): Promise<LayoutStructure[]> {
// Return cached layouts if available
if (layoutsCache) {
return layoutsCache;
}
try {
// Get all grouped layout files
const groupedLayoutFiles = await getGroupedLayoutFiles();
if (groupedLayoutFiles.length === 0) {
throw new Error('No layout files found in the presentation-layouts directory');
}
// Extract layout information from each file in each group
const allLayoutPromises: Promise<LayoutInfo | null>[] = [];
for (const groupData of groupedLayoutFiles) {
for (const fileName of groupData.files) {
const filePath = path.join(process.cwd(), 'presentation-layouts', groupData.group, fileName);
allLayoutPromises.push(extractLayoutFromFile(filePath, fileName, groupData.group));
}
}
const layoutResults = await Promise.all(allLayoutPromises);
// Filter out null results (files without valid schemas)
const validLayouts = layoutResults.filter((layout): layout is LayoutInfo => layout !== null);
if (validLayouts.length === 0) {
throw new Error('No valid schemas found in any layout files');
}
// Extract layout groups
const layoutGroups = await extractLayoutGroups();
// Map layouts to groups
const mappedLayouts = mapLayoutsToGroups(validLayouts, layoutGroups);
// Cache the results
layoutsCache = mappedLayouts;
return mappedLayouts;
} catch (error) {
console.error('Error extracting layouts:', error);
throw error;
}
}
/**
* Clears the layout cache (useful for development/testing)
*/
export function clearLayoutCache(): void {
layoutsCache = null;
}
/**
* Gets a specific layout by ID
*/
export async function getLayoutById(layoutId: string): Promise<LayoutInfo | null> {
const layouts = await extractLayouts();
for (const group of layouts) {
const layout = group.slides.find(slide => slide.id === layoutId);
if (layout) {
return layout;
}
}
return null;
}
/**
* Gets all available layout IDs
*/
export async function getAllLayoutIds(): Promise<string[]> {
const layouts = await extractLayouts();
const ids: string[] = [];
layouts.forEach(group => {
group.slides.forEach(slide => {
ids.push(slide.id);
});
});
return ids;
}

View file

@ -3,9 +3,9 @@
import Wrapper from "@/components/Wrapper";
import React from "react";
import Link from "next/link";
import UserAccount from "@/app/(presentation-generator)/components/UserAccount";
import BackBtn from "@/components/BackBtn";
import { usePathname } from "next/navigation";
import HeaderNav from "@/app/(presentation-generator)/components/HeaderNab";
const Header = () => {
const pathname = usePathname();
return (
@ -23,7 +23,7 @@ const Header = () => {
</Link>
</div>
<div className="flex items-center gap-3 sm:gap-5 md:gap-10">
<UserAccount />
<HeaderNav />
</div>
</div>
</Wrapper>

View file

@ -2,7 +2,6 @@
@tailwind components;
@tailwind utilities;
@import '../app/(presentation-generator)/styles/themes.css';
body {
font-family: Arial, Helvetica, sans-serif;

View file

@ -1,137 +0,0 @@
# Layout Preview Studio
A modular, responsive layout preview system for viewing and testing presentation layout components with realistic sample data.
## ✨ Features
- **Dynamic Layout Discovery**: Automatically discovers and loads layout components
- **Interactive Navigation**: Easy navigation with quick select grid
- **Beautiful Presentation Display**: Mock browser frame with professional styling
- **Detailed Information Panel**: Layout metadata and sample data inspection
- **Responsive Design**: Mobile-friendly with collapsible sidebar
- **Professional Loading States**: Skeleton loaders and error handling
- **Type Safety**: Full TypeScript support with shared types
## 🏗️ Architecture
The system is built with a modular architecture for maintainability and reusability:
```
layout-preview/
├── components/ # Modular UI components
│ ├── LayoutNavigation.tsx # Navigation controls & layout selector
│ ├── LayoutDisplay.tsx # Main layout preview area
│ ├── LayoutInfoPanel.tsx # Information and data structure display
│ └── LoadingStates.tsx # Loading, error, and empty states
├── hooks/ # Custom React hooks
│ └── useLayoutLoader.ts # Layout loading logic and state management
├── utils/ # Utility functions
│ └── sampleDataGenerator.ts # Realistic sample data generation
├── types/ # Shared TypeScript types
│ └── index.ts # Common interfaces and types
├── page.tsx # Main page component
└── README.md # This file
```
## 🧩 Components
### LayoutNavigation
- Previous/Next navigation buttons
- Layout counter and current layout info
- Quick select grid for fast switching
- Responsive design with mobile optimization
### LayoutDisplay
- Mock browser frame presentation
- Layout rendering with sample data
- Professional shadow and styling effects
- Empty state with helpful messaging
### LayoutInfoPanel
- Layout metadata display
- Collapsible sample data viewer
- Copy to clipboard functionality
- Position tracking in collection
### LoadingStates
- Loading spinner with animated dots
- Error state with retry functionality
- Empty state with helpful instructions
- Skeleton loading animations
## 🎯 Custom Hooks
### useLayoutLoader
Handles all layout loading logic:
- Fetches layout files from API
- Imports and validates components
- Generates realistic sample data
- Provides retry functionality
- Manages loading and error states
## 🛠️ Utilities
### sampleDataGenerator
Intelligent sample data generation:
- Context-aware field value generation
- Support for images, emails, phones, URLs
- Realistic business content
- Zod schema parsing and validation
- Array and object handling
## 📱 Responsive Design
The layout preview system is fully responsive:
- **Desktop**: Sidebar navigation with main preview area
- **Tablet**: Collapsible navigation panels
- **Mobile**: Stacked layout with touch-friendly controls
## 🎨 Styling
Built with:
- **Tailwind CSS**: Utility-first styling
- **shadcn/ui**: Consistent component library
- **Gradient Backgrounds**: Modern visual appeal
- **Glass Morphism**: Backdrop blur effects
- **Smooth Animations**: Hover and transition effects
## 🔧 Usage
The system automatically discovers layout components that export:
```typescript
// Layout component
export default function MyLayout({ data }: { data: any }) {
return <div>{/* Your layout */}</div>
}
// Zod schema for type safety and sample data generation
export const Schema = z.object({
title: z.string(),
description: z.string(),
// ... other fields
})
```
## 🚀 Getting Started
1. **Add Layout Components**: Place your layout files in the appropriate directory
2. **Export Requirements**: Ensure each layout exports both a default component and Schema
3. **Navigate**: Use the navigation controls or quick select grid
4. **Inspect**: View layout information and sample data structure
5. **Test**: See how your layouts render with realistic data
## 🎯 Benefits
- **Modular Architecture**: Easy to maintain and extend
- **Type Safety**: Full TypeScript support prevents runtime errors
- **Beautiful UI**: Professional design that's pleasant to use
- **Developer Experience**: Quick feedback loop for layout development
- **Responsive**: Works on all device sizes
- **Accessible**: Keyboard navigation and screen reader support
## 📈 Performance
- **Lazy Loading**: Components are imported only when needed
- **Optimized Rendering**: Efficient re-renders with React best practices
- **Minimal Bundle**: Modular structure enables tree shaking
- **Caching**: Sample data generation is memoized

View file

@ -1,39 +0,0 @@
import { useState, useEffect } from "react";
export const useTypewriter = (text: string, speed = 50, enabled = true) => {
const [displayText, setDisplayText] = useState("");
const [isCursorVisible, setIsCursorVisible] = useState(true);
const [index, setIndex] = useState(0);
// Reset when text changes or enabled status changes
useEffect(() => {
if (enabled) {
setDisplayText("");
setIndex(0);
setIsCursorVisible(true);
} else {
// When disabled, immediately show full text and hide cursor
setDisplayText(text);
setIsCursorVisible(false);
}
}, [text, enabled]);
// Only run the typing effect when enabled
useEffect(() => {
if (!enabled) return;
if (index >= text.length) {
setIsCursorVisible(false);
return;
}
const timeout = setTimeout(() => {
setDisplayText((prev) => prev + text.charAt(index));
setIndex((prev) => prev + 1);
}, speed);
return () => clearTimeout(timeout);
}, [index, text, speed, enabled]);
return { displayText, isCursorVisible };
};

View file

@ -4,66 +4,3 @@ import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
interface ChartData {
categories: string[];
series: {
name: string;
data: number[];
}[];
}
export function randomChartGenerator(): ChartData {
const templates = [
{
categories: ["first Q", "second Q", "third Q", "fourth Q"],
series: [
{
name: "Revenue 2023",
data: [32500, 47800, 41500, 59000]
},
{
name: "Revenue 2022",
data: [28000, 35000, 37500, 41000]
}
]
},
{
categories: ["Mobile", "Desktop", "Tablet", "Other"],
series: [
{
name: "Active Users",
data: [85000, 65000, 22000, 95000]
},
{
name: "Last Month",
data: [72000, 58000, 18000, 85000]
}
]
},
{
categories: ["Product A", "Product B", "Product C", "Product D"],
series: [
{
name: "Sales Volume",
data: [1200, 980, 850, 674]
},
{
name: "Target",
data: [1000, 800, 900, 700]
}
]
}
];
return templates[Math.floor(Math.random() * templates.length)];
}
export function formatLargeNumber(num: number): string {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}

View file

@ -1,13 +1,11 @@
import { configureStore } from "@reduxjs/toolkit";
import presentationGenerationReducer from "./slices/presentationGeneration";
import themeReducer from "@/app/(presentation-generator)/store/themeSlice";
import pptGenUploadReducer from "./slices/presentationGenUpload";
import userConfigReducer from "./slices/userConfig";
export const store = configureStore({
reducer: {
presentationGeneration: presentationGenerationReducer,
theme: themeReducer,
pptGenUpload: pptGenUploadReducer,
userConfig: userConfigReducer,
},

View file

@ -1,122 +0,0 @@
export const toDateTime = (secs: number) => {
var t = new Date('1970-01-01T00:30:00Z');
t.setSeconds(secs);
return t;
};
export const calculateTrialEndUnixTimestamp = (
trialPeriodDays: number | null | undefined
) => {
// Check if trialPeriodDays is null, undefined, or less than 2 days
if (
trialPeriodDays === null ||
trialPeriodDays === undefined ||
trialPeriodDays < 2
) {
return undefined;
}
const currentDate = new Date(); // Current date and time
const trialEnd = new Date(
currentDate.getTime() + (trialPeriodDays + 1) * 24 * 60 * 60 * 1000
); // Add trial days
return Math.floor(trialEnd.getTime() / 1000); // Convert to Unix timestamp in seconds
};
const toastKeyMap: { [key: string]: string[] } = {
status: ['status', 'status_description'],
error: ['error', 'error_description']
};
const getToastRedirect = (
path: string,
toastType: string,
toastName: string,
toastDescription: string = '',
disableButton: boolean = false,
arbitraryParams: string = ''
): string => {
const [nameKey, descriptionKey] = toastKeyMap[toastType];
let redirectPath = `${path}?${nameKey}=${encodeURIComponent(toastName)}`;
if (toastDescription) {
redirectPath += `&${descriptionKey}=${encodeURIComponent(toastDescription)}`;
}
if (disableButton) {
redirectPath += `&disable_button=true`;
}
if (arbitraryParams) {
redirectPath += `&${arbitraryParams}`;
}
return redirectPath;
};
export const getStatusRedirect = (
path: string,
statusName: string,
statusDescription: string = '',
disableButton: boolean = false,
arbitraryParams: string = ''
) =>
getToastRedirect(
path,
'status',
statusName,
statusDescription,
disableButton,
arbitraryParams
);
export const getErrorRedirect = (
path: string,
errorName: string,
errorDescription: string = '',
disableButton: boolean = false,
arbitraryParams: string = ''
) =>
getToastRedirect(
path,
'error',
errorName,
errorDescription,
disableButton,
arbitraryParams
);
export const getMaxLimits = (tier: 'free' | 'standard' | 'premium') => {
const limits = {
free: {
exports: 5,
slides: 8,
aiVideos: 5,
fileSize: 6 * 1024 * 1024, // 6MB in bytes
videoDuration: 5 * 60, // 5 minutes in seconds
hasWatermark: true,
hasAvatar: false
},
standard: {
exports: 10,
slides: 15,
aiVideos: 10,
fileSize: 10 * 1024 * 1024, // 10MB
videoDuration: 10 * 60, // 10 minutes
hasWatermark: false,
hasAvatar: true
},
premium: {
exports: 25,
slides: 25,
aiVideos: 25,
fileSize: 25 * 1024 * 1024, // 25MB
videoDuration: 25 * 60, // 25 minutes
hasWatermark: false,
hasAvatar: true
}
};
return limits[tier];
};