new design added

This commit is contained in:
Saimon8420 2025-02-08 18:01:37 +06:00
parent 2c0ab8b736
commit e2b8d7368f
10 changed files with 225 additions and 422 deletions

View file

@ -61,13 +61,15 @@ export const Home = () => {
{!isLoading &&
<>
<Toaster />
<div>
{
activeObject && <TopBar />
}
</div>
<div className="fixed z-[999] right-0">
{
activeObject &&
<div className="absolute left-[90px] right-[90px] z-[9999] rounded-md p-1 h-fit bg-white border-t border-gray-200 shadow-md my-1 w-[80%] mx-auto">
<TopBar />
</div>
}
<div className="absolute z-[9999] right-0 bottom-0 flex justify-center items-center h-20 bg-white border-t border-gray-200 shadow-md w-fit">
<ActionButtons />
</div>
@ -79,7 +81,7 @@ export const Home = () => {
<EditorPanel />
</div>
<div className="flex-1 flex flex-col h-full overflow-hidden">
<div className="flex-1 flex flex-col h-full overflow-hidden my-2">
<div className="flex-1 overflow-auto">
<Canvas />
</div>

View file

@ -15,7 +15,6 @@ const aspectRatios = [
{ value: "4:3", label: "Standard (4:3)" },
{ value: "3:2", label: "Classic (3:2)" },
{ value: "16:9", label: "Widescreen (16:9)" },
{ value: "9:16", label: "Portrait (9:16)" },
{ value: "21:9", label: "Ultrawide (21:9)" },
{ value: "32:9", label: "Super Ultrawide (32:9)" },
{ value: "1.85:1", label: "Cinema Standard (1.85:1)" },
@ -24,7 +23,9 @@ const aspectRatios = [
{ value: "5:4", label: "Large Format (5:4)" },
{ value: "7:5", label: "Artistic Format (7:5)" },
{ value: "11:8.5", label: "Letter Size (11:8.5)" },
{ value: "3:4", label: "Portrait (3:4)" },
{ value: "3:4", label: "Portrait (4:4)" },
{ value: "9:16", label: "Vertical (9:16)" },
{ value: "1.33:1", label: "Instagram Stories (1.33:1)" },
{ value: "1.91:1", label: "Facebook Ads (1.91:1)" },
];

View file

@ -236,22 +236,9 @@ export default function Canvas() {
}, [canvas, setActiveObject]);
return (
<>
{/* Zoom Controls */}
<div className="fixed bottom-4 right-4 flex items-center gap-4 bg-white p-3 rounded-lg shadow-lg z-50">
<span className="text-sm font-medium min-w-[45px]">{zoomLevel}%</span>
<Slider
value={[zoomLevel]}
onValueChange={(value) => handleZoom(value[0])}
min={0}
max={100}
step={1}
className="w-32"
/>
</div>
<div>
{/* Canvas Container */}
<div className="w-full max-w-4xl mx-auto mt-20">
<div className="w-full max-w-2xl mx-auto mt-20">
<Card
className={`w-full rounded-none flex-1 flex flex-col
mx-auto border-0 shadow-none bg-transparent
@ -282,6 +269,19 @@ export default function Canvas() {
</CardContent>
</Card>
</div>
</>
{/* Zoom Controls */}
<div className="flex my-2 mb-4 bg-white p-3 rounded-lg shadow-lg z-50 w-fit">
<span className="text-sm font-medium min-w-[45px]">{zoomLevel}%</span>
<Slider
value={[zoomLevel]}
onValueChange={(value) => handleZoom(value[0])}
min={0}
max={100}
step={1}
className="w-32"
/>
</div>
</div>
);
}

View file

@ -1,21 +0,0 @@
import { AlignLeft, AlignRight } from 'lucide-react';
import { useContext } from 'react';
import OpenContext from '../Context/openContext/OpenContext';
const Header = () => {
const { setRightPanelOpen, setLeftPanelOpen } = useContext(OpenContext);
return (
<div className='bg-[#f2e6f8] fixed top-0 w-[60px] z-[900] min-w-full flex items-center p-4 shadow-md'>
<p className='mr-auto flex-wrap gap-1 cursor-pointer text-xs items-center font-semibold xl:flex lg:flex md:flex hidden' onClick={() => setLeftPanelOpen(true)}><AlignLeft /><span className='hidden xl:block lg:block md:block sm:block'>Add Element</span></p>
<h1 className='font-semibold'>PlanPostAi Canvas</h1>
<p className='ml-auto flex-wrap gap-1 cursor-pointer text-xs items-center font-semibold xl:flex lg:flex md:flex hidden' onClick={() => setRightPanelOpen(true)}> <span className='hidden xl:block lg:block md:block sm:block'>Edit Panel</span><AlignRight /></p>
</div>
)
}
export default Header

View file

@ -281,7 +281,7 @@ export const ObjectShortcut = ({ value }) => {
<p>Change object position</p>
</TooltipContent>
</Tooltip>
<PopoverContent className="w-40 p-2">
<PopoverContent className="w-40 p-2 z-[9999]">
<div className="flex flex-col gap-2">
<Button
variant="ghost"
@ -340,7 +340,7 @@ function ActionButton({ icon, label, onClick, tooltipContent }) {
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" className="max-w-xs">
<TooltipContent side="bottom" align="center" className="max-w-xs z-[9999]">
<p>{tooltipContent}</p>
</TooltipContent>
</Tooltip>

View file

@ -8,7 +8,7 @@ import { deleteProject, getProjects } from '@/api/projectApi';
import { useNavigate } from 'react-router-dom';
import { toast } from '@/hooks/use-toast';
import ActiveObjectContext from '../Context/activeObject/ObjectContext';
import { FixedSizeList as List } from 'react-window'; // Import FixedSizeList from react-window
import { FixedSizeList as List } from 'react-window';
export const ProjectPanel = () => {
const { setSelectedPanel, canvas } = useContext(CanvasContext);
@ -63,12 +63,11 @@ export const ProjectPanel = () => {
}
});
// Row renderer for react-window
const Row = ({ index, style }) => {
const project = projects?.data?.[index]; // Get project by index
const project = filteredProjects[index]; // Use filtered projects
return (
<div key={project?.id} className='flex flex-col gap-1 bg-red-50 p-1 rounded-md' style={style}>
<div key={project?.id} className='flex flex-col gap-1 bg-red-50 p-1 rounded-md border border-red-100' style={style}>
<div className='rounded-md flex p-1 flex-col gap-1 hover:bg-red-100 cursor-pointer transition-all' onClick={() => navigate(`/${project.id}`)}>
<p className='font-bold text-sm truncate'>{project?.name}</p>
<p className='text-xs truncate'>{project?.description} </p>
@ -77,11 +76,16 @@ export const ProjectPanel = () => {
<Button
disabled={deletePending}
className="w-fit p-1 ml-auto" size="small" onClick={() => { projectDelete(project?.id) }}><Trash className="h-4 w-4" /></Button>
className="w-fit p-1 ml-auto" size="small" onClick={() => { projectDelete(project?.id) }}>
<Trash className="h-4 w-4" />
</Button>
</div>
);
};
// Filter projects where preview_url is not empty or null
const filteredProjects = projects?.data?.filter(project => project?.preview_url) || [];
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
@ -102,9 +106,9 @@ export const ProjectPanel = () => {
{
!projectLoading && projectSuccess && projects?.status === 200 &&
<List
height={500} // Set the height of the viewport
itemCount={projects?.data?.length} // Total number of items
itemSize={300} // Height of each row
height={400} // Set the height of the viewport
itemCount={filteredProjects.length} // Use length of filtered projects
itemSize={280} // Height of each row
width="100%" // Full width of the container
>
{Row}

View file

@ -21,220 +21,124 @@ export function TopBar() {
const activeObjectType = activeObject?.type;
const hasClipPath = !!activeObject?.clipPath;
const customClipPath = activeObject?.isClipPath;
const scrollContainerRef = useRef(null);
const [showScrollButtons, setShowScrollButtons] = useState(false);
const scroll = (direction) => {
const container = scrollContainerRef.current;
if (container) {
const scrollAmount = 200; // Adjust this value to change scroll distance
container.scrollBy({
left: direction * scrollAmount,
behavior: "smooth",
});
}
};
// w - [calc(100 % -80px)] h - full
return (
<div
className={`!absolute top-2 -translate-x-1/2 z-40 ${selectedPanel !== ""
? "md:w-[465px] left-[41%] lg:w-[700px] lg:left-[43%] mdxl:left-[45%] xl:w-[900px] xl:left-[46%] 2xl:left-[50%]"
: "w-[500px] lg:w-[600px] xl:w-[900px] lg:left-[41%] xl:left-[50%]"
}`}
onMouseEnter={() => setShowScrollButtons(true)}
onMouseLeave={() => setShowScrollButtons(false)}
>
<div className="relative h-full hidden xl:block">
{showScrollButtons && (
<>
<div className="w-full h-full overflow-x-scroll p-1">
<div className="flex item-center justify-center w-fit mx-auto">
<div className="bg-white mx-auto flex justify-center items-center gap-2">
<div className="flex-1">
<TextCustomization />
</div>
<div className="flex items-center gap-4 px-2">
<Button
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 bg-accent"
onClick={() => scroll(-1)}
variant="ghost"
size="icon"
variant="outline"
className="flex items-center gap-2 border-dashed border-2 rounded-md hover:bg-gray-50"
onClick={() => setSelectedPanel("image-insert")}
>
<ChevronLeft className="h-4 w-4" />
<ImagePlus className="w-5 h-5" />
<span>Add image</span>
</Button>
<Button
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 bg-accent"
onClick={() => scroll(1)}
variant="ghost"
size="icon"
>
<ChevronRight className="h-4 w-4" />
</Button>
</>
)}
<div
ref={scrollContainerRef}
className="w-full h-full overflow-x-auto scrollbar-hide"
>
<div className="bg-white shadow-sm mx-auto px-4 xl:px-10 py-2 flex justify-start items-center gap-2 min-w-max h-full">
{/* Canvas settings */}
<div>
<TextCustomization />
</div>
<div className="flex items-center gap-4 px-2">
<Button
variant="outline"
className="flex items-center gap-2 border-dashed border-2 rounded-md hover:bg-gray-50"
onClick={() => setSelectedPanel("image-insert")}
>
<ImagePlus className="w-5 h-5" />
<span>Add image</span>
</Button>
{/* canvas settings */}
<div>
<a data-tooltip-id="canvas">
<Button
variant="ghost"
size="icon"
className="w-10 h-10"
onClick={() => setSelectedPanel("canvas")}
<a data-tooltip-id="canvas">
<Button
variant="ghost"
size="icon"
className="w-10 h-10"
onClick={() => setSelectedPanel("canvas")}
>
<svg
width="100"
height="100"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg
width="100"
height="100"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="3"
y="5"
width="18"
height="12"
stroke="black"
strokeWidth="2"
fill="none"
/>
<path d="M14 14 L19 19" stroke="black" strokeWidth="2" />
<path
d="M15 15 Q16 12, 19 11"
stroke="black"
strokeWidth="2"
fill="none"
/>
<line
x1="3"
y1="17"
x2="7"
y2="21"
stroke="black"
strokeWidth="2"
/>
<line
x1="21"
y1="17"
x2="17"
y2="21"
stroke="black"
strokeWidth="2"
/>
</svg>
</Button>
</a>
<Tooltip id="canvas" content="Canvas Settings" place="bottom" />
</div>
<rect x="3" y="5" width="18" height="12" stroke="black" strokeWidth="2" fill="none" />
<path d="M14 14 L19 19" stroke="black" strokeWidth="2" />
<path d="M15 15 Q16 12, 19 11" stroke="black" strokeWidth="2" fill="none" />
<line x1="3" y1="17" x2="7" y2="21" stroke="black" strokeWidth="2" />
<line x1="21" y1="17" x2="17" y2="21" stroke="black" strokeWidth="2" />
</svg>
</Button>
</a>
<Tooltip id="canvas" content="Canvas Settings" place="bottom" />
</div>
<div className="flex items-center gap-2">
{activeObjectType !== "image" &&
activeObject?.type !== "i-text" &&
!hasClipPath &&
!customClipPath && (
<a data-tooltip-id="color-gr">
<Button
variant="ghost"
size="icon"
className="rounded-full"
onClick={() => setSelectedPanel("color")}
style={{ backgroundColor: textColor?.fill || "black" }}
></Button>
</a>
)}
<Tooltip id="color-gr" content="Color" place="bottom" />
{!customClipPath && (
<a data-tooltip-id="stroke">
<div className="flex items-center gap-2">
{activeObjectType !== "image" &&
activeObject?.type !== "i-text" &&
!hasClipPath &&
!customClipPath && (
<a data-tooltip-id="color-gr">
<Button
variant="ghost"
size="icon"
className="w-10 h-10"
onClick={() => setSelectedPanel("stroke")}
>
<RxBorderWidth className="text-lg" />
</Button>
className="rounded-full"
onClick={() => setSelectedPanel("color")}
style={{ backgroundColor: textColor?.fill || "black" }}
></Button>
</a>
)}
<Tooltip id="stroke" content="Stroke" place="bottom" />
{(activeObject || activeObject.type === "group") && (
<a data-tooltip-id="group-obj">
<Button
variant="ghost"
size="icon"
className="w-10 h-10"
onClick={() => setSelectedPanel("group-obj")}
>
<FaRegObjectGroup />
</Button>
</a>
)}
<Tooltip id="group-obj" content="Group Object" place="bottom" />
</div>
<Tooltip id="color-gr" content="Color" place="bottom" />
<div className="h-6 w-px bg-gray-200" />
<div className="flex items-center gap-2">
<a data-tooltip-id="flip">
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("flip")}
>
<LuFlipVertical />
{!customClipPath && (
<a data-tooltip-id="stroke">
<Button variant="ghost" size="icon" className="w-10 h-10" onClick={() => setSelectedPanel("stroke")}>
<RxBorderWidth className="text-lg" />
</Button>
</a>
)}
<Tooltip id="stroke" content="Stroke" place="bottom" />
<Tooltip id="flip" content="Object flip" place="bottom" />
{activeObject?.type !== "group" && (
<a data-tooltip-id="position">
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("position")}
>
<SlTarget />
</Button>
</a>
)}
<Tooltip
id="position"
content="Object position"
place="bottom"
/>
<a data-tooltip-id="shadow">
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("shadow")}
>
<RiShadowLine />
{(activeObject || activeObject.type === "group") && (
<a data-tooltip-id="group-obj">
<Button variant="ghost" size="icon" className="w-10 h-10" onClick={() => setSelectedPanel("group-obj")}>
<FaRegObjectGroup />
</Button>
</a>
<Tooltip id="shadow" content="Shadow color" place="bottom" />
</div>
)}
<Tooltip id="group-obj" content="Group Object" place="bottom" />
</div>
<OpacityCustomization />
<div className="h-4 w-px bg-border mx-2" />
<div>
<ObjectShortcut value="default" />
</div>
<div className="ml-4">
<LockObject />
<div className="h-6 w-px bg-gray-200" />
<div className="flex items-center gap-2">
<a data-tooltip-id="flip">
<Button variant="ghost" size="icon" onClick={() => setSelectedPanel("flip")}>
<LuFlipVertical />
</Button>
</a>
<Tooltip id="flip" content="Object flip" place="bottom" />
{activeObject?.type !== "group" && (
<a data-tooltip-id="position">
<Button variant="ghost" size="icon" onClick={() => setSelectedPanel("position")}>
<SlTarget />
</Button>
</a>
)}
<Tooltip id="position" content="Object position" place="bottom" />
<a data-tooltip-id="shadow">
<Button variant="ghost" size="icon" onClick={() => setSelectedPanel("shadow")}>
<RiShadowLine />
</Button>
</a>
<Tooltip id="shadow" content="Shadow color" place="bottom" />
</div>
</div>
<OpacityCustomization />
<div className="h-4 w-px bg-border mx-2" />
<div>
<ObjectShortcut value="default" />
</div>
<div className="ml-4">
<LockObject />
</div>
</div>
</div>
</div>

View file

@ -1,107 +0,0 @@
const loadCanvasState = (state) => {
if (!canvas2) {
console.error('Canvas2 is not initialized!');
return;
}
try {
const canvasData = typeof state === 'string' ? JSON.parse(state) : state;
// Clear the existing canvas
canvas2.clear();
// Set the background color if it exists
if (canvasData.backgroundColor) {
canvas2.set({ backgroundColor: canvasData.backgroundColor });
}
// Function to load background image
const loadBackgroundImage = () => {
return new Promise((resolve) => {
if (!canvasData.background) {
resolve();
return;
}
const imgElement = new Image();
imgElement.crossOrigin = 'anonymous'; // Handle CORS if needed
imgElement.onload = () => {
const imgInstance = new fabric.Image(imgElement, {
originX: canvasData.background.originX || 'left',
originY: canvasData.background.originY || 'top',
scaleX: canvasData.background.scaleX || canvas2.width / imgElement.width,
scaleY: canvasData.background.scaleY || canvas2.height / imgElement.height,
opacity: canvasData.background.opacity || 1,
});
// Set background image using the set method
canvas2.set({ backgroundImage: imgInstance });
resolve();
};
imgElement.onerror = () => {
console.error("Failed to load the background image");
resolve(); // Resolve anyway to continue loading objects
};
imgElement.src = canvasData.background.src;
});
};
// Function to load objects
const loadObjects = () => {
return new Promise((resolve) => {
canvas2.loadFromJSON({ objects: canvasData.objects }, () => {
resolve();
}, (obj, element) => {
return obj;
});
});
};
// Load background image and objects if they exist
const loadTasks = [];
if (canvasData.background || canvasData.objects?.length) {
if (canvasData.background) {
loadTasks.push(loadBackgroundImage());
}
if (canvasData.objects?.length) {
loadTasks.push(loadObjects());
}
Promise.all(loadTasks)
.then(() => {
canvas2.requestRenderAll(); // Ensure re-rendering
})
.catch((error) => {
console.error('Error during canvas loading:', error);
});
} else {
// If only background color exists
if (canvasData.backgroundColor) {
canvas2.requestRenderAll(); // Apply background color and render
}
}
} catch (error) {
console.error('Error loading canvas state:', error);
}
};
// for download image/canvas
const exportScaledCanvasImage = (scale = 2) => {
if (canvas) {
const imageBase64 = canvas.toDataURL({
format: 'png', // or 'jpeg'
quality: 1, // (0 to 1) for JPEG compression, ignored for PNG
multiplier: scale, // Scale the canvas size
});
// Download the scaled image
const link = document.createElement('a');
link.href = imageBase64;
link.download = `canvas-image-${scale}x.png`;
link.click();
}
};

View file

@ -153,3 +153,23 @@
@apply bg-[#f5f0ff] text-foreground;
}
}
::-webkit-scrollbar {
width: 0px;
height: 5px;
border-radius: 5px;
cursor: pointer;
/* background-color: var(--primary-600); */
@apply bg-red-50
}
::-webkit-scrollbar-track {
background-color: var(--primary-500);
}
::-webkit-scrollbar-thumb {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
width: 5px;
background-color: red;
border-radius: 5px;
}

View file

@ -3,71 +3,71 @@ import scrollbarHide from "tailwind-scrollbar-hide";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
theme: {
extend: {
textColor: {
primary: {
DEFAULT: 'hsl(var(--primary-text))',
foreground: 'hsl(var(--primary-foreground))'
}
},
screens: {
mdxl: '1100px'
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
selection: {
DEFAULT: 'hsl(var(--selection))',
foreground: 'hsl(var(--selection-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [scrollbar, scrollbarHide, require("tailwindcss-animate")],
darkMode: ["class"],
content: ["./index.html", "./src/**/*.{ts,tsx,js,jsx}"],
theme: {
extend: {
textColor: {
primary: {
DEFAULT: 'hsl(var(--primary-text))',
foreground: 'hsl(var(--primary-foreground))'
}
},
screens: {
mdxl: '1100px'
},
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
selection: {
DEFAULT: 'hsl(var(--selection))',
foreground: 'hsl(var(--selection-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [scrollbar, scrollbarHide, require("tailwindcss-animate")],
};