refactor (Nextjs): Unused files and function removed
This commit is contained in:
parent
1c4dca4677
commit
d4813b02a6
44 changed files with 28 additions and 3810 deletions
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
|
@ -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[];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import '../app/(presentation-generator)/styles/themes.css';
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Binary file not shown.
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue