From 1ed96799a08ebc058c067e508ccc32e2a181e677 Mon Sep 17 00:00:00 2001 From: Saimon8420 Date: Tue, 11 Feb 2025 17:55:34 +0600 Subject: [PATCH] download count added, code optimized --- src/App.jsx | 1 + src/Home.jsx | 109 +++++---- src/api/authApi.ts | 24 ++ src/api/downloadApi.ts | 27 +++ src/components/CanvasCapture.jsx | 35 ++- .../Context/authContext/AuthProvider.jsx | 8 +- .../EachComponent/CustomShape/CustomShape.jsx | 8 + .../Customization/AddImageIntoShape.jsx | 226 ++++++++++-------- .../Customization/TextCustomization.jsx | 42 +--- .../EachComponent/CustomizeShape.jsx | 97 -------- .../EachComponent/Icons/AllIcons.jsx | 7 + .../RoundedShapes/RoundedShape.jsx | 8 + .../EachComponent/Shapes/PlainShapes.jsx | 8 + src/components/EachComponent/UploadImage.jsx | 184 +++----------- src/components/ObjectShortcut.jsx | 48 +++- src/components/Pages/NotFound.jsx | 4 +- src/components/Panel/DesignPanel.jsx | 32 +++ src/components/Panel/EditorPanel.jsx | 3 + src/components/Panel/IconPanel.jsx | 17 +- src/components/Panel/ImageLibrary.jsx | 6 +- src/components/Panel/ProjectPanel.jsx | 125 ++++------ src/components/Panel/ShapePanel.jsx | 17 +- src/components/Panel/TextPanel.jsx | 21 +- src/components/Panel/UploadPanel.jsx | 17 +- src/components/SaveCanvas.jsx | 212 ++-------------- src/components/ui/toast.jsx | 2 +- src/hooks/useCanvasCapture.jsx | 50 ++++ src/hooks/useImageHandler.jsx | 64 +++++ src/hooks/useProject.jsx | 122 ++++++++++ src/lib/captureCanvas.js | 33 +++ 30 files changed, 828 insertions(+), 729 deletions(-) create mode 100644 src/api/downloadApi.ts delete mode 100644 src/components/EachComponent/CustomizeShape.jsx create mode 100644 src/components/Panel/DesignPanel.jsx create mode 100644 src/hooks/useCanvasCapture.jsx create mode 100644 src/hooks/useImageHandler.jsx create mode 100644 src/hooks/useProject.jsx create mode 100644 src/lib/captureCanvas.js diff --git a/src/App.jsx b/src/App.jsx index 525f4fb..47392ec 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -71,6 +71,7 @@ function App() { } /> } /> } /> + } /> ); } diff --git a/src/Home.jsx b/src/Home.jsx index 3cfe1b5..421812f 100644 --- a/src/Home.jsx +++ b/src/Home.jsx @@ -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,50 +62,62 @@ 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") { - canvas.loadFromJSON(projectData?.data?.object); - canvas.renderAll(); + if (projectData?.status === 404 || projectData?.status === 500) { + navigate("/"); + toast({ variant: "destructive", title: projectData?.status, description: "No project found" }); } - }, [navigate, isLoading, data, projectData, projectLoading, canvas, selectedPanel]); + 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, id, toast]); return ( -
+
{ - isLoading &&

+ isLoading && +
+

+
} +
- {!isLoading && - <> - + {!isLoading && + <> + - { - activeObject && -
- + { + activeObject && +
+ +
+ } + +
+
- } -
- -
- -
- -
- -
- -
- -
-
- +
+
- {/* canvas capture part */} - -
- - } + +
+ +
+ +
+
+ +
+ {/* canvas capture part */} + +
+ + } +
) } diff --git a/src/api/authApi.ts b/src/api/authApi.ts index daaaef4..9df68bb 100644 --- a/src/api/authApi.ts +++ b/src/api/authApi.ts @@ -21,4 +21,28 @@ export const generateToken = async (id: string) => { console.error('Un-authenticated:', error); 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); + } } \ No newline at end of file diff --git a/src/api/downloadApi.ts b/src/api/downloadApi.ts new file mode 100644 index 0000000..0e68ead --- /dev/null +++ b/src/api/downloadApi.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/components/CanvasCapture.jsx b/src/components/CanvasCapture.jsx index 56167e5..4ac3f6d 100644 --- a/src/components/CanvasCapture.jsx +++ b/src/components/CanvasCapture.jsx @@ -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 = () => {
- diff --git a/src/components/Context/authContext/AuthProvider.jsx b/src/components/Context/authContext/AuthProvider.jsx index d1d8fe6..d49e63d 100644 --- a/src/components/Context/authContext/AuthProvider.jsx +++ b/src/components/Context/authContext/AuthProvider.jsx @@ -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 ( diff --git a/src/components/EachComponent/CustomShape/CustomShape.jsx b/src/components/EachComponent/CustomShape/CustomShape.jsx index 7b071ea..1dbf914 100644 --- a/src/components/EachComponent/CustomShape/CustomShape.jsx +++ b/src/components/EachComponent/CustomShape/CustomShape.jsx @@ -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 }); }); }; diff --git a/src/components/EachComponent/Customization/AddImageIntoShape.jsx b/src/components/EachComponent/Customization/AddImageIntoShape.jsx index d345e9d..c095edc 100644 --- a/src/components/EachComponent/Customization/AddImageIntoShape.jsx +++ b/src/components/EachComponent/Customization/AddImageIntoShape.jsx @@ -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); - clearFileInput(); - }; - - imgElement.onerror = () => { - console.error("Failed to load SVG."); - setErrorMessage("Failed to load SVG."); - URL.revokeObjectURL(blobUrl); - clearFileInput(); - }; + toast({ variant: "destructive", title: "SVG files are not supported!" }); + 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 - clearFileInput(); - }; - - compressedImg.onerror = () => { - console.error("Failed to load compressed image."); - setErrorMessage("Failed to load compressed image."); - URL.revokeObjectURL(compressedBlobUrl); - clearFileInput(); - }; + uploadMutate({ file: compressedFile, id }); // Fixed key name + 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,85 +155,112 @@ 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 - let scaleX = activeObject.width / img.width; - let scaleY = activeObject.height / img.height; - if (activeObject?.width < 100) { - scaleX = 0.2; - } + // 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.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, { - 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 - }); + // Set image properties + img.set({ + 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", // 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 - }); + // Add image to canvas + canvas.add(img); + canvas.setActiveObject(img); + setActiveObject(img); + canvas.renderAll(); - canvas.add(fabricImage); - canvas.setActiveObject(fabricImage); - setActiveObject(fabricImage); - canvas.renderAll(); + // Update project data + projectUpdate({ + id, + updateData: { + ...projectData?.data, + object: canvas.toJSON(["id", "selectable"]), + preview_url: "", + }, + }); + }, + { crossOrigin: "anonymous" } + ); }; - // canvas.remove(activeObject); - const content = () => { return (
- {/* - - - Image Insertion - -

- Insert and customize images within shapes. Adjust image position and clipping after insertion. -

-

Key Features:

-
- {features.map((feature, index) => ( -
- -
-

{feature.title}

-

{feature.description}

-
-
- ))} -
-
-
-
-
*/} - diff --git a/src/components/EachComponent/Customization/TextCustomization.jsx b/src/components/EachComponent/Customization/TextCustomization.jsx index b440c04..9c40f43 100644 --- a/src/components/EachComponent/Customization/TextCustomization.jsx +++ b/src/components/EachComponent/Customization/TextCustomization.jsx @@ -242,7 +242,7 @@ const TextCustomization = () => { - + {fonts.map((font) => ( { + {/* Font Size Controls */} - - {/* Text Input */} - {/*
- - handleTextChange(e.target.value)} - /> -
*/} - - {/* Preview */} - {/* {previewText && ( -
-

Preview:

-

- {previewText} -

-
- )} */} - - {/* Apply Changes Button */} - {/* */}
); }; diff --git a/src/components/EachComponent/CustomizeShape.jsx b/src/components/EachComponent/CustomizeShape.jsx deleted file mode 100644 index 24c91df..0000000 --- a/src/components/EachComponent/CustomizeShape.jsx +++ /dev/null @@ -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

No active object found

; - } - - return ( -
- - - - - - - {/* Apply fill and background color */} - {(activeObjectType !== 'image' && !hasClipPath && !customClipPath) && } - - {/* Apply stroke and stroke color */} - {(!customClipPath) && <>} - - { - activeObject?.type !== "group" && - <> - - } - - {/* Controls for opacity, flip, and rotation */} - - -
- - - -
-
-
- - - {/* Skew Customization */} - - - - {/* Scale Objects */} - - - - {/* Shadow Customization */} - - - - {/* Text Customization */} - {activeObjectType === 'i-text' && } - - - {/* Add image into shape */} - - - - {/* Image Customization */} - {(activeObjectType === 'image' || hasClipPath) && ( - - - - - - )} -
- ) -} - -export default CustomizeShape \ No newline at end of file diff --git a/src/components/EachComponent/Icons/AllIcons.jsx b/src/components/EachComponent/Icons/AllIcons.jsx index 75e6013..7ccd4b1 100644 --- a/src/components/EachComponent/Icons/AllIcons.jsx +++ b/src/components/EachComponent/Icons/AllIcons.jsx @@ -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 }); }); }; diff --git a/src/components/EachComponent/RoundedShapes/RoundedShape.jsx b/src/components/EachComponent/RoundedShapes/RoundedShape.jsx index e512138..57c8bdb 100644 --- a/src/components/EachComponent/RoundedShapes/RoundedShape.jsx +++ b/src/components/EachComponent/RoundedShapes/RoundedShape.jsx @@ -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: , name: "Arrow" }, { icon: , 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 }); }); }; diff --git a/src/components/EachComponent/Shapes/PlainShapes.jsx b/src/components/EachComponent/Shapes/PlainShapes.jsx index f62eed3..ac6e876 100644 --- a/src/components/EachComponent/Shapes/PlainShapes.jsx +++ b/src/components/EachComponent/Shapes/PlainShapes.jsx @@ -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 }); }); }; diff --git a/src/components/EachComponent/UploadImage.jsx b/src/components/EachComponent/UploadImage.jsx index 6f9960f..128c1f1 100644 --- a/src/components/EachComponent/UploadImage.jsx +++ b/src/components/EachComponent/UploadImage.jsx @@ -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 - }) - } - if (response?.data?.id) { - navigate(`/${response.data.id}`); - } - } catch (error) { - console.error("Project creation failed:", error); - } - }; - if (!id) { - createEmptyProject(); + const removeFile = () => { + // Revoke the object URL to free up memory + if (preview) { + URL.revokeObjectURL(preview); } - }, [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" - }) - } + setFile(null); + setPreview(null); + if (fileInputRef.current) { + fileInputRef.current.value = ""; } - }); - - // 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(); - } + if (activeObject?.type === "image") { + const imgUrl = activeObject?._originalElement?.currentSrc; + canvas.remove(activeObject); + setActiveObject(null); + canvas.renderAll(); + deleteMutate(imgUrl); } - }); + }; - // 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 diff --git a/src/components/ObjectShortcut.jsx b/src/components/ObjectShortcut.jsx index f2a09c4..b9fc87c 100644 --- a/src/components/ObjectShortcut.jsx +++ b/src/components/ObjectShortcut.jsx @@ -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 ( diff --git a/src/components/Pages/NotFound.jsx b/src/components/Pages/NotFound.jsx index 7544d85..da8978c 100644 --- a/src/components/Pages/NotFound.jsx +++ b/src/components/Pages/NotFound.jsx @@ -14,10 +14,10 @@ const NotFound = () => {

Page Not Found

Oops! The page you are looking for does not exist or has been moved.

- Go Back to Dashboard + Go Back to Home
diff --git a/src/components/Panel/DesignPanel.jsx b/src/components/Panel/DesignPanel.jsx new file mode 100644 index 0000000..168fdb5 --- /dev/null +++ b/src/components/Panel/DesignPanel.jsx @@ -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 ( +
+
+

Design Panel

+ +
+ + +
+

Coming soon

+ +
+
+
+ ); +} + +export default DesignPanel \ No newline at end of file diff --git a/src/components/Panel/EditorPanel.jsx b/src/components/Panel/EditorPanel.jsx index 3180b83..26ec807 100644 --- a/src/components/Panel/EditorPanel.jsx +++ b/src/components/Panel/EditorPanel.jsx @@ -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 ; case "image": return ; + case "design": + return ; default: return; } diff --git a/src/components/Panel/IconPanel.jsx b/src/components/Panel/IconPanel.jsx index ab989f7..f9a5c5a 100644 --- a/src/components/Panel/IconPanel.jsx +++ b/src/components/Panel/IconPanel.jsx @@ -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 (
diff --git a/src/components/Panel/ImageLibrary.jsx b/src/components/Panel/ImageLibrary.jsx index f5a2e76..8046056 100644 --- a/src/components/Panel/ImageLibrary.jsx +++ b/src/components/Panel/ImageLibrary.jsx @@ -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 = () => { {/* Image library content goes here */} +
+

Coming soon

+ +
); diff --git a/src/components/Panel/ProjectPanel.jsx b/src/components/Panel/ProjectPanel.jsx index a16a90a..eec1779 100644 --- a/src/components/Panel/ProjectPanel.jsx +++ b/src/components/Panel/ProjectPanel.jsx @@ -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, - }); - - // 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", - }); - } - }); - - const Row = ({ index, style }) => { - const project = filteredProjects[index]; - - return ( -
-
navigate(`/${project.id}`)} - > -

