all requirement added

This commit is contained in:
Saimon8420 2025-03-01 14:20:45 +06:00
parent 58376217d3
commit 6b29e136f4
124 changed files with 21573 additions and 0 deletions

1
.env Normal file
View file

@ -0,0 +1 @@
VITE_SERVER_URL=http://localhost:3000/api

30
.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

BIN
bun.lockb Normal file

Binary file not shown.

21
components.json Normal file
View file

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

3
env.examples Normal file
View file

@ -0,0 +1,3 @@
all these env's are required to run the project
VITE_SERVER_URL=

38
eslint.config.js Normal file
View file

@ -0,0 +1,38 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

13
index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/PlanPost AI_Logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PlanPostAi Canvas</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

10
jsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
}
}

10630
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

70
package.json Normal file
View file

@ -0,0 +1,70 @@
{
"name": "canvas",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-aspect-ratio": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.2",
"@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",
"clsx": "^2.1.1",
"fabric": "^5.3.0",
"lodash": "^4.17.21",
"lucide-react": "^0.456.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-dropzone": "^14.3.5",
"react-icons": "^5.4.0",
"react-image-file-resizer": "^0.4.8",
"react-inlinesvg": "^4.2.0",
"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-scrollbar": "^3.1.0",
"tailwind-scrollbar-hide": "^1.3.1",
"tailwindcss-animate": "^1.0.7",
"webfontloader": "^1.6.28"
},
"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",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.13.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"postcss": "^8.4.48",
"sass-embedded": "^1.83.4",
"tailwindcss": "^3.4.14",
"vite": "^5.4.10"
}
}

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

13
src/App.css Normal file
View file

@ -0,0 +1,13 @@
.fabric-canvas-container {
box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.2);
border-radius: 0px;
}
.tooltip {
--tooltip-spacing: 30px;
}
.\[\&_svg\]\:size-4 svg {
width: 1.3rem !important;
height: 1.3rem !important;
}

85
src/App.jsx Normal file
View file

@ -0,0 +1,85 @@
import { useEffect } from "react";
import "./App.css";
import WebFont from "webfontloader";
import { Routes, Route } from "react-router-dom";
import { Home } from "./Home";
import NotFound from "./components/Pages/NotFound";
import Login from "./components/Pages/Login";
import Register from "./components/Pages/Register";
import ResentVerification from "./components/Pages/ResendVerification";
import ForgotPassword from "./components/Pages/ForgotPassword";
function App() {
useEffect(() => {
WebFont.load({
google: {
families: [
"Roboto:300,400,500,700",
"Open Sans:300,400,600,700",
"Lato:300,400,700",
"Montserrat:300,400,500,700",
"Raleway:300,400,500,700",
"Poppins:300,400,500,700",
"Merriweather:300,400,700",
"Playfair Display:400,500,700",
"Nunito:300,400,600,700",
"Oswald:300,400,500,600",
"Source Sans Pro:300,400,600,700",
"Ubuntu:300,400,500,700",
"Noto Sans:300,400,500,700",
"Work Sans:300,400,500,700",
"Bebas Neue",
"Arimo:300,400,500,700",
"PT Sans:300,400,700",
"PT Serif:300,400,700",
"Titillium Web:300,400,600,700",
"Fira Sans:300,400,500,700",
"Karla:300,400,600,700",
"Josefin Sans:300,400,500,700",
"Cairo:300,400,600,700",
"Rubik:300,400,500,700",
"Mulish:300,400,500,700",
"IBM Plex Sans:300,400,500,700",
"Quicksand:300,400,500,700",
"Cabin:300,400,500,700",
"Heebo:300,400,500,700",
"Exo 2:300,400,500,700",
"Manrope:300,400,500,700",
"Jost:300,400,500,700",
"Anton",
"Asap:300,400,600,700",
"Baloo 2:300,400,500,700",
"Barlow:300,400,500,700",
"Cantarell:300,400,700",
"Chivo:300,400,500,700",
"Inter:300,400,500,700",
"Dosis:300,400,500,700",
"Crimson Text:300,400,600,700",
"Amatic SC:300,400,700",
"ABeeZee",
"Raleway Dots",
"Pacifico",
"Orbitron:300,400,500,700",
"Varela Round",
"Acme",
"Teko:300,400,500,700",
],
},
});
}, []);
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/:id" element={<Home />} />
<Route path="*" element={<NotFound />} />
<Route path="/notFound" element={<NotFound />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/resend-verification" element={<ResentVerification />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
</Routes>
);
}
export default App;

54
src/ErrorBoundary.jsx Normal file
View file

@ -0,0 +1,54 @@
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;

125
src/Home.jsx Normal file
View file

@ -0,0 +1,125 @@
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 { useQuery } from '@tanstack/react-query';
import { generateToken } 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 { useToast } from './hooks/use-toast';
import useProject from './hooks/useProject';
export const Home = () => {
const { activeObject } = useContext(ActiveObjectContext);
const { canvas, selectedPanel } = useContext(CanvasContext);
const navigate = useNavigate();
const getToken = () => localStorage.getItem('canvas_token');
const path = location.pathname;
const { toast } = useToast();
// Fetch project data only if token and id exist
const { projectData, isLoading: projectLoading } = useProject();
// Fetch token only if it doesn't exist
const { data, isLoading } = useQuery({
queryKey: ['get-token'],
queryFn: () => generateToken(),
enabled: !!getToken(),
});
useEffect(() => {
const checkToken = () => {
const token = getToken();
if (!token) {
navigate("/login");
}
};
checkToken(); // Run immediately on mount
window.addEventListener("storage", checkToken);
return () => {
window.removeEventListener("storage", checkToken);
};
}, [navigate]);
// to load the project data into canvas when the project is selected or the page is refreshed
useEffect(() => {
if (projectData?.status === 500) {
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();
});
}
}
}
}, [projectData, toast, canvas, selectedPanel, projectLoading, path, navigate, isLoading])
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">
<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-16 flex justify-center items-center h-20 bg-white border-t border-gray-200 shadow-md w-fit">
<p></p>
<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 border'>
<Sidebar />
</div>
</>
}
</div>
)
}

126
src/api/authApi.ts Normal file
View file

@ -0,0 +1,126 @@
export const generateToken = async (id: string) => {
try {
const url = `${import.meta.env.VITE_SERVER_URL}/auth/isAuthenticated/`;
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 {
localStorage.removeItem("canvas_token");
return data;
}
} catch (error) {
console.error('Un-authenticated:', error);
throw error;
}
}
interface LoginBody {
email: string;
password: string;
}
export const login = async (body: LoginBody) => {
try {
const { email, password } = body;
const url = `${import.meta.env.VITE_SERVER_URL}/auth/login`;
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
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
}
return data;
} catch (error) {
console.error('Cannot login:', error);
throw error;
}
}
interface RegisterBody {
email: string;
password: string;
name: string;
}
export const register = async (body: RegisterBody) => {
try {
const { email, password, name } = body;
const url = `${import.meta.env.VITE_SERVER_URL}/auth/register`;
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password, name }),
});
const data = await response.json();
return data;
}
catch (error) {
console.error('Cannot register:', error);
throw error;
}
}
export const resetPassword = async (body: { email: string }) => {
try {
const { email } = body;
const url = `${import.meta.env.VITE_SERVER_URL}/auth/reset-password/verify`;
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error('Cannot reset password:', error);
throw error;
}
}
export const resendVerificationEmail = async (body: { email: string }) => {
try {
const { email } = body;
const url = `${import.meta.env.VITE_SERVER_URL}/auth/resend-verification`;
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
const data = await response.json();
console.log(data);
return data;
}
catch (error) {
console.error('Cannot resend verification email:', error);
throw error;
}
}

74
src/api/categoryApi.ts Normal file
View file

@ -0,0 +1,74 @@
export const getAllCategory = async () => {
const url = `${import.meta.env.VITE_SERVER_URL}/category/get-all`;
try {
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (data?.token) {
localStorage.setItem('canvas_token', data.token);
const { token, ...rest } = data;
return rest;
}
else {
localStorage.removeItem('canvas_token');
return data;
}
} catch (error) {
console.error('Error fetching categories:', error);
throw error;
}
}
export const createCategory = async (category: string) => {
const url = `${import.meta.env.VITE_SERVER_URL}/category/create`;
try {
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: category }),
})
const data = await response.json();
if (data?.token) {
localStorage.setItem('canvas_token', data.token);
return data;
}
else {
localStorage.removeItem('canvas_token');
return data;
}
}
catch (error) {
console.error('Error creating category:', error);
throw error;
}
}
export const deleteCategory = async (categoryId: string) => {
try {
const url = `${import.meta.env.VITE_SERVER_URL}/category/delete/${categoryId}`;
const response = await fetch(url, {
method: 'DELETE',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
}
});
const data = await response.json();
if (data?.token) {
localStorage.setItem('canvas_token', data.token);
return data;
}
else {
localStorage.removeItem('canvas_token');
return data;
}
} catch (error) {
console.error('Error deleting category:', error);
throw error;
}
}

28
src/api/photoLibrary.ts Normal file
View file

@ -0,0 +1,28 @@
interface body {
keyword: string,
per_page: number,
}
export const getPhotos = async (body: body) => {
try {
const { keyword, per_page } = body;
const url = `${import.meta.env.VITE_SERVER_URL}/photos/?keyword=${keyword}&per_page=${per_page}`;
const response = await fetch(url, {
method: 'GET',
credentials: 'include',
});
const data = await response.json();
if (data?.data?.token) {
localStorage.setItem('canvas_token', data.token);
// Remove the token from the response data
const { token, ...restData } = data?.data?.data;
return restData; // Return modified data without token
}
else {
localStorage.removeItem("canvas_token");
return data;
}
} catch (error) {
console.error('Un-authenticated:', error);
}
}

142
src/api/projectApi.ts Normal file
View file

@ -0,0 +1,142 @@
interface Project {
id: string,
name: string;
description: string;
object: JSON;
preview_url: string;
category: 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 {
localStorage.removeItem("canvas_token");
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 {
localStorage.removeItem("canvas_token");
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 {
localStorage.removeItem("canvas_token");
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, category } = body;
let project;
if (category === undefined || category === null) {
project = { name, description, object, preview_url, category: "" };
}
else {
project = { name, description, object, preview_url, category };
}
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 {
localStorage.removeItem("canvas_token");
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 {
localStorage.removeItem("canvas_token");
return data;
}
} catch (error) {
console.error('Failed to delete project:', error);
throw error;
}
}

55
src/api/uploadApi.ts Normal file
View file

@ -0,0 +1,55 @@
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 {
localStorage.removeItem("canvas_token");
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 {
localStorage.removeItem("canvas_token");
return data;
}
} catch (error) {
console.error("Error deleting file:", error);
throw error;
}
}

72
src/api/uploadShapeApi.ts Normal file
View file

@ -0,0 +1,72 @@
export const uploadShape = async (shape: string) => {
try {
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/upload-shapes/add`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ shape }),
})
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 {
localStorage.removeItem("canvas_token");
return data;
}
} catch (error) {
console.error('Error uploading shapes to server:', error);
throw error;
}
}
export const deleteShape = async (id: string) => {
try {
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/upload-shapes/delete/${id}`, {
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 {
localStorage.removeItem("canvas_token");
return data;
}
} catch (error) {
console.error('Error deleting shapes from server:', error);
throw error;
}
}
export const getAllShape = async () => {
try {
const response = await fetch(`${import.meta.env.VITE_SERVER_URL}/upload-shapes/get`, {
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 {
localStorage.removeItem("canvas_token");
return data;
}
} catch (error) {
console.error('Error getting shapes from server:', error);
throw error;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

1
src/assets/react.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,151 @@
import CanvasContext from "./Context/canvasContext/CanvasContext";
import OpenContext from "./Context/openContext/OpenContext";
import { Button } from "./ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import { useContext } from "react";
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: "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 (4:4)" },
{ value: "9:16", label: "Vertical (9:16)" },
{ value: "1.33:1", label: "Instagram Stories (1.33:1)" },
{ value: "1.91:1", label: "Facebook Ads (1.91:1)" },
];
export function ActionButtons() {
const { setCaptureOpen } = useContext(OpenContext);
const { setCanvasRatio, canvasRatio, setSelectedPanel } = useContext(CanvasContext);
const handleRatioChange = (newRatio) => {
setCanvasRatio(newRatio);
};
return (
<div className="absolute top-4 right-0 z-50 flex items-center gap-2 bg-white rounded-l-[16px]">
<div className="px-2 py-2">
<div className="w-full">
<Select onValueChange={handleRatioChange} value={canvasRatio}>
<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>
</div>
<Button
variant="outline"
size="icon"
className="bg-[#F5F2FF] w-10 h-10"
onClick={() => {
setCaptureOpen(true);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="21"
height="21"
viewBox="0 0 21 21"
fill="none"
>
<path
d="M10.1288 13.041V1"
stroke="#6A47ED"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.0448 10.1143L10.1288 13.0423L7.21277 10.1143"
stroke="#6A47ED"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14.62 5.50879C18.199 5.83879 19.5 7.17879 19.5 12.5088C19.5 19.6088 17.189 19.6088 10.25 19.6088C3.309 19.6088 1 19.6088 1 12.5088C1 7.17879 2.3 5.83879 5.88 5.50879"
stroke="#6A47ED"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</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>
);
}

View file

@ -0,0 +1,298 @@
import { useEffect, useContext, useState, useRef, useCallback } from "react";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import OpenContext from "../Context/openContext/OpenContext";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Card, CardContent } from "../ui/card";
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 { canvasRef, canvas, setCanvas, fabricCanvasRef, canvasRatio, setCanvasHeight, setCanvasWidth, setScreenWidth, selectedPanel, setSelectedPanel } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const [zoomLevel, setZoomLevel] = useState(100);
const containerRef = useRef(null);
const handleZoom = useCallback(
(newZoom) => {
const zoom = Math.min(Math.max(newZoom, 50), 100); // Prevent zoom from going below 10
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);
canvas.setWidth(newWidth);
canvas.setHeight(newHeight);
setCanvasWidth(newWidth);
setCanvasHeight(newHeight);
canvas.renderAll();
}
},
[canvas, canvasRef, setCanvasHeight, setCanvasWidth]
);
useEffect(() => {
if (!canvas) return;
const handleWheel = (event) => {
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
const delta = event.deltaY > 0 ? -1 : 1;
handleZoom(zoomLevel + delta);
event.stopPropagation();
}
};
const handleKeyboard = (event) => {
if (
(event.ctrlKey || event.metaKey) &&
(event.key === "=" || event.key === "-")
) {
event.preventDefault();
const delta = event.key === "=" ? 1 : -1;
handleZoom(zoomLevel + delta);
}
};
let lastDistance = 0;
const handleTouchStart = (event) => {
if (event.touches.length === 2) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
lastDistance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
}
};
const handleTouchMove = (event) => {
if (event.touches.length === 2) {
event.preventDefault();
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const distance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
if (lastDistance > 0) {
const delta = distance - lastDistance;
const zoomDelta = delta > 0 ? 1 : -1;
handleZoom(zoomLevel + zoomDelta);
}
lastDistance = distance;
}
};
const handleTouchEnd = () => {
lastDistance = 0;
};
const handleResize = () => {
handleZoom(zoomLevel);
};
const canvasContainer = document.getElementById("canvas-ref");
if (canvasContainer) {
canvasContainer.addEventListener("wheel", handleWheel, {
passive: false,
});
canvasContainer.addEventListener("touchstart", handleTouchStart);
canvasContainer.addEventListener("touchmove", handleTouchMove, {
passive: false,
});
canvasContainer.addEventListener("touchend", handleTouchEnd);
window.addEventListener("keydown", handleKeyboard);
window.addEventListener("resize", handleResize);
}
return () => {
if (canvasContainer) {
canvasContainer.removeEventListener("wheel", handleWheel);
canvasContainer.removeEventListener("touchstart", handleTouchStart);
canvasContainer.removeEventListener("touchmove", handleTouchMove);
canvasContainer.removeEventListener("touchend", handleTouchEnd);
window.removeEventListener("keydown", handleKeyboard);
window.removeEventListener("resize", handleResize);
}
};
}, [canvas, zoomLevel, handleZoom]);
useEffect(() => {
import("fabric").then((fabricModule) => {
window.fabric = fabricModule.fabric;
});
}, [canvasRef, fabricCanvasRef, setCanvas]);
const getRatioValue = (ratio) => {
const [width, height] = ratio.split(":").map(Number);
return width / height;
};
useEffect(() => {
const updateCanvasSize = () => {
if (canvasRef.current && canvas) {
const newWidth = canvasRef.current.offsetWidth;
const newHeight = canvasRef.current.offsetHeight;
canvas.setWidth(newWidth);
canvas.setHeight(newHeight);
setCanvasWidth(newWidth);
setCanvasHeight(newHeight);
const bgImage = canvas.backgroundImage;
if (bgImage) {
const scaleX = newWidth / bgImage.width;
const scaleY = newHeight / bgImage.height;
const scale = Math.max(scaleX, scaleY);
bgImage.scaleX = scale;
bgImage.scaleY = scale;
bgImage.left = 0;
bgImage.top = 0;
canvas.setBackgroundImage(bgImage, canvas.renderAll.bind(canvas));
} else {
canvas.renderAll();
}
}
setScreenWidth(document.getElementById("root").offsetWidth);
if (document.getElementById("root").offsetWidth <= 640) {
setLeftPanelOpen(false);
setRightPanelOpen(false);
}
if (document.getElementById("root").offsetWidth > 640) {
setOpenObjectPanel(false);
setOpenSetting(false);
}
};
updateCanvasSize();
window.addEventListener("resize", updateCanvasSize);
return () => window.removeEventListener("resize", updateCanvasSize);
}, [setCanvasWidth, setCanvasHeight, canvasRatio, canvasRef, canvas, setLeftPanelOpen, setOpenObjectPanel, setRightPanelOpen, rightPanelOpen, setScreenWidth, setOpenSetting, selectedPanel]);
useEffect(() => {
if (window.fabric) {
if (fabricCanvasRef?.current) {
fabricCanvasRef.current.dispose();
}
const canvasElement = document.getElementById("fabric-canvas");
if (canvasElement) {
canvasElement.classList.add("fabric-canvas-container");
}
fabricCanvasRef.current = new window.fabric.Canvas("fabric-canvas", {
width: canvasRef?.current?.offsetWidth,
height: canvasRef?.current?.offsetWidth,
backgroundColor: "#ffffff",
allowTouchScrolling: true,
selection: true,
preserveObjectStacking: true,
});
setCanvas(fabricCanvasRef.current);
}
}, [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>
{/* Canvas Container */}
<div className="w-full max-w-2xl mx-auto mt-16">
<Card
className={`w-full rounded-none flex-1 flex flex-col
mx-auto border-0 shadow-none bg-transparent
${zoomLevel < 100 ? "overflow-hidden" : ""}`}
>
<CardContent
className={`p-0 h-full bg-transparent shadow-none ${selectedPanel === "canvas" ? "border" : ""}`}
ref={containerRef}
onDoubleClick={() => {
setSelectedPanel("canvas");
}}
onClick={() => {
if (selectedPanel === "canvas") {
setSelectedPanel("");
}
}}
>
<AspectRatio
ratio={getRatioValue(canvasRatio)}
className="rounded-lg border-0 border-primary/10 transition-all duration-300 ease-in-out"
>
<div
ref={canvasRef}
className="w-full h-full flex items-start justify-center rounded-md shadow-none touch-none"
id="canvas-ref"
style={{
touchAction: "none",
transform: `scale(${zoomLevel / 100})`,
transformOrigin: "50% 0",
transition: "transform 0.1s ease-out",
}}
>
<canvas id="fabric-canvas" />
</div>
</AspectRatio>
</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>
);
}

View file

@ -0,0 +1,149 @@
import { useContext, useState } from 'react';
import CanvasContext from './Context/canvasContext/CanvasContext';
import OpenContext from './Context/openContext/OpenContext';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle } from './ui/card';
import { X } from 'lucide-react';
import { Separator } from './ui/separator';
import RndComponent from './Layouts/RndComponent';
const resolutions = [
{ value: '720p', label: 'HD (1280x720)', width: 1280, height: 720 },
{ value: '1080p', label: 'Full HD (1920x1080)', width: 1920, height: 1080 },
{ value: '1440p', label: 'QHD (2560x1440)', width: 2560, height: 1440 },
{ value: '4k', label: '4K (3840x2160)', width: 3840, height: 2160 },
{ value: 'story', label: 'Story (1080x1920)', width: 1080, height: 1920 }, // Added for Stories
{ value: '5k', label: '5K (5120x2880)', width: 5120, height: 2880 },
{ value: 'ultrawide', label: 'Ultrawide (3440x1440)', width: 3440, height: 1440 },
{ value: 'cinematic', label: 'Cinematic (2560x1080)', width: 2560, height: 1080 },
];
const imageTypes = [
{ value: 'png', label: 'PNG' },
{ value: 'jpeg', label: 'JPEG' },
{ value: 'webp', label: 'WebP' },
];
const CanvasCapture = () => {
const [resolution, setResolution] = useState(resolutions[0].value);
const [imageType, setImageType] = useState(imageTypes[0].value);
const { canvas } = useContext(CanvasContext);
const { captureOpen, setCaptureOpen } = useContext(OpenContext);
const captureImage = async () => {
if (!canvas) return;
const selectedResolution = resolutions.find(res => res.value === resolution);
const { width, height } = selectedResolution;
// Create a temporary canvas for resizing
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
// Set desired resolution
tempCanvas.width = width;
tempCanvas.height = height;
// Render the fabric.js canvas onto the temporary canvas
const dataUrl = canvas.toDataURL({
format: imageType,
quality: 1,
multiplier: width / canvas.width, // Adjust the scale based on width
});
const img = new Image();
img.src = dataUrl;
img.onload = () => {
tempCtx.drawImage(img, 0, 0, width, height);
// Generate resized image data URL
const resizedDataUrl = tempCanvas.toDataURL(`image/${imageType}`, 1);
// Download the resized image
const link = document.createElement('a');
link.href = resizedDataUrl;
link.download = `PlanPostAi-capture-${resolution}.${imageType}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
};
const rndValue = {
valueX: 0,
valueY: 20,
width: 250,
height: 150,
minWidth: 250,
maxWidth: 300,
minHeight: 150,
maxHeight: 300,
bound: "parent"
};
const handleDownload = () => {
captureImage();
}
return (
<>
{captureOpen && (
<RndComponent value={rndValue}>
<Card className='space-y-2 p-2'>
<CardHeader className='p-0'>
<div className='flex items-center justify-between'>
<Button className="rnd-escape" variant={'ghost'} onClick={() => setCaptureOpen(false)}>
<X className='cursor-pointer' />
</Button>
<CardTitle className='text-lg mr-1'>Capture Panel</CardTitle>
</div>
</CardHeader>
<Separator />
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4 rnd-escape'>
<div className='space-y-2'>
<Label htmlFor='resolution'>Resolution</Label>
<Select onValueChange={setResolution} defaultValue={resolution}>
<SelectTrigger id='resolution'>
<SelectValue placeholder='Select resolution' />
</SelectTrigger>
<SelectContent>
{resolutions.map(res => (
<SelectItem key={res.value} value={res.value}>
{res.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className='space-y-2'>
<Label htmlFor='imageType'>Image Type</Label>
<Select onValueChange={setImageType} defaultValue={imageType}>
<SelectTrigger id='imageType'>
<SelectValue placeholder='Select image type' />
</SelectTrigger>
<SelectContent>
{imageTypes.map(type => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<Button onClick={handleDownload} className='w-full rnd-escape'>
Capture Canvas
</Button>
</Card>
</RndComponent>
)}
</>
);
};
export default CanvasCapture;

View file

@ -0,0 +1,409 @@
import { useContext, useEffect, 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 { 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 { 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";
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);
// create empty project if no id is provided
useEffect(() => {
const createEmptyProject = async () => {
try {
const response = await createProject();
if (response?.status === 200) {
toast({
title: response?.status,
description: response?.message
})
}
if (response?.data?.id) {
navigate(`/${response.data.id}`);
}
} catch (error) {
console.error("Project creation failed:", error);
}
};
if (!id) {
createEmptyProject();
}
}, [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);
// 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.setBackgroundImage(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-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 adjustBackgroundOpacity = (value) => {
if (canvas) {
if (canvas.backgroundImage) {
// Update the opacity of the background image if it exists
canvas.backgroundImage.set("opacity", value);
} else if (canvas.overlayImage) {
// Update the opacity of the overlay image if it exists
canvas.overlayImage.set("opacity", value);
} else {
console.error("No background or overlay image found on the canvas.");
return;
}
// Re-render the canvas to apply changes
canvas.renderAll();
} else {
console.error("Canvas is not initialized.");
}
};
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",
})
}
}
};
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",
})
}
}
};
useEffect(() => {
if (canvas) {
const bgUrl = canvas.backgroundImage?.getSrc();
setBgImage(bgUrl);
const overLayUrl = canvas.overlayImage?.getSrc();
setBgOverLayImage(overLayUrl);
}
}, [canvas, setBgImage, setBgOverLayImage])
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
</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">
<div className="rnd-escape">
<ColorComponent />
<Separator className="mt-2" />
<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
className="absolute top-0 opacity-0"
type="file"
accept="image/*"
onChange={setBackgroundImage}
/>
</Button>
}
{
bgImage &&
<Button
variant="secondary"
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">
<UploadIcon className="cursor-pointer" />
<Input
className="absolute top-0 opacity-0"
type="file"
accept="image/*"
onChange={setBackgroundOverlayImage}
/>
</Button>
}
{
bgOverLayImage &&
<Button
variant="secondary"
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">
<Label>Background Opacity:</Label>
<Slider
defaultValue={[1.0]} // Default value, you can set it to 0.0 or another value
min={0.0}
max={1.0}
step={0.01} // Step size for fine control
onValueChange={(value) => {
const newOpacity = value[0]; // Extract slider value
adjustBackgroundOpacity(newOpacity); // Adjust Fabric.js background opacity
}}
/>
</div>
</div>
<Separator className="mt-4" />
{/* Save canvas Component */}
{
id &&
<SaveCanvas />
}
</div>
</ScrollArea>
</Card>
</div>
);
};
export default CanvasSetting;

View file

@ -0,0 +1,244 @@
import { useContext, useEffect, useState } from 'react'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import ColorContext from './Context/colorContext/ColorContext'
import CanvasContext from './Context/canvasContext/CanvasContext'
import { fabric } from 'fabric'
const ColorComponent = () => {
const { canvas } = useContext(CanvasContext)
const { backgroundType, setBackgroundType, solidColor, gradientColors, setSolidColor, setGradientColors, gradientDirection, setGradientDirection, setDirectionCoords,
} = useContext(ColorContext)
const [previewBackgroundType, setPreviewBackgroundType] = useState(backgroundType)
const [previewSolidColor, setPreviewSolidColor] = useState(solidColor)
const [previewGradientColors, setPreviewGradientColors] = useState(gradientColors)
const [previewGradientDirection, setPreviewGradientDirection] = useState(gradientDirection)
useEffect(() => {
setPreviewBackgroundType(backgroundType)
setPreviewSolidColor(solidColor)
setPreviewGradientColors(gradientColors)
setPreviewGradientDirection(gradientDirection)
}, [backgroundType, solidColor, gradientColors, gradientDirection])
useEffect(() => {
if (previewBackgroundType === "gradient" && canvas) {
const width = canvas.width || 0
const height = canvas.height || 0
const linearCoords = {
"top-to-bottom": { x1: 0, y1: 0, x2: 0, y2: height },
"bottom-to-top": { x1: 0, y1: height, x2: 0, y2: 0 },
"left-to-right": { x1: 0, y1: 0, x2: width, y2: 0 },
"right-to-left": { x1: width, y1: 0, x2: 0, y2: 0 },
}
const coords = linearCoords[previewGradientDirection]
if (coords) {
setDirectionCoords(coords)
}
}
}, [previewBackgroundType, previewGradientDirection, canvas, setDirectionCoords])
const applyChanges = () => {
setBackgroundType(previewBackgroundType)
setSolidColor(previewSolidColor)
setGradientColors(previewGradientColors)
setGradientDirection(previewGradientDirection)
applyToCanvas()
}
const applyToCanvas = () => {
if (!canvas) return
if (previewBackgroundType === "color") {
canvas.backgroundColor = previewSolidColor
} else if (previewBackgroundType === "gradient") {
const width = canvas.width || 0
const height = canvas.height || 0
const linearCoords = {
"top-to-bottom": { x1: 0, y1: 0, x2: 0, y2: height },
"bottom-to-top": { x1: 0, y1: height, x2: 0, y2: 0 },
"left-to-right": { x1: 0, y1: 0, x2: width, y2: 0 },
"right-to-left": { x1: width, y1: 0, x2: 0, y2: 0 },
}
const coords = linearCoords[previewGradientDirection]
if (coords) {
setDirectionCoords(coords)
const gradient = new fabric.Gradient({
type: "linear",
gradientUnits: "pixels",
coords: coords,
colorStops: [
{ offset: 0, color: previewGradientColors.color1 },
{ offset: 1, color: previewGradientColors.color2 },
],
})
canvas.backgroundColor = gradient
}
} else if (previewBackgroundType === "radial") {
const gradient = new fabric.Gradient({
type: "radial",
gradientUnits: "pixels",
coords: {
x1: canvas.width / 2,
y1: canvas.height / 2,
r1: 0,
x2: canvas.width / 2,
y2: canvas.height / 2,
r2: Math.min(canvas.width, canvas.height) / 2,
},
colorStops: [
{ offset: 0, color: previewGradientColors.color1 },
{ offset: 1, color: previewGradientColors.color2 },
],
})
canvas.backgroundColor = gradient
}
canvas.renderAll()
}
const getPreviewStyle = () => {
if (previewBackgroundType === "color") {
return { backgroundColor: previewSolidColor }
} else if (previewBackgroundType === "gradient") {
const direction = {
'top-to-bottom': '180deg',
'bottom-to-top': '0deg',
'left-to-right': '90deg',
'right-to-left': '270deg'
}[previewGradientDirection]
return {
backgroundImage: `linear-gradient(${direction}, ${previewGradientColors.color1}, ${previewGradientColors.color2})`
}
} else if (previewBackgroundType === "radial") {
return {
backgroundImage: `radial-gradient(circle, ${previewGradientColors.color1}, ${previewGradientColors.color2})`
}
}
}
return (
<div className='flex flex-col gap-4 p-1'>
<div className='flex flex-col gap-2'>
<Label>Background Type:</Label>
<Select value={previewBackgroundType} onValueChange={setPreviewBackgroundType}>
<SelectTrigger>
<SelectValue placeholder="Select background type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="color">Solid Color</SelectItem>
<SelectItem value="gradient">Linear Gradient</SelectItem>
<SelectItem value="radial">Radial Gradient</SelectItem>
</SelectContent>
</Select>
</div>
{previewBackgroundType === "color" ? (
<div className='flex flex-col gap-2'>
<Label>Solid Color:</Label>
<Input
type="color"
value={previewSolidColor}
onChange={(e) => setPreviewSolidColor(e.target.value)}
/>
</div>
) : previewBackgroundType === "gradient" ? (
<div className='grid grid-cols-1 gap-2'>
<div className='flex flex-col gap-2'>
<Label>Direction:</Label>
<Select
value={previewGradientDirection}
onValueChange={setPreviewGradientDirection}
>
<SelectTrigger>
<SelectValue placeholder="Select direction" />
</SelectTrigger>
<SelectContent>
<SelectItem value="top-to-bottom">Top to Bottom</SelectItem>
<SelectItem value="bottom-to-top">Bottom to Top</SelectItem>
<SelectItem value="left-to-right">Left to Right</SelectItem>
<SelectItem value="right-to-left">Right to Left</SelectItem>
</SelectContent>
</Select>
</div>
<div className='flex flex-col gap-2'>
<Label>Color 1:</Label>
<Input
type="color"
value={previewGradientColors.color1}
onChange={(e) =>
setPreviewGradientColors((prev) => ({
...prev,
color1: e.target.value,
}))}
/>
</div>
<div className='flex flex-col gap-2'>
<Label>Color 2:</Label>
<Input
type="color"
value={previewGradientColors.color2}
onChange={(e) =>
setPreviewGradientColors((prev) => ({
...prev,
color2: e.target.value,
}))}
/>
</div>
</div>
) : (
<div className='grid grid-cols-1 gap-2'>
<div className='flex flex-col gap-2'>
<Label>Color 1:</Label>
<Input
type="color"
value={previewGradientColors.color1}
onChange={(e) =>
setPreviewGradientColors((prev) => ({
...prev,
color1: e.target.value,
}))}
/>
</div>
<div className='flex flex-col gap-2'>
<Label>Color 2:</Label>
<Input
type="color"
value={previewGradientColors.color2}
onChange={(e) =>
setPreviewGradientColors((prev) => ({
...prev,
color2: e.target.value,
}))}
/>
</div>
</div>
)}
<div>
<Label>Preview:</Label>
<div
className='w-full h-24 rounded-md mt-2'
style={getPreviewStyle()}
></div>
</div>
<Button onClick={applyChanges}>
Apply Changes
</Button>
</div>
)
}
export default ColorComponent

View file

@ -0,0 +1,5 @@
import React from "react";
const ActiveObjectContext = React.createContext();
export default ActiveObjectContext;

View file

@ -0,0 +1,14 @@
import { useState } from "react";
import ActiveObjectContext from "./ObjectContext";
const ObjectProvider = ({ children }) => {
const [activeObject, setActiveObject] = useState(null);
return (
<ActiveObjectContext.Provider value={{ activeObject, setActiveObject }}>
{children}
</ActiveObjectContext.Provider>
);
};
export default ObjectProvider;

View file

@ -0,0 +1,5 @@
import react from "react";
const AuthContext = react.createContext();
export default AuthContext;

View file

@ -0,0 +1,14 @@
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;

View file

@ -0,0 +1,5 @@
import React from "react";
const CanvasContext = React.createContext();
export default CanvasContext;

View file

@ -0,0 +1,41 @@
import { useRef, useState } from "react";
import CanvasContext from "./CanvasContext";
const CanvasContextProvider = ({ children }) => {
const canvasRef = useRef(null);
const [canvas, setCanvas] = useState(null);
const [canvasHeight, setCanvasHeight] = useState(0);
const [canvasWidth, setCanvasWidth] = useState(0);
const [screenWidth, setScreenWidth] = useState(0);
const [canvasRatio, setCanvasRatio] = useState("4:3");
const [selectedPanel, setSelectedPanel] = useState("");
const [textColor, setTextColor] = useState(null);
const fabricCanvasRef = useRef(null);
return (
<CanvasContext.Provider
value={{
canvasRef,
canvas,
setCanvas,
fabricCanvasRef,
canvasHeight,
canvasRatio,
setCanvasRatio,
textColor,
setTextColor,
selectedPanel,
setSelectedPanel,
setCanvasHeight,
canvasWidth,
setCanvasWidth,
screenWidth,
setScreenWidth,
}}
>
{children}
</CanvasContext.Provider>
);
};
export default CanvasContextProvider;

View file

@ -0,0 +1,5 @@
import React from "react";
const ColorContext = React.createContext();
export default ColorContext;

View file

@ -0,0 +1,22 @@
import { useState } from 'react'
import ColorContext from './ColorContext'
const ColorContextProvider = ({ children }) => {
const [backgroundType, setBackgroundType] = useState("color"); // 'color' or 'gradient'
const [solidColor, setSolidColor] = useState("#FFA500");
const [gradientColors, setGradientColors] = useState({
color1: "#ffffff",
color2: "#e26286",
});
const [gradientDirection, setGradientDirection] = useState("top-to-bottom");
const [directionCoords, setDirectionCoords] = useState(null);
return (
<ColorContext.Provider value={{ backgroundType, setBackgroundType, solidColor, setSolidColor, gradientColors, setGradientColors, gradientDirection, setGradientDirection, directionCoords, setDirectionCoords }}>
{children}
</ColorContext.Provider>
)
}
export default ColorContextProvider

View file

@ -0,0 +1,5 @@
import React from "react";
const OpenContext = React.createContext();
export default OpenContext;

View file

@ -0,0 +1,19 @@
import { useState } from 'react'
import OpenContext from './OpenContext';
const IconContextProvider = ({ children }) => {
const [openSetting, setOpenSetting] = useState(false);
const [openObjectPanel, setOpenObjectPanel] = useState(false);
const [tabValue, setTabValue] = useState("icons");
const [captureOpen, setCaptureOpen] = useState(false);
const [rightPanelOpen, setRightPanelOpen] = useState(true);
const [leftPanelOpen, setLeftPanelOpen] = useState(true);
const [openPanel, setOpenPanel] = useState(false);
return (
<OpenContext.Provider value={{ openSetting, setOpenSetting, openObjectPanel, setOpenObjectPanel, tabValue, setTabValue, captureOpen, setCaptureOpen, rightPanelOpen, setRightPanelOpen, leftPanelOpen, setLeftPanelOpen, openPanel, setOpenPanel }}>
{children}
</OpenContext.Provider>
)
}
export default IconContextProvider

View file

@ -0,0 +1,358 @@
import { useCallback, useContext, useEffect, useState } from "react";
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { fabric } from "fabric";
import { Separator } from "../ui/separator";
import { Label } from "../ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
import { Input } from "../ui/input";
import { Card } from "../ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import CollapsibleComponent from "./Customization/CollapsibleComponent";
const ApplyColor = () => {
const [colorField, setColorField] = useState("fill");
const [fillColor, setFillColor] = useState("");
const [backgroundColor, setBackgroundColor] = useState("");
const [gradientFillColors, setGradientFillColors] = useState({
color1: "#f97316",
color2: "#e26286",
});
const [colorType, setColorType] = useState("color"); // 'color' or 'gradient'
const [gradientDirection, setGradientDirection] = useState("top-to-bottom");
// Get values from context
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
const { canvas, setTextColor } = useContext(CanvasContext);
// To get previous values from active object
useEffect(() => {
const handleObjectStyle = (object) => {
if (object.fill) {
if (typeof object.fill === "string") {
setColorType("color");
} else if (object.fill instanceof fabric.Gradient) {
setColorType("gradient");
}
}
// Handle solid colors
if (object.fill && !object.fill.colorStops) {
setFillColor(object.fill); // Solid fill
}
if (object.backgroundColor) {
setBackgroundColor(object.backgroundColor); // Solid background color
}
// Handle gradients
if (object.fill?.colorStops) {
setGradientFillColors({
color1: object.fill.colorStops[0]?.color || "",
color2: object.fill.colorStops[1]?.color || "",
});
}
};
const processGroupObjects = (group) => {
group._objects.forEach((obj) => {
if (obj.type === "group") {
processGroupObjects(obj); // Recursively handle nested groups
} else {
handleObjectStyle(obj); // Apply styles to child objects
}
});
};
if (activeObject) {
if (activeObject.type === "group") {
processGroupObjects(activeObject); // Process all objects in the group
} else {
handleObjectStyle(activeObject); // Handle single object
}
}
}, [activeObject, colorField]);
// Apply color/gradient style to the selected object on the canvas
const applyColor = useCallback(() => {
const applyStyleToObject = (object, style) => {
if (colorType === "color") {
if (colorField === "fill" && object.fill !== style.fill) {
object.set("fill", style.fill);
}
if (
colorField === "background" &&
object.backgroundColor !== style.backgroundColor
) {
object.set("backgroundColor", style.backgroundColor);
}
}
if (colorType === "gradient" && colorField === "fill") {
const width = object?.width || 0;
const height = object?.height || 0;
const coords = {
"top-to-bottom": { x1: 0, y1: 0, x2: 0, y2: height },
"bottom-to-top": { x1: 0, y1: height, x2: 0, y2: 0 },
"left-to-right": { x1: 0, y1: 0, x2: width, y2: 0 },
"right-to-left": { x1: width, y1: 0, x2: 0, y2: 0 },
};
const directionCoords = coords[gradientDirection];
const gradient = new fabric.Gradient({
type: "linear",
gradientUnits: "pixels",
coords: directionCoords,
colorStops: [
{ offset: 0, color: gradientFillColors.color1 },
{ offset: 1, color: gradientFillColors.color2 },
],
});
object.set("fill", gradient);
}
};
const applyStyleRecursively = (object, style) => {
if (object.type === "group" && object._objects) {
// If the object is a group, iterate through its children
object._objects.forEach((child) => applyStyleRecursively(child, style));
} else {
// If the object is not a group, apply the style directly
applyStyleToObject(object, style);
}
};
const style = {
fill: fillColor,
backgroundColor: backgroundColor,
};
if (activeObject) {
applyStyleRecursively(activeObject, style);
setActiveObject(activeObject);
setTextColor(style);
canvas.renderAll();
}
}, [
activeObject,
fillColor,
backgroundColor,
gradientFillColors,
colorType,
gradientDirection,
setActiveObject,
canvas,
colorField,
setTextColor,
]);
// Watch for changes in color-related states and apply them instantly
useEffect(() => {
applyColor();
}, [
applyColor, // Now included in dependencies
fillColor,
backgroundColor,
gradientFillColors,
colorType,
gradientDirection,
]);
const handleSolidColorChange = (newColor) => {
if (colorField === "fill") {
setFillColor(newColor);
} else {
setBackgroundColor(newColor);
}
};
const handleGradientColorChange = (key, value) => {
setGradientFillColors((prev) => ({
...prev,
[key]: value,
}));
};
const content = () => {
return (
<div>
<div className="grid">
<div className="space-y-2">
<div className="space-y-2">
<Label htmlFor="colorField">Color Field</Label>
<Select
value={colorField}
onValueChange={(value) => setColorField(value)}
>
<SelectTrigger id="colorField">
<SelectValue placeholder="Select color field type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="fill">Fill</SelectItem>
<SelectItem value="background">Background</SelectItem>
</SelectContent>
</Select>
</div>
<Tabs
value={colorType}
onValueChange={(value) => setColorType(value)}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="color">Solid Color</TabsTrigger>
<TabsTrigger
value="gradient"
disabled={colorField === "background"}
>
Gradient
</TabsTrigger>
</TabsList>
<TabsContent value="color" className="space-y-4">
<div className="space-y-2">
<Label htmlFor="solidColor">Color</Label>
<div className="flex items-center space-x-2">
<Input
id="solidColor"
type="color"
value={
colorField === "fill" ? fillColor : backgroundColor
}
onChange={(e) => handleSolidColorChange(e.target.value)}
className="w-12 h-12 p-1 rounded-md"
/>
<Input
type="text"
value={
colorField === "fill" ? fillColor : backgroundColor
}
onChange={(e) => handleSolidColorChange(e.target.value)}
className="flex-grow"
/>
</div>
</div>
<div
className="h-24 rounded-md"
style={{
backgroundColor:
colorField === "fill" ? fillColor : backgroundColor,
}}
></div>
</TabsContent>
<TabsContent value="gradient" className="space-y-4">
<div className="space-y-2">
<Label htmlFor="gradientDirection">Direction</Label>
<Select
value={gradientDirection}
onValueChange={setGradientDirection}
>
<SelectTrigger id="gradientDirection">
<SelectValue placeholder="Select direction" />
</SelectTrigger>
<SelectContent>
<SelectItem value="top-to-bottom">
Top to Bottom
</SelectItem>
<SelectItem value="bottom-to-top">
Bottom to Top
</SelectItem>
<SelectItem value="left-to-right">
Left to Right
</SelectItem>
<SelectItem value="right-to-left">
Right to Left
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="gradientColor1">Color 1</Label>
<div className="flex items-center space-x-2">
<Input
id="gradientColor1"
type="color"
value={gradientFillColors.color1}
onChange={(e) =>
handleGradientColorChange("color1", e.target.value)
}
className="w-12 h-12 p-1 rounded-md"
/>
<Input
type="text"
value={gradientFillColors.color1}
onChange={(e) =>
handleGradientColorChange("color1", e.target.value)
}
className="flex-grow"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="gradientColor2">Color 2</Label>
<div className="flex items-center space-x-2">
<Input
id="gradientColor2"
type="color"
value={gradientFillColors.color2}
onChange={(e) =>
handleGradientColorChange("color2", e.target.value)
}
className="w-12 h-12 p-1 rounded-md"
/>
<Input
type="text"
value={gradientFillColors.color2}
onChange={(e) =>
handleGradientColorChange("color2", e.target.value)
}
className="flex-grow"
/>
</div>
</div>
<div
className="h-24 rounded-md"
style={{
background: `linear-gradient(${
gradientDirection === "top-to-bottom"
? "to bottom"
: gradientDirection === "bottom-to-top"
? "to top"
: gradientDirection === "left-to-right"
? "to right"
: "to left"
}, ${gradientFillColors.color1}, ${
gradientFillColors.color2
})`,
}}
></div>
</TabsContent>
</Tabs>
</div>
</div>
</div>
);
};
return (
<div>
<Card className="border-0 shadow-none">
<CollapsibleComponent text={"Color Control"}>
{content()}
</CollapsibleComponent>
</Card>
<Separator className="my-2" />
</div>
);
};
export default ApplyColor;

View file

@ -0,0 +1,173 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { useContext, useEffect, useState } from "react";
import { fabric } from "fabric";
import useProject from "@/hooks/useProject";
import UploadShapes from "./UploadShapes";
import { Separator } from "@/components/ui/separator";
import InlineSVG from "react-inlinesvg";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { deleteShape, getAllShape } from "@/api/uploadShapeApi";
import { Loader, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Toaster } from "@/components/ui/toaster";
import { useToast } from "@/hooks/use-toast";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
const CustomShape = () => {
const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const [open, setOpen] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const { projectData, projectUpdate, id } = useProject();
const [currentShapeId, setCurrentShapeId] = useState(null);
const { toast } = useToast();
const queryClient = useQueryClient(); // Initialize query client
const addShape = (each) => {
// Load the SVG from the imported file
fabric.loadSVGFromString(each, (objects, options) => {
const svgGroup = fabric.util.groupSVGElements(objects, options);
// Calculate canvas center
const centerX = canvas.getWidth() / 2;
const centerY = canvas.getHeight() / 2;
// Set properties for centering the SVG
svgGroup.set({
left: centerX, // Center horizontally
top: centerY, // Center vertically
originX: "center", // Set the origin to the center
originY: "center",
fill: "#f09b0a",
scaleX: 1,
scaleY: 1,
strokeWidth: 0,
});
// Add SVG to the canvas
canvas.add(svgGroup);
canvas.setActiveObject(svgGroup);
// Update the active object state
setActiveObject(svgGroup);
// 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 });
});
};
// get all shapes
const { data: allShapes, isLoading: shapeLoading } = useQuery({
queryKey: ['shapes'],
queryFn: async () => {
return await getAllShape();
}
})
// delete shape
const { mutate: deleteMutate, isLoading: deleteLoading } = useMutation({
mutationFn: async (id) => {
return await deleteShape(id);
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: "Shape delete",
description: `${data?.message || "Shape deleted successfully"}`,
})
queryClient.invalidateQueries({ queryKey: ["shapes"] });
setOpen(false);
confirmDelete(false);
setCurrentShapeId(null);
}
else {
toast({
variant: "destructive",
title: "Error!!",
description: `${data?.message || "Shape deleted failed"}`,
})
setOpen(false);
confirmDelete(false);
setCurrentShapeId(null);
}
}
})
const handleDelete = (id) => {
setCurrentShapeId(id);
setOpen(true);
}
// this will finally trigger the delete function if the user confirms the delete action
useEffect(() => {
if (confirmDelete) {
deleteMutate(currentShapeId);
}
else {
setCurrentShapeId(null);
}
}, [confirmDelete])
return (
<div>
{/* for getting confirmation for delete shape */}
{
open &&
<Alert className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white p-4 rounded-md shadow-lg z-[9999] w-fit">
<AlertTitle>Are you absolutely sure?</AlertTitle>
<AlertDescription>
This action cannot be undone. This will permanently delete your shape and remove your data from our servers.
</AlertDescription>
<hr className="mt-2" />
<div className="flex justify-end space-x-2 mt-2">
<Button onClick={() => setConfirmDelete(true)}>Confirm</Button>
<Button onClick={() => { setConfirmDelete(false); setOpen(false); }} variant="outline">Cancel</Button>
</div>
</Alert>
}
<Toaster />
{
shapeLoading ?
<p><Loader className="animate-spin mx-auto" />
</p> :
<div className={`${open ? "opacity-50" : "opacity-100"}`}>
{/* upload shapes */}
<div>
<UploadShapes />
</div>
<Separator className="my-4" />
{/* all custom shapes */}
<div
className="rounded-md grid grid-cols-2 gap-2"
>
{allShapes?.allShapes?.map((each) => {
return <div className="border border-gray-300 p-2 rounded-md bg-red-100 flex flex-col" key={each?.id}>
<InlineSVG loader="loading..." src={each?.shapes}
onClick={() => addShape(each?.shapes)}
className="h-[100px] w-[100px] cursor-pointer mx-auto" />
<hr className="my-2" />
<Button size="sm" disable={deleteLoading} className="mx-auto w-fit" onClick={() => handleDelete(each?.id)}><Trash2 /></Button>
</div>
})}
</div>
</div>
}
</div>
);
};
export default CustomShape;

View file

@ -0,0 +1,153 @@
import { Upload, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { useEffect, useRef, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { uploadShape } from "@/api/uploadShapeApi";
import { Toaster } from "@/components/ui/toaster";
import { useToast } from "@/hooks/use-toast";
const UploadShapes = () => {
const [preview, setPreview] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const inputRef = useRef(null);
const { toast } = useToast();
const queryClient = useQueryClient(); // Initialize query client
const { mutate: uploadMutate, isPending } = useMutation({
mutationFn: async (shape) => {
return await uploadShape(shape);
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: "Success",
description: `${data?.message || "Shape uploaded successfully"} `,
})
queryClient.invalidateQueries({ queryKey: ["shapes"] });
clearFile();
}
else {
toast({
title: "Error",
description: `${data?.message || "Error uploading shape"} `,
variant: "destructive",
})
}
}
});
const validateSVG = (file) => {
if (!file.type.includes("svg")) {
throw new Error("Please upload an SVG file")
}
};
const handleFileChange = (e) => {
try {
setError(null)
setIsLoading(true)
const file = e.target.files?.[0]
if (!file) return
validateSVG(file)
// Create preview URL
const url = URL.createObjectURL(file)
setPreview(url)
const reader = new FileReader();
reader.onload = (e) => {
const svgText = e.target.result;
uploadMutate(svgText);
};
reader.readAsText(file); // Read the SVG as text
} catch (err) {
setError(err instanceof Error ? err.message : "Error uploading file")
// Reset input
if (inputRef.current) {
inputRef.current.value = ""
}
} finally {
setIsLoading(false)
}
}
const clearFile = () => {
if (preview) {
URL.revokeObjectURL(preview)
}
setPreview(null)
setError(null)
if (inputRef.current) {
inputRef.current.value = ""
}
}
// Cleanup preview URL on unmount
useEffect(() => {
return () => {
if (preview) {
URL.revokeObjectURL(preview)
}
}
}, [preview])
return (
<Card className="w-full max-w-md">
<Toaster />
<CardHeader>
<CardTitle>Upload Shapes</CardTitle>
<CardDescription>Choose an SVG file to upload. Only SVG files are accepted.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="space-y-0">
<Label htmlFor="svg-upload">Choose file</Label>
<div className="flex gap-2">
<Input
ref={inputRef}
id="svg-upload"
type="file"
accept=".svg,image/svg+xml"
onChange={handleFileChange}
disabled={isLoading || isPending}
className="cursor-pointer"
/>
{preview && (
<Button variant="outline" size="icon" onClick={clearFile} type="button">
<X className="h-4 w-4" />
<span className="sr-only">Clear file</span>
</Button>
)}
</div>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
{isLoading && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Upload className="h-4 w-4 animate-pulse" />
Uploading...
</div>
)}
{preview && (
<div className="relative aspect-square w-full max-w-[200px] overflow-hidden rounded-lg border bg-background">
<img src={preview || "/placeholder.svg"} alt="SVG preview" className="h-full w-full object-contain p-2" />
</div>
)}
</div>
</CardContent>
</Card>
)
}
export default UploadShapes;

View file

@ -0,0 +1,424 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { useContext, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { fabric } from "fabric";
import Resizer from "react-image-file-resizer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Slider } from "@/components/ui/slider";
import {
HardDriveUpload,
ImagePlus,
Upload,
Crop,
RotateCw,
FileType,
ChevronUp,
ChevronDown,
} from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import CollapsibleComponent from "./CollapsibleComponent";
import {
Collapsible,
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 = [
{
icon: Upload,
title: "Auto-Resize",
description: "Images larger than 1080px are automatically resized",
},
{
icon: Crop,
title: "Custom Dimensions",
description: "Set your preferred resize dimensions",
},
{
icon: RotateCw,
title: "Quality & Rotation",
description: "Adjust image quality and rotation as needed",
},
{
icon: FileType,
title: "Format Conversion",
description: "Convert between various image formats",
},
];
const AddImageIntoShape = () => {
const { canvas } = useContext(CanvasContext);
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
const [errorMessage, setErrorMessage] = useState("");
const [width, setWidth] = useState(1080);
const [height, setHeight] = useState(1080);
const [quality, setQuality] = useState(100);
const [rotation, setRotation] = useState("0");
const [format, setFormat] = useState("JPEG");
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,
width,
height,
format,
quality,
parseInt(rotation),
(resizedFile) => {
callback(resizedFile); // Pass the resized file to the callback
},
"file" // Output type
);
};
const fileHandler = (e) => {
if (!activeObject) {
toast({ variant: "destructive", title: "No active object selected!" });
return;
}
const file = e.target.files[0];
if (!file && activeObject) {
setErrorMessage("No file selected.");
return;
}
// Check if the file is an SVG (skip compression)
if (file.type === "image/svg+xml") {
toast({ variant: "destructive", title: "SVG files are not supported!" });
clearFileInput();
return;
}
// Handle raster images (JPEG, PNG, etc.)
const imgElement = new Image();
const blobUrl = URL.createObjectURL(file);
imgElement.src = blobUrl;
imgElement.onload = () => {
if (imgElement.width > 1080) {
handleResize(file, (compressedFile) => {
uploadMutate({ file: compressedFile, id }); // Fixed key name
clearFileInput();
});
} else {
uploadMutate({ file, id }); // Direct upload if width is small
clearFileInput();
}
URL.revokeObjectURL(blobUrl); // Clean up
};
imgElement.onerror = () => {
console.error("Failed to load image.");
setErrorMessage("Failed to load image.");
URL.revokeObjectURL(blobUrl);
clearFileInput();
};
};
const clearFileInput = () => {
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
// const handleImageInsert = (img) => {
// if (!activeObject) {
// setErrorMessage("No active object selected!");
// return;
// }
// // Ensure absolute positioning for the clipPath
// activeObject.set({
// isClipPath: true, // Custom property
// absolutePositioned: true,
// });
// // Calculate scale factors based on clip object size
// let scaleX = activeObject.width / img.width;
// let scaleY = activeObject.height / img.height;
// if (activeObject?.width < 100) {
// scaleX = 0.2;
// }
// if (activeObject.height < 100) {
// scaleY = 0.2;
// }
// // Create a fabric image object with scaling and clipPath
// const fabricImage = new fabric.Image.fromURL(img, {
// scaleX: scaleX,
// scaleY: scaleY,
// left: activeObject.left,
// top: activeObject.top,
// clipPath: activeObject, // Apply clipPath to the image
// originX: activeObject.originX, // Match origin point
// originY: activeObject.originY, // Match origin point
// }, { crossOrigin: "anonymous" });
// // Adjust position based on the clipPath's transformations
// fabricImage.set({
// left: activeObject.left,
// top: activeObject.top,
// angle: activeObject.angle, // Match rotation if any
// });
// canvas.add(fabricImage);
// canvas.setActiveObject(fabricImage);
// setActiveObject(fabricImage);
// canvas.renderAll();
// // Update the active object state
// projectUpdate({ id, updateData: { ...projectData?.data, object: canvas.toJSON(['id', 'selectable']), preview_url: "" } });
// };
const handleImageInsert = (imgUrl) => {
console.log(imgUrl);
// Ensure absolute positioning for the clipPath
activeObject.set({
isClipPath: true, // Custom property
absolutePositioned: true,
});
// Load the image asynchronously
fabric.Image.fromURL(
imgUrl,
(img) => {
// Ensure the image is fully loaded before applying transformations
let scaleX = activeObject.width / img.width;
let scaleY = activeObject.height / img.height;
// Prevent the image from being too small
if (activeObject.width < 100) scaleX = 0.2;
if (activeObject.height < 100) scaleY = 0.2;
// Set image properties
img.set({
scaleX: scaleX,
scaleY: scaleY,
left: activeObject.left,
top: activeObject.top,
clipPath: activeObject, // Apply clipPath to the image
originX: activeObject.originX, // Match origin point
originY: activeObject.originY, // Match origin point
crossOrigin: "anonymous", // Ensure CORS handling
});
// Add image to canvas
canvas.add(img);
canvas.setActiveObject(img);
setActiveObject(img);
canvas.renderAll();
// Update project data
projectUpdate({
id,
updateData: {
...projectData?.data,
object: canvas.toJSON(["id", "selectable"]),
preview_url: "",
},
});
},
{ crossOrigin: "anonymous" }
);
};
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>
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<Button
variant="outline"
className="w-full justify-between mt-2"
>
<span>Key Features</span>
{isOpen ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<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>
</CollapsibleContent>
</Collapsible>
</AlertDescription>
</Alert>
</CardContent>
</Card>
{errorMessage && (
<p className="text-red-600 text-sm mt-2">{errorMessage}</p>
)}
<div className="flex flex-col w-[100%]">
<div className="space-y-1">
{/* Width Slider */}
<div className="space-y-1">
<Label className="text-xs">Width: {width}px</Label>
<Slider
min={300}
value={[width]}
max={2000}
step={10}
onValueChange={(value) => setWidth(value[0])}
/>
</div>
{/* Height Slider */}
<div className="space-y-1">
<Label className="text-xs">Height: {height}px</Label>
<Slider
min={300}
value={[height]}
max={2000}
step={10}
onValueChange={(value) => setHeight(value[0])}
/>
</div>
{/* Quality Slider */}
{format === "JPEG" && (
<div className="space-y-1">
<Label className="text-xs">Quality: {quality}%</Label>
<Slider
value={[quality]}
max={100}
step={1}
onValueChange={(value) => setQuality(value[0])}
/>
</div>
)}
<div className="grid grid-cols-2 gap-1 items-center">
{/* Rotation */}
<div>
<Label className="text-xs">Rotation: {rotation}°</Label>
<Select
value={rotation}
onValueChange={(value) => setRotation(value)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select rotation in degree" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">0</SelectItem>
<SelectItem value="45">45</SelectItem>
<SelectItem value="90">90</SelectItem>
<SelectItem value="180">180</SelectItem>
<SelectItem value="270">270</SelectItem>
</SelectContent>
</Select>
</div>
{/* Format Dropdown */}
<div>
<Label className="text-xs">Format</Label>
<Select
value={format}
onValueChange={(value) => setFormat(value)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="JPEG">JPEG</SelectItem>
<SelectItem value="PNG">PNG</SelectItem>
<SelectItem value="WEBP">WEBP</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<Button className="w-fit h-fit flex flex-wrap gap-1 relative py-4 my-4 mx-auto">
<HardDriveUpload className="cursor-pointer" />
Upload Image
<Input
className="cursor-pointer bg-white text-black absolute top-0 opacity-0"
type="file"
accept="image/*"
ref={fileInputRef}
onChange={fileHandler}
/>
</Button>
</div>
</div>
);
};
return (
<Card className="flex flex-col shadow-none border-0">
<CollapsibleComponent text={"Insert Image"}>
{content()}
</CollapsibleComponent>
</Card>
);
};
export default AddImageIntoShape;

View file

@ -0,0 +1,36 @@
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { useEffect, useState } from "react";
const CollapsibleComponent = ({ children, text }) => {
const [isOpen, setIsOpen] = useState(null);
useEffect(() => {
// Check if the text prop is "Canvas Setting" and set isOpen to false
if (text === "Canvas Setting") {
setIsOpen(false);
} else {
setIsOpen(true);
}
}, [text]);
return (
<Collapsible open={isOpen}>
<CollapsibleTrigger asChild>
<div
className={`flex items-center justify-between cursor-pointer ${
!isOpen ? "my-2" : ""
}`}
>
<h2 className="font-bold mb-2">{text}</h2>
</div>
</CollapsibleTrigger>
<CollapsibleContent>{children}</CollapsibleContent>
</Collapsible>
);
};
export default CollapsibleComponent;

View file

@ -0,0 +1,74 @@
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { FlipHorizontal, FlipVertical } from 'lucide-react';
import { useContext, useEffect, useState } from 'react'
const FlipCustomization = () => {
const { canvas } = useContext(CanvasContext);
const { activeObject } = useContext(ActiveObjectContext);
const [isFlippedHorizontal, setIsFlippedHorizontal] = useState(false);
const [isFlippedVertical, setIsFlippedVertical] = useState(false);
useEffect(() => {
if (activeObject) {
setIsFlippedHorizontal(activeObject.flipX || false);
setIsFlippedVertical(activeObject.flipY || false);
}
}, [activeObject])
useEffect(() => {
if (activeObject) {
activeObject.set({
flipX: isFlippedHorizontal,
flipY: isFlippedVertical,
});
canvas.renderAll();
}
}, [isFlippedHorizontal, isFlippedVertical, activeObject, canvas]);
const handleFlip = (direction) => {
if (direction === 'horizontal') {
setIsFlippedHorizontal(!isFlippedHorizontal);
} else {
setIsFlippedVertical(!isFlippedVertical);
}
}
return (
<div>
<Separator className="my-2" />
<div className="space-y-1">
<h2 className='font-bold'>Flip:</h2>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-1">
<FlipHorizontal className="h-4 w-4" />
<Label htmlFor="flip-horizontal">Horizontal</Label>
</div>
<Switch
id="flip-horizontal"
checked={isFlippedHorizontal}
onCheckedChange={() => handleFlip('horizontal')}
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-1">
<FlipVertical className="h-4 w-4" />
<Label htmlFor="flip-vertical">Vertical</Label>
</div>
<Switch
id="flip-vertical"
checked={isFlippedVertical}
onCheckedChange={() => handleFlip('vertical')}
/>
</div>
</div>
<Separator className="my-2" />
</div>
)
}
export default FlipCustomization

View file

@ -0,0 +1,226 @@
import { useContext, useState } from 'react'
import { ImageIcon, Wand2 } from 'lucide-react'
import { fabric } from 'fabric'
import CanvasContext from '@/components/Context/canvasContext/CanvasContext'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider'
import { Separator } from '@/components/ui/separator'
const ImageCustomization = () => {
const { canvas } = useContext(CanvasContext)
const [shadowColor, setShadowColor] = useState("#000000")
const [highlightColor, setHighlightColor] = useState("#ffffff")
const [blendMode, setBlendMode] = useState("multiply")
const [filterType, setFilterType] = useState("sepia")
const [filterIntensity, setFilterIntensity] = useState(0.5)
const applyDuotoneFilter = () => {
if (!canvas) return
const activeObject = canvas.getActiveObject()
if (!activeObject || activeObject.type !== "image") {
console.warn("No active image object selected.")
return
}
activeObject.filters = [
new fabric.Image.filters.BlendColor({
color: shadowColor,
mode: blendMode,
}),
new fabric.Image.filters.BlendColor({
color: highlightColor,
mode: "screen",
}),
]
activeObject.applyFilters()
canvas.renderAll()
}
const createFilter = (value) => {
let effect
switch (value) {
case 'sepia':
effect = new fabric.Image.filters.Sepia()
break
case 'contrast':
effect = new fabric.Image.filters.Contrast({ contrast: filterIntensity })
break
case 'brightness':
effect = new fabric.Image.filters.Brightness({ brightness: filterIntensity })
break
case 'grayscale':
effect = new fabric.Image.filters.Grayscale()
break
case 'invert':
effect = new fabric.Image.filters.Invert()
break
case 'blur':
effect = new fabric.Image.filters.Blur({
blur: filterIntensity,
})
break
case 'pixelate':
effect = new fabric.Image.filters.Pixelate({
blocksize: filterIntensity * 10,
})
break
case 'huerotation':
effect = new fabric.Image.filters.HueRotation({
rotation: filterIntensity * 360,
})
break
case 'noise':
effect = new fabric.Image.filters.Noise({
noise: filterIntensity * 100,
})
break
default:
effect = null
}
return effect
}
const applyFilter = () => {
if (!canvas) return
const activeObject = canvas.getActiveObject()
if (!activeObject || activeObject.type !== "image") {
console.warn("No active image object selected.")
return
}
const filter = createFilter(filterType)
if (filter) {
activeObject.filters = [filter]
activeObject.applyFilters()
canvas.renderAll()
}
}
const revertFilters = () => {
if (!canvas) return
const image = canvas.getActiveObject()
if (image) {
image.filters = []
image.applyFilters()
canvas.renderAll()
}
}
return (
<div className="space-y-2">
<h2 className="font-semibold my-2">Blend filter</h2>
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label>Shadow Color</Label>
<div className="flex items-center space-x-2">
<Input
type="color"
value={shadowColor}
onChange={(e) => setShadowColor(e.target.value)}
className="w-12 h-12 p-1 rounded-md"
/>
<Input
type="text"
value={shadowColor}
onChange={(e) => setShadowColor(e.target.value)}
className="flex-grow"
/>
</div>
</div>
<div className="space-y-2">
<Label>Highlight Color</Label>
<div className="flex items-center space-x-2">
<Input
type="color"
value={highlightColor}
onChange={(e) => setHighlightColor(e.target.value)}
className="w-12 h-12 p-1 rounded-md"
/>
<Input
type="text"
value={highlightColor}
onChange={(e) => setHighlightColor(e.target.value)}
className="flex-grow"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label>Blend Mode</Label>
<Select value={blendMode} onValueChange={setBlendMode}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="multiply">Multiply</SelectItem>
<SelectItem value="screen">Screen</SelectItem>
<SelectItem value="overlay">Overlay</SelectItem>
<SelectItem value="darken">Darken</SelectItem>
<SelectItem value="lighten">Lighten</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Button onClick={applyDuotoneFilter} className="flex-1">
<Wand2 className="mr-2" />
Apply Duotone
</Button>
<Button variant="outline" onClick={revertFilters} className="flex-1">
<ImageIcon className="mr-2" />
Revert Filters
</Button>
</div>
<div>
<Separator className="mt-4" />
<h2 className="font-semibold my-2">Apply filter</h2>
<div className='grid gap-4'>
<div className="space-y-2">
<Label>Filter Type</Label>
<Select value={filterType} onValueChange={setFilterType}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sepia">Sepia</SelectItem>
<SelectItem value="contrast">Contrast</SelectItem>
<SelectItem value="brightness">Brightness</SelectItem>
<SelectItem value="grayscale">Grayscale</SelectItem>
<SelectItem value="invert">Invert</SelectItem>
<SelectItem value="blur">Blur</SelectItem>
<SelectItem value="pixelate">Pixelate</SelectItem>
<SelectItem value="huerotation">Hue Rotation</SelectItem>
<SelectItem value="noise">Noise</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Filter Intensity</Label>
<Slider
min={0}
max={1}
step={0.01}
value={[filterIntensity]}
onValueChange={([value]) => setFilterIntensity(value)}
/>
</div>
<div className="grid gap-2">
<Button onClick={applyFilter} className="flex-1">
<Wand2 className="mr-2" />
Apply Filter
</Button>
<Button variant="outline" onClick={revertFilters} className="flex-1">
<ImageIcon className="mr-2" />
Revert Filters
</Button>
</div>
</div>
</div>
</div>
)
}
export default ImageCustomization

View file

@ -0,0 +1,75 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
import { useContext, useEffect, useState } from "react";
import { Lock, Unlock } from "lucide-react";
import { Tooltip } from "react-tooltip";
const LockObject = () => {
const { canvas } = useContext(CanvasContext);
const { activeObject } = useContext(ActiveObjectContext);
const [isLocked, setIsLocked] = useState(false);
const { toast } = useToast();
useEffect(() => {
if (activeObject?.lockMovementX === false) {
setIsLocked(false);
} else {
setIsLocked(true);
}
}, [activeObject]);
const toggleLock = () => {
if (!canvas || !activeObject) {
toast({
title: "No object selected",
description: "Please select an object to lock or unlock.",
variant: "destructive",
});
return;
}
const newLockState = !activeObject.lockMovementX;
activeObject.set({
lockMovementX: newLockState,
lockMovementY: newLockState,
lockRotation: newLockState,
lockScalingX: newLockState,
lockScalingY: newLockState,
});
setIsLocked(newLockState);
canvas.requestRenderAll();
toast({
title: newLockState ? "Object locked" : "Object unlocked",
description: newLockState
? "The object is now locked in place."
: "The object can now be moved and resized.",
});
};
return (
<div className="shadow-none border-0">
<a data-tooltip-id="lock">
<Button
onClick={toggleLock}
variant="outline"
size="icon"
disabled={!activeObject}
title={isLocked ? "Unlock object" : "Lock object"}
>
{isLocked ? (
<Unlock className="h-4 w-4" />
) : (
<Lock className="h-4 w-4" />
)}
</Button>
</a>
<Tooltip id="lock" content="Lock object" place="bottom" />
</div>
);
};
export default LockObject;

View file

@ -0,0 +1,67 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useContext, useEffect, useState } from "react";
import { BsTransparency } from "react-icons/bs";
import { Button } from "@/components/ui/button";
import { Tooltip } from "react-tooltip";
const OpacityCustomization = () => {
const { activeObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
const [opacity, setOpacity] = useState(0);
useEffect(() => {
if (activeObject) {
setOpacity(activeObject?.opacity);
}
}, [activeObject]);
const adjustBackgroundOpacity = (newOpacity) => {
setOpacity(newOpacity);
if (activeObject) {
activeObject.set("opacity", newOpacity);
canvas.renderAll();
}
};
return (
<div>
<Popover>
<PopoverTrigger asChild>
<a data-tooltip-id="opacity-ic">
<Button variant="ghost" size="icon" className="h-8 w-8">
<BsTransparency className="h-4 w-4" size={20} />
</Button>
</a>
</PopoverTrigger>
<PopoverContent className="w-64 mt-3">
<div className="grid gap-4">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">Opacity</Label>
<span className="text-sm text-gray-500">
{Math.round(opacity * 100)}%
</span>
</div>
<Slider
value={[opacity]}
min={0}
max={1}
step={0.01}
onValueChange={(value) => adjustBackgroundOpacity(value[0])}
/>
</div>
</PopoverContent>
</Popover>
<Tooltip id="opacity-ic" content="Transparency" place="bottom" />
</div>
);
};
export default OpacityCustomization;

View file

@ -0,0 +1,158 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { useContext, useEffect, useState } from "react";
import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import CollapsibleComponent from "./CollapsibleComponent";
const PositionCustomization = () => {
const { canvas } = useContext(CanvasContext);
const { activeObject } = useContext(ActiveObjectContext);
const [objectPosition, setObjectPosition] = useState({ left: 50, top: 50 });
useEffect(() => {
if (activeObject) {
setObjectPosition({
left: Math.round(activeObject.left || 0),
top: Math.round(activeObject.top || 0),
});
}
}, [activeObject]);
const updateObjectPosition = (key, value) => {
const updatedPosition = { ...objectPosition, [key]: value };
setObjectPosition(updatedPosition);
if (canvas && activeObject) {
activeObject.set(updatedPosition);
canvas.renderAll();
}
};
const handleInputChange = (key, value) => {
const numValue = parseInt(value, 10);
if (!isNaN(numValue)) {
updateObjectPosition(key, numValue);
}
};
const handleSliderChange = (key, value) => {
updateObjectPosition(key, value[0]);
};
const adjustPosition = (key, delta) => {
const newValue = objectPosition[key] + delta;
updateObjectPosition(key, newValue);
};
const content = () => {
return (
<CardContent className="space-y-6 p-0">
<div className="space-y-4">
<div className="space-y-1">
<Label htmlFor="position-left">Left</Label>
<div className="flex items-center space-x-2">
<Input
id="position-left"
type="number"
value={objectPosition.left}
onChange={(e) => handleInputChange("left", e.target.value)}
className="w-20"
/>
<Slider
min={0}
max={canvas ? canvas.width : 1000}
step={1}
value={[objectPosition.left]}
onValueChange={(value) => handleSliderChange("left", value)}
className="flex-grow"
/>
</div>
</div>
<div className="space-y-1">
<Label htmlFor="position-top">Top</Label>
<div className="flex items-center space-x-2">
<Input
id="position-top"
type="number"
value={objectPosition.top}
onChange={(e) => handleInputChange("top", e.target.value)}
className="w-20"
/>
<Slider
min={0}
max={canvas ? canvas.height : 1000}
step={1}
value={[objectPosition.top]}
onValueChange={(value) => handleSliderChange("top", value)}
className="flex-grow"
/>
</div>
</div>
</div>
<div className="w-32 h-32 grid grid-cols-3 gap-1 p-2 mx-auto">
<div className="col-start-2">
<Button
onClick={() => adjustPosition("top", -1)}
aria-label="Move up"
variant="outline"
size="icon"
className="w-full h-full"
>
<ArrowUp className="w-4 h-4" />
</Button>
</div>
<div className="col-start-1 row-start-2">
<Button
onClick={() => adjustPosition("left", -1)}
aria-label="Move left"
variant="outline"
size="icon"
className="w-full h-full"
>
<ArrowLeft className="w-4 h-4" />
</Button>
</div>
<div className="col-start-3 row-start-2">
<Button
onClick={() => adjustPosition("left", 1)}
aria-label="Move right"
variant="outline"
size="icon"
className="w-full h-full"
>
<ArrowRight className="w-4 h-4" />
</Button>
</div>
<div className="col-start-2 row-start-3">
<Button
onClick={() => adjustPosition("top", 1)}
aria-label="Move down"
variant="outline"
size="icon"
className="w-full h-full"
>
<ArrowDown className="w-4 h-4" />
</Button>
</div>
</div>
</CardContent>
);
};
return (
<Card className="shadow-none border-0">
<CollapsibleComponent text={"Position Control"}>
{content()}
</CollapsibleComponent>
</Card>
);
};
export default PositionCustomization;

View file

@ -0,0 +1,44 @@
import ActiveObjectContext from '@/components/Context/activeObject/ObjectContext';
import CanvasContext from '@/components/Context/canvasContext/CanvasContext';
import { Label } from '@/components/ui/label';
import { Slider } from '@/components/ui/slider';
import { useContext, useState, useEffect } from 'react'
const RotateCustomization = () => {
const { canvas } = useContext(CanvasContext);
const { activeObject } = useContext(ActiveObjectContext);
const [rotationAngle, setRotationAngle] = useState(0);
useEffect(() => {
if (activeObject) {
setRotationAngle(activeObject?.angle)
}
}, [activeObject])
const handleRotation = (e) => {
activeObject.set({ angle: e });
setRotationAngle(e);
canvas.remove(activeObject);
canvas.add(activeObject);
canvas.setActiveObject(activeObject);
canvas.renderAll();
}
return (
<div className="grid gap-2 pb-1">
<Label className="">Rotation Angle: {rotationAngle}°</Label>
<Slider
min={0}
max={360}
step={1}
value={[rotationAngle]}
onValueChange={(value) =>
handleRotation(value[0])
}
/>
</div>
);
};
export default RotateCustomization

View file

@ -0,0 +1,94 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { useContext, useEffect, useState } from "react";
import CollapsibleComponent from "./CollapsibleComponent";
const ScaleObjects = () => {
const { canvas } = useContext(CanvasContext); // Access Fabric.js canvas from context
const { activeObject } = useContext(ActiveObjectContext);
const [scaleX, setScaleX] = useState(1);
const [scaleY, setScaleY] = useState(1);
useEffect(() => {
if (activeObject) {
setScaleX(activeObject?.scaleX);
setScaleY(activeObject?.scaleY);
}
}, [activeObject]);
// Handle scaleX changes
const handleScaleXChange = (value) => {
const newScaleX = Math.max(0.001, Math.min(value)); // Clamp scaleX between 0.001 and 3
setScaleX(newScaleX);
if (canvas && activeObject) {
activeObject.scaleX = newScaleX;
canvas.discardActiveObject();
canvas.setActiveObject(activeObject);
canvas.renderAll(); // Re-render the canvas
}
};
// Handle scaleY changes
const handleScaleYChange = (value) => {
const newScaleY = Math.max(0.001, Math.min(value)); // Clamp scaleY between 0.001 and 3
setScaleY(newScaleY);
if (canvas && activeObject) {
activeObject.scaleY = newScaleY;
canvas.discardActiveObject();
canvas.setActiveObject(activeObject);
canvas.renderAll(); // Re-render the canvas
}
};
const content = () => {
return (
<div className="grid grid-cols-2 items-center gap-1">
{/* Scale X Input */}
<div className="flex flex-col space-y-1">
<label className="text-xs font-medium">X: {scaleX.toFixed(3)}</label>
<Input
type="number"
step="0.010"
min="0.001"
value={scaleX}
onChange={(e) => {
const inputValue = parseFloat(e.target.value);
if (!isNaN(inputValue)) {
handleScaleXChange(inputValue);
}
}}
/>
</div>
{/* Scale Y Input */}
<div className="flex flex-col space-y-1">
<label className="text-xs font-medium">Y: {scaleY.toFixed(3)}</label>
<Input
type="number"
step="0.010"
min="0.001"
value={scaleY}
onChange={(e) => {
const inputValue = parseFloat(e.target.value);
if (!isNaN(inputValue)) {
handleScaleYChange(inputValue);
}
}}
/>
</div>
</div>
);
};
return (
<Card className="grid items-center gap-2 shadow-none border-0">
<CollapsibleComponent text={"Scale Control"}>
{content()}
</CollapsibleComponent>
</Card>
);
};
export default ScaleObjects;

View file

@ -0,0 +1,159 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useContext, useEffect, useRef, useState } from "react";
import { Card } from "@/components/ui/card";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Separator } from "@/components/ui/separator";
import { fabric } from "fabric";
const SelectObjectFromGroup = () => {
const [groupObjects, setGroupObjects] = useState([]);
const [selectedObject, setSelectedObject] = useState(null);
const { setActiveObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
const previewCanvasRef = useRef(null);
const activeObject = canvas?.getActiveObject();
useEffect(() => {
if (activeObject?.type === "group") {
setGroupObjects(activeObject._objects || []);
setSelectedObject(null);
} else {
setGroupObjects([]);
setSelectedObject(null);
}
}, [activeObject]);
// useEffect(() => {
// if (selectedObject && previewCanvasRef.current) {
// const previewCanvas = new fabric.StaticCanvas(previewCanvasRef.current);
// previewCanvas.setDimensions({
// width: 100,
// height: 100
// })
// // Clone the active object to create a true deep copy
// selectedObject.clone((clonedObject) => {
// if (selectedObject?.width > 100 || selectedObject?.height > 100) {
// clonedObject.set({
// scaleX: 0.2,
// scaleY: 0.2,
// })
// }
// // Add the cloned object to the canvas
// clonedObject.set({
// left: 50,
// top: 50,
// originX: 'center',
// originY: 'center',
// selectable: false,
// evented: false,
// })
// previewCanvas.add(clonedObject);
// previewCanvas.renderAll();
// });
// return () => {
// previewCanvas.dispose()
// }
// }
// }, [selectedObject])
useEffect(() => {
if (selectedObject && previewCanvasRef.current) {
// Create a new static canvas
const previewCanvas = new fabric.StaticCanvas(previewCanvasRef.current);
previewCanvas.setDimensions({
width: 100,
height: 100,
});
// Clear previous objects (if any)
previewCanvas.clear();
// Clone the active object for preview
selectedObject.clone((clonedObject) => {
const scaledWidth = selectedObject.width * selectedObject.scaleX;
const scaledHeight = selectedObject.height * selectedObject.scaleY;
if (scaledWidth > 100 || scaledHeight > 100) {
clonedObject.scaleToWidth(100); // Scale to fit width
clonedObject.scaleToHeight(100); // Scale to fit height
}
clonedObject.set({
left: 50,
top: 50,
originX: "center",
originY: "center",
});
// Add the cloned object to preview canvas
previewCanvas.add(clonedObject);
previewCanvas.renderAll();
});
// Cleanup function to dispose of the canvas
return () => {
previewCanvas.clear(); // Clear all objects
previewCanvas.dispose(); // Properly dispose the canvas
};
}
}, [selectedObject, previewCanvasRef]);
const handleSelectObject = (value) => {
const selected = groupObjects[parseInt(value)];
setSelectedObject(selected);
setActiveObject(selected);
};
if (!activeObject || activeObject.type !== "group") {
return null;
}
return (
<div>
<Card className="p-4 shadow-none border-0">
<h2 className="font-bold mb-4">Group Objects</h2>
<div className="flex flex-col items-center justify-center space-y-4">
<Select
onValueChange={handleSelectObject}
value={
selectedObject
? groupObjects.indexOf(selectedObject).toString()
: undefined
}
>
<SelectTrigger className="w-full max-w-xs">
<SelectValue placeholder="Select object" />
</SelectTrigger>
<SelectContent className="max-h-[250px]">
{groupObjects.map((object, index) => (
<SelectItem key={index} value={index.toString()}>
{object.name || `Object ${index + 1}`}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedObject && (
<div className="w-[100px] h-[100px] bg-muted rounded-md overflow-hidden border border-gray-300">
<canvas ref={previewCanvasRef} width={100} height={100} />
</div>
)}
</div>
</Card>
<Separator className="my-4" />
</div>
);
};
export default SelectObjectFromGroup;

View file

@ -0,0 +1,151 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { useContext, useEffect, useState } from "react";
import CollapsibleComponent from "./CollapsibleComponent";
const ShadowCustomization = () => {
// get values from context
const { activeObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
// state to handle shadow inputs
const [shadowColor, setShadowColor] = useState("");
const [offsetX, setOffsetX] = useState(5);
const [offsetY, setOffsetY] = useState(5);
const [blur, setBlur] = useState(0.5);
const [opacity, setOpacity] = useState(0.5);
useEffect(() => {
if (activeObject) {
setShadowColor(activeObject?.shadow?.color || "#db1d62");
setOffsetX(activeObject?.shadow?.offsetX || 5);
setOffsetY(activeObject?.shadow?.offsetY || 5);
setBlur(activeObject?.shadow?.blur || 0.5);
setOpacity(activeObject?.shadow?.opacity || 0.5);
}
}, [activeObject]);
const handleApplyShadow = () => {
if (activeObject && canvas) {
const shadow = {
color: shadowColor,
blur: blur,
offsetX: offsetX,
offsetY: offsetY,
opacity: opacity,
};
activeObject.set("shadow", shadow);
// Ensure object updates and renders
activeObject.dirty = true; // Mark the object as dirty for re-render
canvas.renderAll(); // Trigger canvas re-render
}
};
const handleDisableShadow = () => {
if (activeObject && canvas) {
activeObject.set("shadow", null);
canvas.renderAll();
}
};
const content = () => {
return (
<div>
<div className="space-y-2">
<div>
<Label htmlFor="shadowColor">Shadow Color</Label>
<div className="flex items-center space-x-2">
<Input
id="shadowColor"
type="color"
value={shadowColor}
onChange={(e) => setShadowColor(e.target.value)}
className="w-16 h-10"
/>
<span>{shadowColor}</span>
</div>
</div>
<div>
<Label>Offset X: {offsetX}px</Label>
<Slider
value={[offsetX]}
onValueChange={(value) => setOffsetX(value[0])}
max={50}
min={-50}
step={1}
/>
</div>
<div>
<Label>Offset Y: {offsetY}px</Label>
<Slider
value={[offsetY]}
onValueChange={(value) => setOffsetY(value[0])}
max={50}
min={-50}
step={1}
/>
</div>
<div>
<Label>Blur: {blur}px</Label>
<Slider
value={[blur]}
onValueChange={(value) => setBlur(value[0])}
max={50}
min={0}
step={1}
/>
</div>
<div>
<Label>Opacity: {opacity}</Label>
<Slider
value={[opacity]}
onValueChange={(value) => setOpacity(value[0])}
max={1}
min={0}
step={0.1}
/>
</div>
</div>
<div
className="w-32 h-32 bg-white rounded-md flex items-center justify-center mx-auto border-2 my-4"
style={{
boxShadow: `${offsetX}px ${offsetY}px ${blur}px ${shadowColor}${Math.round(
opacity * 255
)
.toString(16)
.padStart(2, "0")}`,
}}
>
<span className="text-4xl">A</span>
</div>
<div className="grid gap-2">
<Button onClick={handleApplyShadow}>Apply Shadow</Button>
<Button variant="outline" onClick={handleDisableShadow}>
Disable Shadow
</Button>
</div>
</div>
);
};
return (
<Card className="shadow-none border-0">
<CollapsibleComponent text={"Shadow Control"}>
{content()}
</CollapsibleComponent>
</Card>
);
};
export default ShadowCustomization;

View file

@ -0,0 +1,85 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { useContext, useEffect, useState } from "react";
import CollapsibleComponent from "./CollapsibleComponent";
const SkewCustomization = () => {
const { canvas } = useContext(CanvasContext);
const { activeObject } = useContext(ActiveObjectContext);
const [skewX, setSkewX] = useState(0);
const [skewY, setSkewY] = useState(0);
useEffect(() => {
if (activeObject) {
setSkewX(activeObject?.skewX);
setSkewY(activeObject?.skewY);
}
}, [activeObject]);
// Update skewX directly
const handleSkewXChange = (value) => {
setSkewX(value[0]);
if (activeObject && canvas) {
activeObject.set({ skewX: value[0] });
canvas.discardActiveObject();
canvas.setActiveObject(activeObject);
canvas.renderAll();
}
};
// Update skewY directly
const handleSkewYChange = (value) => {
setSkewY(value[0]);
if (activeObject && canvas) {
activeObject.set({ skewY: value[0] });
canvas.discardActiveObject();
canvas.setActiveObject(activeObject);
canvas.renderAll();
}
};
const content = () => {
return (
<div>
<div className="w-full">
<Label className="mb-2">X: {skewX}°</Label>
<Slider
min={-90}
max={90}
step={1}
value={[skewX]}
onValueChange={(value) => {
handleSkewXChange(value);
}}
/>
</div>
<div className="w-full">
<Label className="mb-2">Y: {skewY}°</Label>
<Slider
min={-90}
max={90}
step={1}
value={[skewY]}
onValueChange={(value) => {
handleSkewYChange(value);
}}
/>
</div>
</div>
);
};
return (
<Card className="shadow-none border-0">
<CollapsibleComponent text={"Skew Control"}>
{content()}
</CollapsibleComponent>
</Card>
);
};
export default SkewCustomization;

View file

@ -0,0 +1,402 @@
import { useContext, useState, useRef, useEffect, useCallback } from "react";
import { fabric } from "fabric";
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Slider } from "@/components/ui/slider";
import { Button } from "@/components/ui/button";
import { Paintbrush } from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import CollapsibleComponent from "./CollapsibleComponent";
const StrokeCustomization = () => {
const { activeObject } = useContext(ActiveObjectContext);
const { canvas } = useContext(CanvasContext);
const previewRef = useRef(null);
const [strokeWidth, setStrokeWidth] = useState(0);
const [strokeColor, setStrokeColor] = useState("#000000");
const [gradientStrokeColors, setGradientStrokeColors] = useState({
color1: "#f97316",
color2: "#e26286",
});
const [colorType, setColorType] = useState("color");
const [gradientDirection, setGradientDirection] = useState("to bottom");
// Utility function to handle styles of objects
const handleObjectStyle = (object) => {
if (object.stroke) {
if (typeof object.stroke === "string") {
setColorType("color");
setStrokeColor(object.stroke); // Solid color fill
} else if (object.stroke.colorStops) {
setColorType("gradient");
setGradientStrokeColors({
color1: object.stroke.colorStops[0]?.color || "#f97316",
color2: object.stroke.colorStops[1]?.color || "#e26286",
});
}
}
if (object.strokeWidth) {
setStrokeWidth(object.strokeWidth || 0);
}
};
// Recursively process group objects
const processGroupObjects = useCallback((group, callback) => {
group._objects.forEach((obj) => {
if (obj.type === "group") {
processGroupObjects(obj, callback); // Handle nested groups
} else {
callback(obj); // Apply callback to each object
}
});
}, []);
// Effect to get previous values from active object
useEffect(() => {
if (activeObject) {
if (activeObject.type === "group") {
processGroupObjects(activeObject, handleObjectStyle); // Process grouped objects
} else {
handleObjectStyle(activeObject); // Process single object
}
}
}, [activeObject, processGroupObjects]);
// Update preview style
const updatePreview = useCallback(() => {
if (!previewRef.current) {
return;
}
const previewStyle = {
width: "80px",
height: "80px",
border: `${strokeWidth}px solid`,
borderRadius: "4px",
};
if (colorType === "color") {
previewStyle.borderColor = strokeColor;
} else {
previewStyle.borderImage = `linear-gradient(${gradientDirection}, ${gradientStrokeColors.color1}, ${gradientStrokeColors.color2}) 1`;
}
Object.assign(previewRef.current.style, previewStyle);
}, [
strokeWidth,
strokeColor,
gradientStrokeColors,
colorType,
gradientDirection,
]);
useEffect(() => {
updatePreview();
}, [updatePreview]);
// Handle stroke width change
const handleStrokeWidthChange = (value) => {
setStrokeWidth(value);
};
// Handle color type change
const handleColorTypeChange = (type) => {
setColorType(type);
};
// Handle stroke color change
const handleStrokeColorChange = (e) => {
setStrokeColor(e.target.value);
};
// Handle gradient color change
const handleGradientColorChange = (key, e) => {
setGradientStrokeColors((prev) => ({ ...prev, [key]: e.target.value }));
};
// Apply stroke style to active object
const applyStrokeStyle = useCallback(() => {
if (!activeObject || activeObject.type === "line" || !canvas) {
return;
}
const width = activeObject?.width || 0;
const height = activeObject?.height || 0;
const coords = {
"to bottom": { x1: 0, y1: 0, x2: 0, y2: height },
"to top": { x1: 0, y1: height, x2: 0, y2: 0 },
"to right": { x1: 0, y1: 0, x2: width, y2: 0 },
"to left": { x1: width, y1: 0, x2: 0, y2: 0 },
};
const directionCoords = coords[gradientDirection];
const applyStrokeToObject = (object) => {
object.set("strokeWidth", strokeWidth);
if (colorType === "color") {
object.set("stroke", strokeColor);
} else if (colorType === "gradient") {
const gradient = new fabric.Gradient({
type: "linear",
gradientUnits: "pixels",
coords: directionCoords,
colorStops: [
{ offset: 0, color: gradientStrokeColors.color1 },
{ offset: 1, color: gradientStrokeColors.color2 },
],
});
object.set("stroke", gradient);
}
};
if (activeObject.type === "group") {
processGroupObjects(activeObject, applyStrokeToObject);
} else {
applyStrokeToObject(activeObject);
}
canvas.renderAll();
}, [
activeObject,
strokeWidth,
strokeColor,
gradientStrokeColors,
colorType,
gradientDirection,
canvas,
processGroupObjects,
]);
// Automatically apply stroke styles on state change
useEffect(() => {
applyStrokeStyle();
}, [applyStrokeStyle]);
// Revert stroke styles
const revertStroke = () => {
if (!activeObject || !canvas) {
return;
}
const revertObject = (object) => {
object.set("strokeWidth", 0);
object.set("stroke", null);
};
if (activeObject.type === "group") {
processGroupObjects(activeObject, revertObject);
} else {
revertObject(activeObject);
}
canvas.renderAll();
};
// Render the component
return (
<Card className="shadow-none border-0">
<CollapsibleComponent text={"Stroke Control"}>
<CardContent className="p-0 mb-2">
<div className="space-y-2">
<div>
<Label htmlFor="stroke-width">Stroke Width</Label>
<div className="flex items-center space-x-2">
<Slider
id="stroke-width"
min={0}
max={50}
step={1}
value={[strokeWidth]}
onValueChange={([value]) => handleStrokeWidthChange(value)}
className="flex-grow"
/>
<Input
type="number"
min={0}
max={50}
value={strokeWidth}
onChange={(e) =>
handleStrokeWidthChange(Number(e.target.value))
}
className="w-16"
/>
</div>
</div>
<div>
<Tabs
value={colorType}
onValueChange={(value) => handleColorTypeChange(value)}
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="color">
<Paintbrush className="w-4 h-4 mr-2" />
Solid
</TabsTrigger>
<TabsTrigger value="gradient" className="flex gap-2">
<div className="h-4 w-4 rounded bg-gradient-to-r from-purple-500 to-pink-500" />
Gradient
</TabsTrigger>
</TabsList>
<TabsContent value="color" className="space-y-4">
<div className="space-y-1">
<Label htmlFor="stroke-color">Stroke Color</Label>
<div className="flex items-center space-x-2">
<div className="relative">
<Input
id="stroke-color"
type="color"
value={strokeColor}
onChange={handleStrokeColorChange}
className="w-10 h-10 p-1 rounded-md cursor-pointer"
/>
<div
className="absolute inset-0 pointer-events-none"
style={{
backgroundColor: strokeColor,
borderRadius: "0.375rem",
}}
></div>
</div>
<Input
type="text"
value={strokeColor}
onChange={handleStrokeColorChange}
className="flex-grow"
/>
</div>
</div>
</TabsContent>
<TabsContent value="gradient" className="space-y-4">
<div className="space-y-1">
<Label htmlFor="gradient-direction">
Gradient Direction
</Label>
<Select
value={gradientDirection}
onValueChange={setGradientDirection}
>
<SelectTrigger id="gradient-direction">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="to bottom" key="to-bottom">
Top to Bottom
</SelectItem>
<SelectItem value="to top" key="to-top">
Bottom to Top
</SelectItem>
<SelectItem value="to right" key="to-right">
Left to Right
</SelectItem>
<SelectItem value="to left" key="to-left">
Right to Left
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label htmlFor="gradient-color-1">Gradient Color 1</Label>
<div className="flex items-center space-x-2">
<div className="relative">
<Input
id="gradient-color-1"
type="color"
value={gradientStrokeColors.color1}
onChange={(e) =>
handleGradientColorChange("color1", e)
}
className="w-10 h-10 p-1 rounded-md cursor-pointer"
/>
<div
className="absolute inset-0 pointer-events-none"
style={{
backgroundColor: gradientStrokeColors.color1,
borderRadius: "0.375rem",
}}
></div>
</div>
<Input
type="text"
value={gradientStrokeColors.color1}
onChange={(e) => handleGradientColorChange("color1", e)}
className="flex-grow"
/>
</div>
</div>
<div className="space-y-1">
<Label htmlFor="gradient-color-2">Gradient Color 2</Label>
<div className="flex items-center space-x-2">
<div className="relative">
<Input
id="gradient-color-2"
type="color"
value={gradientStrokeColors.color2}
onChange={(e) =>
handleGradientColorChange("color2", e)
}
className="w-10 h-10 p-1 rounded-md cursor-pointer"
/>
<div
className="absolute inset-0 pointer-events-none"
style={{
backgroundColor: gradientStrokeColors.color2,
borderRadius: "0.375rem",
}}
></div>
</div>
<Input
type="text"
value={gradientStrokeColors.color2}
onChange={(e) => handleGradientColorChange("color2", e)}
className="flex-grow"
/>
</div>
</div>
</TabsContent>
</Tabs>
</div>
<div className="space-y-1">
<Label>Preview</Label>
<div
className="border rounded-md p-2 flex items-center justify-center"
style={{ height: "120px" }}
>
<div
ref={previewRef}
style={{ width: "80px", height: "80px" }}
></div>
</div>
</div>
<div className="grid gap-1">
<Button onClick={applyStrokeStyle} className="bg-[#FF2B85]">
Apply Stroke
</Button>
<Button onClick={revertStroke} variant="outline">
Revert Stroke
</Button>
</div>
</div>
</CardContent>
</CollapsibleComponent>
</Card>
);
};
export default StrokeCustomization;

View file

@ -0,0 +1,463 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import {
AlignLeft,
AlignCenter,
AlignRight,
Bold,
Italic,
Underline,
Strikethrough,
Minus,
Plus,
} from "lucide-react";
import { RiLineHeight } from "react-icons/ri";
import { Slider } from "@/components/ui/slider";
import { Tooltip } from "react-tooltip";
const fonts = [
"Roboto",
"Open Sans",
"Lato",
"Montserrat",
"Raleway",
"Poppins",
"Merriweather",
"Playfair Display",
"Nunito",
"Oswald",
"Source Sans Pro",
"Ubuntu",
"Noto Sans",
"Work Sans",
"Bebas Neue",
"Arimo",
"PT Sans",
"PT Serif",
"Titillium Web",
"Fira Sans",
"Karla",
"Josefin Sans",
"Cairo",
"Rubik",
"Mulish",
"IBM Plex Sans",
"Quicksand",
"Cabin",
"Heebo",
"Exo 2",
"Manrope",
"Jost",
"Anton",
"Asap",
"Baloo 2",
"Barlow",
"Cantarell",
"Chivo",
"Inter",
"Dosis",
"Crimson Text",
"Amatic SC",
"ABeeZee",
"Raleway Dots",
"Pacifico",
"Orbitron",
"Varela Round",
"Acme",
"Teko",
];
const TextCustomization = () => {
const { activeObject } = useContext(ActiveObjectContext);
const { canvas, setSelectedPanel, textColor } = useContext(CanvasContext);
const activeObjectType = activeObject?.type;
const hasClipPath = !!activeObject?.clipPath;
const customClipPath = activeObject?.isClipPath;
const prevTextRef = useRef("");
const [text, setText] = useState("");
const [fontFamily, setFontFamily] = useState("Arial");
const [fontSize, setFontSize] = useState(20);
const [fontStyle, setFontStyle] = useState("normal");
const [fontWeight, setFontWeight] = useState("normal");
const [lineHeight, setLineHeight] = useState(1.16);
const [charSpacing, setCharSpacing] = useState(0);
const [underline, setUnderline] = useState(false);
const [linethrough, setLinethrough] = useState(false);
const [textAlign, setTextAlign] = useState("left");
const [fillColor, setFillColor] = useState("black");
useEffect(() => {
if (activeObject?.type === "i-text") {
if (
activeObject?.text !== undefined &&
activeObject.text !== prevTextRef.current
) {
setText(activeObject.text);
prevTextRef.current = activeObject.text;
}
setText(activeObject?.text || "");
setFontFamily(activeObject?.fontFamily || "Arial");
setFontSize(activeObject?.fontSize || 20);
setFontStyle(activeObject?.fontStyle || "normal");
setFontWeight(activeObject?.fontWeight || "normal");
setLineHeight(activeObject?.lineHeight || 1.16);
setCharSpacing(activeObject?.charSpacing || 0);
setUnderline(activeObject?.underline || false);
setLinethrough(activeObject?.linethrough || false);
setTextAlign(activeObject?.textAlign || "left");
setFillColor(textColor?.fill || "black");
}
}, [activeObject, textColor]);
const updateActiveObject = useCallback(
(properties) => {
if (activeObject?.type === "i-text") {
// Preserve the text value when updating other properties
const updatedProperties = {
...properties,
text: text || prevTextRef.current || properties.text,
};
activeObject.set(updatedProperties);
canvas?.renderAll();
}
},
[activeObject, canvas, text]
); // Add dependencies
const applyChanges = useCallback(() => {
updateActiveObject({
text,
fontFamily,
fontSize,
fontStyle,
fontWeight,
lineHeight,
charSpacing,
underline,
linethrough,
textAlign,
});
}, [
text,
fontFamily,
fontSize,
fontStyle,
fontWeight,
lineHeight,
charSpacing,
underline,
linethrough,
textAlign,
updateActiveObject, // Add this dependency
]);
const handleColorPanelClick = () => {
// Store current text value before switching panels
prevTextRef.current = text;
setSelectedPanel("color");
};
// Automatically apply changes when state updates
useEffect(() => {
if (activeObject?.type === "i-text") {
applyChanges();
}
}, [
applyChanges, // Now included in dependencies
activeObject?.type, // Track active object type
]);
const handleFontFamilyChange = (newFontFamily) => {
setFontFamily(newFontFamily);
};
const handleFontSizeChange = (newFontSize) => {
setFontSize(newFontSize);
};
const handleTextAlignChange = (newTextAlign) => {
setTextAlign(newTextAlign);
};
const handleFontStyleChange = () => {
setFontStyle(fontStyle === "normal" ? "italic" : "normal");
};
const handleFontWeightChange = () => {
setFontWeight(fontWeight === "normal" ? "bold" : "normal");
};
const handleLineHeightChange = (newLineHeight) => {
setLineHeight(parseFloat(newLineHeight));
};
const handleCharSpacingChange = (newCharSpacing) => {
setCharSpacing(parseInt(newCharSpacing));
};
const handleUnderlineChange = () => {
setUnderline(!underline);
};
const handleLinethroughChange = () => {
setLinethrough(!linethrough);
};
const content = () => {
if (!(activeObject?.type === "i-text")) {
return <div></div>;
}
return (
<div className="space-y-4">
{/* New Toolbar Design */}
<div className="flex w-full items-center space-x-2 rounded-lg p-1 bg-white">
{/* Font Family Select */}
<a data-tooltip-id="fonts">
<Select
value={fontFamily}
onValueChange={handleFontFamilyChange}
title="Font Family"
>
<SelectTrigger className="min-w-[140px] h-8">
<SelectValue placeholder="Select a font" />
</SelectTrigger>
<SelectContent className="h-[250px] z-[9999]">
<SelectGroup>
{fonts.map((font) => (
<SelectItem
key={font}
value={font}
style={{ fontFamily: font }}
>
{font}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</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">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-none"
onClick={() => handleFontSizeChange(Math.max(8, fontSize - 1))}
>
<Minus className="h-4 w-4" />
</Button>
</a>
<Tooltip
id="font-dec"
content="Decrease font size"
place="bottom"
/>
<a data-tooltip-id="font-size">
<Input
type="text"
value={fontSize}
onChange={(e) => {
const numericValue = e.target.value.replace(/\D/g, "");
handleFontSizeChange(parseInt(numericValue) || 12);
}}
className="w-12 h-8 border-0 text-center"
/>
</a>
<Tooltip id="font-size" content="Font size" place="bottom" />
<a data-tooltip-id="font-inc">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-none"
onClick={() => handleFontSizeChange(Math.min(72, fontSize + 1))}
>
<Plus className="h-4 w-4" />
</Button>
</a>
<Tooltip
id="font-inc"
content="Increase font size"
place="bottom"
/>
</div>
{/* Vertical Separator */}
<div className="h-6 w-px bg-gray-200" />
{/* Text Formatting Controls */}
<div className="flex items-center space-x-1">
{activeObjectType !== "image" &&
!hasClipPath &&
!customClipPath && (
<a data-tooltip-id="text-color">
<Button
variant="ghost"
size="icon"
className="relative"
onClick={handleColorPanelClick} // Updated onClick handler
>
<div className="relative">
<span className="text-lg font-semibold">A</span>
<div
className="absolute -bottom-0.5 -left-1 right-0 h-1 w-5 transition-all duration-200"
style={{ backgroundColor: fillColor }}
/>
</div>
</Button>
</a>
)}
<Tooltip id="text-color" content="Text color" place="bottom" />
<a data-tooltip-id="text-left">
<Button
variant={textAlign === "left" ? "secondary" : "ghost"}
size="icon"
onClick={() => handleTextAlignChange("left")}
>
<AlignLeft className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="text-center">
<Button
variant={textAlign === "center" ? "secondary" : "ghost"}
size="icon"
onClick={() => handleTextAlignChange("center")}
>
<AlignCenter className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="text-right">
<Button
variant={textAlign === "right" ? "secondary" : "ghost"}
size="icon"
onClick={() => handleTextAlignChange("right")}
>
<AlignRight className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="text-bold">
<Button
variant={fontWeight === "bold" ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleFontWeightChange}
>
<Bold className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="text-underline">
<Button
variant={underline ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleUnderlineChange}
>
<Underline className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="text-italic">
<Button
variant={fontStyle === "italic" ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleFontStyleChange}
>
<Italic className="h-4 w-4" />
</Button>
</a>
<a data-tooltip-id="line-through">
<Button
variant={linethrough ? "secondary" : "ghost"}
size="icon"
className="h-8 w-8"
onClick={handleLinethroughChange}
>
<Strikethrough className="h-4 w-4" />
</Button>
</a>
<Tooltip id="text-left" content="Left align" place="bottom" />
<Tooltip id="text-center" content="Center align" place="bottom" />
<Tooltip id="text-right" content="Right align" place="bottom" />
<Tooltip id="text-bold" content="Bold" place="bottom" />
<Tooltip id="text-underline" content="Underline" place="bottom" />
<Tooltip id="text-italic" content="Italics" place="bottom" />
<Tooltip id="line-through" content="StrikeThrough" place="bottom" />
</div>
{/* Vertical Separator */}
<div className="h-6 w-px bg-gray-200" />
{/* Spacing Controls */}
<Popover>
<PopoverTrigger asChild>
<a data-tooltip-id="spacing">
<Button variant="ghost" size="icon" className="h-8 w-8">
<RiLineHeight className="h-4 w-4" />
</Button>
</a>
</PopoverTrigger>
<PopoverContent className="w-44 mt-3">
<div className="space-y-4">
<div className="space-y-2">
<Label>Line Spacing</Label>
<div className="flex items-center space-x-2">
<Slider
value={[lineHeight]}
onValueChange={([value]) => handleLineHeightChange(value)}
min={0.5}
max={3}
step={0.1}
/>
</div>
</div>
<div className="space-y-2">
<Label>Letter Spacing</Label>
<div className="flex items-center space-x-2">
<Slider
value={[charSpacing]}
onValueChange={([value]) =>
handleCharSpacingChange(value)
}
min={-20}
max={100}
step={1}
/>
</div>
</div>
</div>
</PopoverContent>
</Popover>
<Tooltip id="spacing" content="Spacing" place="bottom" />
</div>
</div>
);
};
return <div className="">{content()}</div>;
};
export default TextCustomization;

View file

@ -0,0 +1,151 @@
import { useContext, useState } from "react";
import { FixedSizeGrid as Grid } from "react-window";
import { Card, CardDescription, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
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 = () => {
const [search, setSearch] = useState("");
const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
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
);
const filtered = icons.filter(([name, Icon]) =>
name.toLowerCase().includes(search.toLowerCase())
);
const handleSearch = (e) => {
setSearch(e.target.value);
};
const handleIcon = (e) => {
// Check if the target is an SVG or path
if (e.target.tagName.toLowerCase() === "svg") {
// Serialize the SVG element to a string and pass it
const svgString = new XMLSerializer().serializeToString(e.target);
handleAddIcon(svgString);
} else {
toast({
title: "Invalid Choice",
description: "The target is a path element! Select the full icon.",
variant: "destructive",
});
}
};
const handleAddIcon = (svgString) => {
if (!canvas) {
console.error("Canvas is not initialized.");
return;
}
if (!svgString) {
console.error("Failed to retrieve SVG string from the icon.");
return;
}
fabric.loadSVGFromString(svgString, (objects, options) => {
if (!objects || !options) {
console.error("Failed to parse SVG.");
return;
}
// Recursively set fill color for all objects
const setFillColor = (obj, color) => {
if (obj.type === "group" && obj._objects) {
obj._objects.forEach((child) => setFillColor(child, color));
} else {
obj.set("stroke", color);
}
};
objects.forEach((obj) => setFillColor(obj, "#FFA500")); // Set fill color to orange
const iconGroup = fabric.util.groupSVGElements(objects, options);
iconGroup.set({
left: canvas.width / 2,
top: canvas.height / 2,
originX: "center",
originY: "center",
scaleX: 6,
scaleY: 6,
});
canvas.add(iconGroup);
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 });
});
};
// Cell component for rendering each icon
const Cell = ({ columnIndex, rowIndex, style }) => {
const index = rowIndex * 3 + columnIndex; // Adjust columns as needed (3 columns in this case)
if (index >= filtered.length) return null; // Handle out-of-bounds index
const [name, Icon] = filtered[index];
// Define cell-specific styles
return (
<div style={style}>
<div className="bg-red-50 rounded-md ml-1 p-1">
<Icon
size={32}
className="cursor-pointer bg-primary rounded-md text-white p-1 mx-auto"
onClick={handleIcon}
/>
<p className="text-xs text-center truncate w-full overflow-hidden whitespace-nowrap">
{name}
</p>
</div>
</div>
);
};
return (
<Card className="flex flex-col py-2 gap-1 scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white border-none shadow-none">
<CardTitle className="flex items-center flex-wrap my-1">
All Icons
</CardTitle>
<CardDescription className="text-xs">
All copyright (c) for Lucide are held by Lucide Contributors 2022.
</CardDescription>
<Input
type="text"
placeholder="Search icons..."
onChange={handleSearch}
className="border p-2 mb-0 w-[280px]"
/>
<Card className="flex items-center justify-center rounded-none p-1 border-none shadow-none">
<Grid
columnCount={3}
columnWidth={90}
height={330}
rowCount={Math.ceil(filtered.length / 3)}
rowHeight={70}
width={300}
className="scrollbar-thin scrollbar-thumb-secondary scrollbar-track-white"
>
{Cell}
</Grid>
</Card>
</Card>
);
};
export default AllIconsPage;

View file

@ -0,0 +1,103 @@
import React, { useContext } from "react";
import {
ArrowBigRight,
Diamond,
Hexagon,
Octagon,
Pentagon,
Sparkle,
Square,
Star,
Triangle,
} from "lucide-react";
import ReactDOMServer from "react-dom/server";
import { fabric } from "fabric";
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" },
{ icon: <Hexagon />, name: "Hexagon" },
{ icon: <Octagon />, name: "Octagon" },
{ icon: <Pentagon />, name: "Pentagon" },
{ icon: <Sparkle />, name: "Sparkle" },
{ icon: <Square />, name: "Square" },
{ icon: <Star />, name: "Star" },
{ icon: <Triangle />, name: "Triangle" },
];
const addObject = (icon) => {
if (!canvas) {
console.error("Canvas is not initialized.");
return;
}
const svgString = ReactDOMServer.renderToStaticMarkup(icon);
if (!svgString) {
console.error("Failed to retrieve SVG string from icon.");
return;
}
// Load SVG onto the Fabric.js canvas
fabric.loadSVGFromString(svgString, (objects, options) => {
if (!objects || !options) {
console.error("Failed to parse SVG.");
return;
}
const iconGroup = fabric.util.groupSVGElements(objects, options);
iconGroup.set({
left: canvas.width / 2,
top: canvas.height / 2,
originX: "center",
originY: "center",
fill: "#f09b0a",
scaleX: 6,
scaleY: 6,
strokeWidth: 0,
stroke: "#ffffff",
});
canvas.add(iconGroup);
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 });
});
};
return (
<Card className="p-2 border-none shadow-none">
<h2 className="font-semibold text-sm mb-1">Rounded Shapes</h2>
<Separator className="my-2" />
<div className="grid grid-cols-3 gap-y-4 gap-x-2">
{shapes.map((shape, index) => (
<button
key={index}
className="group flex flex-col items-center justify-center p-1 bg-secondary rounded-lg shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105"
onClick={() => addObject(shape.icon)}
>
<div className="text-orange-600 group-hover:text-orange-500 transition-colors duration-300">
{React.cloneElement(shape.icon, { size: 36 })}
</div>
{/* <span className="text-xs font-bold text-gray-700 group-hover:text-blue-600 transition-colors duration-300">{shape.name}</span> */}
</button>
))}
</div>
</Card>
);
};
export default RoundedShape;

View file

@ -0,0 +1,334 @@
import ActiveObjectContext from "@/components/Context/activeObject/ObjectContext";
import CanvasContext from "@/components/Context/canvasContext/CanvasContext";
import React, { useContext } from "react";
import ReactDOMServer from "react-dom/server";
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: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
className="lucide lucide-arrow-big-left"
fill="orange"
>
<path d="M18 15H12v4L5 12l7-7v4h6v6z" />
</svg>
),
name: "Arrow",
},
{ icon: <Badge />, name: "Badge" },
{ icon: <Circle />, name: "Circle" },
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-club"
>
<path d="M17.28 9.05a5.5 5.5 0 1 0-10.56 0A5.5 5.5 0 1 0 12 17.66a5.5 5.5 0 1 0 5.28-8.6Z" />
</svg>
),
name: "Club",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="none"
stroke="#ea580c"
strokeWidth="2"
strokeLinecap="butt"
strokeLinejoin="miter"
className="lucide lucide-cross"
>
<path d="M4 12h16M12 4v16" />
</svg>
),
name: "Cross",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-diamond"
>
<path d="M12 2L22 12L12 22L2 12Z" />
</svg>
),
name: "Diamond",
},
{ icon: <Heart />, name: "Heart" },
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-hexagon"
>
<path d="M12 2L21 8v8l-9 6-9-6V8L12 2z" />
</svg>
),
name: "Hexagon",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="white"
stroke="#ea580c"
strokeWidth="2"
strokeLinecap="butt"
strokeLinejoin="miter"
className="lucide lucide-arrow-right"
>
<path d="M5 12h14" />
</svg>
),
name: "Line",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
stroke="#ec7e7e"
strokeWidth="0"
className="lucide lucide-octagon"
>
<path d="M4 8 L8 4 H16 L20 8 V16 L16 20 H8 L4 16 Z" />
</svg>
),
name: "Octagon",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-pentagon"
>
<path d="M2 11 L12 2 L22 11 L19 21 L5 21 Z" />
</svg>
),
name: "Pentagon",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-rectangle-horizontal"
>
<path d="M2 6h20v12H2z" />
</svg>
),
name: "Rectangle",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-triangle-right"
>
<path d="M2 4 L22 20 L2 20 Z" />
</svg>
),
name: "Right Triangle",
},
{
icon: <Shield fill="orange" stroke="0" />,
name: "Shield",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-square"
>
<path d="M3 3h18v18H3z" />
</svg>
),
name: "Rectangle Square",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-star"
>
<path d="M12 2 L14.85 8.9 L22 10 L16.5 14.5 L18 21 L12 17.5 L6 21 L7.5 14.5 L2 10 L9.15 8.9 Z" />
</svg>
),
name: "Star",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-triangle"
>
<path d="M12 4L4 20h16L12 4Z" />
</svg>
),
name: "Triangle",
},
{
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="36"
height="36"
viewBox="0 0 24 24"
fill="orange"
className="lucide lucide-rectangle-vertical scale-125"
>
<path d="M6 2h12v20H6z" />
</svg>
),
name: "Rectangle Vertical",
},
];
const addObject = (icon, name) => {
if (!canvas) {
console.error("Canvas is not initialized.");
return;
}
const svgString = ReactDOMServer.renderToStaticMarkup(icon);
if (!svgString) {
console.error("Failed to retrieve SVG string from icon.");
return;
}
// Load SVG onto the Fabric.js canvas
fabric.loadSVGFromString(svgString, (objects, options) => {
if (!objects || !options) {
console.error("Failed to parse SVG.");
return;
}
const iconGroup = fabric.util.groupSVGElements(objects, options);
iconGroup.set({
left: canvas.width / 2,
top: canvas.height / 2,
originX: "center",
originY: "center",
fill: "#f09b0a",
scaleX: 6,
scaleY: 6,
strokeWidth: 0,
// rx: 0,
// x: 0,
// y: 0,
});
if (name === "Line") {
iconGroup.set({
strokeWidth: 2,
});
}
canvas.add(iconGroup);
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 });
});
};
return (
<Card className="p-2 rounded-xl shadow-none border-none">
<h2 className="font-semibold text-sm mb-1">Plain Shapes</h2>
<Separator className="my-2" />
<div className="grid grid-cols-3 gap-y-4 gap-x-2">
{shapes.map((shape, index) => (
<button
key={index}
className="group flex flex-col items-center justify-center p-1 bg-secondary rounded-lg shadow-md hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 hover:scale-105"
onClick={() => addObject(shape.icon, shape.name)}
>
<div className="text-orange-600 group-hover:text-orange-500 transition-colors duration-300">
{React.cloneElement(shape.icon, { size: 36, fill: "#ea580c" })}
</div>
{/* <span className="text-xs font-bold text-gray-700 group-hover:text-blue-600 transition-colors duration-300">{shape.name}</span> */}
</button>
))}
</div>
</Card>
);
};
export default PlainShapes;

View file

@ -0,0 +1,339 @@
import { useContext, useEffect, useRef, useState } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "@/components/ui/button";
import { ImageIcon, Trash2 } from "lucide-react";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import Resizer from "react-image-file-resizer";
import { Slider } from "@/components/ui/slider";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
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);
const [width, setWidth] = useState(1080);
const [height, setHeight] = useState(1080);
const [quality, setQuality] = useState(100);
const [rotation, setRotation] = useState("0");
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"],
},
// maxSize: 5 * 1024 * 1024, // 5MB max file size
multiple: false,
onDrop: (acceptedFiles) => {
if (!acceptedFiles.length) {
console.error("No files were dropped.");
return;
}
const selectedFile = acceptedFiles[0];
// Create a preview URL
const blobUrl = URL.createObjectURL(selectedFile);
setFile(selectedFile);
if (selectedFile.type === "image/svg+xml") {
toast({
title: "SVG images are not supported.",
description: "Please upload a different image format.",
variant: "destructive",
})
URL.revokeObjectURL(blobUrl);
return
} else {
const imgElement = new Image();
imgElement.src = blobUrl;
imgElement.onload = () => {
if (imgElement.width > 1080) {
handleResize(selectedFile, (compressedFile) => {
addImageToCanvas(compressedFile);
});
} else {
addImageToCanvas(selectedFile);
}
URL.revokeObjectURL(blobUrl); // Clean up
};
imgElement.onerror = () => {
console.error("Failed to load image.");
URL.revokeObjectURL(blobUrl); // Clean up
};
}
},
});
const handleResize = (selectedFile, callback) => {
const img = new Image();
img.src = URL.createObjectURL(selectedFile);
img.onload = () => {
let newWidth = width; // User-defined width
let newHeight = height; // User-defined height
const aspectRatio = img.width / img.height;
// If the user provided only one dimension, maintain aspect ratio
if (!width) {
newWidth = Math.round(newHeight * aspectRatio);
} else if (!height) {
newHeight = Math.round(newWidth / aspectRatio);
}
Resizer.imageFileResizer(
selectedFile,
newWidth, // Use user-defined width or calculated width
newHeight, // Use user-defined height or calculated height
format,
quality,
parseInt(rotation),
(resizedFile) => {
callback(resizedFile);
},
'file'
);
URL.revokeObjectURL(img.src); // Cleanup
};
img.onerror = () => {
console.error("Failed to load image for resizing.");
URL.revokeObjectURL(img.src); // Cleanup
};
};
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">
<CardHeader className="px-4 py-3">
<CardTitle>Image Upload & Editing</CardTitle>
<CardDescription>
Upload, resize, and apply effects to your images
</CardDescription>
</CardHeader>
<CardContent className="p-2">
<Tabs defaultValue="upload">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="upload">Upload</TabsTrigger>
<TabsTrigger value="effects">Effects</TabsTrigger>
</TabsList>
{/* Uploads */}
<TabsContent value="upload">
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Width: {width}px</Label>
<Slider
value={[width]}
max={2000}
min={300}
step={10}
onValueChange={(value) => setWidth(value[0])}
/>
</div>
<div className="space-y-2">
<Label>Height: {height}px</Label>
<Slider
value={[height]}
max={2000}
min={300}
step={10}
onValueChange={(value) => setHeight(value[0])}
/>
</div>
</div>
{format === "JPEG" && (
<div className="space-y-2">
<Label>Quality: {quality}%</Label>
<Slider
value={[quality]}
max={100}
step={1}
onValueChange={(value) => setQuality(value[0])}
/>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Format</Label>
<Select value={format} onValueChange={setFormat}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="JPEG">JPEG</SelectItem>
<SelectItem value="PNG">PNG</SelectItem>
<SelectItem value="WEBP">WEBP</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Rotation</Label>
<Select value={rotation} onValueChange={setRotation}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">0°</SelectItem>
<SelectItem value="45">45°</SelectItem>
<SelectItem value="90">90°</SelectItem>
<SelectItem value="180">180°</SelectItem>
<SelectItem value="270">270°</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* upload image */}
{!preview && (
<div className="max-w-md mx-auto p-4">
<Card>
<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
? "border-primary bg-primary/10"
: "border-gray-300 hover:border-primary"
} ${preview ? "hidden" : ""}`}
ref={fileInputRef}
>
<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"
}`}
/>
<p className="text-sm text-gray-600">
{isDragActive
? "Drop file here"
: "Drag 'n' drop an image, or click to select a file"}
</p>
<p className="text-xs text-gray-500">
(Max 5MB, image files only)
</p>
</div>
</div>
</CardContent>
</Card>
</div>
)}
{/* preview image */}
{preview && (
<Card className="overflow-y-scroll rounded-none">
<CardContent className="mt-2 mb-2">
<div className="w-fit aspect-square relative h-[100%]">
{file?.type === "image/svg+xml" ? (
<object
data={preview}
type="image/svg+xml"
className="object-cover rounded-lg"
style={{ width: "100%", height: "100%" }}
>
Your browser does not support SVG, no preview
available for SVG.
</object>
) : (
<img
src={preview}
alt="Uploaded image"
className="object-cover rounded-lg overflow-hidden"
/>
)}
<Separator className="my-4" />
<div className="grid grid-cols-1 gap-2 items-center pb-4">
<p className="text-sm text-gray-600 truncate">
{file?.name}
</p>
<Button
variant="destructive"
size="sm"
onClick={removeFile}
>
<Trash2 className="mr-2 h-4 w-4" />
Remove
</Button>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</TabsContent>
{/* Effects */}
<TabsContent value="effects">
<ImageCustomization />
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
};
export default UploadImage;

View file

@ -0,0 +1,237 @@
import { useContext, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Button } from './ui/button';
import { PencilRuler, Save, Settings, Shapes, SquareX, Store, Upload, ChevronDown, ChevronUp, Type, ImageDown, X } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import OpenContext from './Context/openContext/OpenContext';
import CanvasContext from './Context/canvasContext/CanvasContext';
import ActiveObjectContext from './Context/activeObject/ObjectContext';
import { fabric } from 'fabric';
import RndComponent from './Layouts/RndComponent';
import { ObjectShortcut } from "./ObjectShortcut";
export function EditPanel() {
const [isCollapsed, setIsCollapsed] = useState(false);
const { setTabValue, setOpenSetting, setOpenObjectPanel, setCaptureOpen, setOpenPanel } = useContext(OpenContext);
const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const saveCanvasState = () => {
// Get the JSON representation of all objects
const json = canvas.toJSON(['id', 'selectable']); // Include any custom properties you need
console.log(json);
// Get background image data if it exists
let backgroundImageData = null;
if (canvas.backgroundImage) {
backgroundImageData = {
src: canvas.backgroundImage._element.src,
width: canvas.backgroundImage.width,
height: canvas.backgroundImage.height,
scaleX: canvas.backgroundImage.scaleX,
scaleY: canvas.backgroundImage.scaleY,
originX: canvas.backgroundImage.originX,
originY: canvas.backgroundImage.originY,
opacity: canvas.backgroundImage.opacity
};
}
// Create the complete canvas state
const canvasState = {
version: '1.0',
objects: json.objects,
background: backgroundImageData,
width: canvas.width,
height: canvas.height,
backgroundColor: canvas.backgroundColor
};
console.log('Canvas state saved:', canvasState);
// loadCanvasState(canvasState);
};
// for clear canvas
const clearCanvas = () => {
canvas.clear();
canvas.renderAll();
}
const addText = () => {
if (canvas) {
const text = new fabric.IText('Editable Text', {
left: 100,
top: 100,
fontFamily: 'Poppins',
fontSize: 16,
});
// Add the text to the canvas and re-render
canvas.add(text);
// canvas.clipPath = text;
canvas.setActiveObject(text);
setActiveObject(text);
canvas.renderAll();
}
};
const rndValue = {
valueX: 0,
valueY: 20,
width: 250,
height: 0,
minWidth: 250,
maxWidth: 300,
minHeight: 0,
maxHeight: 0,
bound: "parent"
}
return (
<RndComponent value={rndValue}>
<Card className="w-full shadow-lg">
<CardHeader className="p-1 mr-12">
<div className="flex items-center justify-between">
<Button className="rnd-escape" variant={"ghost"} onClick={() => setOpenPanel(false)}><X /></Button>
<CardTitle className="text-lg">Edit Panel</CardTitle>
</div>
</CardHeader>
<Collapsible open={!isCollapsed} onOpenChange={(open) => setIsCollapsed(!open)} className="rnd-escape">
<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>
<CardContent className="p-2">
<Tabs defaultValue="edit" className="w-full">
<TabsList className="w-full flex justify-around gap-2">
<TabsTrigger value="edit">Edit</TabsTrigger>
<TabsTrigger value="add" className="block xl:hidden lg:hidden md:hidden">Add</TabsTrigger>
<TabsTrigger value="canvas">Canvas</TabsTrigger>
</TabsList>
<TabsContent value="edit" className="mt-2">
<ObjectShortcut value={"edit"} />
</TabsContent>
<TabsContent value="add" className="mt-2">
<TooltipProvider>
<div className="grid grid-cols-3 gap-2">
<ActionButton
icon={<Store className="h-4 w-4" />}
label="Icons"
onClick={() => {
setOpenObjectPanel(true);
setTabValue("icons");
}}
tooltipContent="Add icons to canvas"
/>
<ActionButton
icon={<Shapes className="h-4 w-4" />}
label="Shapes"
onClick={() => {
setTabValue("shapes");
setOpenObjectPanel(true);
}}
tooltipContent="Add shapes to canvas"
/>
<ActionButton
icon={<Type className="h-4 w-4" />}
label="Text"
onClick={() => {
addText();
setTabValue("customize"); setOpenObjectPanel(true);
}}
tooltipContent="Add text to canvas"
/>
<ActionButton
icon={<Upload className="h-4 w-4" />}
label="Image"
onClick={() => {
setTabValue("images"); setOpenObjectPanel(true);
}}
tooltipContent="Upload and add image to canvas"
/>
<ActionButton
icon={<PencilRuler className="h-4 w-4" />}
label="Customize"
onClick={() => {
setTabValue("customize"); setOpenObjectPanel(true);
}}
tooltipContent="Customize objects on canvas"
/>
</div>
</TooltipProvider>
</TabsContent>
<TabsContent value="canvas" className="mt-2">
<TooltipProvider>
<div className="grid grid-cols-3 gap-2">
<ActionButton
icon={<Save className="h-4 w-4" />}
label="Save"
onClick={saveCanvasState}
tooltipContent="Save current canvas state"
/>
<div className="block xl:hidden lg:hidden md:hidden">
<ActionButton
icon={<Settings className="h-4 w-4" />}
label="Settings"
onClick={() => setOpenSetting(true)}
tooltipContent="Open canvas settings"
/>
</div>
<ActionButton
icon={<SquareX className="h-4 w-4" />}
label="Clear"
onClick={clearCanvas}
tooltipContent="Clear entire canvas"
/>
<ActionButton
icon={<ImageDown className="h-4 w-4" />}
label="Capture"
onClick={() => setCaptureOpen(true)}
tooltipContent="Capture canvas"
/>
</div>
</TooltipProvider>
</TabsContent>
</Tabs>
</CardContent>
</CollapsibleContent>
</Collapsible>
</Card>
</RndComponent>
)
}
function ActionButton({ icon, label, onClick, tooltipContent }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="md" className="w-full" onClick={onClick}>
<div className="flex flex-col items-center gap-0 p-1">
{icon}
<span className="text-xs">{label}</span>
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" className="max-w-xs">
<p>{tooltipContent}</p>
</TooltipContent>
</Tooltip>
)
}

View file

@ -0,0 +1,69 @@
import RoundedShape from '../EachComponent/RoundedShapes/RoundedShape'
import PlainShapes from '../EachComponent/Shapes/PlainShapes'
import { Card } from '../ui/card'
import { Separator } from '../ui/separator'
import CustomShape from '../EachComponent/CustomShape/CustomShape'
import { useContext } from 'react'
import CanvasContext from '../Context/canvasContext/CanvasContext'
import ActiveObjectContext from '../Context/activeObject/ObjectContext'
import { fabric } from 'fabric'
import { Button } from '../ui/button'
import { Type } from "lucide-react";
const AddShapes = () => {
const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const addText = () => {
if (canvas) {
const text = new fabric.IText('Editable Text', {
left: 100,
top: 100,
fontFamily: 'Poppins',
fontSize: 16,
});
// Add the text to the canvas and re-render
canvas.add(text);
// canvas.clipPath = text;
canvas.setActiveObject(text);
setActiveObject(text);
canvas.renderAll();
}
};
return (
<div className='flex gap-2 item-center flex-col justify-center p-1 overflow-hidden'>
<Card className="p-2">
<h2 className="font-semibold text-sm text-left">Add Text</h2>
<Separator className="my-2" />
<div>
<Button
onClick={() => {
addText();
}}
>
<Type className="h-4 w-4" />
Editable text
</Button>
</div>
</Card>
<Card className='grid gap-0 p-2'>
<h2 className="font-semibold text-sm">Custom Shapes</h2>
<Separator className="my-2" />
<div>
<CustomShape />
</div>
</Card>
<div>
<RoundedShape />
</div>
<div>
<PlainShapes />
</div>
</div>
)
}
export default AddShapes

View file

@ -0,0 +1,48 @@
import { cn } from "@/lib/utils";
import {
Type,
Text,
Shapes,
FolderUp,
Shield,
Image,
FolderKanban,
} from "lucide-react";
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
const sidebarItems = [
{ id: "design", icon: Text, label: "Design" },
{ id: "text", icon: Type, label: "Text" },
{ id: "shape", icon: Shapes, label: "Shape" },
{ id: "upload", icon: FolderUp, label: "Upload" },
{ id: "icon", icon: Shield, label: "Icon" },
{ id: "image", icon: Image, label: "Image" },
{ id: "project", icon: FolderKanban, label: "Project" },
];
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">
{sidebarItems.map((item) => {
const Icon = item.icon;
return (
<button
key={item.id}
onClick={() => {
setSelectedPanel(item.id);
}}
className={cn(
"flex flex-col items-center gap-1 p-2 rounded-lg w-16 hover:bg-accent",
selectedPanel === item.id && "bg-accent"
)}
>
<Icon className="h-5 w-5" />
<span className="text-xs">{item.label}</span>
</button>
);
})}
</div>
);
}

View file

@ -0,0 +1,28 @@
import { Rnd } from 'react-rnd';
const RndComponent = ({ children, value }) => {
const { valueX, valueY, width, height, minWidth, maxWidth, minHeight, maxHeight, bound } = value;
return (
<Rnd
default={{
x: valueX,
y: valueY,
width: width,
height: height,
}}
minWidth={minWidth}
maxWidth={maxWidth}
maxHeight={maxHeight}
bounds={bound}
className="z-[1000]"
cancel=".rnd-escape"
>
{children}
</Rnd>
)
}
export default RndComponent

View file

@ -0,0 +1,403 @@
import { useCallback, useContext, useMemo, useState } from "react";
import CanvasContext from "./Context/canvasContext/CanvasContext";
import ActiveObjectContext from "./Context/activeObject/ObjectContext";
import { fabric } from "fabric";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "./ui/tooltip";
import { Button } from "./ui/button";
import {
BringToFront,
SendToBack,
CopyPlus,
GroupIcon,
SquareX,
Trash2,
UngroupIcon,
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);
const { setActiveObject, activeObject } = useContext(ActiveObjectContext);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const activeObjects = canvas.getActiveObjects();
const multipleObjects = activeObjects.length > 1;
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) {
canvas.discardActiveObject();
const group = new fabric.Group(activeObjects, {
left: canvas?.width / 2,
top: canvas?.height / 2,
originX: "center",
originY: "center",
selectable: true, // Allow group selection
subTargetCheck: true, // Allow individual object selection
hasControls: true, // Enable resizing/movement of the group
});
canvas.remove(...activeObjects);
canvas.add(group);
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",
})
}
};
const ungroupSelectedObjects = () => {
const activeObject = canvas.getActiveObject();
if (activeObject && activeObject.type === "group") {
const groupObjects = activeObject._objects;
canvas.discardActiveObject();
canvas.remove(activeObject);
setActiveObject(null);
const ungroupedObjects = [];
groupObjects.forEach((object) => {
// Calculate absolute position
const objLeft = object.left * activeObject.scaleX + activeObject.left;
const objTop = object.top * activeObject.scaleY + activeObject.top;
object.set({
left: objLeft,
top: objTop,
scaleX: object.scaleX * activeObject.scaleX,
scaleY: object.scaleY * activeObject.scaleY,
angle: object.angle + activeObject.angle,
hasControls: true,
selectable: true,
group: null,
originX: "center",
originY: "center",
});
object.setCoords();
canvas.add(object);
ungroupedObjects.push(object);
});
const selection = new fabric.ActiveSelection(ungroupedObjects, {
canvas: canvas,
originX: "center",
originY: "center",
});
canvas.setActiveObject(selection);
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 });
}
};
// Check if object is at front or back
const objectPosition = useMemo(() => {
if (!activeObject || !canvas) {
return { isAtFront: false, isAtBack: false };
}
const allObjects = canvas.getObjects();
const index = allObjects.indexOf(activeObject);
return {
isAtFront: index === allObjects.length - 1,
isAtBack: index === 0,
};
}, [activeObject, canvas]);
// Layer ordering functions with checks
const bringToFront = () => {
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);
}
};
const sendToBack = () => {
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();
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);
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;
const regex = /^https:\/\/images\.pexels\.com/;
const isPexelsImage = regex.test(imgUrl);
if (isPexelsImage) {
canvas.remove(activeObject);
setActiveObject(null);
canvas.renderAll();
}
else {
canvas.remove(activeObject);
setActiveObject(null);
canvas.renderAll();
deleteMutate(imgUrl);
}
}
}, [canvas, setActiveObject, deleteMutate, id, projectData, projectUpdate]);
// duplicating current objects
const duplicating = () => {
// Clone the active object to create a true deep copy
activeObject.clone((clonedObject) => {
// Add the cloned object to the canvas
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 });
});
};
// for clear canvas
const clearCanvas = () => {
canvas.clear();
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"
? "space-x-4"
: "xl:grid-cols-3 lg:grid-cols-3 md:grid-cols-3"
}`}
>
{multipleObjects && (
<ActionButton
icon={<GroupIcon className="h-4 w-4" />}
label="Group"
onClick={groupSelectedObjects}
tooltipContent={
<div className="text-sm">
<p className="font-semibold mb-1">Group selected objects</p>
<p>To select multiple objects:</p>
<ol className="list-decimal list-inside mt-1">
<li>Hold down the Shift key</li>
<li>
Click and drag with the left mouse button to select
objects
</li>
<li>Release the Shift key and mouse button</li>
</ol>
<p className="mt-1">
Then click this button to group the selected objects.
</p>
</div>
}
/>
)}
{isGroupObject && (
<ActionButton
icon={<UngroupIcon className="h-4 w-4" />}
label="Ungroup"
onClick={ungroupSelectedObjects}
tooltipContent="Ungroup selected objects"
/>
)}
<ActionButton
icon={<CopyPlus className="h-4 w-4" />}
label="Duplicate"
onClick={duplicating}
tooltipContent="Duplicate selected objects"
/>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<Button variant="outline" size="md" className="w-full">
<div className="flex items-center gap-1 p-1">
<Layers className="h-4 w-4" />
<span className="text-[10px] font-bold">Position</span>
</div>
</Button>
</PopoverTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" align="center">
<p>Change object position</p>
</TooltipContent>
</Tooltip>
<PopoverContent className="w-40 p-2 z-[9999]">
<div className="flex flex-col gap-2">
<Button
variant="ghost"
size="sm"
onClick={bringToFront}
disabled={objectPosition.isAtFront}
>
<BringToFront className="h-4 w-4 mr-2" />
<span className="text-sm">Bring to Front</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={sendToBack}
disabled={objectPosition.isAtBack}
>
<SendToBack className="h-4 w-4 mr-2" />
<span className="text-sm">Send to Back</span>
</Button>
</div>
</PopoverContent>
</Popover>
<ActionButton
icon={<Trash2 className="h-4 w-4" />}
label="Remove"
onClick={removeSelected}
tooltipContent="Remove selected objects"
/>
<ActionButton
icon={<SquareX className="h-4 w-4" />}
label="Clear"
onClick={clearCanvas}
tooltipContent="Clear entire canvas"
/>
</div>
</TooltipProvider>
</div>
);
};
function ActionButton({ icon, label, onClick, tooltipContent }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="md"
className="w-full"
onClick={onClick}
>
<div className="flex items-center gap-1 p-1">
{icon}
<span className="text-[10px] font-bold">{label}</span>
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" className="max-w-xs z-[9999]">
<p>{tooltipContent}</p>
</TooltipContent>
</Tooltip>
);
}

View file

@ -0,0 +1,112 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom';
import { Toaster } from '../ui/toaster';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../ui/card';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import { HomeIcon, Loader2 } from 'lucide-react';
import { Button } from '../ui/button';
import { useToast } from '@/hooks/use-toast';
import { useMutation } from '@tanstack/react-query';
import { resetPassword } from '@/api/authApi';
const ForgotPassword = () => {
const navigate = useNavigate();
const getToken = () => localStorage.getItem('canvas_token');
useEffect(() => {
const token = getToken(); // Get latest token
if (token) {
navigate("/");
}
}, [navigate]);
const [formData, setFormData] = useState({
email: "",
})
const { toast } = useToast();
const { mutate: resetMutation, isPending } = useMutation({
mutationFn: async (formData) => {
return await resetPassword(formData)
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: "Reset Password",
description: data?.message || "Check your email for the reset link",
})
navigate("/login");
return
}
else {
toast({
variant: "destructive",
title: "Reset Password",
description: data?.message || "Something went wrong",
})
}
},
})
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}))
}
const handleSubmit = (e) => {
e.preventDefault();
resetMutation(formData);
}
return (
<div className="container relative min-h-screen flex items-center justify-center py-10">
<Toaster />
<Card className="w-full max-w-md">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold tracking-tight">Reset Password</CardTitle>
<CardDescription>Enter your email to reset your password.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="john.doe@example.com"
required
value={formData.email}
onChange={handleChange}
/>
</div>
<Button type="submit" className="w-full" disabled={isPending}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Send Email
</Button>
</form>
</CardContent>
<CardFooter className="flex flex-wrap items-center justify-center gap-1">
<span className="text-sm text-muted-foreground">Back to home?</span>
<p
onClick={() => navigate('/login')}
className="text-sm text-primary-text hover:text-primary underline-offset-4 hover:underline cursor-pointer"
>
<HomeIcon className="mr-1 h-4 w-4" />
</p>
</CardFooter>
</Card>
</div>
)
}
export default ForgotPassword

