all requirement added
This commit is contained in:
parent
58376217d3
commit
6b29e136f4
124 changed files with 21573 additions and 0 deletions
1
.env
Normal file
1
.env
Normal file
|
|
@ -0,0 +1 @@
|
|||
VITE_SERVER_URL=http://localhost:3000/api
|
||||
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal 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
BIN
bun.lockb
Normal file
Binary file not shown.
21
components.json
Normal file
21
components.json
Normal 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
3
env.examples
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
all these env's are required to run the project
|
||||
|
||||
VITE_SERVER_URL=
|
||||
38
eslint.config.js
Normal file
38
eslint.config.js
Normal 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
13
index.html
Normal 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
10
jsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
10630
package-lock.json
generated
Normal file
10630
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
70
package.json
Normal file
70
package.json
Normal 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
6
postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
public/assets/PlanPost AI_Logo.png
Normal file
BIN
public/assets/PlanPost AI_Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
13
src/App.css
Normal file
13
src/App.css
Normal 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
85
src/App.jsx
Normal 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
54
src/ErrorBoundary.jsx
Normal 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
125
src/Home.jsx
Normal 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
126
src/api/authApi.ts
Normal 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
74
src/api/categoryApi.ts
Normal 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
28
src/api/photoLibrary.ts
Normal 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
142
src/api/projectApi.ts
Normal 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
55
src/api/uploadApi.ts
Normal 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
72
src/api/uploadShapeApi.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
BIN
src/assets/PlanPost AI_Logo.png
Normal file
BIN
src/assets/PlanPost AI_Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal 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 |
151
src/components/ActionButtons.jsx
Normal file
151
src/components/ActionButtons.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
298
src/components/Canvas/Canvas.jsx
Normal file
298
src/components/Canvas/Canvas.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
149
src/components/CanvasCapture.jsx
Normal file
149
src/components/CanvasCapture.jsx
Normal 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;
|
||||
409
src/components/CanvasSetting.jsx
Normal file
409
src/components/CanvasSetting.jsx
Normal 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;
|
||||
244
src/components/ColorComponent.jsx
Normal file
244
src/components/ColorComponent.jsx
Normal 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
|
||||
|
||||
5
src/components/Context/activeObject/ObjectContext.js
Normal file
5
src/components/Context/activeObject/ObjectContext.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
const ActiveObjectContext = React.createContext();
|
||||
|
||||
export default ActiveObjectContext;
|
||||
14
src/components/Context/activeObject/ObjectProvider.jsx
Normal file
14
src/components/Context/activeObject/ObjectProvider.jsx
Normal 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;
|
||||
5
src/components/Context/authContext/AuthContext.js
Normal file
5
src/components/Context/authContext/AuthContext.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import react from "react";
|
||||
|
||||
const AuthContext = react.createContext();
|
||||
|
||||
export default AuthContext;
|
||||
14
src/components/Context/authContext/AuthProvider.jsx
Normal file
14
src/components/Context/authContext/AuthProvider.jsx
Normal 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;
|
||||
5
src/components/Context/canvasContext/CanvasContext.js
Normal file
5
src/components/Context/canvasContext/CanvasContext.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
const CanvasContext = React.createContext();
|
||||
|
||||
export default CanvasContext;
|
||||
|
|
@ -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;
|
||||
5
src/components/Context/colorContext/ColorContext.js
Normal file
5
src/components/Context/colorContext/ColorContext.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
const ColorContext = React.createContext();
|
||||
|
||||
export default ColorContext;
|
||||
22
src/components/Context/colorContext/ColorContextProvider.jsx
Normal file
22
src/components/Context/colorContext/ColorContextProvider.jsx
Normal 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
|
||||
5
src/components/Context/openContext/OpenContext.js
Normal file
5
src/components/Context/openContext/OpenContext.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
|
||||
const OpenContext = React.createContext();
|
||||
|
||||
export default OpenContext;
|
||||
19
src/components/Context/openContext/OpenContextProvider.jsx
Normal file
19
src/components/Context/openContext/OpenContextProvider.jsx
Normal 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
|
||||
358
src/components/EachComponent/ApplyColor.jsx
Normal file
358
src/components/EachComponent/ApplyColor.jsx
Normal 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;
|
||||
173
src/components/EachComponent/CustomShape/CustomShape.jsx
Normal file
173
src/components/EachComponent/CustomShape/CustomShape.jsx
Normal 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;
|
||||
153
src/components/EachComponent/CustomShape/UploadShapes.jsx
Normal file
153
src/components/EachComponent/CustomShape/UploadShapes.jsx
Normal 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;
|
||||
|
||||
424
src/components/EachComponent/Customization/AddImageIntoShape.jsx
Normal file
424
src/components/EachComponent/Customization/AddImageIntoShape.jsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
75
src/components/EachComponent/Customization/LockObject.jsx
Normal file
75
src/components/EachComponent/Customization/LockObject.jsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
94
src/components/EachComponent/Customization/ScaleObjects.jsx
Normal file
94
src/components/EachComponent/Customization/ScaleObjects.jsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
463
src/components/EachComponent/Customization/TextCustomization.jsx
Normal file
463
src/components/EachComponent/Customization/TextCustomization.jsx
Normal 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;
|
||||
151
src/components/EachComponent/Icons/AllIcons.jsx
Normal file
151
src/components/EachComponent/Icons/AllIcons.jsx
Normal 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;
|
||||
103
src/components/EachComponent/RoundedShapes/RoundedShape.jsx
Normal file
103
src/components/EachComponent/RoundedShapes/RoundedShape.jsx
Normal 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;
|
||||
334
src/components/EachComponent/Shapes/PlainShapes.jsx
Normal file
334
src/components/EachComponent/Shapes/PlainShapes.jsx
Normal 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;
|
||||
339
src/components/EachComponent/UploadImage.jsx
Normal file
339
src/components/EachComponent/UploadImage.jsx
Normal 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;
|
||||
237
src/components/EditPanel.jsx
Normal file
237
src/components/EditPanel.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
69
src/components/Layouts/AddShapes.jsx
Normal file
69
src/components/Layouts/AddShapes.jsx
Normal 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
|
||||
48
src/components/Layouts/LeftSidebar.jsx
Normal file
48
src/components/Layouts/LeftSidebar.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/components/Layouts/RndComponent.jsx
Normal file
28
src/components/Layouts/RndComponent.jsx
Normal 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
|
||||
403
src/components/ObjectShortcut.jsx
Normal file
403
src/components/ObjectShortcut.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
112
src/components/Pages/ForgotPassword.jsx
Normal file
112
src/components/Pages/ForgotPassword.jsx
Normal 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
|
||||
176
src/components/Pages/Login.jsx
Normal file
176
src/components/Pages/Login.jsx
Normal 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'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'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;
|
||||
|
||||
30
src/components/Pages/NotFound.jsx
Normal file
30
src/components/Pages/NotFound.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { Frown } from "lucide-react"
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background text-foreground p-4">
|
||||
<div className="max-w-md w-full text-center">
|
||||
<h1 className="text-9xl font-bold mb-4">404</h1>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-9xl opacity-10">?</span>
|
||||
</div>
|
||||
<Frown className="mx-auto h-24 w-24 text-muted-foreground" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold mt-4 mb-2">Page Not Found</h2>
|
||||
<p className="text-muted-foreground mb-8">Oops! The page you are looking for does not exist or has been moved.</p>
|
||||
<a
|
||||
href="/"
|
||||
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
|
||||
|
||||
182
src/components/Pages/Register.jsx
Normal file
182
src/components/Pages/Register.jsx
Normal 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;
|
||||
|
||||
112
src/components/Pages/ResendVerification.jsx
Normal file
112
src/components/Pages/ResendVerification.jsx
Normal 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
|
||||
29
src/components/Panel/CanvasPanel.jsx
Normal file
29
src/components/Panel/CanvasPanel.jsx
Normal 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;
|
||||
29
src/components/Panel/ColorPanel.jsx
Normal file
29
src/components/Panel/ColorPanel.jsx
Normal 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;
|
||||
36
src/components/Panel/CommonPanel.jsx
Normal file
36
src/components/Panel/CommonPanel.jsx
Normal 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;
|
||||
32
src/components/Panel/DesignPanel.jsx
Normal file
32
src/components/Panel/DesignPanel.jsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { useContext } from 'react'
|
||||
import CanvasContext from '../Context/canvasContext/CanvasContext';
|
||||
import { Button } from '../ui/button';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { Ellipsis, X } from 'lucide-react';
|
||||
|
||||
const DesignPanel = () => {
|
||||
const { setSelectedPanel } = useContext(CanvasContext);
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">Design Panel</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setSelectedPanel("")}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="md:h-[calc(90vh-190px)] px-4 py-4">
|
||||
<div className='flex items-center gap-2'>
|
||||
<p >Coming soon </p>
|
||||
<Ellipsis className='animate-pulse animate-infinite' />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DesignPanel
|
||||
70
src/components/Panel/EditorPanel.jsx
Normal file
70
src/components/Panel/EditorPanel.jsx
Normal 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;
|
||||
39
src/components/Panel/FlipPanel.jsx
Normal file
39
src/components/Panel/FlipPanel.jsx
Normal 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;
|
||||
36
src/components/Panel/GroupObjectPanel.jsx
Normal file
36
src/components/Panel/GroupObjectPanel.jsx
Normal 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;
|
||||
55
src/components/Panel/IconPanel.jsx
Normal file
55
src/components/Panel/IconPanel.jsx
Normal 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;
|
||||
141
src/components/Panel/ImageLibrary.jsx
Normal file
141
src/components/Panel/ImageLibrary.jsx
Normal 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
|
||||
29
src/components/Panel/ImagePanel.jsx
Normal file
29
src/components/Panel/ImagePanel.jsx
Normal 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;
|
||||
29
src/components/Panel/PositionPanel.jsx
Normal file
29
src/components/Panel/PositionPanel.jsx
Normal 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;
|
||||
102
src/components/Panel/ProjectPanel.jsx
Normal file
102
src/components/Panel/ProjectPanel.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
29
src/components/Panel/ShadowPanel.jsx
Normal file
29
src/components/Panel/ShadowPanel.jsx
Normal 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;
|
||||
91
src/components/Panel/ShapePanel.jsx
Normal file
91
src/components/Panel/ShapePanel.jsx
Normal 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;
|
||||
29
src/components/Panel/StrokePanel.jsx
Normal file
29
src/components/Panel/StrokePanel.jsx
Normal 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;
|
||||
132
src/components/Panel/TextPanel.jsx
Normal file
132
src/components/Panel/TextPanel.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
146
src/components/Panel/TopBar.jsx
Normal file
146
src/components/Panel/TopBar.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
55
src/components/Panel/UploadPanel.jsx
Normal file
55
src/components/Panel/UploadPanel.jsx
Normal 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;
|
||||
184
src/components/ProjectCategory.jsx
Normal file
184
src/components/ProjectCategory.jsx
Normal 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
|
||||
142
src/components/SaveCanvas.jsx
Normal file
142
src/components/SaveCanvas.jsx
Normal 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
|
||||
47
src/components/ui/alert.jsx
Normal file
47
src/components/ui/alert.jsx
Normal 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 }
|
||||
5
src/components/ui/aspect-ratio.jsx
Normal file
5
src/components/ui/aspect-ratio.jsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root
|
||||
|
||||
export { AspectRatio }
|
||||
34
src/components/ui/badge.jsx
Normal file
34
src/components/ui/badge.jsx
Normal 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 }
|
||||
48
src/components/ui/button.jsx
Normal file
48
src/components/ui/button.jsx
Normal 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 }
|
||||
50
src/components/ui/card.jsx
Normal file
50
src/components/ui/card.jsx
Normal 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 }
|
||||
22
src/components/ui/checkbox.jsx
Normal file
22
src/components/ui/checkbox.jsx
Normal 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 }
|
||||
9
src/components/ui/collapsible.jsx
Normal file
9
src/components/ui/collapsible.jsx
Normal 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 }
|
||||
94
src/components/ui/dialog.jsx
Normal file
94
src/components/ui/dialog.jsx
Normal 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,
|
||||
}
|
||||
19
src/components/ui/input.jsx
Normal file
19
src/components/ui/input.jsx
Normal 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 }
|
||||
21
src/components/ui/label.jsx
Normal file
21
src/components/ui/label.jsx
Normal 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
Loading…
Add table
Reference in a new issue