Replace BASE_URL references;Cleaned up the slides Layout.

This commit is contained in:
shiva raj badu 2025-05-10 23:07:45 +05:45
parent 7f37966ee0
commit 10b0c830ef
19 changed files with 101 additions and 3322 deletions

View file

@ -37,6 +37,7 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import ToolTip from "@/components/ToolTip";
import { BASE_URL } from "@/utils/constant";
interface ImageEditorProps {
initialImage: string | null;
@ -90,7 +91,7 @@ const ImageEditor = ({
(properties &&
properties[imageIdx] &&
properties[imageIdx].initialObjectFit) ||
"cover"
"cover"
);
const imageRef = useRef<HTMLImageElement>(null);
const imageContainerRef = useRef<HTMLDivElement>(null);
@ -569,30 +570,30 @@ const ImageEditor = ({
<div className="grid grid-cols-2 gap-4">
{isGenerating || previewImages.length === 0
? Array.from({ length: 4 }).map((_, index) => (
<Skeleton
key={index}
className="aspect-[4/3] w-full rounded-lg"
/>
))
<Skeleton
key={index}
className="aspect-[4/3] w-full rounded-lg"
/>
))
: previewImages.map((image, index) => (
<div
key={index}
onClick={() => handleImageChange(image as string)}
className="aspect-[4/3] w-full overflow-hidden rounded-lg border cursor-pointer"
>
<img
src={
image
? image.startsWith("user")
? `${PresentationGenerationApi.BASE_URL}${image}`
: image
: ""
}
alt={`Preview ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
<div
key={index}
onClick={() => handleImageChange(image as string)}
className="aspect-[4/3] w-full overflow-hidden rounded-lg border cursor-pointer"
>
<img
src={
image
? image.startsWith("user")
? `${BASE_URL}${image}`
: image
: ""
}
alt={`Preview ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
</div>
</div>
</TabsContent>
@ -626,25 +627,25 @@ const ImageEditor = ({
<div className="grid grid-cols-2 gap-4 max-h-[80vh] hide-scrollbar overflow-y-auto">
{isSearching
? Array.from({ length: 6 }).map((_, index) => (
<Skeleton
key={index}
className="aspect-[4/3] w-full rounded-lg"
/>
))
<Skeleton
key={index}
className="aspect-[4/3] w-full rounded-lg"
/>
))
: searchedImages.map((imgSrc, index) => (
<div
key={index}
onClick={() => handleImageChange(imgSrc)}
className="aspect-[4/3] cursor-pointer group relative rounded-lg overflow-hidden"
>
<img
src={imgSrc}
alt={`Search result ${index + 1}`}
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all duration-200" />
</div>
))}
<div
key={index}
onClick={() => handleImageChange(imgSrc)}
className="aspect-[4/3] cursor-pointer group relative rounded-lg overflow-hidden"
>
<img
src={imgSrc}
alt={`Search result ${index + 1}`}
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all duration-200" />
</div>
))}
</div>
</TabsContent>

View file

@ -1,789 +0,0 @@
import React, { useState } from "react";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Slider } from "@/components/ui/slider";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { useDispatch, useSelector } from "react-redux";
import { updateInfographicsChart } from "@/store/slices/presentationGeneration";
import { RootState } from "@/store/store";
import { getPercentage, ICON_LIST, IconMapper } from "../../utils/IconList";
type Chart = {
chart_type: string;
icon?: string;
value: {
number_type: string;
numerator?: number;
denominator?: number;
percentage?: number;
numerical?: number;
suffix?: string;
};
};
const colors = [
"#6453ff",
"#22c1dd",
"#ff6453",
"#ffc122",
"#22ddc1",
"#c122ff",
"#dd22ff",
"#ff22c1",
"#c1ff22",
"#22ffc1",
];
const CHART_TYPES = [
{ value: "progress-dial", label: "Progress Dial" },
{ value: "radial-progress", label: "Radial Progress" },
{ value: "progress-ring", label: "Progress Ring" },
{ value: "progress-bar", label: "Progress Bar" },
{ value: "icon-infographic", label: "Icon Infographic" },
];
const AllInfoGraphics = ({
slideIndex,
itemIndex,
chart,
}: {
slideIndex: number;
itemIndex: number;
chart: Chart;
}) => {
const dispatch = useDispatch();
const [isEditingOpen, setIsEditingOpen] = useState(false);
const [lineWeight, setLineWeight] = useState(20);
const [selectedIcon, setSelectedIcon] = useState(chart.icon || "star");
const [chartType, setChartType] = useState(chart.chart_type);
// State for both percentage and fraction
const [percentageValue, setPercentageValue] = useState(
chart.value.number_type === "fraction"
? getPercentage(chart.value.numerator!, chart.value.denominator!)
: chart.value.percentage || 0
);
const [numerator, setNumerator] = useState(chart.value.numerator || 0);
const [denominator, setDenominator] = useState(
chart.value.denominator || 100
);
const [numberType, setNumberType] = useState(chart.value.number_type);
const { currentColors } = useSelector((state: RootState) => state.theme);
const handlePercentageChange = (value: number) => {
const newValue = Math.min(1000, Math.max(0, value));
setPercentageValue(newValue);
if (numberType === "fraction") {
// Update numerator based on percentage while keeping denominator same
const newNumerator = Math.round((newValue / 100) * denominator);
setNumerator(newNumerator);
}
};
const handleFractionChange = (num: number | null, den: number | null) => {
// Ensure we have valid numbers
const validNum = num !== null ? Math.max(0, num) : numerator;
const validDen = den !== null ? Math.max(1, den) : denominator;
setNumerator(validNum);
setDenominator(validDen);
// Calculate and update percentage
const newPercentage = getPercentage(validNum, validDen);
setPercentageValue(newPercentage);
};
const handlePercentageInputChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
const value = e.target.value;
if (value === "") {
setPercentageValue(0); // Keep internal state at 0
return;
}
handlePercentageChange(Number(value));
};
const handlePercentageBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (e.target.value === "") {
handlePercentageChange(0);
}
};
const handleNumeratorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (value === "") {
setNumerator(0); // Keep internal state at 0
return;
}
handleFractionChange(Number(value), null);
};
const handleNumeratorBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (e.target.value === "") {
handleFractionChange(0, null);
}
};
const handleDenominatorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
if (value === "") {
setDenominator(1); // Keep internal state at 1
return;
}
handleFractionChange(null, Number(value));
};
const handleDenominatorBlur = (e: React.FocusEvent<HTMLInputElement>) => {
if (e.target.value === "") {
handleFractionChange(null, 1);
}
};
const handleContentEdit = (e: React.FormEvent<HTMLParagraphElement>) => {
const content = e.currentTarget.textContent || "";
// Check if content is in fraction format (e.g., "3/4")
if (content.includes("/")) {
const [num, den] = content.split("/").map((n) => parseInt(n));
if (!isNaN(num) && !isNaN(den) && den !== 0) {
setNumberType("fraction");
handleFractionChange(num, den);
}
}
// Check if content is in percentage format (e.g., "75%")
else {
const value = parseInt(content.replace("%", ""));
if (!isNaN(value)) {
setNumberType("percentage");
handlePercentageChange(value);
}
}
};
const handleSave = () => {
const chartData = {
chart_type: chartType,
value: {
number_type: numberType,
},
} as Chart;
// Add icon if it's an icon infographic
if (chartType === "icon-infographic") {
chartData.icon = selectedIcon;
}
// Add values based on number type
if (numberType === "fraction") {
chartData.value.numerator = numerator;
chartData.value.denominator = denominator;
} else if (numberType === "percentage") {
chartData.value.percentage = percentageValue;
}
dispatch(
updateInfographicsChart({
slideIndex: slideIndex,
itemIdx: itemIndex,
chart: chartData,
})
);
setIsEditingOpen(false);
};
const handleSheetClose = () => {
handleSave();
setIsEditingOpen(false);
};
const handleInfographicClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
if (chartType !== "text") {
setIsEditingOpen(true);
}
};
return (
<>
<div
onClick={handleInfographicClick}
key={chart.chart_type}
data-slide-element
data-element-type="graph"
data-graph-type="progress"
data-element-id={`slide-group-${slideIndex}-item-${itemIndex}-graph`}
className={` max-md:pointer-events-none ${
chartType === "text" ? "cursor-default" : "cursor-pointer"
}`}
>
{chartType === "progress-dial" && (
<ProgressDial
color={currentColors.chartColors[0]}
percentage={percentageValue}
/>
)}
{chartType === "radial-progress" && (
<RadialProgress
strokeWidth={lineWeight}
color={currentColors.chartColors[0]}
percentage={percentageValue}
numerator={numerator}
denominator={denominator}
onContentEdit={handleContentEdit}
numberType={numberType}
/>
)}
{chartType === "progress-ring" && (
<ProgressRing
color={currentColors.chartColors[0]}
percentage={percentageValue}
numerator={numerator}
denominator={denominator}
lineWeight={lineWeight}
onContentEdit={handleContentEdit}
numberType={numberType}
/>
)}
{chartType === "progress-bar" && (
<ProgressBar
progressBg={currentColors.chartColors[2]}
color={currentColors.chartColors[0]}
percentage={percentageValue}
lineWeight={lineWeight}
/>
)}
{chartType === "icon-infographic" && (
<IconGraphics
color={currentColors.chartColors[0]}
icon={selectedIcon}
percentage={percentageValue}
/>
)}
{chartType === "text" && (
<TextInfographic
dispatch={dispatch}
slideIndex={slideIndex}
itemIndex={itemIndex}
item={chart}
suffix={chart.value.suffix || ""}
numerical={chart.value.numerical || 0}
iconBg={currentColors.iconBg}
/>
)}
</div>
<Sheet open={isEditingOpen} onOpenChange={handleSheetClose} modal={false}>
<SheetContent>
<SheetHeader>
<SheetTitle>Chart Settings</SheetTitle>
</SheetHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label>Chart Type</Label>
<Select value={chartType} onValueChange={setChartType}>
<SelectTrigger>
<SelectValue placeholder="Select chart type" />
</SelectTrigger>
<SelectContent>
{CHART_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<Label>Display Format</Label>
<Select value={numberType} onValueChange={setNumberType}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="percentage">Percentage</SelectItem>
<SelectItem value="fraction">Fraction</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Percentage</Label>
<div className="flex items-center space-x-4">
<div className="flex-1">
<Slider
value={[percentageValue]}
max={100}
step={1}
onValueChange={(value) => {
handlePercentageChange(value[0]);
}}
/>
</div>
<div className="w-20">
<Input
type="number"
value={percentageValue || ""}
onChange={handlePercentageInputChange}
onBlur={handlePercentageBlur}
min={0}
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label>Fraction</Label>
<div className="flex items-center space-x-2">
<Input
type="number"
value={numerator || ""}
onChange={handleNumeratorChange}
onBlur={handleNumeratorBlur}
min={0}
className="w-20"
/>
<span className="text-lg">/</span>
<Input
type="number"
value={denominator || ""}
onChange={handleDenominatorChange}
onBlur={handleDenominatorBlur}
min={1}
className="w-20"
/>
<div className="ml-2 text-sm text-muted-foreground">
= {percentageValue.toFixed(1)}%
</div>
</div>
</div>
</div>
{chartType === "icon-infographic" && (
<div className="space-y-2">
<Label>Icons</Label>
<div className="grid grid-cols-6 gap-2 max-h-[200px] custom_scrollbar overflow-y-auto p-2 border rounded-md">
{Object.entries(ICON_LIST).map(([key, Icon]) => (
<button
key={key}
className={` rounded-lg border-2 hover:bg-slate-100 transition-colors ${
selectedIcon === key
? "border-primary bg-slate-100"
: "border-transparent"
}`}
onClick={() => setSelectedIcon(key)}
>
{IconMapper(false, key)}
</button>
))}
</div>
</div>
)}
</div>
<SheetFooter>
<Button
className="w-full mt-8 bg-primary text-white"
onClick={handleSave}
>
Save
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</>
);
};
export default AllInfoGraphics;
export function IconGraphics({
color,
icon,
percentage,
}: {
color: string;
icon: string;
percentage?: number;
}) {
const percentageValue = percentage && percentage > 100 ? 100 : percentage;
const radius = 110;
const circumference = 2 * Math.PI * radius;
const gap = percentageValue === 100 ? 0 : 10; // No gap at 100%
const adjustedCircumference = circumference - gap;
return (
<div className="relative w-28 h-28 lg:w-44 lg:h-44 xl:w-48 xl:h-48 mx-auto md:mb-6">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 240 240">
<circle
strokeWidth="20"
stroke={color}
opacity={0.3}
fill="transparent"
r={radius}
cx="120"
cy="120"
/>
{percentageValue !== 0 && (
<circle
style={{ stroke: color }}
strokeWidth="20"
strokeLinecap={percentageValue! >= 100 ? "butt" : "round"}
stroke="currentColor"
fill="transparent"
r={radius}
cx="120"
cy="120"
strokeDasharray={`${adjustedCircumference}`}
strokeDashoffset={
((100 - percentageValue!) / 100) * adjustedCircumference + gap
}
/>
)}
</svg>
<div
style={{ color: color }}
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-4xl font-bold"
>
{IconMapper(false, icon)}
</div>
</div>
);
}
export function ProgressDial({
color,
percentage,
}: {
color: string;
percentage?: number;
}) {
// Calculate needle rotation based on percentage
const needleRotation = Math.round((percentage! / 100) * 180 - 90);
return (
<div className="relative w-32 h-20 lg:w-56 lg:h-44 xl:w-64 xl:h-48 mx-auto md:mb-6 flex justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 120">
{/* Background arc */}
<path
d="M10,100 A 90,90 0 0,1 190,100"
stroke={color}
strokeWidth="20"
fill="none"
/>
{/* Needle */}
<path
id="needle"
d="M92,100 C92,95 108,95 108,100 L100,20 Z"
fill={color}
transform={`rotate(${needleRotation}, 100, 100)`}
/>
{/* Center circle */}
<circle
cx="100"
cy="100"
r="8"
fill="white"
stroke={color}
strokeWidth="2"
/>
</svg>
</div>
);
}
export function RadialProgress({
strokeWidth,
color,
percentage,
numerator,
denominator,
onContentEdit,
numberType = "percentage",
}: {
strokeWidth: number;
color: string;
percentage?: number;
numerator?: number;
denominator?: number;
onContentEdit?: (e: React.FormEvent<HTMLParagraphElement>) => void;
numberType?: string;
}) {
const percentageValue =
percentage && percentage > 100 ? 100 : percentage ?? 0;
const radius = 90;
const arcLength = Math.PI * radius;
const strokeDasharray = arcLength;
const correctionFactor = percentageValue === 100 ? 1 : 0.98;
const strokeDashoffset =
arcLength * (1 - (percentageValue / 100) * correctionFactor);
return (
<div className="relative w-32 h-20 lg:w-56 lg:h-44 xl:w-64 xl:h-48 mx-auto md:mb-6">
<svg className="w-full h-full" viewBox="0 0 200 120">
{/* Background half-circle */}
<path
strokeWidth="15"
stroke={color}
strokeLinecap="round"
opacity={0.3}
fill="transparent"
d="M10,100 A 90,90 0 0,1 190,100"
/>
{/* Foreground half-circle */}
<path
stroke={color}
strokeWidth="15"
strokeLinecap="round"
fill="transparent"
d="M10,100 A 90,90 0 0,1 190,100"
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
/>
</svg>
<div className="absolute top-[65%] left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-4xl font-bold">
<p
onClick={(e) => {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(e.currentTarget);
selection?.removeAllRanges();
selection?.addRange(range);
}}
onInput={onContentEdit}
style={{ color }}
className="text-[24px] focus-visible:outline-none leading-[32px] font-bold"
>
{numberType === "fraction" && numerator && denominator
? `${numerator}/${denominator}`
: `${percentageValue}%`}
</p>
</div>
</div>
);
}
export function ProgressRing({
color,
percentage,
numerator,
denominator,
lineWeight = 20,
onContentEdit,
numberType = "percentage",
}: {
color: string;
percentage?: number;
numerator?: number;
denominator?: number;
lineWeight?: number;
onContentEdit?: (e: React.FormEvent<HTMLParagraphElement>) => void;
numberType?: string;
}) {
const percentageValue = percentage && percentage > 100 ? 100 : percentage;
const radius = 110;
const circumference = 2 * Math.PI * radius;
const gap = percentageValue === 100 ? 0 : 10; // No gap at 100%
const adjustedCircumference = circumference - gap;
return (
<div className="relative w-28 h-28 lg:w-44 lg:h-44 xl:w-48 xl:h-48 mx-auto md:mb-6">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 240 240">
<circle
strokeWidth="20"
stroke={color}
opacity={0.3}
fill="transparent"
r={radius}
cx="120"
cy="120"
/>
{percentageValue !== 0 && (
<circle
style={{ stroke: color }}
strokeWidth="20"
strokeLinecap={percentageValue === 100 ? "butt" : "round"}
stroke="currentColor"
fill="transparent"
r={radius}
cx="120"
cy="120"
strokeDasharray={`${adjustedCircumference}`}
strokeDashoffset={
((100 - percentageValue!) / 100) * adjustedCircumference + gap
}
/>
)}
</svg>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-4xl font-bold">
<p
onClick={(e) => {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(e.currentTarget);
selection?.removeAllRanges();
selection?.addRange(range);
}}
onInput={onContentEdit}
contentEditable
suppressContentEditableWarning
style={{ color: color }}
className="text-base md:text-[24px] focus-visible:outline-none leading-[32px] font-bold"
>
{numberType === "fraction" && numerator && denominator
? `${numerator}/${denominator}`
: `${percentage}%`}
</p>
</div>
</div>
);
}
export function ProgressBar({
progressBg,
color,
percentage,
lineWeight = 20,
}: {
progressBg: string;
color: string;
percentage?: number;
lineWeight?: number;
}) {
const percentageValue = percentage && percentage > 100 ? 100 : percentage;
return (
<div
className={`relative w-full rounded-full mx-auto mb-6 `}
style={{
backgroundColor: progressBg,
height: `${lineWeight}px`,
}}
>
<div
style={{ backgroundColor: color, width: `${percentageValue}%` }}
className="absolute rounded-full inset-0 flex items-center justify-center"
/>
</div>
);
}
export function TextInfographic({
dispatch,
iconBg,
slideIndex,
itemIndex,
item,
suffix,
numerical,
}: {
dispatch: any;
slideIndex: number;
itemIndex: number;
item: any;
suffix: string;
numerical: number;
iconBg: string;
}) {
const updateChart = ({
slideIndex,
itemIdx,
chart,
}: {
slideIndex: number;
itemIdx: number;
chart: any;
}) => {
dispatch(updateInfographicsChart({ slideIndex, itemIdx, chart }));
};
const formatNumber = (value: number) => {
if (isNaN(value)) {
return value;
}
const absValue = Math.abs(value);
if (absValue >= 1000000000) {
return `${(absValue / 1000000000).toFixed(0)}B`;
} else if (absValue >= 1000000) {
return `${(absValue / 1000000).toFixed(0)}M`;
} else if (absValue >= 1000) {
return `${(absValue / 1000).toFixed(0)}k`;
}
return absValue.toString();
};
return (
<div
style={{ backgroundColor: iconBg }}
className="w-28 h-28 lg:w-44 lg:h-44 xl:w-48 xl:h-48 mx-auto rounded-full flex items-center justify-center mb-6"
>
<div
className={`text-center text-white cursor-text ${
suffix.length === 1 ? "flex items-center gap-1" : ""
}`}
>
<p
onBlur={(e) => {
updateChart({
slideIndex,
itemIdx: itemIndex,
chart: {
chart_type: item.chart_type,
value: {
number_type: item.value.number_type,
numerical: e.currentTarget.innerText,
suffix: item.value.suffix,
},
},
});
}}
contentEditable={true}
suppressContentEditableWarning
className="text-base md:text-[24px] focus-visible:outline-none leading-[40px] font-bold"
>
{formatNumber(numerical)}
</p>
<p
onBlur={(e) => {
updateChart({
slideIndex,
itemIdx: itemIndex,
chart: {
chart_type: item.chart_type,
value: {
number_type: item.value.number_type,
numerical: item.value.numerical,
suffix: e.currentTarget.innerText,
},
},
});
}}
contentEditable={true}
suppressContentEditableWarning
className="text-base md:text-[20px] cursor-text focus-visible:outline-none leading-[24px] font-bold"
>
{suffix.toString().replace(/\*\*/g, "")}
</p>
</div>
</div>
);
}

