Merge branch 'feat/custom_schema_and_layout' into feat/presentation_export

This commit is contained in:
Saurav Niraula 2025-07-21 00:38:12 +05:45 committed by GitHub
commit ea434e07dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 136 additions and 1081 deletions

View file

@ -16,6 +16,10 @@ http {
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1; # Required for WebSocket
proxy_set_header Upgrade $http_upgrade; # WebSocket header
proxy_set_header Connection "upgrade"; # WebSocket header
proxy_set_header Host $host;
proxy_read_timeout 30m;
proxy_connect_timeout 30m;
}

View file

@ -1,5 +1,5 @@
"use client";
import { LayoutDashboard, Settings } from "lucide-react";
import { LayoutDashboard, Settings, Upload } from "lucide-react";
import React from "react";
import Link from "next/link";
import { RootState } from "@/store/store";
@ -11,6 +11,7 @@ const HeaderNav = () => {
return (
<div className="flex items-center gap-2">
<Link
href="/dashboard"
prefetch={false}

View file

@ -21,11 +21,11 @@ export default function MarkdownEditor({ content, onChange }: { content: string;
});
// Update editor content when the content prop changes (for streaming)
useEffect(() => {
if (editor && content !== editor.storage.markdown.getMarkdown()) {
editor.commands.setContent(content);
}
}, [content, editor]);
// useEffect(() => {
// if (editor && content !== editor.storage.markdown.getMarkdown()) {
// editor.commands.setContent(content);
// }
// }, [content, editor]);
return (
<div className="relative">

View file

@ -1,666 +0,0 @@
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import React, { useRef, useState, useEffect } from "react";
import { Camera, Loader2, Plus } from "lucide-react";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { getStaticFileUrl, isDarkColor } from "../../utils/others";
import { defaultFooterProperties, useFooterContext } from "../../context/footerContext";
import { FooterProperties } from "../../services/footerService";
import { toast } from "sonner";
const SlideFooter: React.FC = () => {
const [showEditor, setShowEditor] = useState<boolean>(false);
const [isUploading, setIsUploading] = useState({
white: false,
dark: false,
});
const { currentColors } = useSelector((state: RootState) => state.theme);
const isDark = isDarkColor(currentColors.slideBg);
const whiteLogoRef = useRef<HTMLInputElement | null>(null);
const darkLogoRef = useRef<HTMLInputElement | null>(null);
const { footerProperties, setFooterProperties, saveFooterProperties, resetFooterProperties, isPropertyChanged, setIsPropertyChanged } = useFooterContext();
const handleSave = async () => {
await saveFooterProperties(footerProperties);
setIsPropertyChanged(false);
toast.success("Footer properties saved successfully");
};
const handleReset = async () => {
await resetFooterProperties();
setFooterProperties(defaultFooterProperties);
toast.success("Footer properties reset to default");
};
const updateProperty = (path: string, value: any): void => {
setIsPropertyChanged(true);
const keys = path.split(".");
// Security: Validate path to prevent prototype pollution
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
if (keys.some(key => dangerousKeys.includes(key))) {
console.warn('Attempted prototype pollution with path:', path);
return;
}
const allowedPaths = [
'logoProperties.showLogo',
'logoProperties.logoPosition',
'logoProperties.opacity',
'logoProperties.logoImage.light',
'logoProperties.logoImage.dark',
'logoScale',
'logoOffset.x',
'logoOffset.y',
'footerMessage.showMessage',
'footerMessage.message',
'footerMessage.fontSize',
'footerMessage.opacity'
]
if (!allowedPaths.includes(path)) {
console.error(`Invalid path: ${path}`);
return;
}
setFooterProperties((prevProps: FooterProperties) => {
const newProps = { ...prevProps };
let current: any = newProps;
for (let i = 0; i < keys.length - 1; i++) {
if (dangerousKeys.includes(keys[i])) {
console.warn('Attempted prototype pollution with path:', path);
return prevProps;
}
current[keys[i]] = { ...current[keys[i]] };
current = current[keys[i]];
}
const finalKey = keys[keys.length - 1];
if (dangerousKeys.includes(finalKey)) {
console.warn('Dangerous final key detected:', finalKey);
return prevProps;
}
current[keys[keys.length - 1]] = value;
return newProps;
});
};
const handleSwitchChange =
(path: string) =>
(checked: boolean): void => {
updateProperty(path, checked);
};
const handleTextChange =
(path: string) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
updateProperty(path, e.target.value);
};
const handleSelectChange =
(path: string) =>
(value: string): void => {
updateProperty(path, value);
};
const handleSliderChange =
(path: string) =>
(value: number[]): void => {
updateProperty(path, value[0]);
};
const getLogoPositionClass = (): string => {
const { logoPosition } = footerProperties.logoProperties;
return logoPosition === "left"
? "justify-start"
: logoPosition === "center"
? "justify-center"
: "justify-end";
};
const getLogoStyle = (): React.CSSProperties => {
const { opacity } = footerProperties.logoProperties;
const { logoScale, logoOffset } = footerProperties;
return {
opacity: opacity,
transform: `scale(${logoScale}) translate(${logoOffset.x}px, ${logoOffset.y}px)`,
};
};
const getMessageStyle = (): React.CSSProperties => {
const { fontSize, opacity } = footerProperties.footerMessage;
return {
opacity: opacity,
fontSize: `${fontSize}px`,
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
};
};
const getLogoImageSrc = (): string => {
if (isDark) {
return footerProperties.logoProperties.logoImage.dark;
} else {
return footerProperties.logoProperties.logoImage.light;
}
};
const convertToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};
const handleWhiteLogoUpload = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
setIsPropertyChanged(true);
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error("Please Upload An Image File");
return;
}
try {
setIsUploading({ ...isUploading, white: true });
const base64String = await convertToBase64(file);
setFooterProperties((prev: FooterProperties) => ({
...prev,
logoProperties: {
...prev.logoProperties,
logoImage: {
...prev.logoProperties.logoImage,
light: base64String,
},
},
}));
} catch (error) {
console.error("Error converting image:", error);
toast.error("Error uploading image");
} finally {
setIsUploading({ ...isUploading, white: false });
}
};
const handleDarkLogoUpload = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
setIsPropertyChanged(true);
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error("Please Upload An Image File");
return;
}
try {
setIsUploading({ ...isUploading, dark: true });
const base64String = await convertToBase64(file);
setFooterProperties((prev: FooterProperties) => ({
...prev,
logoProperties: {
...prev.logoProperties,
logoImage: {
...prev.logoProperties.logoImage,
dark: base64String,
},
},
}));
} catch (error) {
console.error("Error converting image:", error);
toast.error("Error uploading image");
} finally {
setIsUploading({ ...isUploading, dark: false });
}
};
const getLocalImageUrl = (filePath: string) => {
if (!filePath) return "";
if (filePath.startsWith('data:image')) return filePath;
return getStaticFileUrl(filePath);
};
const handleEditor = () => {
setShowEditor(!showEditor);
return;
};
const handleUploadClick = (isWhite: boolean) => {
if (isWhite) {
whiteLogoRef.current?.click();
} else {
darkLogoRef.current?.click();
}
};
const handleSheetClose = () => {
if (isPropertyChanged) {
toast.error("Unsaved Changes", {
description: "Please save changes before closing the editor",
});
return;
}
setShowEditor(false);
};
return (
<>
<div
onClick={handleEditor}
title="Click to change footer"
id="footer"
className="absolute hidden lg:grid z-10 cursor-pointer px-6 grid-cols-3 items-end left-1/2 -translate-x-1/2 justify-between bottom-5 w-full"
>
{(!footerProperties.logoProperties.showLogo && !footerProperties.footerMessage.showMessage) ? (
<div onClick={handleEditor} className="col-span-3 cursor-pointer flex justify-center items-center text-gray-400 text-sm opacity-0 group-hover:opacity-100 transition-opacity duration-300">
Click to add footer
</div>
) : (
<>
<div
className={`h-8 flex-1 flex ${footerProperties.logoProperties.logoPosition === "left"
? getLogoPositionClass()
: "justify-start"
}`}
>
{footerProperties.logoProperties.showLogo &&
(footerProperties.logoProperties.logoPosition === "left" ? (
getLogoImageSrc() !== "" ? (
<img
data-slide-element
data-element-type="picture"
id="footer-user-logo"
className="w-auto h-full object-contain"
src={getLocalImageUrl(getLogoImageSrc())}
alt="logo"
style={getLogoStyle()}
/>
) : (
<div className="flex gap-2 items-center">
<div className="w-8 h-8 bg-gray-100 rounded-lg flex justify-center items-center">
<Plus className="text-gray-500" />
</div>
<p className="text-sm text-gray-400"
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>Insert Your Logo</p>
</div>
)
) : (
<div></div>
))}
</div>
<div
className={`flex-1 flex items-center font-instrument_sans slide-title justify-center`}
>
<p
id="footer-user-message"
className="text-sm"
data-slide-element
data-element-type="text"
data-text-content={footerProperties.footerMessage.message}
style={getMessageStyle()}
>
{footerProperties.footerMessage.showMessage &&
(footerProperties.footerMessage.message
? footerProperties.footerMessage.message
: "Your text")}
</p>
</div>
<div
className={`h-8 flex-1 flex ${footerProperties.logoProperties.logoPosition === "right"
? getLogoPositionClass()
: "justify-start"
}`}
>
{footerProperties.logoProperties.showLogo &&
footerProperties.logoProperties.logoPosition === "right" ? (
getLogoImageSrc() !== "" ? (
<div data-element-type="picture" data-slide-element>
<img
data-slide-element
data-element-type="picture"
id="footer-user-logo"
className="w-auto h-full object-contain"
src={getLocalImageUrl(getLogoImageSrc())}
alt="logo"
style={getLogoStyle()}
/>
</div>
) : (
<div className="flex gap-2 items-center">
<div className="w-8 h-8 bg-gray-100 rounded-lg flex justify-center items-center">
<Plus className="text-gray-500" />
</div>
<p className="text-sm text-gray-400"
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>Insert Your Logo</p>
</div>
)
) : (
<div className="w-full flex justify-end"></div>
)}
</div>
</>
)}
</div>
<Sheet open={showEditor} onOpenChange={handleSheetClose}>
<SheetContent
onOpenAutoFocus={(e) => e.preventDefault()}
className="sm:max-w-[500px] overflow-y-auto"
>
<SheetHeader className="mb-6 font-inter">
<SheetTitle>Configure Footer</SheetTitle>
<p className="text-sm text-gray-500 font-inter ">
These changes will apply to all slides.
</p>
</SheetHeader>
<div className="space-y-6 h-[calc(100vh-200px)] font-inter overflow-y-auto custom_scrollbar p-4">
<div className=" pb-8">
<h3 className="text-lg font-medium mb-4">Logo Settings</h3>
<div className="space-y-6 font-inter">
<div className="flex items-center justify-between">
<Label htmlFor="showLogo" className="flex-1">
Show Logo
</Label>
<Switch
id="showLogo"
checked={footerProperties.logoProperties.showLogo}
onCheckedChange={handleSwitchChange(
"logoProperties.showLogo"
)}
/>
</div>
<div className="flex w-full gap-2 items-center">
<div className="w-full">
<div
onClick={() => handleUploadClick(true)}
className="h-28 border relative overflow-hidden flex justify-center items-center cursor-pointer group group-hover:border-blue-500"
>
<input
ref={whiteLogoRef}
type="file"
accept="image/*"
id="whiteLogo"
name="whiteLogo"
onChange={handleWhiteLogoUpload}
className="opacity-0 z-[-10] absolute group-hover:border-blue-500 h-full w-full cursor-pointer"
/>
{isUploading.white ? (
<div className="absolute h-20 w-20 mx-auto max-w-full max-h-full object-contain flex justify-center items-center">
<Loader2 className="animate-spin" />
</div>
) : footerProperties.logoProperties.logoImage.light ? (
<div className="absolute">
<img
className=" h-20 w-20 mx-auto object-contain "
src={footerProperties.logoProperties.logoImage.light}
alt="brand white logo"
/>
<div className="w-10 h-10 p-2 rounded-full bg-blue-400 absolute opacity-0 group-hover:opacity-100 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex justify-center items-center duration-300">
<Camera className=" text-gray-100" />
</div>
</div>
) : (
<Camera className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 group-hover:text-blue-500 duration-300" />
)}
</div>
<p className="text-sm text-center">Logo on Light</p>
</div>
<div className="w-full">
<div
onClick={() => handleUploadClick(false)}
className="h-28 flex bg-black justify-center items-center border relative cursor-pointer group"
>
<input
ref={darkLogoRef}
onChange={handleDarkLogoUpload}
accept="image/*"
id="darkLogo"
name="darkLogo"
type="file"
className="opacity-0 h-full w-full cursor-pointer"
/>
{footerProperties.logoProperties.logoImage.dark && (
<img
className="absolute h-20 w-20 mx-auto max-w-full max-h-full object-contain "
src={footerProperties.logoProperties.logoImage.dark}
alt="brand white logo"
/>
)}
{isUploading.dark ? (
<div className="absolute h-20 w-20 mx-auto max-w-full max-h-full object-contain flex justify-center items-center">
<Loader2 className="animate-spin text-white" />
</div>
) : footerProperties.logoProperties.logoImage.dark ? (
<div className="absolute">
<img
className=" h-20 w-20 mx-auto object-contain "
src={footerProperties.logoProperties.logoImage.dark}
alt="brand white logo"
/>
<div className="w-10 h-10 p-2 rounded-full bg-blue-400 absolute opacity-0 group-hover:opacity-100 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex justify-center items-center duration-300">
<Camera className=" text-gray-100" />
</div>
</div>
) : (
<Camera className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 group-hover:text-blue-500 duration-300" />
)}
</div>
<p className="text-sm text-center">Logo on Dark/Color</p>
</div>
</div>
{footerProperties.logoProperties.showLogo && (
<div className="space-y-2">
<Label htmlFor="logoPosition">Logo Position</Label>
<Select
value={footerProperties.logoProperties.logoPosition}
onValueChange={handleSelectChange(
"logoProperties.logoPosition"
)}
>
<SelectTrigger id="logoPosition">
<SelectValue placeholder="Select position" />
</SelectTrigger>
<SelectContent>
<SelectItem value="left">Left</SelectItem>
<SelectItem value="right">Right</SelectItem>
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="opacity">Logo Opacity</Label>
<span className="text-sm text-gray-700 font-semibold">
{footerProperties.logoProperties.opacity}
</span>
</div>
<Slider
id="opacity"
min={0}
max={1}
step={0.1}
defaultValue={[footerProperties.logoProperties.opacity]}
value={[footerProperties.logoProperties.opacity]}
onValueChange={handleSliderChange("logoProperties.opacity")}
/>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="scale">Logo Scale</Label>
<span className="text-sm text-gray-700 font-semibold">
{footerProperties.logoScale}
</span>
</div>
<Slider
id="scale"
min={0.5}
max={1.1}
step={0.1}
defaultValue={[footerProperties.logoScale]}
value={[footerProperties.logoScale]}
onValueChange={handleSliderChange("logoScale")}
/>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="offsetX">Logo Horizontal Offset</Label>
<span className="text-sm text-gray-700 font-semibold">
{footerProperties.logoOffset.x}px
</span>
</div>
<Slider
id="offsetX"
min={-10}
max={50}
step={1}
defaultValue={[footerProperties.logoOffset.x]}
value={[footerProperties.logoOffset.x]}
onValueChange={handleSliderChange("logoOffset.x")}
/>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="offsetY">Logo Vertical Offset</Label>
<span className="text-sm text-gray-700 font-semibold ">
{footerProperties.logoOffset.y * -1}px
</span>
</div>
<Slider
id="offsetY"
min={-10}
max={10}
step={1}
defaultValue={[footerProperties.logoOffset.y]}
value={[footerProperties.logoOffset.y]}
onValueChange={handleSliderChange("logoOffset.y")}
/>
</div>
</div>
</div>
<div className="pb-4">
<h3 className="text-lg font-medium mb-4">Footer Message</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="showMessage" className="flex-1">
Show Message
</Label>
<Switch
id="showMessage"
checked={footerProperties.footerMessage.showMessage}
onCheckedChange={handleSwitchChange(
"footerMessage.showMessage"
)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="message">Message Text</Label>
<Textarea
id="message"
value={footerProperties.footerMessage.message}
onChange={handleTextChange("footerMessage.message")}
className="h-20 border border-blue-500"
/>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="fontSize">Font Size</Label>
<span className="text-sm text-gray-700 font-semibold">
{footerProperties.footerMessage.fontSize}px
</span>
</div>
<Slider
id="fontSize"
min={8}
max={14}
step={0.1}
defaultValue={[footerProperties.footerMessage.fontSize]}
value={[footerProperties.footerMessage.fontSize]}
onValueChange={handleSliderChange("footerMessage.fontSize")}
/>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="font-opacity">Opacity</Label>
<span className="text-sm text-gray-700 font-semibold">
{footerProperties.footerMessage.opacity}
</span>
</div>
<Slider
id="font-opacity"
min={0.5}
max={1}
step={0.1}
defaultValue={[footerProperties.footerMessage.opacity]}
value={[footerProperties.footerMessage.opacity]}
onValueChange={handleSliderChange("footerMessage.opacity")}
/>
</div>
</div>
</div>
</div>
<div className="flex gap-4 mt-4 justify-end">
<Button
variant="outline"
onClick={() => handleReset()}
className="bg-white text-gray-500 hover:bg-gray-100"
>
Reset to Default
</Button>
<Button className="ml-2" onClick={handleSave} variant="default">
Save Changes
</Button>
</div>
</SheetContent>
</Sheet>
</>
);
};
export default SlideFooter;

View file

@ -1,111 +0,0 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
import {
FooterProperties,
useFooterService,
} from "../services/footerService";
// Default footer properties
export const defaultFooterProperties: FooterProperties = {
logoProperties: {
showLogo: false,
logoPosition: "left",
opacity: 0.8,
logoImage: {
light: "",
dark: "",
},
},
logoScale: 1.0,
logoOffset: {
x: 0,
y: 0,
},
footerMessage: {
showMessage: false,
opacity: 1.0,
fontSize: 12,
message: "",
},
};
interface FooterContextProps {
footerProperties: FooterProperties;
setFooterProperties: (newProperties: FooterProperties | ((prev: FooterProperties) => FooterProperties)) => void;
resetFooterProperties: () => Promise<void>;
saveFooterProperties: (newProperties: FooterProperties) => Promise<void>;
isPropertyChanged: boolean;
setIsPropertyChanged: (newIsPropertyChanged: boolean) => void;
}
const FooterContext = createContext<FooterContextProps | undefined>(undefined);
export const useFooterContext = () => {
const context = useContext(FooterContext);
if (!context) {
throw new Error("useFooterContext must be used within a FooterProvider");
}
return context;
};
export const FooterProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [footerProperties, setFooterProperties] = useState<FooterProperties>(defaultFooterProperties);
const footerService = useFooterService();
const [isPropertyChanged, setIsPropertyChanged] = useState(false);
// Load footer properties only once when the provider mounts
useEffect(() => {
const loadFooterProperties = async () => {
try {
const properties = await footerService.getFooterProperties();
if (properties) {
setFooterProperties(properties);
}
} catch (error) {
console.error("Failed to load footer properties:", error);
}
};
loadFooterProperties();
}, []); // Empty dependency array ensures this runs only once
const resetFooterProperties = async () => {
try {
const success = await footerService.resetFooterProperties(defaultFooterProperties);
if (success) {
setFooterProperties(defaultFooterProperties);
}
} catch (error) {
console.error("Failed to reset footer properties:", error);
}
};
const saveFooterProperties = async (newProperties: FooterProperties) => {
try {
const success = await footerService.saveFooterProperties(newProperties);
if (success) {
setFooterProperties(newProperties);
}
} catch (error) {
console.error("Failed to save footer properties:", error);
}
};
return (
<FooterContext.Provider
value={{
footerProperties,
setFooterProperties,
resetFooterProperties,
saveFooterProperties,
isPropertyChanged,
setIsPropertyChanged,
}}
>
{children}
</FooterContext.Provider>
);
};

View file

@ -0,0 +1,65 @@
import { useGroupLayoutLoader } from '@/app/layout-preview/hooks/useGroupLayoutLoader';
import { CheckCircle } from 'lucide-react';
import React from 'react';
import { LayoutGroup } from "../types/index";
interface GroupLayoutsProps {
group: LayoutGroup;
onSelectLayoutGroup: (group: LayoutGroup) => void;
selectedLayoutGroup: LayoutGroup | null;
}
const GroupLayouts: React.FC<GroupLayoutsProps> = ({ group, onSelectLayoutGroup, selectedLayoutGroup }) => {
const { layoutGroup } = useGroupLayoutLoader(group.id);
return (
<div
onClick={() => onSelectLayoutGroup(group)}
className={`relative p-4 rounded-lg border cursor-pointer transition-all duration-200 ${selectedLayoutGroup?.id === group.id
? 'border-blue-500 bg-blue-50 shadow-md'
: 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm'
}`}
>
{selectedLayoutGroup?.id === group.id && (
<div className="absolute top-3 right-3">
<CheckCircle className="w-5 h-5 text-blue-500" />
</div>
)}
<div className="mb-3">
<h6 className="text-base font-medium text-gray-900 mb-1">
{group.name}
</h6>
<p className="text-sm text-gray-600">
{group.description}
</p>
</div>
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2 mb-3">
{layoutGroup && layoutGroup?.layouts.slice(0, 4).map((layout: any, index: number) => {
const { component: LayoutComponent, sampleData, layoutId } = layout
return (
<div key={`${layoutGroup?.groupName}-${index}`} className=" relative cursor-pointer overflow-hidden aspect-video">
<div className="absolute cursor-pointer bg-transparent z-40 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
<LayoutComponent data={sampleData} />
</div>
</div>
)
})}
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>{layoutGroup?.layouts.length} layouts</span>
<span className={`px-2 py-1 rounded text-xs ${group.ordered
? 'bg-gray-100 text-gray-700'
: 'bg-blue-100 text-blue-700'
}`}>
{group.ordered ? 'Structured' : 'Flexible'}
</span>
</div>
</div>
);
};
export default GroupLayouts;

View file

@ -1,17 +1,9 @@
"use client";
import React, { useEffect } from "react";
import { useLayout } from "../../context/LayoutContext";
import { CheckCircle } from "lucide-react";
interface LayoutGroup {
id: string;
name: string;
description: string;
ordered: boolean;
isDefault?: boolean;
slides: string[];
}
import GroupLayouts from "./GroupLayouts";
import { LayoutGroup } from "../types/index";
interface LayoutSelectionProps {
selectedLayoutGroup: LayoutGroup | null;
onSelectLayoutGroup: (group: LayoutGroup) => void;
@ -25,17 +17,15 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
getLayoutsByGroup,
getGroupSetting,
getAllGroups,
getLayout,
loading
} = useLayout();
const layoutGroups: LayoutGroup[] = React.useMemo(() => {
const groups = getAllGroups();
if (groups.length === 0) return [];
const Groups: LayoutGroup[] = groups.map(groupName => {
const layouts = getLayoutsByGroup(groupName);
const settings = getGroupSetting(groupName);
return {
id: groupName,
@ -43,7 +33,6 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
description: settings?.description || `${groupName} presentation layouts`,
ordered: settings?.ordered || false,
isDefault: settings?.isDefault || false,
slides: layouts.map(layout => layout.id)
};
});
@ -63,32 +52,6 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
}
}, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup]);
const renderLayoutPreview = (layoutId: string) => {
const Layout = getLayout(layoutId);
if (!Layout) {
return (
<div className="w-full h-16 bg-gray-100 rounded flex items-center justify-center">
<span className="text-gray-400 text-xs">Preview unavailable</span>
</div>
);
}
// Sample data for preview
const sampleData = {
title: "Sample Title",
description: "This is a preview of the layout",
subtitle: "Sample subtitle",
};
return (
<div className="w-full h-16 overflow-hidden rounded bg-white border">
<div className="transform scale-[0.12] origin-top-left w-[833%] h-[833%]">
<Layout data={sampleData} />
</div>
</div>
);
};
if (loading) {
return (
<div className="space-y-6">
@ -124,52 +87,24 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
);
}
const handleLayoutGroupSelection = (group: LayoutGroup) => {
const slides = getLayoutsByGroup(group.id);
onSelectLayoutGroup({
...group,
slides: slides,
});
}
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{layoutGroups.map((group) => (
<div
<GroupLayouts
key={group.id}
onClick={() => onSelectLayoutGroup(group)}
className={`relative p-4 rounded-lg border cursor-pointer transition-all duration-200 ${selectedLayoutGroup?.id === group.id
? 'border-blue-500 bg-blue-50 shadow-md'
: 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm'
}`}
>
{selectedLayoutGroup?.id === group.id && (
<div className="absolute top-3 right-3">
<CheckCircle className="w-5 h-5 text-blue-500" />
</div>
)}
<div className="mb-3">
<h6 className="text-base font-medium text-gray-900 mb-1">
{group.name}
</h6>
<p className="text-sm text-gray-600">
{group.description}
</p>
</div>
{/* Layout previews */}
<div className="grid grid-cols-3 gap-2 mb-3">
{group.slides.slice(0, 6).map((layoutId, index) => (
<div key={index} className="aspect-video">
{renderLayoutPreview(layoutId)}
</div>
))}
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>{group.slides.length} layouts</span>
<span className={`px-2 py-1 rounded text-xs ${group.ordered
? 'bg-gray-100 text-gray-700'
: 'bg-blue-100 text-blue-700'
}`}>
{group.ordered ? 'Structured' : 'Flexible'}
</span>
</div>
</div>
group={group}
onSelectLayoutGroup={handleLayoutGroupSelection}
selectedLayoutGroup={selectedLayoutGroup}
/>
))}
</div>
</div>

View file

@ -66,16 +66,11 @@ export function OutlineItem({
transform: CSS.Transform.toString(transform),
transition,
}
const handleSlideDelete = () => {
if (isStreaming) return;
dispatch(deleteSlideOutline({ index: index - 1 }))
}
return (
<div className="mb-2 bg-[#F9F9F9]">
{/* Main Title Row */}
@ -110,11 +105,17 @@ export function OutlineItem({
/>
{/* Editable Markdown Content */}
<MarkdownEditor
{isStreaming ? <textarea
defaultValue={slideOutline.body || ''}
onBlur={(e) => handleSlideChange({ ...slideOutline, body: e.target.value })}
className="text-md sm:text-lg flex-1 font-semibold bg-transparent outline-none"
placeholder="Content goes here"
/> : <MarkdownEditor
key={index}
content={slideOutline.body || ''}
onChange={(content) => handleSlideChange({ ...slideOutline, body: content })}
/>
/>}
</div>
{/* Action Buttons */}

View file

@ -24,7 +24,6 @@ const OutlinePage: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>(TABS.OUTLINE);
const [selectedLayoutGroup, setSelectedLayoutGroup] = useState<LayoutGroup | null>(null);
// Custom hooks
const streamState = useOutlineStreaming(presentation_id);
const { handleDragEnd, handleAddSlide } = useOutlineManagement(outlines);
@ -39,6 +38,7 @@ const OutlinePage: React.FC = () => {
return <EmptyStateView />;
}
return (
<Wrapper>
<OverlayLoader

View file

@ -2,7 +2,7 @@ import { useState, useCallback } from "react";
import { useDispatch } from "react-redux";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { clearPresentationData, setPresentationData, SlideOutline } from "@/store/slices/presentationGeneration";
import { clearPresentationData, SlideOutline } from "@/store/slices/presentationGeneration";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { useLayout } from "../../context/LayoutContext";
import { LayoutGroup, LoadingState, TABS } from "../types/index";
@ -46,22 +46,12 @@ export const usePresentationGeneration = (
const prepareLayoutData = useCallback(() => {
if (!selectedLayoutGroup) return null;
const groupLayoutSchemas = selectedLayoutGroup.slides
.map(slideId => {
const layout = getLayoutById(slideId);
return layout ? {
id: layout.id,
name: layout.name,
description: layout.description,
json_schema: layout.json_schema
} : null;
})
.filter(schema => schema !== null);
return {
name: selectedLayoutGroup.name,
ordered: selectedLayoutGroup.ordered,
slides: groupLayoutSchemas
slides: selectedLayoutGroup.slides
};
}, [selectedLayoutGroup, getLayoutById]);
@ -84,7 +74,6 @@ export const usePresentationGeneration = (
try {
const layoutData = prepareLayoutData();
if (!layoutData) return;
const response = await PresentationGenerationApi.presentationPrepare({
presentation_id: presentationId,
outlines: outlines,

View file

@ -4,7 +4,7 @@ export interface LayoutGroup {
description: string;
ordered: boolean;
isDefault?: boolean;
slides: string[];
slides?: any
}
export interface LoadingState {

View file

@ -1,6 +1,5 @@
'use client'
import React from "react";
import { FooterProvider } from "../context/footerContext";
import { Button } from "@/components/ui/button";
import { useRouter, useSearchParams } from "next/navigation";
@ -20,9 +19,7 @@ const page = () => {
);
}
return (
<FooterProvider>
<PdfMakerPage presentation_id={queryId} />
</FooterProvider>
<PdfMakerPage presentation_id={queryId} />
);
};
export default page;

View file

@ -86,13 +86,7 @@ const helpQuestions = [
answer:
"Absolutely! Hover near the bottom of any text box or content block, and you'll see a + icon appear. Click this button to add a new section below the current one. You can also use the Insert menu to add specific section types.",
},
{
id: 7,
category: "Layout",
question: "How do I add a consistent footer to all slides?",
answer:
"Look for the 'Insert your logo' option at the bottom left of the slide. Clicking this will open a side panel where you can customize your footer content, including logos, text, and more. Changes will apply to all slides automatically.",
},
{
id: 8,
category: "Export",
@ -222,11 +216,10 @@ const Help = () => {
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-3 py-1 rounded-full text-sm whitespace-nowrap ${
selectedCategory === category
className={`px-3 py-1 rounded-full text-sm whitespace-nowrap ${selectedCategory === category
? "bg-emerald-600 text-white"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
}`}
>
{category}
</button>

View file

@ -76,9 +76,6 @@ const SlideContent = ({
console.error("Error deleting slide:", error);
}
};
// Scroll to the new slide when streaming and new slides are being generated
useEffect(() => {
if (

View file

@ -1,6 +1,5 @@
'use client'
import React from "react";
import { FooterProvider } from "../context/footerContext";
import PresentationPage from "./components/PresentationPage";
import { Button } from "@/components/ui/button";
import { useRouter, useSearchParams } from "next/navigation";

View file

@ -1,85 +0,0 @@
import { useCallback } from "react";
export interface FooterProperties {
logoProperties: {
showLogo: boolean;
logoPosition: string;
opacity: number;
logoImage: {
light: string;
dark: string;
};
};
logoScale: number;
logoOffset: {
x: number;
y: number;
};
footerMessage: {
showMessage: boolean;
opacity: number;
fontSize: number;
message: string;
};
}
// Client-side service for footer properties
export const useFooterService = () => {
// Get footer properties
const getFooterProperties = useCallback(
async (): Promise<FooterProperties | null> => {
try {
const response = await fetch('/api/footer');
if (!response.ok) {
throw new Error('Failed to fetch footer properties');
}
const data = await response.json();
return data.properties;
} catch (error) {
console.error("Error retrieving footer properties:", error);
return null;
}
},
[]
);
// Save footer properties
const saveFooterProperties = useCallback(
async (properties: FooterProperties): Promise<boolean> => {
try {
const response = await fetch('/api/footer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ properties }),
});
if (!response.ok) {
throw new Error('Failed to save footer properties');
}
const result = await response.json();
return result.success;
} catch (error) {
console.error("Error saving footer properties:", error);
return false;
}
},
[]
);
// Reset footer properties
const resetFooterProperties = useCallback(
async (defaultProperties: FooterProperties): Promise<boolean> => {
return saveFooterProperties(defaultProperties);
},
[saveFooterProperties]
);
return {
getFooterProperties,
saveFooterProperties,
resetFooterProperties,
};
};

View file

@ -1,67 +0,0 @@
import { useCallback } from "react";
import { ThemeType } from "../upload/type";
export interface ThemeColors {
background: string;
slideBg: string;
slideTitle: string;
slideHeading: string;
slideDescription: string;
slideBox: string;
iconBg: string;
chartColors: string[];
fontFamily: string;
theme?: ThemeType;
}
export const useThemeService = () => {
const getTheme = useCallback(async (): Promise<{
name: string;
colors: ThemeColors;
} | null> => {
try {
const response = await fetch('/api/theme');
if (!response.ok) {
throw new Error('Failed to fetch theme');
}
const data = await response.json();
return data.theme;
} catch (error) {
console.error("Error retrieving theme:", error);
return null;
}
}, []);
const saveTheme = useCallback(
async (themeData: {
name: string;
colors: ThemeColors;
}): Promise<boolean> => {
try {
const response = await fetch('/api/theme', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ themeData }),
});
if (!response.ok) {
throw new Error('Failed to save theme');
}
const result = await response.json();
return result.success;
} catch (error) {
console.error("Error saving theme:", error);
return false;
}
},
[]
);
return {
getTheme,
saveTheme,
};
};

View file

@ -43,7 +43,7 @@ const page = () => {
<div className='relative'>
<Header />
<div className='flex flex-col items-center justify-center py-8'>
<h1 className='text-3xl font-semibold font-instrument_sans'>Create Presentation </h1>
<h1 className='text-3xl font-semibold font-instrument_sans'>Create Presentation </h1>
{/* <p className='text-sm text-gray-500'>We will generate a presentation for you</p> */}
</div>

View file

@ -24,8 +24,7 @@ export async function POST(req: NextRequest) {
]
});
const page = await browser.newPage();
await page.goto(`http://localhost/pdf-maker?id=${id}`);
await page.waitForNetworkIdle();
await page.goto(`http://localhost/pdf-maker?id=${id}`, { waitUntil: 'networkidle0',timeout: 80000 });
const pdfBuffer = await page.pdf({
printBackground: true,

View file

@ -3,7 +3,6 @@ import { promises as fs } from 'fs'
import path from 'path'
import { GroupSetting } from '@/app/layout-preview/types'
export async function GET() {
try {
// Get the path to the presentation-layouts directory

View file

@ -5,7 +5,7 @@ import { useGroupLayoutLoader } from '../hooks/useGroupLayoutLoader'
import LoadingStates from '../components/LoadingStates'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { ArrowLeft, Home } from 'lucide-react'
import { ArrowLeft, Home, Wifi, WifiOff, RefreshCw } from 'lucide-react'
const GroupLayoutPreview = () => {
const params = useParams()
@ -63,6 +63,7 @@ const GroupLayoutPreview = () => {
<p className="text-gray-600 mt-2">
{layoutGroup.layouts.length} layout{layoutGroup.layouts.length !== 1 ? 's' : ''} {layoutGroup.settings.description}
</p>
</div>
</div>
</header>

View file

@ -67,7 +67,6 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
ordered: false,
isDefault: false
}
for (const fileName of targetGroupData.files) {
try {
const layoutName = fileName.replace('.tsx', '').replace('.ts', '')
@ -163,8 +162,7 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
}
const retry = () => {
// Clear cache for this group to force reload
layoutGroupCache.delete(groupSlug)
hasMountedRef.current = false
loadGroupLayouts()
}
@ -179,6 +177,6 @@ export const useGroupLayoutLoader = (groupSlug: string): UseGroupLayoutLoaderRet
layoutGroup,
loading,
error,
retry
retry,
}
}

View file

@ -5,7 +5,7 @@ import { useLayoutLoader } from './hooks/useLayoutLoader'
import LoadingStates from './components/LoadingStates'
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { ExternalLink } from 'lucide-react'
import { ExternalLink, Wifi, WifiOff, RefreshCw } from 'lucide-react'
const LayoutPreview = () => {
const { layoutGroups, layouts, loading, error, retry } = useLayoutLoader()
@ -35,6 +35,7 @@ const LayoutPreview = () => {
<p className="text-gray-600 mt-2">
{layoutGroups.length} groups {layouts.length} layouts
</p>
</div>
</div>

View file

@ -3,7 +3,6 @@ import localFont from "next/font/local";
import { Fraunces, Montserrat, Inria_Serif, Roboto, Instrument_Sans } from "next/font/google";
import "./globals.css";
import { Providers } from "./providers";
import { FooterProvider } from "./(presentation-generator)/context/footerContext";
import { LayoutProvider } from "./(presentation-generator)/context/LayoutContext";
import { Toaster } from "sonner";
@ -104,9 +103,9 @@ export default function RootLayout({
>
<Providers>
<LayoutProvider>
<FooterProvider>
{children}
</FooterProvider>
{children}
</LayoutProvider>
</Providers>
<Toaster position="top-center" richColors={true} />

View file

@ -1,6 +1,14 @@
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
images: {
remotePatterns: [

View file

@ -7,9 +7,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"get:version": "next --version"
},
"lint": "next lint"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@ -66,6 +65,7 @@
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.13",
"cypress": "^14.3.3",
"tailwindcss": "^3.4.1",
"typescript": "^5"

View file

@ -1,6 +1,5 @@
{
"id": "analytics",
"name": "Analytics",
"description": "Data-focused layouts with glass visual style and modern typography",
"ordered": true,
"isDefault": false

View file

@ -7,10 +7,10 @@ export const layoutName = 'Type1 Slide'
export const layoutDescription = 'A clean two-column layout with title and description on the left and a featured image on the right.'
const type1SlideSchema = z.object({
title: z.string().min(3).max(100).default('Sample Title').meta({
title: z.string().min(3).max(100).default('Hot NOT Reload Working!').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(500).default('Your description content goes here. This layout provides a clean and professional way to present content with supporting imagery.').meta({
description: z.string().min(10).max(500).default('This is a test of the hot reload system! If you can see this text, hot reload is working perfectly. Changes should appear instantly without page refresh.').meta({
description: "Main description text",
}),
image: ImageSchema.default({
@ -30,7 +30,6 @@ interface Type1SlideLayoutProps {
}
const Type1SlideLayout: React.FC<Type1SlideLayoutProps> = ({ data: slideData }) => {
return (
<div
className=" 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"
@ -40,12 +39,12 @@ const Type1SlideLayout: React.FC<Type1SlideLayoutProps> = ({ data: slideData })
<div className="flex flex-col w-full items-start justify-center space-y-1 md:space-y-2 lg:space-y-6">
{/* Title */}
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
{slideData?.title || 'Sample Title'}
{slideData?.title || ' This is the title of slide'}
</h1>
{/* Description */}
<p className="text-base sm:text-lg lg:text-xl text-gray-700 leading-relaxed">
{slideData?.description || 'Your description content goes here. This layout provides a clean and professional way to present content with supporting imagery.'}
{slideData?.description || 'This is a test of the hot reload system! If you can see this text, hot reload is working perfectly. Changes should appear instantly without page refresh.'}
</p>
</div>