Replace BASE_URL references;Cleaned up the slides Layout.
This commit is contained in:
parent
7f37966ee0
commit
10b0c830ef
19 changed files with 101 additions and 3322 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 "control" 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 "the Company", "We", "Us" or "Our" 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 "Persistent" or "Session" 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 "Last updated" 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
1
servers/nextjs/utils/constant.ts
Normal file
1
servers/nextjs/utils/constant.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const BASE_URL = process.env.NEXT_PUBLIC_FAST_API || 'http://localhost:8000';
|
||||
Loading…
Add table
Reference in a new issue