View file

@ -1,359 +0,0 @@
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { IconMapper } from "../../utils/IconList";
type Chart = {
chart_type: string;
icon?: string;
value: {
number_type: string;
numerator?: number;
denominator?: number;
percentage?: number;
numerical?: number;
suffix?: string;
};
};
const colors = [
"#6453ff",
"#22c1dd",
"#ff6453",
"#ffc122",
"#22ddc1",
"#c122ff",
"#dd22ff",
"#ff22c1",
"#c1ff22",
"#22ffc1",
];
const MiniInfoGraphics = ({
slideIndex,
itemIndex,
chart,
}: {
slideIndex: number;
itemIndex: number;
chart: Chart;
}) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
const percentage =
chart.value.number_type === "fraction" &&
chart.value.numerator &&
chart.value.denominator
? (chart.value.numerator / chart.value.denominator) * 100
: chart.value.percentage || 0;
return (
<div>
{chart.chart_type === "progress-dial" && (
<MiniProgressDial
color={currentColors.chartColors[0]}
percentage={percentage}
/>
)}
{chart.chart_type === "radial-progress" && (
<MiniRadialProgress
color={currentColors.chartColors[0]}
percentage={percentage}
numerator={chart.value.numerator}
denominator={chart.value.denominator}
numberType={chart.value.number_type}
/>
)}
{chart.chart_type === "progress-ring" && (
<MiniProgressRing
color={currentColors.chartColors[0]}
percentage={percentage}
numerator={chart.value.numerator}
denominator={chart.value.denominator}
numberType={chart.value.number_type}
/>
)}
{chart.chart_type === "progress-bar" && (
<MiniProgressBar
color={currentColors.chartColors[0]}
percentage={percentage}
/>
)}
{chart.chart_type === "icon-infographic" && (
<MiniIconGraphics
color={currentColors.chartColors[0]}
icon={chart.icon || "star"}
percentage={percentage}
/>
)}
{chart.chart_type === "text" && (
<MiniTextInfographic
iconBg={currentColors.iconBg}
numerical={chart.value.numerical || 0}
/>
)}
</div>
);
};
export default MiniInfoGraphics;
function MiniIconGraphics({
color,
icon,
percentage,
}: {
color: string;
icon: string;
percentage: number;
}) {
const percentageValue = percentage > 100 ? 100 : percentage;
const radius = 20;
const circumference = 2 * Math.PI * radius;
const gap = percentageValue === 100 ? 0 : 2;
const adjustedCircumference = circumference - gap;
return (
<div className="relative w-12 h-12 mx-auto">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 60 60">
<circle
className="text-gray-200"
strokeWidth="6"
stroke={color}
opacity={0.3}
fill="transparent"
r={radius}
cx="30"
cy="30"
/>
{percentageValue !== 0 && (
<circle
style={{ stroke: color }}
strokeWidth="6"
strokeLinecap={percentageValue === 100 ? "butt" : "round"}
stroke="currentColor"
fill="transparent"
r={radius}
cx="30"
cy="30"
strokeDasharray={`${adjustedCircumference}`}
strokeDashoffset={
((100 - percentage) / 100) * adjustedCircumference + gap
}
/>
)}
</svg>
<div
style={{ color: color }}
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-sm"
>
{IconMapper(true, icon)}
</div>
</div>
);
}
function MiniProgressDial({
color,
percentage,
}: {
color: string;
percentage: number;
}) {
const percentageValue = percentage > 100 ? 100 : percentage;
const needleRotation = Math.round((percentageValue / 100) * 180 - 90);
return (
<div className="relative w-12 h-8 mx-auto">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 80">
<path
d="M10,60 A 50,50 0 0,1 110,60"
stroke={color}
strokeWidth="12"
fill="none"
/>
<path
d="M52,60 C52,57 68,57 68,60 L60,15 Z"
fill={color}
transform={`rotate(${needleRotation}, 60, 60)`}
/>
<circle
cx="60"
cy="60"
r="4"
fill="white"
stroke={color}
strokeWidth="1.5"
/>
</svg>
</div>
);
}
function MiniRadialProgress({
color,
percentage,
numerator,
denominator,
numberType,
}: {
color: string;
percentage: number;
numerator?: number;
denominator?: number;
numberType: string;
}) {
const percentageValue = percentage > 100 ? 100 : percentage;
const radius = 20;
const arcLength = Math.PI * radius;
const offset =
percentageValue === 100 ? 0 : arcLength * (1 - percentageValue / 100);
return (
<div className="relative w-12 h-8 mx-auto">
<svg className="w-full h-full" viewBox="0 0 60 30">
<path
className="text-gray-200"
d="M6,30 A 24,24 0 0,1 54,30"
strokeWidth="6"
stroke={color}
opacity={0.3}
fill="none"
strokeLinecap="round"
/>
<path
style={{ stroke: color }}
strokeWidth="6"
strokeLinecap="round"
stroke="currentColor"
fill="none"
d="M6,30 A 24,24 0 0,1 54,30"
strokeDasharray={`${arcLength}, ${arcLength}`}
strokeDashoffset={offset}
/>
</svg>
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 text-[10px] font-bold">
<p style={{ color: color }}>
{numberType === "fraction" && numerator && denominator
? `${numerator}/${denominator}`
: `${percentage}%`}
</p>
</div>
</div>
);
}
function MiniProgressRing({
color,
percentage,
numerator,
denominator,
numberType,
}: {
color: string;
percentage: number;
numerator?: number;
denominator?: number;
numberType: string;
}) {
const percentageValue = percentage > 100 ? 100 : percentage;
const radius = 20;
const circumference = 2 * Math.PI * radius;
const gap = percentageValue === 100 ? 0 : 2;
const adjustedCircumference = circumference - gap;
return (
<div className="relative w-12 h-12 mx-auto">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 60 60">
<circle
className="text-gray-200"
strokeWidth="6"
stroke={color}
opacity={0.3}
fill="transparent"
r={radius}
cx="30"
cy="30"
/>
{percentageValue !== 0 && (
<circle
style={{ stroke: color }}
strokeWidth="6"
strokeLinecap={percentageValue === 100 ? "butt" : "round"}
stroke="currentColor"
fill="transparent"
r={radius}
cx="30"
cy="30"
strokeDasharray={`${adjustedCircumference}`}
strokeDashoffset={
((100 - percentageValue) / 100) * adjustedCircumference + gap
}
/>
)}
</svg>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-[8px] font-bold">
<p style={{ color: color }}>
{numberType === "fraction" && numerator && denominator
? `${numerator}/${denominator}`
: `${percentage}%`}
</p>
</div>
</div>
);
}
function MiniProgressBar({
color,
percentage,
}: {
color: string;
percentage: number;
}) {
const percentageValue = percentage > 100 ? 100 : percentage;
return (
<div
className="relative w-12 rounded-full mx-auto"
style={{
backgroundColor: "rgb(229 231 235)",
height: "4px",
}}
>
<div
style={{ backgroundColor: color, width: `${percentageValue}%` }}
className="absolute rounded-full inset-0"
/>
</div>
);
}
function MiniTextInfographic({
iconBg,
numerical,
}: {
iconBg: string;
numerical: number;
}) {
const formatNumber = (value: number) => {
if (isNaN(value)) {
return value;
}
const absValue = Math.abs(value);
if (absValue >= 1000000000) {
return `${(absValue / 1000000000).toFixed(0)}B`;
} else if (absValue >= 1000000) {
return `${(absValue / 1000000).toFixed(0)}M`;
} else if (absValue >= 1000) {
return `${(absValue / 1000).toFixed(0)}k`;
}
return absValue.toString();
};
return (
<div
className="w-8 h-8 rounded-full flex-col mx-auto mb-1 flex items-center justify-center"
style={{ backgroundColor: iconBg }}
>
<p className="text-[10px] text-white">{formatNumber(numerical)}</p>
</div>
);
}

