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);
+ });
+};