download count added, code optimized

This commit is contained in:
Saimon8420 2025-02-11 17:55:34 +06:00
parent 20526b3628
commit 1ed96799a0
30 changed files with 828 additions and 729 deletions

View file

@ -71,6 +71,7 @@ function App() {
<Route path="/:id" element={<Home />} />
<Route path="/unAuthenticated" element={<Unauthenticated />} />
<Route path="*" element={<NotFound />} />
<Route path="/notFound" element={<NotFound />} />
</Routes>
);
}

View file

@ -8,16 +8,19 @@ import Canvas from './components/Canvas/Canvas';
import CanvasCapture from './components/CanvasCapture';
import { useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { generateToken } from './api/authApi';
import { generateToken, getUser } from './api/authApi';
import { useNavigate } from "react-router-dom";
import { Toaster } from '@/components/ui/toaster';
import { getProjectById } from './api/projectApi';
import { Loader } from 'lucide-react';
import CanvasContext from './components/Context/canvasContext/CanvasContext';
import AuthContext from './components/Context/authContext/AuthContext';
import { useToast } from './hooks/use-toast';
import useProject from './hooks/useProject';
export const Home = () => {
const { activeObject } = useContext(ActiveObjectContext);
const { canvas, selectedPanel } = useContext(CanvasContext);
const { user, setUser } = useContext(AuthContext);
const params = useParams();
const navigate = useNavigate();
@ -25,6 +28,8 @@ export const Home = () => {
const { id } = params;
const { toast } = useToast();
// Fetch token only if it doesn't exist
const { data, isLoading } = useQuery({
queryKey: ['get-token'],
@ -32,11 +37,21 @@ export const Home = () => {
});
// Fetch project data only if token and id exist
const { data: projectData, isLoading: projectLoading } = useQuery({
queryKey: ['project', id],
queryFn: async () => await getProjectById(id),
enabled: !!getToken() && !!id && selectedPanel === "project",
});
const { projectData, isLoading: projectLoading } = useProject();
// Fetch user related data only if token exists
const { data: userData, isLoading: userLoading } = useQuery({
queryKey: ['user'],
queryFn: async () => await getUser(),
enabled: !!getToken() && user?.length === 0,
})
// update the data into context
useEffect(() => {
if (userData?.status === 200 && !userLoading) {
setUser([userData?.user])
}
}, [userData, userLoading, setUser]);
useEffect(() => {
const token = getToken(); // Get latest token
@ -47,17 +62,28 @@ export const Home = () => {
if (token && !isLoading && data?.status === 201) {
navigate("/");
}
if (projectData?.status === 200 & !projectLoading && projectData?.data?.object && canvas && canvas?._objects?.length === 0 && selectedPanel === "project") {
if (projectData?.status === 404 || projectData?.status === 500) {
navigate("/");
toast({ variant: "destructive", title: projectData?.status, description: "No project found" });
}
if (projectData?.status === 200 & !projectLoading && projectData?.data?.object && canvas && selectedPanel === "project" && id) {
const isEmpty = (obj) => Object.values(obj).length === 0;
if (!isEmpty(projectData?.data?.object)) {
canvas.loadFromJSON(projectData?.data?.object);
canvas.renderAll();
}
}, [navigate, isLoading, data, projectData, projectLoading, canvas, selectedPanel]);
}
}, [navigate, isLoading, data, projectData, projectLoading, canvas, selectedPanel, id, toast]);
return (
<div className="flex h-screen overflow-hidden relative items-center justify-center">
<div>
{
isLoading && <p><Loader className="animate-spin mx-auto" /></p>
isLoading &&
<div className='flex justify-center items-center h-screen'>
<p><Loader className="animate-spin mx-auto" /></p>
</div>
}
<div className="flex h-screen overflow-hidden relative">
{!isLoading &&
<>
@ -70,7 +96,7 @@ export const Home = () => {
</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">
<div className="absolute z-[999] right-0 bottom-0 flex justify-center items-center h-20 bg-white border-t border-gray-200 shadow-md w-fit">
<ActionButtons />
</div>
@ -92,5 +118,6 @@ export const Home = () => {
</>
}
</div>
</div>
)
}

View file

@ -22,3 +22,27 @@ export const generateToken = async (id: string) => {
throw error;
}
}
export const getUser = async () => {
try {
const url = `${import.meta.env.VITE_SERVER_URL}/auth/user/me`;
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (data?.token) {
localStorage.setItem('canvas_token', data.token);
// Remove the token from the response data
const { token, ...restData } = data;
return restData; // Return modified data without token
}
else if (!data?.token) {
localStorage.removeItem("canvas_token");
return data;
}
return data;
} catch (error) {
console.error('Un-authenticated:', error);
}
}

27
src/api/downloadApi.ts Normal file
View file

@ -0,0 +1,27 @@
export const downloadCount = async (date) => {
try {
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/download-count/`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date })
});
const data = await response.json();
if (data?.token) {
localStorage.setItem('canvas_token', data.token);
// Remove the token from the response data
const { token, ...restData } = data;
return restData; // Return modified data without token
}
else if (!data?.token) {
localStorage.removeItem("canvas_token");
return data;
}
return data;
} catch (error) {
console.error('Failed to get download count:', error);
throw error;
}
}

View file

@ -8,6 +8,9 @@ import { Card, CardHeader, CardTitle } from './ui/card';
import { X } from 'lucide-react';
import { Separator } from './ui/separator';
import RndComponent from './Layouts/RndComponent';
import { useMutation } from '@tanstack/react-query';
import { downloadCount } from '@/api/downloadApi';
import { useToast } from '@/hooks/use-toast';
const resolutions = [
{ value: '720p', label: 'HD (1280x720)', width: 1280, height: 720 },
@ -30,6 +33,8 @@ const CanvasCapture = () => {
const [resolution, setResolution] = useState(resolutions[0].value);
const [imageType, setImageType] = useState(imageTypes[0].value);
const { toast } = useToast();
const { canvas } = useContext(CanvasContext);
const { captureOpen, setCaptureOpen } = useContext(OpenContext);
@ -85,6 +90,34 @@ const CanvasCapture = () => {
bound: "parent"
}
const { mutate: downloadMutate } = useMutation({
mutationFn: async (date) => {
return await downloadCount(date)
},
onSuccess: (data) => {
if (data?.success === true && data?.status === 200) {
toast({
title: data?.status,
description: data?.message,
})
captureImage();
}
else {
toast({
title: data?.status,
description: data?.message,
variant: "destructive",
})
}
}
})
const handleDownload = () => {
const date = new Date().toLocaleDateString();
downloadMutate(date);
}
return (
<>
{captureOpen && (
@ -131,7 +164,7 @@ const CanvasCapture = () => {
</Select>
</div>
</div>
<Button onClick={captureImage} className='w-full rnd-escape'>
<Button onClick={handleDownload} className='w-full rnd-escape'>
Capture Canvas
</Button>
</Card>

View file

@ -1,12 +1,8 @@
import { useState } from "react";
import AuthContext from "./authContext";
import AuthContext from "./AuthContext";
const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState({
token: null,
userId: null,
currentProjectId: null,
});
const [user, setUser] = useState([]);
return (
<AuthContext.Provider value={{ user, setUser }}>

View file

@ -3,11 +3,14 @@ import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { useContext } from "react";
import { shapes } from "./shapes";
import { fabric } from "fabric";
import useProject from "@/hooks/useProject";
const CustomShape = () => {
const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const { projectData, projectUpdate, id } = useProject();
const addShape = (each) => {
// Load the SVG from the imported file
fabric.loadSVGFromURL(each, (objects, options) => {
@ -38,6 +41,11 @@ const CustomShape = () => {
// Render the canvas
canvas.renderAll();
const object = canvas.toJSON(['id', 'selectable']);
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
});
};

View file

@ -32,6 +32,10 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { useToast } from "@/hooks/use-toast";
import { useMutation } from "@tanstack/react-query";
import useProject from "@/hooks/useProject";
import { uploadImage } from "@/api/uploadApi";
const features = [
{
@ -68,6 +72,24 @@ const AddImageIntoShape = () => {
const fileInputRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const { toast } = useToast();
const { id, projectData, projectUpdate } = useProject();
// Upload image handler
const { mutate: uploadMutate } = useMutation({
mutationFn: async ({ file, id }) => await uploadImage({ file, id }),
onSuccess: (data) => {
if (data?.status === 200) {
toast({ title: data?.status, description: data?.message });
handleImageInsert(data?.data[0]?.url);
} else {
toast({ variant: "destructive", title: data?.status, description: data?.message });
}
},
});
const handleResize = (file, callback) => {
Resizer.imageFileResizer(
file,
@ -84,31 +106,20 @@ const AddImageIntoShape = () => {
};
const fileHandler = (e) => {
if (!activeObject) {
toast({ variant: "destructive", title: "No active object selected!" });
return;
}
const file = e.target.files[0];
if (!file) {
if (!file && activeObject) {
setErrorMessage("No file selected.");
return;
}
// Check if the file is an SVG
// Check if the file is an SVG (skip compression)
if (file.type === "image/svg+xml") {
// Add SVG directly to canvas without compression
const blobUrl = URL.createObjectURL(file);
const imgElement = new Image();
imgElement.src = blobUrl;
imgElement.onload = () => {
handleImageInsert(imgElement); // Insert the image without resizing
URL.revokeObjectURL(blobUrl);
toast({ variant: "destructive", title: "SVG files are not supported!" });
clearFileInput();
};
imgElement.onerror = () => {
console.error("Failed to load SVG.");
setErrorMessage("Failed to load SVG.");
URL.revokeObjectURL(blobUrl);
clearFileInput();
};
return;
}
@ -118,32 +129,16 @@ const AddImageIntoShape = () => {
imgElement.src = blobUrl;
imgElement.onload = () => {
// If the width is greater than 1080px, compress the image
if (imgElement.width > 1080) {
handleResize(file, (compressedFile) => {
const compressedBlobUrl = URL.createObjectURL(compressedFile);
const compressedImg = new Image();
compressedImg.src = compressedBlobUrl;
compressedImg.onload = () => {
handleImageInsert(compressedImg); // Insert the resized image
URL.revokeObjectURL(compressedBlobUrl); // Clean up
uploadMutate({ file: compressedFile, id }); // Fixed key name
clearFileInput();
};
compressedImg.onerror = () => {
console.error("Failed to load compressed image.");
setErrorMessage("Failed to load compressed image.");
URL.revokeObjectURL(compressedBlobUrl);
clearFileInput();
};
});
} else {
handleImageInsert(imgElement); // Insert the original image if no resizing needed
uploadMutate({ file, id }); // Direct upload if width is small
clearFileInput();
}
URL.revokeObjectURL(blobUrl); // Clean up
clearFileInput();
};
imgElement.onerror = () => {
@ -160,30 +155,79 @@ const AddImageIntoShape = () => {
}
};
const handleImageInsert = (img) => {
if (!activeObject) {
setErrorMessage("No active object selected!");
return;
}
// const handleImageInsert = (img) => {
// if (!activeObject) {
// setErrorMessage("No active object selected!");
// return;
// }
// // Ensure absolute positioning for the clipPath
// activeObject.set({
// isClipPath: true, // Custom property
// absolutePositioned: true,
// });
// // Calculate scale factors based on clip object size
// let scaleX = activeObject.width / img.width;
// let scaleY = activeObject.height / img.height;
// if (activeObject?.width < 100) {
// scaleX = 0.2;
// }
// if (activeObject.height < 100) {
// scaleY = 0.2;
// }
// // Create a fabric image object with scaling and clipPath
// const fabricImage = new fabric.Image.fromURL(img, {
// scaleX: scaleX,
// scaleY: scaleY,
// left: activeObject.left,
// top: activeObject.top,
// clipPath: activeObject, // Apply clipPath to the image
// originX: activeObject.originX, // Match origin point
// originY: activeObject.originY, // Match origin point
// }, { crossOrigin: "anonymous" });
// // Adjust position based on the clipPath's transformations
// fabricImage.set({
// left: activeObject.left,
// top: activeObject.top,
// angle: activeObject.angle, // Match rotation if any
// });
// canvas.add(fabricImage);
// canvas.setActiveObject(fabricImage);
// setActiveObject(fabricImage);
// canvas.renderAll();
// // Update the active object state
// projectUpdate({ id, updateData: { ...projectData?.data, object: canvas.toJSON(['id', 'selectable']), preview_url: "" } });
// };
const handleImageInsert = (imgUrl) => {
console.log(imgUrl);
// Ensure absolute positioning for the clipPath
activeObject.set({
isClipPath: true, // Custom property
absolutePositioned: true,
});
// Calculate scale factors based on clip object size
// Load the image asynchronously
fabric.Image.fromURL(
imgUrl,
(img) => {
// Ensure the image is fully loaded before applying transformations
let scaleX = activeObject.width / img.width;
let scaleY = activeObject.height / img.height;
if (activeObject?.width < 100) {
scaleX = 0.2;
}
if (activeObject.height < 100) {
scaleY = 0.2;
}
// Prevent the image from being too small
if (activeObject.width < 100) scaleX = 0.2;
if (activeObject.height < 100) scaleY = 0.2;
// Create a fabric image object with scaling and clipPath
const fabricImage = new fabric.Image(img, {
// Set image properties
img.set({
scaleX: scaleX,
scaleY: scaleY,
left: activeObject.left,
@ -191,54 +235,32 @@ const AddImageIntoShape = () => {
clipPath: activeObject, // Apply clipPath to the image
originX: activeObject.originX, // Match origin point
originY: activeObject.originY, // Match origin point
crossOrigin: "anonymous", // Ensure CORS handling
});
// Adjust position based on the clipPath's transformations
fabricImage.set({
left: activeObject.left,
top: activeObject.top,
angle: activeObject.angle, // Match rotation if any
});
canvas.add(fabricImage);
canvas.setActiveObject(fabricImage);
setActiveObject(fabricImage);
// Add image to canvas
canvas.add(img);
canvas.setActiveObject(img);
setActiveObject(img);
canvas.renderAll();
};
// canvas.remove(activeObject);
// Update project data
projectUpdate({
id,
updateData: {
...projectData?.data,
object: canvas.toJSON(["id", "selectable"]),
preview_url: "",
},
});
},
{ crossOrigin: "anonymous" }
);
};
const content = () => {
return (
<div>
{/* <Card className="my-2">
<CardContent className="p-0">
<Alert>
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">Image Insertion <ImagePlus className="h-5 w-5" /></AlertTitle>
<AlertDescription className="mt-1">
<p className="mb-1">
Insert and customize images within shapes. Adjust image position and clipping after insertion.
</p>
<h4 className="font-medium mb-1">Key Features:</h4>
<div className="grid grid-cols-1 gap-2 mt-2">
{features.map((feature, index) => (
<div
key={index}
className="flex items-start p-4 bg-white rounded-lg shadow-sm"
>
<feature.icon className="h-6 w-6 mr-3 flex-shrink-0 text-primary" />
<div>
<h3 className="font-semibold mb-1">{feature.title}</h3>
<p className="text-sm text-gray-600">{feature.description}</p>
</div>
</div>
))}
</div>
</AlertDescription>
</Alert>
</CardContent>
</Card> */}
<Card className="my-2">
<CardContent className="p-0">
<Alert>

View file

@ -242,7 +242,7 @@ const TextCustomization = () => {
<SelectTrigger className="min-w-[140px] h-8">
<SelectValue placeholder="Select a font" />
</SelectTrigger>
<SelectContent className="h-[250px]">
<SelectContent className="h-[250px] z-[9999]">
<SelectGroup>
{fonts.map((font) => (
<SelectItem
@ -258,6 +258,7 @@ const TextCustomization = () => {
</Select>
</a>
<Tooltip id="fonts" content="Font Family" place="bottom" />
{/* Font Size Controls */}
<div className="flex items-center border rounded-md">
<a data-tooltip-id="font-dec">
@ -452,45 +453,6 @@ const TextCustomization = () => {
</Popover>
<Tooltip id="spacing" content="Spacing" place="bottom" />
</div>
{/* Text Input */}
{/* <div className="space-y-2">
<Label htmlFor="text-content">Text Content</Label>
<Input
id="text-content"
value={text}
onChange={(e) => handleTextChange(e.target.value)}
/>
</div> */}
{/* Preview */}
{/* {previewText && (
<div className="p-4 border rounded-md overflow-hidden">
<p className="font-bold mb-2">Preview:</p>
<p
style={{
fontFamily,
fontSize: `${fontSize}px`,
fontStyle,
fontWeight,
lineHeight,
letterSpacing: `${charSpacing}px`,
textDecoration: `${underline ? "underline" : ""} ${
linethrough ? "line-through" : ""
}`,
textAlign,
}}
className="truncate"
>
{previewText}
</p>
</div>
)} */}
{/* Apply Changes Button */}
{/* <Button onClick={applyChanges} className="w-full">
Apply Changes
</Button> */}
</div>
);
};

View file

@ -1,97 +0,0 @@
import { Separator } from '../ui/separator';
import FlipCustomization from './Customization/FlipCustomization';
import RotateCustomization from './Customization/RotateCustomization';
import SkewCustomization from './Customization/SkewCustomization';
import ShadowCustomization from './Customization/ShadowCustomization';
import TextCustomization from './Customization/TextCustomization';
import AddImageIntoShape from './Customization/AddImageIntoShape';
import ScaleObjects from './Customization/ScaleObjects';
import ApplyColor from './ApplyColor';
import OpacityCustomization from './Customization/OpacityCustomization';
import StrokeCustomization from './Customization/StrokeCustomization';
import { useContext } from 'react';
import CanvasContext from '../Context/canvasContext/CanvasContext';
import { Card } from '../ui/card';
import PositionCustomization from './Customization/PositionCustomization';
import CollapsibleComponent from './Customization/CollapsibleComponent';
import ImageCustomization from './Customization/ImageCustomization';
import SelectObjectFromGroup from './Customization/SelectObjectFromGroup';
import LockObject from './Customization/LockObject';
const CustomizeShape = () => {
const { canvas } = useContext(CanvasContext);
const activeObject = canvas?.getActiveObject();
const activeObjectType = activeObject?.type;
const hasClipPath = !!activeObject?.clipPath;
const customClipPath = activeObject?.isClipPath;
// Return message if no active object is selected
if (!activeObject) {
return <p className='text-sm font-semibold'>No active object found</p>;
}
return (
<div className='p-1'>
<LockObject />
<Separator className="my-2" />
<SelectObjectFromGroup />
{/* Apply fill and background color */}
{(activeObjectType !== 'image' && !hasClipPath && !customClipPath) && <ApplyColor />}
{/* Apply stroke and stroke color */}
{(!customClipPath) && <><StrokeCustomization /><Separator className="my-2" /></>}
{
activeObject?.type !== "group" &&
<><PositionCustomization /><Separator className="my-2" />
</>
}
{/* Controls for opacity, flip, and rotation */}
<Card className="p-2">
<CollapsibleComponent text={"Opacity, Flip, Rotate Control"}>
<div className="space-y-2">
<OpacityCustomization />
<FlipCustomization />
<RotateCustomization />
</div>
</CollapsibleComponent>
</Card>
<Separator className="my-2" />
{/* Skew Customization */}
<SkewCustomization />
<Separator className="my-2" />
{/* Scale Objects */}
<ScaleObjects />
<Separator className="my-2" />
{/* Shadow Customization */}
<ShadowCustomization />
<Separator className="my-2" />
{/* Text Customization */}
{activeObjectType === 'i-text' && <TextCustomization />}
<Separator className="my-2" />
{/* Add image into shape */}
<AddImageIntoShape />
<Separator className="my-2" />
{/* Image Customization */}
{(activeObjectType === 'image' || hasClipPath) && (
<Card className="p-2">
<CollapsibleComponent text={"Image Customization"}>
<ImageCustomization />
</CollapsibleComponent>
</Card>
)}
</div>
)
}
export default CustomizeShape

View file

@ -6,6 +6,7 @@ import * as lucideIcons from "lucide-react";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { fabric } from "fabric";
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import useProject from "@/hooks/useProject";
import { useToast } from "@/hooks/use-toast";
const AllIconsPage = () => {
@ -15,6 +16,8 @@ const AllIconsPage = () => {
const { toast } = useToast();
const { projectData, projectUpdate, id } = useProject();
// Assume icons is already defined as shown previously, and filtered is created based on the search query
const icons = Object.entries(lucideIcons)?.filter(
([name, Icon]) => !name.includes("Icon") && Icon?.$$typeof
@ -85,6 +88,10 @@ const AllIconsPage = () => {
canvas.setActiveObject(iconGroup);
setActiveObject(iconGroup);
canvas.renderAll();
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
});
};

View file

@ -16,11 +16,14 @@ import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import { Card } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import useProject from "@/hooks/useProject";
const RoundedShape = () => {
const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const { projectData, projectUpdate, id } = useProject();
const shapes = [
{ icon: <ArrowBigRight />, name: "Arrow" },
{ icon: <Diamond />, name: "Diamond" },
@ -67,6 +70,11 @@ const RoundedShape = () => {
canvas.setActiveObject(iconGroup);
setActiveObject(iconGroup);
canvas.renderAll();
const object = canvas.toJSON(['id', 'selectable']);
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
});
};

View file

@ -6,11 +6,14 @@ import { fabric } from "fabric";
import { Card } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { Badge, Circle, Heart, Shield } from "lucide-react";
import useProject from "@/hooks/useProject";
const PlainShapes = () => {
const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const { projectData, projectUpdate, id } = useProject();
const shapes = [
{
icon: (
@ -298,6 +301,11 @@ const PlainShapes = () => {
canvas.setActiveObject(iconGroup);
setActiveObject(iconGroup);
canvas.renderAll();
const object = canvas.toJSON(['id', 'selectable']);
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
});
};

View file

@ -1,7 +1,6 @@
import { useContext, useEffect, useRef, useState } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "@/components/ui/button";
import { fabric } from "fabric";
import { ImageIcon, Trash2 } from "lucide-react";
import { Label } from "@/components/ui/label";
import {
@ -25,11 +24,9 @@ import { useDropzone } from "react-dropzone";
import ImageCustomization from "./Customization/ImageCustomization";
import { Separator } from "../ui/separator";
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
import { useNavigate, useParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { deleteImage, uploadImage } from "../../api/uploadApi";
import { createProject, getProjectById, updateProject } from "../../api/projectApi";
import { useToast } from "../../hooks/use-toast";
import useProject from "@/hooks/useProject";
import useImageHandler from "@/hooks/useImageHandler";
import { useToast } from "@/hooks/use-toast";
const UploadImage = () => {
const { canvas } = useContext(CanvasContext);
@ -40,146 +37,39 @@ const UploadImage = () => {
const [format, setFormat] = useState("JPEG");
const fileInputRef = useRef(null);
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
const { toast } = useToast();
const params = useParams();
const { id } = params;
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
const [file, setFile] = useState(null);
const [preview, setPreview] = useState(null);
const { toast } = useToast();
const navigate = useNavigate();
const { id } = useProject();
// create empty project if no id is provided
useEffect(() => {
const createEmptyProject = async () => {
try {
const response = await createProject();
if (response?.status === 200) {
toast({
title: response?.status,
description: response?.message
})
const removeFile = () => {
// Revoke the object URL to free up memory
if (preview) {
URL.revokeObjectURL(preview);
}
if (response?.data?.id) {
navigate(`/${response.data.id}`);
setFile(null);
setPreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
} catch (error) {
console.error("Project creation failed:", error);
if (activeObject?.type === "image") {
const imgUrl = activeObject?._originalElement?.currentSrc;
canvas.remove(activeObject);
setActiveObject(null);
canvas.renderAll();
deleteMutate(imgUrl);
}
};
if (!id) {
createEmptyProject();
}
}, [id, navigate, toast]);
const queryClient = useQueryClient();
// to get each canvas_project data
const { data: projectData } = useQuery({
queryKey: ['project', id],
queryFn: async () => await getProjectById(id),
enabled: !!id,
});
console.log(projectData);
// to update the project
const { mutate: projectUpdate } = useMutation({
mutationFn: async ({ id, updateData }) => {
return await updateProject({ id, ...updateData })
},
onSuccess: (data) => {
if (data?.status === 200) {
// Invalidate a single query key
queryClient.invalidateQueries({ queryKey: ["project", id] });
}
else {
toast({
title: data?.status,
description: data?.message,
variant: "destructive"
})
}
}
});
// upload image handler
const { mutate } = useMutation({
mutationFn: async ({ file, id }) => {
return await uploadImage({ file, id });
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: data?.status,
description: data?.message
});
fabric.Image.fromURL(
data?.data[0]?.url,
(img) => {
img.applyFilters();
img.scale(0.5);
img.set("top", canvas.width / 4);
img.set("left", canvas.height / 4);
canvas.add(img);
canvas.setActiveObject(img);
// Update the active object state
setActiveObject(img);
canvas.renderAll();
},
{ crossOrigin: "anonymous" }
);
if (canvas) {
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object, preview_url: "" };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
}
}
else {
toast({
variant: "destructive",
title: data?.status,
description: data?.message
});
removeFile();
}
}
});
// handle image remove
const { mutate: deleteMutate } = useMutation({
mutationFn: async (url) => {
return await deleteImage(url);
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: data?.status,
description: data?.message
})
if (canvas) {
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
}
}
else {
toast({
variant: "destructive",
title: data?.status,
description: data?.message
})
}
}
});
const { uploadMutate, deleteMutate } = useImageHandler({ removeFile });
const { getRootProps, getInputProps, isDragActive } = useDropzone({
accept: {
"image/*": [".jpeg", ".png", ".gif", ".jpg", ".webp", ".svg"],
"image/*": [".jpeg", ".png", ".jpg", ".webp"],
},
// maxSize: 5 * 1024 * 1024, // 5MB max file size
multiple: false,
@ -194,8 +84,13 @@ const UploadImage = () => {
setFile(selectedFile);
if (selectedFile.type === "image/svg+xml") {
addImageToCanvas(selectedFile);
toast({
title: "SVG images are not supported.",
description: "Please upload a different image format.",
variant: "destructive",
})
URL.revokeObjectURL(blobUrl);
return
} else {
const imgElement = new Image();
imgElement.src = blobUrl;
@ -219,25 +114,6 @@ const UploadImage = () => {
},
});
const removeFile = () => {
// Revoke the object URL to free up memory
if (preview) {
URL.revokeObjectURL(preview);
}
setFile(null);
setPreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
if (activeObject?.type === "image") {
const imgUrl = activeObject?._originalElement?.currentSrc;
canvas.remove(activeObject);
setActiveObject(null);
canvas.renderAll();
deleteMutate(imgUrl);
}
};
const handleResize = (selectedFile, callback) => {
const img = new Image();
img.src = URL.createObjectURL(selectedFile);
@ -277,7 +153,7 @@ const UploadImage = () => {
};
const addImageToCanvas = (selectedFile) => {
mutate({ file: selectedFile, id })
uploadMutate({ file: selectedFile, id })
};
// useEffect for preview update

View file

@ -23,6 +23,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { useMutation } from "@tanstack/react-query";
import { deleteImage } from "../api/uploadApi";
import { toast } from "../hooks/use-toast";
import useProject from "@/hooks/useProject";
export const ObjectShortcut = ({ value }) => {
const { canvas } = useContext(CanvasContext);
@ -34,6 +35,8 @@ export const ObjectShortcut = ({ value }) => {
const objectActive = canvas.getActiveObject();
const isGroupObject = objectActive && objectActive.type === "group";
const { projectData, projectUpdate, id } = useProject();
const groupSelectedObjects = () => {
const activeObjects = canvas.getActiveObjects(); // Get selected objects
if (activeObjects.length > 1) {
@ -53,6 +56,10 @@ export const ObjectShortcut = ({ value }) => {
canvas.setActiveObject(group);
setActiveObject(group);
canvas.renderAll();
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
} else {
toast({
title: "Select at least two objects",
@ -108,6 +115,11 @@ export const ObjectShortcut = ({ value }) => {
setActiveObject(selection);
canvas.renderAll();
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
}
};
@ -131,6 +143,10 @@ export const ObjectShortcut = ({ value }) => {
if (activeObject && !objectPosition.isAtFront) {
activeObject.bringToFront();
canvas.renderAll();
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
setIsPopoverOpen(false);
}
};
@ -139,6 +155,10 @@ export const ObjectShortcut = ({ value }) => {
if (activeObject && !objectPosition.isAtBack) {
activeObject.sendToBack();
canvas.renderAll();
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
setIsPopoverOpen(false);
}
};
@ -153,6 +173,12 @@ export const ObjectShortcut = ({ value }) => {
title: data?.status,
description: data?.message
})
if (canvas) {
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
}
}
else {
toast({
@ -178,14 +204,26 @@ export const ObjectShortcut = ({ value }) => {
if (activeObject) {
canvas.remove(activeObject);
setActiveObject(null);
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
}
if (activeObject && textObjects?.length === 1) {
canvas.remove(activeObject);
setActiveObject(null);
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
}
if (activeObject.length > 1) {
canvas.remove(...activeObject);
setActiveObject(null);
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
}
if (activeObject?.type === "image") {
@ -195,7 +233,7 @@ export const ObjectShortcut = ({ value }) => {
canvas.renderAll();
deleteMutate(imgUrl);
}
}, [canvas, setActiveObject, deleteMutate]);
}, [canvas, setActiveObject, deleteMutate, id, projectData, projectUpdate]);
// duplicating current objects
const duplicating = () => {
@ -205,6 +243,10 @@ export const ObjectShortcut = ({ value }) => {
clonedObject.set("left", clonedObject?.left + 30);
canvas.add(clonedObject);
canvas.renderAll();
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
});
};
@ -214,6 +256,10 @@ export const ObjectShortcut = ({ value }) => {
canvas.renderAll();
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
setActiveObject(null);
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
};
return (

View file

@ -14,10 +14,10 @@ const NotFound = () => {
<h2 className="text-2xl font-semibold mt-4 mb-2">Page Not Found</h2>
<p className="text-muted-foreground mb-8">Oops! The page you are looking for does not exist or has been moved.</p>
<a
href="https://dashboard.planpostai.com"
href="/"
className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90 h-10 py-2 px-4"
>
Go Back to Dashboard
Go Back to Home
</a>
</div>
<div className="absolute top-4 left-4 w-24 h-24 border-t-4 border-l-4 border-primary opacity-20"></div>

View file

@ -0,0 +1,32 @@
import { useContext } from 'react'
import CanvasContext from '../Context/canvasContext/CanvasContext';
import { Button } from '../ui/button';
import { ScrollArea } from '../ui/scroll-area';
import { Ellipsis, X } from 'lucide-react';
const DesignPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Design Panel</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
<div className='flex items-center gap-2'>
<p >Coming soon </p>
<Ellipsis className='animate-pulse animate-infinite' />
</div>
</ScrollArea>
</div>
);
}
export default DesignPanel

View file

@ -14,6 +14,7 @@ import GroupObjectPanel from "./GroupObjectPanel";
import CanvasPanel from "./CanvasPanel";
import { ProjectPanel } from "./ProjectPanel";
import ImageLibrary from "./ImageLibrary";
import DesignPanel from "./DesignPanel";
const EditorPanel = () => {
const { selectedPanel } = useContext(CanvasContext);
@ -48,6 +49,8 @@ const EditorPanel = () => {
return <ProjectPanel />;
case "image":
return <ImageLibrary />;
case "design":
return <DesignPanel />;
default:
return;
}

View file

@ -1,12 +1,27 @@
import { useContext } from "react";
import { useContext, useEffect, useRef } from "react";
import { Button } from "../ui/button";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { X } from "lucide-react";
import { ScrollArea } from "../ui/scroll-area";
import AllIconsPage from "../EachComponent/Icons/AllIcons";
import { useParams } from "react-router-dom";
import useProject from "@/hooks/useProject";
const IconPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
const params = useParams();
const { id } = params;
const { createEmptyProject } = useProject();
const hasCreatedProject = useRef(false);
useEffect(() => {
if (!id && !hasCreatedProject.current) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
}, [id, createEmptyProject]);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">

View file

@ -1,7 +1,7 @@
import { useContext } from 'react'
import CanvasContext from '../Context/canvasContext/CanvasContext';
import { Button } from '../ui/button';
import { X } from 'lucide-react';
import { Ellipsis, X } from 'lucide-react';
import { ScrollArea } from '../ui/scroll-area';
const ImageLibrary = () => {
const { setSelectedPanel } = useContext(CanvasContext);
@ -20,6 +20,10 @@ const ImageLibrary = () => {
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
{/* Image library content goes here */}
<div className='flex items-center gap-2'>
<p >Coming soon </p>
<Ellipsis className='animate-pulse animate-infinite' />
</div>
</ScrollArea>
</div>
);

View file

@ -3,16 +3,14 @@ import CanvasContext from '../Context/canvasContext/CanvasContext';
import { Button } from '../ui/button';
import { Loader, Trash, X } from 'lucide-react';
import { ScrollArea } from '../ui/scroll-area';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { deleteProject, getProjects } from '@/api/projectApi';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { 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 useProject from '@/hooks/useProject';
export const ProjectPanel = () => {
const { setSelectedPanel, canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const { setSelectedPanel } = useContext(CanvasContext);
const navigate = useNavigate();
const queryClient = useQueryClient(); // Initialize query client
@ -22,79 +20,20 @@ export const ProjectPanel = () => {
});
// To delete a project
const { mutate: projectDelete, isPending: deletePending } = useMutation({
mutationFn: async (id) => await deleteProject(id),
onSuccess: (data, id) => {
if (data?.status === 200) {
toast({
title: data?.status,
description: data?.message,
});
const { projectDelete, deletePending } = useProject();
// Invalidate queries to refresh data
queryClient.invalidateQueries({ queryKey: ["projects"] });
const handleNavigate = (id) => {
// Invalidate a single query key
queryClient.invalidateQueries({ queryKey: ["project", id] });
// Clear canvas if it exists
if (canvas) {
canvas.clear();
canvas.renderAll();
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
navigate(`/${id}`);
}
setActiveObject(null);
setSelectedPanel("");
navigate("/");
} else {
toast({
title: data?.status,
description: data?.message,
variant: "destructive"
});
}
},
onError: (error) => {
toast({
title: "Error",
description: error.message || "Failed to delete the project",
variant: "destructive",
});
}
});
const Row = ({ index, style }) => {
const project = filteredProjects[index];
return (
<div key={project?.id} className="flex flex-col gap-1 p-1 rounded-md border">
<div
className="rounded-md flex p-1 flex-col gap-1 bg-red-50 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>
<img className="rounded-md" src={project?.preview_url} alt="each_project" />
</div>
<Button
disabled={deletePending}
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">
<div className='relative'>
<div className="flex justify-between items-center p-4 border-b sticky top-0 z-[9999] bg-red-50">
<h2 className="text-lg font-semibold">Projects</h2>
<Button
variant="ghost"
@ -109,17 +48,33 @@ export const ProjectPanel = () => {
{
projectLoading && <p><Loader className="animate-spin mx-auto" /></p>
}
<div className='grid grid-cols-1 gap-2'>
{
!projectLoading && projectSuccess && projects?.status === 200 &&
<List
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
filteredProjects.map((project) => {
return (
<div key={project?.id} className="flex flex-col gap-1 p-1 rounded-md border">
<div
className="rounded-md flex p-1 flex-col gap-1 bg-red-50 hover:bg-red-100 cursor-pointer transition-all"
onClick={() => handleNavigate(project.id)}
>
{Row}
</List>
<p className="font-bold text-sm truncate">{project?.name}</p>
<p className="text-xs truncate">{project?.description} </p>
<img className="rounded-md" src={project?.preview_url} alt="each_project" />
</div>
<Button
disabled={deletePending}
className="w-fit p-1 ml-auto" size="small"
onClick={() => { projectDelete(project?.id) }}
>
<Trash className="h-4 w-4" />
</Button>
</div>
)
})
}
</div>
{
projects?.status !== 200 && <p>{projects?.message}</p>
}

View file

@ -1,4 +1,4 @@
import { useContext } from "react";
import { useContext, useEffect, useRef } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
@ -7,9 +7,24 @@ import { Separator } from "../ui/separator";
import CustomShape from "../EachComponent/CustomShape/CustomShape";
import RoundedShape from "../EachComponent/RoundedShapes/RoundedShape";
import PlainShapes from "../EachComponent/Shapes/PlainShapes";
import { useParams } from "react-router-dom";
import useProject from "@/hooks/useProject";
const ShapePanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
const params = useParams();
const { id } = params;
const { createEmptyProject } = useProject();
const hasCreatedProject = useRef(false);
useEffect(() => {
if (!id && !hasCreatedProject.current) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
}, [id, createEmptyProject]);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">

View file

@ -1,16 +1,30 @@
import { Button } from "../ui/Button";
import { X } from "lucide-react";
import { ScrollArea } from "../ui/scroll-area";
import { useContext, useEffect, useState } from "react";
import { useContext, useEffect, useRef, useState } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
import { fabric } from "fabric";
import CommonPanel from "./CommonPanel";
import useProject from "@/hooks/useProject";
import { useParams } from "react-router-dom";
export default function TextPanel() {
const { canvas, setSelectedPanel } = useContext(CanvasContext);
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
const params = useParams();
const { id } = params;
const { createEmptyProject, projectData, projectUpdate } = useProject();
const hasCreatedProject = useRef(false);
useEffect(() => {
if (!id && !hasCreatedProject.current) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
}, [id, createEmptyProject]);
const [open, setOpen] = useState(false);
useEffect(() => {
@ -37,6 +51,11 @@ export default function TextPanel() {
canvas.setActiveObject(text);
setActiveObject(text);
canvas.renderAll();
const object = canvas.toJSON(['id', 'selectable']);
const updateData = { ...projectData?.data, object };
// Wait for the project update before continuing
projectUpdate({ id, updateData });
}
};

View file

@ -1,12 +1,27 @@
import { useContext } from "react";
import { useContext, useEffect, useRef } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { X } from "lucide-react";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import UploadImage from "../EachComponent/UploadImage";
import { useParams } from "react-router-dom";
import useProject from "@/hooks/useProject";
const UploadPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
const params = useParams();
const { id } = params;
const { createEmptyProject } = useProject();
const hasCreatedProject = useRef(false);
useEffect(() => {
if (!id && !hasCreatedProject.current) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
}, [id, createEmptyProject]);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">

View file

@ -1,22 +1,21 @@
import { useContext, useEffect, useState } from 'react'
import CanvasContext from './Context/canvasContext/CanvasContext';
import { useNavigate, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { useToast } from '../hooks/use-toast';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Save, Trash2 } from 'lucide-react';
import { Textarea } from './ui/textarea';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { deleteProject, getProjectById, updateProject } from '@/api/projectApi';
import { useMutation } from '@tanstack/react-query';
import { Button } from './ui/button';
import { Separator } from './ui/separator';
import { deleteImage, uploadImage } from '@/api/uploadApi';
import ActiveObjectContext from './Context/activeObject/ObjectContext';
import useProject from '@/hooks/useProject';
import { captureCanvas } from '@/lib/captureCanvas';
import useCanvasCapture from '@/hooks/useCanvasCapture';
const SaveCanvas = () => {
const { canvas } = useContext(CanvasContext);
const { setSelectedPanel } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const [saveCanvas, setSaveCanvas] = useState({
name: "",
@ -27,89 +26,12 @@ const SaveCanvas = () => {
const params = useParams();
const { id } = params;
const { toast } = useToast();
const navigate = useNavigate();
const queryClient = useQueryClient();
// to get each canvas_project data
const { data: projectData, isLoading: projectLoading } = useQuery({
queryKey: ['project', id],
queryFn: async () => await getProjectById(id),
enabled: !!id,
});
// to update the project
const { mutate: projectUpdate, isPending } = useMutation({
mutationFn: async ({ id, projectData }) => {
return await updateProject({ id, ...projectData })
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: data?.status,
description: data?.message,
})
// Invalidate a single query key
queryClient.invalidateQueries({ queryKey: ["project", id] });
// Clear canvas if it exists
if (canvas) {
canvas.clear();
canvas.renderAll();
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
}
setActiveObject(null);
setSelectedPanel("");
navigate("/");
}
else {
toast({
title: data?.status,
description: data?.message,
variant: "destructive"
})
}
}
})
// to delete the project
const { mutate: projectDelete, isPending: deletePending } = useMutation({
mutationFn: async (id) => {
return await deleteProject(id)
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: data?.status,
description: data?.message,
})
// Clear canvas if it exists
if (canvas) {
canvas.clear();
canvas.renderAll();
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
}
setActiveObject(null);
// Invalidate a single query key
queryClient.invalidateQueries({ queryKey: ["project", id] });
setSelectedPanel("");
navigate("/");
}
else {
toast({
title: data?.status,
description: data?.message,
variant: "destructive"
})
}
}
})
const { projectDelete, deletePending, isLoading, projectData, projectUpdate, updatePending } = useProject();
// to set projectData into state
useEffect(() => {
if (projectData?.data && !projectLoading && projectData?.data?.preview_url !== null) {
if (projectData?.data && !isLoading && projectData?.data?.preview_url !== null) {
setSaveCanvas((prev) => ({
...prev,
name: projectData?.data?.name,
@ -117,60 +39,12 @@ const SaveCanvas = () => {
preview_url: projectData?.data?.preview_url
}));
}
}, [projectData, projectLoading]);
}, [projectData, isLoading]);
// upload preview-image handler
const { mutate: uploadCanvasImage, isPending: uploadCanvasPending } = useMutation({
mutationFn: async ({ file, id }) => {
return await uploadImage({ file, id });
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: data?.status,
description: data?.message
});
console.log(data?.data[0]?.url);
handleSaveWithPreViewImage({ preview_url: data?.data[0]?.url, name: saveCanvas?.name, description: saveCanvas?.description });
}
else {
toast({
variant: "destructive",
title: data?.status,
description: data?.message
});
}
}
})
const { uploadCanvasImage, uploadCanvasPending, removeCanvasImage, removeCanvasPending } =
useCanvasCapture({ handleSaveWithPreViewImage, canvas, id, saveCanvas });
// preview-image remove handler
const { mutate: removeCanvasImage, isPending: removeCanvasPending } = useMutation({
mutationFn: async (url) => {
return await deleteImage(url);
},
onSuccess: async (data) => {
console.log(data);
if (data?.status === 200) {
toast({
title: data?.status,
description: data?.message
})
const file = await captureImage();
if (file) {
uploadCanvasImage({ file, id });
}
}
else {
toast({
variant: "destructive",
title: data?.status,
description: data?.message
})
}
}
});
const handleSaveProject = async () => {
async function handleSaveProject() {
if (!saveCanvas?.name || saveCanvas?.name.trim() === "") {
toast({
title: "Name error",
@ -190,7 +64,7 @@ const SaveCanvas = () => {
}
} else {
try {
const file = await captureImage();
const file = await captureCanvas(canvas);
if (file) {
uploadCanvasImage({ file, id });
}
@ -201,7 +75,7 @@ const SaveCanvas = () => {
};
// this will save the canvas as a json object
const handleSaveWithPreViewImage = (body) => {
async function handleSaveWithPreViewImage(body) {
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
if (object?.objects?.length === 0) {
toast({
@ -210,63 +84,13 @@ const SaveCanvas = () => {
variant: "destructive"
});
} else {
const projectData = { ...body, object };
// Wait for the project update before continuing
projectUpdate({ id, projectData });
const updateData = { ...body, object };
projectUpdate({ id, updateData });
}
}
// this will capture canvas so that it will be saved as a preview_image for each project
const captureImage = async () => {
if (!canvas) return;
const width = canvas.width;
const height = canvas.height;
// Create a temporary canvas
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = width;
tempCanvas.height = height;
// Convert canvas content to data URL
const dataUrl = canvas.toDataURL('image/jpeg', 1);
return new Promise((resolve, reject) => {
const img = new Image();
// Set cross-origin attribute to avoid CORS issues
img.crossOrigin = "anonymous";
img.src = dataUrl;
img.onload = () => {
tempCtx.drawImage(img, 0, 0, width, height);
// Generate final image as JPEG
const resizedDataUrl = tempCanvas.toDataURL('image/jpeg', 1);
// Convert the data URL to Blob
fetch(resizedDataUrl)
.then(res => res.blob())
.then(blob => {
const file = new File([blob], `PlanPostAi-capture.jpg`, {
type: 'image/jpeg',
});
resolve(file);
})
.catch(reject);
};
img.onerror = (error) => {
console.error("Image loading error:", error);
reject(error);
};
});
};
const handleDeleteProject = () => {
projectDelete(id);
projectDelete();
}
return (
@ -301,8 +125,8 @@ const SaveCanvas = () => {
<Separator className='my-2' />
<div className='flex my-2 gap-2 justify-end'>
<Button disabled={deletePending || projectLoading || uploadCanvasPending || removeCanvasPending} onClick={handleDeleteProject}> <Trash2 /> </Button>
<Button disabled={isPending || projectLoading} onClick={handleSaveProject}>Save</Button>
<Button disabled={deletePending || isLoading || uploadCanvasPending || removeCanvasPending} onClick={handleDeleteProject}> <Trash2 /> </Button>
<Button disabled={isLoading || updatePending} onClick={handleSaveProject}>Save</Button>
</div>
</div>
)

View file

@ -11,7 +11,7 @@ const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[1000] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
"fixed top-0 z-[9999] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props} />

View file

@ -0,0 +1,50 @@
import { deleteImage, uploadImage } from "@/api/uploadApi";
import { useMutation } from "@tanstack/react-query";
import { useToast } from "./use-toast";
import { captureCanvas } from "@/lib/captureCanvas";
const useCanvasCapture = ({ handleSaveWithPreViewImage, canvas, id, saveCanvas }) => {
const { toast } = useToast();
// Upload Image Mutation
const { mutate: uploadCanvasImage, isPending: uploadCanvasPending } = useMutation({
mutationFn: async ({ file, id }) => await uploadImage({ file, id }),
onSuccess: (data) => {
toast({
title: data?.status,
description: data?.message,
variant: data?.status === 200 ? "default" : "destructive",
});
if (data?.status === 200) {
handleSaveWithPreViewImage({
preview_url: data?.data[0]?.url,
name: saveCanvas?.name,
description: saveCanvas?.description,
});
}
},
});
// Remove Image Mutation
const { mutate: removeCanvasImage, isPending: removeCanvasPending } = useMutation({
mutationFn: async (url) => await deleteImage(url),
onSuccess: async (data) => {
toast({
title: data?.status,
description: data?.message,
variant: data?.status === 200 ? "default" : "destructive",
});
if (data?.status === 200) {
const file = await captureCanvas(canvas);
if (file) {
uploadCanvasImage({ file, id });
}
}
},
});
return { uploadCanvasImage, uploadCanvasPending, removeCanvasImage, removeCanvasPending };
}
export default useCanvasCapture

View file

@ -0,0 +1,64 @@
import { useMutation } from "@tanstack/react-query";
import { useContext } from "react";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { fabric } from "fabric";
import useProject from "./useProject";
import { useToast } from "./use-toast";
import { deleteImage, uploadImage } from "@/api/uploadApi";
const useImageHandler = ({ removeFile }) => {
const { toast } = useToast();
const { canvas } = useContext(CanvasContext);
const { projectData, projectUpdate } = useProject();
// Upload image handler
const { mutate: uploadMutate } = useMutation({
mutationFn: async ({ file, id }) => await uploadImage({ file, id }),
onSuccess: (data, { id }) => {
if (data?.status === 200) {
toast({ title: data?.status, description: data?.message });
fabric.Image.fromURL(
data?.data[0]?.url,
(img) => {
img.applyFilters();
img.scale(0.5);
img.set("top", canvas.width / 4);
img.set("left", canvas.height / 4);
canvas.add(img);
canvas.setActiveObject(img);
// Update the active object state
projectUpdate({ id, updateData: { ...projectData?.data, object: canvas.toJSON(['id', 'selectable']), preview_url: "" } });
canvas.renderAll();
},
{ crossOrigin: "anonymous" }
);
} else {
toast({ variant: "destructive", title: data?.status, description: data?.message });
removeFile();
}
},
});
// Remove image handler
const { mutate: deleteMutate } = useMutation({
mutationFn: async (url) => await deleteImage(url),
onSuccess: (data, { id }) => {
if (data?.status === 200) {
toast({ title: data?.status, description: data?.message });
if (canvas) {
projectUpdate({ id, updateData: { ...projectData?.data, object: canvas.toJSON(['id', 'selectable']) } });
}
} else {
toast({ variant: "destructive", title: data?.status, description: data?.message });
}
},
});
return { uploadMutate, deleteMutate };
};
export default useImageHandler;

122
src/hooks/useProject.jsx Normal file
View file

@ -0,0 +1,122 @@
import { useNavigate, useParams } from "react-router-dom";
import { useToast } from "./use-toast";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createProject, deleteProject, getProjectById, updateProject } from "@/api/projectApi";
import { useContext } from "react";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
const useProject = () => {
const { toast } = useToast();
const { canvas, setSelectedPanel, selectedPanel } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const params = useParams();
const { id } = params;
const navigate = useNavigate();
const queryClient = useQueryClient();
const createEmptyProject = async () => {
try {
const response = await createProject();
if (response?.status === 200) {
toast({
title: response?.status,
description: response?.message,
});
}
if (response?.data?.id) {
navigate(`/${response.data.id}`);
}
} catch (error) {
console.error("Project creation failed:", error);
}
};
// Fetch project data
const { data: projectData, isLoading } = useQuery({
queryKey: ["project", id],
queryFn: async () => await getProjectById(id),
enabled: !!id,
});
// Update project
const { mutate: projectUpdate, isPending: updatePending } = useMutation({
mutationFn: async ({ id, updateData }) => {
return await updateProject({ id, ...updateData });
},
onSuccess: (data) => {
if (data?.status === 200) {
if (selectedPanel === "canvas") {
toast({
title: data?.status,
description: data?.message,
})
setSelectedPanel("");
queryClient.invalidateQueries({ queryKey: ["project", id] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
if (canvas) {
canvas.clear();
canvas.renderAll();
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
}
setActiveObject(null);
navigate("/");
}
queryClient.invalidateQueries({ queryKey: ["project", id] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
} else {
toast({
title: data?.status,
description: data?.message,
variant: "destructive",
});
}
},
});
const { mutate: projectDelete, isPending: deletePending } = useMutation({
mutationFn: async (id) => await deleteProject(id),
onSuccess: (data, id) => {
if (data?.status === 200) {
toast({
title: data?.status,
description: data?.message,
});
// Invalidate queries to refresh data
queryClient.invalidateQueries({ queryKey: ["projects"] });
queryClient.invalidateQueries({ queryKey: ["project", id] });
// Clear canvas if it exists
if (canvas) {
canvas.clear();
canvas.renderAll();
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
}
setActiveObject(null);
setSelectedPanel("");
navigate("/");
} else {
toast({
title: data?.status,
description: data?.message,
variant: "destructive"
});
}
},
onError: (error) => {
toast({
title: "Error",
description: error.message || "Failed to delete the project",
variant: "destructive",
});
}
});
return { projectData, isLoading, projectUpdate, id, projectDelete, deletePending, updatePending, createEmptyProject };
};
export default useProject;

33
src/lib/captureCanvas.js Normal file
View file

@ -0,0 +1,33 @@
export const captureCanvas = async (canvas) => {
if (!canvas) return;
const width = canvas.width;
const height = canvas.height;
const tempCanvas = document.createElement("canvas");
const tempCtx = tempCanvas.getContext("2d");
tempCanvas.width = width;
tempCanvas.height = height;
const dataUrl = canvas.toDataURL("image/jpeg", 1);
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = dataUrl;
img.onload = () => {
tempCtx.drawImage(img, 0, 0, width, height);
fetch(tempCanvas.toDataURL("image/jpeg", 1))
.then((res) => res.blob())
.then((blob) => {
resolve(new File([blob], "PlanPostAi-capture.jpg", { type: "image/jpeg" }));
})
.catch(reject);
};
img.onerror = (error) => reject(error);
});
};