View file

@ -1,82 +0,0 @@
import React from 'react'
import AllInfoGraphics from '../info_graphics/AllInfoGraphics';
import MiniInfoGraphics from '../info_graphics/MiniInfoGraphics';
import MiniTypeWriter from './MiniTypeWriter';
type Type10MiniProps = {
slideIndex: number;
title: string;
description?: string;
design_index: number;
infographics: {
title: string;
description: string;
chart: {
chart_type: string;
value: {
number_type: string;
numerical: number;
suffix: string;
numerator?: number;
denominator?: number;
percentage?: number;
}
}
}[];
}
const Type10Mini = ({ slideIndex, title, description, infographics, design_index }: Type10MiniProps) => {
if (infographics.length === 1) {
return (
<div className="slide-container border shadow-xl border-gray-200 max-h-[150px] aspect-video rounded-lg pointer-events-none flex items-center justify-center p-2 w-full">
<div className="grid grid-cols-2 gap-4 w-full">
<div>
<p className='text-[8px] slide-title font-semibold'>
<MiniTypeWriter text={title} />
</p>
{description && <p className='text-[6px] slide-description'>
<MiniTypeWriter text={description} />
</p>}
</div>
<div>
<MiniInfoGraphics slideIndex={slideIndex} itemIndex={0} chart={infographics[0].chart} />
<p className='text-[6px] font-medium slide-description'>
<MiniTypeWriter text={infographics[0].description} />
</p>
</div>
</div>
</div>
)
} else {
return (
<div className='w-full aspect-video border shadow-xl border-gray-200 bg-white p-4 flex rounded-lg flex-col items-center justify-center slide-container'>
<div className="text-center mb-4">
<p className='text-[8px] slide-title font-semibold'>
<MiniTypeWriter text={title} />
</p>
{description && <p className='text-[6px] slide-description'>
<MiniTypeWriter text={description} />
</p>}
</div>
<div style={{ gridTemplateColumns: `repeat(${infographics.length}, 1fr)` }} className='grid w-full gap-1'>
{infographics.map((item, index) => (
<div key={index} className={`text-center p-1 ${design_index === 1 ? 'slide-box rounded-lg ' : ''}`}>
<MiniInfoGraphics slideIndex={slideIndex} itemIndex={index} chart={item.chart} />
<p className='text-[6px] line-clamp-1 font-medium slide-heading'>
<MiniTypeWriter text={item.title} />
</p>
<p className='text-[5px] line-clamp-2 font-medium slide-description'>
<MiniTypeWriter text={item.description} />
</p>
</div>
))}
</div>
</div>
)
}
}
export default Type10Mini

View file

@ -1,47 +0,0 @@
import { RootState } from '@/store/store';
import React from 'react'
import { useSelector } from 'react-redux';
type Type11MiniProps = {
title: string;
description?: string;
infographics: {
title: string;
description: string;
chart: {
chart_type: string;
value: {
number_type: string;
numerical: number;
suffix: string;
}
}
}[];
}
const Type11Mini = ({ title, description, infographics }: Type11MiniProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
return (
<div className='w-full aspect-video bg-white rounded-lg p-4 flex flex-col items-center justify-center slide-container'>
<div className="text-center mb-4">
<p className='text-[8px] slide-title font-semibold'>{title}</p>
{description && <p className='text-[6px] slide-description'>{description}</p>}
</div>
<div style={{ gridTemplateColumns: `repeat(${infographics.length}, 1fr)` }} className='grid justify-center w-full gap-2'>
{infographics.map((item, index) => (
<div key={index} className={`bg-gray-50 slide-box rounded p-1 text-center ${infographics.length === 1 ? 'max-w-[100px] mx-auto' : ''}`}>
<div className="w-8 h-8 rounded-full flex-col mx-auto mb-1 flex items-center justify-center"
style={{ backgroundColor: currentColors.iconBg }}
>
<p className='text-[10px] text-white'>{item.chart.value.numerical}</p>
</div>
<p className='text-[8px] font-medium slide-heading'>{item.title}</p>
</div>
))}
</div>
</div>
)
}
export default Type11Mini

View file

@ -1,105 +0,0 @@
import React, { useEffect, useRef } from "react";
import MiniTypeWriter from "./MiniTypeWriter";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import mermaid from "mermaid";
interface Type12MiniProps {
title: string;
description: string;
mermaidCode: string;
slideIndex: number;
isFullSizeGraph: boolean;
}
const Type12Mini = ({
title,
description,
mermaidCode,
slideIndex,
isFullSizeGraph,
}: Type12MiniProps) => {
const { currentColors, currentTheme } = useSelector(
(state: RootState) => state.theme
);
const mermaidRef = useRef<HTMLDivElement>(null);
const hasInitialized = useRef<boolean>(false);
// Initialize Mermaid once
useEffect(() => {
if (!hasInitialized.current) {
mermaid.initialize({
startOnLoad: false,
theme: "base",
themeVariables: {
primaryColor: currentColors.slideBox,
primaryTextColor: currentColors.slideTitle,
primaryBorderColor: currentColors.slideBox,
lineColor: currentColors.chartColors[0],
secondaryColor: currentColors.slideHeading,
fontFamily: currentColors.fontFamily || "Inter",
background: currentColors.background || "#ffffff",
},
});
hasInitialized.current = true;
}
}, [currentColors]);
// Render the diagram on code/theme change
useEffect(() => {
if (typeof window !== "undefined" && mermaidCode && mermaidRef.current) {
const uniqueId = `mermaid-${slideIndex}-mini`;
mermaid
.render(uniqueId, mermaidCode)
.then(({ svg }) => {
if (mermaidRef.current) {
mermaidRef.current.innerHTML = svg;
// Optional: apply inline styling after render
const svgEl = mermaidRef.current.querySelector("svg");
if (svgEl) {
svgEl.style.width = "90px";
svgEl.style.maxWidth = "100%";
svgEl.style.background = currentColors.background || "#ffffff";
svgEl.style.color = currentColors.slideTitle || "#000000";
svgEl.style.fontFamily = currentColors.fontFamily || "Inter";
}
}
})
.catch((err) => {
console.error("Mermaid render error:", err);
});
}
}, [mermaidCode, slideIndex, currentColors]);
return (
<div className="slide-container w-full aspect-video bg-white p-2 flex flex-col justify-center rounded-lg text-[6px] border shadow-xl">
<div className="text-center mb-2">
<div className="font-semibold text-[10px] text-center slide-title truncate">
<MiniTypeWriter text={title} />
</div>
</div>
<div
className={`flex gap-2 w-full items-center ${
isFullSizeGraph ? " flex-col " : ""
} `}
>
<div className={` w-[80%]`} ref={mermaidRef}>
{/* <MiniCharts chartData={chartData} /> */}
</div>
{/* <div className="w-full h-full">
</div> */}
<div className="w-full text-gray-600 text-[8px] line-clamp-6 slide-description">
<MiniTypeWriter text={description} />
</div>
</div>
{/* <div className="grid grid-cols-2 gap-2">
<div className="text-gray-600 text-[8px] line-clamp-6 slide-description">{description}</div>
</div> */}
</div>
);
};
export default Type12Mini;

View file

@ -30,12 +30,10 @@ import {
XAxis,
} from "recharts";
import { ResponsiveContainer } from "recharts";
import Type10Layout from "./slide_layouts/Type10Layout";
import Type10Mini from "./mini-slides/Type10Mini";
import { ThemeColors } from "../store/themeSlice";
import { isDarkColor } from "../utils/others";
import Type12Layout from "./slide_layouts/Type12Layout";
import Type12Mini from "./mini-slides/Type12Mini";
import {
formatTooltipValue,
formatYAxisTick,
@ -149,29 +147,7 @@ export const renderSlideContent = (slide: Slide, language: string) => {
graphData={slide.content.graph}
/>
);
case 10:
case 11:
return (
<Type10Layout
design_index={slide.design_index || 1}
description={slide.content.description || ""}
slideIndex={slide.index}
slideId={slide.id || ""}
title={slide.content.title}
infographics={slide.content.infographics}
/>
);
case 12:
return (
<Type12Layout
slideIndex={slide.index}
slideId={slide.id || ""}
title={slide.content.title}
mermaidCode={slide.content.diagram}
description={slide.content.description || ""}
isFullSizeGraph={false}
/>
);
default:
return null;
@ -258,27 +234,7 @@ export const renderMiniSlideContent = (slide: Slide) => {
slideIndex={slide.index}
/>
);
case 10:
case 11:
return (
<Type10Mini
design_index={slide.design_index || 1}
slideIndex={slide.index}
description={slide.content.description || ""}
title={slide.content.title}
infographics={slide.content.infographics}
/>
);
case 12:
return (
<Type12Mini
title={slide.content.title}
description={slide.content.description || ""}
isFullSizeGraph={false}
mermaidCode={slide.content.diagram || ""}
slideIndex={slide.index}
/>
);
default:
return null;
}
@ -433,14 +389,14 @@ export const renderChart = (
dataKey={serie.name || `Series ${index + 1}`}
stroke={chartColors[index % chartColors.length]}
style={{ cursor: "pointer" }}
// label={(chartSettings?.showDataLabel && localChartData.data.series.length === 1) ? {
// position: chartSettings?.dataLabel.dataLabelPosition === "Outside" ? "top" : "center",
// formatter: (value: number) => formatYAxisTick(value),
// fill: chartSettings?.dataLabel.dataLabelPosition === "Outside" ? theme.slideTitle : '#ffffff',
// fontWeight: 'bold',
// fontSize: '12px',
// fontFamily: theme.fontFamily
// } : undefined}
// label={(chartSettings?.showDataLabel && localChartData.data.series.length === 1) ? {
// position: chartSettings?.dataLabel.dataLabelPosition === "Outside" ? "top" : "center",
// formatter: (value: number) => formatYAxisTick(value),
// fill: chartSettings?.dataLabel.dataLabelPosition === "Outside" ? theme.slideTitle : '#ffffff',
// fontWeight: 'bold',
// fontSize: '12px',
// fontFamily: theme.fontFamily
// } : undefined}
/>
))}
</LineChart>
@ -602,27 +558,27 @@ export const renderChart = (
label={
chartSettings?.showDataLabel
? {
position:
chartSettings?.dataLabel.dataLabelPosition ===
position:
chartSettings?.dataLabel.dataLabelPosition ===
"Outside"
? "top"
: chartSettings?.dataLabel.dataLabelAlignment ===
"Base"
? "top"
: chartSettings?.dataLabel.dataLabelAlignment ===
"Base"
? "insideBottom"
: chartSettings?.dataLabel.dataLabelAlignment ===
"Center"
? "center"
: "insideTop",
formatter: (value: number) => formatYAxisTick(value),
fill:
chartSettings?.dataLabel.dataLabelPosition ===
? "center"
: "insideTop",
formatter: (value: number) => formatYAxisTick(value),
fill:
chartSettings?.dataLabel.dataLabelPosition ===
"Outside"
? theme.slideTitle
: "#ffffff",
fontWeight: "bold",
fontSize: "14px",
fontFamily: theme.fontFamily,
}
? theme.slideTitle
: "#ffffff",
fontWeight: "bold",
fontSize: "14px",
fontFamily: theme.fontFamily,
}
: undefined
}
/>

View file