View file

@ -0,0 +1,176 @@
import { Eye, EyeOff, Loader2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useNavigate } from "react-router-dom"
import { useEffect, useState } from "react"
import { login } from "@/api/authApi"
import { useMutation } from "@tanstack/react-query"
import { useToast } from "@/hooks/use-toast"
import { Toaster } from "../ui/toaster"
const Login = () => {
const navigate = useNavigate();
const [verificationEmail, setVerificationEmail] = useState(false);
const getToken = () => localStorage.getItem('canvas_token');
useEffect(() => {
const token = getToken(); // Get latest token
if (token) {
navigate("/");
}
}, [navigate]);
const [showPassword, setShowPassword] = useState(false)
const [formData, setFormData] = useState({
email: "",
password: "",
})
const { toast } = useToast();
const { mutate: loginMutate, isPending } = useMutation({
mutationFn: async (formData) => {
return await login(formData)
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: "Login Successful",
description: data?.message || "You have successfully logged in.",
})
navigate("/");
}
else if (data?.status === 401) {
setVerificationEmail(true);
toast({
variant: "destructive",
title: "Verification Required",
description: data?.message || "Please verify your email to login.",
})
}
else {
toast({
variant: "destructive",
title: "Login Failed",
description: data?.message || "Something went wrong",
})
}
},
})
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}))
}
const handleSubmit = (e) => {
e.preventDefault();
loginMutate(formData);
}
return (
<div className="container relative min-h-screen flex items-center justify-center py-10 mx-auto">
<Toaster />
<Card className="w-full max-w-md mx-auto">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold tracking-tight">Welcome back</CardTitle>
<CardDescription>Enter your email and password to sign in to your account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="john.doe@example.com"
required
value={formData.email}
onChange={handleChange}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<p
onClick={() => navigate("/forgot-password")}
className="text-sm text-primary-text hover:text-primary underline-offset-4 hover:underline"
>
Forgot password?
</p>
</div>
<div className="relative">
<Input
id="password"
name="password"
type={showPassword ? "text" : "password"}
placeholder="Enter your password"
required
value={formData.password}
onChange={handleChange}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
<Button type="submit" className="w-full" disabled={isPending}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Sign in
</Button>
</form>
</CardContent>
<CardFooter className="flex flex-wrap items-center justify-center gap-1">
{
verificationEmail &&
<div className="flex flex-col items-center gap-1">
<span className="text-sm text-muted-foreground">Don&apos;t verify yet? or verification expired?</span>
<p
onClick={() => navigate('/resend-verification')}
className="text-sm text-primary-text hover:text-primary underline-offset-4 hover:underline cursor-pointer"
>
Resend verification email
</p>
</div>
}
{
!verificationEmail &&
<div className="flex items-center gap-1">
<span className="text-sm text-muted-foreground">Don&apos;t have an account?</span>
<p
onClick={() => navigate('/register')}
className="text-sm text-primary-text hover:text-primary underline-offset-4 hover:underline cursor-pointer"
>
Create an account
</p>
</div>
}
</CardFooter>
</Card>
</div>
)
}
export default Login;

View file

@ -0,0 +1,30 @@
import { Frown } from "lucide-react"
const NotFound = () => {
return (
<div className="min-h-screen flex items-center justify-center bg-background text-foreground p-4">
<div className="max-w-md w-full text-center">
<h1 className="text-9xl font-bold mb-4">404</h1>
<div className="relative">
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-9xl opacity-10">?</span>
</div>
<Frown className="mx-auto h-24 w-24 text-muted-foreground" />
</div>
<h2 className="text-2xl font-semibold mt-4 mb-2">Page Not Found</h2>
<p className="text-muted-foreground mb-8">Oops! The page you are looking for does not exist or has been moved.</p>
<a
href="/"
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

View file

@ -0,0 +1,182 @@
import { Eye, EyeOff, Loader2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useNavigate } from "react-router-dom"
import { useEffect, useState } from "react"
import { register } from "@/api/authApi"
import { useToast } from "@/hooks/use-toast"
import { useMutation } from "@tanstack/react-query"
const Register = () => {
const [showPassword, setShowPassword] = useState(false)
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
confirmPassword: "",
})
const [error, setError] = useState("")
const navigate = useNavigate();
const getToken = () => localStorage.getItem('canvas_token');
useEffect(() => {
const token = getToken(); // Get latest token
if (token) {
navigate("/");
}
}, [navigate]);
const { toast } = useToast();
const { mutate: registerMutate, isPending } = useMutation({
mutationFn: async (formData) => {
return await register(formData)
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: "Registration Successful",
description: data?.message,
})
navigate("/login");
}
else {
toast({
variant: "destructive",
title: "Registration Failed",
description: data?.message || "Something went wrong",
})
}
},
})
const handleChange = (e) => {
const { name, value } = e.target
setFormData((prev) => ({
...prev,
[name]: value,
}))
setError("")
}
const handleSubmit = (e) => {
e.preventDefault()
if (formData.password !== formData.confirmPassword) {
setError("Passwords do not match")
return
}
registerMutate(formData)
}
return (
<div className="container relative min-h-screen py-10 flex items-center justify-center mx-auto">
<Card className="w-full max-w-md mx-auto">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold tracking-tight">Create an account</CardTitle>
<CardDescription>Enter your information below to create your account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
name="name"
placeholder="John Doe"
required
minLength={2}
value={formData.name}
onChange={handleChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="john.doe@example.com"
required
value={formData.email}
onChange={handleChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<div className="relative">
<Input
id="password"
name="password"
type={showPassword ? "text" : "password"}
placeholder="Enter your password"
required
minLength={8}
value={formData.password}
onChange={handleChange}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">Confirm Password</Label>
<div className="relative">
<Input
id="confirmPassword"
name="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
placeholder="Confirm your password"
required
minLength={8}
value={formData.confirmPassword}
onChange={handleChange}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
{error && <p className="text-sm font-medium text-destructive">{error}</p>}
<Button type="submit" className="w-full" disabled={isPending}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Sign up
</Button>
</form>
</CardContent>
<CardFooter className="flex flex-wrap items-center justify-center gap-1">
<span className="text-sm text-muted-foreground">Already have an account?</span>
<p
onClick={() => navigate('/login')}
className="text-sm text-primary-text hover:text-primary underline-offset-4 hover:underline cursor-pointer"
>
Sign in
</p>
</CardFooter>
</Card>
</div>
)
}
export default Register;

View file

@ -0,0 +1,112 @@
import { useToast } from '@/hooks/use-toast';
import { useMutation } from '@tanstack/react-query';
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom';
import { Toaster } from '../ui/toaster';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../ui/card';
import { Label } from '../ui/label';
import { Input } from '../ui/input';
import { Button } from '../ui/button';
import { HomeIcon, Loader2 } from 'lucide-react';
import { resendVerificationEmail } from '@/api/authApi';
const ResentVerification = () => {
const navigate = useNavigate();
const getToken = () => localStorage.getItem('canvas_token');
useEffect(() => {
const token = getToken(); // Get latest token
if (token) {
navigate("/");
}
}, [navigate]);
const [formData, setFormData] = useState({
email: "",
})
const { toast } = useToast();
const { mutate: resendMutation, isPending } = useMutation({
mutationFn: async (formData) => {
return await resendVerificationEmail(formData)
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: "Verification Email Sent",
description: data?.message || "Verification email sent successfully",
})
navigate("/login");
return
}
else {
toast({
variant: "destructive",
title: "Resend verification email sending failed",
description: data?.message || "Something went wrong",
})
}
},
})
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}))
}
const handleSubmit = (e) => {
e.preventDefault();
resendMutation(formData);
}
return (
<div className="container relative min-h-screen flex items-center justify-center py-10">
<Toaster />
<Card className="w-full max-w-md">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl font-bold tracking-tight">Resend Verification Email</CardTitle>
<CardDescription>Enter your email to resend verification email.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="john.doe@example.com"
required
value={formData.email}
onChange={handleChange}
/>
</div>
<Button type="submit" className="w-full" disabled={isPending}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Send Email
</Button>
</form>
</CardContent>
<CardFooter className="flex flex-wrap items-center justify-center gap-1">
<span className="text-sm text-muted-foreground">Back to home?</span>
<p
onClick={() => navigate('/login')}
className="text-sm text-primary-text hover:text-primary underline-offset-4 hover:underline cursor-pointer"
>
<HomeIcon className="mr-1 h-4 w-4" />
</p>
</CardFooter>
</Card>
</div>
)
}
export default ResentVerification