{project?.name}

-

{project?.description}

- each_project -
- - -
- ); - }; + const { projectDelete, deletePending } = useProject(); + const handleNavigate = (id) => { + // Invalidate a single query key + queryClient.invalidateQueries({ queryKey: ["project", id] }); + navigate(`/${id}`); + } // Filter projects where preview_url is not empty or null const filteredProjects = projects?.data?.filter(project => project?.preview_url) || []; return ( -
-
+
+

Projects

+
+ ) + }) + } +
{ projects?.status !== 200 &&

{projects?.message}

} diff --git a/src/components/Panel/ShapePanel.jsx b/src/components/Panel/ShapePanel.jsx index ef197a1..2e664d9 100644 --- a/src/components/Panel/ShapePanel.jsx +++ b/src/components/Panel/ShapePanel.jsx @@ -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 (
diff --git a/src/components/Panel/TextPanel.jsx b/src/components/Panel/TextPanel.jsx index 95e2e83..dfcfaf1 100644 --- a/src/components/Panel/TextPanel.jsx +++ b/src/components/Panel/TextPanel.jsx @@ -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 }); } }; diff --git a/src/components/Panel/UploadPanel.jsx b/src/components/Panel/UploadPanel.jsx index 1b9a79e..fa5bc45 100644 --- a/src/components/Panel/UploadPanel.jsx +++ b/src/components/Panel/UploadPanel.jsx @@ -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 (
diff --git a/src/components/SaveCanvas.jsx b/src/components/SaveCanvas.jsx index eb1ac24..067140b 100644 --- a/src/components/SaveCanvas.jsx +++ b/src/components/SaveCanvas.jsx @@ -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 = () => {
- - + +
) diff --git a/src/components/ui/toast.jsx b/src/components/ui/toast.jsx index 2065882..d530f66 100644 --- a/src/components/ui/toast.jsx +++ b/src/components/ui/toast.jsx @@ -11,7 +11,7 @@ const ToastViewport = React.forwardRef(({ className, ...props }, ref) => ( diff --git a/src/hooks/useCanvasCapture.jsx b/src/hooks/useCanvasCapture.jsx new file mode 100644 index 0000000..5ab7391 --- /dev/null +++ b/src/hooks/useCanvasCapture.jsx @@ -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 \ No newline at end of file diff --git a/src/hooks/useImageHandler.jsx b/src/hooks/useImageHandler.jsx new file mode 100644 index 0000000..89b48ce --- /dev/null +++ b/src/hooks/useImageHandler.jsx @@ -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; diff --git a/src/hooks/useProject.jsx b/src/hooks/useProject.jsx new file mode 100644 index 0000000..b081e42 --- /dev/null +++ b/src/hooks/useProject.jsx @@ -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; diff --git a/src/lib/captureCanvas.js b/src/lib/captureCanvas.js new file mode 100644 index 0000000..9d34676 --- /dev/null +++ b/src/lib/captureCanvas.js @@ -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); + }); +};