@ -1,277 +0,0 @@
import React from "react";
import EditableText from "../EditableText";
import ElementMenu from "../ElementMenu";
import { MoreVertical, Plus } from "lucide-react";
import { useDispatch, useSelector } from "react-redux";
import {
addInfographics,
deleteInfographics,
updateSlideVariant,
} from "@/store/slices/presentationGeneration";
import AllInfoGraphics from "../info_graphics/AllInfoGraphics";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@radix-ui/react-popover";
import { RootState } from "@/store/store";
import SlideFooter from "./SlideFooter";
type Type10LayoutProps = {
title: string;
slideIndex: number;
slideId: string | null;
description: string;
design_index: number;
infographics: {
title: string;
description: string;
chart: {
chart_type: string;
value: {
number_type: string;
numerical: number;
suffix: string;
numerator?: number;
denominator?: number;
percentage?: number;
};
};
}[];
};
const Type10Layout = ({
title,
slideIndex,
slideId,
infographics,
description,
design_index,
}: Type10LayoutProps) => {
const { currentColors } = useSelector((state: RootState) => state.theme);
const dispatch = useDispatch();
const handleDeleteItem = (index: number) => {
dispatch(
deleteInfographics({
slideIndex: slideIndex,
itemIdx: index,
})
);
};
const handleAddItem = () => {
dispatch(
addInfographics({
slideIndex: slideIndex,
item: {
title: "Enter Title",
description: "Enter Description",
chart: {
chart_type: infographics[0].chart.chart_type,
value: {
percentage: 80,
number_type: "percentage",
},
},
},
})
);
};
const handleVariantChange = (newVariant: number) => {
dispatch(updateSlideVariant({ index: slideIndex, variant: newVariant }));
};
const VariantMenu = () => (
<Popover>
<PopoverTrigger asChild>
<button className="absolute hidden lg:block top-0 -left-7 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50">
<MoreVertical className="w-4 h-4 text-black" />
</button>
</PopoverTrigger>
<PopoverContent align="start" className="w-[180px] z-50 p-2 bg-white">
<button
onClick={() => handleVariantChange(1)}
className={`w-full text-base font-medium py-2 hover:bg-gray-200 transition-colors rounded-md bg-transparent ${
design_index === 1 ? "bg-gray-200" : ""
}`}
>
With Background
</button>
<button
onClick={() => handleVariantChange(2)}
className="w-full text-base font-medium py-2 hover:bg-gray-200 transition-colors rounded-md bg-transparent"
>
Without Background
</button>
</PopoverContent>
</Popover>
);
if (infographics.length === 1) {
return (
<div
className="slide-container shadow-lg border rounded-sm w-full max-w-[1280px] px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] font-inter flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative "
key={infographics[0].chart.chart_type}
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-slide-type="10"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
>
<div className="grid grid-cols-2 gap-4 w-full">
<div className=" flex flex-col w-full items-start justify-center space-y-6">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
/>
{description && (
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description`}
type="description"
content={description}
/>
)}
</div>
<div className="group relative">
<div className="absolute -inset-[2px] border-2 border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
{infographics.length < 3 && (
<button
onClick={handleAddItem}
className="absolute top-1/2 -right-4 -translate-y-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
)}
<AllInfoGraphics
key={infographics[0].chart.chart_type}
slideIndex={slideIndex}
itemIndex={0}
chart={infographics[0].chart}
/>
<div className="mt-6 text-center w-full">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description-body`}
isAlingCenter={true}
type="info-description"
bodyIdx={0}
content={infographics[0].description}
/>
</div>
</div>
</div>
<SlideFooter />
</div>
);
} else {
return (
<div
className="slide-container px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] shadow-lg rounded-sm w-full max-w-[1280px] font-inter flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative"
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-slide-type="10"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<div className="text-center space-y-2 lg:space-y-4 mb-3 sm:mb-8 lg:mb-12 w-full">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
isAlingCenter={true}
content={title}
/>
{description && (
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description`}
type="description"
isAlingCenter={true}
content={description}
/>
)}
</div>
<div
// style={{
// gridTemplateColumns: `repeat(${infographics.length}, 1fr)`,
// }}
className={`grid w-full justify-between gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-${infographics.length} lg:gap-6 relative group"`}
>
<VariantMenu />
{/* hover border and icon */}
<div className="absolute -inset-[2px] border-2 hidden lg:block border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
{infographics.length < 3 && (
<button
onClick={handleAddItem}
className="absolute top-1/2 -right-4 -translate-y-1/2 p-1 rounded-md bg-white shadow-md opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50"
>
<Plus className="w-4 h-4 text-gray-600" />
</button>
)}
{infographics.map((item: any, index: number) => {
return (
<div
data-slide-element={design_index === 1 ? true : false}
data-slide-index={slideIndex}
data-element-type={design_index === 1 ? "slide-box" : ""}
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow:
design_index === 1
? "0 2px 10px 0 rgba(43, 43, 43, 0.2)"
: "",
}}
key={index}
className={`text-center w-full relative p-2 lg:px-6 lg:py-8 ${
design_index === 1 ? "slide-box rounded-lg " : ""
}`}
>
<ElementMenu
index={index}
handleDeleteItem={handleDeleteItem}
/>
<AllInfoGraphics
key={item.chart.chart_type}
slideIndex={slideIndex}
itemIndex={index}
chart={item.chart}
/>
<div className="text-center max-w-md mx-auto lg:space-y-2">
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-heading-${index}`}
type="info-heading"
isAlingCenter={true}
content={item.title}
/>
<EditableText
slideIndex={slideIndex}
bodyIdx={index}
elementId={`slide-${slideIndex}-description-${index}`}
type="info-description"
isAlingCenter={true}
content={item.description}
/>
</div>
</div>
);
})}
</div>
<SlideFooter />
</div>
);
}
};
export default Type10Layout;

View file

@ -1,176 +0,0 @@
import React, { useState } from 'react'
import EditableText from '../EditableText'
import { Plus } from 'lucide-react';
import ElementMenu from '../ElementMenu';
import { addInfographics, deleteInfographics, updateInfographicsChart } from '@/store/slices/presentationGeneration';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '@/store/store';
type Type11LayoutProps = {
title: string;
slideIndex: number;
slideId: string;
description?: string;
infographics: {
title: string;
description: string;
chart: {
chart_type: string;
value: {
number_type: string;
numerical: number;
suffix: string;
}
}
}[];
}
const Type11Layout = ({
title,
slideIndex,
slideId,
description,
infographics
}: Type11LayoutProps) => {
const dispatch = useDispatch();
const { currentColors } = useSelector((state: RootState) => state.theme);
// const percentageColors = ['#ff00ef', '#6453ff', '#f00000']
const handleDeleteItem = (index: number) => {
dispatch(deleteInfographics({
slideIndex: slideIndex,
itemIdx: index
}))
}
const handleAddItem = () => {
dispatch(addInfographics({
slideIndex: slideIndex,
item: {
title: 'Enter Title',
description: 'Enter Description',
chart: {
chart_type: infographics[0].chart.chart_type,
value: {
percentage: 80,
number_type: 'percentage',
}
}
}
}))
}
const updateChart = ({ slideIndex, itemIdx, chart }: { slideIndex: number, itemIdx: number, chart: any }) => {
dispatch(updateInfographicsChart({ slideIndex, itemIdx, chart }))
}
return (
<div className='slide-container px-20 shadow-lg border rounded-sm w-full max-w-[1280px] font-inter py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white'
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-slide-type="11"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || 'Inter, sans-serif'
}}
>
<div className={`text-center space-y-4 ${description ? 'mb-8' : 'mb-12'} w-full`}>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
isAlingCenter={true}
content={title}
/>
{description && <EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description`}
type="description"
isAlingCenter={true}
content={description}
/>}
</div>
<div
style={{
gridTemplateColumns: `repeat(${infographics.length}, 1fr)`
}}
className={`grid grid-cols-${infographics.length} gap-8 w-full relative group `}>
<div className="absolute -inset-[2px] border-2 border-blue-500 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
<button onClick={handleAddItem} className="absolute top-1/2 -right-4 -translate-y-1/2 p-1 rounded-md bg-white shadow-sm opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-50 z-50">
<Plus className="w-4 h-4 text-gray-600" />
</button>
{infographics.map((item, index) => (
<div
data-slide-element
data-slide-index={slideIndex}
data-element-type="slide-box"
data-element-id={`slide-${slideIndex}-item-${index}-box`}
style={{
boxShadow: '0 2px 10px 0 rgba(43, 43, 43, 0.2)'
}}
key={index} className={`bg-white w-full rounded-lg py-4 px-4 slide-box flex flex-col items-center relative ${infographics.length === 1 ? 'max-w-[420px] mx-auto' : ''}`}>
<ElementMenu index={index} handleDeleteItem={handleDeleteItem} />
<div data-slide-element
data-slide-index={slideIndex}
data-element-type="filledbox"
data-element-id={`slide-${slideIndex}-item-${index}-box`} style={{ backgroundColor: currentColors.iconBg }} className="w-40 h-40 rounded-full flex items-center justify-center mb-6">
<div className="text-center text-white">
<p onBlur={(e) => {
updateChart({
slideIndex, itemIdx: index, chart: {
chart_type: item.chart.chart_type,
value: {
number_type: item.chart.value.number_type,
numerical: parseInt(e.currentTarget.innerText),
suffix: item.chart.value.suffix
}
}
})
}} contentEditable suppressContentEditableWarning data-slide-element data-slide-index={slideIndex} data-element-type="text" data-is-align={true} data-element-id={`slide-${slideIndex}-value-${index}`} className='text-[24px] focus-visible:outline-none leading-[40px] font-bold'>{item.chart.value.numerical}</p>
<p onBlur={(e) => {
updateChart({
slideIndex, itemIdx: index, chart: {
chart_type: item.chart.chart_type,
value: {
number_type: item.chart.value.number_type,
numerical: item.chart.value.numerical,
suffix: e.currentTarget.innerText
}
}
})
}} contentEditable suppressContentEditableWarning data-slide-element data-slide-index={slideIndex} data-element-type="text" data-is-align={true} data-element-id={`slide-${slideIndex}-subtitle-${index}`} className='text-[20px] focus-visible:outline-none leading-[24px] font-bold'>{item.chart.value.suffix.toString().replace(/\*\*/g, '')}</p>
</div>
</div>
<div className="text-center space-y-4">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-heading-${index}`}
type="info-heading"
bodyIdx={index}
isAlingCenter={true}
content={item.title}
/>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description-${index}`}
type="info-description"
bodyIdx={index}
isAlingCenter={true}
content={item.description}
/>
</div>
</div>
))}
</div>
</div>
)
}
export default Type11Layout

View file

@ -1,126 +0,0 @@
import React, { useEffect, useRef } from "react";
import EditableText from "../EditableText";
import { useSelector } from "react-redux";
import SlideFooter from "./SlideFooter";
import { RootState } from "@/store/store";
import mermaid from "mermaid";
const Type12Layout = ({
title,
description,
slideId,
mermaidCode,
slideIndex,
isFullSizeGraph,
}: {
title: string;
description?: string;
slideId: string | null;
mermaidCode: string;
slideIndex: number;
isFullSizeGraph: boolean;
}) => {
const { currentColors, currentTheme } = useSelector(
(state: RootState) => state.theme
);
const mermaidRef = useRef<HTMLDivElement>(null);
const hasInitialized = useRef<boolean>(false);
// Initialize Mermaid once
useEffect(() => {
// if (!hasInitialized.current) {
mermaid.initialize({
startOnLoad: false,
theme: "base",
themeVariables: {
primaryColor: currentColors.slideBox,
primaryTextColor: currentColors.slideTitle,
primaryBorderColor: currentColors.slideBox,
lineColor: currentColors.chartColors[0],
secondaryColor: currentColors.slideHeading,
fontFamily: currentColors.fontFamily || "Inter",
background: currentColors.slideBg || "#ffffff",
},
});
hasInitialized.current = true;
// }
}, [currentColors]);
// Render the diagram on code/theme change
useEffect(() => {
if (typeof window !== "undefined" && mermaidCode && mermaidRef.current) {
const uniqueId = `mermaid-${slideIndex}`;
mermaid
.render(uniqueId, mermaidCode)
.then(({ svg }) => {
if (mermaidRef.current) {
mermaidRef.current.innerHTML = svg;
// Optional: apply inline styling after render
const svgEl = mermaidRef.current.querySelector("svg");
if (svgEl) {
svgEl.style.width = "600px";
svgEl.style.maxWidth = "100%";
svgEl.style.background = currentColors.slideBox || "#ffffff";
svgEl.style.color = currentColors.slideTitle || "#000000";
svgEl.style.fontFamily = currentColors.fontFamily || "Inter";
}
}
})
.catch((err) => {
console.error("Mermaid render error:", err);
});
}
}, [mermaidCode, slideIndex, currentColors]);
return (
<div
className="slide-container px-20 font-inter rounded-sm w-full max-w-[1280px] shadow-lg py-[86px] max-h-[720px] flex flex-col items-center justify-center aspect-video bg-white relative"
data-slide-element
data-slide-index={slideIndex}
data-slide-id={slideId}
data-slide-type="5"
data-element-type="slide-container"
data-element-id={`slide-${slideIndex}-container`}
style={{
fontFamily: currentColors.fontFamily || "Inter, sans-serif",
}}
>
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-title`}
type="title"
content={title}
isAlingCenter={false}
/>
<div
className={`flex w-full items-center mt-6 ${
isFullSizeGraph ? "flex-col gap-10" : " gap-16"
}`}
>
<div
data-slide-element
data-element-type="graph"
data-graph-type="graph"
data-element-id={`slide-group-${slideIndex}-graph`}
className=" w-[80%] "
ref={mermaidRef}
/>
<div className="w-full text-center">
<EditableText
slideIndex={slideIndex}
elementId={`slide-${slideIndex}-description-body`}
type="description-body"
isAlingCenter={isFullSizeGraph}
content={description || ""}
/>
</div>
</div>
<SlideFooter />
</div>
);
};
export default Type12Layout;

View file

@ -27,6 +27,7 @@ import { jsonrepair } from "jsonrepair";
import { Button } from "@/components/ui/button";
import { AlertCircle } from "lucide-react";
import Help from "./Help";
import { BASE_URL } from "@/utils/constant";
// Custom debounce function
function useDebounce<T extends (...args: any[]) => void>(
@ -128,7 +129,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
dispatch(setStreaming(true));
evtSource = new EventSource(
`${PresentationGenerationApi.BASE_URL}/ppt/generate/stream?presentation_id=${presentation_id}&session=${session}`
`${BASE_URL}/ppt/generate/stream?presentation_id=${presentation_id}&session=${session}`
);
evtSource.onopen = () => {

View file

@ -1,16 +1,16 @@
import { BASE_URL } from "@/utils/constant";
import { getHeader, getHeaderForFormData } from "./header";
import { IconSearch, ImageGenerate, ImageSearch } from "./params";
export class PresentationGenerationApi {
// static BASE_URL="https://api.presenton.ai";
// static BASE_URL="https://presentation-generator-fragrant-mountain-1643.fly.dev";
static BASE_URL = process.env.NEXT_PUBLIC_FAST_API || 'http://localhost:8000';
// static BASE_URL = "http://localhost:48388";
static async getChapterDetails() {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/chapter-details`,
`${BASE_URL}/ppt/chapter-details`,
{
method: "GET",
headers: getHeader(),
@ -40,7 +40,7 @@ export class PresentationGenerationApi {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/files/upload`,
`${BASE_URL}/ppt/files/upload`,
{
method: "POST",
headers: getHeaderForFormData(),
@ -69,7 +69,7 @@ export class PresentationGenerationApi {
};
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/report/generate`,
`${BASE_URL}/ppt/report/generate`,
{
method: "POST",
headers: getHeader(),
@ -94,7 +94,7 @@ export class PresentationGenerationApi {
static async decomposeDocuments(documentKeys: string[], imageKeys: string[]) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/files/decompose`,
`${BASE_URL}/ppt/files/decompose`,
{
method: "POST",
headers: getHeader(),
@ -124,7 +124,7 @@ export class PresentationGenerationApi {
}) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/titles/generate`,
`${BASE_URL}/ppt/titles/generate`,
{
method: "POST",
headers: getHeader(),
@ -151,7 +151,7 @@ export class PresentationGenerationApi {
static async generatePresentation(presentationData: any) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/generate`,
`${BASE_URL}/ppt/generate`,
{
method: "POST",
headers: getHeader(),
@ -180,7 +180,7 @@ export class PresentationGenerationApi {
) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/edit`,
`${BASE_URL}/ppt/edit`,
{
method: "POST",
headers: getHeader(),
@ -209,7 +209,7 @@ export class PresentationGenerationApi {
static async updatePresentationContent(body: any) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/slides/update`,
`${BASE_URL}/ppt/slides/update`,
{
method: "POST",
headers: getHeader(),
@ -235,7 +235,7 @@ export class PresentationGenerationApi {
static async generateData(presentationData: any) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/generate/data`,
`${BASE_URL}/ppt/generate/data`,
{
method: "POST",
headers: getHeader(),
@ -259,7 +259,7 @@ export class PresentationGenerationApi {
static async imageSearch(imageSearch: ImageSearch) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/image/search`,
`${BASE_URL}/ppt/image/search`,
{
method: "POST",
headers: getHeader(),
@ -281,7 +281,7 @@ export class PresentationGenerationApi {
static async generateImage(imageGenerate: ImageGenerate) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/image/generate`,
`${BASE_URL}/ppt/image/generate`,
{
method: "POST",
headers: getHeader(),
@ -304,7 +304,7 @@ export class PresentationGenerationApi {
static async searchIcons(iconSearch: IconSearch) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/icon/search`,
`${BASE_URL}/ppt/icon/search`,
{
method: "POST",
headers: getHeader(),
@ -328,7 +328,7 @@ export class PresentationGenerationApi {
static async updateDocuments(body: any) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/document/update`,
`${BASE_URL}/ppt/document/update`,
{
method: "POST",
headers: getHeaderForFormData(),
@ -352,7 +352,7 @@ export class PresentationGenerationApi {
static async exportAsPPTX(presentationData: any) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/presentation/export_as_pptx/`,
`${BASE_URL}/ppt/presentation/export_as_pptx/`,
{
method: "POST",
headers: getHeader(),
@ -365,7 +365,7 @@ export class PresentationGenerationApi {
return {
...data,
url: `${PresentationGenerationApi.BASE_URL}${data.url}`,
url: `${BASE_URL}${data.url}`,
};
} else {
throw new Error(`Failed to export as pptx: ${response.statusText}`);
@ -378,7 +378,7 @@ export class PresentationGenerationApi {
static async exportAsPDF(presentationData: any) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/presentation/export_as_pdf/`,
`${BASE_URL}/ppt/presentation/export_as_pdf/`,
{
method: "POST",
headers: getHeader(),
@ -400,7 +400,7 @@ export class PresentationGenerationApi {
static async deleteSlide(presentation_id: string, slide_id: string) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/slide/delete?presentation_id=${presentation_id}&slide_id=${slide_id}`,
`${BASE_URL}/ppt/slide/delete?presentation_id=${presentation_id}&slide_id=${slide_id}`,
{
method: "DELETE",
headers: getHeader(),
@ -421,7 +421,7 @@ export class PresentationGenerationApi {
static async setThemeColors(presentation_id: string, theme: any) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/presentation/theme`,
`${BASE_URL}/ppt/presentation/theme`,
{
method: "POST",
headers: getHeader(),
@ -464,7 +464,7 @@ export class PresentationGenerationApi {
}) {
try {
const response = await fetch(
`${PresentationGenerationApi.BASE_URL}/ppt/create`,
`${BASE_URL}/ppt/create`,
{
method: "POST",
headers: getHeader(),

View file

@ -1,78 +0,0 @@
import BabyIcon from "@/components/icons/Baby";
import PersonIcon from "@/components/icons/Person";
import HandIcon from "@/components/icons/Hand";
import TreeIcon from "@/components/icons/Tree";
import StarIcon from "@/components/icons/Star";
import CornIcon from "@/components/icons/Corn";
import MealIcon from "@/components/icons/Meal";
import DrinkBottleIcon from "@/components/icons/DrinkBottle";
import CupIcon from "@/components/icons/Cup";
import DropletIcon from "@/components/icons/Droplet";
import HouseIcon from "@/components/icons/House";
import BuildingIcon from "@/components/icons/Building";
import TentIcon from "@/components/icons/Tent";
import CarIcon from "@/components/icons/Car";
import BicycleIcon from "@/components/icons/Bicycle";
import ClockIcon from "@/components/icons/Clock";
import BanknoteIcon from "@/components/icons/Banknote";
import BriefcaseIcon from "@/components/icons/Briefcase";
import TruckIcon from "@/components/icons/Truck";
import AirplaneIcon from "@/components/icons/Airplane";
import LaptopIcon from "@/components/icons/Laptop";
import MobilePhoneIcon from "@/components/icons/MobilePhone";
import LightBulbIcon from "@/components/icons/LightBulb";
import SpannerIcon from "@/components/icons/Spanner";
import FireIcon from "@/components/icons/Fire";
import MortarboardIcon from "@/components/icons/Mortarboard";
import BookIcon from "@/components/icons/Book";
import SyringeIcon from "@/components/icons/Syringe";
import FirstAidIcon from "@/components/icons/FirstAid";
import GlobeIcon from "@/components/icons/Globe";
// First, let's create a constant object with all the icon mappings
export const ICON_LIST = {
person: PersonIcon,
female_person: PersonIcon,
male_person: PersonIcon,
baby: BabyIcon,
hand: HandIcon,
tree: TreeIcon,
star: StarIcon,
corn: CornIcon,
meal: MealIcon,
drink_bottle: DrinkBottleIcon,
cup: CupIcon,
droplet: DropletIcon,
house: HouseIcon,
building: BuildingIcon,
tent: TentIcon,
car: CarIcon,
bicycle: BicycleIcon,
clock: ClockIcon,
banknote: BanknoteIcon,
briefcase: BriefcaseIcon,
truck: TruckIcon,
airplane: AirplaneIcon,
laptop_computer: LaptopIcon,
mobile_phone: MobilePhoneIcon,
light_bulb: LightBulbIcon,
spanner: SpannerIcon,
fire: FireIcon,
mortarboard: MortarboardIcon,
book: BookIcon,
syringe: SyringeIcon,
first_aid: FirstAidIcon,
globe: GlobeIcon,
} as const;
// Then modify IconMapper to use this list
export const IconMapper = (isMini = false, icon: string) => {
const IconComponent = ICON_LIST[icon as keyof typeof ICON_LIST];
if (!IconComponent) return null;
return <IconComponent className={isMini ? "w-4 h-4" : "w-12 h-12"} />;
};
export const getPercentage = (numerator: number, denominator: number) => {
if (denominator === 0) return 0;
return Math.round((numerator / denominator) * 100);
};

View file

@ -143,241 +143,7 @@ export const getEmptySlideContent = (
],
},
};
case 10:
return {
...baseSlide,
type: 10,
// @ts-ignore
content: {
title: "New Title",
infographics: [
{
title: "Enter Heading",
chart: {
chart_type: "progress-ring",
value: {
number_type: "fraction",
numerator: 4,
denominator: 5,
},
},
description: "Enter Description",
},
{
title: "Enter Heading",
chart: {
chart_type: "progress-ring",
value: {
number_type: "percentage",
percentage: 40,
},
},
description: "Enter Description",
},
],
},
};
case 11:
return {
...baseSlide,
type: 10,
// @ts-ignore
content: {
title: "New Title",
infographics: [
{
title: "Enter Heading",
chart: {
chart_type: "text",
value: {
number_type: "numerical",
numerical: "50",
suffix: "quids",
},
},
description: "Enter Description",
},
{
title: "Enter Heading",
chart: {
chart_type: "text",
value: {
number_type: "numerical",
numerical: "23.4",
suffix: "pence",
},
},
description: "Enter Description",
},
],
},
};
case 12:
return {
...baseSlide,
type: 10,
// @ts-ignore
content: {
title: "New Title",
description: "Enter Description",
infographics: [
{
title: "Enter Heading",
chart: {
chart_type: "icon-infographic",
icon: "hand",
value: {
number_type: "percentage",
percentage: 75,
},
},
description: "Enter Description",
},
],
},
};
case 13:
return {
...baseSlide,
type: 10,
// @ts-ignore
content: {
title: "New Title",
description: "Enter Description",
infographics: [
{
title: "Enter Heading",
chart: {
chart_type: "icon-infographic",
icon: "hand",
value: {
number_type: "percentage",
percentage: 75,
},
},
description: "Enter Description",
},
{
title: "Enter Heading",
chart: {
chart_type: "icon-infographic",
icon: "baby",
value: {
number_type: "percentage",
percentage: 75,
},
},
description: "Enter Description",
},
],
},
};
case 14:
return {
...baseSlide,
type: 10,
// @ts-ignore
content: {
title: "New Title",
description: "Enter Description",
infographics: [
{
title: "Enter Heading",
chart: {
chart_type: "progress-ring",
value: {
number_type: "percentage",
percentage: 40,
},
},
description: "Enter Description",
},
{
title: "Enter Heading",
chart: {
chart_type: "progress-ring",
value: {
number_type: "fraction",
numerator: 4,
denominator: 5,
},
},
description: "Enter Description",
},
],
},
};
case 15:
return {
...baseSlide,
type: 10,
// @ts-ignore
content: {
title: "New Title",
description: "Enter Description",
infographics: [
{
title: "Enter Heading",
chart: {
chart_type: "progress-dial",
value: {
number_type: "fraction",
numerator: 2,
denominator: 3,
},
},
description: "Enter Description",
},
{
title: "Enter Heading",
chart: {
chart_type: "progress-dial",
value: {
number_type: "percentage",
percentage: 40,
},
},
description: "Enter Description",
},
],
},
};
case 16:
return {
...baseSlide,
type: 10,
// @ts-ignore
content: {
title: "New Title",
description: "Enter Description",
infographics: [
{
title: "Enter Heading",
chart: {
chart_type: "progress-bar",
value: {
number_type: "percentage",
percentage: 40,
},
},
description: "Enter Description",
},
{
title: "Enter Heading",
chart: {
chart_type: "progress-bar",
value: {
number_type: "fraction",
numerator: 4,
denominator: 5,
},
},
description: "Enter Description",
},
],
},
};
default:
return baseSlide;
}

View file

@ -1,340 +0,0 @@
interface ElementPosition {
left: number;
top: number;
width: number;
height: number;
}
interface FontStyles {
name: string;
size: number;
weight: string;
color: string;
}
interface TextMetadata {
position: ElementPosition;
paragraphs: Array<{
text: string;
font: FontStyles;
}>;
}
interface PictureMetadata {
position: ElementPosition;
picture:{
is_network: boolean;
path: string;
}
border_radius: number;
}
interface GraphMetadata {
position: ElementPosition;
categoryFont: FontStyles;
valueFont: FontStyles;
legendFont: FontStyles;
graphData: {
type: string;
data: any; // Replace with your specific graph data structure
};
}
interface FilledBoxMetadata {
position: ElementPosition;
type: 1 | 5 | 9; // 1 for rectangle, 2 for circle
fill: {
color:string;
};
stroke:{
color:string;
thickness:number;
},
shadow:{
radius:number;
color:string;
offset:number;
opacity:number;
angle:number;
},
}
interface LineMetadata {
position: ElementPosition;
lineType: 1;
thickness: string;
color: string;
}
interface SlideBoxMetadata {
position: ElementPosition;
}
type ElementMetadata = TextMetadata | PictureMetadata | GraphMetadata | FilledBoxMetadata | LineMetadata | SlideBoxMetadata;
interface SlideMetadata {
slideIndex: number;
backgroundColor:string;
elements: ElementMetadata[];
}
const FIXED_SLIDE_WIDTH = 1280; // Standard slide width
const FIXED_SLIDE_HEIGHT = 720; // Standard slide height
// Add this helper function before collectSlideMetadata
const rgbToHex = (color: string): string => {
// Handle empty or invalid colors
if (!color || color === 'transparent' || color === 'none') return '#000000';
// If already hex, return as is
if (color.startsWith('#')) return color;
// Extract RGB/RGBA values
const matches = color.match(/\d+/g);
if (!matches) return '#000000';
// Convert to hex
const r = parseInt(matches[0]);
const g = parseInt(matches[1]);
const b = parseInt(matches[2]);
return [r, g, b]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
};
export const collectSlideMetadata = (): SlideMetadata[] => {
const slidesMetadata: SlideMetadata[] = [];
const slideContainers = document.querySelectorAll('[data-element-type="slide-container"]');
slideContainers.forEach((container) => {
const containerEl = container as HTMLElement;
const containerRect = containerEl.getBoundingClientRect();
const slideIndex = parseInt(containerEl.getAttribute('data-slide-index') || '0');
// Get container computed styles
const containerComputedStyle = window.getComputedStyle(containerEl);
const slideMetadata: SlideMetadata = {
slideIndex,
backgroundColor: rgbToHex(containerComputedStyle.backgroundColor),
elements: []
};
const elements = containerEl.querySelectorAll('[data-slide-element]:not([data-element-type="slide-container"])');
elements.forEach((element) => {
const el = element as HTMLElement;
const elementRect = el.getBoundingClientRect();
const computedStyle = window.getComputedStyle(el);
// Calculate position relative to slide container
const position: ElementPosition = {
left: Math.round(elementRect.left - containerRect.left),
top: Math.round(elementRect.top - containerRect.top),
width: Math.round(elementRect.width),
height: Math.round(elementRect.height)
};
const elementType = el.getAttribute('data-element-type');
if (!elementType) return;
// Get computed font styles after Tailwind has been applied
const fontStyles: FontStyles = {
// name: computedStyle.fontFamily.replace(/['"]/g, ''),
name:"Inter",
size: parseInt(computedStyle.fontSize),
weight: computedStyle.fontWeight,
color: rgbToHex(computedStyle.color)
};
switch (elementType) {
case 'text':
slideMetadata.elements.push({
position,
paragraphs: [{
text: el.textContent || '',
font: fontStyles
}]
});
break;
case 'picture':
// Handle both img elements and elements containing img
let imgEl: HTMLImageElement | null;
if (el.tagName.toLowerCase() === 'img') {
imgEl = el as HTMLImageElement;
} else {
imgEl = el.querySelector('img');
}
if (imgEl) {
slideMetadata.elements.push({
position,
picture:{
is_network: imgEl.src.startsWith('http'),
path: imgEl.src || imgEl.getAttribute('data-image-path') || '',
},
border_radius: parseInt(computedStyle.borderRadius)
});
}
break;
case 'graph':
slideMetadata.elements.push({
position,
categoryFont: {
name: computedStyle.fontFamily.replace(/['"]/g, ''),
size: parseInt(computedStyle.fontSize) ,
weight: computedStyle.fontWeight,
color: computedStyle.color
},
valueFont: fontStyles,
legendFont: fontStyles,
graphData: {
type: el.getAttribute('data-graph-type') || 'bar',
data: JSON.parse(el.getAttribute('data-graph-data') || '{}')
}
});
break;
case 'filledbox':{
const boxShadow = computedStyle.boxShadow;
// Default shadow properties
let shadowRadius = 0;
let shadowColor = '#000000';
let shadowOffsetX = 0;
let shadowOffsetY = 0;
let shadowOpacity = 0;
if (boxShadow && boxShadow !== 'none') {
// Regex to parse box-shadow values: "offset-x offset-y blur-radius spread-radius color"
const boxShadowRegex = /rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([\d.]+)?\)?\s+(-?\d+px)\s+(-?\d+px)\s+(-?\d+px)\s+(-?\d+px)|(-?\d+px)\s+(-?\d+px)\s+(-?\d+px)\s+(-?\d+px)\s+rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([\d.]+)?\)?/;
const match = boxShadow.match(boxShadowRegex);
if (!match) return null;
// Extract values based on format
shadowColor =rgbToHex( match[1]
? "rgb(" + match[1] + ", " + match[2] + ", " + match[3] + ")"
: "rgb(" + match[13] + ", " + match[14] + ", " + match[15] + ")");
shadowOpacity = parseInt(match[4] || match[16] || '1');
shadowOffsetX = parseInt(match[5] || match[9]);
shadowOffsetY = parseInt(match[6] || match[10]);
shadowRadius = parseInt(match[7] || match[11]);
}
slideMetadata.elements.push({
position,
type: computedStyle.borderRadius === '9999px' || computedStyle.borderRadius === '50%' ? 9 : 5,
fill: {
color: rgbToHex(computedStyle.backgroundColor),
},
border_radius: parseInt(computedStyle.borderRadius) || 0,
stroke: {
color: rgbToHex(computedStyle.borderColor),
thickness: parseInt(computedStyle.borderWidth) || 0,
},
shadow: {
radius: shadowRadius,
color: shadowColor,
offset: Math.sqrt(shadowOffsetX ** 2 + shadowOffsetY ** 2), // Total offset length
opacity: shadowOpacity,
angle: Math.round((Math.atan2(shadowOffsetY, shadowOffsetX) * 180) / Math.PI), // Shadow angle in degrees
},
});
break;
}
case 'line':
slideMetadata.elements.push({
position,
lineType: 1,
thickness: computedStyle.borderWidth || computedStyle.height,
color: rgbToHex(computedStyle.borderColor || computedStyle.backgroundColor)
});
break;
case 'slide-box': {
const boxShadow = computedStyle.boxShadow;
console.log('slide-box', boxShadow, 'slide-box');
// Default shadow properties
let shadowRadius = 0;
let shadowColor = '#000000';
let shadowOffsetX = 0;
let shadowOffsetY = 0;
let shadowOpacity = 0;
if (boxShadow && boxShadow !== 'none') {
// Regex to parse box-shadow values: "offset-x offset-y blur-radius spread-radius color"
const boxShadowRegex = /rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)\s*(-?\d+)px\s*(-?\d+)px\s*(-?\d+)px\s*(-?\d+)px/;
const match = boxShadow.match(boxShadowRegex);
if (match) {
const [_, r, g, b, opacity, xOffset, yOffset, blurRadius, spreadRadius] = match;
console.log('rgb',`rgb(${r},${g},${b})`);
shadowOpacity=parseInt(opacity);
shadowRadius=parseInt(blurRadius);
shadowColor= rgbToHex(`rgba(${r},${g},${b},1)`)
shadowOffsetX=parseInt(xOffset);
shadowOffsetY=parseInt(yOffset);
}
}
slideMetadata.elements.push({
position,
type: computedStyle.borderRadius === '9999px' || computedStyle.borderRadius === '50%' ? 9 : 5,
fill: {
color: rgbToHex(computedStyle.backgroundColor),
},
border_radius: parseInt(computedStyle.borderRadius) || 0,
stroke: {
color: rgbToHex(computedStyle.borderColor),
thickness: parseInt(computedStyle.borderWidth) || 0,
},
shadow: {
radius: shadowRadius,
color: shadowColor,
offset: Math.sqrt(shadowOffsetX ** 2 + shadowOffsetY ** 2), // Total offset length
opacity: shadowOpacity,
angle: Math.round((Math.atan2(shadowOffsetY, shadowOffsetX) * 180) / Math.PI), // Shadow angle in degrees
},
});
break;
}
}
});
slidesMetadata.push(slideMetadata);
});
return slidesMetadata;
};

View file

@ -2,6 +2,7 @@ import {
getHeader,
getHeaderForFormData,
} from "@/app/(presentation-generator)/services/api/header";
import { BASE_URL } from "@/utils/constant";
export interface PresentationResponse {
id: string;
@ -22,11 +23,11 @@ export interface PresentationResponse {
export class DashboardApi {
// static BASE_URL = "http://localhost:48388";
static BASE_URL = process.env.NEXT_PUBLIC_FAST_API || 'http://localhost:8000';
static async getPresentations(): Promise<PresentationResponse[]> {
try {
const response = await fetch(
`${DashboardApi.BASE_URL}/ppt/user_presentations`,
`${BASE_URL}/ppt/user_presentations`,
{
method: "GET",
headers: getHeader(),
@ -48,7 +49,7 @@ export class DashboardApi {
static async getPresentation(id: string) {
try {
const response = await fetch(
`${DashboardApi.BASE_URL}/ppt/presentation?presentation_id=${id}`,
`${BASE_URL}/ppt/presentation?presentation_id=${id}`,
{
method: "GET",
headers: getHeader(),
@ -67,7 +68,7 @@ export class DashboardApi {
static async deletePresentation(presentation_id: string) {
try {
const response = await fetch(
`${DashboardApi.BASE_URL}/ppt/delete?presentation_id=${presentation_id}`,
`${BASE_URL}/ppt/delete?presentation_id=${presentation_id}`,
{
method: "DELETE",
headers: getHeader(),
@ -90,7 +91,7 @@ export class DashboardApi {
formData.append("thumbnail", file);
try {
const response = await fetch(
`${DashboardApi.BASE_URL}/ppt/presentation/thumbnail`,
`${BASE_URL}/ppt/presentation/thumbnail`,
{
method: "POST",
headers: getHeaderForFormData(),

View file

@ -1,377 +0,0 @@
import React from 'react'
import { Metadata } from 'next'
import MarkdownRenderer from '../(presentation-generator)/documents-preview/components/MarkdownRenderer';
export const metadata: Metadata = {
title: 'Presenton Privacy Policy | Data Protection Guidelines',
description: 'Learn how Present On protects your privacy and personal data. Our privacy policy outlines data collection, usage, security measures and your rights as a user.',
}
export default function PrivacyPolicyPage() {
return (
// <div className="container max-w-4xl mx-auto p-6 py-12">
// <h1 className="text-4xl font-bold mb-4">Privacy Policy</h1>
// <p className="text-gray-600 mb-6">Last updated: November 29, 2024</p>
// <p className="mb-6">This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.</p>
// <p className="mb-8">We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy. </p>
// <h2 className="text-3xl font-semibold mt-8 mb-4">Interpretation and Definitions</h2>
// <h3 className="text-2xl font-semibold mt-6 mb-3">Interpretation</h3>
// <p className="mb-4">
// The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.
// </p>
// <h2 className="text-3xl font-semibold mt-8 mb-4">Definitions</h2>
// <p className="mb-4">
// For the purposes of this Privacy Policy:
// </p>
// <ul className="list-disc pl-6 mb-6 space-y-2">
// <li className="mb-2">
// <p><strong>Account</strong> means a unique account created for You to access our Service or parts of our Service.</p>
// </li>
// <li className="mb-2">
// <p><strong>Affiliate</strong> means an entity that controls, is controlled by or is under common control with a party, where &quot;control&quot; means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.</p>
// </li>
// <li className="mb-2">
// <p><strong>Company</strong> (referred to as either &quot;the Company&quot;, &quot;We&quot;, &quot;Us&quot; or &quot;Our&quot; in this Agreement) refers to Present On.</p>
// </li>
// <li className="mb-2">
// <p><strong>Cookies</strong> are small files that are placed on Your computer, mobile device or any other device by a website, containing the details of Your browsing history on that website among its many uses.</p>
// </li>
// <li className="mb-2">
// <p><strong>Country</strong> refers to: Nepal</p>
// </li>
// <li className="mb-2">
// <p><strong>Device</strong> means any device that can access the Service such as a computer, a cellphone or a digital tablet.</p>
// </li>
// <li className="mb-2">
// <p><strong>Personal Data</strong> is any information that relates to an identified or identifiable individual.</p>
// </li>
// <li className="mb-2">
// <p><strong>Service</strong> refers to the Website.</p>
// </li>
// <li className="mb-2">
// <p><strong>Service Provider</strong> means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.</p>
// </li>
// <li className="mb-2">
// <p><strong>Third-party Social Media Service</strong> refers to any website or any social network website through which a User can log in or create an account to use the Service.</p>
// </li>
// <li className="mb-2">
// <p><strong>Usage Data</strong> refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).</p>
// </li>
// <li className="mb-2">
// <p><strong>Website</strong> refers to Present On, accessible from <a href="https://presenton.ai" rel="external nofollow noopener" target="_blank" className="text-blue-600 hover:text-blue-800 underline">https://presenton.ai</a></p>
// </li>
// <li className="mb-2">
// <p><strong>You</strong> means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.</p>
// </li>
// </ul>
// <h2 className="text-3xl font-semibold mt-8 mb-4">Collecting and Using Your Personal Data</h2>
// <h3 className="text-2xl font-semibold mt-6 mb-3">Types of Data Collected</h3>
// <h4 className="text-xl font-semibold mt-4 mb-2">Personal Data</h4>
// <p className="mb-4">
// While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to:
// </p>
// <ul className="list-disc pl-6 mb-6 space-y-2">
// <li className="mb-2">
// <p>Email address</p>
// </li>
// <li className="mb-2">
// <p>First name and last name</p>
// </li>
// <li className="mb-2">
// <p>Address, State, Province, ZIP/Postal code, City</p>
// </li>
// <li className="mb-2">
// <p>Usage Data</p>
// </li>
// </ul>
// <h4 className="text-xl font-semibold mt-4 mb-2">Usage Data</h4>
// <p className="mb-4">
// Usage Data is collected automatically when using the Service.
// </p>
// <p className="mb-4">
// Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data.
// </p>
// <p className="mb-4">
// When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data.
// </p>
// <p className="mb-4">
// We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device.
// </p>
// <h4 className="text-xl font-semibold mt-4 mb-2">Information from Third-Party Social Media Services</h4>
// <p className="mb-4">
// The Company allows You to create an account and log in to use the Service through the following Third-party Social Media Services:
// </p>
// <ul className="list-disc pl-6 mb-6 space-y-2">
// <li className="mb-2">Google</li>
// <li className="mb-2">Facebook</li>
// <li className="mb-2">Instagram</li>
// <li className="mb-2">Twitter</li>
// <li className="mb-2">LinkedIn</li>
// </ul>
// <p className="mb-4">
// If You decide to register through or otherwise grant us access to a Third-Party Social Media Service, We may collect Personal data that is already associated with Your Third-Party Social Media Service's account, such as Your name, Your email address, Your activities or Your contact list associated with that account.
// </p>
// <p className="mb-4">
// You may also have the option of sharing additional information with the Company through Your Third-Party Social Media Service's account. If You choose to provide such information and Personal Data, during registration or otherwise, You are giving the Company permission to use, share, and store it in a manner consistent with this Privacy Policy.
// </p>
// <h4 className="text-xl font-semibold mt-4 mb-2">Tracking Technologies and Cookies</h4>
// <p className="mb-4">
// We use Cookies and similar tracking technologies to track the activity on Our Service and store certain information. Tracking technologies used are beacons, tags, and scripts to collect and track information and to improve and analyze Our Service. The technologies We use may include:
// </p>
// <ul className="list-disc pl-6 mb-6 space-y-2">
// <li className="mb-2">
// <p><strong>Cookies or Browser Cookies.</strong> A cookie is a small file placed on Your Device. You can instruct Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You do not accept Cookies, You may not be able to use some parts of our Service. Unless you have adjusted Your browser setting so that it will refuse Cookies, our Service may use Cookies.</p>
// </li>
// <li className="mb-2">
// <p><strong>Web Beacons.</strong> Certain sections of our Service and our emails may contain small electronic files known as web beacons (also referred to as clear gifs, pixel tags, and single-pixel gifs) that permit the Company, for example, to count users who have visited those pages or opened an email and for other related website statistics (for example, recording the popularity of a certain section and verifying system and server integrity).</p>
// </li>
// </ul>
// <p className="mb-4">
// Cookies can be &quot;Persistent&quot; or &quot;Session&quot; Cookies. Persistent Cookies remain on Your personal computer or mobile device when You go offline, while Session Cookies are deleted as soon as You close Your web browser. You can learn more about cookies on <a href="https://www.termsfeed.com/blog/cookies/#What_Are_Cookies" target="_blank" className="text-blue-600 hover:text-blue-800 underline">TermsFeed website</a> article.
// </p>
// <p className="mb-4">
// We use both Session and Persistent Cookies for the purposes set out below:
// </p>
// <ul className="list-disc pl-6 mb-6 space-y-2">
// <li className="mb-2">
// <p><strong>Necessary / Essential Cookies</strong></p>
// <p>Type: Session Cookies</p>
// <p>Administered by: Us</p>
// <p>Purpose: These Cookies are essential to provide You with services available through the Website and to enable You to use some of its features. They help to authenticate users and prevent fraudulent use of user accounts. Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to provide You with those services.</p>
// </li>
// <li className="mb-2">
// <p><strong>Cookies Policy / Notice Acceptance Cookies</strong></p>
// <p>Type: Persistent Cookies</p>
// <p>Administered by: Us</p>
// <p>Purpose: These Cookies identify if users have accepted the use of cookies on the Website.</p>
// </li>
// <li className="mb-2">
// <p><strong>Functionality Cookies</strong></p>
// <p>Type: Persistent Cookies</p>
// <p>Administered by: Us</p>
// <p>Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your login details or language preference. The purpose of these Cookies is to provide You with a more personal experience and to avoid You having to re-enter your preferences every time You use the Website.</p>
// </li>
// </ul>
// <p className="mb-4">
// For more information about the cookies we use and your choices regarding cookies, please visit our Cookies Policy or the Cookies section of our Privacy Policy.
// </p>
// <h3 className="text-2xl font-semibold mt-6 mb-3">Use of Your Personal Data</h3>
// <p className="mb-4">
// The Company may use Personal Data for the following purposes:
// </p>
// <ul className="list-disc pl-6 mb-6 space-y-2">
// <li className="mb-2">
// <p><strong>To provide and maintain our Service</strong>, including to monitor the usage of our Service.</p>
// </li>
// <li className="mb-2">
// <p><strong>To manage Your Account:</strong> to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user.</p>
// </li>
// <li className="mb-2">
// <p><strong>For the performance of a contract:</strong> the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service.</p>
// </li>
// <li className="mb-2">
// <p><strong>To contact You:</strong> To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation.</p>
// </li>
// <li className="mb-2">
// <p><strong>To provide You</strong> with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information.</p>
// </li>
// <li className="mb-2">
// <p><strong>To manage Your requests:</strong> To attend and manage Your requests to Us.</p>
// </li>
// <li className="mb-2">
// <p><strong>For business transfers:</strong> We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred.</p>
// </li>
// <li className="mb-2">
// <p><strong>For other purposes</strong>: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience.</p>
// </li>
// </ul>
// <p className="mb-4">
// We may share Your personal information in the following situations:
// </p>
// <ul className="list-disc pl-6 mb-6 space-y-2">
// <li className="mb-2">
// <p><strong>With Service Providers:</strong> We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You.</p>
// </li>
// <li className="mb-2">
// <p><strong>For business transfers:</strong> We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company.</p>
// </li>
// <li className="mb-2">
// <p><strong>With Affiliates:</strong> We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us.</p>
// </li>
// <li className="mb-2">
// <p><strong>With business partners:</strong> We may share Your information with Our business partners to offer You certain products, services or promotions.</p>
// </li>
// <li className="mb-2">
// <p><strong>With other users:</strong> when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside. If You interact with other users or register through a Third-Party Social Media Service, Your contacts on the Third-Party Social Media Service may see Your name, profile, pictures and description of Your activity. Similarly, other users will be able to view descriptions of Your activity, communicate with You and view Your profile.</p>
// </li>
// <li className="mb-2">
// <p><strong>With Your consent</strong>: We may disclose Your personal information for any other purpose with Your consent.</p>
// </li>
// </ul>
// <h3 className="text-2xl font-semibold mt-6 mb-3">Retention of Your Personal Data</h3>
// <p className="mb-4">
// The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.
// </p>
// <p className="mb-4">
// The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.
// </p>
// <h3 className="text-2xl font-semibold mt-6 mb-3">Transfer of Your Personal Data</h3>
// <p className="mb-4">
// Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction.
// </p>
// <p className="mb-4">
// Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer.
// </p>
// <p className="mb-4">
// The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information.
// </p>
// <h3 className="text-2xl font-semibold mt-6 mb-3">Delete Your Personal Data</h3>
// <p className="mb-4">
// You have the right to delete or request that We assist in deleting the Personal Data that We have collected about You.
// </p>
// <p className="mb-4">
// Our Service may give You the ability to delete certain information about You from within the Service.
// </p>
// <p className="mb-4">
// You may update, amend, or delete Your information at any time by signing in to Your Account, if you have one, and visiting the account settings section that allows you to manage Your personal information. You may also contact Us to request access to, correct, or delete any personal information that You have provided to Us.
// </p>
// <p className="mb-4">
// Please note, however, that We may need to retain certain information when we have a legal obligation or lawful basis to do so.
// </p>
// <h3 className="text-2xl font-semibold mt-6 mb-3">Disclosure of Your Personal Data</h3>
// <h4 className="text-xl font-semibold mt-4 mb-2">Business Transactions</h4>
// <p className="mb-4">
// If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy.
// </p>
// <h4 className="text-xl font-semibold mt-4 mb-2">Law enforcement</h4>
// <p className="mb-4">
// Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency).
// </p>
// <h4 className="text-xl font-semibold mt-4 mb-2">Other legal requirements</h4>
// <p className="mb-4">
// The Company may disclose Your Personal Data in the good faith belief that such action is necessary to:
// </p>
// <ul className="list-disc pl-6 mb-6 space-y-2">
// <li className="mb-2">Comply with a legal obligation</li>
// <li className="mb-2">Protect and defend the rights or property of the Company</li>
// <li className="mb-2">Prevent or investigate possible wrongdoing in connection with the Service</li>
// <li className="mb-2">Protect the personal safety of Users of the Service or the public</li>
// <li className="mb-2">Protect against legal liability</li>
// </ul>
// <h3 className="text-2xl font-semibold mt-6 mb-3">Security of Your Personal Data</h3>
// <p className="mb-4">
// The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security.
// </p>
// <h2 className="text-3xl font-semibold mt-8 mb-4">Children's Privacy</h2>
// <p className="mb-4">
// Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers.
// </p>
// <p className="mb-4">
// If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information.
// </p>
// <h2 className="text-3xl font-semibold mt-8 mb-4">Links to Other Websites</h2>
// <p className="mb-4">
// Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.
// </p>
// <p className="mb-4">
// We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.
// </p>
// <h2 className="text-3xl font-semibold mt-8 mb-4">Changes to this Privacy Policy</h2>
// <p className="mb-4">
// We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page.
// </p>
// <p className="mb-4">
// We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the &quot;Last updated&quot; date at the top of this Privacy Policy.
// </p>
// <p className="mb-4">
// You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.
// </p>
// <h2 className="text-3xl font-semibold mt-8 mb-4">Contact Us</h2>
// <p className="mb-4">
// If you have any questions about this Privacy Policy, You can contact us:
// </p>
// <ul className="list-disc pl-6 mb-6 space-y-2">
// <li className="mb-2">
// By visiting this page on our website: <a href="https://presenton.ai/contact" rel="external nofollow noopener" target="_blank" className="text-blue-600 hover:text-blue-800 underline">https://presenton.ai/contact</a>
// </li>
// </ul>
// </div>
<div className='max-w-[1280px] mx-auto py-10'>
<MarkdownRenderer content={`
# Privacy Policy for Kinu Tech Pvt. Ltd. (Presenton)
**Effective Date:** ${new Date().toLocaleDateString()} <br>
Kinu Tech Pvt. Ltd. ("Presenton," "we," "us," or "our") is committed to safeguarding your privacy. This Privacy Policy describes how we collect, use, disclose, and protect your personal information in connection with our services. By accessing or using our services, you agree to this Privacy Policy.
## 1. Scope
This Privacy Policy applies to all personal data collected through our website, applications, and any related services.
## 2. Information We Collect
### Personal Information
- **Identity Data:** Name, username, or similar identifiers.
- **Contact Data:** Email address, phone numbers.
- **Financial Data:** Payment card details (processed via Stripe).
- **Transaction Data:** Details about payments and services.
- **Technical Data:** IP address, login data, browser type, time zone, browser plug-in types, and platform.
- **Profile Data:** Preferences, feedback, and survey responses.
- **Usage Data:** Information on how you use our website and services.
- **Marketing and Communications Data:** Your preferences in receiving marketing from us and third parties.
## 3. How We Collect Data
- **Direct Interactions:** Account creation, purchase of services, feedback forms.
- **Automated Technologies:** Cookies, server logs, and analytics tools (via Google Analytics).
- **Third Parties:** Identity and contact data from third-party platforms like Supabase.
## 4. Legal Basis for Processing
We process your personal data under the following legal grounds:
- **Consent:** When you provide consent for marketing.
- **Contractual Necessity:** To fulfill our service agreement with you.
- **Legal Obligation:** To comply with applicable laws.
- **Legitimate Interests:** Improving our services and user experience.
## 5. How We Use Your Data
We use your data for:
- Account setup and maintenance
- Processing transactions
- Customer support
- Improving our platform and services
- Marketing and promotional communications
## 6. Sharing Your Information
We may share your information with:
- **Service Providers:** For hosting, payment processing (Stripe), and analytics.
- **Professional Advisers:** Lawyers, bankers, auditors.
- **Regulatory Authorities:** As required by law.
- **Business Transfers:** As part of mergers, acquisitions, or sales.
## 7. International Transfers
Your data may be transferred to countries outside your location. We ensure adequate data protection measures are in place, such as standard contractual clauses.
## 8. Data Security
We implement advanced security measures, including:
- **Encryption:** Protecting personal data during transmission.
- **Access Controls:** Limiting access to personal information.
- **Monitoring:** Regular security audits and vulnerability assessments.
## 9. Data Retention
We retain your data for as long as necessary to fulfill our services and comply with legal obligations. Upon request, we will delete your data in accordance with applicable laws.
## 10. Cookies and Tracking
Our website uses cookies for:
- Functionality: Remembering your preferences.
- Performance: Analyzing usage to improve our site.
- Targeting: Providing personalized advertising based on browsing activity.
You may control cookies through browser settings, but disabling them may affect your experience.
## 11. Your Privacy Rights
Depending on your location, you may have the right to:
- Access, correct, or delete your personal data.
- Object to or restrict processing.
- Withdraw consent where processing is based on consent.
- Request data portability.
To exercise these rights, contact us at [suraj@presenton.ai](mailto:suraj@presenton.ai).
## 12. Children's Privacy
Our services are not intended for individuals under 13 years old. We do not knowingly collect data from children.
## 13. Policy Updates
We may update this Privacy Policy to reflect changes in technology or legislation. Changes will be notified via email or site notifications.
## 14. Contact Us
For any questions or concerns about this Privacy Policy, please contact:
- **Suraj Jha**
- Email: [suraj@presenton.ai](mailto:suraj@presenton.ai)
## 15. Governing Law and Dispute Resolution
This Privacy Policy is governed by the laws of [Specify Jurisdiction]. Any disputes arising under this policy will be resolved in accordance with local regulations. `} />
</div>
);
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
export const BASE_URL = process.env.NEXT_PUBLIC_FAST_API || 'http://localhost:8000';