View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import CanvasSetting from "../CanvasSetting";
const CanvasPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Canvas Settings</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">
<CanvasSetting />
</ScrollArea>
</div>
);
};
export default CanvasPanel;

View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import ApplyColor from "../EachComponent/ApplyColor";
import { Button } from "../ui/button";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { X } from "lucide-react";
import { ScrollArea } from "../ui/scroll-area";
const ColorPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Color</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">
<ApplyColor />
</ScrollArea>
</div>
);
};
export default ColorPanel;

View file

@ -0,0 +1,36 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import SkewCustomization from "../EachComponent/Customization/SkewCustomization";
import ScaleObjects from "../EachComponent/Customization/ScaleObjects";
import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape";
import ApplyColor from "../EachComponent/ApplyColor";
const CommonPanel = () => {
const { canvas } = useContext(CanvasContext);
const activeObject = canvas?.getActiveObject();
const activeObjectType = activeObject?.type;
const hasClipPath = !!activeObject?.clipPath;
const customClipPath = activeObject?.isClipPath;
return (
<div>
<div className="space-y-5">
{/* Apply fill and background color */}
{activeObjectType !== "image" && !hasClipPath && !customClipPath && (
<ApplyColor />
)}
{/* Skew Customization */}
<SkewCustomization />
{/* Scale Objects */}
<ScaleObjects />
{/* Add image into shape */}
<AddImageIntoShape />
</div>
</div>
);
};
export default CommonPanel;

View file

@ -0,0 +1,32 @@
import { useContext } from 'react'
import CanvasContext from '../Context/canvasContext/CanvasContext';
import { Button } from '../ui/button';
import { ScrollArea } from '../ui/scroll-area';
import { Ellipsis, X } from 'lucide-react';
const DesignPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Design Panel</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
<div className='flex items-center gap-2'>
<p >Coming soon </p>
<Ellipsis className='animate-pulse animate-infinite' />
</div>
</ScrollArea>
</div>
);
}
export default DesignPanel

View file

@ -0,0 +1,70 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import TextPanel from "./TextPanel";
import ColorPanel from "./ColorPanel";
import ShapePanel from "./ShapePanel";
import IconPanel from "./IconPanel";
import UploadPanel from "./UploadPanel";
import StrokePanel from "./StrokePanel";
import ShadowPanel from "./ShadowPanel";
import FlipPanel from "./FlipPanel";
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);
const renderPanel = () => {
switch (selectedPanel) {
case "text":
return <TextPanel />;
case "shape":
return <ShapePanel />;
case "icon":
return <IconPanel />;
case "upload":
return <UploadPanel />;
case "color":
return <ColorPanel />;
case "stroke":
return <StrokePanel />;
case "shadow":
return <ShadowPanel />;
case "flip":
return <FlipPanel />;
case "position":
return <PositionPanel />;
case "image-insert":
return <ImagePanel />;
case "group-obj":
return <GroupObjectPanel />;
case "canvas":
return <CanvasPanel />;
case "project":
return <ProjectPanel />;
case "image":
return <ImageLibrary />;
case "design":
return <DesignPanel />;
default:
return;
}
};
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">
{renderPanel()}
</div>
)}
</>
);
};
export default EditorPanel;

View file

@ -0,0 +1,39 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import { Card } from "../ui/card";
import CollapsibleComponent from "../EachComponent/Customization/CollapsibleComponent";
import FlipCustomization from "../EachComponent/Customization/FlipCustomization";
import RotateCustomization from "../EachComponent/Customization/RotateCustomization";
const FlipPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Flip & Rotate</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">
<Card className="shadow-none border-0">
<CollapsibleComponent text={"Flip, Rotate Control"}>
<div className="space-y-2">
<FlipCustomization />
<RotateCustomization />
</div>
</CollapsibleComponent>
</Card>
</ScrollArea>
</div>
);
};
export default FlipPanel;

View file

@ -0,0 +1,36 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import SelectObjectFromGroup from "../EachComponent/Customization/SelectObjectFromGroup";
import { X } from "lucide-react";
import SkewCustomization from "../EachComponent/Customization/SkewCustomization";
import ScaleObjects from "../EachComponent/Customization/ScaleObjects";
const GroupObjectPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Group Object</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">
<SelectObjectFromGroup />
{/* Skew Customization */}
<SkewCustomization />
{/* Scale Objects */}
<ScaleObjects />
</ScrollArea>
</div>
);
};
export default GroupObjectPanel;

View file

@ -0,0 +1,55 @@
import { useContext, useEffect, useRef } from "react";
import { Button } from "../ui/button";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { X } from "lucide-react";
import { ScrollArea } from "../ui/scroll-area";
import AllIconsPage from "../EachComponent/Icons/AllIcons";
import { useParams } from "react-router-dom";
import useProject from "@/hooks/useProject";
const IconPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
const params = useParams();
const { id } = params;
const { createEmptyProject } = useProject();
function isUUID(value) {
if (typeof value !== "string") return false;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(value);
}
const hasCreatedProject = useRef(false);
useEffect(() => {
if (!id && !hasCreatedProject.current) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
if (id && !isUUID(id)) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
}, [id, createEmptyProject]);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Icons</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">
<AllIconsPage />
</ScrollArea>
</div>
);
};
export default IconPanel;

View file

@ -0,0 +1,141 @@
import { useContext, useEffect, useRef, useState } from 'react'
import CanvasContext from '../Context/canvasContext/CanvasContext';
import { Button } from '../ui/button';
import { LoaderIcon, X } from 'lucide-react';
import { ScrollArea } from '../ui/scroll-area';
import { getPhotos } from '@/api/photoLibrary';
import { useQuery } from '@tanstack/react-query';
import { Input } from '../ui/input';
import { fabric } from 'fabric';
import ActiveObjectContext from '../Context/activeObject/ObjectContext';
import useProject from '@/hooks/useProject';
import { useParams } from 'react-router-dom';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Separator } from '../ui/separator';
const ImageLibrary = () => {
const { setSelectedPanel } = useContext(CanvasContext);
const params = useParams();
const { id } = params;
const { projectData, projectUpdate, createEmptyProject } = useProject();
const hasCreatedProject = useRef(false);
function isUUID(value) {
if (typeof value !== "string") return false;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(value);
}
useEffect(() => {
if (!id && !hasCreatedProject.current) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
if (id && !isUUID(id)) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
}, [id, createEmptyProject]);
const { canvas } = useContext(CanvasContext);
const { setActiveObject } = useContext(ActiveObjectContext);
const [query, setQuery] = useState({
keyword: "nature",
per_page: 10,
});
const { data, isLoading, isSuccess, isPending, refetch } = useQuery({
queryKey: ['photos', query.keyword, query.per_page],
queryFn: async () => await getPhotos(query),
});
const handleSubmit = (e) => {
e.preventDefault();
refetch();
};
const handleImage = async (photo) => {
if (projectData?.data && photo) {
fabric.Image.fromURL(
photo,
(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);
setActiveObject(img);
// Update the active object state
projectUpdate({ id, updateData: { ...projectData?.data, object: canvas.toJSON(['id', 'selectable']), preview_url: "" } });
canvas.renderAll();
},
{ crossOrigin: "anonymous" }
);
}
}
return (
<div className='relative'>
<div className='sticky top-0 bg-red-50 z-[999] border-b'>
<div className="flex justify-between items-center px-4 py-2">
<h2 className="text-lg font-semibold">Image Library</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<div className='p-1'>
{/* Search input */}
<form onSubmit={handleSubmit} className='flex items-center gap-1'>
<Input value={query.keyword} placeholder="Enter search keyword" onChange={(e) => {
setQuery({ ...query, keyword: e.target.value })
}} />
<Button type='submit'>
Search
</Button>
</form>
<Separator className='my-2' />
<div className='mt-2 flex items-center justify-between font-bold'>
{/* image quantity */}
<Select onValueChange={(value) => { setQuery({ ...query, per_page: value }) }}>
<SelectTrigger className="w-[fit]">
<SelectValue placeholder="Quantity" />
</SelectTrigger>
<SelectContent className="font-bold">
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="30">30</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{
isLoading && !isSuccess && isPending && <LoaderIcon className='h-5 w-5 mx-auto my-4 animate-spin' />
}
<ScrollArea className="md:h-[calc(90vh-190px)] px-2 py-2">
{/* Image library content goes here */}
<div className='grid grid-cols-2 items-center gap-2'>
{
data?.photos?.map((photo, i) => {
return <img key={i} src={photo?.src?.large} alt={photo?.alt} onClick={() => handleImage(photo?.src?.large)} className='cursor-pointer rounded-md' />
})
}
</div>
</ScrollArea>
</div>
);
}
export default ImageLibrary

View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import AddImageIntoShape from "../EachComponent/Customization/AddImageIntoShape";
const ImagePanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Image</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">
<AddImageIntoShape />
</ScrollArea>
</div>
);
};
export default ImagePanel;

View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import PositionCustomization from "../EachComponent/Customization/PositionCustomization";
const PositionPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Position Controller</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">
<PositionCustomization />
</ScrollArea>
</div>
);
};
export default PositionPanel;

View file

@ -0,0 +1,102 @@
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}`);
}
}
const filteredProjects = projects?.data;
filteredProjects?.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
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>
{
!project?.preview_url ? <p className='font-bold text-xs mx-auto'>Unsaved Project</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>
);
};

View file

@ -0,0 +1,29 @@
import { useContext } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
import ShadowCustomization from "../EachComponent/Customization/ShadowCustomization";
const ShadowPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Shadow Color</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">
<ShadowCustomization />
</ScrollArea>
</div>
);
};
export default ShadowPanel;

View file

@ -0,0 +1,91 @@
import { useContext, useEffect, useRef, useState } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { X } from "lucide-react";
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 [shapes, setShapes] = useState("custom")
const params = useParams();
const { id } = params;
const { createEmptyProject } = useProject();
const hasCreatedProject = useRef(false);
function isUUID(value) {
if (typeof value !== "string") return false;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(value);
}
useEffect(() => {
if (!id && !hasCreatedProject.current) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
if (id && !isUUID(id)) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
}, [id, createEmptyProject]);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Shape</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="flex justify-between items-center p-2 border-b">
<Button variant={`${shapes === "custom" ? "default" : "outline"}`} onClick={() => setShapes("custom")}>Custom </Button>
<Button variant={`${shapes === "rounded" ? "default" : "outline"}`} onClick={() => setShapes("rounded")}>Rounded </Button>
<Button variant={`${shapes === "plain" ? "default" : "outline"}`} onClick={() => setShapes("plain")}>Plain </Button>
</div>
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
<div className="space-y-4">
{
shapes === "custom" &&
<div>
<h2 className="font-semibold text-sm">Custom Shapes</h2>
<Separator className="my-2" />
<CustomShape />
</div>
}
{
shapes === "rounded" &&
<div>
<RoundedShape />
</div>
}
{
shapes === "plain" &&
<div>
<PlainShapes />
</div>
}
</div>
</ScrollArea>
</div>
);
};
export default ShapePanel;

View file

@ -0,0 +1,29 @@
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 StrokeCustomization from "../EachComponent/Customization/StrokeCustomization";
const StrokePanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Stroke Color</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">
<StrokeCustomization />
</ScrollArea>
</div>
);
};
export default StrokePanel;

View file

@ -0,0 +1,132 @@
import { Button } from "../ui/Button";
import { X } from "lucide-react";
import { ScrollArea } from "../ui/scroll-area";
import { useContext, useEffect, useRef, useState } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
import { fabric } from "fabric";
import CommonPanel from "./CommonPanel";
import useProject from "@/hooks/useProject";
import { useParams } from "react-router-dom";
export default function TextPanel() {
const { canvas, setSelectedPanel } = useContext(CanvasContext);
const { activeObject, setActiveObject } = useContext(ActiveObjectContext);
const params = useParams();
const { id } = params;
const { createEmptyProject, projectData, projectUpdate } = useProject();
const hasCreatedProject = useRef(false);
function isUUID(value) {
if (typeof value !== "string") return false;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(value);
}
useEffect(() => {
if (!id && !hasCreatedProject.current) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
if (id && !isUUID(id)) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
}, [id, createEmptyProject]);
const [open, setOpen] = useState(false);
useEffect(() => {
if (activeObject) {
setOpen(true);
} else {
setOpen(false);
}
}, [activeObject]);
const addText = () => {
if (canvas) {
const text = new fabric.IText("Editable Text", {
left: 100,
top: 100,
fontFamily: "Poppins",
fontSize: 16,
stroke: "", // empty string for no stroke color
strokeWidth: 0, // set stroke width to 0
});
// Add the text to the canvas and re-render
canvas.add(text);
// canvas.clipPath = text;
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 });
}
};
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Text</h2>
<Button
variant="ghost"
size="icon"
onClick={() => setSelectedPanel("")}
>
<X className="h-4 w-4" />
</Button>
</div>
<ScrollArea className="md:h-[calc(90vh-195px)] px-4 py-4">
<Button
className="w-full bg-[#FF2B85] hover:bg-[#FF2B85] text-white rounded-[10px] mb-6 h-12 font-medium text-xl"
onClick={() => {
addText();
}}
>
Add Text
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<path
d="M7.5 18.3333H12.5C16.6667 18.3333 18.3333 16.6666 18.3333 12.5V7.49996C18.3333 3.33329 16.6667 1.66663 12.5 1.66663H7.5C3.33333 1.66663 1.66667 3.33329 1.66667 7.49996V12.5C1.66667 16.6666 3.33333 18.3333 7.5 18.3333Z"
stroke="white"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5.83333 7.40837C8.45833 6.10004 11.5417 6.10004 14.1667 7.40837"
stroke="white"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M10 13.5833V6.60828"
stroke="white"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Button>
{!open ? (
<p className="text-sm font-semibold text-center">
No active object found
</p>
) : (
<div className="space-y-4">
<CommonPanel />
</div>
)}
</ScrollArea>
</div>
);
}

View file

@ -0,0 +1,146 @@
import { useContext, useRef, useState } from "react";
import TextCustomization from "../EachComponent/Customization/TextCustomization";
import LockObject from "../EachComponent/Customization/LockObject";
import OpacityCustomization from "../EachComponent/Customization/OpacityCustomization";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { ObjectShortcut } from "../ObjectShortcut";
import ActiveObjectContext from "../Context/activeObject/ObjectContext";
import { Button } from "../ui/button";
import { ImagePlus, ChevronLeft, ChevronRight } from "lucide-react";
import { RxBorderWidth } from "react-icons/rx";
import { LuFlipVertical } from "react-icons/lu";
import { SlTarget } from "react-icons/sl";
import { RiShadowLine } from "react-icons/ri";
import { FaRegObjectGroup } from "react-icons/fa";
import { Tooltip } from "react-tooltip";
export function TopBar() {
const { activeObject } = useContext(ActiveObjectContext);
const { selectedPanel, setSelectedPanel, textColor } =
useContext(CanvasContext);
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">
<TextCustomization />
</div>
<div className="flex items-center gap-4 px-2">
<Button
variant="outline"
className="flex items-center gap-2 border-dashed border-2 rounded-md hover:bg-gray-50"
onClick={() => setSelectedPanel("image-insert")}
>
<ImagePlus className="w-5 h-5" />
<span>Add image</span>
</Button>
{/* Canvas settings */}
<div>
<a data-tooltip-id="canvas">
<Button
variant="ghost"
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>
</a>
<Tooltip id="canvas" content="Canvas Settings" place="bottom" />
</div>
<div className="flex items-center gap-2">
{activeObjectType !== "image" &&
activeObject?.type !== "i-text" &&
!hasClipPath &&
!customClipPath && (
<a data-tooltip-id="color-gr">
<Button
variant="ghost"
size="icon"
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")}>
<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")}>
<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")}>
<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")}>
<SlTarget />
</Button>
</a>
)}
<Tooltip id="position" content="Object position" place="bottom" />
<a data-tooltip-id="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" />
</div>
<div className="ml-4">
<LockObject />
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,55 @@
import { useContext, useEffect, useRef } from "react";
import CanvasContext from "../Context/canvasContext/CanvasContext";
import { X } from "lucide-react";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import UploadImage from "../EachComponent/UploadImage";
import { useParams } from "react-router-dom";
import useProject from "@/hooks/useProject";
const UploadPanel = () => {
const { setSelectedPanel } = useContext(CanvasContext);
const params = useParams();
const { id } = params;
const { createEmptyProject } = useProject();
const hasCreatedProject = useRef(false);
function isUUID(value) {
if (typeof value !== "string") return false;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return uuidRegex.test(value);
}
useEffect(() => {
if (!id && !hasCreatedProject.current) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
if (id && !isUUID(id)) {
createEmptyProject();
hasCreatedProject.current = true; // Prevent further calls
}
}, [id, createEmptyProject]);
return (
<div>
<div className="flex justify-between items-center p-4 border-b">
<h2 className="text-lg font-semibold">Upload</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">
<UploadImage />
</ScrollArea>
</div>
);
};
export default UploadPanel;

View file

@ -0,0 +1,184 @@
import { createCategory, getAllCategory } from '@/api/categoryApi'
import { useToast } from '@/hooks/use-toast';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Label } from './ui/label';
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from './ui/select';
import { Loader2, Plus } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Dialog } from '@/components/ui/dialog';
import { DialogContent } from '@/components/ui/dialog';
import { DialogHeader } from '@/components/ui/dialog';
import { DialogTitle } from '@/components/ui/dialog';
import { DialogDescription } from '@/components/ui/dialog';
import { DialogFooter } from '@/components/ui/dialog';
import PropTypes from "prop-types";
const ProjectCategory = ({ value }) => {
const { toast } = useToast();
const { saveCanvas, setSaveCanvas } = value;
const [newCategoryName, setNewCategoryName] = useState("");
const [selectedCategory, setSelectedCategory] = useState("");
useEffect(() => {
if (saveCanvas?.category) {
setSelectedCategory(saveCanvas?.category);
}
}, [saveCanvas])
const [isDialogOpen, setIsDialogOpen] = useState(false);
const queryClient = useQueryClient();
// to get all category data
const { data: categories, isLoading: categoryLoading } = useQuery({
queryKey: ['projectCategory'],
queryFn: async () => {
return await getAllCategory();
}
});
// to create category
const { mutate: createCategoryMutation, isPending: createCategoryPending } = useMutation({
mutationFn: async (name) => {
return await createCategory(name);
},
onSuccess: (data) => {
if (data?.status === 200) {
toast({
title: 'Success!',
description: `${data?.message || 'Category created successfully'}`,
});
setIsDialogOpen(false);
setNewCategoryName("");
queryClient.invalidateQueries({ queryKey: ['projectCategory'] });
}
else {
toast({
title: 'Error!',
description: `${data?.message || 'Something went wrong'}`,
variant: 'destructive',
})
}
}
})
const handleCreateCategory = (e) => {
e.preventDefault()
if (!newCategoryName.trim()) {
toast({
title: "Error!",
description: "Category name cannot be empty",
variant: "destructive",
})
return
}
createCategoryMutation(newCategoryName)
};
const handleSelectChange = (value) => {
if (value === "add-new") {
setIsDialogOpen(true)
} else {
setSelectedCategory(value)
setSaveCanvas((prev) => ({
...prev,
category: value,
}));
}
};
return (
<div className="w-full max-w-md mx-auto">
{
categoryLoading ? <p><Loader2 className="animate-spin mx-auto my-4" /></p> :
<>
<Card className="p-0">
<CardHeader className="px-3 py-2">
<CardTitle>Project Category</CardTitle>
<CardDescription>Select a category for your project</CardDescription>
</CardHeader>
<CardContent className="px-3 py-1 mb-4">
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<Select value={selectedCategory} onValueChange={handleSelectChange}>
<SelectTrigger id="category" className="w-full">
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent className="max-h-60 overflow-y-auto">
<SelectGroup>
<SelectLabel>Available Categories</SelectLabel>
<hr className='mb-2' />
{categories?.data?.map((category) => (
<SelectItem key={category.id} value={category.id} className="font-medium truncate flex border mb-1">
{category.name}
</SelectItem>
))}
<hr className='mt-2' />
<SelectItem value="add-new" className="text-primary font-medium">
<span className="flex items-center">
<Plus className="mr-2 h-4 w-4" />
Add New Category
</span>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Category</DialogTitle>
<DialogDescription>Add a new category for your projects.</DialogDescription>
</DialogHeader>
<form onSubmit={handleCreateCategory}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Category Name</Label>
<Input
id="name"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
placeholder="Enter category name"
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setIsDialogOpen(false)}>
Cancel
</Button>
<Button type="submit" disabled={createCategoryPending}>
{createCategoryPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
"Create Category"
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
}
</div>
)
}
ProjectCategory.propTypes = {
value: PropTypes.shape({
saveCanvas: PropTypes.object.isRequired,
setSaveCanvas: PropTypes.func.isRequired,
}).isRequired
}
export default ProjectCategory

View file

@ -0,0 +1,142 @@
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';
import ProjectCategory from './ProjectCategory';
const SaveCanvas = () => {
const { canvas } = useContext(CanvasContext);
const [saveCanvas, setSaveCanvas] = useState({
name: "",
description: "",
preview_url: "",
category: "",
});
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,
category: projectData?.data?.category_id,
}));
}
}, [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' />
<ProjectCategory value={{ saveCanvas, setSaveCanvas }} />
<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 || saveCanvas?.name === "" || saveCanvas?.category === ""} onClick={handleSaveProject}>Save</Button>
</div>
</div>
)
}
export default SaveCanvas

View file

@ -0,0 +1,47 @@
import * as React from "react"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props} />
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props} />
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props} />
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View file

@ -0,0 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

View file

@ -0,0 +1,34 @@
import * as React from "react"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
...props
}) {
return (<div className={cn(badgeVariants({ variant }), className)} {...props} />);
}
export { Badge, badgeVariants }

View file

@ -0,0 +1,48 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
(<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} />)
);
})
Button.displayName = "Button"
export { Button, buttonVariants }

View file

@ -0,0 +1,50 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
{...props} />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} />
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props} />
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props} />
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View file

@ -0,0 +1,22 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View file

@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View file

@ -0,0 +1,94 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-[9999] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props} />
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-[9999] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}>
{children}
<DialogPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}) => (
<div
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} />
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props} />
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View file

@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
(<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground 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}
{...props} />)
);
})
Input.displayName = "Input"
export { Input }

View file

@ -0,0 +1,21 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva } from "class-variance-authority";
import PropTypes from 'prop-types';
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
Label.propTypes = {
className: PropTypes.string,
};
export { Label }

Some files were not shown because too many files have changed in this diff Show more