Compare commits
No commits in common. "4f55e0ced649969660b448f38474dfaf00642033" and "3a07457404ac4c10cf875bb276ccdf896ad9c62f" have entirely different histories.
4f55e0ced6
...
3a07457404
63 changed files with 1817 additions and 3556 deletions
2
.env
2
.env
|
|
@ -1 +1 @@
|
|||
VITE_SERVER_URL=http://localhost:3000/api
|
||||
VITE_GOOGLE_FONT_API_KEY=AIzaSyBPOYGT26jwMjlDuf6sM5JwaZDkiYigeQg
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
}
|
||||
1453
package-lock.json
generated
1453
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -25,8 +25,7 @@
|
|||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@radix-ui/react-tooltip": "^1.1.4",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"fabric": "^5.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
|
@ -37,11 +36,9 @@
|
|||
"react-icons": "^5.4.0",
|
||||
"react-image-file-resizer": "^0.4.8",
|
||||
"react-rnd": "^10.4.13",
|
||||
"react-router-dom": "^7.1.5",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"react-window": "^1.8.10",
|
||||
"shadcn-ui": "^0.9.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwind-scrollbar-hide": "^1.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
|
@ -49,8 +46,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@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-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
|
|
|
|||
49
public/assets/svgs/Group 25403.svg
Normal file
49
public/assets/svgs/Group 25403.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 129 KiB |
10
src/App.css
10
src/App.css
|
|
@ -6,6 +6,16 @@
|
|||
.tooltip {
|
||||
--tooltip-spacing: 30px;
|
||||
}
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.example::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.example {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.\[\&_svg\]\:size-4 svg {
|
||||
width: 1.3rem !important;
|
||||
|
|
|
|||
60
src/App.jsx
60
src/App.jsx
|
|
@ -1,10 +1,21 @@
|
|||
import { useEffect } from "react";
|
||||
import { useContext, useEffect } from "react";
|
||||
import "./App.css";
|
||||
// import Canvas from "./components/Canvas";
|
||||
import WebFont from "webfontloader";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { Home } from "./Home";
|
||||
import NotFound from "./components/Pages/NotFound";
|
||||
import Unauthenticated from "./components/Pages/UnAuthenticated";
|
||||
// import Header from "./components/Layouts/Header";
|
||||
// import SheetRightPanel from "./components/Layouts/SheetRightPanel";
|
||||
// import SheetLeftPanel from "./components/Layouts/SheetLeftPanel";
|
||||
// import CanvasCapture from "./components/CanvasCapture";
|
||||
// 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() {
|
||||
useEffect(() => {
|
||||
|
|
@ -65,14 +76,39 @@ function App() {
|
|||
});
|
||||
}, []);
|
||||
|
||||
const { selectedPanel } = useContext(CanvasContext);
|
||||
const { activeObject } = useContext(ActiveObjectContext);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/:id" element={<Home />} />
|
||||
<Route path="/unAuthenticated" element={<Unauthenticated />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
<Route path="/notFound" element={<NotFound />} />
|
||||
</Routes>
|
||||
// <div className="relative flex flex-col h-screen overflow-hidden">
|
||||
// <Toaster />
|
||||
// <SheetLeftPanel />
|
||||
// <Header />
|
||||
// <SheetRightPanel />
|
||||
|
||||
// <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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
import { AlertCircle, RefreshCw } from "lucide-react";
|
||||
import { Component } from "react";
|
||||
|
||||
class GlobalErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.error("Global Error Caught:", error, errorInfo);
|
||||
// You can log this to an external service like Sentry or LogRocket
|
||||
}
|
||||
|
||||
handleRetry = () => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
window.location.reload(); // Reload the app
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-background text-foreground p-6">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="bg-card text-card-foreground rounded-lg shadow-lg p-8">
|
||||
<div className="flex items-center justify-center mb-6">
|
||||
<AlertCircle className="w-16 h-16 text-destructive" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-primary-text text-center mb-4">Oops! Something went wrong</h1>
|
||||
<p className="text-muted-foreground text-center mb-6">
|
||||
{this.state.error?.message || "An unexpected error occurred."}
|
||||
</p>
|
||||
<button
|
||||
onClick={this.handleRetry}
|
||||
className="w-full flex items-center justify-center px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-5 h-5 mr-2" />
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default GlobalErrorBoundary;
|
||||
136
src/Home.jsx
136
src/Home.jsx
|
|
@ -1,136 +0,0 @@
|
|||
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, getUser } from './api/authApi';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
import { Loader } from 'lucide-react';
|
||||
import CanvasContext from './components/Context/canvasContext/CanvasContext';
|
||||
import AuthContext from './components/Context/authContext/AuthContext';
|
||||
import { useToast } from './hooks/use-toast';
|
||||
import useProject from './hooks/useProject';
|
||||
|
||||
export const Home = () => {
|
||||
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
||||
const { canvas, selectedPanel } = useContext(CanvasContext);
|
||||
const { user, setUser } = useContext(AuthContext);
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const getToken = () => localStorage.getItem('canvas_token');
|
||||
|
||||
const { id } = params;
|
||||
|
||||
const path = location.pathname;
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
// Fetch token only if it doesn't exist
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['get-token'],
|
||||
queryFn: () => generateToken(id),
|
||||
});
|
||||
|
||||
// Fetch project data only if token and id exist
|
||||
const { projectData, isLoading: projectLoading } = useProject();
|
||||
|
||||
// Fetch user related data only if token exists
|
||||
const { data: userData, isLoading: userLoading } = useQuery({
|
||||
queryKey: ['user'],
|
||||
queryFn: async () => await getUser(),
|
||||
enabled: !!getToken() && user?.length === 0,
|
||||
})
|
||||
|
||||
// update the data into context
|
||||
useEffect(() => {
|
||||
if (userData?.status === 200 && !userLoading) {
|
||||
setUser([userData?.user])
|
||||
}
|
||||
}, [userData, userLoading, setUser]);
|
||||
|
||||
useEffect(() => {
|
||||
const token = getToken(); // Get latest token
|
||||
|
||||
if (!token && !isLoading) {
|
||||
navigate("/unAuthenticated");
|
||||
}
|
||||
if (token && !isLoading && data?.status === 201) {
|
||||
navigate("/");
|
||||
}
|
||||
if (projectData?.status === 500 && id) {
|
||||
navigate("/");
|
||||
toast({ variant: "destructive", title: projectData?.status, description: "No project found" });
|
||||
}
|
||||
if (projectData && projectData?.status === 200 && !projectLoading && canvas && (selectedPanel === "project" || selectedPanel === "") && path !== "/") {
|
||||
if (canvas?._objects?.length === 0) {
|
||||
const isEmpty = (obj) => Object.values(obj).length === 0;
|
||||
if (!isEmpty(projectData?.data?.object)) {
|
||||
canvas.loadFromJSON(projectData?.data?.object, () => {
|
||||
// Ensure background image fills the canvas
|
||||
if (canvas.backgroundImage) {
|
||||
canvas.backgroundImage.scaleToWidth(canvas.width);
|
||||
canvas.backgroundImage.scaleToHeight(canvas.height);
|
||||
}
|
||||
canvas.renderAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [navigate, isLoading, data, projectData, id, toast, canvas, selectedPanel, projectLoading, setActiveObject, path]);
|
||||
|
||||
return (
|
||||
<div className='relative flex flex-col'>
|
||||
{
|
||||
isLoading &&
|
||||
<div className='flex justify-center items-center h-screen'>
|
||||
<p><Loader className="animate-spin mx-auto" /></p>
|
||||
</div>
|
||||
}
|
||||
<div className="flex h-screen overflow-hidden relative">
|
||||
|
||||
{!isLoading &&
|
||||
<>
|
||||
<Toaster />
|
||||
|
||||
{
|
||||
activeObject &&
|
||||
<div className="absolute left-0 xl:left-[90px] lg:left-[90px] md:left-[90px] sm:left-[80px] right-0 xl:right-[90px] lg:right-[90px] md:right-[90px] sm:right-[80px] z-[9999] rounded-md p-1 h-fit bg-white border-t border-gray-200 shadow-md my-1 w-[80%] mx-auto">
|
||||
<TopBar />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="absolute z-[999] right-0 xl:bottom-0 lg:bottom-0 md:bottom-0 sm:bottom-0 bottom-10 flex justify-center items-center h-20 bg-white border-t border-gray-200 shadow-md w-fit">
|
||||
<ActionButtons />
|
||||
</div>
|
||||
|
||||
<div className="h-full mr-1 hidden xl:block lg:block md:block sm:block">
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
||||
<div className="absolute ml-0 xl:ml-20 lg:ml-20 md:ml-20 z-[999] top-[15%]">
|
||||
<EditorPanel />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col h-full overflow-hidden my-2">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Canvas />
|
||||
</div>
|
||||
{/* canvas capture part */}
|
||||
<CanvasCapture />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<div className='h-full z-[999] block xl:hidden lg:hidden md:hidden sm:hidden'>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export const getUser = async () => {
|
||||
try {
|
||||
const url = `${import.meta.env.VITE_SERVER_URL}/auth/user/me`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data?.token) {
|
||||
localStorage.setItem('canvas_token', data.token);
|
||||
// Remove the token from the response data
|
||||
const { token, ...restData } = data;
|
||||
return restData; // Return modified data without token
|
||||
}
|
||||
else if (!data?.token) {
|
||||
localStorage.removeItem("canvas_token");
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Un-authenticated:', error);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
export const downloadCount = async (date) => {
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/download-count/`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ date })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data?.token) {
|
||||
localStorage.setItem('canvas_token', data.token);
|
||||
// Remove the token from the response data
|
||||
const { token, ...restData } = data;
|
||||
return restData; // Return modified data without token
|
||||
}
|
||||
else if (!data?.token) {
|
||||
localStorage.removeItem("canvas_token");
|
||||
return data;
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to get download count:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ const aspectRatios = [
|
|||
{ 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)" },
|
||||
|
|
@ -23,15 +24,13 @@ const aspectRatios = [
|
|||
{ 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 (4:4)" },
|
||||
{ value: "9:16", label: "Vertical (9:16)" },
|
||||
{ value: "1.33:1", label: "Instagram Stories (1.33:1)" },
|
||||
{ value: "3:4", label: "Portrait (3:4)" },
|
||||
{ value: "1.91:1", label: "Facebook Ads (1.91:1)" },
|
||||
];
|
||||
|
||||
export function ActionButtons() {
|
||||
const { setCaptureOpen } = useContext(OpenContext);
|
||||
const { setCanvasRatio, canvasRatio, setSelectedPanel } = useContext(CanvasContext);
|
||||
const { setCanvasRatio, canvasRatio } = useContext(CanvasContext);
|
||||
const handleRatioChange = (newRatio) => {
|
||||
setCanvasRatio(newRatio);
|
||||
};
|
||||
|
|
@ -57,14 +56,16 @@ export function ActionButtons() {
|
|||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="bg-[#F5F2FF] w-10 h-10"
|
||||
<div
|
||||
className="mr-5"
|
||||
onClick={() => {
|
||||
setCaptureOpen(true);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-[10px] border-[#6A47ED] border-[0.5px] bg-[#F5F2FF]"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
@ -96,55 +97,6 @@ export function ActionButtons() {
|
|||
/>
|
||||
</svg>
|
||||
</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>
|
||||
);
|
||||
|
|
|
|||
240
src/components/AspectCanvas.jsx
Normal file
240
src/components/AspectCanvas.jsx
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
98
src/components/Canvas.jsx
Normal file
98
src/components/Canvas.jsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
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,9 +7,25 @@ import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
|||
import { Slider } from "@/components/ui/slider";
|
||||
|
||||
export default function Canvas() {
|
||||
const { setLeftPanelOpen, setRightPanelOpen, setOpenSetting, setOpenObjectPanel, rightPanelOpen } = useContext(OpenContext);
|
||||
const {
|
||||
setLeftPanelOpen,
|
||||
setRightPanelOpen,
|
||||
setOpenSetting,
|
||||
setOpenObjectPanel,
|
||||
rightPanelOpen,
|
||||
} = useContext(OpenContext);
|
||||
|
||||
const { canvasRef, canvas, setCanvas, fabricCanvasRef, canvasRatio, setCanvasHeight, setCanvasWidth, setScreenWidth, selectedPanel, setSelectedPanel } = useContext(CanvasContext);
|
||||
const {
|
||||
canvasRef,
|
||||
canvas,
|
||||
setCanvas,
|
||||
fabricCanvasRef,
|
||||
canvasRatio,
|
||||
setCanvasHeight,
|
||||
setCanvasWidth,
|
||||
setScreenWidth,
|
||||
selectedPanel,
|
||||
} = useContext(CanvasContext);
|
||||
|
||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||
const [zoomLevel, setZoomLevel] = useState(100);
|
||||
|
|
@ -17,18 +33,15 @@ export default function Canvas() {
|
|||
|
||||
const handleZoom = useCallback(
|
||||
(newZoom) => {
|
||||
const zoom = Math.min(Math.max(newZoom, 50), 100); // Prevent zoom from going below 10
|
||||
const zoom = Math.min(Math.max(newZoom, 0), 100);
|
||||
setZoomLevel(zoom);
|
||||
|
||||
if (canvasRef.current && canvas) {
|
||||
const scale = zoom / 100;
|
||||
|
||||
// Ensure minimum dimensions
|
||||
const minWidth = 50; // Set a reasonable minimum width
|
||||
const minHeight = 50; // Set a reasonable minimum height
|
||||
|
||||
const newWidth = Math.max(canvasRef.current.offsetWidth * scale, minWidth);
|
||||
const newHeight = Math.max(canvasRef.current.offsetHeight * scale, minHeight);
|
||||
// Update canvas dimensions
|
||||
const newWidth = canvasRef.current.offsetWidth * scale;
|
||||
const newHeight = canvasRef.current.offsetHeight * scale;
|
||||
|
||||
canvas.setWidth(newWidth);
|
||||
canvas.setHeight(newHeight);
|
||||
|
|
@ -44,6 +57,21 @@ export default function Canvas() {
|
|||
useEffect(() => {
|
||||
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) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
|
|
@ -102,6 +130,8 @@ export default function Canvas() {
|
|||
const handleResize = () => {
|
||||
handleZoom(zoomLevel);
|
||||
};
|
||||
|
||||
canvas.on("mouse:down", handleMouseDown);
|
||||
const canvasContainer = document.getElementById("canvas-ref");
|
||||
|
||||
if (canvasContainer) {
|
||||
|
|
@ -118,6 +148,7 @@ export default function Canvas() {
|
|||
}
|
||||
|
||||
return () => {
|
||||
canvas.off("mouse:down", handleMouseDown);
|
||||
if (canvasContainer) {
|
||||
canvasContainer.removeEventListener("wheel", handleWheel);
|
||||
canvasContainer.removeEventListener("touchstart", handleTouchStart);
|
||||
|
|
@ -127,7 +158,7 @@ export default function Canvas() {
|
|||
window.removeEventListener("resize", handleResize);
|
||||
}
|
||||
};
|
||||
}, [canvas, zoomLevel, handleZoom]);
|
||||
}, [canvas, setActiveObject, zoomLevel, handleZoom]);
|
||||
|
||||
useEffect(() => {
|
||||
import("fabric").then((fabricModule) => {
|
||||
|
|
@ -183,7 +214,20 @@ export default function Canvas() {
|
|||
updateCanvasSize();
|
||||
window.addEventListener("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(() => {
|
||||
if (window.fabric) {
|
||||
|
|
@ -209,55 +253,31 @@ export default function Canvas() {
|
|||
}
|
||||
}, [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 (
|
||||
<div>
|
||||
<>
|
||||
{/* Zoom Controls */}
|
||||
<div className="fixed bottom-4 right-4 flex items-center gap-4 bg-white p-3 rounded-lg shadow-lg z-50">
|
||||
<span className="text-sm font-medium min-w-[45px]">{zoomLevel}%</span>
|
||||
<Slider
|
||||
value={[zoomLevel]}
|
||||
onValueChange={(value) => handleZoom(value[0])}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Canvas Container */}
|
||||
<div className="w-full max-w-2xl mx-auto mt-16">
|
||||
<div className="w-full max-w-4xl mx-auto p-4">
|
||||
<Card
|
||||
className={`w-full rounded-none flex-1 flex flex-col
|
||||
mx-auto border-0 shadow-none bg-transparent
|
||||
className={`w-full p-2 rounded-none flex-1 flex flex-col
|
||||
mx-auto pl-5 pb-5 pt-5 border-0 shadow-none bg-transparent mt-20
|
||||
${zoomLevel < 100 ? "overflow-hidden" : ""}`}
|
||||
>
|
||||
<CardContent
|
||||
className={`p-0 h-full bg-transparent shadow-none ${selectedPanel === "canvas" ? "border" : ""}`}
|
||||
className="p-0 h-full bg-transparent shadow-none"
|
||||
ref={containerRef}
|
||||
onDoubleClick={() => {
|
||||
setSelectedPanel("canvas");
|
||||
}}
|
||||
onClick={() => {
|
||||
if (selectedPanel === "canvas") {
|
||||
setSelectedPanel("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AspectRatio
|
||||
ratio={getRatioValue(canvasRatio)}
|
||||
|
|
@ -280,19 +300,6 @@ export default function Canvas() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Zoom Controls */}
|
||||
<div className="flex my-2 mb-4 bg-white p-3 rounded-lg shadow-lg z-50 w-fit">
|
||||
<span className="text-sm font-medium min-w-[45px]">{zoomLevel}%</span>
|
||||
<Slider
|
||||
value={[zoomLevel]}
|
||||
onValueChange={(value) => handleZoom(value[0])}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ import { Card, CardHeader, CardTitle } from './ui/card';
|
|||
import { X } from 'lucide-react';
|
||||
import { Separator } from './ui/separator';
|
||||
import RndComponent from './Layouts/RndComponent';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { downloadCount } from '@/api/downloadApi';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
const resolutions = [
|
||||
{ value: '720p', label: 'HD (1280x720)', width: 1280, height: 720 },
|
||||
|
|
@ -33,8 +30,6 @@ const CanvasCapture = () => {
|
|||
const [resolution, setResolution] = useState(resolutions[0].value);
|
||||
const [imageType, setImageType] = useState(imageTypes[0].value);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
const { captureOpen, setCaptureOpen } = useContext(OpenContext);
|
||||
|
||||
|
|
@ -90,34 +85,6 @@ const CanvasCapture = () => {
|
|||
bound: "parent"
|
||||
}
|
||||
|
||||
const { mutate: downloadMutate } = useMutation({
|
||||
mutationFn: async (date) => {
|
||||
return await downloadCount(date)
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data?.success === true && data?.status === 200) {
|
||||
toast({
|
||||
title: data?.status,
|
||||
description: data?.message,
|
||||
})
|
||||
captureImage();
|
||||
}
|
||||
else {
|
||||
toast({
|
||||
title: data?.status,
|
||||
description: data?.message,
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleDownload = () => {
|
||||
const date = new Date().toLocaleDateString();
|
||||
downloadMutate(date);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{captureOpen && (
|
||||
|
|
@ -164,7 +131,7 @@ const CanvasCapture = () => {
|
|||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleDownload} className='w-full rnd-escape'>
|
||||
<Button onClick={captureImage} className='w-full rnd-escape'>
|
||||
Capture Canvas
|
||||
</Button>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,73 +1,78 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { useContext, useRef, useState } from "react";
|
||||
import CanvasContext from "./Context/canvasContext/CanvasContext";
|
||||
import { Card, CardTitle } from "./ui/card";
|
||||
import { Button } from "./ui/button";
|
||||
import { Trash2, UploadIcon } from "lucide-react";
|
||||
import { Trash2, UploadIcon, X } from "lucide-react";
|
||||
import { Separator } from "./ui/separator";
|
||||
import ColorComponent from "./ColorComponent";
|
||||
import { Label } from "./ui/label";
|
||||
import { Input } from "./ui/input";
|
||||
import { Slider } from "./ui/slider";
|
||||
import { fabric } from "fabric";
|
||||
import OpenContext from "./Context/openContext/OpenContext";
|
||||
import { ScrollArea } from "./ui/scroll-area";
|
||||
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";
|
||||
import RndComponent from "./Layouts/RndComponent";
|
||||
|
||||
const CanvasSetting = () => {
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
const params = useParams();
|
||||
const { id } = params;
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const [bgImage, setBgImage] = useState(null);
|
||||
const [bgOverLayImage, setBgOverLayImage] = useState(null);
|
||||
const { canvas, canvasHeight, canvasWidth, screenWidth } =
|
||||
useContext(CanvasContext);
|
||||
const { setOpenSetting } = useContext(OpenContext);
|
||||
const bgImgRef = useRef(null);
|
||||
|
||||
// create empty project if no id is provided
|
||||
useEffect(() => {
|
||||
const createEmptyProject = async () => {
|
||||
try {
|
||||
const response = await createProject();
|
||||
if (response?.status === 200) {
|
||||
toast({
|
||||
title: response?.status,
|
||||
description: response?.message
|
||||
})
|
||||
const [canvasSettings, setCanvasSettings] = useState({
|
||||
width: canvas?.width,
|
||||
height: canvas?.height,
|
||||
perPixelTargetFind: true, // Enable per-pixel detection globally
|
||||
targetFindTolerance: 4, // Adjust for leniency in pixel-perfect detection
|
||||
});
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
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
|
||||
}
|
||||
if (response?.data?.id) {
|
||||
navigate(`/${response.data.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Project creation failed:", error);
|
||||
|
||||
// Adjust the background image to fit the updated canvas
|
||||
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, toast]);
|
||||
|
||||
// upload bg-image handler
|
||||
const { mutate: uploadBackgroundImage } = useMutation({
|
||||
mutationFn: async ({ file, id }) => {
|
||||
return await uploadImage({ file, id });
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data?.status === 200) {
|
||||
toast({
|
||||
title: data?.status,
|
||||
description: data?.message
|
||||
});
|
||||
setBgImage(data?.data[0]?.url);
|
||||
const setBackgroundImage = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
|
||||
// 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.src = blobUrl;
|
||||
|
||||
imgElement.onload = () => {
|
||||
// Create a fabric.Image instance
|
||||
|
|
@ -78,152 +83,41 @@ const CanvasSetting = () => {
|
|||
|
||||
// Set the background image on the canvas
|
||||
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
|
||||
|
||||
// Revoke the object URL to free memory
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
};
|
||||
|
||||
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."
|
||||
})
|
||||
imgElement.onerror = () => {
|
||||
console.error("Failed to load the image.");
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
};
|
||||
}
|
||||
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 file = e.target.files[0];
|
||||
if (file) {
|
||||
uploadOverlayImage({ file, id })
|
||||
}
|
||||
else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Please select a file",
|
||||
})
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
|
||||
// Create an image element
|
||||
const imgElement = new Image();
|
||||
imgElement.src = blobUrl;
|
||||
|
||||
imgElement.onload = () => {
|
||||
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);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -249,52 +143,44 @@ const CanvasSetting = () => {
|
|||
|
||||
const removeBackgroundImage = () => {
|
||||
if (canvas) {
|
||||
const bgUrl = canvas.backgroundImage?.getSrc();
|
||||
console.log("background image from remove", bgUrl)
|
||||
if (bgUrl) {
|
||||
removeBackgroundMutate(bgUrl)
|
||||
}
|
||||
else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "No background image found",
|
||||
})
|
||||
canvas.backgroundImage = null;
|
||||
canvas.renderAll();
|
||||
}
|
||||
if (bgImgRef.current) {
|
||||
bgImgRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const removeBackgroundOverlayImage = () => {
|
||||
if (canvas) {
|
||||
const overLayUrl = canvas.overlayImage?.getSrc();
|
||||
console.log("overlay image from remove", overLayUrl);
|
||||
if (overLayUrl) {
|
||||
removeOverLayMutate(overLayUrl);
|
||||
}
|
||||
else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "No overlay image found",
|
||||
})
|
||||
}
|
||||
canvas.overlayImage = null;
|
||||
canvas.renderAll();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (canvas) {
|
||||
const bgUrl = canvas.backgroundImage?.getSrc();
|
||||
setBgImage(bgUrl);
|
||||
const overLayUrl = canvas.overlayImage?.getSrc();
|
||||
setBgOverLayImage(overLayUrl);
|
||||
}
|
||||
}, [canvas, setBgImage, setBgOverLayImage])
|
||||
|
||||
const rndValue = {
|
||||
valueX: 0,
|
||||
valueY: 20,
|
||||
width: 250,
|
||||
height: 0,
|
||||
minWidth: 250,
|
||||
maxWidth: 300,
|
||||
minHeight: 0,
|
||||
maxHeight: 400,
|
||||
bound: "parent",
|
||||
};
|
||||
const content = () => {
|
||||
return (
|
||||
<div>
|
||||
<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">
|
||||
Canvas Setting
|
||||
Canvas Setting{" "}
|
||||
<Button
|
||||
className="rnd-escape"
|
||||
variant="secondary"
|
||||
onClick={() => setOpenSetting(false)}
|
||||
>
|
||||
<X />
|
||||
</Button>{" "}
|
||||
</CardTitle>
|
||||
<Separator className="mt-4 block xl:hidden lg:hidden md:hidden" />
|
||||
<ScrollArea className="h-[400px] xl:h-fit lg:h-fit md:h-fit">
|
||||
|
|
@ -306,49 +192,32 @@ const CanvasSetting = () => {
|
|||
<div className="flex flex-col my-2 gap-2 rnd-escape">
|
||||
<div>
|
||||
<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">
|
||||
{
|
||||
!bgImage &&
|
||||
<Button className="top-0 absolute flex items-center w-[30px]">
|
||||
<UploadIcon className="cursor-pointer" />
|
||||
<Input
|
||||
ref={bgImgRef}
|
||||
className="absolute top-0 opacity-0"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={setBackgroundImage}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
|
||||
{
|
||||
bgImage &&
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="ml-[35px]"
|
||||
onClick={removeBackgroundImage}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{
|
||||
bgImage ? <Separator className="mt-4" /> : <Separator className="mt-12" />
|
||||
}
|
||||
|
||||
<div>
|
||||
<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">
|
||||
{
|
||||
!bgOverLayImage &&
|
||||
<Button className="top-0 absolute flex items-center w-[30px] mb">
|
||||
<Button className="top-0 absolute flex items-center w-[30px]">
|
||||
<UploadIcon className="cursor-pointer" />
|
||||
<Input
|
||||
className="absolute top-0 opacity-0"
|
||||
|
|
@ -357,23 +226,16 @@ const CanvasSetting = () => {
|
|||
onChange={setBackgroundOverlayImage}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
|
||||
{
|
||||
bgOverLayImage &&
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="ml-[35px]"
|
||||
onClick={removeBackgroundOverlayImage}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{
|
||||
bgOverLayImage ? <Separator className="mt-4" /> : <Separator className="mt-12" />
|
||||
}
|
||||
|
||||
{/* opacity */}
|
||||
<div className="flex flex-col gap-2 rnd-escape mt-2">
|
||||
|
|
@ -393,16 +255,46 @@ const CanvasSetting = () => {
|
|||
|
||||
<Separator className="mt-4" />
|
||||
|
||||
{/* Save canvas Component */}
|
||||
{
|
||||
id &&
|
||||
<SaveCanvas />
|
||||
{/* canvas size customization (width/height) */}
|
||||
<div className="flex gap-2 my-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Width:</Label>
|
||||
<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>
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return screenWidth <= 768 ? (
|
||||
<RndComponent value={rndValue}>{content()}</RndComponent>
|
||||
) : (
|
||||
<div>{content()}</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
import react from "react";
|
||||
|
||||
const AuthContext = react.createContext();
|
||||
|
||||
export default AuthContext;
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import AuthContext from "./AuthContext";
|
||||
|
||||
const AuthContextProvider = ({ children }) => {
|
||||
const [user, setUser] = useState([]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, setUser }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthContextProvider;
|
||||
|
|
@ -3,14 +3,11 @@ import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
|||
import { useContext } from "react";
|
||||
import { shapes } from "./shapes";
|
||||
import { fabric } from "fabric";
|
||||
import useProject from "@/hooks/useProject";
|
||||
|
||||
const CustomShape = () => {
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||
|
||||
const { projectData, projectUpdate, id } = useProject();
|
||||
|
||||
const addShape = (each) => {
|
||||
// Load the SVG from the imported file
|
||||
fabric.loadSVGFromURL(each, (objects, options) => {
|
||||
|
|
@ -41,11 +38,6 @@ const CustomShape = () => {
|
|||
|
||||
// Render the canvas
|
||||
canvas.renderAll();
|
||||
|
||||
const object = canvas.toJSON(['id', 'selectable']);
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export const shapes = [
|
|||
{ shape: 25400, source: "/assets/svgs/Group 25400.svg" },
|
||||
{ shape: 25401, source: "/assets/svgs/Group 25401.svg" },
|
||||
{ shape: 25402, source: "/assets/svgs/Group 25402.svg" },
|
||||
{ shape: 25403, source: "/assets/svgs/Group 25403.svg" },
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -32,10 +32,6 @@ import {
|
|||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import useProject from "@/hooks/useProject";
|
||||
import { uploadImage } from "@/api/uploadApi";
|
||||
|
||||
const features = [
|
||||
{
|
||||
|
|
@ -72,24 +68,6 @@ const AddImageIntoShape = () => {
|
|||
const fileInputRef = useRef(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const { id, projectData, projectUpdate } = useProject();
|
||||
|
||||
// Upload image handler
|
||||
const { mutate: uploadMutate } = useMutation({
|
||||
mutationFn: async ({ file, id }) => await uploadImage({ file, id }),
|
||||
onSuccess: (data) => {
|
||||
if (data?.status === 200) {
|
||||
toast({ title: data?.status, description: data?.message });
|
||||
handleImageInsert(data?.data[0]?.url);
|
||||
} else {
|
||||
toast({ variant: "destructive", title: data?.status, description: data?.message });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const handleResize = (file, callback) => {
|
||||
Resizer.imageFileResizer(
|
||||
file,
|
||||
|
|
@ -106,20 +84,31 @@ const AddImageIntoShape = () => {
|
|||
};
|
||||
|
||||
const fileHandler = (e) => {
|
||||
if (!activeObject) {
|
||||
toast({ variant: "destructive", title: "No active object selected!" });
|
||||
return;
|
||||
}
|
||||
const file = e.target.files[0];
|
||||
if (!file && activeObject) {
|
||||
if (!file) {
|
||||
setErrorMessage("No file selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the file is an SVG (skip compression)
|
||||
// Check if the file is an SVG
|
||||
if (file.type === "image/svg+xml") {
|
||||
toast({ variant: "destructive", title: "SVG files are not supported!" });
|
||||
// Add SVG directly to canvas without compression
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
const imgElement = new Image();
|
||||
imgElement.src = blobUrl;
|
||||
|
||||
imgElement.onload = () => {
|
||||
handleImageInsert(imgElement); // Insert the image without resizing
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
clearFileInput();
|
||||
};
|
||||
|
||||
imgElement.onerror = () => {
|
||||
console.error("Failed to load SVG.");
|
||||
setErrorMessage("Failed to load SVG.");
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
clearFileInput();
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -129,16 +118,32 @@ const AddImageIntoShape = () => {
|
|||
imgElement.src = blobUrl;
|
||||
|
||||
imgElement.onload = () => {
|
||||
// If the width is greater than 1080px, compress the image
|
||||
if (imgElement.width > 1080) {
|
||||
handleResize(file, (compressedFile) => {
|
||||
uploadMutate({ file: compressedFile, id }); // Fixed key name
|
||||
const compressedBlobUrl = URL.createObjectURL(compressedFile);
|
||||
const compressedImg = new Image();
|
||||
compressedImg.src = compressedBlobUrl;
|
||||
|
||||
compressedImg.onload = () => {
|
||||
handleImageInsert(compressedImg); // Insert the resized image
|
||||
URL.revokeObjectURL(compressedBlobUrl); // Clean up
|
||||
clearFileInput();
|
||||
};
|
||||
|
||||
compressedImg.onerror = () => {
|
||||
console.error("Failed to load compressed image.");
|
||||
setErrorMessage("Failed to load compressed image.");
|
||||
URL.revokeObjectURL(compressedBlobUrl);
|
||||
clearFileInput();
|
||||
};
|
||||
});
|
||||
} else {
|
||||
uploadMutate({ file, id }); // Direct upload if width is small
|
||||
handleImageInsert(imgElement); // Insert the original image if no resizing needed
|
||||
clearFileInput();
|
||||
}
|
||||
URL.revokeObjectURL(blobUrl); // Clean up
|
||||
clearFileInput();
|
||||
};
|
||||
|
||||
imgElement.onerror = () => {
|
||||
|
|
@ -155,79 +160,30 @@ const AddImageIntoShape = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// const handleImageInsert = (img) => {
|
||||
// if (!activeObject) {
|
||||
// setErrorMessage("No active object selected!");
|
||||
// return;
|
||||
// }
|
||||
// // Ensure absolute positioning for the clipPath
|
||||
// activeObject.set({
|
||||
// isClipPath: true, // Custom property
|
||||
// absolutePositioned: true,
|
||||
// });
|
||||
|
||||
// // Calculate scale factors based on clip object size
|
||||
// let scaleX = activeObject.width / img.width;
|
||||
// let scaleY = activeObject.height / img.height;
|
||||
// if (activeObject?.width < 100) {
|
||||
// scaleX = 0.2;
|
||||
// }
|
||||
|
||||
// if (activeObject.height < 100) {
|
||||
// scaleY = 0.2;
|
||||
// }
|
||||
|
||||
// // Create a fabric image object with scaling and clipPath
|
||||
// const fabricImage = new fabric.Image.fromURL(img, {
|
||||
// scaleX: scaleX,
|
||||
// scaleY: scaleY,
|
||||
// left: activeObject.left,
|
||||
// top: activeObject.top,
|
||||
// clipPath: activeObject, // Apply clipPath to the image
|
||||
// originX: activeObject.originX, // Match origin point
|
||||
// originY: activeObject.originY, // Match origin point
|
||||
// }, { crossOrigin: "anonymous" });
|
||||
|
||||
// // Adjust position based on the clipPath's transformations
|
||||
// fabricImage.set({
|
||||
// left: activeObject.left,
|
||||
// top: activeObject.top,
|
||||
// angle: activeObject.angle, // Match rotation if any
|
||||
// });
|
||||
|
||||
// canvas.add(fabricImage);
|
||||
// canvas.setActiveObject(fabricImage);
|
||||
// setActiveObject(fabricImage);
|
||||
// canvas.renderAll();
|
||||
// // Update the active object state
|
||||
// projectUpdate({ id, updateData: { ...projectData?.data, object: canvas.toJSON(['id', 'selectable']), preview_url: "" } });
|
||||
// };
|
||||
|
||||
|
||||
const handleImageInsert = (imgUrl) => {
|
||||
|
||||
console.log(imgUrl);
|
||||
|
||||
const handleImageInsert = (img) => {
|
||||
if (!activeObject) {
|
||||
setErrorMessage("No active object selected!");
|
||||
return;
|
||||
}
|
||||
// Ensure absolute positioning for the clipPath
|
||||
activeObject.set({
|
||||
isClipPath: true, // Custom property
|
||||
absolutePositioned: true,
|
||||
});
|
||||
|
||||
// Load the image asynchronously
|
||||
fabric.Image.fromURL(
|
||||
imgUrl,
|
||||
(img) => {
|
||||
// Ensure the image is fully loaded before applying transformations
|
||||
// Calculate scale factors based on clip object size
|
||||
let scaleX = activeObject.width / img.width;
|
||||
let scaleY = activeObject.height / img.height;
|
||||
if (activeObject?.width < 100) {
|
||||
scaleX = 0.2;
|
||||
}
|
||||
|
||||
// Prevent the image from being too small
|
||||
if (activeObject.width < 100) scaleX = 0.2;
|
||||
if (activeObject.height < 100) scaleY = 0.2;
|
||||
if (activeObject.height < 100) {
|
||||
scaleY = 0.2;
|
||||
}
|
||||
|
||||
// Set image properties
|
||||
img.set({
|
||||
// Create a fabric image object with scaling and clipPath
|
||||
const fabricImage = new fabric.Image(img, {
|
||||
scaleX: scaleX,
|
||||
scaleY: scaleY,
|
||||
left: activeObject.left,
|
||||
|
|
@ -235,32 +191,54 @@ const AddImageIntoShape = () => {
|
|||
clipPath: activeObject, // Apply clipPath to the image
|
||||
originX: activeObject.originX, // Match origin point
|
||||
originY: activeObject.originY, // Match origin point
|
||||
crossOrigin: "anonymous", // Ensure CORS handling
|
||||
});
|
||||
|
||||
// Add image to canvas
|
||||
canvas.add(img);
|
||||
canvas.setActiveObject(img);
|
||||
setActiveObject(img);
|
||||
// Adjust position based on the clipPath's transformations
|
||||
fabricImage.set({
|
||||
left: activeObject.left,
|
||||
top: activeObject.top,
|
||||
angle: activeObject.angle, // Match rotation if any
|
||||
});
|
||||
|
||||
canvas.add(fabricImage);
|
||||
canvas.setActiveObject(fabricImage);
|
||||
setActiveObject(fabricImage);
|
||||
canvas.renderAll();
|
||||
|
||||
// Update project data
|
||||
projectUpdate({
|
||||
id,
|
||||
updateData: {
|
||||
...projectData?.data,
|
||||
object: canvas.toJSON(["id", "selectable"]),
|
||||
preview_url: "",
|
||||
},
|
||||
});
|
||||
},
|
||||
{ crossOrigin: "anonymous" }
|
||||
);
|
||||
};
|
||||
|
||||
// canvas.remove(activeObject);
|
||||
|
||||
const content = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* <Card className="my-2">
|
||||
<CardContent className="p-0">
|
||||
<Alert>
|
||||
<AlertTitle className="text-lg font-semibold flex items-center gap-1 flex-wrap">Image Insertion <ImagePlus className="h-5 w-5" /></AlertTitle>
|
||||
<AlertDescription className="mt-1">
|
||||
<p className="mb-1">
|
||||
Insert and customize images within shapes. Adjust image position and clipping after insertion.
|
||||
</p>
|
||||
<h4 className="font-medium mb-1">Key Features:</h4>
|
||||
<div className="grid grid-cols-1 gap-2 mt-2">
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start p-4 bg-white rounded-lg shadow-sm"
|
||||
>
|
||||
<feature.icon className="h-6 w-6 mr-3 flex-shrink-0 text-primary" />
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">{feature.title}</h3>
|
||||
<p className="text-sm text-gray-600">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card> */}
|
||||
|
||||
<Card className="my-2">
|
||||
<CardContent className="p-0">
|
||||
<Alert>
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ const TextCustomization = () => {
|
|||
<SelectTrigger className="min-w-[140px] h-8">
|
||||
<SelectValue placeholder="Select a font" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="h-[250px] z-[9999]">
|
||||
<SelectContent className="h-[250px]">
|
||||
<SelectGroup>
|
||||
{fonts.map((font) => (
|
||||
<SelectItem
|
||||
|
|
@ -258,7 +258,6 @@ const TextCustomization = () => {
|
|||
</Select>
|
||||
</a>
|
||||
<Tooltip id="fonts" content="Font Family" place="bottom" />
|
||||
|
||||
{/* Font Size Controls */}
|
||||
<div className="flex items-center border rounded-md">
|
||||
<a data-tooltip-id="font-dec">
|
||||
|
|
@ -453,6 +452,45 @@ const TextCustomization = () => {
|
|||
</Popover>
|
||||
<Tooltip id="spacing" content="Spacing" place="bottom" />
|
||||
</div>
|
||||
|
||||
{/* Text Input */}
|
||||
{/* <div className="space-y-2">
|
||||
<Label htmlFor="text-content">Text Content</Label>
|
||||
<Input
|
||||
id="text-content"
|
||||
value={text}
|
||||
onChange={(e) => handleTextChange(e.target.value)}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
{/* Preview */}
|
||||
{/* {previewText && (
|
||||
<div className="p-4 border rounded-md overflow-hidden">
|
||||
<p className="font-bold mb-2">Preview:</p>
|
||||
<p
|
||||
style={{
|
||||
fontFamily,
|
||||
fontSize: `${fontSize}px`,
|
||||
fontStyle,
|
||||
fontWeight,
|
||||
lineHeight,
|
||||
letterSpacing: `${charSpacing}px`,
|
||||
textDecoration: `${underline ? "underline" : ""} ${
|
||||
linethrough ? "line-through" : ""
|
||||
}`,
|
||||
textAlign,
|
||||
}}
|
||||
className="truncate"
|
||||
>
|
||||
{previewText}
|
||||
</p>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* Apply Changes Button */}
|
||||
{/* <Button onClick={applyChanges} className="w-full">
|
||||
Apply Changes
|
||||
</Button> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
97
src/components/EachComponent/CustomizeShape.jsx
Normal file
97
src/components/EachComponent/CustomizeShape.jsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { Separator } from '../ui/separator';
|
||||
import FlipCustomization from './Customization/FlipCustomization';
|
||||
import RotateCustomization from './Customization/RotateCustomization';
|
||||
import SkewCustomization from './Customization/SkewCustomization';
|
||||
import ShadowCustomization from './Customization/ShadowCustomization';
|
||||
import TextCustomization from './Customization/TextCustomization';
|
||||
import AddImageIntoShape from './Customization/AddImageIntoShape';
|
||||
import ScaleObjects from './Customization/ScaleObjects';
|
||||
import ApplyColor from './ApplyColor';
|
||||
import OpacityCustomization from './Customization/OpacityCustomization';
|
||||
import StrokeCustomization from './Customization/StrokeCustomization';
|
||||
import { useContext } from 'react';
|
||||
import CanvasContext from '../Context/canvasContext/CanvasContext';
|
||||
import { Card } from '../ui/card';
|
||||
import PositionCustomization from './Customization/PositionCustomization';
|
||||
import CollapsibleComponent from './Customization/CollapsibleComponent';
|
||||
import ImageCustomization from './Customization/ImageCustomization';
|
||||
import SelectObjectFromGroup from './Customization/SelectObjectFromGroup';
|
||||
import LockObject from './Customization/LockObject';
|
||||
|
||||
const CustomizeShape = () => {
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
const activeObject = canvas?.getActiveObject();
|
||||
const activeObjectType = activeObject?.type;
|
||||
const hasClipPath = !!activeObject?.clipPath;
|
||||
const customClipPath = activeObject?.isClipPath;
|
||||
|
||||
// Return message if no active object is selected
|
||||
if (!activeObject) {
|
||||
return <p className='text-sm font-semibold'>No active object found</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='p-1'>
|
||||
|
||||
<LockObject />
|
||||
<Separator className="my-2" />
|
||||
|
||||
<SelectObjectFromGroup />
|
||||
|
||||
{/* Apply fill and background color */}
|
||||
{(activeObjectType !== 'image' && !hasClipPath && !customClipPath) && <ApplyColor />}
|
||||
|
||||
{/* Apply stroke and stroke color */}
|
||||
{(!customClipPath) && <><StrokeCustomization /><Separator className="my-2" /></>}
|
||||
|
||||
{
|
||||
activeObject?.type !== "group" &&
|
||||
<><PositionCustomization /><Separator className="my-2" />
|
||||
</>
|
||||
}
|
||||
|
||||
{/* Controls for opacity, flip, and rotation */}
|
||||
<Card className="p-2">
|
||||
<CollapsibleComponent text={"Opacity, Flip, Rotate Control"}>
|
||||
<div className="space-y-2">
|
||||
<OpacityCustomization />
|
||||
<FlipCustomization />
|
||||
<RotateCustomization />
|
||||
</div>
|
||||
</CollapsibleComponent>
|
||||
</Card>
|
||||
<Separator className="my-2" />
|
||||
|
||||
{/* Skew Customization */}
|
||||
<SkewCustomization />
|
||||
<Separator className="my-2" />
|
||||
|
||||
{/* Scale Objects */}
|
||||
<ScaleObjects />
|
||||
<Separator className="my-2" />
|
||||
|
||||
{/* Shadow Customization */}
|
||||
<ShadowCustomization />
|
||||
<Separator className="my-2" />
|
||||
|
||||
{/* Text Customization */}
|
||||
{activeObjectType === 'i-text' && <TextCustomization />}
|
||||
<Separator className="my-2" />
|
||||
|
||||
{/* Add image into shape */}
|
||||
<AddImageIntoShape />
|
||||
<Separator className="my-2" />
|
||||
|
||||
{/* Image Customization */}
|
||||
{(activeObjectType === 'image' || hasClipPath) && (
|
||||
<Card className="p-2">
|
||||
<CollapsibleComponent text={"Image Customization"}>
|
||||
<ImageCustomization />
|
||||
</CollapsibleComponent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomizeShape
|
||||
|
|
@ -6,7 +6,6 @@ import * as lucideIcons from "lucide-react";
|
|||
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||
import { fabric } from "fabric";
|
||||
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||
import useProject from "@/hooks/useProject";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
const AllIconsPage = () => {
|
||||
|
|
@ -16,8 +15,6 @@ const AllIconsPage = () => {
|
|||
|
||||
const { toast } = useToast();
|
||||
|
||||
const { projectData, projectUpdate, id } = useProject();
|
||||
|
||||
// Assume icons is already defined as shown previously, and filtered is created based on the search query
|
||||
const icons = Object.entries(lucideIcons)?.filter(
|
||||
([name, Icon]) => !name.includes("Icon") && Icon?.$$typeof
|
||||
|
|
@ -88,10 +85,6 @@ const AllIconsPage = () => {
|
|||
canvas.setActiveObject(iconGroup);
|
||||
setActiveObject(iconGroup);
|
||||
canvas.renderAll();
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,14 +16,11 @@ import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
|||
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import useProject from "@/hooks/useProject";
|
||||
|
||||
const RoundedShape = () => {
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||
|
||||
const { projectData, projectUpdate, id } = useProject();
|
||||
|
||||
const shapes = [
|
||||
{ icon: <ArrowBigRight />, name: "Arrow" },
|
||||
{ icon: <Diamond />, name: "Diamond" },
|
||||
|
|
@ -70,11 +67,6 @@ const RoundedShape = () => {
|
|||
canvas.setActiveObject(iconGroup);
|
||||
setActiveObject(iconGroup);
|
||||
canvas.renderAll();
|
||||
|
||||
const object = canvas.toJSON(['id', 'selectable']);
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,14 +6,11 @@ import { fabric } from "fabric";
|
|||
import { Card } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Badge, Circle, Heart, Shield } from "lucide-react";
|
||||
import useProject from "@/hooks/useProject";
|
||||
|
||||
const PlainShapes = () => {
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||
|
||||
const { projectData, projectUpdate, id } = useProject();
|
||||
|
||||
const shapes = [
|
||||
{
|
||||
icon: (
|
||||
|
|
@ -301,11 +298,6 @@ const PlainShapes = () => {
|
|||
canvas.setActiveObject(iconGroup);
|
||||
setActiveObject(iconGroup);
|
||||
canvas.renderAll();
|
||||
|
||||
const object = canvas.toJSON(['id', 'selectable']);
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { useContext, useRef, useState } from "react";
|
||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { fabric } from "fabric";
|
||||
import { ImageIcon, Trash2 } from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
|
|
@ -24,9 +25,6 @@ import { useDropzone } from "react-dropzone";
|
|||
import ImageCustomization from "./Customization/ImageCustomization";
|
||||
import { Separator } from "../ui/separator";
|
||||
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||
import useProject from "@/hooks/useProject";
|
||||
import useImageHandler from "@/hooks/useImageHandler";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
const UploadImage = () => {
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
|
|
@ -37,39 +35,14 @@ const UploadImage = () => {
|
|||
const [format, setFormat] = useState("JPEG");
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
||||
|
||||
const [file, setFile] = useState(null);
|
||||
const [preview, setPreview] = useState(null);
|
||||
|
||||
const { id } = useProject();
|
||||
|
||||
const removeFile = () => {
|
||||
// Revoke the object URL to free up memory
|
||||
if (preview) {
|
||||
URL.revokeObjectURL(preview);
|
||||
}
|
||||
setFile(null);
|
||||
setPreview(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
if (activeObject?.type === "image") {
|
||||
const imgUrl = activeObject?._originalElement?.currentSrc;
|
||||
canvas.remove(activeObject);
|
||||
setActiveObject(null);
|
||||
canvas.renderAll();
|
||||
deleteMutate(imgUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const { uploadMutate, deleteMutate } = useImageHandler({ removeFile });
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
accept: {
|
||||
"image/*": [".jpeg", ".png", ".jpg", ".webp"],
|
||||
"image/*": [".jpeg", ".png", ".gif", ".jpg", ".webp", ".svg"],
|
||||
},
|
||||
// maxSize: 5 * 1024 * 1024, // 5MB max file size
|
||||
multiple: false,
|
||||
|
|
@ -82,15 +55,11 @@ const UploadImage = () => {
|
|||
// Create a preview URL
|
||||
const blobUrl = URL.createObjectURL(selectedFile);
|
||||
setFile(selectedFile);
|
||||
setPreview(blobUrl);
|
||||
|
||||
if (selectedFile.type === "image/svg+xml") {
|
||||
toast({
|
||||
title: "SVG images are not supported.",
|
||||
description: "Please upload a different image format.",
|
||||
variant: "destructive",
|
||||
})
|
||||
addImageToCanvas(selectedFile);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
return
|
||||
} else {
|
||||
const imgElement = new Image();
|
||||
imgElement.src = blobUrl;
|
||||
|
|
@ -114,57 +83,55 @@ const UploadImage = () => {
|
|||
},
|
||||
});
|
||||
|
||||
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);
|
||||
const removeFile = () => {
|
||||
// Revoke the object URL to free up memory
|
||||
if (preview) {
|
||||
URL.revokeObjectURL(preview);
|
||||
}
|
||||
setFile(null);
|
||||
setPreview(null);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
if (activeObject?.type === "image") {
|
||||
canvas.remove(activeObject);
|
||||
setActiveObject(null);
|
||||
canvas.renderAll();
|
||||
}
|
||||
};
|
||||
|
||||
const handleResize = (file, callback) => {
|
||||
Resizer.imageFileResizer(
|
||||
selectedFile,
|
||||
newWidth, // Use user-defined width or calculated width
|
||||
newHeight, // Use user-defined height or calculated height
|
||||
file,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
quality,
|
||||
parseInt(rotation),
|
||||
(resizedFile) => {
|
||||
callback(resizedFile);
|
||||
},
|
||||
'file'
|
||||
"file"
|
||||
);
|
||||
URL.revokeObjectURL(img.src); // Cleanup
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
console.error("Failed to load image for resizing.");
|
||||
URL.revokeObjectURL(img.src); // Cleanup
|
||||
const addImageToCanvas = (file) => {
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
fabric.Image.fromURL(blobUrl, (img) => {
|
||||
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) => {
|
||||
uploadMutate({ file: selectedFile, id })
|
||||
};
|
||||
|
||||
// useEffect for preview update
|
||||
useEffect(() => {
|
||||
if (activeObject?.type === "image") {
|
||||
setPreview(activeObject._originalElement?.currentSrc);
|
||||
}
|
||||
else {
|
||||
setPreview(null);
|
||||
}
|
||||
}, [activeObject]);
|
||||
|
||||
return (
|
||||
<Card className="w-full border-none shadow-none">
|
||||
|
|
@ -256,7 +223,8 @@ const UploadImage = () => {
|
|||
<CardContent className="p-6 space-y-4">
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors duration-300 ${isDragActive
|
||||
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors duration-300 ${
|
||||
isDragActive
|
||||
? "border-primary bg-primary/10"
|
||||
: "border-gray-300 hover:border-primary"
|
||||
} ${preview ? "hidden" : ""}`}
|
||||
|
|
@ -265,7 +233,8 @@ const UploadImage = () => {
|
|||
<input {...getInputProps()} />
|
||||
<div className="flex flex-col items-center justify-center space-y-4">
|
||||
<ImageIcon
|
||||
className={`h-12 w-12 ${isDragActive ? "text-primary" : "text-gray-400"
|
||||
className={`h-12 w-12 ${
|
||||
isDragActive ? "text-primary" : "text-gray-400"
|
||||
}`}
|
||||
/>
|
||||
<p className="text-sm text-gray-600">
|
||||
|
|
|
|||
69
src/components/FabricCanvas.jsx
Normal file
69
src/components/FabricCanvas.jsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
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;
|
||||
21
src/components/Layouts/Header.jsx
Normal file
21
src/components/Layouts/Header.jsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { AlignLeft, AlignRight } from 'lucide-react';
|
||||
import { useContext } from 'react';
|
||||
import OpenContext from '../Context/openContext/OpenContext';
|
||||
|
||||
const Header = () => {
|
||||
const { setRightPanelOpen, setLeftPanelOpen } = useContext(OpenContext);
|
||||
|
||||
return (
|
||||
<div className='bg-[#f2e6f8] fixed top-0 w-[60px] z-[900] min-w-full flex items-center p-4 shadow-md'>
|
||||
|
||||
<p className='mr-auto flex-wrap gap-1 cursor-pointer text-xs items-center font-semibold xl:flex lg:flex md:flex hidden' onClick={() => setLeftPanelOpen(true)}><AlignLeft /><span className='hidden xl:block lg:block md:block sm:block'>Add Element</span></p>
|
||||
|
||||
<h1 className='font-semibold'>PlanPostAi Canvas</h1>
|
||||
|
||||
<p className='ml-auto flex-wrap gap-1 cursor-pointer text-xs items-center font-semibold xl:flex lg:flex md:flex hidden' onClick={() => setRightPanelOpen(true)}> <span className='hidden xl:block lg:block md:block sm:block'>Edit Panel</span><AlignRight /></p>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
|
@ -24,7 +24,7 @@ const sidebarItems = [
|
|||
export function Sidebar() {
|
||||
const { selectedPanel, setSelectedPanel } = useContext(CanvasContext);
|
||||
return (
|
||||
<div className="w-full xl:w-20 lg:w-20 md:w-20 sm:w-20 border-r bg-background flex xl:flex-col lg:flex-col md:flex-col sm:flex-col items-center justify-center py-4 gap-6 md:pt-16 xl:pt-0 h-full overflow-x-scroll">
|
||||
<div className="w-20 border-r bg-background flex flex-col items-center py-4 gap-6 md:pt-16 xl:pt-0">
|
||||
{sidebarItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
|
|
|
|||
79
src/components/Layouts/SheetLeftPanel.jsx
Normal file
79
src/components/Layouts/SheetLeftPanel.jsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
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
|
||||
88
src/components/Layouts/SheetRightPanel.jsx
Normal file
88
src/components/Layouts/SheetRightPanel.jsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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;
|
||||
151
src/components/ObjectPanel.jsx
Normal file
151
src/components/ObjectPanel.jsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
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,10 +20,6 @@ import {
|
|||
Layers,
|
||||
} from "lucide-react";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { deleteImage } from "../api/uploadApi";
|
||||
import { toast } from "../hooks/use-toast";
|
||||
import useProject from "@/hooks/useProject";
|
||||
|
||||
export const ObjectShortcut = ({ value }) => {
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
|
|
@ -35,8 +31,6 @@ export const ObjectShortcut = ({ value }) => {
|
|||
const objectActive = canvas.getActiveObject();
|
||||
const isGroupObject = objectActive && objectActive.type === "group";
|
||||
|
||||
const { projectData, projectUpdate, id } = useProject();
|
||||
|
||||
const groupSelectedObjects = () => {
|
||||
const activeObjects = canvas.getActiveObjects(); // Get selected objects
|
||||
if (activeObjects.length > 1) {
|
||||
|
|
@ -56,16 +50,8 @@ export const ObjectShortcut = ({ value }) => {
|
|||
canvas.setActiveObject(group);
|
||||
setActiveObject(group);
|
||||
canvas.renderAll();
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
} else {
|
||||
toast({
|
||||
title: "Select at least two objects",
|
||||
description: "Please select at least two objects to group.",
|
||||
variant: "destructive",
|
||||
})
|
||||
console.log("Select at least two objects");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -115,11 +101,6 @@ export const ObjectShortcut = ({ value }) => {
|
|||
setActiveObject(selection);
|
||||
|
||||
canvas.renderAll();
|
||||
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -143,10 +124,6 @@ export const ObjectShortcut = ({ value }) => {
|
|||
if (activeObject && !objectPosition.isAtFront) {
|
||||
activeObject.bringToFront();
|
||||
canvas.renderAll();
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
};
|
||||
|
|
@ -155,41 +132,10 @@ export const ObjectShortcut = ({ value }) => {
|
|||
if (activeObject && !objectPosition.isAtBack) {
|
||||
activeObject.sendToBack();
|
||||
canvas.renderAll();
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const { mutate: deleteMutate } = useMutation({
|
||||
mutationFn: async (url) => {
|
||||
return await deleteImage(url);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data?.status === 200) {
|
||||
toast({
|
||||
title: data?.status,
|
||||
description: data?.message
|
||||
})
|
||||
if (canvas) {
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
}
|
||||
}
|
||||
else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: data?.status,
|
||||
description: data?.message
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remove Selected Element
|
||||
const removeSelected = useCallback(() => {
|
||||
const activeObject = canvas?.getActiveObject();
|
||||
|
|
@ -204,36 +150,16 @@ export const ObjectShortcut = ({ value }) => {
|
|||
if (activeObject) {
|
||||
canvas.remove(activeObject);
|
||||
setActiveObject(null);
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
}
|
||||
if (activeObject && textObjects?.length === 1) {
|
||||
canvas.remove(activeObject);
|
||||
setActiveObject(null);
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
}
|
||||
if (activeObject.length > 1) {
|
||||
canvas.remove(...activeObject);
|
||||
setActiveObject(null);
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
}
|
||||
|
||||
if (activeObject?.type === "image") {
|
||||
const imgUrl = activeObject?._originalElement?.currentSrc;
|
||||
canvas.remove(activeObject);
|
||||
setActiveObject(null);
|
||||
canvas.renderAll();
|
||||
deleteMutate(imgUrl);
|
||||
}
|
||||
}, [canvas, setActiveObject, deleteMutate, id, projectData, projectUpdate]);
|
||||
}, [canvas, setActiveObject]);
|
||||
|
||||
// duplicating current objects
|
||||
const duplicating = () => {
|
||||
|
|
@ -243,10 +169,6 @@ export const ObjectShortcut = ({ value }) => {
|
|||
clonedObject.set("left", clonedObject?.left + 30);
|
||||
canvas.add(clonedObject);
|
||||
canvas.renderAll();
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -256,17 +178,14 @@ export const ObjectShortcut = ({ value }) => {
|
|||
canvas.renderAll();
|
||||
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
||||
setActiveObject(null);
|
||||
const object = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TooltipProvider>
|
||||
<div
|
||||
className={`flex items-center gap-2 ${value === "default"
|
||||
className={`flex items-center gap-2 ${
|
||||
value === "default"
|
||||
? "space-x-4"
|
||||
: "xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-3"
|
||||
}`}
|
||||
|
|
@ -327,7 +246,7 @@ export const ObjectShortcut = ({ value }) => {
|
|||
<p>Change object position</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="w-40 p-2 z-[9999]">
|
||||
<PopoverContent className="w-40 p-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -386,7 +305,7 @@ function ActionButton({ icon, label, onClick, tooltipContent }) {
|
|||
</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" align="center" className="max-w-xs z-[9999]">
|
||||
<TooltipContent side="bottom" align="center" className="max-w-xs">
|
||||
<p>{tooltipContent}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
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="/"
|
||||
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 Home
|
||||
</a>
|
||||
</div>
|
||||
<div className="absolute top-4 left-4 w-24 h-24 border-t-4 border-l-4 border-primary opacity-20"></div>
|
||||
<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
|
||||
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
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"
|
||||
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
|
||||
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { useContext } from 'react'
|
||||
import CanvasContext from '../Context/canvasContext/CanvasContext';
|
||||
import { Button } from '../ui/button';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { Ellipsis, X } from 'lucide-react';
|
||||
|
||||
const DesignPanel = () => {
|
||||
const { setSelectedPanel } = useContext(CanvasContext);
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">Design Panel</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSelectedPanel("")}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
|
||||
<div className='flex items-center gap-2'>
|
||||
<p >Coming soon </p>
|
||||
<Ellipsis className='animate-pulse animate-infinite' />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DesignPanel
|
||||
|
|
@ -12,9 +12,6 @@ import PositionPanel from "./PositionPanel";
|
|||
import ImagePanel from "./ImagePanel";
|
||||
import GroupObjectPanel from "./GroupObjectPanel";
|
||||
import CanvasPanel from "./CanvasPanel";
|
||||
import { ProjectPanel } from "./ProjectPanel";
|
||||
import ImageLibrary from "./ImageLibrary";
|
||||
import DesignPanel from "./DesignPanel";
|
||||
|
||||
const EditorPanel = () => {
|
||||
const { selectedPanel } = useContext(CanvasContext);
|
||||
|
|
@ -45,12 +42,6 @@ const EditorPanel = () => {
|
|||
return <GroupObjectPanel />;
|
||||
case "canvas":
|
||||
return <CanvasPanel />;
|
||||
case "project":
|
||||
return <ProjectPanel />;
|
||||
case "image":
|
||||
return <ImageLibrary />;
|
||||
case "design":
|
||||
return <DesignPanel />;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
|
@ -59,7 +50,7 @@ const EditorPanel = () => {
|
|||
return (
|
||||
<>
|
||||
{selectedPanel !== "" && (
|
||||
<div className="w-screen xl:w-80 lg:w-80 md:w-80 sm:w-80 h-[450px] bg-background rounded-xl shadow-lg mx-0 xl:mx-4 lg:mx-4 md:mx-4 sm:mx-4 my-auto overflow-y-scroll scrollbar-hide">
|
||||
<div className="w-80 md:h-[calc(90vh-120px)] bg-background rounded-xl shadow-lg mx-4 my-auto">
|
||||
{renderPanel()}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,12 @@
|
|||
import { useContext, useEffect, useRef } from "react";
|
||||
import { useContext } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||
import { X } from "lucide-react";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import AllIconsPage from "../EachComponent/Icons/AllIcons";
|
||||
import { useParams } from "react-router-dom";
|
||||
import useProject from "@/hooks/useProject";
|
||||
|
||||
const IconPanel = () => {
|
||||
const { setSelectedPanel } = useContext(CanvasContext);
|
||||
|
||||
const params = useParams();
|
||||
const { id } = params;
|
||||
const { createEmptyProject } = useProject();
|
||||
const hasCreatedProject = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id && !hasCreatedProject.current) {
|
||||
createEmptyProject();
|
||||
hasCreatedProject.current = true; // Prevent further calls
|
||||
}
|
||||
}, [id, createEmptyProject]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import { useContext } from 'react'
|
||||
import CanvasContext from '../Context/canvasContext/CanvasContext';
|
||||
import { Button } from '../ui/button';
|
||||
import { Ellipsis, X } from 'lucide-react';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
const ImageLibrary = () => {
|
||||
const { setSelectedPanel } = useContext(CanvasContext);
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">Image Library</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSelectedPanel("")}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
|
||||
{/* Image library content goes here */}
|
||||
<div className='flex items-center gap-2'>
|
||||
<p >Coming soon </p>
|
||||
<Ellipsis className='animate-pulse animate-infinite' />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageLibrary
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
import { useContext } from 'react';
|
||||
import CanvasContext from '../Context/canvasContext/CanvasContext';
|
||||
import { Button } from '../ui/button';
|
||||
import { Loader, Trash, X } from 'lucide-react';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { getProjects } from '@/api/projectApi';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import useProject from '@/hooks/useProject';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
export const ProjectPanel = () => {
|
||||
const { canvas, setSelectedPanel } = useContext(CanvasContext);
|
||||
|
||||
const params = useParams();
|
||||
const { id } = params;
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient(); // Initialize query client
|
||||
|
||||
const { data: projects, isLoading: projectLoading, isSuccess: projectSuccess } = useQuery({
|
||||
queryKey: ['projects'],
|
||||
queryFn: getProjects, // Simplified function call
|
||||
});
|
||||
|
||||
// To delete a project
|
||||
const { projectDelete, deletePending } = useProject();
|
||||
|
||||
const handleNavigate = (id) => {
|
||||
if (canvas?._objects?.length > 0) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Unsaved Changes",
|
||||
description: "Please save your changes before navigating to another project.",
|
||||
})
|
||||
}
|
||||
else {
|
||||
// Invalidate a single query key
|
||||
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
||||
navigate(`/${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter projects where preview_url is not empty or null
|
||||
const filteredProjects = projects?.data?.filter(project => project?.preview_url) || [];
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className="flex justify-between items-center p-4 border-b sticky top-0 z-[9999] bg-red-50">
|
||||
<h2 className="text-lg font-semibold">Projects</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSelectedPanel("")}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
|
||||
{
|
||||
projectLoading && <p><Loader className="animate-spin mx-auto" /></p>
|
||||
}
|
||||
<div className='grid grid-cols-1 gap-2'>
|
||||
{
|
||||
!projectLoading && projectSuccess && projects?.status === 200 &&
|
||||
filteredProjects.map((project) => {
|
||||
return (
|
||||
<div key={project?.id} className="flex flex-col gap-1 p-1 rounded-md border">
|
||||
<div
|
||||
className={`rounded-md flex p-1 flex-col gap-1 bg-red-50 hover:bg-red-100 cursor-pointer transition-all ${project?.id === id ? "border-2 border-red-200" : ""} `}
|
||||
onClick={() => handleNavigate(project.id)}
|
||||
>
|
||||
<p className="font-bold text-sm truncate">{project?.name}</p>
|
||||
<p className="text-xs truncate">{project?.description} </p>
|
||||
<img className="rounded-md" src={project?.preview_url} alt="each_project" />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled={deletePending}
|
||||
className="w-fit p-1 ml-auto" size="small"
|
||||
onClick={() => { projectDelete(project?.id) }}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
{
|
||||
projects?.status !== 200 && <p>{projects?.message}</p>
|
||||
}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useRef } from "react";
|
||||
import { useContext } from "react";
|
||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||
import { Button } from "../ui/button";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
|
|
@ -7,24 +7,9 @@ import { Separator } from "../ui/separator";
|
|||
import CustomShape from "../EachComponent/CustomShape/CustomShape";
|
||||
import RoundedShape from "../EachComponent/RoundedShapes/RoundedShape";
|
||||
import PlainShapes from "../EachComponent/Shapes/PlainShapes";
|
||||
import { useParams } from "react-router-dom";
|
||||
import useProject from "@/hooks/useProject";
|
||||
|
||||
const ShapePanel = () => {
|
||||
const { setSelectedPanel } = useContext(CanvasContext);
|
||||
|
||||
const params = useParams();
|
||||
const { id } = params;
|
||||
const { createEmptyProject } = useProject();
|
||||
const hasCreatedProject = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id && !hasCreatedProject.current) {
|
||||
createEmptyProject();
|
||||
hasCreatedProject.current = true; // Prevent further calls
|
||||
}
|
||||
}, [id, createEmptyProject]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
|
|
|
|||
|
|
@ -1,30 +1,16 @@
|
|||
import { Button } from "../ui/Button";
|
||||
import { X } from "lucide-react";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||
import { fabric } from "fabric";
|
||||
import CommonPanel from "./CommonPanel";
|
||||
import useProject from "@/hooks/useProject";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
export default function TextPanel() {
|
||||
const { canvas, setSelectedPanel } = useContext(CanvasContext);
|
||||
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
|
||||
|
||||
const params = useParams();
|
||||
const { id } = params;
|
||||
const { createEmptyProject, projectData, projectUpdate } = useProject();
|
||||
const hasCreatedProject = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id && !hasCreatedProject.current) {
|
||||
createEmptyProject();
|
||||
hasCreatedProject.current = true; // Prevent further calls
|
||||
}
|
||||
}, [id, createEmptyProject]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -51,11 +37,6 @@ export default function TextPanel() {
|
|||
canvas.setActiveObject(text);
|
||||
setActiveObject(text);
|
||||
canvas.renderAll();
|
||||
|
||||
const object = canvas.toJSON(['id', 'selectable']);
|
||||
const updateData = { ...projectData?.data, object };
|
||||
// Wait for the project update before continuing
|
||||
projectUpdate({ id, updateData });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { useContext, useRef, useState } from "react";
|
||||
import TextCustomization from "../EachComponent/Customization/TextCustomization";
|
||||
import LockObject from "../EachComponent/Customization/LockObject";
|
||||
import { ScrollArea, ScrollBar } from "../ui/scroll-area";
|
||||
import OpacityCustomization from "../EachComponent/Customization/OpacityCustomization";
|
||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||
import { useContext } from "react";
|
||||
import { ObjectShortcut } from "../ObjectShortcut";
|
||||
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
|
||||
import { Button } from "../ui/button";
|
||||
import { ImagePlus, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { ImagePlus } from "lucide-react";
|
||||
import { RxBorderWidth } from "react-icons/rx";
|
||||
import { LuFlipVertical } from "react-icons/lu";
|
||||
import { SlTarget } from "react-icons/sl";
|
||||
|
|
@ -21,15 +22,19 @@ export function TopBar() {
|
|||
const activeObjectType = activeObject?.type;
|
||||
const hasClipPath = !!activeObject?.clipPath;
|
||||
const customClipPath = activeObject?.isClipPath;
|
||||
// w - [calc(100 % -80px)] h - full
|
||||
return (
|
||||
<div className="w-full h-full overflow-x-scroll p-1">
|
||||
<div className="flex item-center justify-center w-fit mx-auto">
|
||||
<div className="bg-white mx-auto flex justify-center items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<div>
|
||||
<ScrollArea
|
||||
className={`!absolute top-2 -translate-x-1/2 z-40 h-28 ${
|
||||
selectedPanel !== ""
|
||||
? "md:w-[475px] 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%]"
|
||||
} `}
|
||||
>
|
||||
<div className="bg-white shadow-sm mx-auto px-4 py-2 flex justify-center items-center gap-2">
|
||||
<div>
|
||||
<TextCustomization />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 px-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -40,7 +45,6 @@ export function TopBar() {
|
|||
<span>Add image</span>
|
||||
</Button>
|
||||
|
||||
{/* Canvas settings */}
|
||||
<div>
|
||||
<a data-tooltip-id="canvas">
|
||||
<Button
|
||||
|
|
@ -56,15 +60,43 @@ export function TopBar() {
|
|||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="3" y="5" width="18" height="12" stroke="black" strokeWidth="2" fill="none" />
|
||||
<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" />
|
||||
<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>
|
||||
</a>
|
||||
<Tooltip id="canvas" content="Canvas Settings" place="bottom" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -76,71 +108,94 @@ export function TopBar() {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="rounded-full"
|
||||
className={"rounded-full"}
|
||||
onClick={() => setSelectedPanel("color")}
|
||||
style={{ backgroundColor: textColor?.fill || "black" }}
|
||||
></Button>
|
||||
</a>
|
||||
)}
|
||||
<Tooltip id="color-gr" content="Color" place="bottom" />
|
||||
|
||||
{!customClipPath && (
|
||||
<a data-tooltip-id="stroke">
|
||||
<Button variant="ghost" size="icon" className="w-10 h-10" onClick={() => setSelectedPanel("stroke")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-10 h-10"
|
||||
onClick={() => setSelectedPanel("stroke")}
|
||||
>
|
||||
<RxBorderWidth className="text-lg" />
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
<Tooltip id="stroke" content="Stroke" place="bottom" />
|
||||
|
||||
{(activeObject || activeObject.type === "group") && (
|
||||
<a data-tooltip-id="group-obj">
|
||||
<Button variant="ghost" size="icon" className="w-10 h-10" onClick={() => setSelectedPanel("group-obj")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-10 h-10"
|
||||
onClick={() => setSelectedPanel("group-obj")}
|
||||
>
|
||||
<FaRegObjectGroup />
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
<Tooltip id="group-obj" content="Group Object" place="bottom" />
|
||||
</div>
|
||||
|
||||
<div className="h-6 w-px bg-gray-200" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<a data-tooltip-id="flip">
|
||||
<Button variant="ghost" size="icon" onClick={() => setSelectedPanel("flip")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSelectedPanel("flip")}
|
||||
>
|
||||
<LuFlipVertical />
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<Tooltip id="flip" content="Object flip" place="bottom" />
|
||||
{activeObject?.type !== "group" && (
|
||||
<a data-tooltip-id="position">
|
||||
<Button variant="ghost" size="icon" onClick={() => setSelectedPanel("position")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSelectedPanel("position")}
|
||||
>
|
||||
<SlTarget />
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
|
||||
<Tooltip id="position" content="Object position" place="bottom" />
|
||||
<a data-tooltip-id="shadow">
|
||||
<Button variant="ghost" size="icon" onClick={() => setSelectedPanel("shadow")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSelectedPanel("shadow")}
|
||||
>
|
||||
<RiShadowLine />
|
||||
</Button>
|
||||
</a>
|
||||
<Tooltip id="shadow" content="Shadow color" place="bottom" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OpacityCustomization />
|
||||
<div className="h-4 w-px bg-border mx-2" />
|
||||
|
||||
<div>
|
||||
<ObjectShortcut value="default" />
|
||||
<ObjectShortcut value={"default"} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<LockObject />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
|
||||
<Tooltip id="color-gr" content="Color" place="bottom" />
|
||||
<Tooltip id="stroke" content="Stroke" place="bottom" />
|
||||
<Tooltip id="position" content="Object position" place="bottom" />
|
||||
<Tooltip id="shadow" content="Shadow color" place="bottom" />
|
||||
<Tooltip id="flip" content="Object flip" place="bottom" />
|
||||
<Tooltip id="group-obj" content="Group Object" place="bottom" />
|
||||
<Tooltip id="canvas" content="Canvas Settings" place="bottom" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,12 @@
|
|||
import { useContext, useEffect, useRef } from "react";
|
||||
import { useContext } from "react";
|
||||
import CanvasContext from "../Context/canvasContext/CanvasContext";
|
||||
import { X } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { ScrollArea } from "../ui/scroll-area";
|
||||
import UploadImage from "../EachComponent/UploadImage";
|
||||
import { useParams } from "react-router-dom";
|
||||
import useProject from "@/hooks/useProject";
|
||||
|
||||
const UploadPanel = () => {
|
||||
const { setSelectedPanel } = useContext(CanvasContext);
|
||||
|
||||
const params = useParams();
|
||||
const { id } = params;
|
||||
const { createEmptyProject } = useProject();
|
||||
const hasCreatedProject = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id && !hasCreatedProject.current) {
|
||||
createEmptyProject();
|
||||
hasCreatedProject.current = true; // Prevent further calls
|
||||
}
|
||||
}, [id, createEmptyProject]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
import { useContext, useEffect, useState } from 'react'
|
||||
import CanvasContext from './Context/canvasContext/CanvasContext';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useToast } from '../hooks/use-toast';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Save, Trash2 } from 'lucide-react';
|
||||
import { Textarea } from './ui/textarea';
|
||||
import { Button } from './ui/button';
|
||||
import { Separator } from './ui/separator';
|
||||
import useProject from '@/hooks/useProject';
|
||||
import { captureCanvas } from '@/lib/captureCanvas';
|
||||
import useCanvasCapture from '@/hooks/useCanvasCapture';
|
||||
|
||||
const SaveCanvas = () => {
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
|
||||
const [saveCanvas, setSaveCanvas] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
preview_url: "",
|
||||
});
|
||||
|
||||
const params = useParams();
|
||||
const { id } = params;
|
||||
const { toast } = useToast();
|
||||
|
||||
const { projectDelete, deletePending, isLoading, projectData, projectUpdate, updatePending } = useProject();
|
||||
|
||||
// to set projectData into state
|
||||
useEffect(() => {
|
||||
if (projectData?.data && !isLoading && projectData?.data?.preview_url !== null) {
|
||||
setSaveCanvas((prev) => ({
|
||||
...prev,
|
||||
name: projectData?.data?.name,
|
||||
description: projectData?.data?.description,
|
||||
preview_url: projectData?.data?.preview_url
|
||||
}));
|
||||
}
|
||||
}, [projectData, isLoading]);
|
||||
|
||||
const { uploadCanvasImage, uploadCanvasPending, removeCanvasImage, removeCanvasPending } =
|
||||
useCanvasCapture({ handleSaveWithPreViewImage, canvas, id, saveCanvas });
|
||||
|
||||
async function handleSaveProject() {
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error("Error removing image:", error);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const file = await captureCanvas(canvas);
|
||||
if (file) {
|
||||
uploadCanvasImage({ file, id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error capturing image:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// this will save the canvas as a json object
|
||||
async function 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 updateData = { ...body, object };
|
||||
projectUpdate({ id, updateData });
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteProject = () => {
|
||||
projectDelete(id);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='my-2'>
|
||||
<h2 className='font-bold text-sm flex gap-2 my-4'><Save /> Save Canvas </h2>
|
||||
|
||||
{
|
||||
saveCanvas?.preview_url &&
|
||||
<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 || isLoading || uploadCanvasPending || removeCanvasPending} onClick={handleDeleteProject}> <Trash2 /> </Button>
|
||||
<Button disabled={isLoading || updatePending} onClick={handleSaveProject}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SaveCanvas
|
||||
107
src/components/loadCanvas.js
Normal file
107
src/components/loadCanvas.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
const loadCanvasState = (state) => {
|
||||
if (!canvas2) {
|
||||
console.error('Canvas2 is not initialized!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const canvasData = typeof state === 'string' ? JSON.parse(state) : state;
|
||||
|
||||
// Clear the existing canvas
|
||||
canvas2.clear();
|
||||
|
||||
// Set the background color if it exists
|
||||
if (canvasData.backgroundColor) {
|
||||
canvas2.set({ backgroundColor: canvasData.backgroundColor });
|
||||
}
|
||||
|
||||
// Function to load background image
|
||||
const loadBackgroundImage = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (!canvasData.background) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const imgElement = new Image();
|
||||
imgElement.crossOrigin = 'anonymous'; // Handle CORS if needed
|
||||
|
||||
imgElement.onload = () => {
|
||||
const imgInstance = new fabric.Image(imgElement, {
|
||||
originX: canvasData.background.originX || 'left',
|
||||
originY: canvasData.background.originY || 'top',
|
||||
scaleX: canvasData.background.scaleX || canvas2.width / imgElement.width,
|
||||
scaleY: canvasData.background.scaleY || canvas2.height / imgElement.height,
|
||||
opacity: canvasData.background.opacity || 1,
|
||||
});
|
||||
|
||||
// Set background image using the set method
|
||||
canvas2.set({ backgroundImage: imgInstance });
|
||||
resolve();
|
||||
};
|
||||
|
||||
imgElement.onerror = () => {
|
||||
console.error("Failed to load the background image");
|
||||
resolve(); // Resolve anyway to continue loading objects
|
||||
};
|
||||
|
||||
imgElement.src = canvasData.background.src;
|
||||
});
|
||||
};
|
||||
|
||||
// Function to load objects
|
||||
const loadObjects = () => {
|
||||
return new Promise((resolve) => {
|
||||
canvas2.loadFromJSON({ objects: canvasData.objects }, () => {
|
||||
resolve();
|
||||
}, (obj, element) => {
|
||||
return obj;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Load background image and objects if they exist
|
||||
const loadTasks = [];
|
||||
if (canvasData.background || canvasData.objects?.length) {
|
||||
if (canvasData.background) {
|
||||
loadTasks.push(loadBackgroundImage());
|
||||
}
|
||||
if (canvasData.objects?.length) {
|
||||
loadTasks.push(loadObjects());
|
||||
}
|
||||
|
||||
Promise.all(loadTasks)
|
||||
.then(() => {
|
||||
canvas2.requestRenderAll(); // Ensure re-rendering
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during canvas loading:', error);
|
||||
});
|
||||
} else {
|
||||
// If only background color exists
|
||||
if (canvasData.backgroundColor) {
|
||||
canvas2.requestRenderAll(); // Apply background color and render
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading canvas state:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// for download image/canvas
|
||||
const exportScaledCanvasImage = (scale = 2) => {
|
||||
if (canvas) {
|
||||
const imageBase64 = canvas.toDataURL({
|
||||
format: 'png', // or 'jpeg'
|
||||
quality: 1, // (0 to 1) for JPEG compression, ignored for PNG
|
||||
multiplier: scale, // Scale the canvas size
|
||||
});
|
||||
|
||||
// Download the scaled image
|
||||
const link = document.createElement('a');
|
||||
link.href = imageBase64;
|
||||
link.download = `canvas-image-${scale}x.png`;
|
||||
link.click();
|
||||
}
|
||||
};
|
||||
|
|
@ -6,7 +6,7 @@ const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
|||
return (
|
||||
(<textarea
|
||||
className={cn(
|
||||
"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",
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const ToastViewport = React.forwardRef(({ className, ...props }, ref) => (
|
|||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[9999] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
"fixed top-0 z-[1000] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
import { deleteImage, uploadImage } from "@/api/uploadApi";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useToast } from "./use-toast";
|
||||
import { captureCanvas } from "@/lib/captureCanvas";
|
||||
|
||||
const useCanvasCapture = ({ handleSaveWithPreViewImage, canvas, id, saveCanvas }) => {
|
||||
const { toast } = useToast();
|
||||
// Upload Image Mutation
|
||||
const { mutate: uploadCanvasImage, isPending: uploadCanvasPending } = useMutation({
|
||||
mutationFn: async ({ file, id }) => await uploadImage({ file, id }),
|
||||
onSuccess: (data) => {
|
||||
toast({
|
||||
title: data?.status,
|
||||
description: data?.message,
|
||||
variant: data?.status === 200 ? "default" : "destructive",
|
||||
});
|
||||
|
||||
if (data?.status === 200) {
|
||||
handleSaveWithPreViewImage({
|
||||
preview_url: data?.data[0]?.url,
|
||||
name: saveCanvas?.name,
|
||||
description: saveCanvas?.description,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Remove Image Mutation
|
||||
const { mutate: removeCanvasImage, isPending: removeCanvasPending } = useMutation({
|
||||
mutationFn: async (url) => await deleteImage(url),
|
||||
onSuccess: async (data) => {
|
||||
toast({
|
||||
title: data?.status,
|
||||
description: data?.message,
|
||||
variant: data?.status === 200 ? "default" : "destructive",
|
||||
});
|
||||
|
||||
if (data?.status === 200) {
|
||||
const file = await captureCanvas(canvas);
|
||||
if (file) {
|
||||
uploadCanvasImage({ file, id });
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return { uploadCanvasImage, uploadCanvasPending, removeCanvasImage, removeCanvasPending };
|
||||
}
|
||||
|
||||
export default useCanvasCapture
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useContext } from "react";
|
||||
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||
import { fabric } from "fabric";
|
||||
import useProject from "./useProject";
|
||||
import { useToast } from "./use-toast";
|
||||
import { deleteImage, uploadImage } from "@/api/uploadApi";
|
||||
|
||||
const useImageHandler = ({ removeFile }) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const { canvas } = useContext(CanvasContext);
|
||||
|
||||
const { projectData, projectUpdate } = useProject();
|
||||
|
||||
// Upload image handler
|
||||
const { mutate: uploadMutate } = useMutation({
|
||||
mutationFn: async ({ file, id }) => await uploadImage({ file, id }),
|
||||
onSuccess: (data, { id }) => {
|
||||
if (data?.status === 200) {
|
||||
toast({ title: data?.status, description: data?.message });
|
||||
|
||||
fabric.Image.fromURL(
|
||||
data?.data[0]?.url,
|
||||
(img) => {
|
||||
img.applyFilters();
|
||||
img.scale(0.5);
|
||||
img.set("top", canvas.width / 4);
|
||||
img.set("left", canvas.height / 4);
|
||||
canvas.add(img);
|
||||
canvas.setActiveObject(img);
|
||||
// Update the active object state
|
||||
projectUpdate({ id, updateData: { ...projectData?.data, object: canvas.toJSON(['id', 'selectable']), preview_url: "" } });
|
||||
canvas.renderAll();
|
||||
},
|
||||
{ crossOrigin: "anonymous" }
|
||||
);
|
||||
} else {
|
||||
toast({ variant: "destructive", title: data?.status, description: data?.message });
|
||||
removeFile();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Remove image handler
|
||||
const { mutate: deleteMutate } = useMutation({
|
||||
mutationFn: async (url) => await deleteImage(url),
|
||||
onSuccess: (data, { id }) => {
|
||||
if (data?.status === 200) {
|
||||
toast({ title: data?.status, description: data?.message });
|
||||
|
||||
if (canvas) {
|
||||
projectUpdate({ id, updateData: { ...projectData?.data, object: canvas.toJSON(['id', 'selectable']) } });
|
||||
}
|
||||
} else {
|
||||
toast({ variant: "destructive", title: data?.status, description: data?.message });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return { uploadMutate, deleteMutate };
|
||||
};
|
||||
|
||||
export default useImageHandler;
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useToast } from "./use-toast";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { createProject, deleteProject, getProjectById, updateProject } from "@/api/projectApi";
|
||||
import { useContext } from "react";
|
||||
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
|
||||
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
|
||||
|
||||
const useProject = () => {
|
||||
const { toast } = useToast();
|
||||
const { canvas, setSelectedPanel, selectedPanel } = useContext(CanvasContext);
|
||||
const { setActiveObject } = useContext(ActiveObjectContext);
|
||||
|
||||
const params = useParams();
|
||||
const { id } = params;
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createEmptyProject = async () => {
|
||||
try {
|
||||
const response = await createProject();
|
||||
if (response?.status === 200) {
|
||||
toast({
|
||||
title: response?.status,
|
||||
description: response?.message,
|
||||
});
|
||||
}
|
||||
if (response?.data?.id) {
|
||||
navigate(`/${response.data.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Project creation failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch project data
|
||||
const { data: projectData, isLoading } = useQuery({
|
||||
queryKey: ["project", id],
|
||||
queryFn: () => getProjectById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
// Update project
|
||||
const { mutate: projectUpdate, isPending: updatePending } = useMutation({
|
||||
mutationFn: async ({ id, updateData }) => {
|
||||
return await updateProject({ id, ...updateData });
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data?.status === 200) {
|
||||
if (selectedPanel === "canvas" && canvas) {
|
||||
toast({
|
||||
title: data?.status,
|
||||
description: data?.message,
|
||||
})
|
||||
canvas.clear();
|
||||
canvas.renderAll();
|
||||
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
||||
setActiveObject(null);
|
||||
setTimeout(() => {
|
||||
navigate("/");
|
||||
setSelectedPanel("");
|
||||
}, 500); // Ensure clearing happens before navigation
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
} else {
|
||||
toast({
|
||||
title: data?.status,
|
||||
description: data?.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: projectDelete, isPending: deletePending } = useMutation({
|
||||
mutationFn: async (id) => await deleteProject(id),
|
||||
onSuccess: (data, id) => {
|
||||
if (data?.status === 200) {
|
||||
toast({
|
||||
title: data?.status,
|
||||
description: data?.message,
|
||||
});
|
||||
|
||||
// Invalidate queries to refresh data
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["project", id] });
|
||||
|
||||
// Clear canvas if it exists
|
||||
if (canvas) {
|
||||
canvas.clear();
|
||||
canvas.renderAll();
|
||||
canvas.setBackgroundColor("#ffffff", canvas.renderAll.bind(canvas));
|
||||
}
|
||||
setActiveObject(null);
|
||||
|
||||
setSelectedPanel("");
|
||||
navigate("/");
|
||||
|
||||
} else {
|
||||
toast({
|
||||
title: data?.status,
|
||||
description: data?.message,
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error.message || "Failed to delete the project",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { projectData, isLoading, projectUpdate, id, projectDelete, deletePending, updatePending, createEmptyProject };
|
||||
};
|
||||
|
||||
export default useProject;
|
||||
|
|
@ -153,23 +153,3 @@
|
|||
@apply bg-[#f5f0ff] text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
height: 5px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
/* background-color: var(--primary-600); */
|
||||
@apply bg-red-50
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--primary-500);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
width: 5px;
|
||||
background-color: red;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
export const captureCanvas = async (canvas) => {
|
||||
if (!canvas) return;
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
const tempCtx = tempCanvas.getContext("2d");
|
||||
|
||||
tempCanvas.width = width;
|
||||
tempCanvas.height = height;
|
||||
|
||||
const dataUrl = canvas.toDataURL("image/jpeg", 1);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.src = dataUrl;
|
||||
|
||||
img.onload = () => {
|
||||
tempCtx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
fetch(tempCanvas.toDataURL("image/jpeg", 1))
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
resolve(new File([blob], "PlanPostAi-capture.jpg", { type: "image/jpeg" }));
|
||||
})
|
||||
.catch(reject);
|
||||
};
|
||||
|
||||
img.onerror = (error) => reject(error);
|
||||
});
|
||||
};
|
||||
15
src/main.jsx
15
src/main.jsx
|
|
@ -5,32 +5,17 @@ import CanvasContextProvider from "./components/Context/canvasContext/CanvasCont
|
|||
import ColorContextProvider from "./components/Context/colorContext/ColorContextProvider";
|
||||
import ObjectProvider from "./components/Context/activeObject/ObjectProvider";
|
||||
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";
|
||||
import GlobalErrorBoundary from "./ErrorBoundary";
|
||||
|
||||
// Create a client
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
// <StrictMode>
|
||||
<AuthContextProvider>
|
||||
<CanvasContextProvider>
|
||||
<ColorContextProvider>
|
||||
<ObjectProvider>
|
||||
<OpenContextProvider>
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<GlobalErrorBoundary>
|
||||
<App />
|
||||
</GlobalErrorBoundary>
|
||||
</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
</OpenContextProvider>
|
||||
</ObjectProvider>
|
||||
</ColorContextProvider>
|
||||
</CanvasContextProvider>
|
||||
</AuthContextProvider>
|
||||
// </StrictMode>,
|
||||
);
|
||||
|
|
|
|||
10
src/vite-env.d.ts
vendored
10
src/vite-env.d.ts
vendored
|
|
@ -1,10 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_TITLE: string
|
||||
// more env variables...
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
|
|
@ -9,65 +9,58 @@ export default {
|
|||
extend: {
|
||||
textColor: {
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary-text))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
}
|
||||
DEFAULT: "hsl(var(--primary-text))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
},
|
||||
screens: {
|
||||
mdxl: '1100px'
|
||||
mdxl: "1100px",
|
||||
},
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
selection: {
|
||||
DEFAULT: 'hsl(var(--selection))',
|
||||
foreground: 'hsl(var(--selection-foreground))'
|
||||
DEFAULT: "hsl(var(--selection))",
|
||||
foreground: "hsl(var(--selection-foreground))",
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
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: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
}
|
||||
}
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
plugins: [scrollbar, scrollbarHide, require("tailwindcss-animate")],
|
||||
},
|
||||
},
|
||||
plugins: [scrollbar, scrollbarHide],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,4 @@ export default defineConfig({
|
|||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5175
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue