refactor(Nextjs/outline): Remove markdwon editor in streaming & increase export_as_pdf navigation timeout
This commit is contained in:
parent
0b13a5dac2
commit
516c9bade0
13 changed files with 18 additions and 957 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -105,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 */}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -17,7 +17,7 @@ export async function POST(req: NextRequest) {
|
|||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`http://localhost/pdf-maker?id=${id}`, { waitUntil: 'networkidle0' });
|
||||
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 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,5 @@
|
|||
{
|
||||
"id": "analytics",
|
||||
"name": "Analytics",
|
||||
|
||||
"description": "Data-focused layouts with glass visual style and modern typography",
|
||||
"ordered": true,
|
||||
"isDefault": false
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ 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">
|
||||
nice {slideData?.title || ' This is the title of slide'}
|
||||
{slideData?.title || ' This is the title of slide'}
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue