Merge branch 'feat/custom_schema_and_layout' into feat/presentation_export
This commit is contained in:
commit
ea434e07dd
28 changed files with 136 additions and 1081 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export interface LayoutGroup {
|
|||
description: string;
|
||||
ordered: boolean;
|
||||
isDefault?: boolean;
|
||||
slides: string[];
|
||||
slides?: any
|
||||
}
|
||||
|
||||
export interface LoadingState {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"id": "analytics",
|
||||
"name": "Analytics",
|
||||
|
||||
"description": "Data-focused layouts with glass visual style and modern typography",
|
||||
"ordered": true,
|
||||
"isDefault": false
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue