download count added, code optimized
This commit is contained in:
parent
20526b3628
commit
1ed96799a0
30 changed files with 828 additions and 729 deletions
|
|
@ -71,6 +71,7 @@ function App() {
|
||||||
<Route path="/:id" element={<Home />} />
|
<Route path="/:id" element={<Home />} />
|
||||||
<Route path="/unAuthenticated" element={<Unauthenticated />} />
|
<Route path="/unAuthenticated" element={<Unauthenticated />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
|
<Route path="/notFound" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
109
src/Home.jsx
109
src/Home.jsx
|
|
@ -8,16 +8,19 @@ import Canvas from './components/Canvas/Canvas';
|
||||||
import CanvasCapture from './components/CanvasCapture';
|
import CanvasCapture from './components/CanvasCapture';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { generateToken } from './api/authApi';
|
import { generateToken, getUser } from './api/authApi';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Toaster } from '@/components/ui/toaster';
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
import { getProjectById } from './api/projectApi';
|
|
||||||
import { Loader } from 'lucide-react';
|
import { Loader } from 'lucide-react';
|
||||||
import CanvasContext from './components/Context/canvasContext/CanvasContext';
|
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 = () => {
|
export const Home = () => {
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
const { canvas, selectedPanel } = useContext(CanvasContext);
|
const { canvas, selectedPanel } = useContext(CanvasContext);
|
||||||
|
const { user, setUser } = useContext(AuthContext);
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
@ -25,6 +28,8 @@ export const Home = () => {
|
||||||
|
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Fetch token only if it doesn't exist
|
// Fetch token only if it doesn't exist
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['get-token'],
|
queryKey: ['get-token'],
|
||||||
|
|
@ -32,11 +37,21 @@ export const Home = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch project data only if token and id exist
|
// Fetch project data only if token and id exist
|
||||||
const { data: projectData, isLoading: projectLoading } = useQuery({
|
const { projectData, isLoading: projectLoading } = useProject();
|
||||||
queryKey: ['project', id],
|
|
||||||
queryFn: async () => await getProjectById(id),
|
// Fetch user related data only if token exists
|
||||||
enabled: !!getToken() && !!id && selectedPanel === "project",
|
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(() => {
|
useEffect(() => {
|
||||||
const token = getToken(); // Get latest token
|
const token = getToken(); // Get latest token
|
||||||
|
|
@ -47,50 +62,62 @@ export const Home = () => {
|
||||||
if (token && !isLoading && data?.status === 201) {
|
if (token && !isLoading && data?.status === 201) {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
if (projectData?.status === 200 & !projectLoading && projectData?.data?.object && canvas && canvas?._objects?.length === 0 && selectedPanel === "project") {
|
if (projectData?.status === 404 || projectData?.status === 500) {
|
||||||
canvas.loadFromJSON(projectData?.data?.object);
|
navigate("/");
|
||||||
canvas.renderAll();
|
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 (
|
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 &&
|
{!isLoading &&
|
||||||
<>
|
<>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|
||||||
{
|
{
|
||||||
activeObject &&
|
activeObject &&
|
||||||
<div className="absolute left-[90px] right-[90px] z-[9999] rounded-md p-1 h-fit bg-white border-t border-gray-200 shadow-md my-1 w-[80%] mx-auto">
|
<div className="absolute left-[90px] right-[90px] z-[9999] rounded-md p-1 h-fit bg-white border-t border-gray-200 shadow-md my-1 w-[80%] mx-auto">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<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>
|
</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="h-full mr-1">
|
||||||
<ActionButtons />
|
<Sidebar />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="h-full mr-1">
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="absolute ml-20 z-[999] top-[15%]">
|
|
||||||
<EditorPanel />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col h-full overflow-hidden my-2">
|
|
||||||
<div className="flex-1 overflow-auto">
|
|
||||||
<Canvas />
|
|
||||||
</div>
|
</div>
|
||||||
{/* canvas capture part */}
|
|
||||||
<CanvasCapture />
|
<div className="absolute ml-20 z-[999] top-[15%]">
|
||||||
</div>
|
<EditorPanel />
|
||||||
</>
|
</div>
|
||||||
}
|
|
||||||
|
<div className="flex-1 flex flex-col h-full overflow-hidden my-2">
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<Canvas />
|
||||||
|
</div>
|
||||||
|
{/* canvas capture part */}
|
||||||
|
<CanvasCapture />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,28 @@ export const generateToken = async (id: string) => {
|
||||||
console.error('Un-authenticated:', error);
|
console.error('Un-authenticated:', error);
|
||||||
throw 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
27
src/api/downloadApi.ts
Normal file
27
src/api/downloadApi.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,9 @@ import { Card, CardHeader, CardTitle } from './ui/card';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
import { Separator } from './ui/separator';
|
import { Separator } from './ui/separator';
|
||||||
import RndComponent from './Layouts/RndComponent';
|
import RndComponent from './Layouts/RndComponent';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { downloadCount } from '@/api/downloadApi';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
const resolutions = [
|
const resolutions = [
|
||||||
{ value: '720p', label: 'HD (1280x720)', width: 1280, height: 720 },
|
{ value: '720p', label: 'HD (1280x720)', width: 1280, height: 720 },
|
||||||
|
|
@ -30,6 +33,8 @@ const CanvasCapture = () => {
|
||||||
const [resolution, setResolution] = useState(resolutions[0].value);
|
const [resolution, setResolution] = useState(resolutions[0].value);
|
||||||
const [imageType, setImageType] = useState(imageTypes[0].value);
|
const [imageType, setImageType] = useState(imageTypes[0].value);
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { captureOpen, setCaptureOpen } = useContext(OpenContext);
|
const { captureOpen, setCaptureOpen } = useContext(OpenContext);
|
||||||
|
|
||||||
|
|
@ -85,6 +90,34 @@ const CanvasCapture = () => {
|
||||||
bound: "parent"
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{captureOpen && (
|
{captureOpen && (
|
||||||
|
|
@ -131,7 +164,7 @@ const CanvasCapture = () => {
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={captureImage} className='w-full rnd-escape'>
|
<Button onClick={handleDownload} className='w-full rnd-escape'>
|
||||||
Capture Canvas
|
Capture Canvas
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import AuthContext from "./authContext";
|
import AuthContext from "./AuthContext";
|
||||||
|
|
||||||
const AuthContextProvider = ({ children }) => {
|
const AuthContextProvider = ({ children }) => {
|
||||||
const [user, setUser] = useState({
|
const [user, setUser] = useState([]);
|
||||||
token: null,
|
|
||||||
userId: null,
|
|
||||||
currentProjectId: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ user, setUser }}>
|
<AuthContext.Provider value={{ user, setUser }}>
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,14 @@ import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { shapes } from "./shapes";
|
import { shapes } from "./shapes";
|
||||||
import { fabric } from "fabric";
|
import { fabric } from "fabric";
|
||||||
|
import useProject from "@/hooks/useProject";
|
||||||
|
|
||||||
const CustomShape = () => {
|
const CustomShape = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
|
const { projectData, projectUpdate, id } = useProject();
|
||||||
|
|
||||||
const addShape = (each) => {
|
const addShape = (each) => {
|
||||||
// Load the SVG from the imported file
|
// Load the SVG from the imported file
|
||||||
fabric.loadSVGFromURL(each, (objects, options) => {
|
fabric.loadSVGFromURL(each, (objects, options) => {
|
||||||
|
|
@ -38,6 +41,11 @@ const CustomShape = () => {
|
||||||
|
|
||||||
// Render the canvas
|
// Render the canvas
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
|
|
||||||
|
const object = canvas.toJSON(['id', 'selectable']);
|
||||||
|
const updateData = { ...projectData?.data, object };
|
||||||
|
// Wait for the project update before continuing
|
||||||
|
projectUpdate({ id, updateData });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ import {
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/components/ui/collapsible";
|
} 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 = [
|
const features = [
|
||||||
{
|
{
|
||||||
|
|
@ -68,6 +72,24 @@ const AddImageIntoShape = () => {
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
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) => {
|
const handleResize = (file, callback) => {
|
||||||
Resizer.imageFileResizer(
|
Resizer.imageFileResizer(
|
||||||
file,
|
file,
|
||||||
|
|
@ -84,31 +106,20 @@ const AddImageIntoShape = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileHandler = (e) => {
|
const fileHandler = (e) => {
|
||||||
|
if (!activeObject) {
|
||||||
|
toast({ variant: "destructive", title: "No active object selected!" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) {
|
if (!file && activeObject) {
|
||||||
setErrorMessage("No file selected.");
|
setErrorMessage("No file selected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the file is an SVG
|
// Check if the file is an SVG (skip compression)
|
||||||
if (file.type === "image/svg+xml") {
|
if (file.type === "image/svg+xml") {
|
||||||
// Add SVG directly to canvas without compression
|
toast({ variant: "destructive", title: "SVG files are not supported!" });
|
||||||
const blobUrl = URL.createObjectURL(file);
|
clearFileInput();
|
||||||
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();
|
|
||||||
};
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,32 +129,16 @@ const AddImageIntoShape = () => {
|
||||||
imgElement.src = blobUrl;
|
imgElement.src = blobUrl;
|
||||||
|
|
||||||
imgElement.onload = () => {
|
imgElement.onload = () => {
|
||||||
// If the width is greater than 1080px, compress the image
|
|
||||||
if (imgElement.width > 1080) {
|
if (imgElement.width > 1080) {
|
||||||
handleResize(file, (compressedFile) => {
|
handleResize(file, (compressedFile) => {
|
||||||
const compressedBlobUrl = URL.createObjectURL(compressedFile);
|
uploadMutate({ file: compressedFile, id }); // Fixed key name
|
||||||
const compressedImg = new Image();
|
clearFileInput();
|
||||||
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();
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
handleImageInsert(imgElement); // Insert the original image if no resizing needed
|
uploadMutate({ file, id }); // Direct upload if width is small
|
||||||
clearFileInput();
|
clearFileInput();
|
||||||
}
|
}
|
||||||
URL.revokeObjectURL(blobUrl); // Clean up
|
URL.revokeObjectURL(blobUrl); // Clean up
|
||||||
clearFileInput();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
imgElement.onerror = () => {
|
imgElement.onerror = () => {
|
||||||
|
|
@ -160,85 +155,112 @@ const AddImageIntoShape = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageInsert = (img) => {
|
// const handleImageInsert = (img) => {
|
||||||
if (!activeObject) {
|
// if (!activeObject) {
|
||||||
setErrorMessage("No active object selected!");
|
// setErrorMessage("No active object selected!");
|
||||||
return;
|
// 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
|
// Ensure absolute positioning for the clipPath
|
||||||
activeObject.set({
|
activeObject.set({
|
||||||
isClipPath: true, // Custom property
|
isClipPath: true, // Custom property
|
||||||
absolutePositioned: true,
|
absolutePositioned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate scale factors based on clip object size
|
// Load the image asynchronously
|
||||||
let scaleX = activeObject.width / img.width;
|
fabric.Image.fromURL(
|
||||||
let scaleY = activeObject.height / img.height;
|
imgUrl,
|
||||||
if (activeObject?.width < 100) {
|
(img) => {
|
||||||
scaleX = 0.2;
|
// 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) {
|
// Prevent the image from being too small
|
||||||
scaleY = 0.2;
|
if (activeObject.width < 100) scaleX = 0.2;
|
||||||
}
|
if (activeObject.height < 100) scaleY = 0.2;
|
||||||
|
|
||||||
// Create a fabric image object with scaling and clipPath
|
// Set image properties
|
||||||
const fabricImage = new fabric.Image(img, {
|
img.set({
|
||||||
scaleX: scaleX,
|
scaleX: scaleX,
|
||||||
scaleY: scaleY,
|
scaleY: scaleY,
|
||||||
left: activeObject.left,
|
left: activeObject.left,
|
||||||
top: activeObject.top,
|
top: activeObject.top,
|
||||||
clipPath: activeObject, // Apply clipPath to the image
|
clipPath: activeObject, // Apply clipPath to the image
|
||||||
originX: activeObject.originX, // Match origin point
|
originX: activeObject.originX, // Match origin point
|
||||||
originY: activeObject.originY, // Match origin point
|
originY: activeObject.originY, // Match origin point
|
||||||
});
|
crossOrigin: "anonymous", // Ensure CORS handling
|
||||||
|
});
|
||||||
|
|
||||||
// Adjust position based on the clipPath's transformations
|
// Add image to canvas
|
||||||
fabricImage.set({
|
canvas.add(img);
|
||||||
left: activeObject.left,
|
canvas.setActiveObject(img);
|
||||||
top: activeObject.top,
|
setActiveObject(img);
|
||||||
angle: activeObject.angle, // Match rotation if any
|
canvas.renderAll();
|
||||||
});
|
|
||||||
|
|
||||||
canvas.add(fabricImage);
|
// Update project data
|
||||||
canvas.setActiveObject(fabricImage);
|
projectUpdate({
|
||||||
setActiveObject(fabricImage);
|
id,
|
||||||
canvas.renderAll();
|
updateData: {
|
||||||
|
...projectData?.data,
|
||||||
|
object: canvas.toJSON(["id", "selectable"]),
|
||||||
|
preview_url: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ crossOrigin: "anonymous" }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// canvas.remove(activeObject);
|
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<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">
|
<Card className="my-2">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<Alert>
|
<Alert>
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,7 @@ const TextCustomization = () => {
|
||||||
<SelectTrigger className="min-w-[140px] h-8">
|
<SelectTrigger className="min-w-[140px] h-8">
|
||||||
<SelectValue placeholder="Select a font" />
|
<SelectValue placeholder="Select a font" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="h-[250px]">
|
<SelectContent className="h-[250px] z-[9999]">
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{fonts.map((font) => (
|
{fonts.map((font) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
|
|
@ -258,6 +258,7 @@ const TextCustomization = () => {
|
||||||
</Select>
|
</Select>
|
||||||
</a>
|
</a>
|
||||||
<Tooltip id="fonts" content="Font Family" place="bottom" />
|
<Tooltip id="fonts" content="Font Family" place="bottom" />
|
||||||
|
|
||||||
{/* Font Size Controls */}
|
{/* Font Size Controls */}
|
||||||
<div className="flex items-center border rounded-md">
|
<div className="flex items-center border rounded-md">
|
||||||
<a data-tooltip-id="font-dec">
|
<a data-tooltip-id="font-dec">
|
||||||
|
|
@ -452,45 +453,6 @@ const TextCustomization = () => {
|
||||||
</Popover>
|
</Popover>
|
||||||
<Tooltip id="spacing" content="Spacing" place="bottom" />
|
<Tooltip id="spacing" content="Spacing" place="bottom" />
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -6,6 +6,7 @@ import * as lucideIcons from "lucide-react";
|
||||||
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import { fabric } from "fabric";
|
import { fabric } from "fabric";
|
||||||
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
|
import useProject from "@/hooks/useProject";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
const AllIconsPage = () => {
|
const AllIconsPage = () => {
|
||||||
|
|
@ -15,6 +16,8 @@ const AllIconsPage = () => {
|
||||||
|
|
||||||
const { toast } = useToast();
|
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
|
// Assume icons is already defined as shown previously, and filtered is created based on the search query
|
||||||
const icons = Object.entries(lucideIcons)?.filter(
|
const icons = Object.entries(lucideIcons)?.filter(
|
||||||
([name, Icon]) => !name.includes("Icon") && Icon?.$$typeof
|
([name, Icon]) => !name.includes("Icon") && Icon?.$$typeof
|
||||||
|
|
@ -85,6 +88,10 @@ const AllIconsPage = () => {
|
||||||
canvas.setActiveObject(iconGroup);
|
canvas.setActiveObject(iconGroup);
|
||||||
setActiveObject(iconGroup);
|
setActiveObject(iconGroup);
|
||||||
canvas.renderAll();
|
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 });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,14 @@ import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||||
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import useProject from "@/hooks/useProject";
|
||||||
|
|
||||||
const RoundedShape = () => {
|
const RoundedShape = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
|
const { projectData, projectUpdate, id } = useProject();
|
||||||
|
|
||||||
const shapes = [
|
const shapes = [
|
||||||
{ icon: <ArrowBigRight />, name: "Arrow" },
|
{ icon: <ArrowBigRight />, name: "Arrow" },
|
||||||
{ icon: <Diamond />, name: "Diamond" },
|
{ icon: <Diamond />, name: "Diamond" },
|
||||||
|
|
@ -67,6 +70,11 @@ const RoundedShape = () => {
|
||||||
canvas.setActiveObject(iconGroup);
|
canvas.setActiveObject(iconGroup);
|
||||||
setActiveObject(iconGroup);
|
setActiveObject(iconGroup);
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
|
|
||||||
|
const object = canvas.toJSON(['id', 'selectable']);
|
||||||
|
const updateData = { ...projectData?.data, object };
|
||||||
|
// Wait for the project update before continuing
|
||||||
|
projectUpdate({ id, updateData });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,14 @@ import { fabric } from "fabric";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Badge, Circle, Heart, Shield } from "lucide-react";
|
import { Badge, Circle, Heart, Shield } from "lucide-react";
|
||||||
|
import useProject from "@/hooks/useProject";
|
||||||
|
|
||||||
const PlainShapes = () => {
|
const PlainShapes = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
|
const { projectData, projectUpdate, id } = useProject();
|
||||||
|
|
||||||
const shapes = [
|
const shapes = [
|
||||||
{
|
{
|
||||||
icon: (
|
icon: (
|
||||||
|
|
@ -298,6 +301,11 @@ const PlainShapes = () => {
|
||||||
canvas.setActiveObject(iconGroup);
|
canvas.setActiveObject(iconGroup);
|
||||||
setActiveObject(iconGroup);
|
setActiveObject(iconGroup);
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
|
|
||||||
|
const object = canvas.toJSON(['id', 'selectable']);
|
||||||
|
const updateData = { ...projectData?.data, object };
|
||||||
|
// Wait for the project update before continuing
|
||||||
|
projectUpdate({ id, updateData });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { fabric } from "fabric";
|
|
||||||
import { ImageIcon, Trash2 } from "lucide-react";
|
import { ImageIcon, Trash2 } from "lucide-react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,11 +24,9 @@ import { useDropzone } from "react-dropzone";
|
||||||
import ImageCustomization from "./Customization/ImageCustomization";
|
import ImageCustomization from "./Customization/ImageCustomization";
|
||||||
import { Separator } from "../ui/separator";
|
import { Separator } from "../ui/separator";
|
||||||
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import useProject from "@/hooks/useProject";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import useImageHandler from "@/hooks/useImageHandler";
|
||||||
import { deleteImage, uploadImage } from "../../api/uploadApi";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { createProject, getProjectById, updateProject } from "../../api/projectApi";
|
|
||||||
import { useToast } from "../../hooks/use-toast";
|
|
||||||
|
|
||||||
const UploadImage = () => {
|
const UploadImage = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
@ -40,146 +37,39 @@ const UploadImage = () => {
|
||||||
const [format, setFormat] = useState("JPEG");
|
const [format, setFormat] = useState("JPEG");
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
const { toast } = useToast();
|
||||||
|
|
||||||
const params = useParams();
|
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
||||||
const { id } = params;
|
|
||||||
|
|
||||||
const [file, setFile] = useState(null);
|
const [file, setFile] = useState(null);
|
||||||
const [preview, setPreview] = useState(null);
|
const [preview, setPreview] = useState(null);
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { id } = useProject();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// create empty project if no id is provided
|
const removeFile = () => {
|
||||||
useEffect(() => {
|
// Revoke the object URL to free up memory
|
||||||
const createEmptyProject = async () => {
|
if (preview) {
|
||||||
try {
|
URL.revokeObjectURL(preview);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}, [id, navigate, toast]);
|
setFile(null);
|
||||||
|
setPreview(null);
|
||||||
const queryClient = useQueryClient();
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = "";
|
||||||
// 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"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
if (activeObject?.type === "image") {
|
||||||
|
const imgUrl = activeObject?._originalElement?.currentSrc;
|
||||||
// upload image handler
|
canvas.remove(activeObject);
|
||||||
const { mutate } = useMutation({
|
setActiveObject(null);
|
||||||
mutationFn: async ({ file, id }) => {
|
canvas.renderAll();
|
||||||
return await uploadImage({ file, id });
|
deleteMutate(imgUrl);
|
||||||
},
|
|
||||||
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 { uploadMutate, deleteMutate } = useImageHandler({ removeFile });
|
||||||
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 { getRootProps, getInputProps, isDragActive } = useDropzone({
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||||
accept: {
|
accept: {
|
||||||
"image/*": [".jpeg", ".png", ".gif", ".jpg", ".webp", ".svg"],
|
"image/*": [".jpeg", ".png", ".jpg", ".webp"],
|
||||||
},
|
},
|
||||||
// maxSize: 5 * 1024 * 1024, // 5MB max file size
|
// maxSize: 5 * 1024 * 1024, // 5MB max file size
|
||||||
multiple: false,
|
multiple: false,
|
||||||
|
|
@ -194,8 +84,13 @@ const UploadImage = () => {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
|
|
||||||
if (selectedFile.type === "image/svg+xml") {
|
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);
|
URL.revokeObjectURL(blobUrl);
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
const imgElement = new Image();
|
const imgElement = new Image();
|
||||||
imgElement.src = blobUrl;
|
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 handleResize = (selectedFile, callback) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = URL.createObjectURL(selectedFile);
|
img.src = URL.createObjectURL(selectedFile);
|
||||||
|
|
@ -277,7 +153,7 @@ const UploadImage = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const addImageToCanvas = (selectedFile) => {
|
const addImageToCanvas = (selectedFile) => {
|
||||||
mutate({ file: selectedFile, id })
|
uploadMutate({ file: selectedFile, id })
|
||||||
};
|
};
|
||||||
|
|
||||||
// useEffect for preview update
|
// useEffect for preview update
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { deleteImage } from "../api/uploadApi";
|
import { deleteImage } from "../api/uploadApi";
|
||||||
import { toast } from "../hooks/use-toast";
|
import { toast } from "../hooks/use-toast";
|
||||||
|
import useProject from "@/hooks/useProject";
|
||||||
|
|
||||||
export const ObjectShortcut = ({ value }) => {
|
export const ObjectShortcut = ({ value }) => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
@ -34,6 +35,8 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
const objectActive = canvas.getActiveObject();
|
const objectActive = canvas.getActiveObject();
|
||||||
const isGroupObject = objectActive && objectActive.type === "group";
|
const isGroupObject = objectActive && objectActive.type === "group";
|
||||||
|
|
||||||
|
const { projectData, projectUpdate, id } = useProject();
|
||||||
|
|
||||||
const groupSelectedObjects = () => {
|
const groupSelectedObjects = () => {
|
||||||
const activeObjects = canvas.getActiveObjects(); // Get selected objects
|
const activeObjects = canvas.getActiveObjects(); // Get selected objects
|
||||||
if (activeObjects.length > 1) {
|
if (activeObjects.length > 1) {
|
||||||
|
|
@ -53,6 +56,10 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
canvas.setActiveObject(group);
|
canvas.setActiveObject(group);
|
||||||
setActiveObject(group);
|
setActiveObject(group);
|
||||||
canvas.renderAll();
|
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 {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: "Select at least two objects",
|
title: "Select at least two objects",
|
||||||
|
|
@ -108,6 +115,11 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
setActiveObject(selection);
|
setActiveObject(selection);
|
||||||
|
|
||||||
canvas.renderAll();
|
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) {
|
if (activeObject && !objectPosition.isAtFront) {
|
||||||
activeObject.bringToFront();
|
activeObject.bringToFront();
|
||||||
canvas.renderAll();
|
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);
|
setIsPopoverOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -139,6 +155,10 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
if (activeObject && !objectPosition.isAtBack) {
|
if (activeObject && !objectPosition.isAtBack) {
|
||||||
activeObject.sendToBack();
|
activeObject.sendToBack();
|
||||||
canvas.renderAll();
|
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);
|
setIsPopoverOpen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -153,6 +173,12 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
title: data?.status,
|
title: data?.status,
|
||||||
description: data?.message
|
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 {
|
else {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -178,14 +204,26 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
if (activeObject) {
|
if (activeObject) {
|
||||||
canvas.remove(activeObject);
|
canvas.remove(activeObject);
|
||||||
setActiveObject(null);
|
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) {
|
if (activeObject && textObjects?.length === 1) {
|
||||||
canvas.remove(activeObject);
|
canvas.remove(activeObject);
|
||||||
setActiveObject(null);
|
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) {
|
if (activeObject.length > 1) {
|
||||||
canvas.remove(...activeObject);
|
canvas.remove(...activeObject);
|
||||||
setActiveObject(null);
|
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") {
|
if (activeObject?.type === "image") {
|
||||||
|
|
@ -195,7 +233,7 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
deleteMutate(imgUrl);
|
deleteMutate(imgUrl);
|
||||||
}
|
}
|
||||||
}, [canvas, setActiveObject, deleteMutate]);
|
}, [canvas, setActiveObject, deleteMutate, id, projectData, projectUpdate]);
|
||||||
|
|
||||||
// duplicating current objects
|
// duplicating current objects
|
||||||
const duplicating = () => {
|
const duplicating = () => {
|
||||||
|
|
@ -205,6 +243,10 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
clonedObject.set("left", clonedObject?.left + 30);
|
clonedObject.set("left", clonedObject?.left + 30);
|
||||||
canvas.add(clonedObject);
|
canvas.add(clonedObject);
|
||||||
canvas.renderAll();
|
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.renderAll();
|
||||||
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
||||||
setActiveObject(null);
|
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 (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@ const NotFound = () => {
|
||||||
<h2 className="text-2xl font-semibold mt-4 mb-2">Page Not Found</h2>
|
<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>
|
<p className="text-muted-foreground mb-8">Oops! The page you are looking for does not exist or has been moved.</p>
|
||||||
<a
|
<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"
|
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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-4 left-4 w-24 h-24 border-t-4 border-l-4 border-primary opacity-20"></div>
|
<div className="absolute top-4 left-4 w-24 h-24 border-t-4 border-l-4 border-primary opacity-20"></div>
|
||||||
|
|
|
||||||
32
src/components/Panel/DesignPanel.jsx
Normal file
32
src/components/Panel/DesignPanel.jsx
Normal 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
|
||||||
|
|
@ -14,6 +14,7 @@ import GroupObjectPanel from "./GroupObjectPanel";
|
||||||
import CanvasPanel from "./CanvasPanel";
|
import CanvasPanel from "./CanvasPanel";
|
||||||
import { ProjectPanel } from "./ProjectPanel";
|
import { ProjectPanel } from "./ProjectPanel";
|
||||||
import ImageLibrary from "./ImageLibrary";
|
import ImageLibrary from "./ImageLibrary";
|
||||||
|
import DesignPanel from "./DesignPanel";
|
||||||
|
|
||||||
const EditorPanel = () => {
|
const EditorPanel = () => {
|
||||||
const { selectedPanel } = useContext(CanvasContext);
|
const { selectedPanel } = useContext(CanvasContext);
|
||||||
|
|
@ -48,6 +49,8 @@ const EditorPanel = () => {
|
||||||
return <ProjectPanel />;
|
return <ProjectPanel />;
|
||||||
case "image":
|
case "image":
|
||||||
return <ImageLibrary />;
|
return <ImageLibrary />;
|
||||||
|
case "design":
|
||||||
|
return <DesignPanel />;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
import { useContext } from "react";
|
import { useContext, useEffect, useRef } from "react";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
import AllIconsPage from "../EachComponent/Icons/AllIcons";
|
import AllIconsPage from "../EachComponent/Icons/AllIcons";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import useProject from "@/hooks/useProject";
|
||||||
|
|
||||||
const IconPanel = () => {
|
const IconPanel = () => {
|
||||||
const { setSelectedPanel } = useContext(CanvasContext);
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useContext } from 'react'
|
import { useContext } from 'react'
|
||||||
import CanvasContext from '../Context/canvasContext/CanvasContext';
|
import CanvasContext from '../Context/canvasContext/CanvasContext';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { X } from 'lucide-react';
|
import { Ellipsis, X } from 'lucide-react';
|
||||||
import { ScrollArea } from '../ui/scroll-area';
|
import { ScrollArea } from '../ui/scroll-area';
|
||||||
const ImageLibrary = () => {
|
const ImageLibrary = () => {
|
||||||
const { setSelectedPanel } = useContext(CanvasContext);
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
|
@ -20,6 +20,10 @@ const ImageLibrary = () => {
|
||||||
|
|
||||||
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
|
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
|
||||||
{/* Image library content goes here */}
|
{/* Image library content goes here */}
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<p >Coming soon </p>
|
||||||
|
<Ellipsis className='animate-pulse animate-infinite' />
|
||||||
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,14 @@ import CanvasContext from '../Context/canvasContext/CanvasContext';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { Loader, Trash, X } from 'lucide-react';
|
import { Loader, Trash, X } from 'lucide-react';
|
||||||
import { ScrollArea } from '../ui/scroll-area';
|
import { ScrollArea } from '../ui/scroll-area';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { deleteProject, getProjects } from '@/api/projectApi';
|
import { getProjects } from '@/api/projectApi';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import useProject from '@/hooks/useProject';
|
||||||
import ActiveObjectContext from '../Context/activeObject/ObjectContext';
|
|
||||||
import { FixedSizeList as List } from 'react-window';
|
|
||||||
|
|
||||||
export const ProjectPanel = () => {
|
export const ProjectPanel = () => {
|
||||||
const { setSelectedPanel, canvas } = useContext(CanvasContext);
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient(); // Initialize query client
|
const queryClient = useQueryClient(); // Initialize query client
|
||||||
|
|
||||||
|
|
@ -22,79 +20,20 @@ export const ProjectPanel = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// To delete a project
|
// To delete a project
|
||||||
const { mutate: projectDelete, isPending: deletePending } = useMutation({
|
const { projectDelete, deletePending } = useProject();
|
||||||
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 (
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
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
|
// Filter projects where preview_url is not empty or null
|
||||||
const filteredProjects = projects?.data?.filter(project => project?.preview_url) || [];
|
const filteredProjects = projects?.data?.filter(project => project?.preview_url) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='relative'>
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
<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>
|
<h2 className="text-lg font-semibold">Projects</h2>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -109,17 +48,33 @@ export const ProjectPanel = () => {
|
||||||
{
|
{
|
||||||
projectLoading && <p><Loader className="animate-spin mx-auto" /></p>
|
projectLoading && <p><Loader className="animate-spin mx-auto" /></p>
|
||||||
}
|
}
|
||||||
{
|
<div className='grid grid-cols-1 gap-2'>
|
||||||
!projectLoading && projectSuccess && projects?.status === 200 &&
|
{
|
||||||
<List
|
!projectLoading && projectSuccess && projects?.status === 200 &&
|
||||||
height={400} // Set the height of the viewport
|
filteredProjects.map((project) => {
|
||||||
itemCount={filteredProjects.length} // Use length of filtered projects
|
return (
|
||||||
itemSize={280} // Height of each row
|
<div key={project?.id} className="flex flex-col gap-1 p-1 rounded-md border">
|
||||||
width="100%" // Full width of the container
|
<div
|
||||||
>
|
className="rounded-md flex p-1 flex-col gap-1 bg-red-50 hover:bg-red-100 cursor-pointer transition-all"
|
||||||
{Row}
|
onClick={() => handleNavigate(project.id)}
|
||||||
</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>
|
projects?.status !== 200 && <p>{projects?.message}</p>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useContext } from "react";
|
import { useContext, useEffect, useRef } from "react";
|
||||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
|
@ -7,9 +7,24 @@ import { Separator } from "../ui/separator";
|
||||||
import CustomShape from "../EachComponent/CustomShape/CustomShape";
|
import CustomShape from "../EachComponent/CustomShape/CustomShape";
|
||||||
import RoundedShape from "../EachComponent/RoundedShapes/RoundedShape";
|
import RoundedShape from "../EachComponent/RoundedShapes/RoundedShape";
|
||||||
import PlainShapes from "../EachComponent/Shapes/PlainShapes";
|
import PlainShapes from "../EachComponent/Shapes/PlainShapes";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import useProject from "@/hooks/useProject";
|
||||||
|
|
||||||
const ShapePanel = () => {
|
const ShapePanel = () => {
|
||||||
const { setSelectedPanel } = useContext(CanvasContext);
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,30 @@
|
||||||
import { Button } from "../ui/Button";
|
import { Button } from "../ui/Button";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
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 CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||||
import { fabric } from "fabric";
|
import { fabric } from "fabric";
|
||||||
import CommonPanel from "./CommonPanel";
|
import CommonPanel from "./CommonPanel";
|
||||||
|
import useProject from "@/hooks/useProject";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
export default function TextPanel() {
|
export default function TextPanel() {
|
||||||
const { canvas, setSelectedPanel } = useContext(CanvasContext);
|
const { canvas, setSelectedPanel } = useContext(CanvasContext);
|
||||||
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
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);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -37,6 +51,11 @@ export default function TextPanel() {
|
||||||
canvas.setActiveObject(text);
|
canvas.setActiveObject(text);
|
||||||
setActiveObject(text);
|
setActiveObject(text);
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
|
|
||||||
|
const object = canvas.toJSON(['id', 'selectable']);
|
||||||
|
const updateData = { ...projectData?.data, object };
|
||||||
|
// Wait for the project update before continuing
|
||||||
|
projectUpdate({ id, updateData });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
import { useContext } from "react";
|
import { useContext, useEffect, useRef } from "react";
|
||||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
import UploadImage from "../EachComponent/UploadImage";
|
import UploadImage from "../EachComponent/UploadImage";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import useProject from "@/hooks/useProject";
|
||||||
|
|
||||||
const UploadPanel = () => {
|
const UploadPanel = () => {
|
||||||
const { setSelectedPanel } = useContext(CanvasContext);
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center p-4 border-b">
|
<div className="flex justify-between items-center p-4 border-b">
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
import { useContext, useEffect, useState } from 'react'
|
import { useContext, useEffect, useState } from 'react'
|
||||||
import CanvasContext from './Context/canvasContext/CanvasContext';
|
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 { useToast } from '../hooks/use-toast';
|
||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
import { Label } from './ui/label';
|
import { Label } from './ui/label';
|
||||||
import { Save, Trash2 } from 'lucide-react';
|
import { Save, Trash2 } from 'lucide-react';
|
||||||
import { Textarea } from './ui/textarea';
|
import { Textarea } from './ui/textarea';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import { deleteProject, getProjectById, updateProject } from '@/api/projectApi';
|
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Separator } from './ui/separator';
|
import { Separator } from './ui/separator';
|
||||||
import { deleteImage, uploadImage } from '@/api/uploadApi';
|
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 SaveCanvas = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
const { setSelectedPanel } = useContext(CanvasContext);
|
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
|
||||||
|
|
||||||
const [saveCanvas, setSaveCanvas] = useState({
|
const [saveCanvas, setSaveCanvas] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
|
|
@ -27,89 +26,12 @@ const SaveCanvas = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const { projectDelete, deletePending, isLoading, projectData, projectUpdate, updatePending } = useProject();
|
||||||
|
|
||||||
// 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"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// to set projectData into state
|
// to set projectData into state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectData?.data && !projectLoading && projectData?.data?.preview_url !== null) {
|
if (projectData?.data && !isLoading && projectData?.data?.preview_url !== null) {
|
||||||
setSaveCanvas((prev) => ({
|
setSaveCanvas((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
name: projectData?.data?.name,
|
name: projectData?.data?.name,
|
||||||
|
|
@ -117,60 +39,12 @@ const SaveCanvas = () => {
|
||||||
preview_url: projectData?.data?.preview_url
|
preview_url: projectData?.data?.preview_url
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [projectData, projectLoading]);
|
}, [projectData, isLoading]);
|
||||||
|
|
||||||
// upload preview-image handler
|
const { uploadCanvasImage, uploadCanvasPending, removeCanvasImage, removeCanvasPending } =
|
||||||
const { mutate: uploadCanvasImage, isPending: uploadCanvasPending } = useMutation({
|
useCanvasCapture({ handleSaveWithPreViewImage, canvas, id, saveCanvas });
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// preview-image remove handler
|
async function handleSaveProject() {
|
||||||
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 () => {
|
|
||||||
if (!saveCanvas?.name || saveCanvas?.name.trim() === "") {
|
if (!saveCanvas?.name || saveCanvas?.name.trim() === "") {
|
||||||
toast({
|
toast({
|
||||||
title: "Name error",
|
title: "Name error",
|
||||||
|
|
@ -190,7 +64,7 @@ const SaveCanvas = () => {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const file = await captureImage();
|
const file = await captureCanvas(canvas);
|
||||||
if (file) {
|
if (file) {
|
||||||
uploadCanvasImage({ file, id });
|
uploadCanvasImage({ file, id });
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +75,7 @@ const SaveCanvas = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// this will save the canvas as a json object
|
// 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
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||||
if (object?.objects?.length === 0) {
|
if (object?.objects?.length === 0) {
|
||||||
toast({
|
toast({
|
||||||
|
|
@ -210,63 +84,13 @@ const SaveCanvas = () => {
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const projectData = { ...body, object };
|
const updateData = { ...body, object };
|
||||||
// Wait for the project update before continuing
|
projectUpdate({ id, updateData });
|
||||||
projectUpdate({ id, projectData });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = () => {
|
const handleDeleteProject = () => {
|
||||||
projectDelete(id);
|
projectDelete();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -301,8 +125,8 @@ const SaveCanvas = () => {
|
||||||
<Separator className='my-2' />
|
<Separator className='my-2' />
|
||||||
|
|
||||||
<div className='flex my-2 gap-2 justify-end'>
|
<div className='flex my-2 gap-2 justify-end'>
|
||||||
<Button disabled={deletePending || projectLoading || uploadCanvasPending || removeCanvasPending} onClick={handleDeleteProject}> <Trash2 /> </Button>
|
<Button disabled={deletePending || isLoading || uploadCanvasPending || removeCanvasPending} onClick={handleDeleteProject}> <Trash2 /> </Button>
|
||||||
<Button disabled={isPending || projectLoading} onClick={handleSaveProject}>Save</Button>
|
<Button disabled={isLoading || updatePending} onClick={handleSaveProject}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
|
||||||
<ToastPrimitives.Viewport
|
<ToastPrimitives.Viewport
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props} />
|
{...props} />
|
||||||
|
|
|
||||||
50
src/hooks/useCanvasCapture.jsx
Normal file
50
src/hooks/useCanvasCapture.jsx
Normal 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
|
||||||
64
src/hooks/useImageHandler.jsx
Normal file
64
src/hooks/useImageHandler.jsx
Normal 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
122
src/hooks/useProject.jsx
Normal 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
33
src/lib/captureCanvas.js
Normal 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);
|
||||||
|
});
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue