some bugs fixed and some part of ui changed
This commit is contained in:
parent
4fd46b789c
commit
704fb6cee1
36 changed files with 2785 additions and 1256 deletions
2
.env
2
.env
|
|
@ -1 +1 @@
|
||||||
VITE_GOOGLE_FONT_API_KEY=AIzaSyBPOYGT26jwMjlDuf6sM5JwaZDkiYigeQg
|
VITE_SERVER_URL=http://localhost:3000/api
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
"config": "tailwind.config.js",
|
||||||
"css": "src/index.css",
|
"css": "src/index.css",
|
||||||
"baseColor": "slate",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,10 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": [
|
|
||||||
"ES2020",
|
|
||||||
"DOM",
|
|
||||||
"DOM.Iterable"
|
|
||||||
],
|
|
||||||
"module": "ESNext",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
1455
package-lock.json
generated
1455
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -25,7 +25,8 @@
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
"@radix-ui/react-toast": "^1.2.4",
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
"@radix-ui/react-tooltip": "^1.1.4",
|
"@radix-ui/react-tooltip": "^1.1.4",
|
||||||
"class-variance-authority": "^0.7.0",
|
"@tanstack/react-query": "^5.66.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"fabric": "^5.3.0",
|
"fabric": "^5.3.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
@ -36,9 +37,11 @@
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-image-file-resizer": "^0.4.8",
|
"react-image-file-resizer": "^0.4.8",
|
||||||
"react-rnd": "^10.4.13",
|
"react-rnd": "^10.4.13",
|
||||||
|
"react-router-dom": "^7.1.5",
|
||||||
"react-tooltip": "^5.28.0",
|
"react-tooltip": "^5.28.0",
|
||||||
"react-window": "^1.8.10",
|
"react-window": "^1.8.10",
|
||||||
"tailwind-merge": "^2.5.4",
|
"shadcn-ui": "^0.9.4",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwind-scrollbar": "^3.1.0",
|
"tailwind-scrollbar": "^3.1.0",
|
||||||
"tailwind-scrollbar-hide": "^1.3.1",
|
"tailwind-scrollbar-hide": "^1.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|
@ -46,6 +49,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.13.0",
|
"@eslint/js": "^9.13.0",
|
||||||
|
"@tanstack/eslint-plugin-query": "^5.66.0",
|
||||||
|
"@tanstack/react-query-devtools": "^5.66.0",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@vitejs/plugin-react": "^4.3.3",
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
|
|
||||||
59
src/App.jsx
59
src/App.jsx
|
|
@ -1,21 +1,10 @@
|
||||||
import { useContext, useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
// import Canvas from "./components/Canvas";
|
|
||||||
import WebFont from "webfontloader";
|
import WebFont from "webfontloader";
|
||||||
// import Header from "./components/Layouts/Header";
|
import { Routes, Route } from "react-router-dom";
|
||||||
// import SheetRightPanel from "./components/Layouts/SheetRightPanel";
|
import { Home } from "./Home";
|
||||||
// import SheetLeftPanel from "./components/Layouts/SheetLeftPanel";
|
import NotFound from "./components/Pages/NotFound";
|
||||||
// import CanvasCapture from "./components/CanvasCapture";
|
import Unauthenticated from "./components/Pages/UnAuthenticated";
|
||||||
// import { Toaster } from "./components/ui/toaster";
|
|
||||||
import { Sidebar } from "./components/Layouts/LeftSidebar";
|
|
||||||
// import TextPanel from "./components/Panel/TextPanel";
|
|
||||||
import { TopBar } from "./components/Panel/TopBar";
|
|
||||||
import { ActionButtons } from "./components/ActionButtons";
|
|
||||||
import EditorPanel from "./components/Panel/EditorPanel";
|
|
||||||
import CanvasContext from "./components/Context/canvasContext/CanvasContext";
|
|
||||||
import Canvas from "./components/Canvas/Canvas";
|
|
||||||
import ActiveObjectContext from "./components/Context/activeObject/ObjectContext";
|
|
||||||
import CanvasCapture from "./components/CanvasCapture";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -76,39 +65,13 @@ function App() {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { selectedPanel } = useContext(CanvasContext);
|
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <div className="relative flex flex-col h-screen overflow-hidden">
|
<Routes>
|
||||||
// <Toaster />
|
<Route path="/" element={<Home />} />
|
||||||
// <SheetLeftPanel />
|
<Route path="/:id" element={<Home />} />
|
||||||
// <Header />
|
<Route path="/unAuthenticated" element={<Unauthenticated />} />
|
||||||
// <SheetRightPanel />
|
<Route path="*" element={<NotFound />} />
|
||||||
|
</Routes>
|
||||||
// <main className="flex-1 overflow-hidden mt-[60px]">
|
|
||||||
// <div className="h-full overflow-auto">
|
|
||||||
// <Canvas />
|
|
||||||
// <CanvasCapture />
|
|
||||||
// </div>
|
|
||||||
// </main>
|
|
||||||
// </div>
|
|
||||||
<div className="flex h-screen overflow-hidden">
|
|
||||||
<Sidebar />
|
|
||||||
{selectedPanel !== "" && <EditorPanel />}
|
|
||||||
<div className="flex-1 flex flex-col h-full overflow-hidden">
|
|
||||||
{" "}
|
|
||||||
{/* Changed */}
|
|
||||||
{activeObject && <TopBar />}
|
|
||||||
<ActionButtons />
|
|
||||||
<div className="flex-1 overflow-auto">
|
|
||||||
{" "}
|
|
||||||
{/* Added wrapper */}
|
|
||||||
<Canvas />
|
|
||||||
</div>
|
|
||||||
<CanvasCapture />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
76
src/Home.jsx
Normal file
76
src/Home.jsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { useContext, useEffect } from 'react'
|
||||||
|
import ActiveObjectContext from './components/Context/activeObject/ObjectContext';
|
||||||
|
import { TopBar } from './components/Panel/TopBar';
|
||||||
|
import { ActionButtons } from './components/ActionButtons';
|
||||||
|
import { Sidebar } from './components/Layouts/LeftSidebar';
|
||||||
|
import EditorPanel from './components/Panel/EditorPanel';
|
||||||
|
import Canvas from './components/Canvas/Canvas';
|
||||||
|
import CanvasCapture from './components/CanvasCapture';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { generateToken } from './api/authApi';
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
|
|
||||||
|
export const Home = () => {
|
||||||
|
const { activeObject } = useContext(ActiveObjectContext);
|
||||||
|
const params = useParams();
|
||||||
|
const getToken = localStorage.getItem('canvas_token');
|
||||||
|
|
||||||
|
const { id } = params;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ['get-token'],
|
||||||
|
queryFn: () => generateToken(id),
|
||||||
|
enabled: !getToken
|
||||||
|
})
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!getToken && !isLoading) {
|
||||||
|
navigate("/unAuthenticated");
|
||||||
|
}
|
||||||
|
if (getToken && !isLoading && data?.status === 201) {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
|
}, [getToken, navigate, isLoading, data])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen overflow-hidden relative">
|
||||||
|
{
|
||||||
|
isLoading && <div>Loading...</div>
|
||||||
|
}
|
||||||
|
{!isLoading &&
|
||||||
|
<>
|
||||||
|
<Toaster />
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
activeObject && <TopBar />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="fixed z-[999] right-0">
|
||||||
|
<ActionButtons />
|
||||||
|
</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">
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<Canvas />
|
||||||
|
</div>
|
||||||
|
{/* canvas capture part */}
|
||||||
|
<CanvasCapture />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
24
src/api/authApi.ts
Normal file
24
src/api/authApi.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
export const generateToken = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const url = `${import.meta.env.VITE_SERVER_URL}/auth/generate-token/${id}`;
|
||||||
|
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);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/api/projectApi.ts
Normal file
139
src/api/projectApi.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
interface Project {
|
||||||
|
id: string,
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
object: JSON;
|
||||||
|
preview_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProjects = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/projects`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
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 projects:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProjectById = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/projects/each/${projectId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
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 project by ID:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createProject = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/projects/create`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
|
||||||
|
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 create project:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateProject = async (body: Project) => {
|
||||||
|
try {
|
||||||
|
const { id, name, description, object, preview_url } = body;
|
||||||
|
|
||||||
|
const project = { name, description, object, preview_url };
|
||||||
|
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/projects/update/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(project),
|
||||||
|
})
|
||||||
|
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 update project:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteProject = async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/projects/delete/${projectId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
|
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 delete project:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
57
src/api/uploadApi.ts
Normal file
57
src/api/uploadApi.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
interface body {
|
||||||
|
file: File,
|
||||||
|
id: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadImage = async (body: body) => {
|
||||||
|
try {
|
||||||
|
const { file, id } = body;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('id', id);
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/uploads/add`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
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('Error uploading file:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteImage = async (imgUrl: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/uploads/delete?url=${imgUrl}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
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("Error deleting file:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ const aspectRatios = [
|
||||||
|
|
||||||
export function ActionButtons() {
|
export function ActionButtons() {
|
||||||
const { setCaptureOpen } = useContext(OpenContext);
|
const { setCaptureOpen } = useContext(OpenContext);
|
||||||
const { setCanvasRatio, canvasRatio } = useContext(CanvasContext);
|
const { setCanvasRatio, canvasRatio, setSelectedPanel } = useContext(CanvasContext);
|
||||||
const handleRatioChange = (newRatio) => {
|
const handleRatioChange = (newRatio) => {
|
||||||
setCanvasRatio(newRatio);
|
setCanvasRatio(newRatio);
|
||||||
};
|
};
|
||||||
|
|
@ -56,16 +56,14 @@ export function ActionButtons() {
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="mr-5"
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="bg-[#F5F2FF] w-10 h-10"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCaptureOpen(true);
|
setCaptureOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="rounded-[10px] border-[#6A47ED] border-[0.5px] bg-[#F5F2FF]"
|
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -97,6 +95,55 @@ export function ActionButtons() {
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<div className="mr-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="w-10 h-10"
|
||||||
|
onClick={() => setSelectedPanel("canvas")}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="5"
|
||||||
|
width="18"
|
||||||
|
height="12"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path d="M14 14 L19 19" stroke="black" strokeWidth="2" />
|
||||||
|
<path
|
||||||
|
d="M15 15 Q16 12, 19 11"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="2"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="3"
|
||||||
|
y1="17"
|
||||||
|
x2="7"
|
||||||
|
y2="21"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="21"
|
||||||
|
y1="17"
|
||||||
|
x2="17"
|
||||||
|
y2="21"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,240 +0,0 @@
|
||||||
import { useState, useEffect, useContext } from 'react'
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
||||||
import { AspectRatio } from "@/components/ui/aspect-ratio"
|
|
||||||
import CanvasContext from './Context/canvasContext/CanvasContext'
|
|
||||||
import OpenContext from './Context/openContext/OpenContext';
|
|
||||||
import { Settings, Download, Share2, Settings2 } from "lucide-react";
|
|
||||||
import { Card, CardContent } from './ui/card';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
|
|
||||||
import { Button } from './ui/button';
|
|
||||||
import { Separator } from './ui/separator';
|
|
||||||
import { ObjectShortcut } from './ObjectShortcut';
|
|
||||||
|
|
||||||
const aspectRatios = [
|
|
||||||
{ value: "1:1", label: "Square (1:1)" },
|
|
||||||
{ value: "4:3", label: "Standard (4:3)" },
|
|
||||||
{ value: "3:2", label: "Classic (3:2)" },
|
|
||||||
{ value: "16:9", label: "Widescreen (16:9)" },
|
|
||||||
{ value: "9:16", label: "Portrait (9:16)" },
|
|
||||||
{ value: "21:9", label: "Ultrawide (21:9)" },
|
|
||||||
{ value: "32:9", label: "Super Ultrawide (32:9)" },
|
|
||||||
{ value: "1.85:1", label: "Cinema Standard (1.85:1)" },
|
|
||||||
{ value: "2.39:1", label: "Anamorphic Widescreen (2.39:1)" },
|
|
||||||
{ value: "2.76:1", label: "Ultra Panavision 70 (2.76:1)" },
|
|
||||||
{ value: "5:4", label: "Large Format (5:4)" },
|
|
||||||
{ value: "7:5", label: "Artistic Format (7:5)" },
|
|
||||||
{ value: "11:8.5", label: "Letter Size (11:8.5)" },
|
|
||||||
{ value: "3:4", label: "Portrait (3:4)" },
|
|
||||||
{ value: "1.91:1", label: "Facebook Ads (1.91:1)" }
|
|
||||||
];
|
|
||||||
|
|
||||||
export function AspectCanvas() {
|
|
||||||
const { setLeftPanelOpen, setRightPanelOpen, setOpenPanel, setCaptureOpen, setOpenSetting, setOpenObjectPanel, rightPanelOpen } = useContext(OpenContext);
|
|
||||||
const [selectedRatio, setSelectedRatio] = useState("4:3");
|
|
||||||
|
|
||||||
const { canvasRef, canvas, setCanvas, fabricCanvasRef, setCanvasHeight, setCanvasWidth, setScreenWidth } = useContext(CanvasContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
import('fabric').then((fabricModule) => {
|
|
||||||
window.fabric = fabricModule.fabric;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getRatioValue = (ratio) => {
|
|
||||||
const [width, height] = ratio.split(':').map(Number)
|
|
||||||
return width / height
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateCanvasSize = () => {
|
|
||||||
if (canvasRef.current && canvas) {
|
|
||||||
// Update canvas dimensions
|
|
||||||
const newWidth = canvasRef?.current?.offsetWidth;
|
|
||||||
const newHeight = canvasRef?.current?.offsetHeight;
|
|
||||||
|
|
||||||
canvas.setWidth(newWidth);
|
|
||||||
canvas.setHeight(newHeight);
|
|
||||||
setCanvasWidth(newWidth);
|
|
||||||
setCanvasHeight(newHeight);
|
|
||||||
|
|
||||||
// Adjust the background image to fit the updated canvas size
|
|
||||||
const bgImage = canvas.backgroundImage;
|
|
||||||
if (bgImage) {
|
|
||||||
// Calculate scaling factors for width and height
|
|
||||||
const scaleX = newWidth / bgImage.width;
|
|
||||||
const scaleY = newHeight / bgImage.height;
|
|
||||||
|
|
||||||
// Use the larger scale to cover the entire canvas
|
|
||||||
const scale = Math.max(scaleX, scaleY);
|
|
||||||
|
|
||||||
// Apply scale and position the image
|
|
||||||
bgImage.scaleX = scale;
|
|
||||||
bgImage.scaleY = scale;
|
|
||||||
bgImage.left = 0; // Align left
|
|
||||||
bgImage.top = 0; // Align top
|
|
||||||
|
|
||||||
// Update the background image
|
|
||||||
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
|
|
||||||
} else {
|
|
||||||
// Render the canvas if no background image
|
|
||||||
canvas.renderAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setScreenWidth(document.getElementById("root").offsetWidth);
|
|
||||||
|
|
||||||
// Handle responsive behavior for panels
|
|
||||||
if (document.getElementById("root").offsetWidth <= 640) {
|
|
||||||
setLeftPanelOpen(false);
|
|
||||||
setRightPanelOpen(false);
|
|
||||||
}
|
|
||||||
if (document.getElementById("root").offsetWidth > 640) {
|
|
||||||
setOpenObjectPanel(false);
|
|
||||||
setOpenSetting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Initial setup
|
|
||||||
updateCanvasSize();
|
|
||||||
|
|
||||||
// Listen for window resize
|
|
||||||
window.addEventListener('resize', updateCanvasSize);
|
|
||||||
|
|
||||||
// Cleanup listener on unmount
|
|
||||||
return () => window.removeEventListener('resize', updateCanvasSize);
|
|
||||||
}, [setCanvasWidth, setCanvasHeight, selectedRatio, canvasRef, canvas, setLeftPanelOpen, setOpenObjectPanel, setRightPanelOpen, rightPanelOpen, setScreenWidth, setOpenSetting]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (window.fabric) {
|
|
||||||
if (fabricCanvasRef?.current) {
|
|
||||||
fabricCanvasRef?.current.dispose()
|
|
||||||
}
|
|
||||||
// Set styles directly on the canvas element
|
|
||||||
const canvasElement = document.getElementById('fabric-canvas')
|
|
||||||
if (canvasElement) {
|
|
||||||
canvasElement.classList.add('fabric-canvas-container') // Add the CSS class
|
|
||||||
}
|
|
||||||
|
|
||||||
fabricCanvasRef.current = new window.fabric.Canvas('fabric-canvas', {
|
|
||||||
width: canvasRef?.current?.offsetWidth,
|
|
||||||
height: canvasRef?.current?.offsetWidth,
|
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
})
|
|
||||||
|
|
||||||
setCanvas(fabricCanvasRef?.current);
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleRatioChange = (newRatio) => {
|
|
||||||
setSelectedRatio(newRatio)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="w-full max-w-3xl p-2 my-4 overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-track-background rounded-none">
|
|
||||||
<CardContent className="p-0 space-y-2">
|
|
||||||
<div className='flex w-full flex-wrap items-center justify-between mx-auto'>
|
|
||||||
|
|
||||||
<div className='flex gap-1'>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon" onClick={() => setOpenPanel(true)}>
|
|
||||||
<Settings2 className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Open Panel</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<div className="flex justify-between gap-1 items-center">
|
|
||||||
<div className="block xl:hidden lg:hidden md:hidden">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon" onClick={() => setOpenSetting(true)}>
|
|
||||||
<Settings className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Open Settings</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon" onClick={() => { setCaptureOpen(true) }}>
|
|
||||||
<Download className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Download</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button variant="outline" size="icon">
|
|
||||||
<Share2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Share (Coming soon)</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="w-full">
|
|
||||||
<Select onValueChange={handleRatioChange} value={selectedRatio}>
|
|
||||||
<SelectTrigger className="w-full text-xs font-bold">
|
|
||||||
<SelectValue placeholder="Select aspect ratio" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{aspectRatios.map((ratio) => (
|
|
||||||
<SelectItem key={ratio.value} value={ratio.value} className="text-xs font-bold">
|
|
||||||
{ratio.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" className="max-w-xs">
|
|
||||||
<p>Changing the canvas size.</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<ObjectShortcut value={"default"} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<AspectRatio ratio={getRatioValue(selectedRatio)} className="overflow-y-scroll shadow-red-200 overflow-x-hidden shadow-lg rounded-lg border-2 border-primary/10 transition-all duration-300 ease-in-out hover:shadow-xl scrollbar-hide">
|
|
||||||
<div ref={canvasRef} className="w-full h-full flex items-center justify-center bg-white rounded-md shadow-lg" id='canvas-ref'>
|
|
||||||
<canvas id="fabric-canvas" />
|
|
||||||
</div>
|
|
||||||
</AspectRatio>
|
|
||||||
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
import { useEffect, useCallback, useContext } from 'react';
|
|
||||||
import CanvasContext from './Context/canvasContext/CanvasContext';
|
|
||||||
import ActiveObjectContext from './Context/activeObject/ObjectContext';
|
|
||||||
import OpenContext from './Context/openContext/OpenContext';
|
|
||||||
import CanvasSetting from './CanvasSetting';
|
|
||||||
import { EditPanel } from './EditPanel';
|
|
||||||
import ObjectPanel from './ObjectPanel';
|
|
||||||
import { AspectCanvas } from './AspectCanvas';
|
|
||||||
|
|
||||||
const Canvas = () => {
|
|
||||||
const { canvas } = useContext(CanvasContext);
|
|
||||||
|
|
||||||
const { openSetting, openObjectPanel, openPanel } = useContext(OpenContext);
|
|
||||||
|
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!canvas) return; // Ensure canvas is available
|
|
||||||
|
|
||||||
// Event handler for mouse down
|
|
||||||
const handleMouseDown = (event) => {
|
|
||||||
const target = event.target; // Get the clicked target
|
|
||||||
const activeObject = canvas.getActiveObject(); // Get the active object
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
if (target.type === 'group') {
|
|
||||||
setActiveObject(activeObject);
|
|
||||||
} else {
|
|
||||||
setActiveObject(activeObject);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setActiveObject(activeObject);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Attach the event listener
|
|
||||||
canvas.on('mouse:down', handleMouseDown);
|
|
||||||
|
|
||||||
// Cleanup function to remove the event listener
|
|
||||||
return () => {
|
|
||||||
canvas.off('mouse:down', handleMouseDown); // Remove the listener on unmount
|
|
||||||
};
|
|
||||||
}, [canvas]); // Re-run only when canvas changes
|
|
||||||
|
|
||||||
|
|
||||||
const removeSelected = useCallback(() => {
|
|
||||||
const activeObject = canvas?.getActiveObject();
|
|
||||||
const allObjects = canvas?.getObjects();
|
|
||||||
|
|
||||||
const textObjects = allObjects.filter((obj) => obj.type === 'textbox' || obj.type === 'text' || obj.type === 'i-text');
|
|
||||||
|
|
||||||
if (activeObject) {
|
|
||||||
canvas.remove(activeObject);
|
|
||||||
setActiveObject(null);
|
|
||||||
}
|
|
||||||
if (activeObject && textObjects?.length === 1) {
|
|
||||||
canvas.remove(activeObject);
|
|
||||||
setActiveObject(null);
|
|
||||||
}
|
|
||||||
if (activeObject.length > 1) {
|
|
||||||
canvas.remove(...activeObject);
|
|
||||||
setActiveObject(null);
|
|
||||||
}
|
|
||||||
}, [canvas, setActiveObject]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleDeleteKey = (event) => {
|
|
||||||
if (event.key === 'Delete') {
|
|
||||||
removeSelected();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', handleDeleteKey);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleDeleteKey);
|
|
||||||
};
|
|
||||||
}, [removeSelected]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col items-center relative w-full h-full overflow-hidden'>
|
|
||||||
|
|
||||||
{openSetting &&
|
|
||||||
<CanvasSetting />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
openPanel &&
|
|
||||||
<EditPanel />
|
|
||||||
}
|
|
||||||
|
|
||||||
{openObjectPanel && <ObjectPanel />}
|
|
||||||
|
|
||||||
{/* Main canvas */}
|
|
||||||
<AspectCanvas />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Canvas;
|
|
||||||
|
|
@ -7,25 +7,9 @@ import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
|
||||||
export default function Canvas() {
|
export default function Canvas() {
|
||||||
const {
|
const { setLeftPanelOpen, setRightPanelOpen, setOpenSetting, setOpenObjectPanel, rightPanelOpen } = useContext(OpenContext);
|
||||||
setLeftPanelOpen,
|
|
||||||
setRightPanelOpen,
|
|
||||||
setOpenSetting,
|
|
||||||
setOpenObjectPanel,
|
|
||||||
rightPanelOpen,
|
|
||||||
} = useContext(OpenContext);
|
|
||||||
|
|
||||||
const {
|
const { canvasRef, canvas, setCanvas, fabricCanvasRef, canvasRatio, setCanvasHeight, setCanvasWidth, setScreenWidth, selectedPanel } = useContext(CanvasContext);
|
||||||
canvasRef,
|
|
||||||
canvas,
|
|
||||||
setCanvas,
|
|
||||||
fabricCanvasRef,
|
|
||||||
canvasRatio,
|
|
||||||
setCanvasHeight,
|
|
||||||
setCanvasWidth,
|
|
||||||
setScreenWidth,
|
|
||||||
selectedPanel,
|
|
||||||
} = useContext(CanvasContext);
|
|
||||||
|
|
||||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
const [zoomLevel, setZoomLevel] = useState(100);
|
const [zoomLevel, setZoomLevel] = useState(100);
|
||||||
|
|
@ -57,21 +41,6 @@ export default function Canvas() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
|
|
||||||
const handleMouseDown = (event) => {
|
|
||||||
const target = event.target;
|
|
||||||
const activeObject = canvas.getActiveObject();
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
if (target.type === "group") {
|
|
||||||
setActiveObject(activeObject);
|
|
||||||
} else {
|
|
||||||
setActiveObject(activeObject);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setActiveObject(activeObject);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWheel = (event) => {
|
const handleWheel = (event) => {
|
||||||
if (event.ctrlKey || event.metaKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
@ -130,8 +99,6 @@ export default function Canvas() {
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
handleZoom(zoomLevel);
|
handleZoom(zoomLevel);
|
||||||
};
|
};
|
||||||
|
|
||||||
canvas.on("mouse:down", handleMouseDown);
|
|
||||||
const canvasContainer = document.getElementById("canvas-ref");
|
const canvasContainer = document.getElementById("canvas-ref");
|
||||||
|
|
||||||
if (canvasContainer) {
|
if (canvasContainer) {
|
||||||
|
|
@ -148,7 +115,6 @@ export default function Canvas() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
canvas.off("mouse:down", handleMouseDown);
|
|
||||||
if (canvasContainer) {
|
if (canvasContainer) {
|
||||||
canvasContainer.removeEventListener("wheel", handleWheel);
|
canvasContainer.removeEventListener("wheel", handleWheel);
|
||||||
canvasContainer.removeEventListener("touchstart", handleTouchStart);
|
canvasContainer.removeEventListener("touchstart", handleTouchStart);
|
||||||
|
|
@ -158,7 +124,7 @@ export default function Canvas() {
|
||||||
window.removeEventListener("resize", handleResize);
|
window.removeEventListener("resize", handleResize);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [canvas, setActiveObject, zoomLevel, handleZoom]);
|
}, [canvas, zoomLevel, handleZoom]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
import("fabric").then((fabricModule) => {
|
import("fabric").then((fabricModule) => {
|
||||||
|
|
@ -214,20 +180,7 @@ export default function Canvas() {
|
||||||
updateCanvasSize();
|
updateCanvasSize();
|
||||||
window.addEventListener("resize", updateCanvasSize);
|
window.addEventListener("resize", updateCanvasSize);
|
||||||
return () => window.removeEventListener("resize", updateCanvasSize);
|
return () => window.removeEventListener("resize", updateCanvasSize);
|
||||||
}, [
|
}, [setCanvasWidth, setCanvasHeight, canvasRatio, canvasRef, canvas, setLeftPanelOpen, setOpenObjectPanel, setRightPanelOpen, rightPanelOpen, setScreenWidth, setOpenSetting, selectedPanel]);
|
||||||
setCanvasWidth,
|
|
||||||
setCanvasHeight,
|
|
||||||
canvasRatio,
|
|
||||||
canvasRef,
|
|
||||||
canvas,
|
|
||||||
setLeftPanelOpen,
|
|
||||||
setOpenObjectPanel,
|
|
||||||
setRightPanelOpen,
|
|
||||||
rightPanelOpen,
|
|
||||||
setScreenWidth,
|
|
||||||
setOpenSetting,
|
|
||||||
selectedPanel,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.fabric) {
|
if (window.fabric) {
|
||||||
|
|
@ -253,6 +206,35 @@ export default function Canvas() {
|
||||||
}
|
}
|
||||||
}, [canvasRef, fabricCanvasRef, setCanvas]);
|
}, [canvasRef, fabricCanvasRef, setCanvas]);
|
||||||
|
|
||||||
|
// to set active object
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canvas) return; // Ensure canvas is available
|
||||||
|
|
||||||
|
// Event handler for mouse down
|
||||||
|
const handleMouseDown = (event) => {
|
||||||
|
const target = event.target; // Get the clicked target
|
||||||
|
const activeObject = canvas.getActiveObject(); // Get the active object
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
if (target.type === 'group') {
|
||||||
|
setActiveObject(activeObject);
|
||||||
|
} else {
|
||||||
|
setActiveObject(activeObject);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setActiveObject(activeObject);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attach the event listener
|
||||||
|
canvas.on('mouse:down', handleMouseDown);
|
||||||
|
|
||||||
|
// Cleanup function to remove the event listener
|
||||||
|
return () => {
|
||||||
|
canvas.off('mouse:down', handleMouseDown); // Remove the listener on unmount
|
||||||
|
};
|
||||||
|
}, [canvas, setActiveObject]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Zoom Controls */}
|
{/* Zoom Controls */}
|
||||||
|
|
@ -269,10 +251,10 @@ export default function Canvas() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Canvas Container */}
|
{/* Canvas Container */}
|
||||||
<div className="w-full max-w-4xl mx-auto p-4">
|
<div className="w-full max-w-4xl mx-auto mt-20">
|
||||||
<Card
|
<Card
|
||||||
className={`w-full p-2 rounded-none flex-1 flex flex-col
|
className={`w-full rounded-none flex-1 flex flex-col
|
||||||
mx-auto pl-5 pb-5 pt-5 border-0 shadow-none bg-transparent mt-20
|
mx-auto border-0 shadow-none bg-transparent
|
||||||
${zoomLevel < 100 ? "overflow-hidden" : ""}`}
|
${zoomLevel < 100 ? "overflow-hidden" : ""}`}
|
||||||
>
|
>
|
||||||
<CardContent
|
<CardContent
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,68 @@
|
||||||
import { useContext, useRef, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import CanvasContext from "./Context/canvasContext/CanvasContext";
|
import CanvasContext from "./Context/canvasContext/CanvasContext";
|
||||||
import { Card, CardTitle } from "./ui/card";
|
import { Card, CardTitle } from "./ui/card";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Trash2, UploadIcon, X } from "lucide-react";
|
import { Trash2, UploadIcon } from "lucide-react";
|
||||||
import { Separator } from "./ui/separator";
|
import { Separator } from "./ui/separator";
|
||||||
import ColorComponent from "./ColorComponent";
|
import ColorComponent from "./ColorComponent";
|
||||||
import { Label } from "./ui/label";
|
import { Label } from "./ui/label";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Slider } from "./ui/slider";
|
import { Slider } from "./ui/slider";
|
||||||
import { fabric } from "fabric";
|
import { fabric } from "fabric";
|
||||||
import OpenContext from "./Context/openContext/OpenContext";
|
|
||||||
import { ScrollArea } from "./ui/scroll-area";
|
import { ScrollArea } from "./ui/scroll-area";
|
||||||
import RndComponent from "./Layouts/RndComponent";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useToast } from "../hooks/use-toast";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { deleteImage, uploadImage } from "../api/uploadApi";
|
||||||
|
import { createProject } from "../api/projectApi";
|
||||||
|
import SaveCanvas from "./SaveCanvas";
|
||||||
|
|
||||||
const CanvasSetting = () => {
|
const CanvasSetting = () => {
|
||||||
const { canvas, canvasHeight, canvasWidth, screenWidth } =
|
const { canvas } = useContext(CanvasContext);
|
||||||
useContext(CanvasContext);
|
const params = useParams();
|
||||||
const { setOpenSetting } = useContext(OpenContext);
|
const { id } = params;
|
||||||
const bgImgRef = useRef(null);
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [bgImage, setBgImage] = useState(null);
|
||||||
|
const [bgOverLayImage, setBgOverLayImage] = useState(null);
|
||||||
|
|
||||||
const [canvasSettings, setCanvasSettings] = useState({
|
// create empty project if no id is provided
|
||||||
width: canvas?.width,
|
useEffect(() => {
|
||||||
height: canvas?.height,
|
const createEmptyProject = async () => {
|
||||||
perPixelTargetFind: true, // Enable per-pixel detection globally
|
try {
|
||||||
targetFindTolerance: 4, // Adjust for leniency in pixel-perfect detection
|
const response = await createProject();
|
||||||
});
|
console.log(response);
|
||||||
|
if (response?.data?.id) {
|
||||||
const handleChange = (key, value) => {
|
navigate(`/${response.data.id}`);
|
||||||
setCanvasSettings((prevSettings) => ({
|
|
||||||
...prevSettings,
|
|
||||||
[key]: value,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Update canvas dimensions
|
|
||||||
if (key === "width") {
|
|
||||||
canvas.setWidth(value); // Update canvas width
|
|
||||||
} else if (key === "height") {
|
|
||||||
canvas.setHeight(value); // Update canvas height
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
// Adjust the background image to fit the updated canvas
|
console.error("Project creation failed:", error);
|
||||||
const bgImage = canvas.backgroundImage;
|
|
||||||
if (bgImage) {
|
|
||||||
const canvasWidth = canvas.width;
|
|
||||||
const canvasHeight = canvas.height;
|
|
||||||
|
|
||||||
// Calculate scaling factors for width and height
|
|
||||||
const scaleX = canvasWidth / bgImage.width;
|
|
||||||
const scaleY = canvasHeight / bgImage.height;
|
|
||||||
|
|
||||||
// Choose the larger scale to cover the entire canvas
|
|
||||||
const scale = Math.max(scaleX, scaleY);
|
|
||||||
|
|
||||||
// Apply the scale and center the image
|
|
||||||
bgImage.scaleX = scale;
|
|
||||||
bgImage.scaleY = scale;
|
|
||||||
bgImage.left = 0; // Align left
|
|
||||||
bgImage.top = 0; // Align top
|
|
||||||
|
|
||||||
// Mark the background image as needing an update
|
|
||||||
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
|
|
||||||
} else {
|
|
||||||
canvas.renderAll(); // Render if no background image
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (!id) {
|
||||||
|
createEmptyProject();
|
||||||
|
}
|
||||||
|
}, [id, navigate]);
|
||||||
|
|
||||||
const setBackgroundImage = (e) => {
|
// upload bg-image handler
|
||||||
const file = e.target.files[0];
|
const { mutate: uploadBackgroundImage } = useMutation({
|
||||||
if (file) {
|
mutationFn: async ({ file, id }) => {
|
||||||
const blobUrl = URL.createObjectURL(file);
|
return await uploadImage({ file, id });
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
});
|
||||||
|
setBgImage(data?.data[0]?.url);
|
||||||
|
|
||||||
// Create an image element
|
// Create an image element
|
||||||
const imgElement = new Image();
|
const imgElement = new Image();
|
||||||
imgElement.src = blobUrl;
|
|
||||||
|
// Set the crossOrigin attribute BEFORE setting the src
|
||||||
|
imgElement.crossOrigin = "anonymous"; // This ensures CORS headers are sent
|
||||||
|
imgElement.src = data?.data[0]?.url;
|
||||||
|
|
||||||
imgElement.onload = () => {
|
imgElement.onload = () => {
|
||||||
// Create a fabric.Image instance
|
// Create a fabric.Image instance
|
||||||
|
|
@ -83,41 +73,152 @@ const CanvasSetting = () => {
|
||||||
|
|
||||||
// Set the background image on the canvas
|
// Set the background image on the canvas
|
||||||
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
|
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
|
||||||
|
|
||||||
// Revoke the object URL to free memory
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
imgElement.onerror = () => {
|
imgElement.onerror = (error) => {
|
||||||
console.error("Failed to load the image.");
|
console.error('Failed to load image:', error);
|
||||||
URL.revokeObjectURL(blobUrl);
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Image Load Error",
|
||||||
|
description: "Failed to load the image. Please try again."
|
||||||
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle bg-image remove
|
||||||
|
const { mutate: removeBackgroundMutate } = useMutation({
|
||||||
|
mutationFn: async (url) => {
|
||||||
|
return await deleteImage(url);
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
})
|
||||||
|
canvas.backgroundImage = null;
|
||||||
|
canvas.renderAll();
|
||||||
|
setBgImage(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// upload bg-overLayImage handler
|
||||||
|
const { mutate: uploadOverlayImage } = useMutation({
|
||||||
|
mutationFn: async ({ file, id }) => {
|
||||||
|
return await uploadImage({ file, id });
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
});
|
||||||
|
setBgOverLayImage(data?.data[0]?.url);
|
||||||
|
// Create an image element
|
||||||
|
const imgElement = new Image();
|
||||||
|
|
||||||
|
// Set the crossOrigin attribute BEFORE setting the src
|
||||||
|
imgElement.crossOrigin = "anonymous"; // This ensures CORS headers are sent
|
||||||
|
imgElement.src = data?.data[0]?.url;
|
||||||
|
|
||||||
|
imgElement.onload = () => {
|
||||||
|
// Create a fabric.Image instance
|
||||||
|
const img = new fabric.Image(imgElement, {
|
||||||
|
scaleX: canvas.width / imgElement.width,
|
||||||
|
scaleY: canvas.height / imgElement.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the background image on the canvas
|
||||||
|
canvas.setOverlayImage(img, canvas.renderAll.bind(canvas));
|
||||||
|
};
|
||||||
|
|
||||||
|
imgElement.onerror = (error) => {
|
||||||
|
console.error('Failed to load image:', error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Image Load Error",
|
||||||
|
description: "Failed to load the image. Please try again."
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle bg-overLayImage remove
|
||||||
|
const { mutate: removeOverLayMutate } = useMutation({
|
||||||
|
mutationFn: async (url) => {
|
||||||
|
return await deleteImage(url);
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
})
|
||||||
|
canvas.overlayImage = null;
|
||||||
|
canvas.renderAll();
|
||||||
|
setBgOverLayImage(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setBackgroundImage = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
uploadBackgroundImage({ file, id })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Error",
|
||||||
|
description: "Please select a file",
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setBackgroundOverlayImage = (e) => {
|
const setBackgroundOverlayImage = (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
const blobUrl = URL.createObjectURL(file);
|
uploadOverlayImage({ file, id })
|
||||||
|
}
|
||||||
// Create an image element
|
else {
|
||||||
const imgElement = new Image();
|
toast({
|
||||||
imgElement.src = blobUrl;
|
variant: "destructive",
|
||||||
|
title: "Error",
|
||||||
imgElement.onload = () => {
|
description: "Please select a file",
|
||||||
const img = new fabric.Image(imgElement, {
|
})
|
||||||
scaleX: canvas.width / imgElement.width,
|
|
||||||
scaleY: canvas.height / imgElement.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use setOverlayImage method to add the image as an overlay
|
|
||||||
canvas.setOverlayImage(img, () => {
|
|
||||||
canvas.renderAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Revoke the object URL after image is loaded and set
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -143,44 +244,52 @@ const CanvasSetting = () => {
|
||||||
|
|
||||||
const removeBackgroundImage = () => {
|
const removeBackgroundImage = () => {
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
canvas.backgroundImage = null;
|
const bgUrl = canvas.backgroundImage?.getSrc();
|
||||||
canvas.renderAll();
|
console.log("background image from remove", bgUrl)
|
||||||
|
if (bgUrl) {
|
||||||
|
removeBackgroundMutate(bgUrl)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Error",
|
||||||
|
description: "No background image found",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (bgImgRef.current) {
|
|
||||||
bgImgRef.current.value = "";
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeBackgroundOverlayImage = () => {
|
const removeBackgroundOverlayImage = () => {
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
canvas.overlayImage = null;
|
const overLayUrl = canvas.overlayImage?.getSrc();
|
||||||
canvas.renderAll();
|
console.log("overlay image from remove", overLayUrl);
|
||||||
|
if (overLayUrl) {
|
||||||
|
removeOverLayMutate(overLayUrl);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Error",
|
||||||
|
description: "No overlay image found",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const rndValue = {
|
useEffect(() => {
|
||||||
valueX: 0,
|
if (canvas) {
|
||||||
valueY: 20,
|
const bgUrl = canvas.backgroundImage?.getSrc();
|
||||||
width: 250,
|
setBgImage(bgUrl);
|
||||||
height: 0,
|
const overLayUrl = canvas.overlayImage?.getSrc();
|
||||||
minWidth: 250,
|
setBgOverLayImage(overLayUrl);
|
||||||
maxWidth: 300,
|
}
|
||||||
minHeight: 0,
|
}, [canvas, setBgImage, setBgOverLayImage])
|
||||||
maxHeight: 400,
|
|
||||||
bound: "parent",
|
|
||||||
};
|
|
||||||
const content = () => {
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
<Card className="xl:p-0 lg:p-0 md:p-0 p-2 border-none shadow-none">
|
<Card className="xl:p-0 lg:p-0 md:p-0 p-2 border-none shadow-none">
|
||||||
<CardTitle className="flex items-center flex-wrap justify-between gap-1 xl:hidden lg:hidden md:hidden">
|
<CardTitle className="flex items-center flex-wrap justify-between gap-1 xl:hidden lg:hidden md:hidden">
|
||||||
Canvas Setting{" "}
|
Canvas Setting
|
||||||
<Button
|
|
||||||
className="rnd-escape"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setOpenSetting(false)}
|
|
||||||
>
|
|
||||||
<X />
|
|
||||||
</Button>{" "}
|
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Separator className="mt-4 block xl:hidden lg:hidden md:hidden" />
|
<Separator className="mt-4 block xl:hidden lg:hidden md:hidden" />
|
||||||
<ScrollArea className="h-[400px] xl:h-fit lg:h-fit md:h-fit">
|
<ScrollArea className="h-[400px] xl:h-fit lg:h-fit md:h-fit">
|
||||||
|
|
@ -192,32 +301,49 @@ const CanvasSetting = () => {
|
||||||
<div className="flex flex-col my-2 gap-2 rnd-escape">
|
<div className="flex flex-col my-2 gap-2 rnd-escape">
|
||||||
<div>
|
<div>
|
||||||
<Label>Background:</Label>
|
<Label>Background:</Label>
|
||||||
|
{
|
||||||
|
bgImage && <img src={bgImage} alt="canvas_bg_image" className="rounded-md mb-2" />
|
||||||
|
}
|
||||||
<div className="flex items-center w-fit gap-2 flex-wrap relative">
|
<div className="flex items-center w-fit gap-2 flex-wrap relative">
|
||||||
|
{
|
||||||
|
!bgImage &&
|
||||||
<Button className="top-0 absolute flex items-center w-[30px]">
|
<Button className="top-0 absolute flex items-center w-[30px]">
|
||||||
<UploadIcon className="cursor-pointer" />
|
<UploadIcon className="cursor-pointer" />
|
||||||
<Input
|
<Input
|
||||||
ref={bgImgRef}
|
|
||||||
className="absolute top-0 opacity-0"
|
className="absolute top-0 opacity-0"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
onChange={setBackgroundImage}
|
onChange={setBackgroundImage}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
bgImage &&
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="ml-[35px]"
|
|
||||||
onClick={removeBackgroundImage}
|
onClick={removeBackgroundImage}
|
||||||
>
|
>
|
||||||
<Trash2 />
|
<Trash2 />
|
||||||
</Button>
|
</Button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
bgImage ? <Separator className="mt-4" /> : <Separator className="mt-12" />
|
||||||
|
}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Background Overlay:</Label>
|
<Label>Background Overlay:</Label>
|
||||||
|
{
|
||||||
|
bgOverLayImage && <img src={bgOverLayImage} alt="canvas_bgOverLay_image" className="rounded-md mb-2" />
|
||||||
|
}
|
||||||
<div className="flex items-center w-fit gap-2 flex-wrap relative">
|
<div className="flex items-center w-fit gap-2 flex-wrap relative">
|
||||||
<Button className="top-0 absolute flex items-center w-[30px]">
|
{
|
||||||
|
!bgOverLayImage &&
|
||||||
|
<Button className="top-0 absolute flex items-center w-[30px] mb">
|
||||||
<UploadIcon className="cursor-pointer" />
|
<UploadIcon className="cursor-pointer" />
|
||||||
<Input
|
<Input
|
||||||
className="absolute top-0 opacity-0"
|
className="absolute top-0 opacity-0"
|
||||||
|
|
@ -226,16 +352,23 @@ const CanvasSetting = () => {
|
||||||
onChange={setBackgroundOverlayImage}
|
onChange={setBackgroundOverlayImage}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
bgOverLayImage &&
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="ml-[35px]"
|
|
||||||
onClick={removeBackgroundOverlayImage}
|
onClick={removeBackgroundOverlayImage}
|
||||||
>
|
>
|
||||||
<Trash2 />
|
<Trash2 />
|
||||||
</Button>
|
</Button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
bgOverLayImage ? <Separator className="mt-4" /> : <Separator className="mt-12" />
|
||||||
|
}
|
||||||
|
|
||||||
{/* opacity */}
|
{/* opacity */}
|
||||||
<div className="flex flex-col gap-2 rnd-escape mt-2">
|
<div className="flex flex-col gap-2 rnd-escape mt-2">
|
||||||
|
|
@ -255,46 +388,16 @@ const CanvasSetting = () => {
|
||||||
|
|
||||||
<Separator className="mt-4" />
|
<Separator className="mt-4" />
|
||||||
|
|
||||||
{/* canvas size customization (width/height) */}
|
{/* Save canvas Component */}
|
||||||
<div className="flex gap-2 my-2">
|
{
|
||||||
<div className="flex flex-col gap-2">
|
id &&
|
||||||
<Label>Width:</Label>
|
<SaveCanvas />
|
||||||
<Input
|
|
||||||
min={300}
|
|
||||||
type="number"
|
|
||||||
value={canvasSettings.width}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (canvasWidth > parseInt(e.target.value)) {
|
|
||||||
handleChange("width", parseInt(e.target.value, 10));
|
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label>Height:</Label>
|
|
||||||
<Input
|
|
||||||
min={300}
|
|
||||||
type="number"
|
|
||||||
value={canvasSettings.height}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (canvasHeight > parseInt(e.target.value)) {
|
|
||||||
handleChange("height", parseInt(e.target.value, 10));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
</div>
|
||||||
};
|
|
||||||
|
|
||||||
return screenWidth <= 768 ? (
|
|
||||||
<RndComponent value={rndValue}>{content()}</RndComponent>
|
|
||||||
) : (
|
|
||||||
<div>{content()}</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
5
src/components/Context/authContext/AuthContext.js
Normal file
5
src/components/Context/authContext/AuthContext.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import react from "react";
|
||||||
|
|
||||||
|
const AuthContext = react.createContext();
|
||||||
|
|
||||||
|
export default AuthContext;
|
||||||
18
src/components/Context/authContext/AuthProvider.jsx
Normal file
18
src/components/Context/authContext/AuthProvider.jsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import AuthContext from "./authContext";
|
||||||
|
|
||||||
|
const AuthContextProvider = ({ children }) => {
|
||||||
|
const [user, setUser] = useState({
|
||||||
|
token: null,
|
||||||
|
userId: null,
|
||||||
|
currentProjectId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ user, setUser }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthContextProvider;
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useContext, 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 { fabric } from "fabric";
|
||||||
|
|
@ -25,6 +25,11 @@ 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 { useMutation } from "@tanstack/react-query";
|
||||||
|
import { deleteImage, uploadImage } from "../../api/uploadApi";
|
||||||
|
import { createProject } from "../../api/projectApi";
|
||||||
|
import { useToast } from "../../hooks/use-toast";
|
||||||
|
|
||||||
const UploadImage = () => {
|
const UploadImage = () => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
@ -37,9 +42,92 @@ const UploadImage = () => {
|
||||||
|
|
||||||
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
|
const params = useParams();
|
||||||
|
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 navigate = useNavigate();
|
||||||
|
|
||||||
|
// create empty project if no id is provided
|
||||||
|
useEffect(() => {
|
||||||
|
const createEmptyProject = async () => {
|
||||||
|
try {
|
||||||
|
const response = await createProject();
|
||||||
|
if (response?.data?.id) {
|
||||||
|
navigate(`/${response.data.id}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Project creation failed:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!id) {
|
||||||
|
createEmptyProject();
|
||||||
|
}
|
||||||
|
}, [id, navigate]);
|
||||||
|
|
||||||
|
// upload image handler
|
||||||
|
const { mutate } = useMutation({
|
||||||
|
mutationFn: async ({ file, id }) => {
|
||||||
|
return await uploadImage({ file, id });
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
});
|
||||||
|
fabric.Image.fromURL(
|
||||||
|
data?.data[0]?.url,
|
||||||
|
(img) => {
|
||||||
|
img.applyFilters();
|
||||||
|
img.scale(0.5);
|
||||||
|
img.set("top", canvas.width / 4);
|
||||||
|
img.set("left", canvas.height / 4);
|
||||||
|
canvas.add(img);
|
||||||
|
canvas.setActiveObject(img);
|
||||||
|
// Update the active object state
|
||||||
|
setActiveObject(img);
|
||||||
|
canvas.renderAll();
|
||||||
|
},
|
||||||
|
{ crossOrigin: "anonymous" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
});
|
||||||
|
removeFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle image remove
|
||||||
|
const { mutate: deleteMutate } = useMutation({
|
||||||
|
mutationFn: async (url) => {
|
||||||
|
return await deleteImage(url);
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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", ".gif", ".jpg", ".webp", ".svg"],
|
||||||
|
|
@ -55,7 +143,6 @@ const UploadImage = () => {
|
||||||
// Create a preview URL
|
// Create a preview URL
|
||||||
const blobUrl = URL.createObjectURL(selectedFile);
|
const blobUrl = URL.createObjectURL(selectedFile);
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreview(blobUrl);
|
|
||||||
|
|
||||||
if (selectedFile.type === "image/svg+xml") {
|
if (selectedFile.type === "image/svg+xml") {
|
||||||
addImageToCanvas(selectedFile);
|
addImageToCanvas(selectedFile);
|
||||||
|
|
@ -94,44 +181,65 @@ const UploadImage = () => {
|
||||||
fileInputRef.current.value = "";
|
fileInputRef.current.value = "";
|
||||||
}
|
}
|
||||||
if (activeObject?.type === "image") {
|
if (activeObject?.type === "image") {
|
||||||
|
const imgUrl = activeObject?._originalElement?.currentSrc;
|
||||||
canvas.remove(activeObject);
|
canvas.remove(activeObject);
|
||||||
setActiveObject(null);
|
setActiveObject(null);
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
|
deleteMutate(imgUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = (file, callback) => {
|
const handleResize = (selectedFile, callback) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = URL.createObjectURL(selectedFile);
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
let newWidth = width; // User-defined width
|
||||||
|
let newHeight = height; // User-defined height
|
||||||
|
|
||||||
|
const aspectRatio = img.width / img.height;
|
||||||
|
|
||||||
|
// If the user provided only one dimension, maintain aspect ratio
|
||||||
|
if (!width) {
|
||||||
|
newWidth = Math.round(newHeight * aspectRatio);
|
||||||
|
} else if (!height) {
|
||||||
|
newHeight = Math.round(newWidth / aspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
Resizer.imageFileResizer(
|
Resizer.imageFileResizer(
|
||||||
file,
|
selectedFile,
|
||||||
width,
|
newWidth, // Use user-defined width or calculated width
|
||||||
height,
|
newHeight, // Use user-defined height or calculated height
|
||||||
format,
|
format,
|
||||||
quality,
|
quality,
|
||||||
parseInt(rotation),
|
parseInt(rotation),
|
||||||
(resizedFile) => {
|
(resizedFile) => {
|
||||||
callback(resizedFile);
|
callback(resizedFile);
|
||||||
},
|
},
|
||||||
"file"
|
'file'
|
||||||
);
|
);
|
||||||
|
URL.revokeObjectURL(img.src); // Cleanup
|
||||||
};
|
};
|
||||||
|
|
||||||
const addImageToCanvas = (file) => {
|
img.onerror = () => {
|
||||||
const blobUrl = URL.createObjectURL(file);
|
console.error("Failed to load image for resizing.");
|
||||||
fabric.Image.fromURL(blobUrl, (img) => {
|
URL.revokeObjectURL(img.src); // Cleanup
|
||||||
img.set({
|
|
||||||
left: canvas.width / 4,
|
|
||||||
top: canvas.height / 4,
|
|
||||||
scaleX: 0.5,
|
|
||||||
scaleY: 0.5,
|
|
||||||
});
|
|
||||||
canvas.add(img);
|
|
||||||
canvas.setActiveObject(img);
|
|
||||||
// Update the active object state
|
|
||||||
setActiveObject(img);
|
|
||||||
canvas.renderAll();
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const addImageToCanvas = (selectedFile) => {
|
||||||
|
mutate({ file: selectedFile, id })
|
||||||
|
};
|
||||||
|
|
||||||
|
// useEffect for preview update
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeObject?.type === "image") {
|
||||||
|
setPreview(activeObject._originalElement?.currentSrc);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setPreview(null);
|
||||||
|
}
|
||||||
|
}, [activeObject]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full border-none shadow-none">
|
<Card className="w-full border-none shadow-none">
|
||||||
|
|
@ -223,8 +331,7 @@ const UploadImage = () => {
|
||||||
<CardContent className="p-6 space-y-4">
|
<CardContent className="p-6 space-y-4">
|
||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors duration-300 ${
|
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors duration-300 ${isDragActive
|
||||||
isDragActive
|
|
||||||
? "border-primary bg-primary/10"
|
? "border-primary bg-primary/10"
|
||||||
: "border-gray-300 hover:border-primary"
|
: "border-gray-300 hover:border-primary"
|
||||||
} ${preview ? "hidden" : ""}`}
|
} ${preview ? "hidden" : ""}`}
|
||||||
|
|
@ -233,8 +340,7 @@ const UploadImage = () => {
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
<div className="flex flex-col items-center justify-center space-y-4">
|
<div className="flex flex-col items-center justify-center space-y-4">
|
||||||
<ImageIcon
|
<ImageIcon
|
||||||
className={`h-12 w-12 ${
|
className={`h-12 w-12 ${isDragActive ? "text-primary" : "text-gray-400"
|
||||||
isDragActive ? "text-primary" : "text-gray-400"
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
|
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import { fabric } from 'fabric';
|
|
||||||
|
|
||||||
const FabricCanvas = () => {
|
|
||||||
const canvasRef = useRef(null);
|
|
||||||
const [canvas, setCanvas] = useState(null);
|
|
||||||
|
|
||||||
// Canvas settings
|
|
||||||
const canvasSettings = {
|
|
||||||
width: 800,
|
|
||||||
height: 600,
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Initialize the canvas
|
|
||||||
const newCanvas = new fabric.Canvas(canvasRef.current, {
|
|
||||||
backgroundColor: 'black',
|
|
||||||
selection: true,
|
|
||||||
height: canvasSettings.height,
|
|
||||||
width: canvasSettings.width,
|
|
||||||
controlsAboveOverlay: true,
|
|
||||||
preserveObjectStacking: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the clipping path (rectangle)
|
|
||||||
const clipPath = new fabric.Rect({
|
|
||||||
width: 300,
|
|
||||||
height: 300,
|
|
||||||
left: (canvasSettings.width - 300) / 2, // Center horizontally
|
|
||||||
top: (canvasSettings.height - 300) / 2, // Center vertically
|
|
||||||
fill: 'transparent', // Transparent fill
|
|
||||||
stroke: 'red', // White outline
|
|
||||||
strokeWidth: 2,
|
|
||||||
absolutePositioned: true, // Important for clipping path
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply the clipPath to the canvas
|
|
||||||
newCanvas.clipPath = clipPath;
|
|
||||||
|
|
||||||
// Add the clipping path to visualize it
|
|
||||||
newCanvas.add(clipPath);
|
|
||||||
|
|
||||||
// Add a sample object to the canvas
|
|
||||||
const rect = new fabric.Rect({
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
fill: 'orange',
|
|
||||||
left: 200,
|
|
||||||
top: 150,
|
|
||||||
});
|
|
||||||
newCanvas.add(rect);
|
|
||||||
|
|
||||||
// Save the canvas instance
|
|
||||||
setCanvas(newCanvas);
|
|
||||||
|
|
||||||
// Clean up on unmount
|
|
||||||
return () => {
|
|
||||||
newCanvas.dispose();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<canvas ref={canvasRef} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FabricCanvas;
|
|
||||||
|
|
@ -24,7 +24,7 @@ const sidebarItems = [
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const { selectedPanel, setSelectedPanel } = useContext(CanvasContext);
|
const { selectedPanel, setSelectedPanel } = useContext(CanvasContext);
|
||||||
return (
|
return (
|
||||||
<div className="w-20 border-r bg-background flex flex-col items-center py-4 gap-6 md:pt-16 xl:pt-0">
|
<div className="w-20 border-r bg-background flex flex-col items-center justify-center py-4 gap-6 md:pt-16 xl:pt-0 h-full">
|
||||||
{sidebarItems.map((item) => {
|
{sidebarItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
import { Sheet, SheetContent, SheetDescription } from '../ui/sheet';
|
|
||||||
import AddShapes from './AddShapes';
|
|
||||||
import { Separator } from '../ui/separator';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { X, Store, Shapes, Upload } from "lucide-react";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
|
||||||
import AllIconsPage from '../EachComponent/Icons/AllIcons';
|
|
||||||
import UploadImage from '../EachComponent/UploadImage';
|
|
||||||
import { useContext } from 'react';
|
|
||||||
import OpenContext from '../Context/openContext/OpenContext';
|
|
||||||
|
|
||||||
const left = "left";
|
|
||||||
|
|
||||||
const SheetLeftPanel = () => {
|
|
||||||
const { leftPanelOpen, setLeftPanelOpen } = useContext(OpenContext);
|
|
||||||
|
|
||||||
// Prevent closing on outside clicks
|
|
||||||
const handleOpenChange = (isOpen) => {
|
|
||||||
if (!isOpen) {
|
|
||||||
// Do nothing when clicking outside
|
|
||||||
return; // Sheet won't close
|
|
||||||
}
|
|
||||||
setLeftPanelOpen(isOpen); // Update only on valid trigger
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to handle closing the sheet
|
|
||||||
const handleClose = () => {
|
|
||||||
setLeftPanelOpen(false); // Close when button is clicked
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sheet onOpenChange={handleOpenChange} open={leftPanelOpen} modal={false}>
|
|
||||||
<SheetContent side={left} className="w-[300px] top-[60px]">
|
|
||||||
<SheetDescription className="text-left flex font-semibold">
|
|
||||||
<p>Your customizable, canvas playground.</p>
|
|
||||||
<Button variant={"outline"} onClick={handleClose}>
|
|
||||||
<X />
|
|
||||||
</Button>
|
|
||||||
</SheetDescription>
|
|
||||||
|
|
||||||
<Separator className="my-2" />
|
|
||||||
<Tabs defaultValue="shapes" className="w-full relative">
|
|
||||||
<TabsList className="grid w-full grid-cols-3 gap-1 h-fit">
|
|
||||||
<TabsTrigger value="shapes" className="truncate text-xs grid items-center justify-center">
|
|
||||||
<Shapes className="h-4 w-4 mx-auto" />
|
|
||||||
<p>Shapes & Text</p>
|
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger value="icons" className="truncate text-xs grid items-center justify-center">
|
|
||||||
<Store className="h-4 w-4 mx-auto" />
|
|
||||||
<p>Icons</p>
|
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger value="image" className="truncate text-xs grid items-center justify-center">
|
|
||||||
<Upload className="h-4 w-4 mx-auto" />
|
|
||||||
<p>Image</p>
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<Separator className="my-2" />
|
|
||||||
|
|
||||||
<TabsContent value="shapes" className="mt-2 h-[450px] overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white">
|
|
||||||
<AddShapes />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="icons" className="mt-2">
|
|
||||||
<AllIconsPage />
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="image" className="mt-2 h-[450px] overflow-y-scroll px-1 scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white">
|
|
||||||
<UploadImage />
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SheetLeftPanel
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
} from "@/components/ui/sheet"
|
|
||||||
import CustomizeShape from "../EachComponent/CustomizeShape";
|
|
||||||
import { Separator } from "../ui/separator";
|
|
||||||
import { Button } from "../ui/button";
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { useContext, useEffect, useState } from "react";
|
|
||||||
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
|
||||||
import OpenContext from "../Context/openContext/OpenContext";
|
|
||||||
import CanvasSetting from "../CanvasSetting";
|
|
||||||
import CollapsibleComponent from "../EachComponent/Customization/CollapsibleComponent";
|
|
||||||
import { Card } from "../ui/card";
|
|
||||||
|
|
||||||
const SheetRightPanel = () => {
|
|
||||||
const { rightPanelOpen, setRightPanelOpen } = useContext(OpenContext)
|
|
||||||
|
|
||||||
const { activeObject } = useContext(ActiveObjectContext);
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeObject) {
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
}, [activeObject])
|
|
||||||
|
|
||||||
// Prevent closing on outside clicks
|
|
||||||
const handleOpenChange = (isOpen) => {
|
|
||||||
if (!isOpen) {
|
|
||||||
|
|
||||||
// Do nothing when clicking outside
|
|
||||||
return; // Sheet won't close
|
|
||||||
}
|
|
||||||
setRightPanelOpen(isOpen); // Update only
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to handle closing the sheet
|
|
||||||
const handleClose = () => {
|
|
||||||
setRightPanelOpen(false); // Close when button is clicked
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Sheet onOpenChange={handleOpenChange} open={rightPanelOpen} modal={false}>
|
|
||||||
<SheetContent className="w-[300px] top-[60px]">
|
|
||||||
<SheetHeader>
|
|
||||||
<SheetTitle className="text-left flex items-center gap-1 flex-wrap justify-between">
|
|
||||||
<SheetDescription>Edit Customization</SheetDescription>
|
|
||||||
<Button variant={"outline"} onClick={handleClose}>
|
|
||||||
<X />
|
|
||||||
</Button>
|
|
||||||
</SheetTitle>
|
|
||||||
|
|
||||||
<SheetDescription className="text-left text-xs">
|
|
||||||
Customize each shapes, and text as per your choice.
|
|
||||||
</SheetDescription>
|
|
||||||
|
|
||||||
</SheetHeader>
|
|
||||||
|
|
||||||
<Separator className="my-2" />
|
|
||||||
|
|
||||||
<div className="h-[500px] overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white">
|
|
||||||
<div>
|
|
||||||
<Card className="p-2 mx-1">
|
|
||||||
<CollapsibleComponent text={"Canvas Setting"}>
|
|
||||||
<div className="mt-2">
|
|
||||||
<CanvasSetting />
|
|
||||||
</div>
|
|
||||||
</CollapsibleComponent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="my-2">
|
|
||||||
<Separator className="my-2" />
|
|
||||||
{
|
|
||||||
open ? <CustomizeShape /> : <p className='text-sm font-semibold'>No active object found</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SheetRightPanel;
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
import { Button } from './ui/button'
|
|
||||||
import { Card, CardHeader, CardTitle } from './ui/card'
|
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible'
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs'
|
|
||||||
import { useContext, useState } from 'react'
|
|
||||||
import { PencilRuler, Shapes, Store, Upload, ChevronDown, ChevronUp, X } from 'lucide-react';
|
|
||||||
import OpenContext from './Context/openContext/OpenContext';
|
|
||||||
import AllIconsPage from './EachComponent/Icons/AllIcons'
|
|
||||||
import { Separator } from './ui/separator'
|
|
||||||
import { ScrollArea } from './ui/scroll-area'
|
|
||||||
import AddShapes from './Layouts/AddShapes'
|
|
||||||
import UploadImage from './EachComponent/UploadImage'
|
|
||||||
import CustomizeShape from './EachComponent/CustomizeShape'
|
|
||||||
import RndComponent from './Layouts/RndComponent'
|
|
||||||
|
|
||||||
const ObjectPanel = () => {
|
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
||||||
const { tabValue, setTabValue, setOpenObjectPanel } = useContext(OpenContext);
|
|
||||||
|
|
||||||
const rndValue = {
|
|
||||||
valueX: 0,
|
|
||||||
valueY: 20,
|
|
||||||
width: 250,
|
|
||||||
height: 0,
|
|
||||||
minWidth: 280,
|
|
||||||
maxWidth: 300,
|
|
||||||
minHeight: 0,
|
|
||||||
maxHeight: 500,
|
|
||||||
bound: "parent"
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RndComponent value={rndValue}>
|
|
||||||
<Card className="w-full shadow-lg px-1">
|
|
||||||
<CardHeader className="p-1 mr-12">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Button className="rnd-escape" variant={"ghost"} size={"sm"} onClick={() => setOpenObjectPanel(false)}><X /></Button>
|
|
||||||
<CardTitle className="text-lg">Object Panel</CardTitle>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<Collapsible className="rnd-escape" open={!isCollapsed} onOpenChange={(open) => setIsCollapsed(!open)}>
|
|
||||||
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
<Button variant="ghost" size="sm" className="absolute top-1 right-2">
|
|
||||||
{isCollapsed ? <ChevronDown className="h-4 w-4" /> : <ChevronUp className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
|
|
||||||
<CollapsibleContent>
|
|
||||||
<div className="h-[450px] xl:h-fit lg:h-fit md:h-fit overflow-y-scroll scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white px-1 mb-2">
|
|
||||||
<Tabs className="w-full h-fit"
|
|
||||||
value={tabValue}
|
|
||||||
onValueChange={(value) => setTabValue(value)} // Sync tab state with context
|
|
||||||
>
|
|
||||||
|
|
||||||
<TabsList className="grid w-full grid-cols-4 h-fit gap-1">
|
|
||||||
<TabsTrigger
|
|
||||||
value="icons"
|
|
||||||
className="flex flex-col gap-1 text-xs"
|
|
||||||
onClick={() => setTabValue("icons")}
|
|
||||||
>
|
|
||||||
<Store className="h-4 w-4" />
|
|
||||||
<span className="hidden sm:block">Icons</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger
|
|
||||||
value="shapes"
|
|
||||||
className="flex flex-col gap-1 text-xs"
|
|
||||||
onClick={() => setTabValue("shapes")}
|
|
||||||
>
|
|
||||||
<Shapes className="h-4 w-4" />
|
|
||||||
<span className="hidden sm:block">Shapes</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger
|
|
||||||
value="images"
|
|
||||||
className="flex flex-col gap-1 text-xs"
|
|
||||||
onClick={() => setTabValue("images")}
|
|
||||||
>
|
|
||||||
<Upload className="h-4 w-4" />
|
|
||||||
<span className="hidden sm:block">Images</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
|
|
||||||
<TabsTrigger
|
|
||||||
value="customize"
|
|
||||||
className="flex flex-col gap-1 text-xs"
|
|
||||||
onClick={() => setTabValue("customize")}
|
|
||||||
>
|
|
||||||
<PencilRuler className="h-4 w-4" />
|
|
||||||
<span className="hidden sm:block">Customize</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{/* All icons */}
|
|
||||||
<TabsContent value="icons" className="mt-2">
|
|
||||||
<div className="grid grid-cols-1">
|
|
||||||
<AllIconsPage />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* All shapes */}
|
|
||||||
<TabsContent value="shapes" className="mt-2">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="p-1">Shapes </CardTitle>
|
|
||||||
<Separator className="my-2" />
|
|
||||||
<ScrollArea className="h-[450px]">
|
|
||||||
<AddShapes />
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Upload images */}
|
|
||||||
<TabsContent value="images" className="mt-2">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="p-1">Upload</CardTitle>
|
|
||||||
<Separator className="my-2" />
|
|
||||||
<ScrollArea className="h-[450px]">
|
|
||||||
<Card>
|
|
||||||
<UploadImage />
|
|
||||||
</Card>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
{/* Customization */}
|
|
||||||
<TabsContent value="customize" className="mt-2">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="p-1">Object customization </CardTitle>
|
|
||||||
<Separator className="my-2" />
|
|
||||||
<ScrollArea className="h-[450px]">
|
|
||||||
<CustomizeShape />
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</CollapsibleContent>
|
|
||||||
|
|
||||||
</Collapsible>
|
|
||||||
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</RndComponent>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ObjectPanel
|
|
||||||
|
|
@ -20,6 +20,9 @@ import {
|
||||||
Layers,
|
Layers,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { deleteImage } from "../api/uploadApi";
|
||||||
|
import { toast } from "../hooks/use-toast";
|
||||||
|
|
||||||
export const ObjectShortcut = ({ value }) => {
|
export const ObjectShortcut = ({ value }) => {
|
||||||
const { canvas } = useContext(CanvasContext);
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
|
@ -51,7 +54,11 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
setActiveObject(group);
|
setActiveObject(group);
|
||||||
canvas.renderAll();
|
canvas.renderAll();
|
||||||
} else {
|
} else {
|
||||||
console.log("Select at least two objects");
|
toast({
|
||||||
|
title: "Select at least two objects",
|
||||||
|
description: "Please select at least two objects to group.",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -136,6 +143,27 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { mutate: deleteMutate } = useMutation({
|
||||||
|
mutationFn: async (url) => {
|
||||||
|
return await deleteImage(url);
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Remove Selected Element
|
// Remove Selected Element
|
||||||
const removeSelected = useCallback(() => {
|
const removeSelected = useCallback(() => {
|
||||||
const activeObject = canvas?.getActiveObject();
|
const activeObject = canvas?.getActiveObject();
|
||||||
|
|
@ -159,7 +187,15 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
canvas.remove(...activeObject);
|
canvas.remove(...activeObject);
|
||||||
setActiveObject(null);
|
setActiveObject(null);
|
||||||
}
|
}
|
||||||
}, [canvas, setActiveObject]);
|
|
||||||
|
if (activeObject?.type === "image") {
|
||||||
|
const imgUrl = activeObject?._originalElement?.currentSrc;
|
||||||
|
canvas.remove(activeObject);
|
||||||
|
setActiveObject(null);
|
||||||
|
canvas.renderAll();
|
||||||
|
deleteMutate(imgUrl);
|
||||||
|
}
|
||||||
|
}, [canvas, setActiveObject, deleteMutate]);
|
||||||
|
|
||||||
// duplicating current objects
|
// duplicating current objects
|
||||||
const duplicating = () => {
|
const duplicating = () => {
|
||||||
|
|
@ -184,8 +220,7 @@ export const ObjectShortcut = ({ value }) => {
|
||||||
<div>
|
<div>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div
|
<div
|
||||||
className={`flex items-center gap-2 ${
|
className={`flex items-center gap-2 ${value === "default"
|
||||||
value === "default"
|
|
||||||
? "space-x-4"
|
? "space-x-4"
|
||||||
: "xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-3"
|
: "xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-3"
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
30
src/components/Pages/NotFound.jsx
Normal file
30
src/components/Pages/NotFound.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { Frown } from "lucide-react"
|
||||||
|
|
||||||
|
const NotFound = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-background text-foreground p-4">
|
||||||
|
<div className="max-w-md w-full text-center">
|
||||||
|
<h1 className="text-9xl font-bold mb-4">404</h1>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="text-9xl opacity-10">?</span>
|
||||||
|
</div>
|
||||||
|
<Frown className="mx-auto h-24 w-24 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-semibold mt-4 mb-2">Page Not Found</h2>
|
||||||
|
<p className="text-muted-foreground mb-8">Oops! The page you are looking for does not exist or has been moved.</p>
|
||||||
|
<a
|
||||||
|
href="https://dashboard.planpostai.com"
|
||||||
|
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
|
||||||
|
</a>
|
||||||
|
</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 bottom-4 right-4 w-24 h-24 border-b-4 border-r-4 border-primary opacity-20"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotFound
|
||||||
|
|
||||||
32
src/components/Pages/UnAuthenticated.jsx
Normal file
32
src/components/Pages/UnAuthenticated.jsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Lock } from "lucide-react"
|
||||||
|
|
||||||
|
const Unauthenticated = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-background text-foreground p-4">
|
||||||
|
<div className="max-w-md w-full text-center">
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="text-9xl opacity-10">!</span>
|
||||||
|
</div>
|
||||||
|
<Lock className="mx-auto h-24 w-24 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold mb-4">Access Denied</h1>
|
||||||
|
<h2 className="text-2xl font-semibold mt-4 mb-2">Authentication Required</h2>
|
||||||
|
<p className="text-muted-foreground mb-8">
|
||||||
|
Sorry, you need to be authenticated to access this page. Please log in to continue.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://dashboard.planpostai.com/login"
|
||||||
|
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
|
||||||
|
</a>
|
||||||
|
</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 bottom-4 right-4 w-24 h-24 border-b-4 border-r-4 border-primary opacity-20"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Unauthenticated
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@ const EditorPanel = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{selectedPanel !== "" && (
|
{selectedPanel !== "" && (
|
||||||
<div className="w-80 md:h-[calc(90vh-120px)] bg-background rounded-xl shadow-lg mx-4 my-auto">
|
<div className="w-80 h-[450px] bg-background rounded-xl shadow-lg mx-4 my-auto overflow-y-scroll scrollbar-hide">
|
||||||
{renderPanel()}
|
{renderPanel()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,7 @@ export function TopBar() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`!absolute top-2 -translate-x-1/2 z-40 ${
|
className={`!absolute top-2 -translate-x-1/2 z-40 ${selectedPanel !== ""
|
||||||
selectedPanel !== ""
|
|
||||||
? "md:w-[465px] left-[41%] lg:w-[700px] lg:left-[43%] mdxl:left-[45%] xl:w-[900px] xl:left-[46%] 2xl:left-[50%]"
|
? "md:w-[465px] left-[41%] lg:w-[700px] lg:left-[43%] mdxl:left-[45%] xl:w-[900px] xl:left-[46%] 2xl:left-[50%]"
|
||||||
: "w-[500px] lg:w-[600px] xl:w-[900px] lg:left-[41%] xl:left-[50%]"
|
: "w-[500px] lg:w-[600px] xl:w-[900px] lg:left-[41%] xl:left-[50%]"
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -84,6 +83,7 @@ export function TopBar() {
|
||||||
<span>Add image</span>
|
<span>Add image</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* canvas settings */}
|
||||||
<div>
|
<div>
|
||||||
<a data-tooltip-id="canvas">
|
<a data-tooltip-id="canvas">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -194,6 +194,7 @@ export function TopBar() {
|
||||||
<LuFlipVertical />
|
<LuFlipVertical />
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<Tooltip id="flip" content="Object flip" place="bottom" />
|
<Tooltip id="flip" content="Object flip" place="bottom" />
|
||||||
{activeObject?.type !== "group" && (
|
{activeObject?.type !== "group" && (
|
||||||
<a data-tooltip-id="position">
|
<a data-tooltip-id="position">
|
||||||
|
|
@ -206,6 +207,7 @@ export function TopBar() {
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
id="position"
|
id="position"
|
||||||
content="Object position"
|
content="Object position"
|
||||||
|
|
|
||||||
305
src/components/SaveCanvas.jsx
Normal file
305
src/components/SaveCanvas.jsx
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
import { useContext, useEffect, useState } from 'react'
|
||||||
|
import CanvasContext from './Context/canvasContext/CanvasContext';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { useToast } from '../hooks/use-toast';
|
||||||
|
import { Input } from './ui/input';
|
||||||
|
import { Label } from './ui/label';
|
||||||
|
import { Save, Trash2 } from 'lucide-react';
|
||||||
|
import { Textarea } from './ui/textarea';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { deleteProject, getProjectById, updateProject } from '@/api/projectApi';
|
||||||
|
import { Button } from './ui/button';
|
||||||
|
import { Separator } from './ui/separator';
|
||||||
|
import { deleteImage, uploadImage } from '@/api/uploadApi';
|
||||||
|
import ActiveObjectContext from './Context/activeObject/ObjectContext';
|
||||||
|
|
||||||
|
const SaveCanvas = () => {
|
||||||
|
const { canvas } = useContext(CanvasContext);
|
||||||
|
const { setSelectedPanel } = useContext(CanvasContext);
|
||||||
|
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||||
|
|
||||||
|
const [saveCanvas, setSaveCanvas] = useState({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
preview_url: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = useParams();
|
||||||
|
const { id } = params;
|
||||||
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// to get each canvas_project data
|
||||||
|
const { data: projectData, isLoading: projectLoading } = useQuery({
|
||||||
|
queryKey: ['project', id],
|
||||||
|
queryFn: async () => await getProjectById(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// to update the project
|
||||||
|
const { mutate: projectUpdate, isPending } = useMutation({
|
||||||
|
mutationFn: async ({ id, projectData }) => {
|
||||||
|
return await updateProject({ id, ...projectData })
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message,
|
||||||
|
})
|
||||||
|
console.log(data);
|
||||||
|
// Invalidate a single query key
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
||||||
|
|
||||||
|
// to load the object into canvas
|
||||||
|
// canvas.loadFromJSON(data?.data?.object, () => {
|
||||||
|
// canvas.renderAll();
|
||||||
|
// }, (err) => {
|
||||||
|
// console.error('Error loading object:', err);
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
// Invalidate a single query key
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
||||||
|
setSelectedPanel("");
|
||||||
|
navigate("/");
|
||||||
|
// for clear canvas
|
||||||
|
canvas.clear();
|
||||||
|
canvas.renderAll();
|
||||||
|
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
||||||
|
setActiveObject(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message,
|
||||||
|
variant: "destructive"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// to set projectData into state
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectData?.data && !projectLoading && projectData?.data?.preview_url !== null) {
|
||||||
|
setSaveCanvas((prev) => ({
|
||||||
|
...prev,
|
||||||
|
name: projectData?.data?.name,
|
||||||
|
description: projectData?.data?.description,
|
||||||
|
preview_url: projectData?.data?.preview_url
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [projectData, projectLoading]);
|
||||||
|
|
||||||
|
// upload preview-image handler
|
||||||
|
const { mutate: uploadCanvasImage, isPending: uploadCanvasPending } = useMutation({
|
||||||
|
mutationFn: async ({ file, id }) => {
|
||||||
|
return await uploadImage({ file, id });
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
});
|
||||||
|
console.log(data?.data[0]?.url);
|
||||||
|
handleSaveWithPreViewImage({ preview_url: data?.data[0]?.url, name: saveCanvas?.name, description: saveCanvas?.description });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// preview-image remove handler
|
||||||
|
const { mutate: removeCanvasImage, isPending: removeCanvasPending } = useMutation({
|
||||||
|
mutationFn: async (url) => {
|
||||||
|
return await deleteImage(url);
|
||||||
|
},
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
console.log(data);
|
||||||
|
if (data?.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
})
|
||||||
|
const file = await captureImage();
|
||||||
|
if (file) {
|
||||||
|
uploadCanvasImage({ file, id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: data?.status,
|
||||||
|
description: data?.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSaveProject = async () => {
|
||||||
|
if (!saveCanvas?.name || saveCanvas?.name.trim() === "") {
|
||||||
|
toast({
|
||||||
|
title: "Name error",
|
||||||
|
description: "Please enter a name for your project",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
return; // Exit the function early
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if preview_url is valid
|
||||||
|
|
||||||
|
if (saveCanvas?.preview_url) {
|
||||||
|
try {
|
||||||
|
removeCanvasImage(saveCanvas?.preview_url);
|
||||||
|
console.log("Image removed", saveCanvas?.preview_url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error removing image:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
console.log("Capturing image...");
|
||||||
|
const file = await captureImage();
|
||||||
|
console.log("Captured file:", file);
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
uploadCanvasImage({ file, id });
|
||||||
|
console.log("Image uploaded successfully");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error capturing image:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// this will save the canvas as a json object
|
||||||
|
const handleSaveWithPreViewImage = (body) => {
|
||||||
|
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||||
|
if (object?.objects?.length === 0) {
|
||||||
|
toast({
|
||||||
|
title: "Canvas is empty",
|
||||||
|
description: "Please add some elements to your canvas",
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const projectData = { ...body, object };
|
||||||
|
// Wait for the project update before continuing
|
||||||
|
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 = () => {
|
||||||
|
projectDelete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='my-2'>
|
||||||
|
<h2 className='font-bold text-sm flex gap-2'><Save /> Save Canvas </h2>
|
||||||
|
<div className='bg-red-50 p-2 my-2'>
|
||||||
|
{
|
||||||
|
saveCanvas?.preview_url &&
|
||||||
|
<img src={saveCanvas?.preview_url} alt="" className='rounded-md shadow-sm' />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Name:</Label>
|
||||||
|
<Input
|
||||||
|
value={saveCanvas?.name}
|
||||||
|
onChange={(e) => setSaveCanvas({ ...saveCanvas, name: e.target.value })}
|
||||||
|
placeholder="Project name"
|
||||||
|
/>
|
||||||
|
<Label>Descriptions:</Label>
|
||||||
|
<Textarea
|
||||||
|
value={saveCanvas?.description}
|
||||||
|
onChange={(e) => setSaveCanvas({ ...saveCanvas, description: e.target.value })}
|
||||||
|
placeholder="Describe your project"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className='my-2' />
|
||||||
|
|
||||||
|
<div className='flex my-2 gap-2 justify-end'>
|
||||||
|
<Button disabled={deletePending || projectLoading || uploadCanvasPending || removeCanvasPending} onClick={handleDeleteProject}> <Trash2 /> </Button>
|
||||||
|
<Button disabled={isPending || projectLoading} onClick={handleSaveProject}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SaveCanvas
|
||||||
|
|
@ -6,7 +6,7 @@ const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
(<textarea
|
(<textarea
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-xs shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
||||||
12
src/main.jsx
12
src/main.jsx
|
|
@ -5,17 +5,29 @@ import CanvasContextProvider from "./components/Context/canvasContext/CanvasCont
|
||||||
import ColorContextProvider from "./components/Context/colorContext/ColorContextProvider";
|
import ColorContextProvider from "./components/Context/colorContext/ColorContextProvider";
|
||||||
import ObjectProvider from "./components/Context/activeObject/ObjectProvider";
|
import ObjectProvider from "./components/Context/activeObject/ObjectProvider";
|
||||||
import OpenContextProvider from "./components/Context/openContext/OpenContextProvider";
|
import OpenContextProvider from "./components/Context/openContext/OpenContextProvider";
|
||||||
|
import AuthContextProvider from "./components/Context/authContext/AuthProvider";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
createRoot(document.getElementById("root")).render(
|
createRoot(document.getElementById("root")).render(
|
||||||
// <StrictMode>
|
// <StrictMode>
|
||||||
|
<AuthContextProvider>
|
||||||
<CanvasContextProvider>
|
<CanvasContextProvider>
|
||||||
<ColorContextProvider>
|
<ColorContextProvider>
|
||||||
<ObjectProvider>
|
<ObjectProvider>
|
||||||
<OpenContextProvider>
|
<OpenContextProvider>
|
||||||
|
<BrowserRouter>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<App />
|
<App />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</BrowserRouter>
|
||||||
</OpenContextProvider>
|
</OpenContextProvider>
|
||||||
</ObjectProvider>
|
</ObjectProvider>
|
||||||
</ColorContextProvider>
|
</ColorContextProvider>
|
||||||
</CanvasContextProvider>
|
</CanvasContextProvider>
|
||||||
|
</AuthContextProvider>
|
||||||
// </StrictMode>,
|
// </StrictMode>,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
10
src/vite-env.d.ts
vendored
Normal file
10
src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_APP_TITLE: string
|
||||||
|
// more env variables...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
@ -9,58 +9,65 @@ export default {
|
||||||
extend: {
|
extend: {
|
||||||
textColor: {
|
textColor: {
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary-text))",
|
DEFAULT: 'hsl(var(--primary-text))',
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
mdxl: "1100px",
|
mdxl: '1100px'
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border))",
|
border: 'hsl(var(--border))',
|
||||||
input: "hsl(var(--input))",
|
input: 'hsl(var(--input))',
|
||||||
ring: "hsl(var(--ring))",
|
ring: 'hsl(var(--ring))',
|
||||||
background: "hsl(var(--background))",
|
background: 'hsl(var(--background))',
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: 'hsl(var(--foreground))',
|
||||||
selection: {
|
selection: {
|
||||||
DEFAULT: "hsl(var(--selection))",
|
DEFAULT: 'hsl(var(--selection))',
|
||||||
foreground: "hsl(var(--selection-foreground))",
|
foreground: 'hsl(var(--selection-foreground))'
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: 'hsl(var(--primary-foreground))'
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
foreground: 'hsl(var(--secondary-foreground))'
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: 'hsl(var(--destructive-foreground))'
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: 'hsl(var(--muted-foreground))'
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: "hsl(var(--accent))",
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
foreground: 'hsl(var(--accent-foreground))'
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: "hsl(var(--popover))",
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
foreground: 'hsl(var(--popover-foreground))'
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: 'hsl(var(--card-foreground))'
|
||||||
},
|
},
|
||||||
|
chart: {
|
||||||
|
'1': 'hsl(var(--chart-1))',
|
||||||
|
'2': 'hsl(var(--chart-2))',
|
||||||
|
'3': 'hsl(var(--chart-3))',
|
||||||
|
'4': 'hsl(var(--chart-4))',
|
||||||
|
'5': 'hsl(var(--chart-5))'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
lg: "var(--radius)",
|
lg: 'var(--radius)',
|
||||||
md: "calc(var(--radius) - 2px)",
|
md: 'calc(var(--radius) - 2px)',
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: 'calc(var(--radius) - 4px)'
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
plugins: [scrollbar, scrollbarHide, require("tailwindcss-animate")],
|
||||||
},
|
|
||||||
plugins: [scrollbar, scrollbarHide],
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,7 @@ export default defineConfig({
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
port: 5175
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue