SME Web - 27-06-2025
This commit is contained in:
commit
11ebd9e1fb
|
|
@ -0,0 +1,3 @@
|
||||||
|
# VITE_BASE_URI="https://officially-powerful-goat.ngrok-free.app"
|
||||||
|
# VITE_BASE_URI="https://python-amused-adder.ngrok-free.app"
|
||||||
|
VITE_BASE_URI="https://concrete-gladly-mullet.ngrok-free.app"
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||||
|
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
// Set the react version
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
// Add the react plugin
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended rules
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"LOGIN": {
|
||||||
|
"path": "/api/auth/login",
|
||||||
|
"method": "POST",
|
||||||
|
"expecte_success_status_code": 200,
|
||||||
|
"expected_error_status_code": 401,
|
||||||
|
"default_error_message": "Internal server error"
|
||||||
|
},
|
||||||
|
"AUTH": {
|
||||||
|
"path": "/api/auth",
|
||||||
|
"sub": {
|
||||||
|
"LOGIN":{
|
||||||
|
"path": "/login"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CREATE_TRANSACTION": {
|
||||||
|
"path": "/api/account/${var1}/transactions/${var2}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"name": "sme-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"build:skip-check": "tsc --noEmit && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdx-js/react": "^3.1.0",
|
||||||
|
"@react-oauth/google": "^0.12.1",
|
||||||
|
"@reduxjs/toolkit": "^2.5.1",
|
||||||
|
"@tailwindcss/vite": "^4.0.7",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"canvas-confetti": "^1.9.3",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
|
"pdfjs-dist": "^5.1.91",
|
||||||
|
"randomcolor": "^0.6.2",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-confetti": "^6.4.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-draggable": "^4.4.6",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
|
"react-id-generator": "^3.0.2",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
|
"react-router-dom": "^7.2.0",
|
||||||
|
"react-sortablejs": "^6.1.4",
|
||||||
|
"react-toastify": "^11.0.3",
|
||||||
|
"react-use": "^17.6.0",
|
||||||
|
"redux-persist": "^6.0.0",
|
||||||
|
"tailwindcss": "^4.0.7",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.19.0",
|
||||||
|
"@types/jwt-decode": "^2.2.1",
|
||||||
|
"@types/react": "^19.0.8",
|
||||||
|
"@types/react-dom": "^19.0.3",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"eslint": "^9.19.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.18",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"typescript": "~5.7.2",
|
||||||
|
"typescript-eslint": "^8.22.0",
|
||||||
|
"vite": "^6.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { BrowserRouter, Route, Routes } from "react-router-dom"
|
||||||
|
import Dashboard from "./components/dashboard/Dashboard"
|
||||||
|
import Home from "./components/home/Home"
|
||||||
|
import Login from "./components/login/Login";
|
||||||
|
import Register from "./components/register/Register";
|
||||||
|
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
import ProcessStepTwo from "./components/process/processStepTwo/ProcessStepTwo";
|
||||||
|
import ProcessStepOne from "./components/process/processStepOne/ProcessStepOne";
|
||||||
|
|
||||||
|
import DocView from "./components/view/DocView";
|
||||||
|
import SignDocument from "./components/client/SignDocument";
|
||||||
|
|
||||||
|
import Transactions from "./components/transactions/Transactions";
|
||||||
|
import ProtectedRoute from "./components/routes_helper/ProtectedRoute";
|
||||||
|
|
||||||
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { fetchUserDetails } from "./store/slices/userSlice";
|
||||||
|
import { AppDispatch, RootState } from "./store/store";
|
||||||
|
import { useEffect, useLayoutEffect } from "react";
|
||||||
|
|
||||||
|
import Setting from "./components/setting/Setting";
|
||||||
|
import Accounts from "./components/accounts/Accounts";
|
||||||
|
import Account from "./components/accounts/Account";
|
||||||
|
import TransactionView from "./components/transactions/TransactionView";
|
||||||
|
import AccountTransactionWrapper from "./components/AccountTransactionWrapper";
|
||||||
|
import SigningOrderDiagram from "./SigningOrderDiagram ";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
{/* <HashRouter> */}
|
||||||
|
<ToastContainer />
|
||||||
|
|
||||||
|
<Routes>
|
||||||
|
|
||||||
|
<Route element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<Dashboard />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="/templates" element={<Home />} />
|
||||||
|
|
||||||
|
<Route element={<AccountTransactionWrapper />}>
|
||||||
|
<Route path="/accounts" element={<Accounts />} />
|
||||||
|
<Route path="/accounts/:accountId/transactions" element={<Account />} />
|
||||||
|
<Route path="/accounts/:accountId/transactions/:transactionId" element={<TransactionView />} />
|
||||||
|
<Route path="/transactions/:filterParam" element={<Transactions />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/s/:page1" element={<Setting />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Route path="/process-step-one/:accountId/:transactionId" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ProcessStepOne />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
<Route path="/process-step-two/:accountId/:transactionId" element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<ProcessStepTwo />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
|
||||||
|
{/* Links work wothout Login */}
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/register" element={<Register />} />
|
||||||
|
<Route path="/sign/:recipientId" element={<SignDocument />} />
|
||||||
|
<Route path="/view" element={<DocView />} />
|
||||||
|
|
||||||
|
<Route path="/SigningOrderDiagram" element={<SigningOrderDiagram />} />
|
||||||
|
|
||||||
|
{/* Links work wothout Login */}
|
||||||
|
</Routes>
|
||||||
|
|
||||||
|
{/* </HashRouter> */}
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import React from "react";
|
||||||
|
import { FaCheck, FaLock, FaClock, FaEnvelope, FaTimes } from "react-icons/fa";
|
||||||
|
import { IoCloseOutline } from "react-icons/io5";
|
||||||
|
|
||||||
|
type Signer = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
routingOrder: number;
|
||||||
|
completed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sampleData: Signer[] = [
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
name: "rohit singh",
|
||||||
|
routingOrder: 1,
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: "anita kumari",
|
||||||
|
routingOrder: 1,
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
name: "sunil sharma",
|
||||||
|
routingOrder: 2,
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
name: "laxmi agrawal",
|
||||||
|
routingOrder: 3,
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const groupByRoutingOrder = (data: Signer[]) => {
|
||||||
|
const grouped: Record<number, Signer[]> = {};
|
||||||
|
data.forEach((signer) => {
|
||||||
|
if (!grouped[signer.routingOrder]) {
|
||||||
|
grouped[signer.routingOrder] = [];
|
||||||
|
}
|
||||||
|
grouped[signer.routingOrder].push(signer);
|
||||||
|
});
|
||||||
|
return grouped;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInitials = (name: string) => {
|
||||||
|
return name
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0].toUpperCase())
|
||||||
|
.join("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const SigningOrderDiagram : React.FC<Signer[]> = (signers) => {
|
||||||
|
|
||||||
|
console.log(signers, "Line 59")
|
||||||
|
|
||||||
|
const grouped = groupByRoutingOrder(signers.signers);
|
||||||
|
const sortedOrders = Object.keys(grouped)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
const currentProcessingOrder =
|
||||||
|
sortedOrders.find((order) =>
|
||||||
|
grouped[order].some((signer) => !signer.completed)
|
||||||
|
) || -1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative p-8 bg-white max-w-max">
|
||||||
|
{/* Single mail icon on top */}
|
||||||
|
<div className="flex justify-center items-center gap-4">
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="bg-blue-100 p-4 rounded-full">
|
||||||
|
<FaEnvelope className="text-blue-600 text-2xl" />
|
||||||
|
</div>
|
||||||
|
{/* <p className="text-sm text-gray-600 mt-2">Transaction Sent</p> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-16 border-1 border-dotted border-slate-200 "></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center space-x-6">
|
||||||
|
{sortedOrders.map((order, index) => (
|
||||||
|
<React.Fragment key={order}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<div className="flex flex-col items-center space-y-3">
|
||||||
|
{grouped[order].map((signer) => {
|
||||||
|
let bgColor = "";
|
||||||
|
let icon = null;
|
||||||
|
|
||||||
|
if (signer.completed) {
|
||||||
|
bgColor = "bg-green-500";
|
||||||
|
icon = <FaCheck className="text-green-600" />;
|
||||||
|
} else if (order === currentProcessingOrder) {
|
||||||
|
bgColor = "bg-yellow-500";
|
||||||
|
icon = <FaClock className="text-yellow-800" />;
|
||||||
|
} else {
|
||||||
|
bgColor = "bg-gray-400";
|
||||||
|
icon = <FaLock className="text-slate-500" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={signer.id} className="relative">
|
||||||
|
<div
|
||||||
|
className={`w-10 h-10 rounded-full flex items-center justify-center text-white text-sm font-semibold ${bgColor}`}
|
||||||
|
title={signer.name}
|
||||||
|
>
|
||||||
|
{getInitials(signer.name)}
|
||||||
|
<span className="absolute bottom-[12px] right-[-12px] p-1 rounded-full bg-white shadow">
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-gray-600 text-center">{signer.name.split(" ")[0]}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{index < sortedOrders.length - 1 && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-16 border-1 border-dotted border-slate-200 "></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SigningOrderDiagram;
|
||||||
|
|
@ -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.0 KiB |
|
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
import { Outlet } from 'react-router-dom'
|
||||||
|
import AccountNav from './accounts/AccountNav'
|
||||||
|
|
||||||
|
const AccountTransactionWrapper = () => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccountNav />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AccountTransactionWrapper
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
|
||||||
|
import AccountNav from "./AccountNav";
|
||||||
|
import api from "../../config/api";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { FaCheck } from "react-icons/fa";
|
||||||
|
|
||||||
|
interface Transaction {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
active: boolean;
|
||||||
|
subject: string | null;
|
||||||
|
createdAt: number;
|
||||||
|
expireAfter: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (timestamp: number) => {
|
||||||
|
return new Date(timestamp * 1000).toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const Account = () => {
|
||||||
|
const { accountId } = useParams<{ accountId: string }>();
|
||||||
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
|
const [selectedTransactions, setSelectedTransactions] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const fetchTransactions = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api(`/api/accounts/${accountId}/transactions`);
|
||||||
|
setTransactions(response.data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching transactions:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accountId) fetchTransactions();
|
||||||
|
}, [accountId]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleTransactionClick = (id: number) => {
|
||||||
|
navigate(`/accounts/${accountId}/transactions/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{transactions.length > 0 && (
|
||||||
|
<div className="overflow-x-auto py-4">
|
||||||
|
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
|
||||||
|
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left"></th>
|
||||||
|
<th className="px-4 py-3 text-center">Name</th>
|
||||||
|
<th className="px-4 py-3 text-center">Status</th>
|
||||||
|
<th className="px-4 py-3 text-center">Active</th>
|
||||||
|
<th className="px-4 py-3 text-center">Last Change</th>
|
||||||
|
<th className="px-4 py-3 text-center">Expire After (days)</th>
|
||||||
|
<th className="px-4 py-3 text-center"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{transactions.map((tx) => {
|
||||||
|
const lastModifiedAt = new Date(tx.lastModifiedAt, "DD/MM/YYYY");
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={tx.id}
|
||||||
|
onClick={() => handleTransactionClick(tx.id)}
|
||||||
|
className="hover:bg-gray-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="w-5 h-5 rounded border border-slate-300 flex justify-center items-center">
|
||||||
|
{selectedTransactions.includes(tx.id) && (
|
||||||
|
<FaCheck className="text-purple-600 text-sm" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
{tx.subject || "Untitled"}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<span className="px-2 py-0.5 rounded-xl text-xs font-medium bg-blue-100 text-blue-700">
|
||||||
|
{tx.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
{tx.active ? "Yes" : "No"}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
{formatDate(tx.lastModifiedAt)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
{tx.expireAfter ?? "-"}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Account;
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
import React, { use, useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import { FaCog } from 'react-icons/fa';
|
||||||
|
import { IoIosNotifications } from 'react-icons/io';
|
||||||
|
import { RootState } from '../../store/store';
|
||||||
|
import { UserState } from '../../store/slices/userSlice';
|
||||||
|
import api from '../../config/api';
|
||||||
|
|
||||||
|
// const fetchAccountData = async (accountId: string) => {
|
||||||
|
// try {
|
||||||
|
// if(!accountId) return null;
|
||||||
|
// const response = await api.get(`/api/accounts/${accountId}`);
|
||||||
|
// return response.data;
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error fetching account data:", error);
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const fetchTransactionData = async (accountId: string, transactionId: string) => {
|
||||||
|
// try {
|
||||||
|
// if(!accountId || !transactionId) return null;
|
||||||
|
// const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}`);
|
||||||
|
// return response.data;
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error fetching account data:", error);
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
interface AccountNavProps {
|
||||||
|
accountName: string | null;
|
||||||
|
transactionName: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccountNav: React.FC<AccountNavProps> = ({ accountName = null, transactionName = null }) => {
|
||||||
|
const user: UserState = useSelector((state: RootState) => state.user);
|
||||||
|
|
||||||
|
const initials = user.username
|
||||||
|
? user.username
|
||||||
|
.split(' ')
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join('')
|
||||||
|
.slice(0, 2)
|
||||||
|
.toUpperCase()
|
||||||
|
: 'U';
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (accountId) {
|
||||||
|
// api.get(`/api/accounts/${accountId}`)
|
||||||
|
// .then(response => {
|
||||||
|
// // Handle the account data if needed
|
||||||
|
// console.log(response.data, "Line 29");
|
||||||
|
// })
|
||||||
|
// .catch(error => {
|
||||||
|
// console.error("Error fetching account data:", error);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }, [accountId, transactionId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
|
||||||
|
|
||||||
|
<h1 className="text-md font-medium text-gray-800 capitalize">
|
||||||
|
Account
|
||||||
|
{accountName ? ` > ${accountName}` : ''}
|
||||||
|
{transactionName ? ` > ${transactionName}` : ''}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* <h1 className="text-md font-medium text-gray-800 capitalize">Account</h1> */}
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* Notification + Settings with smaller gap */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
aria-label="Notifications"
|
||||||
|
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
||||||
|
>
|
||||||
|
<IoIosNotifications className="text-xl" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/settings"
|
||||||
|
aria-label="Settings"
|
||||||
|
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
||||||
|
>
|
||||||
|
<FaCog className="text-sm" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Avatar with separate spacing */}
|
||||||
|
<div className="w-9 h-9 ms-3 rounded-full overflow-hidden bg-[var(--primary)] flex items-center justify-center text-white font-semibold text-sm">
|
||||||
|
{user.picture ? (
|
||||||
|
<img
|
||||||
|
src={user.picture}
|
||||||
|
alt="User avatar"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
initials
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccountNav;
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FaCheck } from "react-icons/fa";
|
||||||
|
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
|
||||||
|
import { FaDatabase } from "react-icons/fa";
|
||||||
|
import AccountNav from "./AccountNav";
|
||||||
|
import api from "../../config/api";
|
||||||
|
import { AccountData, AccountStatus, UserRole } from "../../utlity/types";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
// import { UserRole, AccountStatus, AccountData } from "../../types/account";
|
||||||
|
|
||||||
|
const roleColors: Record<UserRole, string> = {
|
||||||
|
[UserRole.OWNER]: "bg-purple-100 text-purple-700",
|
||||||
|
[UserRole.ADMIN]: "bg-blue-100 text-blue-700",
|
||||||
|
[UserRole.EDITOR]: "bg-green-100 text-green-700",
|
||||||
|
[UserRole.VIEWER]: "bg-gray-100 text-gray-600",
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (
|
||||||
|
status: AccountStatus,
|
||||||
|
role: UserRole
|
||||||
|
): { text: string; color: string } => {
|
||||||
|
if (status === AccountStatus.ACTIVE && role !== UserRole.OWNER)
|
||||||
|
return { text: "Accepted", color: "text-green-600 bg-green-100" };
|
||||||
|
if (status === AccountStatus.REJECTED)
|
||||||
|
return { text: "Rejected", color: "text-red-600 bg-red-100" };
|
||||||
|
if (status === AccountStatus.INVITED)
|
||||||
|
return { text: "Pending", color: "text-yellow-600 bg-yellow-100" };
|
||||||
|
return { text: "Active", color: "text-blue-600 bg-blue-100" };
|
||||||
|
};
|
||||||
|
|
||||||
|
const Accounts = () => {
|
||||||
|
const [allAccounts, setAllAccounts] = useState<AccountData[]>([]);
|
||||||
|
const [selectedAccounts, setSelectedAccounts] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const fetchAccounts = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api("/api/user/accounts");
|
||||||
|
const data: AccountData[] = response.data || [];
|
||||||
|
setAllAccounts(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching accounts:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAccounts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSelect = (id: number) => {
|
||||||
|
navigate(`/accounts/${id}/transactions`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{allAccounts.length > 0 && (
|
||||||
|
<div className="overflow-x-auto py-2">
|
||||||
|
<table className="min-w-full text-sm border border-gray-200 rounded bg-white shadow-sm">
|
||||||
|
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
|
||||||
|
<tr>
|
||||||
|
{/* <th className="px-4 py-3 w-12"></th> */}
|
||||||
|
<th className="px-4 py-3 text-left">Account Name</th>
|
||||||
|
<th className="px-4 py-3 text-center">Account ID</th>
|
||||||
|
<th className="px-4 py-3 text-center">Role</th>
|
||||||
|
<th className="px-4 py-3 text-center">Status</th>
|
||||||
|
<th className="px-4 py-3 text-center"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{allAccounts.map((account) => {
|
||||||
|
const status = getStatusLabel(account.status, account.role);
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={account.accountId}
|
||||||
|
onClick={() => handleSelect(account.accountId)}
|
||||||
|
className="hover:bg-gray-50 cursor-pointer"
|
||||||
|
>
|
||||||
|
{/* <td className="px-4 py-3">
|
||||||
|
<div className="w-5 h-5 rounded border border-slate-300 flex justify-center items-center">
|
||||||
|
{selectedAccounts.includes(account.accountId) && (
|
||||||
|
<FaCheck className="text-purple-600 text-sm" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td> */}
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FaDatabase className="text-[var(--primary)]" />
|
||||||
|
<span>{account.accountName}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">{account.accountId}</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<span className={`px-2 py-0.5 rounded-xl text-xs font-medium ${roleColors[account.role]}`}>
|
||||||
|
{account.role}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<span className={`px-2 py-0.5 rounded-xl text-xs font-medium ${status.color}`}>
|
||||||
|
{status.text}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-center">
|
||||||
|
<PiDotsThreeOutlineVerticalFill className="text-lg text-slate-300 hover:text-black mx-auto" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Accounts;
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
// import OAuthGoogle from "./OAuthGoogle";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useState } from "react";
|
||||||
|
// import { FcGoogle } from "react-icons/fc";
|
||||||
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import OAuthGoogle from "./OAuthGoogle";
|
||||||
|
import { Bounce, toast } from "react-toastify";
|
||||||
|
// import Cookies from "js-cookie";
|
||||||
|
import { updateUser } from "../../store/slices/userSlice";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { AppDispatch } from "../../store/store";
|
||||||
|
// import api from "../../config/api";
|
||||||
|
|
||||||
|
interface FormDataState {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function LoginForm() {
|
||||||
|
const [formData, setForm] = useState<FormDataState>({ email: "", password: "" });
|
||||||
|
const [cacheKey, setCacheKey] = useState<string>('');
|
||||||
|
const [nextClick, setNextClick] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
||||||
|
const handleNextClick = () => {
|
||||||
|
console.log('Next Button Clicked');
|
||||||
|
|
||||||
|
// Validation Starts
|
||||||
|
if (formData.email === "") {
|
||||||
|
console.log('All Fields are Required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Email Format
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(formData.email)) {
|
||||||
|
console.log("Invalid email format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Validation Ends
|
||||||
|
|
||||||
|
// Checking user exists getting cacheKey
|
||||||
|
axios.get(`${import.meta.env.VITE_BASE_URI}/api/auth/user?email=${formData.email}`, { headers: { "ngrok-skip-browser-warning": "69420" } })
|
||||||
|
.then((res) => {
|
||||||
|
|
||||||
|
console.log(res, "Cache Key res", formData.email)
|
||||||
|
console.log(res.data, "Cache Key res 2", formData.email)
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.dismiss();
|
||||||
|
console.log('User Exists');
|
||||||
|
setCacheKey(res.data);
|
||||||
|
setNextClick(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('User Does Not Exists');
|
||||||
|
toast.dismiss();
|
||||||
|
toast.error("User Does Not Exists", {
|
||||||
|
position: "top-center",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormLoginClick = () => {
|
||||||
|
console.log('Login Function Called Line 7 LoginForm.tsx');
|
||||||
|
|
||||||
|
// Validation Starts
|
||||||
|
if (formData.email === "" || formData.password === "") {
|
||||||
|
console.log('All Fields are Required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Email Format
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(formData.email)) {
|
||||||
|
console.log("Invalid email format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Validation Ends
|
||||||
|
|
||||||
|
// Login API Call
|
||||||
|
console.log('Login API Call');
|
||||||
|
console.log(formData, "Submitting Data :", { cacheKey: cacheKey, password: formData.password });
|
||||||
|
axios.post(`${import.meta.env.VITE_BASE_URI}/api/auth/login`,
|
||||||
|
{ cacheKey: cacheKey, password: formData.password },
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
"ngrok-skip-browser-warning": "69420",
|
||||||
|
"Access-Control-Allow-Origin": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res.status, "Response Status Line 42 LoginForm.tsx", res);
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.dismiss();
|
||||||
|
console.log('User Logined Successfully Line 45 LoginForm.tsx headerCookie:', res.headers.authorization);
|
||||||
|
// Cookies.set("authorization", res.headers.authorization, {
|
||||||
|
// path: "/",
|
||||||
|
// secure: true, // Required for HTTPS
|
||||||
|
// sameSite: "Lax", // Required for cross-origin requests
|
||||||
|
// });
|
||||||
|
|
||||||
|
dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization }));
|
||||||
|
setTimeout(() => navigate('/'), 1000); // Redirect after 1 second
|
||||||
|
toast.success("Login Successful", {
|
||||||
|
position: "top-center",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err, "ERROR")
|
||||||
|
if (err.status === 401) {
|
||||||
|
console.log('Unauthorized');
|
||||||
|
toast.dismiss();
|
||||||
|
toast.error("Credentials Not Match", {
|
||||||
|
position: "top-center",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Some Error Occurred');
|
||||||
|
toast.dismiss();
|
||||||
|
toast.error("Some Error Occurred", {
|
||||||
|
position: "top-center",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Login</h1>
|
||||||
|
|
||||||
|
|
||||||
|
<form action="#" onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<div className={`${nextClick ? 'hidden' : ''}`}>
|
||||||
|
<div className="input-box">
|
||||||
|
<input type="email" placeholder="Email" required
|
||||||
|
onChange={(e) => setForm({ ...formData, email: e.target.value })} />
|
||||||
|
<i className='bx bxs-user'></i>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="btn" onClick={handleNextClick}>Next</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<form action="#" onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleFormLoginClick();
|
||||||
|
}}>
|
||||||
|
<div className={`${!nextClick ? 'hidden' : ''}`}>
|
||||||
|
<div className="input-box">
|
||||||
|
<input type="password" placeholder="Password" required
|
||||||
|
onChange={(e) => setForm({ ...formData, password: e.target.value })}
|
||||||
|
/>
|
||||||
|
<i className='bx bxs-lock-alt' ></i>
|
||||||
|
</div>
|
||||||
|
<div className="forgot-link">
|
||||||
|
<a href="#">Forgot Password?</a>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="btn">Login</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>or login with social platforms</p>
|
||||||
|
|
||||||
|
<div className="social-icons">
|
||||||
|
<OAuthGoogle />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Create an Account. <Link to={'/register'} className="text-blue-400">Register</Link></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoginForm
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GoogleLogin } from '@react-oauth/google';
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import axios from 'axios';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { AppDispatch } from '../../store/store';
|
||||||
|
import { updateUser } from '../../store/slices/userSlice';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
// import api from '../../config/api';
|
||||||
|
|
||||||
|
// Define a basic type for the response object
|
||||||
|
interface GoogleResponse {
|
||||||
|
credential: string; // This is usually the JWT token
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OAuthGoogleProps {
|
||||||
|
isLogin: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const OAuthGoogle: React.FC<OAuthGoogleProps> = ({isLogin}) => {
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Define the success handler type (based on the Google login response)
|
||||||
|
const responseMessage = (response: GoogleResponse) => {
|
||||||
|
console.log(response, "OAuthGoogle .tsx Line 23 Response from Google");
|
||||||
|
const token = response.credential;
|
||||||
|
|
||||||
|
console.log(token, 'token');
|
||||||
|
|
||||||
|
axios.get(`${import.meta.env.VITE_BASE_URI}/api/oauth2/authorization/google?token=${token}`, {
|
||||||
|
headers: {
|
||||||
|
"ngrok-skip-browser-warning": "69420",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 200 || res.status === 201) {
|
||||||
|
toast.dismiss();
|
||||||
|
console.log('User Logined Successfully Line 45 LoginForm.tsx headerCookie:', res.headers.authorization);
|
||||||
|
|
||||||
|
dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization }));
|
||||||
|
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err, 'err');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const decoded = jwtDecode(token);
|
||||||
|
|
||||||
|
console.log(decoded, 'decoded');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define the error handler type (error can be of any type)
|
||||||
|
const errorMessage = (error: any) => {
|
||||||
|
console.log(error, "From Error");
|
||||||
|
};
|
||||||
|
|
||||||
|
return <GoogleLogin data-text={`${isLogin ? "signin_with" : "signup_with"}`} onSuccess={responseMessage} onError={errorMessage} />
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OAuthGoogle;
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
// import { GoogleSvg, SalesforceSvg } from "../svgs/Svg"
|
||||||
|
import OAuthGoogle from "./OAuthGoogle"
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Link, Navigate, useNavigate } from "react-router-dom";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
// import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
import { Bounce, toast } from "react-toastify";
|
||||||
|
import { updateUser, UserState } from "../../store/slices/userSlice";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { AppDispatch, RootState } from "../../store/store";
|
||||||
|
// import api from "../../config/api";
|
||||||
|
|
||||||
|
interface FormDataState {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function RegisterForm() {
|
||||||
|
console.log("RegisterForm Component Rendered");
|
||||||
|
const [formData, setForm] = useState<FormDataState>({ email: "", password: "", confirmPassword: "", firstName: "", lastName: "" });
|
||||||
|
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleFormRegisterClick = () => {
|
||||||
|
console.log('Register Function Called Line 7 RegisterForm.tsx');
|
||||||
|
formData.firstName = formData.firstName.trim();
|
||||||
|
formData.lastName = formData.lastName.trim();
|
||||||
|
// Validation Starts
|
||||||
|
if (formData.email === "" || formData.password === "" || formData.confirmPassword === "" || formData.firstName === "" || formData.lastName === "") {
|
||||||
|
console.log('All Fields are Required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Email Format
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(formData.email)) {
|
||||||
|
console.log("Invalid email format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Validation Ends
|
||||||
|
|
||||||
|
|
||||||
|
// Register API Call
|
||||||
|
console.log('Register API Call');
|
||||||
|
console.dir(formData);
|
||||||
|
|
||||||
|
axios.post(`${import.meta.env.VITE_BASE_URI}/api/auth/signup`, { firstName: formData.firstName, lastName: formData.lastName, email: formData.email, password: formData.password }, {
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
"ngrok-skip-browser-warning": "69420",
|
||||||
|
"Access-Control-Allow-Origin": "*"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res.status, "Response Status Line 42 RegisterForm.tsx");
|
||||||
|
if (res.status === 201) {
|
||||||
|
|
||||||
|
console.log(res, "line 58");
|
||||||
|
toast.dismiss();
|
||||||
|
// dispatch(updateUser({ ...res.data, authorized: true, accessToken: res.headers.authorization }));
|
||||||
|
dispatch(updateUser({ ...res.data, authorized: true }));
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err.status === 409) {
|
||||||
|
console.log('User Already Exists');
|
||||||
|
toast.dismiss();
|
||||||
|
toast.error("User Already Exists", {
|
||||||
|
position: "top-center",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Some Error Occurred');
|
||||||
|
toast.dismiss();
|
||||||
|
toast.error("Some Error Occurred", {
|
||||||
|
position: "top-center",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form action="#" onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleFormRegisterClick();
|
||||||
|
}}>
|
||||||
|
<h1>Registration</h1>
|
||||||
|
{/* <div className="input-box">
|
||||||
|
<input type="text" placeholder="Username" required />
|
||||||
|
<i className='bx bxs-user'></i>
|
||||||
|
</div> */}
|
||||||
|
<div className="fl-container flex gap-2">
|
||||||
|
<div className="input-box">
|
||||||
|
<input
|
||||||
|
onChange={(e) => setForm({ ...formData, firstName: e.target.value })}
|
||||||
|
type="text" placeholder="First Name" pattern="^(?!\s*$).+"
|
||||||
|
required
|
||||||
|
title="Input cannot be empty or just spaces"
|
||||||
|
/>
|
||||||
|
<i className='bx bxs-envelope' ></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-box">
|
||||||
|
<input
|
||||||
|
onChange={(e) => setForm({ ...formData, lastName: e.target.value })}
|
||||||
|
type="text"
|
||||||
|
placeholder="Last Name"
|
||||||
|
pattern="^(?!\s*$).+"
|
||||||
|
required
|
||||||
|
title="Input cannot be empty or just spaces" />
|
||||||
|
<i className='bx bxs-envelope' ></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-box">
|
||||||
|
<input
|
||||||
|
onChange={(e) => setForm({ ...formData, email: e.target.value })}
|
||||||
|
type="email" placeholder="Email" required />
|
||||||
|
<i className='bx bxs-envelope' ></i>
|
||||||
|
</div>
|
||||||
|
<div className="input-box">
|
||||||
|
<input
|
||||||
|
onChange={(e) => setForm({ ...formData, password: e.target.value })}
|
||||||
|
type="password" placeholder="Password"
|
||||||
|
pattern="^.{8,}$"
|
||||||
|
title="Password must be at least 8 digits"
|
||||||
|
required />
|
||||||
|
<i className='bx bxs-lock-alt' ></i>
|
||||||
|
</div>
|
||||||
|
<div className="input-box">
|
||||||
|
<input
|
||||||
|
onChange={(e) => setForm({ ...formData, confirmPassword: e.target.value })}
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
style={{
|
||||||
|
border: (formData.confirmPassword != "" && formData.password !== formData.confirmPassword) ? "1px solid red" : "none"
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<i className='bx bxs-lock-alt' ></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
className={`btn ${(formData.email != "" && formData.password != "" && formData.password === formData.confirmPassword) ? "opacity-100" : "opacity-60"} transition-all duration-150`}>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p>or register with social platforms</p>
|
||||||
|
<div className="social-icons">
|
||||||
|
<OAuthGoogle isLogin={false} />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Already! have an Account. <Link to={'/login'} className="text-blue-400">Login</Link></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterForm
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { useEffect, useLayoutEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import "../styles/style.css"
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import LoginForm from "./LoginForm";
|
||||||
|
import RegisterForm from "./RegisterForm";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { AppDispatch } from "../../store/store";
|
||||||
|
import { fetchUserDetails } from "../../store/slices/userSlice";
|
||||||
|
|
||||||
|
function UserAuthUI({ pg }: { pg: string }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
dispatch(fetchUserDetails());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const elementRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pg === 'login') {
|
||||||
|
elementRef.current?.classList.add('active')
|
||||||
|
setTimeout(() => {
|
||||||
|
elementRef.current?.classList.remove('active')
|
||||||
|
}, 0)
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
elementRef.current?.classList.add('active')
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// const [activeToggle, setActiveToggle] = useState<string>('');
|
||||||
|
|
||||||
|
// const [page, setPage] = useState(pg || 'login');
|
||||||
|
function handleToggleForm(val: string) {
|
||||||
|
navigate(`/${val}`);
|
||||||
|
// setPage((prev) => prev === "login" ? "register" : "login")
|
||||||
|
}
|
||||||
|
|
||||||
|
// const user: UserState = useSelector((state: RootState) => state.user);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (page === 'register') {
|
||||||
|
// setActiveToggle('active')
|
||||||
|
// } else {
|
||||||
|
// setActiveToggle('')
|
||||||
|
// }
|
||||||
|
// }, [page])
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// setPage(pg);
|
||||||
|
// }, [pg])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="auth-ui-con main-container-l">
|
||||||
|
<div ref={elementRef} className={`container-l`}>
|
||||||
|
<div className={`form-box login flex-col`}>
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`form-box register flex-col`}>
|
||||||
|
<RegisterForm />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="toggle-box">
|
||||||
|
<div className="toggle-panel toggle-left">
|
||||||
|
<h1>Sign Made Easy</h1>
|
||||||
|
<p>Don't have an account?</p>
|
||||||
|
<button className="btn register-btn" onClick={() => handleToggleForm("register")}>Register</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="toggle-panel toggle-right">
|
||||||
|
<h1>Welcome Back!</h1>
|
||||||
|
<p>Already have an account?</p>
|
||||||
|
<button className="btn login-btn" onClick={() => handleToggleForm("login")}>Login</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserAuthUI
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,88 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import React from 'react';
|
||||||
|
import { AiOutlineCheck, AiOutlineCloseCircle } from 'react-icons/ai';
|
||||||
|
import { FaRegFileAlt } from 'react-icons/fa';
|
||||||
|
|
||||||
|
interface TermsModalProps {
|
||||||
|
showTerms: boolean;
|
||||||
|
setShowTerms: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
recipientId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TermsModal: React.FC<TermsModalProps> = ({ showTerms, setShowTerms, recipientId }) => {
|
||||||
|
|
||||||
|
const onAccept = () => {
|
||||||
|
axios.put(`${import.meta.env.VITE_BASE_URI}/api/sign/accept?recipientId=${recipientId}`, { headers: { "ngrok-skip-browser-warning": "69420" } })
|
||||||
|
.then(response => {
|
||||||
|
setShowTerms(false);
|
||||||
|
console.log(response, "Accept");
|
||||||
|
// Handle success
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
// Handle error
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const onDecline = () => {
|
||||||
|
axios.put(`${import.meta.env.VITE_BASE_URI}/api/sign/declinerecipientId=${recipientId}`, { headers: { "ngrok-skip-browser-warning": "69420" } })
|
||||||
|
.then(response => {
|
||||||
|
setShowTerms(false);
|
||||||
|
console.log(response, "Decline");
|
||||||
|
// Handle success
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
// Handle error
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!showTerms) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center px-4">
|
||||||
|
<div className="bg-white rounded-md w-full max-w-xl shadow-2xl overflow-hidden animate-fade-in-up">
|
||||||
|
|
||||||
|
{/* Top bar with logo and title */}
|
||||||
|
<div className="flex items-center gap-2 bg-[var(--primary)] text-white px-6 py-4">
|
||||||
|
<FaRegFileAlt className="text-2xl" />
|
||||||
|
<h2 className="text-xl font-semibold">Sign Made Easy</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Terms content scrollable */}
|
||||||
|
<div className="relative p-6 max-h-80 overflow-y-auto text-sm text-gray-700 space-y-4">
|
||||||
|
{/* Gradient scroll hint */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 h-6 bg-gradient-to-b from-white to-transparent pointer-events-none z-10"></div>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t from-white to-transparent pointer-events-none z-10"></div>
|
||||||
|
|
||||||
|
<p>Welcome to Sign Made Easy! These Terms & Conditions govern your use of our application.</p>
|
||||||
|
<p>By accessing our services, you agree to comply with and be bound by the following terms.</p>
|
||||||
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam varius placerat tellus, sed sodales odio cursus nec. Sed lacinia...</p>
|
||||||
|
<p>Etiam in diam efficitur, congue lorem sed, malesuada orci. Aliquam bibendum purus nec velit fermentum laoreet. Integer finibus...</p>
|
||||||
|
<p>We reserve the right to modify these terms at any time without prior notice. Continued use constitutes your acceptance of those changes.</p>
|
||||||
|
<p>Thanks for choosing us to make signing easy, secure, and smooth. 🙌</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="flex justify-between px-6 py-4 border-t bg-gray-50">
|
||||||
|
<button
|
||||||
|
onClick={onDecline}
|
||||||
|
className="flex items-center gap-2 text-red-500 hover:text-red-600 hover:bg-red-50 px-4 py-2 rounded-md transition"
|
||||||
|
>
|
||||||
|
<AiOutlineCloseCircle className="text-lg" />
|
||||||
|
Decline
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onAccept}
|
||||||
|
className="flex items-center gap-2 bg-[var(--primary)] hover:bg-[var(--primary)] text-white px-4 py-2 rounded-md shadow-sm transition"
|
||||||
|
>
|
||||||
|
<AiOutlineCheck className="text-lg" />
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TermsModal;
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
import React, { useEffect, useLayoutEffect } from 'react'
|
||||||
|
import SideBar from './SideBar'
|
||||||
|
import { Outlet, useNavigate } from 'react-router-dom'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { RootState } from '../../store/store'
|
||||||
|
import { UserState } from '../../store/slices/userSlice'
|
||||||
|
import Loader from './Loader'
|
||||||
|
|
||||||
|
import { clearTransaction, TRANSACTION_STATUS_ENUMS } from '../../store/slices/transactionSlice'
|
||||||
|
import { resetSelDoc } from '../../store/slices/selectedDocsSlice'
|
||||||
|
import { resetSelRecipient } from '../../store/slices/selectedRecipientsSlice'
|
||||||
|
import { hideLoader, showLoader } from '../../store/slices/loaderSlice'
|
||||||
|
import { Bounce, toast } from 'react-toastify'
|
||||||
|
import SignModal from '../modal/SignModal'
|
||||||
|
|
||||||
|
import confetti from 'canvas-confetti'
|
||||||
|
import { useWindowSize } from 'react-use'
|
||||||
|
import { FaCheckCircle } from 'react-icons/fa'
|
||||||
|
import { IoClose } from 'react-icons/io5'
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
const [showSideBar, setShowSideBar] = React.useState(true)
|
||||||
|
const [showConfetti, setShowConfetti] = React.useState(false)
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const user: UserState = useSelector((state: RootState) => state.user);
|
||||||
|
|
||||||
|
const globalLoader = useSelector((state: RootState) => state.loader)
|
||||||
|
const globalModalIsOpen = useSelector((state: RootState) => state.modal.isOpen)
|
||||||
|
|
||||||
|
const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments)
|
||||||
|
const documentSignType = useSelector((state: RootState) => state.selectedDocuments.signType)
|
||||||
|
|
||||||
|
const transactionStatus = useSelector((state: RootState) => state.transaction.status)
|
||||||
|
|
||||||
|
// Check if cookie already exists - Redirect to Dashboard
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// if (!user.authorized) {
|
||||||
|
// window.location.href = '/login';
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
console.log("OUT MIC CHECK", transactionStatus, TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS, TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED);
|
||||||
|
|
||||||
|
// Clear all if transaction success
|
||||||
|
if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_FAILURE) {
|
||||||
|
console.log("IN MIC CHECK");
|
||||||
|
dispatch(resetSelDoc())
|
||||||
|
dispatch(resetSelRecipient())
|
||||||
|
dispatch(clearTransaction())
|
||||||
|
|
||||||
|
if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS) {
|
||||||
|
setShowConfetti(true)
|
||||||
|
// setTimeout(() => {
|
||||||
|
// setShowConfetti(false)
|
||||||
|
// dispatch(hideLoader())
|
||||||
|
// toast.dismiss();
|
||||||
|
// toast.info("Transaction Send Successfully!", {
|
||||||
|
// position: "bottom-right",
|
||||||
|
// autoClose: 5000,
|
||||||
|
// hideProgressBar: false,
|
||||||
|
// closeOnClick: false,
|
||||||
|
// pauseOnHover: true,
|
||||||
|
// draggable: true,
|
||||||
|
// progress: undefined,
|
||||||
|
// theme: "light",
|
||||||
|
// transition: Bounce,
|
||||||
|
// });
|
||||||
|
// }, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS || transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED) return;
|
||||||
|
|
||||||
|
if (selectedDocuments === null || (Array.isArray(selectedDocuments) && !selectedDocuments.length)) {
|
||||||
|
dispatch(resetSelDoc())
|
||||||
|
}
|
||||||
|
dispatch(showLoader("Loading ..."));
|
||||||
|
|
||||||
|
if (documentSignType && transactionStatus === TRANSACTION_STATUS_ENUMS.TRANSACTION_NOT_INITIATED) {
|
||||||
|
if (documentSignType === 'self-sign') {
|
||||||
|
console.log('Self Sign');
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/process-step-one');
|
||||||
|
}, 1000)
|
||||||
|
return
|
||||||
|
} else if (documentSignType === 'multiple-sign') {
|
||||||
|
console.log('multiple-sign');
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate('/process-step-one');
|
||||||
|
}, 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispatch(hideLoader());
|
||||||
|
}, [documentSignType])
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showConfetti) {
|
||||||
|
const duration = 3000;
|
||||||
|
const end = Date.now() + duration;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (Date.now() > end) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confetti({
|
||||||
|
particleCount: 80, // more particles
|
||||||
|
angle: Math.random() * 360, // fire in random direction
|
||||||
|
spread: 220, // wider spread
|
||||||
|
startVelocity: 15, // more power
|
||||||
|
scalar: 1.2, // bigger pieces (default is 1)
|
||||||
|
origin: {
|
||||||
|
x: Math.random(),
|
||||||
|
y: Math.random() * 0.6,
|
||||||
|
},
|
||||||
|
zIndex: 9999,
|
||||||
|
});
|
||||||
|
}, 350); // fire every 150ms
|
||||||
|
}
|
||||||
|
}, [showConfetti]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
showConfetti && (
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full z-[9999] bg-white/80 backdrop-blur-sm flex items-center justify-center">
|
||||||
|
{/* <Confetti width={width} height={height} /> */}
|
||||||
|
|
||||||
|
<div className="relative bg-white bg-opacity-90 backdrop-blur-lg border border-green-200 shadow-xl rounded-md px-8 py-6 w-[90%] max-w-sm text-center animate-fade-in-up">
|
||||||
|
{/* Close Icon */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowConfetti(false)} // or whatever logic hides the modal
|
||||||
|
className="absolute top-2 right-2 text-gray-500 hover:text-gray-700"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<IoClose className="text-2xl" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Success Icon */}
|
||||||
|
<FaCheckCircle className="text-green-500 text-5xl mb-4 mx-auto drop-shadow-lg" />
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-800 mb-1">Transaction Successful</h2>
|
||||||
|
<p className="text-gray-600 text-sm">Your transaction has been created and saved successfully.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
|
||||||
|
{
|
||||||
|
user?.authorized &&
|
||||||
|
<div className={`relative`}>
|
||||||
|
{/* Complet DashBoard Back Side SideBar */}
|
||||||
|
<div className={`relative w-full h-screen overflow-hidden bg-[var(--dashBoardBg)] ${(globalLoader.isLoading || globalModalIsOpen) ? 'pointer-events-none' : ''}`}>
|
||||||
|
|
||||||
|
<SideBar showSideBar={showSideBar} setShowSideBar={setShowSideBar} />
|
||||||
|
|
||||||
|
{/* On Top Main Content - START */}
|
||||||
|
<div className={`overflow-y-auto absolute ${showSideBar ? 'w-[calc(100%-250px)]' : 'w-[calc(100%-60px)]'} h-full right-0 bottom-0 overflow-hidden bg-[var(--mainViewBg)] transition-all duration-300`}>
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className='px-2 py-1.5'>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* On Top Main Content - END */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{globalLoader.isLoading && <Loader />}
|
||||||
|
{globalModalIsOpen && <SignModal />}
|
||||||
|
</div>}
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { LuSignature } from "react-icons/lu";
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '../../store/store';
|
||||||
|
|
||||||
|
|
||||||
|
const Loader = () => {
|
||||||
|
|
||||||
|
const loaderData = useSelector((state: RootState) => state.loader);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='absolute w-full h-full top-0 left-0 transition-all duration-75'>
|
||||||
|
{/* Background */}
|
||||||
|
{/* <div className="z-50 absolute w-screen h-screen flex items-center justify-center bg-gradient-to-br from-white/20 to-white/10 backdrop-blur-xs"></div> */}
|
||||||
|
<div className="z-50 absolute w-screen h-screen flex items-center justify-center bg-[var(--hideModalBg)] opacity-60"></div>
|
||||||
|
|
||||||
|
{/* Loader */}
|
||||||
|
<div className="absolute z-50 w-screen h-screen flex items-center justify-center">
|
||||||
|
<div className='bg-[var(--loaderBg)] flex flex-col justify-center items-center gap-2 px-16 py-8 rounded-md shadow-lg'>
|
||||||
|
<LuSignature className="text-4xl font-bold animate-text" />
|
||||||
|
<p>
|
||||||
|
{loaderData.message != undefined ? `${loaderData.message}` : 'Loading ...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loader
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
// import { useState } from "react";
|
||||||
|
// import { IoIosArrowRoundForward } from "react-icons/io";
|
||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
interface NavLinkProps {
|
||||||
|
to: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
showSideBar: boolean;
|
||||||
|
menuPathMap: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// enum TransactionFilterEnum {
|
||||||
|
// ALL,
|
||||||
|
// DRAFT,
|
||||||
|
// SENT,
|
||||||
|
// COMPLETE,
|
||||||
|
// DECLINED,
|
||||||
|
// DISCARDED,
|
||||||
|
// CANCELED,
|
||||||
|
// EXPIRED
|
||||||
|
// }
|
||||||
|
|
||||||
|
const NavLink: React.FC<NavLinkProps> = ({ to, icon, label, showSideBar, menuPathMap }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const isActive = menuPathMap[location.pathname] === label;
|
||||||
|
|
||||||
|
// const [transactionFilter, setTransationFilter] = useState<TransactionFilterEnum>(TransactionFilterEnum.ALL)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to={to}
|
||||||
|
className={`${isActive ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'}
|
||||||
|
p-2 rounded-md flex justify-left gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{showSideBar && <p className="text-[0.8rem]">{label}</p>}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavLink;
|
||||||
|
|
||||||
|
|
||||||
|
// {label === "Transactions" ? <div>
|
||||||
|
// <Link
|
||||||
|
// to={to + "/all"}
|
||||||
|
// className={`${isActive ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'}
|
||||||
|
// p-2 rounded-md flex justify-left gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
|
||||||
|
// >
|
||||||
|
// {icon}
|
||||||
|
// {showSideBar && <p className="text-[0.8rem]">{label}</p>}
|
||||||
|
// </Link>
|
||||||
|
|
||||||
|
// <div
|
||||||
|
// className={`${showSideBar ? "pl-6" : ""} mt-0.5 overflow-hidden transition-all duration-300 ease-in-out transform ${isActive ? 'max-h-max opacity-100 translate-y-0' : 'max-h-0 opacity-0 -translate-y-2'
|
||||||
|
// }`}
|
||||||
|
// >
|
||||||
|
// <Link
|
||||||
|
// to={to + "/all"}
|
||||||
|
// className={`px-2 py-1 mt-1 ${transactionFilter === TransactionFilterEnum.ALL ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'} rounded-md flex justify-between gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
|
||||||
|
// onClick={() => setTransationFilter(TransactionFilterEnum.ALL)}
|
||||||
|
// >
|
||||||
|
// {showSideBar && <p className="text-[0.8rem]">All</p>}
|
||||||
|
// {transactionFilter === TransactionFilterEnum.ALL && <IoIosArrowRoundForward />}
|
||||||
|
// </Link>
|
||||||
|
|
||||||
|
// <Link
|
||||||
|
// to={to + "/draft"}
|
||||||
|
// className={`px-2 py-1 mt-1 ${transactionFilter === TransactionFilterEnum.DRAFT ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'} rounded-md flex justify-between gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
|
||||||
|
// onClick={() => setTransationFilter(TransactionFilterEnum.DRAFT)}
|
||||||
|
// >
|
||||||
|
// {showSideBar && <p className="text-[0.8rem]">Draft</p>}
|
||||||
|
// {transactionFilter === TransactionFilterEnum.DRAFT && <IoIosArrowRoundForward />}
|
||||||
|
// </Link>
|
||||||
|
|
||||||
|
// <Link
|
||||||
|
// to={to + "/sent"}
|
||||||
|
// className={`px-2 py-1 mt-1 ${transactionFilter === TransactionFilterEnum.SENT ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'} rounded-md flex justify-between gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
|
||||||
|
// onClick={() => setTransationFilter(TransactionFilterEnum.SENT)}
|
||||||
|
// >
|
||||||
|
// {showSideBar && <p className="text-[0.8rem]">Sent</p>}
|
||||||
|
// {transactionFilter === TransactionFilterEnum.SENT && <IoIosArrowRoundForward />}
|
||||||
|
// </Link>
|
||||||
|
|
||||||
|
// <Link
|
||||||
|
// to={to + "/complete"}
|
||||||
|
// className={`px-2 py-1 my-1 ${transactionFilter === TransactionFilterEnum.COMPLETE ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'} rounded-md flex justify-between gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
|
||||||
|
// onClick={() => setTransationFilter(TransactionFilterEnum.COMPLETE)}
|
||||||
|
// >
|
||||||
|
// {showSideBar && <p className="text-[0.8rem]">Completed</p>}
|
||||||
|
// {transactionFilter === TransactionFilterEnum.COMPLETE && <IoIosArrowRoundForward />}
|
||||||
|
// </Link>
|
||||||
|
|
||||||
|
// <Link
|
||||||
|
// to={to + "/declined"}
|
||||||
|
// className={`px-2 py-1 my-1 ${transactionFilter === TransactionFilterEnum.DECLINED ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'} rounded-md flex justify-between gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
|
||||||
|
// onClick={() => setTransationFilter(TransactionFilterEnum.DECLINED)}
|
||||||
|
// >
|
||||||
|
// {showSideBar && <p className="text-[0.8rem]">Declined</p>}
|
||||||
|
// {transactionFilter === TransactionFilterEnum.DECLINED && <IoIosArrowRoundForward />}
|
||||||
|
// </Link>
|
||||||
|
|
||||||
|
|
||||||
|
// <Link
|
||||||
|
// to={to + "/discarded"}
|
||||||
|
// className={`px-2 py-1 my-1 ${transactionFilter === TransactionFilterEnum.DISCARDED ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'} rounded-md flex justify-between gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
|
||||||
|
// onClick={() => setTransationFilter(TransactionFilterEnum.DISCARDED)}
|
||||||
|
// >
|
||||||
|
// {showSideBar && <p className="text-[0.8rem]">Discarded</p>}
|
||||||
|
// {transactionFilter === TransactionFilterEnum.DISCARDED && <IoIosArrowRoundForward />}
|
||||||
|
// </Link>
|
||||||
|
|
||||||
|
|
||||||
|
// <Link
|
||||||
|
// to={to + "/canceled"}
|
||||||
|
// className={`px-2 py-1 my-1 ${transactionFilter === TransactionFilterEnum.CANCELED ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'} rounded-md flex justify-between gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
|
||||||
|
// onClick={() => setTransationFilter(TransactionFilterEnum.CANCELED)}
|
||||||
|
// >
|
||||||
|
// {showSideBar && <p className="text-[0.8rem]">Canceled</p>}
|
||||||
|
// {transactionFilter === TransactionFilterEnum.CANCELED && <IoIosArrowRoundForward />}
|
||||||
|
// </Link>
|
||||||
|
|
||||||
|
|
||||||
|
// <Link
|
||||||
|
// to={to + "/expired"}
|
||||||
|
// className={`px-2 py-1 my-1 ${transactionFilter === TransactionFilterEnum.EXPIRED ? 'text-[var(--menuTextColActive)] bg-[var(--menuBgColActive)]' : 'text-[var(--menuTextCol)]'} rounded-md flex justify-between gap-2 items-center transition-all duration-300 ${showSideBar ? 'text-lg' : 'text-lg max-w-max'}`}
|
||||||
|
// onClick={() => setTransationFilter(TransactionFilterEnum.EXPIRED)}
|
||||||
|
// >
|
||||||
|
// {showSideBar && <p className="text-[0.8rem]">Expired</p>}
|
||||||
|
// {transactionFilter === TransactionFilterEnum.EXPIRED && <IoIosArrowRoundForward />}
|
||||||
|
// </Link>
|
||||||
|
|
||||||
|
|
||||||
|
// </div>
|
||||||
|
// </div> :
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React, { useEffect, useRef } from 'react'
|
||||||
|
import { IoMdArrowDropdown } from "react-icons/io";
|
||||||
|
import { IoIosSend } from "react-icons/io";
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { setSignType } from '../../store/slices/selectedDocsSlice';
|
||||||
|
import { RootState } from '../../store/store';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Bounce, toast } from 'react-toastify';
|
||||||
|
// import { IoMdArrowDropright } from "react-icons/io";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
showSideBar: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SendForSign: React.FC<Props> = ({ showSideBar }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [sendForSignDropdown, setSendForSignDropdown] = React.useState(false);
|
||||||
|
const dropDownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sendForSignDropdown && dropDownRef.current?.focus();
|
||||||
|
}, [sendForSignDropdown])
|
||||||
|
|
||||||
|
const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments);
|
||||||
|
|
||||||
|
const handleSendSignDispatch: (type: "self-sign" | "multiple-sign") => void = (signType) => {
|
||||||
|
if (selectedDocuments === null || !selectedDocuments.length) {
|
||||||
|
if (selectedDocuments === null || !selectedDocuments.length) {
|
||||||
|
toast.dismiss();
|
||||||
|
toast.error("Select 📄 Documents first!", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedDocuments.length) {
|
||||||
|
dispatch(setSignType(signType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative max-h-max pb-3'
|
||||||
|
tabIndex={0}
|
||||||
|
ref={dropDownRef}
|
||||||
|
onBlur={() => setSendForSignDropdown(false)}
|
||||||
|
>
|
||||||
|
<div className={`flex justify-between items-center gap-2 cursor-pointer bg-[var(--primary)] rounded text-[var(--sendForSignTextCol)] ${showSideBar ? "" : "pt-[0.4rem] pr-[0.6rem] pb-[0.4rem] pl-[0.2rem]"}`}
|
||||||
|
onClick={() => setSendForSignDropdown(!sendForSignDropdown)}>
|
||||||
|
<div className={`w-full`}>
|
||||||
|
{
|
||||||
|
showSideBar ?
|
||||||
|
<p className='text-center text-sm'>Send for signature</p>
|
||||||
|
:
|
||||||
|
<IoIosSend className={`pl-2 text-3xl`} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{showSideBar && <p className='border-l-1 border-[var(--sendForSignTextCol)] px-2 py-2'>
|
||||||
|
<IoMdArrowDropdown className={`text-2xl ${sendForSignDropdown ? 'rotate-180' : ''} transition-all duration-150`} />
|
||||||
|
</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Drop Down Box */}
|
||||||
|
{
|
||||||
|
sendForSignDropdown &&
|
||||||
|
<div className={`w-[300px] rounded absolute z-40 bg-[var(--sendForSignDropDownBg)] shadow-lg top-[54px] text-left outline-none`}>
|
||||||
|
<div className='py-3 px-8 leading-6 rounded-t-sm hover:bg-[var(--sendForSignDropDownHover)] cursor-pointer' onClick={() => handleSendSignDispatch('self-sign')}>
|
||||||
|
<p>Self Sign</p>
|
||||||
|
<p className='text-[var(--greyTextCol)] text-sm'>You're the only signer.</p>
|
||||||
|
</div>
|
||||||
|
<div className='w-full h-[1px] opacity-30 bg-[var(--greyTextCol)]'></div>
|
||||||
|
<div className='py-3 px-8 leading-6 rounded-b-sm hover:bg-[var(--sendForSignDropDownHover)] cursor-pointer' onClick={() => handleSendSignDispatch('multiple-sign')}>
|
||||||
|
<p>Send for Sign</p>
|
||||||
|
<p className='text-[var(--greyTextCol)] text-sm'>You and other need to sign.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SendForSign
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
import NavLink from './NavLink';
|
||||||
|
import SendForSign from './SendForSign';
|
||||||
|
import { HiTemplate } from "react-icons/hi";
|
||||||
|
import { BsLayoutSidebarInsetReverse, BsLayoutSidebarInset } from "react-icons/bs";
|
||||||
|
import { FaHome } from "react-icons/fa";
|
||||||
|
import { RiLogoutBoxLine } from 'react-icons/ri';
|
||||||
|
import { IoSettingsOutline } from "react-icons/io5";
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import { MdManageAccounts } from 'react-icons/md';
|
||||||
|
|
||||||
|
type SideBarProps = {
|
||||||
|
showSideBar: boolean;
|
||||||
|
setShowSideBar: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MenuPathMap = {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuPathMap: MenuPathMap = {
|
||||||
|
'/': 'Home',
|
||||||
|
'/templates': 'Templates',
|
||||||
|
'/s/contact': 'Contact',
|
||||||
|
'/accounts': 'Accounts',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||||
|
|
||||||
|
const handleLogoutConfirm = async () => {
|
||||||
|
try {
|
||||||
|
await axios.post(`${import.meta.env.VITE_BASE_URI}/api/auth/logout`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: { "ngrok-skip-browser-warning": "69420" }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Redirect to login or refresh the page after logout
|
||||||
|
window.location.href = '/login';
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Logout failed:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Side Bar */}
|
||||||
|
<div className={`relative h-screen ${showSideBar ? 'w-[250px]' : 'w-[60px]'} transition-all duration-300 bg-white shadow-md`}>
|
||||||
|
{/* Menu */}
|
||||||
|
<div className={`absolute top-0 ${showSideBar ? 'p-4' : 'py-4'} w-full max-h-max ${showSideBar ? '' : 'text-center'}`}>
|
||||||
|
<h1 className='w-full text-lg text-center tracking-widest font-medium text-gray-800'>
|
||||||
|
{showSideBar ? 'Sign Made Easy' : 'SME'}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className={`flex flex-col mt-4 ${showSideBar ? '' : 'items-center'}`}>
|
||||||
|
<SendForSign showSideBar={showSideBar} />
|
||||||
|
<NavLink to="/" icon={<FaHome />} label="Home" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||||
|
<NavLink to="/templates" icon={<HiTemplate />} label="Templates" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||||
|
<NavLink to="/accounts" icon={<MdManageAccounts />} label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SideBar Toggle Button */}
|
||||||
|
<div
|
||||||
|
onClick={() => setShowSideBar(!showSideBar)}
|
||||||
|
className='absolute top-[100%] translate-y-[-100%] right-0 bg-[var(--highlightBg1)] p-2 rounded-tl-sm rounded-bl-sm cursor-pointer transition-colors'
|
||||||
|
>
|
||||||
|
<div className='absolute top-[-14px] right-0 w-[14px] h-[14px] rounded-full bg-white shadow-[5px_5px_0px_0px_rgba(237,234,235,1)]'></div>
|
||||||
|
<div className='absolute bottom-[-14px] right-0 w-[14px] h-[14px] rounded-full bg-white shadow-[5px_-5px_0px_0px_rgba(237,234,235,1)]'></div>
|
||||||
|
{showSideBar ? (
|
||||||
|
<BsLayoutSidebarInsetReverse className='fill-[var(--primary)] hover:text-[var(--primary)] transition-all text-2xl' />
|
||||||
|
) : (
|
||||||
|
<BsLayoutSidebarInset className='fill-[var(--primary)] hover:text-[var(--primary)] transition-all text-2xl' />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Action Buttons */}
|
||||||
|
<div className='absolute bottom-14 right-0 flex flex-col gap-1'>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLogoutModal(true)}
|
||||||
|
className='flex items-center gap-2 px-3 py-2 text-gray-600 hover:bg-red-50 hover:text-red-600 rounded-l-sm transition-all duration-200 group'
|
||||||
|
title="Log out"
|
||||||
|
>
|
||||||
|
<RiLogoutBoxLine className="text-xl group-hover:scale-110 transition-transform" />
|
||||||
|
{/* {showSideBar && <span className="text-sm">Log Out</span>} */}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/s/settings"
|
||||||
|
className='flex items-center gap-2 px-3 py-2 text-gray-600 hover:bg-blue-50 hover:text-blue-600 rounded-l-sm transition-all duration-200 group'
|
||||||
|
title="Settings"
|
||||||
|
>
|
||||||
|
<IoSettingsOutline className="text-xl group-hover:scale-110 transition-transform" />
|
||||||
|
{/* {showSideBar && <span className="text-sm">Settings</span>} */}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logout Confirmation Modal */}
|
||||||
|
{showLogoutModal && (
|
||||||
|
<div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl p-6 w-80">
|
||||||
|
<h3 className="text-lg font-medium text-gray-800 mb-4">Confirm Logout</h3>
|
||||||
|
<p className="text-gray-600 mb-6">Are you sure you want to log out?</p>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowLogoutModal(false)}
|
||||||
|
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLogoutConfirm}
|
||||||
|
className="px-4 py-2 bg-red-600 text-white hover:bg-red-700 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Log Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SideBar;
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { FaCheckCircle, FaClock, FaHourglassHalf, FaTasks } from 'react-icons/fa';
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
title: 'Action Required',
|
||||||
|
count: 12,
|
||||||
|
icon: <FaTasks size={20} className="text-orange-500" />,
|
||||||
|
ring: 'ring-orange-400/20',
|
||||||
|
bg: 'bg-orange-50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Waiting for Others',
|
||||||
|
count: 8,
|
||||||
|
icon: <FaClock size={20} className="text-yellow-500" />,
|
||||||
|
ring: 'ring-yellow-400/20',
|
||||||
|
bg: 'bg-yellow-50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Expiring Soon',
|
||||||
|
count: 5,
|
||||||
|
icon: <FaHourglassHalf size={20} className="text-red-500" />,
|
||||||
|
ring: 'ring-red-400/20',
|
||||||
|
bg: 'bg-red-50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Completed',
|
||||||
|
count: 27,
|
||||||
|
icon: <FaCheckCircle size={20} className="text-green-500" />,
|
||||||
|
ring: 'ring-green-400/20',
|
||||||
|
bg: 'bg-green-50',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const filters = ['Last 7 Days', 'Last 30 Days', 'Last 6 Months', 'This Year'];
|
||||||
|
|
||||||
|
const Cards = () => {
|
||||||
|
const [selectedFilter, setSelectedFilter] = useState('Last 7 Days');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full px-4 py-3">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between mb-5 gap-3">
|
||||||
|
<h2 className="text-lg font-medium text-gray-700">Overview</h2>
|
||||||
|
|
||||||
|
<div className="flex gap-2 w-full md:w-auto">
|
||||||
|
{/* Account Selector */}
|
||||||
|
<select
|
||||||
|
value={'All'}
|
||||||
|
onChange={() => {}}
|
||||||
|
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
|
||||||
|
>
|
||||||
|
<option>All</option>
|
||||||
|
<option>Account 1</option>
|
||||||
|
<option>Account 2</option>
|
||||||
|
<option>Account 3</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Date Filter */}
|
||||||
|
<select
|
||||||
|
value={selectedFilter}
|
||||||
|
onChange={(e) => setSelectedFilter(e.target.value)}
|
||||||
|
className="text-sm text-gray-700 border border-gray-300 rounded-sm px-3 py-1.5 focus:outline-none bg-white shadow-sm"
|
||||||
|
>
|
||||||
|
{filters.map((f) => (
|
||||||
|
<option key={f}>{f}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Cards */}
|
||||||
|
<div className="cursor-pointer grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
{cards.map((card) => (
|
||||||
|
<div
|
||||||
|
key={card.title}
|
||||||
|
className={`rounded-md bg-white/60 backdrop-blur-md border border-gray-200 p-5 transition-transform shadow-md hover:scale-[1.02] hover:shadow-lg`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className={`p-3 rounded-full ${card.bg} ${card.ring}`}>
|
||||||
|
{card.icon}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500">{card.title}</p>
|
||||||
|
<p className="text-xl font-bold text-gray-800">{card.count}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cards;
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { MdFilterList } from "react-icons/md";
|
||||||
|
import { IoCaretDownOutline } from "react-icons/io5";
|
||||||
|
import { FaSort } from "react-icons/fa";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const FilterSort = () => {
|
||||||
|
return (
|
||||||
|
<div className='mb-2'>
|
||||||
|
<div className='flex gap-2'>
|
||||||
|
<div className='flex justify-center items-center max-w-max gap-2 bg-[var(--compBgCol)] text-xs rounded px-2 py-1'>
|
||||||
|
<MdFilterList />
|
||||||
|
<p>Filter</p>
|
||||||
|
<IoCaretDownOutline />
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-center items-center max-w-max gap-2 bg-[var(--compBgCol)] text-xs rounded px-2 py-1'>
|
||||||
|
<p>Sort</p>
|
||||||
|
<FaSort />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterSort
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react'
|
||||||
|
import HomeTopNav from './HomeTopNav'
|
||||||
|
import UploadDoc from './UploadDoc'
|
||||||
|
import FilterSort from './FilterSort'
|
||||||
|
import ShowDocs from './ShowDocs'
|
||||||
|
import Cards from './Cards'
|
||||||
|
import StartSection from './StartSection'
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HomeTopNav />
|
||||||
|
<Cards />
|
||||||
|
<StartSection />
|
||||||
|
{/* <UploadDoc /> */}
|
||||||
|
{/* <FilterSort /> */}
|
||||||
|
{/* <ShowDocs /> */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import { FaCog } from 'react-icons/fa';
|
||||||
|
import { IoIosNotifications } from 'react-icons/io';
|
||||||
|
import { RootState } from '../../store/store';
|
||||||
|
import { UserState } from '../../store/slices/userSlice';
|
||||||
|
|
||||||
|
const HomeTopNav = () => {
|
||||||
|
const user: UserState = useSelector((state: RootState) => state.user);
|
||||||
|
|
||||||
|
const initials = user.username
|
||||||
|
? user.username
|
||||||
|
.split(' ')
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join('')
|
||||||
|
.slice(0, 2)
|
||||||
|
.toUpperCase()
|
||||||
|
: 'U';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
|
||||||
|
<h1 className="text-md font-medium text-gray-800 capitalize">Home</h1>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* Notification + Settings with smaller gap */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
aria-label="Notifications"
|
||||||
|
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
||||||
|
>
|
||||||
|
<IoIosNotifications className="text-xl" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/settings"
|
||||||
|
aria-label="Settings"
|
||||||
|
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
||||||
|
>
|
||||||
|
<FaCog className="text-sm" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Avatar with separate spacing */}
|
||||||
|
<div className="w-9 h-9 ms-3 rounded-full overflow-hidden bg-[var(--primary)] flex items-center justify-center text-white font-semibold text-sm">
|
||||||
|
{user.picture ? (
|
||||||
|
<img
|
||||||
|
src={user.picture}
|
||||||
|
alt="User avatar"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
initials
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomeTopNav;
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { RootState, AppDispatch } from "../../store/store";
|
||||||
|
import { updateSelectedDocuments } from "../../store/slices/selectedDocsSlice";
|
||||||
|
|
||||||
|
import { BsFiletypePdf, BsFiletypeDocx } from "react-icons/bs";
|
||||||
|
import { PiDotsThreeOutlineVerticalFill } from "react-icons/pi";
|
||||||
|
import { fetchDocuments } from "../../store/slices/documentSlice";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { FaCheck } from "react-icons/fa";
|
||||||
|
|
||||||
|
const ShowDocs = () => {
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
// const signTypeGlobalState = useSelector((state: RootState) => state.selectedDocuments.signType);
|
||||||
|
|
||||||
|
const documents = useSelector((state: RootState) => state.documents.documents);
|
||||||
|
|
||||||
|
// Local state to track selected documents and sign type
|
||||||
|
const [selectedDocs, setSelectedDocs] = useState<string[]>([]);
|
||||||
|
// const [signType, setSignType] = useState<"self-sign" | "multiple-sign" | null>(null);
|
||||||
|
|
||||||
|
// Handle checkbox selection
|
||||||
|
const handleSelect = (docId: string) => {
|
||||||
|
setSelectedDocs((prev) =>
|
||||||
|
prev.includes(docId) ? prev.filter((id) => id !== docId) : [...prev, docId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(selectedDocs, "selectedDocsselectedDocs")
|
||||||
|
console.log(selectedDocs.length, "LENGTHselectedDocs")
|
||||||
|
|
||||||
|
// Handle button click to update Redux state
|
||||||
|
const handleUpdateSelectedDocs = () => {
|
||||||
|
const selectedDocuments = documents.filter((doc) => selectedDocs.includes(doc.id));
|
||||||
|
console.log(selectedDocuments, "selected Document Line 34 ShowDocs.tsx")
|
||||||
|
dispatch(updateSelectedDocuments({ documents: selectedDocuments }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Updating Redux State When Anywhere Sign Type is set ["self-sign" | "multiple-sign"]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if (signTypeGlobalState === null) return;
|
||||||
|
handleUpdateSelectedDocs()
|
||||||
|
}, [selectedDocs])
|
||||||
|
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// setTimeout(async () => {
|
||||||
|
// const res = await dispatch(fetchDocuments());
|
||||||
|
// if (res.meta.requestStatus === "rejected") {
|
||||||
|
// Object.keys(localStorage).forEach((key) => {
|
||||||
|
// if (key.startsWith("persist:root-")) {
|
||||||
|
// localStorage.removeItem(key);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// window.location.href = "/login";
|
||||||
|
// }
|
||||||
|
// }, 1000)
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* {documents.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-2 mb-3">
|
||||||
|
{documents.map((doc, idx) => (
|
||||||
|
<div key={doc.id} className="flex justify-between items-center bg-[var(--compBgCol)] px-4 py-2 rounded-md">
|
||||||
|
<div className="text-sm flex gap-4 items-center">
|
||||||
|
<input
|
||||||
|
id={doc.name + idx}
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedDocs.includes(doc.id)}
|
||||||
|
onChange={() => handleSelect(doc.id)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{doc.extension === "pdf" ? (
|
||||||
|
<BsFiletypePdf className="text-lg text-red-400" />
|
||||||
|
) : (
|
||||||
|
<BsFiletypeDocx className="text-lg text-blue-400" />
|
||||||
|
)}
|
||||||
|
<label htmlFor={doc.name + idx}>{doc.name}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm">.{doc.extension}</p>
|
||||||
|
<p className={`text-sm ${doc.original ? "text-blue-400" : "text-green-400"}`}>
|
||||||
|
{doc.original ? "Original" : "Signed"}
|
||||||
|
</p>
|
||||||
|
<CiMenuKebab className="cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
{documents.length > 0 && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full text-xs text-left border border-gray-200 rounded shadow-sm bg-white">
|
||||||
|
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3"></th>
|
||||||
|
<th className="px-4 py-3">Name</th>
|
||||||
|
<th className="px-4 py-3 text-center">Type</th>
|
||||||
|
<th className="px-4 py-3 text-center">Status</th>
|
||||||
|
<th className="px-4 py-3 text-center"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{documents.map((doc, idx) => (
|
||||||
|
<tr onClick={() => handleSelect(doc.id)} key={doc.id} className="hover:bg-gray-50">
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
{/* <input
|
||||||
|
id={doc.name + idx}
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedDocs.includes(doc.id)}
|
||||||
|
onChange={() => handleSelect(doc.id)}
|
||||||
|
className="form-checkbox text-blue-600"
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
<div className="w-5 h-5 rounded border-1 border-slate-300 flex justify-center items-center">
|
||||||
|
{selectedDocs.includes(doc.id) && <FaCheck className="text-[var(--primary)]" />}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<label htmlFor={doc.name + idx} className="flex items-center gap-2 cursor-pointer">
|
||||||
|
{doc.extension === "pdf" ? (
|
||||||
|
<BsFiletypePdf className="text-xl text-red-500" />
|
||||||
|
) : (
|
||||||
|
<BsFiletypeDocx className="text-xl text-blue-500" />
|
||||||
|
)}
|
||||||
|
{doc.name}
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-xs text-gray-600 text-center">
|
||||||
|
<span className="bg-slate-100 px-2.5 py-0.5 rounded-xl">{doc.extension}</span>
|
||||||
|
</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<span className={`${doc.original ? "text-blue-600 bg-blue-100" : "text-green-600 bg-green-100"} text-xs px-2.5 py-1 rounded-xl`}>
|
||||||
|
{doc.original ? "Original" : "Signed"}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{/* <td className="px-4 py-3 text-center">
|
||||||
|
<PiDotsThreeOutlineVerticalFill className="text-lg cursor-pointer text-slate-300 hover:text-black ml-[50%] translate-x-[-50%]" />
|
||||||
|
</td> */}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowDocs;
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import React, { ReactNode, useEffect } from 'react';
|
||||||
|
import { FiUpload, FiEdit, FiFolder, FiArrowRight } from 'react-icons/fi';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { AppDispatch, RootState } from '../../store/store';
|
||||||
|
import api from '../../config/api';
|
||||||
|
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../store/slices/transactionSlice';
|
||||||
|
|
||||||
|
|
||||||
|
interface StartCard {
|
||||||
|
icon: ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
buttonText?: string;
|
||||||
|
bg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
icon: <FiUpload className="w-5 h-5 text-blue-700" />,
|
||||||
|
title: 'Start a Workflow',
|
||||||
|
description: 'Begin by uploading a document and tagging fields.',
|
||||||
|
buttonText: 'Start Now',
|
||||||
|
bg: 'bg-white/60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FiEdit className="w-5 h-5 text-green-700" />,
|
||||||
|
title: 'Sign an Agreement',
|
||||||
|
description: 'Upload a document you need to sign and place your signature.',
|
||||||
|
bg: 'bg-white/60',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FiFolder className="w-5 h-5 text-purple-700" />,
|
||||||
|
title: 'Use a Template',
|
||||||
|
description: 'Pick from ready-made templates for common agreements.',
|
||||||
|
bg: 'bg-white/60',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const StartSection = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch<AppDispatch>()
|
||||||
|
|
||||||
|
const user = useSelector((state: RootState) => state.user);
|
||||||
|
if (!user.account || user.account === null) {
|
||||||
|
window.location.href = "/login";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAccountId = user.account.id;
|
||||||
|
|
||||||
|
const handleStartSectionClick = (card: StartCard) => {
|
||||||
|
if ("buttonText" in card && card.buttonText === "Start Now") {
|
||||||
|
api.post(`/api/accounts/${defaultAccountId}/transactions`)
|
||||||
|
.then(res => {
|
||||||
|
if (res.status === 200 || res.status === 201) {
|
||||||
|
dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED));
|
||||||
|
dispatch(setTransactionId(res.data));
|
||||||
|
setTimeout(() => navigate(`/process-step-one/${defaultAccountId}/${res.data}`), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.log(err))
|
||||||
|
}
|
||||||
|
// navigate("/process-step-one");
|
||||||
|
}
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// try {
|
||||||
|
// axios.get(`${import.meta.env.VITE_BASE_URI}/api/dashboard`, {"withCredentials": true, "ngrok-skip-browser-warning": "69420"}).then((res) => {
|
||||||
|
// console.log(res)
|
||||||
|
// })
|
||||||
|
// } catch (error) {
|
||||||
|
// console.log(error)
|
||||||
|
// }
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="px-4 py-3 w-full">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-800 mb-6">Get Started</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{cards.map((card, index) => (
|
||||||
|
<div
|
||||||
|
onClick={() => handleStartSectionClick(card)}
|
||||||
|
key={index}
|
||||||
|
className={`${card.bg} cursor-pointer relative border border-gray-200 rounded-xl p-4 shadow-sm hover:shadow-md transition-all h-full flex flex-col justify-between min-h-[220px]`}
|
||||||
|
>
|
||||||
|
{/* Icon + Text */}
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="w-9 h-9 flex items-center justify-center border border-gray-200 rounded-md bg-white">
|
||||||
|
{card.icon}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-base font-medium text-gray-900">{card.title}</h3>
|
||||||
|
<p className="text-sm text-gray-700">{card.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Area */}
|
||||||
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
{card.buttonText ? (
|
||||||
|
<button className="text-sm cursor-pointer font-medium text-white bg-gradient-to-r from-blue-600 to-blue-500 px-4 py-2 rounded-md shadow hover:opacity-90 transition">
|
||||||
|
{card.buttonText}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FiArrowRight className="w-4 h-4 text-gray-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StartSection;
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { AppDispatch } from "../../store/store";
|
||||||
|
|
||||||
|
import { FaCloudUploadAlt } from "react-icons/fa";
|
||||||
|
import { MdCancel } from "react-icons/md";
|
||||||
|
import { uploadDocuments } from "../../store/slices/documentSlice";
|
||||||
|
|
||||||
|
import { Bounce, toast } from "react-toastify";
|
||||||
|
|
||||||
|
const UploadDoc: React.FC = () => {
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selectedFiles = e.target.files ? Array.from(e.target.files) : [];
|
||||||
|
if (selectedFiles.length > 0) {
|
||||||
|
setFiles((prevFiles) => [...prevFiles, ...selectedFiles]);
|
||||||
|
handleUpload(selectedFiles);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||||
|
if (droppedFiles.length > 0) {
|
||||||
|
setFiles((prevFiles) => [...prevFiles, ...droppedFiles]);
|
||||||
|
handleUpload(droppedFiles);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = (filesToUpload: File[]) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
filesToUpload.forEach((file) => formData.append("document", file));
|
||||||
|
|
||||||
|
dispatch(uploadDocuments(formData))
|
||||||
|
.unwrap()
|
||||||
|
.then((res) => {
|
||||||
|
console.log("Documents uploaded", res);
|
||||||
|
// toast.dismiss();
|
||||||
|
// toast.info("📄 Documents uploaded!", {
|
||||||
|
// position: "bottom-right",
|
||||||
|
// autoClose: 5000,
|
||||||
|
// hideProgressBar: false,
|
||||||
|
// closeOnClick: false,
|
||||||
|
// pauseOnHover: true,
|
||||||
|
// draggable: true,
|
||||||
|
// progress: undefined,
|
||||||
|
// theme: "light",
|
||||||
|
// transition: Bounce,
|
||||||
|
// });
|
||||||
|
setFiles([]);
|
||||||
|
})
|
||||||
|
.catch((err) => console.error("Failed to upload documents", err));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-full bg-[var(--compBgCol)] rounded border border-gray-200 p-5 mb-2 shadow-sm">
|
||||||
|
<div
|
||||||
|
className="w-full border-1 border-dashed border-gray-300 rounded p-6 text-center transition-all duration-200 hover:border-[var(--primary)] hover:bg-gray-50 cursor-pointer"
|
||||||
|
onClick={() => document.getElementById("file-input")?.click()}
|
||||||
|
onDragOver={(e) => e.preventDefault()}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="file-input"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept=".pdf,.doc,.docx,.jpg,.png,.jpeg,.txt"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-2">
|
||||||
|
<FaCloudUploadAlt className="text-5xl text-[var(--primary)]" />
|
||||||
|
<p className="text-sm font-medium text-gray-700">Upload or drag & drop files</p>
|
||||||
|
<p className="text-xs text-gray-500">PDF, DOCX, JPG, PNG, TXT (max 10MB)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* {files.length > 0 && (
|
||||||
|
<div className="mt-5 text-left">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-700 mb-2">Files</h3>
|
||||||
|
<div className="space-y-2 max-h-40 overflow-y-auto pr-1">
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between bg-white border border-gray-200 rounded-md px-3 py-2 hover:shadow-sm transition"
|
||||||
|
>
|
||||||
|
<span className="truncate text-sm text-gray-800 max-w-[85%]">{file.name}</span>
|
||||||
|
<MdCancel
|
||||||
|
onClick={() => setFiles(files.filter((_, i) => i !== index))}
|
||||||
|
className="text-gray-400 hover:text-red-500 cursor-pointer transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UploadDoc;
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import UserAuthUI from "../auth/UsertAuthUI"
|
||||||
|
|
||||||
|
function Login() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-center items-center w-full h-[100vh]">
|
||||||
|
<UserAuthUI pg={"login"} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { LuSignature } from "react-icons/lu";
|
||||||
|
import { IoMdClose } from "react-icons/io";
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '../../store/store';
|
||||||
|
import { hideModal } from '../../store/slices/modalSlice';
|
||||||
|
import { setSignType } from '../../store/slices/selectedDocsSlice';
|
||||||
|
|
||||||
|
const ProcessStepModal = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const modalState = useSelector((state: RootState) => state.modal);
|
||||||
|
|
||||||
|
const handleCloseBtnClick = () => {
|
||||||
|
dispatch(hideModal());
|
||||||
|
dispatch(hideModal());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='absolute w-full h-full top-0 left-0 transition-all duration-75'>
|
||||||
|
{/* Background */}
|
||||||
|
<div className="z-50 absolute w-screen h-screen flex items-center justify-center bg-[var(--hideModalBg)] opacity-60"></div>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="absolute z-50 left-[50%] translate-x-[-50%] top-[80px] bg-[var(--modalBg)] px-8 py-4 shadow-lg rounded-md">
|
||||||
|
<div className='flex items-center justify-between mb-4'>
|
||||||
|
<p className='text-xl text-slate-600'>Discard Changes</p>
|
||||||
|
<IoMdClose className='text-xl hover:text-[var(--primary)] transition-all duration-150 cursor-pointer' onClick={handleCloseBtnClick} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='text-sm'>
|
||||||
|
<p className='whitespace-nowrap text-slate-500'>All changes you have made to this form will be discarded</p>
|
||||||
|
<div className='flex justify-end gap-2 pt-4'>
|
||||||
|
<button className='rounded-sm px-3 py-1.5 bg-[var(--primaryLight3)] text-[var(--primary)] border-1 border-[var(--primary)]'>Cancel</button>
|
||||||
|
<button className='rounded-sm px-3 py-1.5 bg-red-400 text-white'>Discard</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcessStepModal
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { LuSignature } from "react-icons/lu";
|
||||||
|
import { IoMdClose } from "react-icons/io";
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '../../store/store';
|
||||||
|
import { hideModal } from '../../store/slices/modalSlice';
|
||||||
|
import { setSignType } from '../../store/slices/selectedDocsSlice';
|
||||||
|
|
||||||
|
const SignModal = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const modalState = useSelector((state: RootState) => state.modal);
|
||||||
|
|
||||||
|
const handleCloseBtnClick = () => {
|
||||||
|
dispatch(hideModal());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSignStateUpdate = (type: "self-sign" | "multiple-sign") => {
|
||||||
|
dispatch(setSignType(type))
|
||||||
|
dispatch(hideModal());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='absolute w-full h-full top-0 left-0 transition-all duration-75'>
|
||||||
|
{/* Background */}
|
||||||
|
<div className="z-50 absolute w-screen h-screen flex items-center justify-center bg-[var(--hideModalBg)] opacity-60"></div>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="absolute z-50 left-[50%] translate-x-[-50%] top-[80px] bg-[var(--modalBg)] px-8 py-4 shadow-lg rounded-md">
|
||||||
|
<div className='flex items-center justify-between mb-4'>
|
||||||
|
<p className='text-xl'>Document Uploaded {modalState.message && `( ${modalState.message} )`}</p>
|
||||||
|
<IoMdClose className='text-xl hover:text-[var(--primary)] transition-all duration-150 cursor-pointer' onClick={handleCloseBtnClick} />
|
||||||
|
</div>
|
||||||
|
<div className='flex py-4 gap-4'>
|
||||||
|
|
||||||
|
{/* Self Sign - START */}
|
||||||
|
<div className='w-[226px] flex flex-col justify-center items-center text-center px-8 py-4 border-[1px] border-[var(--greyTextCol)] rounded-md cursor-pointer opacity-60 hover:opacity-100 transition-all duration-150'
|
||||||
|
onClick={() => handleSignStateUpdate('self-sign')}
|
||||||
|
>
|
||||||
|
<LuSignature className='text-4xl text-[var(--primary)]' />
|
||||||
|
<div className='py-3'>
|
||||||
|
<p className='text-[var(--primary)] font-semibold'>Self Signature</p>
|
||||||
|
<p className='whitespace-nowrap text-[var(--greyTextCol)]'>You're the only signer.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Self Sign - END */}
|
||||||
|
|
||||||
|
{/* Multi Sign - START */}
|
||||||
|
<div className='w-[226px] flex flex-col justify-center items-center text-center px-8 py-4 border-[1px] border-[var(--greyTextCol)] rounded-md cursor-pointer opacity-60 hover:opacity-100 transition-all duration-150'
|
||||||
|
onClick={() => handleSignStateUpdate('multiple-sign')}
|
||||||
|
>
|
||||||
|
<LuSignature className='text-4xl text-[var(--primary)]' />
|
||||||
|
<div className='py-3'>
|
||||||
|
<p className='text-[var(--primary)] font-semibold'>Send for Signature</p>
|
||||||
|
<p className='whitespace-nowrap text-[var(--greyTextCol)]'>You and other need to sign.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Multi Sign - END */}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignModal
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const ProcessTopNav = () => {
|
||||||
|
return (
|
||||||
|
<div>ProcessTopNav</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcessTopNav
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FaPlus } from "react-icons/fa";
|
||||||
|
|
||||||
|
type RecipientAddCardProps = {
|
||||||
|
addRecipient: (name: string, email: string, type: "mailToSign" | "inPerson" | "cc") => void; // Adjust the parameter type as needed
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecipientAddCard: React.FC<RecipientAddCardProps> = ({ addRecipient }) => {
|
||||||
|
|
||||||
|
const [recipient] = useState()
|
||||||
|
|
||||||
|
const handleRecipientAdd = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-[var(--primaryLight)] shadow-sm mb-4 w-[650px] rounded-md">
|
||||||
|
<p className="pb-4 text-md">Add Recipient Details</p>
|
||||||
|
<div className="py-1">
|
||||||
|
<input
|
||||||
|
|
||||||
|
type="text"
|
||||||
|
placeholder="Recipient Name"
|
||||||
|
className="w-full p-2 rounded mb-2 bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="py-1">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Recipient Email"
|
||||||
|
className="w-full p-2 rounded mb-2 bg-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between py-2">
|
||||||
|
<div className="flex gap-4 justify-end">
|
||||||
|
{[
|
||||||
|
{ label: "Mail To Sign", value: "mailToSign" },
|
||||||
|
{ label: "In-Person", value: "inPerson" },
|
||||||
|
{ label: "CC", value: "cc" }
|
||||||
|
].map((option) => (
|
||||||
|
<label key={option.value} className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={`send-type`}
|
||||||
|
value={option.value}
|
||||||
|
defaultChecked={option.value === "mailToSign"}
|
||||||
|
className="accent-blue-500"
|
||||||
|
/>
|
||||||
|
{option.label}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<button
|
||||||
|
// onClick={addRecipient}
|
||||||
|
className="flex cursor-pointer items-center gap-2 bg-green-500 text-white px-4 py-2 rounded-lg shadow-md hover:bg-green-600"
|
||||||
|
>
|
||||||
|
<FaPlus /> Add Recipient
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecipientAddCard;
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { ContactTypeObj } from './RecipientAddCard'
|
||||||
|
import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
|
||||||
|
|
||||||
|
interface ContactDataProps {
|
||||||
|
contactData: ContactTypeObj;
|
||||||
|
inpFirstName: string;
|
||||||
|
inpLastName: string;
|
||||||
|
inpEmail: string;
|
||||||
|
openContactModal: boolean;
|
||||||
|
setOpenContactModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setRecipients: React.Dispatch<React.SetStateAction<Recipient>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContactModal: React.FC<ContactDataProps> = ({ contactData, inpFirstName, inpLastName, inpEmail, openContactModal, setRecipients, setOpenContactModal }) => {
|
||||||
|
|
||||||
|
const handleContactClick = (indx: number) => {
|
||||||
|
if (!contactData.contacts.length) return;
|
||||||
|
|
||||||
|
setRecipients((prev) => {
|
||||||
|
return {
|
||||||
|
firstName: contactData.contacts[indx].firstName,
|
||||||
|
lastName: contactData.contacts[indx].lastName,
|
||||||
|
email: contactData.contacts[indx].email,
|
||||||
|
type: prev.type
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpenContactModal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
contactData.contacts.map((recipient, indx) => {
|
||||||
|
return <div
|
||||||
|
tabIndex={1}
|
||||||
|
onClick={() => handleContactClick(indx)}
|
||||||
|
style={{display: `${openContactModal ? "flex" : "none"}`}}
|
||||||
|
className='flex items-center px-3 py-2 cursor-pointer hover:bg-slate-100 transiton-all w-full'>
|
||||||
|
<div
|
||||||
|
key={indx}
|
||||||
|
className='w-8 h-8 bg-gray-200 rounded-full'></div>
|
||||||
|
<div className='ml-2'>
|
||||||
|
<div className='flex gap-1.5'>
|
||||||
|
<p className='text-sm font-semibold'
|
||||||
|
style={{ background: `${contactData.type === "firstName" ? `linear-gradient(to right, yellow 0ch, yellow ${inpFirstName.length}ch, white ${inpFirstName.length}ch, white 100%)` : "white"}` }}
|
||||||
|
>
|
||||||
|
{recipient.firstName}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{ background: `${contactData.type === "lastName" ? `linear-gradient(to right, yellow 0ch, yellow ${inpLastName.length}ch, white ${inpLastName.length}ch, white 100%)` : "white"}` }}
|
||||||
|
className='text-sm font-semibold'>
|
||||||
|
{recipient.lastName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className='text-xs text-gray-500'
|
||||||
|
style={{ background: `${contactData.type === "email" ? `linear-gradient(to right, yellow 0ch, yellow ${inpEmail.length}ch, white ${inpEmail.length}ch, white 100%)` : "white"}` }}
|
||||||
|
>
|
||||||
|
{recipient.email}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return <p
|
||||||
|
// key={indx}
|
||||||
|
// className='w-full hover:bg-slate-100 cursor-pointer px-2 py-1'>
|
||||||
|
// {val.firstName}
|
||||||
|
// </p>
|
||||||
|
export default ContactModal
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { FaFileAlt, FaCheckCircle, FaUpload } from 'react-icons/fa'
|
||||||
|
import { MdFileUpload } from 'react-icons/md'
|
||||||
|
import { RiDraggable } from 'react-icons/ri'
|
||||||
|
import { deleteDocument, setDocProgComplete } from '../../../store/slices/documentSlice'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { AppDispatch } from '../../../store/store'
|
||||||
|
import { IoMdClose } from 'react-icons/io'
|
||||||
|
import api from '../../../config/api'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
interface DocumentCardProps {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isCompleted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentCard: React.FC<DocumentCardProps> = ({ id, name, isCompleted }) => {
|
||||||
|
|
||||||
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
|
if (!accountId || !transactionId) {
|
||||||
|
window.location.href = "/";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [progress, setProgress] = useState<number>(0)
|
||||||
|
const [message, setMessage] = useState<string>('')
|
||||||
|
const [documentDel, setDocumentDel] = useState<{ docId: string | null, isDel: boolean }>({ docId: null, isDel: false })
|
||||||
|
|
||||||
|
const dispatch = useDispatch<AppDispatch>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isCompleted = false;
|
||||||
|
let updateTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let lastUpdate = 0;
|
||||||
|
const delay = 2000;
|
||||||
|
|
||||||
|
const docProgSSE = new EventSource(`http://192.168.2.112:8083/api/progress/${id}`);
|
||||||
|
|
||||||
|
docProgSSE.addEventListener('progress', (event) => {
|
||||||
|
const jsonData = JSON.parse(event.data);
|
||||||
|
console.log(jsonData, "Kaboom");
|
||||||
|
|
||||||
|
if (jsonData.isComplete) {
|
||||||
|
isCompleted = true;
|
||||||
|
if (updateTimeout) clearTimeout(updateTimeout);
|
||||||
|
dispatch(setDocProgComplete(id));
|
||||||
|
setProgress(jsonData.progress);
|
||||||
|
setMessage(jsonData.message);
|
||||||
|
docProgSSE.close(); // Close connection ourselves
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const sinceLast = now - lastUpdate;
|
||||||
|
|
||||||
|
if (sinceLast >= delay) {
|
||||||
|
setProgress(jsonData.progress);
|
||||||
|
setMessage(jsonData.message);
|
||||||
|
lastUpdate = now;
|
||||||
|
} else {
|
||||||
|
if (updateTimeout) clearTimeout(updateTimeout);
|
||||||
|
updateTimeout = setTimeout(() => {
|
||||||
|
setProgress(jsonData.progress);
|
||||||
|
setMessage(jsonData.message);
|
||||||
|
lastUpdate = Date.now();
|
||||||
|
}, delay - sinceLast);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
docProgSSE.onerror = (error) => {
|
||||||
|
if (!isCompleted) {
|
||||||
|
console.error("SSE Error", error);
|
||||||
|
}
|
||||||
|
docProgSSE.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
docProgSSE.close();
|
||||||
|
if (updateTimeout) clearTimeout(updateTimeout);
|
||||||
|
};
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
console.log(isCompleted, "line 118")
|
||||||
|
|
||||||
|
const radius = 28
|
||||||
|
const circumference = 2 * Math.PI * radius
|
||||||
|
const offset = circumference - (progress / 100) * circumference
|
||||||
|
|
||||||
|
const handleDeleteDocument = async () => {
|
||||||
|
const docId = documentDel.docId;
|
||||||
|
if (!docId || !accountId || !transactionId) {
|
||||||
|
console.error("Missing document ID, account ID, or transaction ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await api.delete(`/api/accounts/${accountId}/transactions/${transactionId}/documents/${docId}`)
|
||||||
|
if (res.status === 202) {
|
||||||
|
dispatch(deleteDocument(docId));
|
||||||
|
setDocumentDel({ docId: null, isDel: false });
|
||||||
|
console.log("Document deleted successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<div
|
||||||
|
key={id}
|
||||||
|
className="relative w-[160px] h-[170px] bg-white rounded-xl shadow-sm hover:shadow-md transition p-4 flex flex-col items-center justify-between"
|
||||||
|
draggable
|
||||||
|
>
|
||||||
|
<IoMdClose
|
||||||
|
onClick={() => setDocumentDel({ docId: id, isDel: false }) }
|
||||||
|
className='absolute right-2.5 top-2.5 text-slate-400 text-xl hover:text-red-500 cursor-pointer transition-all' />
|
||||||
|
{/* Draggable handle */}
|
||||||
|
<RiDraggable
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => e.dataTransfer.setData('text/plain', id)}
|
||||||
|
className="absolute top-2 left-2 text-gray-400 text-lg cursor-grab drag-handle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main icon or circular loader */}
|
||||||
|
<div className="flex flex-col items-center mt-2 mb-1">
|
||||||
|
{!isCompleted ? (
|
||||||
|
<div className="relative w-16 h-16">
|
||||||
|
<svg width="64" height="64" className="rotate-[-90deg]">
|
||||||
|
<circle
|
||||||
|
cx="32"
|
||||||
|
cy="32"
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="#e5e7eb"
|
||||||
|
strokeWidth="6"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="32"
|
||||||
|
cy="32"
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="#3b82f6"
|
||||||
|
strokeWidth="6"
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
strokeDashoffset={offset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
className="transition-all duration-300 ease-in-out"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center text-xs font-semibold text-gray-700 select-none">
|
||||||
|
{progress}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<FaFileAlt className="text-gray-600 text-4xl mb-1" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Document name */}
|
||||||
|
<span className="text-center text-sm text-gray-800 font-medium truncate max-w-[14ch] leading-snug">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Status message */}
|
||||||
|
<span className="text-xs text-gray-500 mt-1">{message}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
documentDel.docId !== null && <div className="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl p-6 w-80">
|
||||||
|
<h3 className="text-lg font-medium text-gray-800 mb-4">Confirm Delete</h3>
|
||||||
|
<p className="text-gray-600 mb-6">Are you sure you want to delete?</p>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setDocumentDel({ docId: null, isDel: false })}
|
||||||
|
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDeleteDocument}
|
||||||
|
className="px-4 py-2 bg-red-600 text-white hover:bg-red-700 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocumentCard
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
import { IoMdClose } from "react-icons/io";
|
||||||
|
|
||||||
|
import RecipientAddCard from './RecipientAddCard';
|
||||||
|
import RecipientCards from './RecipientCards';
|
||||||
|
import SelectedDocuments from "./SelectedDocuments";
|
||||||
|
import api from "../../../config/api";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../../../store/store";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { setImages } from "../../../store/slices/selectedDocsSlice";
|
||||||
|
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
|
||||||
|
import { setTransactionId, TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from "../../../store/slices/transactionSlice";
|
||||||
|
import { hideLoader, showLoader } from "../../../store/slices/loaderSlice";
|
||||||
|
import Loader from "../../dashboard/Loader";
|
||||||
|
import { Bounce, toast } from "react-toastify";
|
||||||
|
import { fetchDocuments, resetDocuments, setDocuments, uploadDocuments } from "../../../store/slices/documentSlice";
|
||||||
|
|
||||||
|
const ProcessStepOne = () => {
|
||||||
|
interface EmailDtls {
|
||||||
|
subject: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
|
if (!accountId || !transactionId) {
|
||||||
|
window.location.href = "/";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
||||||
|
// const { selectedDocuments, signType } = useSelector((state: RootState) => state.selectedDocuments)
|
||||||
|
|
||||||
|
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
||||||
|
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
|
||||||
|
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder)
|
||||||
|
// const user = useSelector((state: RootState) => state.user)
|
||||||
|
|
||||||
|
const [emailDtls, setEmailDtls] = useState<EmailDtls>({ subject: "Documents for Signature", message: "" });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(hideLoader());
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleNextClick = async (isSendNow: boolean) => {
|
||||||
|
if (!selectedDocuments.length || !selectedRecipients.length) {
|
||||||
|
console.log(selectedDocuments, 'line 193')
|
||||||
|
toast.dismiss();
|
||||||
|
toast.error("Add Documents | Recipients First", {
|
||||||
|
position: "top-center",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dispatch(showLoader("Processing Documents..."));
|
||||||
|
|
||||||
|
const documentsData = selectedDocuments.map((doc, index) => ({
|
||||||
|
name: doc.name,
|
||||||
|
documentId: doc.id,
|
||||||
|
order: index,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await api.patch(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, { "documents": documentsData });
|
||||||
|
|
||||||
|
await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`).then(res => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
dispatch(setDocuments(res.data));
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(showLoader("Processing Recipients..."));
|
||||||
|
|
||||||
|
const recipientsData = selectedRecipients.map((recipient, index) => ({
|
||||||
|
// index,
|
||||||
|
routingOrder: "routingOrder" in recipient && recipient.routingOrder ? recipient.routingOrder : 1,
|
||||||
|
type: recipient.type,
|
||||||
|
firstName: recipient.firstName,
|
||||||
|
lastName: recipient.lastName,
|
||||||
|
authType: "NONE",
|
||||||
|
deliveryMethod: "EMAIL",
|
||||||
|
email: recipient.email,
|
||||||
|
hostName: recipient.hostName,
|
||||||
|
hostEmail: recipient.hostEmail,
|
||||||
|
company: "",
|
||||||
|
title: "",
|
||||||
|
website: "",
|
||||||
|
phone: "",
|
||||||
|
accessCode: "",
|
||||||
|
accessCodeDelivery: true,
|
||||||
|
language: "en",
|
||||||
|
name: `${recipient.firstName} ${recipient.lastName}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(recipientsData, "printing Recipient Data Line 114");
|
||||||
|
|
||||||
|
// await api.post(`/api/accounts/${accountId}/transactions/${transactionId}/recipients`, recipientsData).then((res) => {
|
||||||
|
// console.log(res, "Response Line 117")
|
||||||
|
// }).catch((err) => {
|
||||||
|
// console.log(err)
|
||||||
|
// })
|
||||||
|
|
||||||
|
dispatch(addRecipientAsync({recipientsData, accountId, transactionId}));
|
||||||
|
|
||||||
|
dispatch(hideLoader());
|
||||||
|
|
||||||
|
if (isSendNow) {
|
||||||
|
dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_SEND_SUCCESS));
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate("/");
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
// Preview & Tag
|
||||||
|
// Assuming you'd fetch or pass doc/recipients response here
|
||||||
|
// dispatch(setImages(...));
|
||||||
|
// dispatch(updateRecipientIds(...));
|
||||||
|
// dispatch(setTransactionId(...));
|
||||||
|
dispatch(resetDocuments())
|
||||||
|
dispatch(resetRecipients())
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate(`/process-step-two/${accountId}/${transactionId}`);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(hideLoader());
|
||||||
|
console.error("Error submitting data step-by-step:", error);
|
||||||
|
toast.error("Something went wrong. Please try again.", {
|
||||||
|
position: "top-center",
|
||||||
|
autoClose: 5000,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(updateTransactionStatus(TRANSACTION_STATUS_ENUMS.TRANSACTION_INITIATED))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dispatch(resetDocuments())
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalLoader = useSelector((state: RootState) => state.loader)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none h-screen overflow-hidden' : ''}`}>
|
||||||
|
|
||||||
|
{/* Top Navigation */}
|
||||||
|
<div className='fixed top-0 w-full flex items-center justify-between bg-white shadow z-50 px-4 py-3'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<IoMdClose
|
||||||
|
onClick={handleClose}
|
||||||
|
className='text-2xl text-slate-500 cursor-pointer hover:text-[var(--primary)] transition'
|
||||||
|
/>
|
||||||
|
<p className="text-base font-normal text-gray-700 border-l pl-4">Add Recipient and Message</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-3'>
|
||||||
|
<button
|
||||||
|
onClick={() => handleNextClick(true)}
|
||||||
|
className="text-sm px-4 py-2 rounded border border-gray-300 hover:bg-gray-100 transition"
|
||||||
|
>
|
||||||
|
Send Now
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleNextClick(false)}
|
||||||
|
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition"
|
||||||
|
>
|
||||||
|
Add Tags
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="relative max-w-4xl mx-auto pt-[80px] px-4 pb-10">
|
||||||
|
|
||||||
|
{/* Documents */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-lg font-medium mb-4 text-[var(--processStepOneTextHead)]">Documents</h2>
|
||||||
|
<SelectedDocuments />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Recipients */}
|
||||||
|
<section className="mt-8">
|
||||||
|
<h2 className="text-lg font-medium mb-4 pt-6 border-t text-[var(--processStepOneTextHead)]">Signers</h2>
|
||||||
|
<div className="flex flex-col gap-4 align-start">
|
||||||
|
<RecipientCards />
|
||||||
|
{/* {signType !== "self-sign" && <RecipientAddCard />} */}
|
||||||
|
<RecipientAddCard />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<section className="mt-8">
|
||||||
|
<h2 className="text-lg font-medium mb-4 pt-6 border-t text-[var(--processStepOneTextHead)]">Email</h2>
|
||||||
|
|
||||||
|
{/* Subject */}
|
||||||
|
<div className='flex flex-col mb-4'>
|
||||||
|
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Subject</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={emailDtls.subject || "Documents for Signature"}
|
||||||
|
onChange={(e) => setEmailDtls({ ...emailDtls, subject: e.target.value })}
|
||||||
|
placeholder="Email Subject"
|
||||||
|
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message */}
|
||||||
|
<div className='flex flex-col'>
|
||||||
|
<label className='text-sm font-medium text-[var(--greyTextCol)] mb-1'>Message for Signers</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
value={emailDtls.message}
|
||||||
|
onChange={(e) => setEmailDtls({ ...emailDtls, message: e.target.value })}
|
||||||
|
placeholder="Message"
|
||||||
|
className="w-full px-3 py-2 rounded border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{globalLoader.isLoading && <Loader />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcessStepOne
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { FaPlus } from "react-icons/fa";
|
||||||
|
import { addRecipient, Recipient, recipientType } from "../../../store/slices/selectedRecipientsSlice";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { AppDispatch } from "../../../store/store";
|
||||||
|
import api from "../../../config/api";
|
||||||
|
import ContactModal from "./ContactModal";
|
||||||
|
import useDebouncedEffect from "../../../utlity/useDebounce";
|
||||||
|
|
||||||
|
interface ContactType {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContactTypeObj {
|
||||||
|
type: string;
|
||||||
|
contacts: ContactType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecipientAddCard = () => {
|
||||||
|
|
||||||
|
const [contactData, setContactData] = useState<ContactTypeObj>({ type: "", contacts: [] });
|
||||||
|
const [openContactModal, setOpenContactModal] = useState(false);
|
||||||
|
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const formRef = useRef<HTMLFormElement | null>(null);
|
||||||
|
|
||||||
|
const [recipient, setRecipients] = useState<Recipient>({ firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", type: recipientType.SIGNER });
|
||||||
|
|
||||||
|
const handleRecipientAdd = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (recipient.firstName === "" || recipient.lastName === "" || recipient.email === "" || !recipient.type) return;
|
||||||
|
|
||||||
|
// Validate Email Format
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(recipient.email)) {
|
||||||
|
console.log("Invalid email format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Validation Ends
|
||||||
|
|
||||||
|
dispatch(addRecipient(recipient));
|
||||||
|
setRecipients({ firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", type: recipientType.SIGNER });
|
||||||
|
formRef.current?.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!recipient.firstName.length) return;
|
||||||
|
|
||||||
|
// const handler = setTimeout(() => {
|
||||||
|
// api.get(`/api/contact?firstName=${recipient.firstName}`)
|
||||||
|
// .then(res => {
|
||||||
|
// setContactData({ type: "firstName", contacts: res.data.content });
|
||||||
|
// })
|
||||||
|
// .catch(err => console.log(err));
|
||||||
|
// }, 500); // wait 500ms after the last change
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// clearTimeout(handler); // cancel previous timeout if effect runs again
|
||||||
|
// };
|
||||||
|
|
||||||
|
// }, [recipient.firstName]);
|
||||||
|
|
||||||
|
// useDebouncedEffect(() => {
|
||||||
|
// if (!recipient.firstName.length) return;
|
||||||
|
// api.get(`/api/contact?firstName=${recipient.firstName}`)
|
||||||
|
// .then(res => {
|
||||||
|
// setContactData({ type: "firstName", contacts: res.data.content });
|
||||||
|
// })
|
||||||
|
// .catch(err => console.log(err));
|
||||||
|
// }, [recipient.firstName], 500);
|
||||||
|
|
||||||
|
// useDebouncedEffect(() => {
|
||||||
|
// if (!recipient.lastName.length) return
|
||||||
|
// api.get(`/api/contact?lastName=${recipient.lastName}`)
|
||||||
|
// .then(res => {
|
||||||
|
// setContactData({ type: "lastName", contacts: res.data.content });
|
||||||
|
// })
|
||||||
|
// .catch(err => console.log(err))
|
||||||
|
// }, [recipient.lastName], 500);
|
||||||
|
|
||||||
|
// useDebouncedEffect(() => {
|
||||||
|
// if (!recipient.email.length) return
|
||||||
|
// api.get(`/api/contact?email=${recipient.email}`)
|
||||||
|
// .then(res => {
|
||||||
|
// setContactData({ type: "email", contacts: res.data.content });
|
||||||
|
// })
|
||||||
|
// .catch(err => console.log(err))
|
||||||
|
// }, [recipient.email], 500);
|
||||||
|
|
||||||
|
const handleContactModalBlur = () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setOpenContactModal(false)
|
||||||
|
}, 350)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
ref={formRef}
|
||||||
|
onSubmit={handleRecipientAdd}
|
||||||
|
className="w-full bg-white border border-gray-200 rounded-lg shadow-md px-6 py-6 space-y-5 relative"
|
||||||
|
>
|
||||||
|
{/* Name Inputs */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
onFocus={() => setOpenContactModal(true)}
|
||||||
|
onBlur={handleContactModalBlur}
|
||||||
|
value={recipient.firstName}
|
||||||
|
placeholder="First Name"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
|
||||||
|
onChange={(e) => setRecipients({ ...recipient, firstName: e.target.value })}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
onFocus={() => setOpenContactModal(true)}
|
||||||
|
onBlur={handleContactModalBlur}
|
||||||
|
value={recipient.lastName}
|
||||||
|
placeholder="Last Name"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
|
||||||
|
onChange={(e) => setRecipients({ ...recipient, lastName: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{recipient.type === recipientType.INPERSONSIGNER && <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
onFocus={() => setOpenContactModal(true)}
|
||||||
|
onBlur={handleContactModalBlur}
|
||||||
|
value={recipient.hostName}
|
||||||
|
placeholder="Host Name"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
|
||||||
|
onChange={(e) => setRecipients({ ...recipient, hostName: e.target.value })}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
onFocus={() => setOpenContactModal(true)}
|
||||||
|
onBlur={handleContactModalBlur}
|
||||||
|
value={recipient.hostEmail}
|
||||||
|
placeholder="Host Email"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
|
||||||
|
onChange={(e) => setRecipients({ ...recipient, hostEmail: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{/* Email Input */}
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
onFocus={() => setOpenContactModal(true)}
|
||||||
|
onBlur={handleContactModalBlur}
|
||||||
|
value={recipient.email}
|
||||||
|
placeholder="Recipient Email"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] transition"
|
||||||
|
onChange={(e) => setRecipients({ ...recipient, email: e.target.value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Contact Modal */}
|
||||||
|
{contactData.contacts.length > 0 && openContactModal && (
|
||||||
|
<div className="absolute z-50 mt-1 w-full max-w-2xl bg-white shadow-lg rounded-md p-3 border border-gray-100">
|
||||||
|
<ContactModal
|
||||||
|
contactData={contactData}
|
||||||
|
inpFirstName={recipient.firstName}
|
||||||
|
inpLastName={recipient.lastName}
|
||||||
|
inpEmail={recipient.email}
|
||||||
|
openContactModal={openContactModal}
|
||||||
|
setRecipients={setRecipients}
|
||||||
|
setOpenContactModal={setOpenContactModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Signing options + Add button in one line */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<div className="flex flex-wrap gap-6">
|
||||||
|
{[
|
||||||
|
{ label: "Signer", value: recipientType.SIGNER },
|
||||||
|
{ label: "In Person Signer", value: recipientType.INPERSONSIGNER },
|
||||||
|
{ label: "CC", value: recipientType.CC },
|
||||||
|
].map((option) => (
|
||||||
|
<label key={option.value} className="flex items-center gap-2 text-gray-700 text-sm cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="recipient-type"
|
||||||
|
value={option.value}
|
||||||
|
defaultChecked={option.value === recipientType.SIGNER}
|
||||||
|
onClick={(e) => setRecipients({ ...recipient, type: e.target.value })}
|
||||||
|
className="accent-[var(--primary)] cursor-pointer"
|
||||||
|
/>
|
||||||
|
{option.label}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex items-center gap-2 bg-[var(--primary)] hover:bg-[var(--primaryLight2)] text-white text-sm font-semibold px-5 py-2 rounded-md transition"
|
||||||
|
>
|
||||||
|
<FaPlus size={14} /> Add Recipient
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecipientAddCard;
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
removeSelectedRecipient,
|
||||||
|
// setSigningOrder,
|
||||||
|
updateRecipientOrder, // 👈 You need to define this in your slice
|
||||||
|
} from "../../../store/slices/selectedRecipientsSlice";
|
||||||
|
import { AppDispatch, RootState } from "../../../store/store";
|
||||||
|
|
||||||
|
import { ImBin } from "react-icons/im";
|
||||||
|
import { FaUsersLine } from "react-icons/fa6";
|
||||||
|
import { useState } from "react";
|
||||||
|
import SigningOrderDiagram from "../../../SigningOrderDiagram ";
|
||||||
|
import { IoCloseOutline } from "react-icons/io5";
|
||||||
|
|
||||||
|
const getInitials = (name: string) => {
|
||||||
|
if (!name) return "";
|
||||||
|
return name
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join("")
|
||||||
|
.toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const RecipientCards = () => {
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const recipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
||||||
|
// const signingOrder = useSelector((state: RootState) => state.selectedRecipients.signingOrder);
|
||||||
|
const signType = useSelector((state: RootState) => state.selectedDocuments.signType);
|
||||||
|
|
||||||
|
const [signingOrderModal, setSigningOrderModal] = useState({ open: false });
|
||||||
|
|
||||||
|
const handleOrderChange = (id: string, value: number) => {
|
||||||
|
dispatch(updateRecipientOrder({ id, routingOrder: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveRecipient = (recipient) => {
|
||||||
|
dispatch(removeSelectedRecipient(recipient))
|
||||||
|
if (recipients.length === 2) {
|
||||||
|
let leftRecipient = recipients.filter((obj) => {
|
||||||
|
if ("id" in obj && "id" in recipient) {
|
||||||
|
if (obj.id !== recipient.id) return true
|
||||||
|
}
|
||||||
|
if ("email" in obj && "email" in recipient) {
|
||||||
|
if (obj.email !== recipient.email) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
dispatch(updateRecipientOrder({ id: leftRecipient[0].id || leftRecipient[0].email, routingOrder: 1 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full bg-white rounded-md shadow-sm border border-gray-200">
|
||||||
|
{recipients.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{recipients.length > 1 && (
|
||||||
|
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-300">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-700">
|
||||||
|
Recipients ({recipients.length})
|
||||||
|
</h3>
|
||||||
|
{/* <label className="flex items-center cursor-pointer select-none">
|
||||||
|
<span className="mr-2 text-xs font-medium text-gray-600">Signing Order</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={signingOrder}
|
||||||
|
onChange={() => dispatch(setSigningOrder(!signingOrder))}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-9 h-5 bg-gray-300 rounded-full peer-checked:bg-[var(--primary)] relative transition-colors duration-300">
|
||||||
|
<span
|
||||||
|
className={`absolute left-1 top-1/2 w-3.5 h-3.5 bg-white rounded-full shadow transform transition-transform duration-300 -translate-y-1/2 ${signingOrder ? "translate-x-4" : "translate-x-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</label> */}
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer select-none"
|
||||||
|
onClick={() => setSigningOrderModal({ ...signingOrderModal, open: true })}
|
||||||
|
>
|
||||||
|
<span className="text-xs font-medium text-gray-600">Signing Order</span>
|
||||||
|
<FaUsersLine className="text-[var(--primary)] mt-0.5 text-lg" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col divide-y divide-gray-200">
|
||||||
|
{recipients.map((recipient, idx) => (
|
||||||
|
<div
|
||||||
|
key={recipient.email + idx}
|
||||||
|
className="flex items-center justify-between px-4 py-3 hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
|
{/* {signingOrder && ( */}
|
||||||
|
{recipients.length > 1 ? <input
|
||||||
|
type="number"
|
||||||
|
value={recipient.routingOrder || 1}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
|
||||||
|
}
|
||||||
|
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]"
|
||||||
|
/> : <input
|
||||||
|
type="number"
|
||||||
|
disabled
|
||||||
|
value={recipient.routingOrder || 1}
|
||||||
|
min={1}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleOrderChange(recipient.id || recipient.email, Number(e.target.value))
|
||||||
|
}
|
||||||
|
className="w-8 text-center rounded text-sm text-gray-700 border-none focus:outline-none focus:ring-[var(--primary)]"
|
||||||
|
/>}
|
||||||
|
{/* )} */}
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-[var(--primary)] text-white flex items-center justify-center font-semibold text-xs select-none">
|
||||||
|
{getInitials(`${recipient.firstName} ${recipient.lastName || ""}`)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-semibold text-gray-800 truncate text-sm">
|
||||||
|
{recipient.firstName} {recipient.lastName || ""}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 truncate">
|
||||||
|
{recipient.email} | {recipient.type}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{signType !== "self-sign" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={`Remove recipient ${recipient.firstName}`}
|
||||||
|
onClick={() => handleRemoveRecipient(recipient)}
|
||||||
|
className="text-gray-400 hover:text-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
<ImBin size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="p-6 text-center text-gray-500 font-medium text-sm">
|
||||||
|
No Recipients Added
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{signingOrderModal.open &&
|
||||||
|
<div className="fixed top-0 left-0 inset-0 z-50 bg-black/50 backdrop-blur-sm flex justify-center items-center">
|
||||||
|
<div className="flex-col relative justify-center items-center max-w-[80%] max-h-[60vh] h-full overflow-auto">
|
||||||
|
<div className="pb-2 text-white flex justify-between items-center">
|
||||||
|
<p className="font-semibold">Signing Order</p>
|
||||||
|
<div
|
||||||
|
onClick={() => setSigningOrderModal({...signingOrderModal, open: false})}
|
||||||
|
className="text-xl cursor-pointer hover:text-gray-200 transition-all">
|
||||||
|
<IoCloseOutline />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SigningOrderDiagram signers={recipients} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecipientCards;
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
import { useState, useRef } from 'react';
|
||||||
|
import { ReactSortable } from 'react-sortablejs';
|
||||||
|
import { RiDraggable } from "react-icons/ri";
|
||||||
|
import { FaFileAlt, FaPlus, FaGoogleDrive, FaDropbox, FaMicrosoft, FaCloudUploadAlt } from "react-icons/fa";
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
// import { setSelectedDocuments } from '../../../store/slices/selectedDocsSlice';
|
||||||
|
import { setDocuments, uploadDocuments } from '../../../store/slices/documentSlice';
|
||||||
|
import { RootState, AppDispatch } from '../../../store/store';
|
||||||
|
import { IoMdClose } from 'react-icons/io';
|
||||||
|
import { Bounce, toast } from "react-toastify";
|
||||||
|
import api from '../../../config/api';
|
||||||
|
import DocumentCard from './DocumentCard';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
const SelectedDocuments = () => {
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
const documents = useSelector((state: RootState) => state.documents.documents);
|
||||||
|
|
||||||
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
|
if (!accountId || !transactionId) {
|
||||||
|
window.location.href = "/";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [isModalOpen, setModalOpen] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState('Upload');
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ name: 'Upload', icon: <FaPlus /> },
|
||||||
|
{ name: 'Google Drive', icon: <FaGoogleDrive /> },
|
||||||
|
{ name: 'OneDrive', icon: <FaMicrosoft /> },
|
||||||
|
{ name: 'Dropbox', icon: <FaDropbox /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selectedFiles = e.target.files ? Array.from(e.target.files) : [];
|
||||||
|
if (selectedFiles.length > 0) {
|
||||||
|
setFiles((prev) => [...prev, ...selectedFiles]);
|
||||||
|
handleUpload(selectedFiles);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const droppedFiles = Array.from(e.dataTransfer.files);
|
||||||
|
if (droppedFiles.length > 0) {
|
||||||
|
setFiles((prev) => [...prev, ...droppedFiles]);
|
||||||
|
handleUpload(droppedFiles);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = (filesToUpload: File[]) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", filesToUpload[0])
|
||||||
|
console.log(formData, "Line 59")
|
||||||
|
// filesToUpload.forEach((file) => formData.append("document", file));
|
||||||
|
dispatch(uploadDocuments({ formData, accountId, transactionId }))
|
||||||
|
.unwrap()
|
||||||
|
.then((res) => {
|
||||||
|
toast.dismiss();
|
||||||
|
// toast.info("📄 Documents uploaded!", {
|
||||||
|
// position: "bottom-right",
|
||||||
|
// autoClose: 5000,
|
||||||
|
// hideProgressBar: false,
|
||||||
|
// closeOnClick: false,
|
||||||
|
// pauseOnHover: true,
|
||||||
|
// draggable: true,
|
||||||
|
// progress: undefined,
|
||||||
|
// theme: "light",
|
||||||
|
// transition: Bounce,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Update selected documents after upload
|
||||||
|
// dispatch(setSelectedDocuments([...documents, ...res]));
|
||||||
|
|
||||||
|
setFiles([]);
|
||||||
|
setModalOpen(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Upload failed", err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Upload Card First */}
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{/* Uploaded documents */}
|
||||||
|
<ReactSortable
|
||||||
|
className="flex flex-wrap gap-3"
|
||||||
|
list={documents.map(doc => ({ ...doc }))}
|
||||||
|
setList={(newState) => dispatch(setDocuments(newState))}
|
||||||
|
handle=".drag-handle"
|
||||||
|
animation={200}
|
||||||
|
ghostClass="sortable-ghost"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
{documents.map((doc) => (
|
||||||
|
<DocumentCard key={doc.id} id={doc.id} name={doc.name} isCompleted={("isCompleted" in doc || ("status" in doc && doc.status === "UPLOADED")) ? true : false} />
|
||||||
|
))}
|
||||||
|
</ReactSortable>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => setModalOpen(true)}
|
||||||
|
className="cursor-pointer w-[160px] h-[170px] flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-xl bg-white hover:bg-gray-50 transition shadow-sm hover:shadow-md"
|
||||||
|
>
|
||||||
|
<FaPlus className="text-[var(--primary)] text-3xl mb-2" />
|
||||||
|
<p className="text-sm text-gray-600 text-center px-2">Upload Document</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
{isModalOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-100/60 backdrop-blur-sm px-4">
|
||||||
|
<div className="bg-white w-full max-w-5xl h-[500px] rounded-xl shadow-lg flex overflow-hidden relative">
|
||||||
|
{/* Close Button */}
|
||||||
|
<button
|
||||||
|
className="absolute top-2 right-2 text-gray-500 hover:text-red-500 transition text-xl bg-white rounded-full p-2"
|
||||||
|
onClick={() => setModalOpen(false)}
|
||||||
|
>
|
||||||
|
<IoMdClose />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Sidebar Tabs */}
|
||||||
|
<div className="w-1/4 bg-gray-100 p-4 flex flex-col gap-2 border-r border-gray-200">
|
||||||
|
{tabs.map(tab => (
|
||||||
|
<button
|
||||||
|
key={tab.name}
|
||||||
|
className={`flex items-center gap-2 p-3 rounded-md text-sm font-medium transition ${activeTab === tab.name
|
||||||
|
? 'bg-white shadow text-[var(--primary)]'
|
||||||
|
: 'hover:bg-gray-200 text-gray-600'
|
||||||
|
}`}
|
||||||
|
onClick={() => setActiveTab(tab.name)}
|
||||||
|
>
|
||||||
|
{tab.icon} {tab.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Content */}
|
||||||
|
<div className="w-3/4 p-6 overflow-y-auto">
|
||||||
|
{activeTab === 'Upload' && (
|
||||||
|
<div
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
onDragOver={(e) => e.preventDefault()}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
className="flex flex-col items-center justify-center h-full border-2 border-dashed border-gray-300 rounded-xl p-6 text-center cursor-pointer hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<FaCloudUploadAlt className="text-5xl text-[var(--primary)] mb-4" />
|
||||||
|
<p className="text-sm text-gray-500 mb-2">Drag & drop files here or</p>
|
||||||
|
<button className="px-5 py-2 text-sm bg-[var(--primary)] text-white rounded-md hover:scale-103 cursor-pointer transition-all">
|
||||||
|
Browse Files
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept=".pdf,.doc,.docx,.jpg,.png,.jpeg,.txt"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{activeTab !== 'Upload' && (
|
||||||
|
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
|
||||||
|
<p>{activeTab} integration coming soon...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectedDocuments;
|
||||||
|
|
@ -0,0 +1,735 @@
|
||||||
|
import React, { useEffect, useLayoutEffect, useState } from 'react'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { RootState } from '../../../store/store'
|
||||||
|
|
||||||
|
import api from '../../../config/api';
|
||||||
|
|
||||||
|
import RightSideBar from './RightSideBar';
|
||||||
|
import { DroppedItem } from '../../../utlity/types';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { TRANSACTION_STATUS_ENUMS, updateTransactionStatus } from '../../../store/slices/transactionSlice';
|
||||||
|
import { hideLoader, showLoader } from '../../../store/slices/loaderSlice';
|
||||||
|
import { IoMdClose } from 'react-icons/io';
|
||||||
|
import fields from '../../../helper/fieldsListData';
|
||||||
|
import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice';
|
||||||
|
import { fetchDocuments } from '../../../store/slices/documentSlice';
|
||||||
|
import { FaQuestionCircle } from 'react-icons/fa';
|
||||||
|
import typeToIconMap from '../../../helper/iconMap';
|
||||||
|
|
||||||
|
export interface DroppedItemsObj {
|
||||||
|
[key: string]: DroppedItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DroppedTagsDefaultFieldType {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const droppedTagsDefaultField: DroppedTagsDefaultFieldType = {
|
||||||
|
text: "paragraph",
|
||||||
|
temporal: "date",
|
||||||
|
number: "integer",
|
||||||
|
// image: "STAMP",
|
||||||
|
select: "single select",
|
||||||
|
checkbox: "TOGGLE"
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocumentContainerProps {
|
||||||
|
isSend: boolean | null;
|
||||||
|
setIsSend: React.Dispatch<React.SetStateAction<boolean | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DocumentContainer: React.FC<DocumentContainerProps> = ({ isSend }) => {
|
||||||
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
|
if (!accountId || !transactionId) {
|
||||||
|
window.location.href = "/";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// const transactionId = useSelector((state: RootState) => state.transaction.transactionId)
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients);
|
||||||
|
// const selectedDocuments = useSelector((state: RootState) => state.selectedDocuments.selectedDocuments)
|
||||||
|
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
|
||||||
|
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// FETCHING TAG - START
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedRecipients.length === 0) return;
|
||||||
|
const fetchTags = async () => {
|
||||||
|
try {
|
||||||
|
const requests = selectedRecipients.map((recipient) =>
|
||||||
|
api.get(`/api/accounts/${accountId}/transactions/${transactionId}/recipients/${recipient.id}/tags`)
|
||||||
|
);
|
||||||
|
const response = await Promise.all(requests);
|
||||||
|
let recTags = {}
|
||||||
|
for (const res of response) {
|
||||||
|
for (const tag of res.data) {
|
||||||
|
const key = `${tag.documentId}_${tag.pageIndex}`;
|
||||||
|
if (recTags[key]) {
|
||||||
|
recTags[key].push(tag);
|
||||||
|
} else {
|
||||||
|
recTags[key] = [tag]; // Initialize with an array
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// console.log(recTags, "Line 83");
|
||||||
|
setDroppedItems(recTags)
|
||||||
|
// Handle your response here
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTags();
|
||||||
|
}, [selectedRecipients]);
|
||||||
|
|
||||||
|
// FETCHING TAG - END
|
||||||
|
|
||||||
|
const [droppedItems, setDroppedItems] = useState<DroppedItemsObj>({});
|
||||||
|
const [droppedItemsCounter, setDroppedItemsCounter] = useState<number>(1);
|
||||||
|
const [selectedTag, setSelectedTag] = useState<{ key: string, index: number } | null>(null);
|
||||||
|
|
||||||
|
// NAVIGATING to Dashboard if Transaction is not created - START
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (transactionId) return;
|
||||||
|
navigate("/");
|
||||||
|
}, [transactionId])
|
||||||
|
// NAVIGATING to Dashboard if Transaction is not created - END
|
||||||
|
|
||||||
|
|
||||||
|
const groupByRecipientId = (tagsArray: any[]) => {
|
||||||
|
const tagsData: Record<number, any[]> = {};
|
||||||
|
|
||||||
|
for (const tag of tagsArray) {
|
||||||
|
const recipientId = tag.recipientId;
|
||||||
|
|
||||||
|
if (!tagsData[recipientId]) {
|
||||||
|
tagsData[recipientId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsData[recipientId].push(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagsData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendTagsToRecipients = async (
|
||||||
|
tagsData: Record<number, any[]>,
|
||||||
|
accountId: string,
|
||||||
|
transactionId: string
|
||||||
|
) => {
|
||||||
|
const requests = Object.entries(tagsData).map(([recipientId, tags]) => {
|
||||||
|
return api.put(
|
||||||
|
`/api/accounts/${accountId}/transactions/${transactionId}/recipients/${recipientId}/tags`,
|
||||||
|
tags
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const responses = await Promise.all(requests);
|
||||||
|
console.log("All recipient tags submitted:", responses);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting recipient tags:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save Tags - START
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSend === null) return;
|
||||||
|
|
||||||
|
// dispatch(showLoader("Creating Transaction"))
|
||||||
|
|
||||||
|
let updatedTags = { ...droppedItems }
|
||||||
|
|
||||||
|
// const sendTags = {
|
||||||
|
// "mod": [],
|
||||||
|
// "remove": []
|
||||||
|
// }
|
||||||
|
|
||||||
|
let tagsData = []
|
||||||
|
|
||||||
|
// const modified = []
|
||||||
|
// const remove = []
|
||||||
|
|
||||||
|
// console.log(updatedTags, "updatedTags");
|
||||||
|
|
||||||
|
for (const dropPageIds of Object.keys(updatedTags)) {
|
||||||
|
for (const dropTag of updatedTags[dropPageIds]) {
|
||||||
|
let mask = null
|
||||||
|
|
||||||
|
|
||||||
|
console.log(dropTag, "line 132")
|
||||||
|
|
||||||
|
// if (dropTag.subType?.toLowerCase() === "password") {
|
||||||
|
// console.log(dropTag)
|
||||||
|
mask = {
|
||||||
|
"type": dropTag.maskType ? `${dropTag.maskType}`.toUpperCase() : null,
|
||||||
|
"character": dropTag.maskCharacter ? `${dropTag.maskCharacter}`.toUpperCase() : null
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
|
||||||
|
const isDeleted = "isDeleted" in dropTag ? dropTag.isDeleted : false
|
||||||
|
|
||||||
|
const updatedTagData = {
|
||||||
|
"id": "id" in dropTag ? dropTag.id : null,
|
||||||
|
"label": dropTag.label,
|
||||||
|
"dimensions": dropTag.dimensions,
|
||||||
|
"type": dropTag.type.toUpperCase(),
|
||||||
|
// "subType": (dropTag.subType ? dropTag.subType.toUpperCase() : null),
|
||||||
|
validation: {
|
||||||
|
"minLength": 0,
|
||||||
|
"maxLength": 0,
|
||||||
|
"error": ("error" in dropTag && dropTag.error) ? `${dropTag.error}`.toUpperCase() : "error",
|
||||||
|
"regex": "regex" in dropTag ? dropTag.regex : []
|
||||||
|
},
|
||||||
|
"documentId": dropTag.documentId,
|
||||||
|
"options": ("options" in dropTag) ? dropTag.options : [],
|
||||||
|
"pageIndex": dropTag.pageIndex,
|
||||||
|
"recipientId": dropTag.recipientId,
|
||||||
|
"styles": "styles" in dropTag ? dropTag.styles : {
|
||||||
|
"font": "Great Vibes",
|
||||||
|
"italic": false,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"color": "black",
|
||||||
|
"textSize": 14.0
|
||||||
|
},
|
||||||
|
// "required": (dropTag.type.toUpperCase() !== "SIGNATURE" && dropTag.type.toUpperCase() !== "INITIALS") ? dropTag.required : true,
|
||||||
|
"required": false,
|
||||||
|
"readOnly": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("format" in dropTag) {
|
||||||
|
updatedTagData.format = `${dropTag.subType}`.toUpperCase();
|
||||||
|
updatedTagData.dateFormat = dropTag.format;
|
||||||
|
updatedTagData.currentTimeStamp = dropTag.currentTimeStamp
|
||||||
|
}
|
||||||
|
|
||||||
|
if (`${dropTag.type}`.toLowerCase() === "number") {
|
||||||
|
updatedTagData.decimalPlace = dropTag.decimalPlace
|
||||||
|
updatedTagData.isCurrency = dropTag.isCurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedTagData.type === "TEXT") {
|
||||||
|
updatedTagData.multiLine = dropTag.subType === "paragraph"
|
||||||
|
if (dropTag.subType === "password") {
|
||||||
|
updatedTagData.maskCharacter = mask.character
|
||||||
|
updatedTagData.maskType = mask.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if ("required" in updatedTagData && !updatedTagData.required) delete updatedTagData.required;
|
||||||
|
|
||||||
|
if (updatedTagData.subType === 'SINGLE SELECT') updatedTagData.subType = "SINGLE_SELECT"
|
||||||
|
if (updatedTagData.subType === 'MULTI SELECT') updatedTagData.subType = "MULTI_SELECT"
|
||||||
|
|
||||||
|
if (updatedTagData.options && !updatedTagData.options.length) delete updatedTagData.options;
|
||||||
|
|
||||||
|
if (!updatedTagData.id) delete updatedTagData.id;
|
||||||
|
|
||||||
|
if (!isDeleted) {
|
||||||
|
// sendTags.mod.push(updatedTagData);
|
||||||
|
tagsData.push(updatedTagData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for(const recipient of selectedRecipients) {
|
||||||
|
// const recrecipient
|
||||||
|
// }
|
||||||
|
|
||||||
|
tagsData = groupByRecipientId(tagsData);
|
||||||
|
sendTagsToRecipients(tagsData, accountId, transactionId);
|
||||||
|
}, [isSend])
|
||||||
|
// Save Tags - END
|
||||||
|
|
||||||
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault(); // Allows the drop event to work
|
||||||
|
};
|
||||||
|
|
||||||
|
// HANDLE DROP - START
|
||||||
|
const handleDrop = (event: React.DragEvent<HTMLDivElement>, docId: string, documentId: number, pageIndex: number) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const data = window.dragElemData;
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
const dragElemData = JSON.parse(data); // Get the dropped label
|
||||||
|
|
||||||
|
const field = { ...dragElemData }
|
||||||
|
if (!field.label) return;
|
||||||
|
|
||||||
|
// const field = fields.find((item) => item.label === dragElemData.label); // Find corresponding field
|
||||||
|
// if (!field) return; // If not found, exit
|
||||||
|
|
||||||
|
const dropZone = event.currentTarget.getBoundingClientRect();
|
||||||
|
const x = event.clientX - dropZone.left - dragElemData.offsetX;
|
||||||
|
const y = event.clientY - dropZone.top - dragElemData.offsetY;
|
||||||
|
|
||||||
|
const key = `${docId}_${pageIndex}`;
|
||||||
|
// const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;
|
||||||
|
|
||||||
|
let prepData = {
|
||||||
|
icon: typeToIconMap[field.label.toUpperCase()] || <FaQuestionCircle />,
|
||||||
|
type: dragElemData.label,
|
||||||
|
subType: (dragElemData.label in droppedTagsDefaultField) ? droppedTagsDefaultField[dragElemData.label] : '',
|
||||||
|
label: dragElemData.label,
|
||||||
|
dimensions: {
|
||||||
|
left: x,
|
||||||
|
top: y,
|
||||||
|
width: dragElemData.width,
|
||||||
|
height: dragElemData.height,
|
||||||
|
},
|
||||||
|
styles: dragElemData.label === "text" ? {
|
||||||
|
"font": "Great Vibes",
|
||||||
|
"italic": false,
|
||||||
|
"bold": false,
|
||||||
|
"underline": false,
|
||||||
|
"color": "black",
|
||||||
|
"textSize": 14.0
|
||||||
|
} : null,
|
||||||
|
documentId: docId,
|
||||||
|
pageIndex: pageIndex,
|
||||||
|
recipientId: dragElemData.recipientId,
|
||||||
|
recipientIndx: dragElemData.recipientIndx,
|
||||||
|
isDeleted: false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prepData.type === "temporal") {
|
||||||
|
prepData.currentTimeStamp = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prepData.type.toLowerCase() === "number") {
|
||||||
|
prepData.decimalPlace = 0;
|
||||||
|
prepData.isCurrency = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataToSet: DroppedItemsObj = {
|
||||||
|
...droppedItems,
|
||||||
|
[key]: [...(droppedItems[key] || []),
|
||||||
|
{
|
||||||
|
...prepData,
|
||||||
|
radioGroups: dragElemData.label === "radio" ?
|
||||||
|
[
|
||||||
|
{
|
||||||
|
...prepData,
|
||||||
|
dimensions: {
|
||||||
|
...prepData.dimensions, top: 8, left: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...prepData, dimensions: {
|
||||||
|
...prepData.dimensions,
|
||||||
|
top: 8,
|
||||||
|
left: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
:
|
||||||
|
[]
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragElemData.label === "radio") {
|
||||||
|
setDroppedItems(dataToSet);
|
||||||
|
} else {
|
||||||
|
setDroppedItems(dataToSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDroppedItemsCounter((prev) => prev + 1);
|
||||||
|
setTimeout(() => {
|
||||||
|
const tagIndex = key in droppedItems ? droppedItems[key].length : 0;
|
||||||
|
handleTagClick(key, tagIndex)
|
||||||
|
}, 0)
|
||||||
|
};
|
||||||
|
// HANDLE DROP - END
|
||||||
|
|
||||||
|
// HANDLE TAG MOUSE DOWN - START
|
||||||
|
const handleTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
let targetContainer = event?.currentTarget?.parentElement?.parentElement;
|
||||||
|
let targetElement = event?.currentTarget?.parentElement;
|
||||||
|
|
||||||
|
if (!targetContainer || !targetElement) return;
|
||||||
|
|
||||||
|
const containerRect = targetContainer.getBoundingClientRect();
|
||||||
|
const elementRect = targetElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Calculate the initial offset (where the mouse was clicked inside the element)
|
||||||
|
const offsetX = event.clientX - elementRect.left;
|
||||||
|
const offsetY = event.clientY - elementRect.top;
|
||||||
|
|
||||||
|
const conScrollWidth = targetContainer?.scrollWidth;
|
||||||
|
const conScrollHeight = targetContainer?.scrollHeight;
|
||||||
|
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
// Get new X and Y positions relative to the container
|
||||||
|
let left = event.clientX - containerRect.left - offsetX;
|
||||||
|
let top = event.clientY - containerRect.top - offsetY;
|
||||||
|
|
||||||
|
// **Restrict movement within container bounds**
|
||||||
|
left = Math.max(0, Math.min(left, conScrollWidth - elementRect.width));
|
||||||
|
top = Math.max(0, Math.min(top, conScrollHeight - elementRect.height));
|
||||||
|
|
||||||
|
// Apply the movement to the element
|
||||||
|
setDroppedItems({
|
||||||
|
...droppedItems,
|
||||||
|
[key]: Array.isArray(droppedItems[key]) // Ensure it's an array
|
||||||
|
? droppedItems[key].map((item, i) =>
|
||||||
|
i === index ? { ...item, dimensions: { ...item.dimensions, left, top } } : item
|
||||||
|
)
|
||||||
|
: [], // Provide a fallback in case it's undefined or not an array
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
document.removeEventListener("pointermove", handleMouseMove);
|
||||||
|
document.removeEventListener("pointerup", handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
document.addEventListener("pointermove", handleMouseMove);
|
||||||
|
document.addEventListener("pointerup", handleMouseUp);
|
||||||
|
};
|
||||||
|
// HANDLE TAG MOUSE DOWN - END
|
||||||
|
|
||||||
|
// HANDLE EXPAND TAG - START
|
||||||
|
const handleExpandTag = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number) => {
|
||||||
|
event.preventDefault(); // Prevent text selection issues
|
||||||
|
|
||||||
|
const expandingElemIcon = event.currentTarget;
|
||||||
|
const targetElement = expandingElemIcon.nextElementSibling?.parentElement;
|
||||||
|
const targetContainer = targetElement?.parentElement;
|
||||||
|
|
||||||
|
if (!targetContainer || !targetElement) return;
|
||||||
|
|
||||||
|
const elementRect = targetElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
const startX = event.clientX;
|
||||||
|
const startY = event.clientY;
|
||||||
|
|
||||||
|
const startWidth = elementRect.width;
|
||||||
|
const startHeight = elementRect.height;
|
||||||
|
|
||||||
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
|
|
||||||
|
let newWidth = startWidth + (event.clientX - startX);
|
||||||
|
let newHeight = startHeight + (event.clientY - startY);
|
||||||
|
|
||||||
|
if (newHeight > 200 || newWidth > 400) {
|
||||||
|
if (newHeight > 200) {
|
||||||
|
newHeight = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newWidth > 400) {
|
||||||
|
newWidth = 400
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the new width & height
|
||||||
|
setDroppedItems({
|
||||||
|
...droppedItems,
|
||||||
|
[key]: Array.isArray(droppedItems[key]) // Ensure it's an array
|
||||||
|
? droppedItems[key].map((item, i) =>
|
||||||
|
i === index ? { ...item, dimensions: { ...item.dimensions, width: newWidth, height: newHeight } } : item
|
||||||
|
)
|
||||||
|
: [], // Provide a fallback in case it's undefined or not an array
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
document.removeEventListener("pointermove", handleMouseMove);
|
||||||
|
document.removeEventListener("pointerup", handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("pointermove", handleMouseMove);
|
||||||
|
document.addEventListener("pointerup", handleMouseUp);
|
||||||
|
};
|
||||||
|
// HANDLE EXPAND TAG - END
|
||||||
|
|
||||||
|
const handleTagClick = (key: string, index: number) => {
|
||||||
|
setSelectedTag({ key, index })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveTag = (key: string, targetIndex: number) => {
|
||||||
|
const tag = droppedItems[key][targetIndex];
|
||||||
|
const recipientId = tag.recipientId;
|
||||||
|
(async () => {
|
||||||
|
if ("id" in tag) {
|
||||||
|
try {
|
||||||
|
const response = await api.delete(`/api/accounts/${accountId}/transactions/${transactionId}/recipients/${recipientId}/tags/${tag.id}`)
|
||||||
|
if(response.status !== 200) throw "Tag Deletion error"
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Tag Deletion error")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSelectedTag(null)
|
||||||
|
setDroppedItems(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
[key]: prevState[key].map((item, index) =>
|
||||||
|
index === targetIndex ? { ...item, isDeleted: true } : item
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
})()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleAddRadio = (e: React.MouseEvent<HTMLElement>, key: string, index: number) => {
|
||||||
|
// const dataToSet: DroppedItemsObj = {
|
||||||
|
// ...droppedItems,
|
||||||
|
// [key]: [...(droppedItems[key] || []),
|
||||||
|
// {
|
||||||
|
// ...prepData,
|
||||||
|
// radioGroups: dragElemData.label === "radio" ? [{ ...prepData }] : []
|
||||||
|
// }],
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const lastRadioElemRect = e.currentTarget?.parentElement.previousSibling.getBoundingClientRect();
|
||||||
|
// console.log(lastRadioElemRect, "lastRadioElemRect")
|
||||||
|
|
||||||
|
const setNewData = droppedItems;
|
||||||
|
|
||||||
|
if (Array.isArray(setNewData[key][index].radioGroups) && setNewData[key][index].radioGroups?.length > 0) {
|
||||||
|
setNewData[key][index].radioGroups?.push(setNewData[key][index].radioGroups[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
setDroppedItems(setNewData);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const handleRadioTagMouseDown = (event: React.MouseEvent<HTMLDivElement>, key: string, index: number, radioIndex: number) => {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
let targetContainer = event?.currentTarget?.parentElement;
|
||||||
|
let targetElement = event?.currentTarget;
|
||||||
|
|
||||||
|
if (!targetContainer || !targetElement) return;
|
||||||
|
|
||||||
|
const containerRect = targetContainer.getBoundingClientRect();
|
||||||
|
const elementRect = targetElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Calculate the initial offset (where the mouse was clicked inside the element)
|
||||||
|
const offsetX = event.clientX - elementRect.left;
|
||||||
|
const offsetY = event.clientY - elementRect.top;
|
||||||
|
|
||||||
|
const handleMouseMove = (event: PointerEvent) => {
|
||||||
|
// Get new X and Y positions relative to the container
|
||||||
|
// let x = offsetX;
|
||||||
|
// let y = offsetY;
|
||||||
|
|
||||||
|
let x = event.clientX - elementRect.left;
|
||||||
|
let y = event.clientY - elementRect.top;
|
||||||
|
|
||||||
|
const newData = droppedItems;
|
||||||
|
|
||||||
|
if (newData[key][index].radioGroups && newData[key][index].radioGroups.length > 0) {
|
||||||
|
newData[key][index].radioGroups[radioIndex].dimensions.left = x;
|
||||||
|
newData[key][index].radioGroups[radioIndex].dimensions.top = y;
|
||||||
|
|
||||||
|
let topMin = Number.POSITIVE_INFINITY;
|
||||||
|
let topMax = Number.NEGATIVE_INFINITY;
|
||||||
|
let leftMin = Number.POSITIVE_INFINITY;
|
||||||
|
let leftMax = Number.NEGATIVE_INFINITY;
|
||||||
|
|
||||||
|
for (const radio of newData[key][index].radioGroups) {
|
||||||
|
if (radio.dimensions.left < leftMin) {
|
||||||
|
leftMin = radio.dimensions.left;
|
||||||
|
}
|
||||||
|
if (radio.dimensions.left > leftMax) {
|
||||||
|
leftMax = radio.dimensions.left;
|
||||||
|
}
|
||||||
|
if (radio.dimensions.top < topMin) {
|
||||||
|
topMin = radio.dimensions.top;
|
||||||
|
}
|
||||||
|
if (radio.dimensions.top > topMax) {
|
||||||
|
topMax = radio.dimensions.top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newData[key][index].radioPosition = {
|
||||||
|
topMin: topMin,
|
||||||
|
topMax: topMax,
|
||||||
|
leftMin: leftMin,
|
||||||
|
leftMax: leftMax,
|
||||||
|
width: leftMax - leftMin,
|
||||||
|
height: topMax - topMin
|
||||||
|
}
|
||||||
|
|
||||||
|
newData[key][index].dimensions.left = x;
|
||||||
|
newData[key][index].dimensions.top = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDroppedItems({ ...newData });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
document.removeEventListener("pointermove", handleMouseMove);
|
||||||
|
document.removeEventListener("pointerup", handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
document.addEventListener("pointermove", handleMouseMove);
|
||||||
|
document.addEventListener("pointerup", handleMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Center Doc Container - START */}
|
||||||
|
<div id="doc-con" className='zoom-con col-span-8 bg-slate-100 h-full w-full overflow-auto scroll-smooth'>
|
||||||
|
|
||||||
|
{
|
||||||
|
selectedDocuments.length > 0 && selectedDocuments.map((doc, indxDoc) => {
|
||||||
|
return (
|
||||||
|
<div key={indxDoc} className='flex flex-col'>
|
||||||
|
<div className='py-2 text-sm'>
|
||||||
|
<p className='text-center'>{doc.name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col gap-3 max-w-max relative left-[50%] translate-x-[-50%]'>
|
||||||
|
{
|
||||||
|
doc.images && doc.images.length > 0 && doc.images.map((page, indx) => {
|
||||||
|
const key = `${doc.id}_${page.index}`;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={indx}
|
||||||
|
|
||||||
|
id={`${doc.id}${indx}`}
|
||||||
|
onDrop={(e) => handleDrop(e, doc.id, indxDoc, page.index)}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
className='drop-page-in shadow-md relative'
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* DROPPED ITEMS - Render only the dropped items for this specific docId and page - STARTS */}
|
||||||
|
{droppedItems[key]?.map((item, index) => {
|
||||||
|
if (item.isDeleted) return;
|
||||||
|
const type = item.type.toUpperCase();
|
||||||
|
const IconComponent = typeToIconMap[type] || <FaQuestionCircle />;
|
||||||
|
const selectedRecipient = selectedRecipients.filter((obj, indx) => obj.id === item.recipientId);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
draggable="false"
|
||||||
|
className="absolute rounded dropped-item"
|
||||||
|
onClick={() => handleTagClick(key, index)}
|
||||||
|
style={{
|
||||||
|
left: `${item.dimensions.left}px`,
|
||||||
|
top: `${item.dimensions.top}px`,
|
||||||
|
width: item.type === "radio" ? `${item.dimensions.width + 20}px` : `${item.dimensions.width}px`,
|
||||||
|
height: item.type === "radio" ? `max-content` : `${item.dimensions.height}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
{/* If Selected Tag Recipient Info- START */}
|
||||||
|
{selectedTag?.key && selectedTag.key === key && selectedTag.index === index &&
|
||||||
|
<div className='absolute flex justify-between items-center w-full max-w-max bg-white top-[-22px] shadow z-20 text-sm rounded-t-sm'>
|
||||||
|
<div className='flex justify-center items-center gap-1 bg-white px-2 shadow'>
|
||||||
|
<p className='w-2.5 h-2.5 rounded'
|
||||||
|
style={{ background: selectedRecipient[0]?.color }}
|
||||||
|
></p>
|
||||||
|
<p
|
||||||
|
className=''>{IconComponent}
|
||||||
|
</p>
|
||||||
|
<p className='capitalize text-ellipsis whitespace-nowrap'>{item.label}</p>
|
||||||
|
<IoMdClose onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemoveTag(key, index)
|
||||||
|
}} className='cursor-pointer' />
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
|
{/* If Selected Tag Recipient Info- END */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Expand Circle - STARTS */}
|
||||||
|
{item.type !== "radio" && <div
|
||||||
|
onPointerDown={(e) => handleExpandTag(e, key, index)}
|
||||||
|
className='w-2.5 h-2.5 absolute bg-red-400 rounded-full bottom-[-8px] right-[-8px] cursor-nw-resize hover:bg-blue-400'>
|
||||||
|
</div>}
|
||||||
|
{/* Expand Circle - ENDS */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
onPointerDown={(e) => handleTagMouseDown(e, key, index)}
|
||||||
|
// style={{ width: `${item.width}px !important` }}
|
||||||
|
className='p-2 rounded-sm h-full shadow opacity-40 cursor-grab w-full'
|
||||||
|
style={{ background: selectedRecipient[0]?.color }}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className={`flex justify-center items-center w-full text-xl text-white h-full`}
|
||||||
|
// style={{ color: selectedRecipients[item.recipientIndx]?.color }}
|
||||||
|
>
|
||||||
|
{IconComponent}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
)}
|
||||||
|
{/* Render only the dropped items for this specific docId and page - ENDS */}
|
||||||
|
|
||||||
|
<img
|
||||||
|
style={{ "minWidth": page.width, "minHeight": page.height, "maxWidth": page.width, "maxHeight": page.height }}
|
||||||
|
draggable="false"
|
||||||
|
width={page.width}
|
||||||
|
height={page.height}
|
||||||
|
src={page.src}
|
||||||
|
loading='lazy'
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Side details of documents Pages and document Name - START */}
|
||||||
|
<p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs left-[-60px] text-slate-600'>
|
||||||
|
page {indx + 1} of {doc.images && doc.images.length}
|
||||||
|
</p>
|
||||||
|
{/* <p className='absolute top-[50%] translate-y-[-50%] rotate-[-90deg] text-xs right-[-140px] text-slate-600'>
|
||||||
|
Document {indxDoc + 1} {doc.name}
|
||||||
|
</p> */}
|
||||||
|
{/* Side details of documents Pages and document Name - END */}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedTag && <RightSideBar selectedTag={droppedItems[selectedTag.key][selectedTag.index]} setDroppedItems={setDroppedItems} selTagKey={selectedTag.key} selTagIndx={selectedTag.index} />}
|
||||||
|
{/* Center Doc Container - END */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocumentContainer;
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import fields from '../../../helper/fieldsListData';
|
||||||
|
import api from '../../../config/api';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import typeToIconMap from '../../../helper/iconMap';
|
||||||
|
import { FaQuestionCircle } from 'react-icons/fa';
|
||||||
|
|
||||||
|
const handlePointerDown = (event: React.DragEvent<HTMLDivElement>, label: string, recipientId: number, recipientIndx: number) => {
|
||||||
|
|
||||||
|
const dragElemRect = event.currentTarget.getBoundingClientRect();
|
||||||
|
let width = dragElemRect.width
|
||||||
|
let height = dragElemRect.height
|
||||||
|
|
||||||
|
if (label === "radio" || label === "checkbox") {
|
||||||
|
const innerIconConRect = event.currentTarget.children[0].getBoundingClientRect();
|
||||||
|
width = innerIconConRect.width + 10
|
||||||
|
height = innerIconConRect.height + 10
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetX = event.clientX - dragElemRect.left;
|
||||||
|
const offsetY = event.clientY - dragElemRect.top;
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
label,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
recipientId: recipientId,
|
||||||
|
recipientIndx: recipientIndx,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dragElemData = JSON.stringify(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldListProps {
|
||||||
|
color: string;
|
||||||
|
recipientId: number;
|
||||||
|
recipientIndx: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FieldList: React.FC<FieldListProps> = ({ color, recipientId, recipientIndx }) => {
|
||||||
|
|
||||||
|
const [tagsData, setTagsData] = useState([]);
|
||||||
|
|
||||||
|
const { accountId } = useParams<{ accountId: string }>();
|
||||||
|
if (!accountId) {
|
||||||
|
window.location.href = "/";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api
|
||||||
|
.get(`/api/accounts/${accountId}/tags`)
|
||||||
|
.then((res) => {
|
||||||
|
const priorityOrder = ['SIGNATURE', 'INITIALS', 'STAMP'];
|
||||||
|
|
||||||
|
const data = res.data;
|
||||||
|
|
||||||
|
// Separate prioritized and other tags
|
||||||
|
const prioritized = data.filter((tag: any) =>
|
||||||
|
priorityOrder.includes(tag.type)
|
||||||
|
);
|
||||||
|
|
||||||
|
const others = data
|
||||||
|
.filter((tag: any) => !priorityOrder.includes(tag.type))
|
||||||
|
.sort((a: any, b: any) => a.type.localeCompare(b.type));
|
||||||
|
|
||||||
|
// Maintain the custom order of priority
|
||||||
|
const prioritizedSorted = priorityOrder
|
||||||
|
.map((type) => prioritized.find((tag: any) => tag.type === type))
|
||||||
|
.filter(Boolean); // remove undefined in case not found
|
||||||
|
|
||||||
|
setTagsData([...prioritizedSorted, ...others]);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{tagsData.map((field, index) => {
|
||||||
|
|
||||||
|
const IconComponent = typeToIconMap[field.type] || <FaQuestionCircle />;
|
||||||
|
// Optional: skip unknown types
|
||||||
|
if (!IconComponent) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <div key={index} draggable={true} onDragStart={(e) => handleDragStart(e, field.label, recipientId)} className="p-1 flex justify-start items-center gap-2 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300">
|
||||||
|
<div
|
||||||
|
draggable={true}
|
||||||
|
key={index}
|
||||||
|
onPointerDown={(e) => handlePointerDown(e, field.type.toLowerCase(), recipientId, recipientIndx)}
|
||||||
|
className="p-1 flex justify-start items-center gap-2 rounded-sm cursor-grab hover:bg-slate-200 transition-all duration-300">
|
||||||
|
<div
|
||||||
|
className={`p-1 text-sm rounded-sm text-white`}
|
||||||
|
style={{ background: color }}>
|
||||||
|
{IconComponent}
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="capitalize text-xs">
|
||||||
|
{field.type.toLowerCase()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FieldList;
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { RootState } from '../../../store/store';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import FieldList from './FieldList';
|
||||||
|
|
||||||
|
import { addColorsToRecipient, fetchRecipients } from '../../../store/slices/selectedRecipientsSlice';
|
||||||
|
|
||||||
|
import { IoCheckmark } from 'react-icons/io5'
|
||||||
|
import { MdKeyboardArrowDown } from 'react-icons/md'
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { fetchDocuments } from '../../../store/slices/documentSlice';
|
||||||
|
|
||||||
|
interface LeftSideBarProps {
|
||||||
|
isFields: boolean;
|
||||||
|
selectedRecipientIndx: number;
|
||||||
|
setSelectedRecipient: React.Dispatch<React.SetStateAction<number>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const LeftSideBar: React.FC<LeftSideBarProps> = ({ isFields, selectedRecipientIndx, setSelectedRecipient }) => {
|
||||||
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
|
if (!accountId || !transactionId) {
|
||||||
|
window.location.href = "/";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
|
||||||
|
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedRecipients.length === 0) {
|
||||||
|
dispatch(fetchRecipients({ accountId, transactionId }));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId, transactionId, selectedRecipients.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedDocuments.length === 0) {
|
||||||
|
dispatch(fetchDocuments({ accountId, transactionId }));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId, transactionId, selectedDocuments.length]);
|
||||||
|
|
||||||
|
// For Recipient
|
||||||
|
const [dropDownToggle, setDropDownToggle] = useState(false);
|
||||||
|
const [dropDownTogglePage, setDropDownTogglePage] = useState<string>("");
|
||||||
|
|
||||||
|
const selectedRecipient = selectedRecipients[selectedRecipientIndx];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedRecipients.length) {
|
||||||
|
if ("color" in selectedRecipients[0]) return;
|
||||||
|
dispatch(addColorsToRecipient())
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Left Side Bar - START */}
|
||||||
|
{
|
||||||
|
selectedRecipients.length && <div className='col-span-2 bg-white h-full p-4 overflow-y-auto'>
|
||||||
|
{isFields && <>
|
||||||
|
{/* Fields List and Recipeint - Start */}
|
||||||
|
{/* Side Bar showing Recipients */}
|
||||||
|
<div className='relative'
|
||||||
|
tabIndex={0}
|
||||||
|
onBlur={() => setDropDownToggle(false)}>
|
||||||
|
{/* Selected Recipient */}
|
||||||
|
<div
|
||||||
|
onClick={() => setDropDownToggle(!dropDownToggle)}
|
||||||
|
className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm border-1 border-slate-200 cursor-pointer'>
|
||||||
|
<div className='flex items-center gap-1.5'>
|
||||||
|
<p className='w-3 h-3 rounded-sm' style={{ background: selectedRecipient.color }}></p>
|
||||||
|
{/* <FaUserTie className='w-3 h-3 text-2xl mt-[1px]' style={{ color: selectedRecipient.color }} /> */}
|
||||||
|
<p>{selectedRecipient.firstName} {selectedRecipient.lastName}</p>
|
||||||
|
</div>
|
||||||
|
<MdKeyboardArrowDown />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`absolute w-full shadow-md rounded-md mt-1 px-2 py-1 z-20 bg-white ${dropDownToggle ? '' : 'hidden'}`}>
|
||||||
|
{selectedRecipients.length && selectedRecipients.map((recipient, indx) => {
|
||||||
|
return (<div
|
||||||
|
key={indx}
|
||||||
|
className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-pointer hover:bg-slate-100'
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedRecipient(indx);
|
||||||
|
setTimeout(() => {
|
||||||
|
setDropDownToggle(false);
|
||||||
|
}, 0)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex items-center gap-1.5'>
|
||||||
|
<p className='w-3 h-3 rounded-sm' style={{ background: recipient.color }}></p>
|
||||||
|
{/* <FaUser className='w-3 h-3 text-2xl mt-[1px]' style={{ color: recipient.color }} /> */}
|
||||||
|
<p>{recipient.firstName} {recipient.lastName} </p>
|
||||||
|
</div>
|
||||||
|
{selectedRecipientIndx === indx && <IoCheckmark className='text-[var(--primary)]' />}
|
||||||
|
</div>)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Side Bar showing Recipients - ENDS*/}
|
||||||
|
|
||||||
|
{/* Drag Drop Tags Start */}
|
||||||
|
<div className='mt-4'>
|
||||||
|
<FieldList color={selectedRecipient?.color || '#2d2d2d'} recipientId={selectedRecipient.id} recipientIndx={selectedRecipientIndx} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fields List and Recipeint - End */}
|
||||||
|
</>}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{!isFields && <>
|
||||||
|
{/* Pages - START */}
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
{selectedDocuments.length && selectedDocuments.map((document, indx) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={indx}
|
||||||
|
// tabIndex={1}
|
||||||
|
// onBlur={() => setDropDownTogglePage('')}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => setDropDownTogglePage(`${(dropDownTogglePage === 'pageDropDown' + indx) ? '' : 'pageDropDown' + indx}`)}
|
||||||
|
className={`p-2 ${(dropDownTogglePage === 'pageDropDown' + indx) ? "bg-[var(--primary)]" : "bg-slate-200"} flex flex-col justify-center items-center gap-2 rounded`}>
|
||||||
|
{
|
||||||
|
document.images != undefined && <img
|
||||||
|
key={indx}
|
||||||
|
className='rounded-sm shadow'
|
||||||
|
src={document.images[0].src}
|
||||||
|
alt="" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className='flex items-center justify-between w-full text-xs rounded-sm cursor-pointer text-slate-100'>
|
||||||
|
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[18ch]'>{document.name}</p>
|
||||||
|
<MdKeyboardArrowDown className={`${(dropDownTogglePage === 'pageDropDown' + indx) ? 'rotate-180' : ''}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
dropDownTogglePage === 'pageDropDown' + indx &&
|
||||||
|
<div className='my-1 flex justify-center items-center gap-1 flex-wrap'>
|
||||||
|
{document.images != undefined && document.images.map((img, indx) => (
|
||||||
|
<a
|
||||||
|
href={"#" + document.id + indx}
|
||||||
|
className='max-w-max px-4 py-2 shadow cursor-pointer border-1 rounded-sm border-slate-200 hover:border-[var(--primary)] transition-all duration-300 bg-slate-300'
|
||||||
|
>
|
||||||
|
<p className='text-center text-xs text-slate-600'>{indx + 1}</p>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{/* Pages - END */}
|
||||||
|
</>}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div >
|
||||||
|
}
|
||||||
|
|
||||||
|
{/* Left Side Bar - END */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LeftSideBar
|
||||||
|
|
@ -0,0 +1,271 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { RootState } from '../../../store/store'
|
||||||
|
|
||||||
|
import { FaCheck, FaSignature } from 'react-icons/fa'
|
||||||
|
import { IoIosDocument } from 'react-icons/io'
|
||||||
|
import { LuTableOfContents } from 'react-icons/lu'
|
||||||
|
import { MdKeyboardArrowDown } from 'react-icons/md'
|
||||||
|
import { RiResetLeftLine, RiResetRightLine } from 'react-icons/ri'
|
||||||
|
import { fetchRecipients } from '../../../store/slices/selectedRecipientsSlice'
|
||||||
|
import { fetchDocuments } from '../../../store/slices/documentSlice'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
interface NavTwoProps {
|
||||||
|
isFields: boolean;
|
||||||
|
setIsFields: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
isSend: boolean | null;
|
||||||
|
setIsSend: React.Dispatch<React.SetStateAction<boolean | null>>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface FIT {
|
||||||
|
label: string,
|
||||||
|
value: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fitObj = [
|
||||||
|
{
|
||||||
|
label: "Fit",
|
||||||
|
value: "FIT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "50%",
|
||||||
|
value: "50"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "150%",
|
||||||
|
value: "150"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "200%",
|
||||||
|
value: "200"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const NavTwo: React.FC<NavTwoProps> = ({ isFields, setIsFields, isSend, setIsSend }) => {
|
||||||
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
|
if (!accountId || !transactionId) {
|
||||||
|
window.location.href = "/";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [transactionSaving, setTransactionSaving] = useState<boolean>(false);
|
||||||
|
const [dotCount, setDotCount] = useState(0);
|
||||||
|
|
||||||
|
const [isFitDd, setIsFitDd] = useState<boolean>(false)
|
||||||
|
const [selFitIndx, setSelFitIndx] = useState<number>(0);
|
||||||
|
|
||||||
|
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
|
||||||
|
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
|
||||||
|
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [nav2RecToggle, setNav2RecToggle] = useState<boolean>(false);
|
||||||
|
const [nav2DocToggle, setNav2DocToggle] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
function handleSend() {
|
||||||
|
// dispatch(showLoader("Loading ..."));
|
||||||
|
if (isSend === null) {
|
||||||
|
setIsSend(false);
|
||||||
|
}
|
||||||
|
setIsSend(!isSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSetIsFields = (setTo: boolean) => {
|
||||||
|
if (setTo !== isFields) setIsFields(setTo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoom = (percentage: number, contentClass: string) => {
|
||||||
|
const content = document.querySelector(`.${contentClass}`);
|
||||||
|
if (content) {
|
||||||
|
(content as HTMLElement).style.zoom = `${percentage}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let indx = fitObj[selFitIndx].value
|
||||||
|
if (indx === "FIT") {
|
||||||
|
indx = "100";
|
||||||
|
}
|
||||||
|
zoom(Number(indx), 'zoom-con')
|
||||||
|
}, [selFitIndx])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeoutId: number;
|
||||||
|
const timeOutIn = 8000;
|
||||||
|
|
||||||
|
const updateLabel = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
setTransactionSaving(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setTransactionSaving(false);
|
||||||
|
}, 4000)
|
||||||
|
}
|
||||||
|
timeoutId = setTimeout(updateLabel, timeOutIn);
|
||||||
|
};
|
||||||
|
timeoutId = setTimeout(updateLabel, timeOutIn);
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!transactionSaving) return;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setDotCount((prev) => (prev + 1) % 4); // Cycle through 0, 1, 2, 3
|
||||||
|
}, 500); // Update every 0.5s
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [transactionSaving]);
|
||||||
|
|
||||||
|
const dots = '. '.repeat(dotCount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
{/* Nav Two - Oprions - START */}
|
||||||
|
<div className='flex justify-between items-center px-4 py-1 bg-white'>
|
||||||
|
|
||||||
|
{/* Left Side */}
|
||||||
|
<div className='flex justify-start items-center gap-2'>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => handleSetIsFields(true)}
|
||||||
|
className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${isFields ? 'bg-slate-100' : ''}`}>
|
||||||
|
<LuTableOfContents />
|
||||||
|
<p className='tracking-wide'>Fields</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => handleSetIsFields(false)}
|
||||||
|
className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${!isFields ? 'bg-slate-100' : ''}`}>
|
||||||
|
<IoIosDocument />
|
||||||
|
<p className='tracking-wide'>Pages</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||||
|
<RiResetLeftLine />
|
||||||
|
<p className='tracking-wide'>Reset</p>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* <div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||||
|
<p className='tracking-wide'>Fit</p>
|
||||||
|
<MdKeyboardArrowDown />
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div
|
||||||
|
tabIndex={1}
|
||||||
|
onBlur={() => setIsFitDd(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => setIsFitDd(!isFitDd)}
|
||||||
|
className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||||
|
<p className='tracking-wide'>Fit</p>
|
||||||
|
<MdKeyboardArrowDown style={{ transform: `${isFitDd ? "rotate(180deg)" : ""}` }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFitDd && <div className='absolute bg-white rounded-sm z-50'>
|
||||||
|
{fitObj.map((val, indx) => {
|
||||||
|
return (<div
|
||||||
|
key={indx}
|
||||||
|
onClick={() => setSelFitIndx(indx)}
|
||||||
|
className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'>
|
||||||
|
<div
|
||||||
|
|
||||||
|
className='flex items-center justify-between w-full'>
|
||||||
|
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{val.label}</p>
|
||||||
|
{selFitIndx === indx && <FaCheck className='text-sm text-slate-400' />}
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||||
|
<RiResetRightLine
|
||||||
|
className={`${transactionSaving ? "animate-spin" : ""} transition duration-300`}
|
||||||
|
/>
|
||||||
|
<p className="tracking-wide transition-all">{transactionSaving ? `Saving ${dots}` : "Save"}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Right Side - STARTS */}
|
||||||
|
<div className='flex justify-center items-center'>
|
||||||
|
|
||||||
|
{/* Documnt Nav */}
|
||||||
|
<div tabIndex={1} onBlur={() => setNav2DocToggle(false)}>
|
||||||
|
<div onClick={() => setNav2DocToggle(!nav2DocToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||||
|
<p className='tracking-wide'>{selectedDocuments.length} Documents</p>
|
||||||
|
<MdKeyboardArrowDown className={`${nav2DocToggle ? 'rotate-180' : ''} transition-all`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`absolute max-w-max shadow rounded-md mt-1 px-2 py-1 z-20 bg-white ${nav2DocToggle ? '' : 'hidden'}`}>
|
||||||
|
|
||||||
|
{selectedDocuments.length && selectedDocuments.map((document, indx) => {
|
||||||
|
return (<div key={indx} className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-default'>
|
||||||
|
<div className='flex items-center gap-1'>
|
||||||
|
{/* <p className='w-3 h-3 rounded-sm' style={{ background: recipient.color }}></p> */}
|
||||||
|
<IoIosDocument className='text-slate-600 text-lg' />
|
||||||
|
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[15ch]'>{document.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recipients Nav */}
|
||||||
|
<div className='relative' tabIndex={1} onBlur={() => setNav2RecToggle(false)}>
|
||||||
|
<div onClick={() => setNav2RecToggle(!nav2RecToggle)} className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||||
|
<p className='tracking-wide'>{selectedRecipients.length} Recipients</p>
|
||||||
|
<MdKeyboardArrowDown className={`${nav2RecToggle ? 'rotate-180' : ''} transition-all`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`absolute max-w-max shadow rounded-md mt-1 px-2 py-1 z-20 bg-white ${nav2RecToggle ? '' : 'hidden'}`}>
|
||||||
|
|
||||||
|
{selectedRecipients.length && selectedRecipients.map((recipient, indx) => {
|
||||||
|
return (<div key={indx} className='flex items-center justify-between gap-2 w-full p-2 text-xs rounded-sm cursor-default'>
|
||||||
|
<div className='flex items-center gap-1.5'>
|
||||||
|
<p className='w-3 h-3 rounded-sm' style={{ background: recipient.color }}></p>
|
||||||
|
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{recipient.firstName} {recipient.lastName} segfsdtzgszrdtgrs serdgvsrdgs dtrgv</p>
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Send */}
|
||||||
|
<div
|
||||||
|
onClick={() => handleSend()}
|
||||||
|
className='flex items-center gap-2 cursor-pointer text-xs px-4 py-2 ml-3 rounded-sm bg-[var(--primary)]'>
|
||||||
|
<p className='tracking-wide text-white'>Send</p>
|
||||||
|
<FaSignature className='text-white text-md' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/* Right Side - ENDS */}
|
||||||
|
</div >
|
||||||
|
{/* Nav Two - Oprions - End */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavTwo
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import NavTwo from './NavTwo';
|
||||||
|
import LeftSideBar from './LeftSideBar';
|
||||||
|
import DocumentContainer from './DocumentContainer';
|
||||||
|
|
||||||
|
import { IoMdClose } from 'react-icons/io'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '../../../store/store';
|
||||||
|
import Loader from '../../dashboard/Loader';
|
||||||
|
import { hideLoader } from '../../../store/slices/loaderSlice';
|
||||||
|
|
||||||
|
const ProcessStepTwo = () => {
|
||||||
|
const [isFields, setIsFields] = useState(true);
|
||||||
|
const [selectedRecipient, setSelectedRecipient] = useState(0);
|
||||||
|
const [isSend, setIsSend] = useState<boolean | null>(null);
|
||||||
|
const globalLoader = useSelector((state: RootState) => state.loader)
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(hideLoader());
|
||||||
|
}, 800)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`min-w-full relative h-screen bg-slate-100 overflow-hidden ${(globalLoader.isLoading) ? 'pointer-events-none' : ''}`}>
|
||||||
|
|
||||||
|
{/* Top Header One */}
|
||||||
|
|
||||||
|
{/* <div className='flex items-center shadow-sm pb-3 bg-slate-100'>
|
||||||
|
|
||||||
|
<div className='flex w-full justify-center items-center px-4'>
|
||||||
|
<div className='cursor-default flex justify-center items-center gap-4 border-b-3 relative border-[var(--primary)] py-1 px-4'>
|
||||||
|
<p className="text-xs tracking-wider">Setup your Document</p>
|
||||||
|
<div className='absolute bottom-[-7px] w-3 h-3 rounded-full right-[-4px] bg-[var(--primary)] z-50' ></div>
|
||||||
|
</div>
|
||||||
|
<div className='cursor-default flex justify-center items-center gap-4 border-b-3 relative border-[var(--primary)] py-1 px-4'>
|
||||||
|
<p className="text-xs tracking-wider">Prepare</p>
|
||||||
|
<div className='absolute bottom-[-7px] w-3 h-3 rounded-full right-[-4px] bg-[var(--primary)] z-50' ></div>
|
||||||
|
</div>
|
||||||
|
<div className='cursor-default flex justify-center items-center gap-4 border-b-3 relative border-[var(--primaryLight)] py-1 px-4'>
|
||||||
|
<p className="text-xs tracking-wider">Hooray !</p>
|
||||||
|
<div className='absolute bottom-[-7px] w-3 h-3 rounded-full right-[-4px] bg-[var(--primaryLight)] z-50' ></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
{/* Top Header One */}
|
||||||
|
|
||||||
|
<NavTwo isFields={isFields} setIsFields={setIsFields} isSend={isSend} setIsSend={setIsSend} />
|
||||||
|
|
||||||
|
<div className='grid grid-cols-12 gap-1 mt-1 h-[calc(100vh-46px)] overflow-hidden'>
|
||||||
|
<LeftSideBar isFields={isFields} selectedRecipientIndx={selectedRecipient} setSelectedRecipient={setSelectedRecipient} />
|
||||||
|
<DocumentContainer isSend={isSend} setIsSend={setIsSend} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{globalLoader.isLoading && <Loader />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcessStepTwo
|
||||||
|
|
@ -0,0 +1,653 @@
|
||||||
|
import { MdKeyboardArrowDown } from "react-icons/md";
|
||||||
|
import { DroppedItemsObj } from "./DocumentContainer";
|
||||||
|
import React, { ReactHTMLElement, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
import { RiAsterisk } from "react-icons/ri";
|
||||||
|
import { GoDotFill } from "react-icons/go";
|
||||||
|
import { IoClose } from "react-icons/io5";
|
||||||
|
import { LuAsterisk } from "react-icons/lu";
|
||||||
|
import { dateFormats, dateTimeFormats, monthFormats, timeFormats, weekFormats, yearFormats } from "../../../utlity/dateTimeUtils";
|
||||||
|
import { DroppedItem } from "../../../utlity/types";
|
||||||
|
import { IoMdClose } from "react-icons/io";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
interface DroppedItemProps {
|
||||||
|
selectedTag: DroppedItem
|
||||||
|
setDroppedItems: React.Dispatch<React.SetStateAction<DroppedItemsObj>>
|
||||||
|
selTagKey: string,
|
||||||
|
selTagIndx: number
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TEXT {
|
||||||
|
PARAGRAPH = "paragraph",
|
||||||
|
SINGLE_LINE = "single_line",
|
||||||
|
PASSWORD = "password",
|
||||||
|
}
|
||||||
|
// enum SIGNATURE {
|
||||||
|
// TYPED = "typed",
|
||||||
|
// DRAW = "draw",
|
||||||
|
// UPLOAD = "upload",
|
||||||
|
// }
|
||||||
|
// enum INITIALS {
|
||||||
|
// TYPED = "typed",
|
||||||
|
// DRAW = "draw",
|
||||||
|
// UPLOAD = "upload",
|
||||||
|
// }
|
||||||
|
export enum TEMPORAL {
|
||||||
|
DATE = "date",
|
||||||
|
TIME = "time",
|
||||||
|
DATE_TIME = "date_time",
|
||||||
|
// YEAR = "year",
|
||||||
|
// MONTH = "month",
|
||||||
|
// WEEK = "week",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NUMBER {
|
||||||
|
INTEGER = "integer",
|
||||||
|
DECIMAL = "decimal",
|
||||||
|
CURRENCY = "currency"
|
||||||
|
}
|
||||||
|
// enum IMAGE {
|
||||||
|
// STAMP = "stamp",
|
||||||
|
// LOGO = "logo"
|
||||||
|
// }
|
||||||
|
|
||||||
|
enum SELECT {
|
||||||
|
SINGLE_SELECT = "single select",
|
||||||
|
MULTI_SELECT = "multi select"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum MASKTYPE {
|
||||||
|
ALL = "all",
|
||||||
|
LAST_FOUR = "last_four",
|
||||||
|
CREDIT_CARD = "credit_Card",
|
||||||
|
NATIONAL_INSURANCE = "national_insurance",
|
||||||
|
SOCIAL_SECURITY = "social_security",
|
||||||
|
SOCIAL_INSURANCE = "social_insurance"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MASKCHARACTER {
|
||||||
|
ASTERISK = "ASTERISK",
|
||||||
|
DOT = "DOT",
|
||||||
|
X = "CROSS",
|
||||||
|
}
|
||||||
|
|
||||||
|
const enumMap = {
|
||||||
|
text: TEXT,
|
||||||
|
temporal: TEMPORAL,
|
||||||
|
number: NUMBER,
|
||||||
|
image: String,
|
||||||
|
select: SELECT
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DROPDOWNTOGGLE {
|
||||||
|
TYPE,
|
||||||
|
MASK_TYPE,
|
||||||
|
MASK_CHARACTER,
|
||||||
|
DDMMYY_FORMAT
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateTimeFormatMap = {
|
||||||
|
"date": dateFormats,
|
||||||
|
"time": timeFormats,
|
||||||
|
"date_time": dateTimeFormats,
|
||||||
|
"year": yearFormats,
|
||||||
|
"month": monthFormats,
|
||||||
|
"week": weekFormats
|
||||||
|
}
|
||||||
|
|
||||||
|
const RightSideBar: React.FC<DroppedItemProps> = ({ selectedTag, setDroppedItems, selTagKey, selTagIndx }) => {
|
||||||
|
console.log(selectedTag, "selectedTag Line 102")
|
||||||
|
const selectedType = selectedTag.type in enumMap && enumMap[selectedTag.type as keyof typeof enumMap];
|
||||||
|
const [dropDownToggle, setDropDownToggle] = useState<DROPDOWNTOGGLE | null>(null);
|
||||||
|
|
||||||
|
// const [showFormatEg, setShowFormatEg] = useState<boolean>(false);
|
||||||
|
const [is24Hrs, setIs24Hrs] = useState<boolean>(true);
|
||||||
|
const [isCurrentTimeStamp, setIsCurrentTimeStamp] = useState<boolean>(true);
|
||||||
|
|
||||||
|
// const [selectedOptions, setSelectedOptions] = useState<string[]>([])
|
||||||
|
|
||||||
|
const [decimalPlace, setDecimalPlace] = useState<number>(2)
|
||||||
|
const [selectedOption, setSelectedOption] = useState<string>()
|
||||||
|
|
||||||
|
const handlePropertyChange = (changeTo: string, changeWith: string | boolean | number) => {
|
||||||
|
console.log("Handle Property Change Called", "changeTo", changeTo, "changeWith", changeWith)
|
||||||
|
if (changeTo === '' || changeWith === '') return
|
||||||
|
|
||||||
|
if (changeTo === "subType" && (changeWith === TEMPORAL.DATE || changeWith === TEMPORAL.DATE_TIME || changeWith === TEMPORAL.TIME)) {
|
||||||
|
// let change: string = changeWith;
|
||||||
|
// if (change === "DATE TIME") {
|
||||||
|
// change = "DATE_TIME"
|
||||||
|
// }
|
||||||
|
setDroppedItems((prev) => (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
|
||||||
|
? prev[selTagKey].map((item, i) =>
|
||||||
|
i === selTagIndx ? { ...item, [changeTo]: changeWith || "", format: "" } : item
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
}// Provide a fallback in case it's undefined or not an array
|
||||||
|
));
|
||||||
|
} else if (changeTo === "subType" && typeof changeWith === "string" && changeWith.toLowerCase() === "password") {
|
||||||
|
console.log("Line 161")
|
||||||
|
setDroppedItems((prev) => (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
[selTagKey]: Array.isArray(prev[selTagKey])
|
||||||
|
? prev[selTagKey].map((item, i) =>
|
||||||
|
i === selTagIndx ? { ...item, [changeTo]: changeWith, maskType: "All", maskCharacter: "ASTERISK" } : item
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else if (changeTo === "subType" && (changeWith === NUMBER.CURRENCY || changeWith === NUMBER.INTEGER || changeWith === NUMBER.DECIMAL)) {
|
||||||
|
let decPlace = changeWith !== NUMBER.DECIMAL ? 0 : decimalPlace;
|
||||||
|
const isCurrency = changeWith === NUMBER.CURRENCY;
|
||||||
|
// const currencyType = null
|
||||||
|
|
||||||
|
setDroppedItems((prev) => (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
|
||||||
|
? prev[selTagKey].map((item, i) =>
|
||||||
|
i === selTagIndx ? { ...item, [changeTo]: changeWith, decimalPlace: decPlace, isCurrency } : item
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
}// Provide a fallback in case it's undefined or not an array
|
||||||
|
));
|
||||||
|
|
||||||
|
} else if (changeTo === "format" && selectedTag.subType === TEMPORAL.DATE_TIME) {
|
||||||
|
let val = timeFormats[0].format;
|
||||||
|
if (!is24Hrs) {
|
||||||
|
val = timeFormats[1].format
|
||||||
|
}
|
||||||
|
setDroppedItems((prev) => (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
|
||||||
|
? prev[selTagKey].map((item, i) =>
|
||||||
|
i === selTagIndx ? { ...item, [changeTo]: changeWith + " " + val } : item
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
}// Provide a fallback in case it's undefined or not an array
|
||||||
|
));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
setDroppedItems((prev) => (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
[selTagKey]: Array.isArray(prev[selTagKey]) // Ensure it's an array
|
||||||
|
? prev[selTagKey].map((item, i) =>
|
||||||
|
i === selTagIndx ? { ...item, [changeTo]: changeWith } : item
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
}// Provide a fallback in case it's undefined or not an array
|
||||||
|
));
|
||||||
|
}
|
||||||
|
setDropDownToggle(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// const handleSelectTagOptions = () => {
|
||||||
|
// if (!selectedOptions.length) return
|
||||||
|
// setDroppedItems((prev) => (
|
||||||
|
// {
|
||||||
|
// ...prev,
|
||||||
|
// [selTagKey]: Array.isArray(prev[selTagKey])
|
||||||
|
// ? prev[selTagKey].map((item, i) => {
|
||||||
|
// if (i === selTagIndx) {
|
||||||
|
// return { ...item, options: [...selectedOptions] };
|
||||||
|
// }
|
||||||
|
// return item;
|
||||||
|
// })
|
||||||
|
// : [],
|
||||||
|
// }
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// handleSelectTagOptions();
|
||||||
|
// }, [selectedOptions])
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTag.subType === TEMPORAL.DATE || selectedTag.subType === TEMPORAL.DATE_TIME || selectedTag.subType === TEMPORAL.TIME) {
|
||||||
|
|
||||||
|
if (selectedTag.subType === TEMPORAL.DATE) {
|
||||||
|
handlePropertyChange("format", dateFormats[0].format)
|
||||||
|
}
|
||||||
|
else if (selectedTag.subType === TEMPORAL.DATE_TIME) {
|
||||||
|
handlePropertyChange("format", dateTimeFormats[0].format)
|
||||||
|
}
|
||||||
|
else if (selectedTag.subType === TEMPORAL.TIME) {
|
||||||
|
handlePropertyChange("format", timeFormats[0].format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// else if(selectedTag.subType === TEXT.PASSWORD) {
|
||||||
|
// handlePropertyChange("format", timeFormats[0].format)
|
||||||
|
// }
|
||||||
|
}, [selectedTag.subType])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTag.format === timeFormats[1].format && is24Hrs) {
|
||||||
|
setIs24Hrs(false);
|
||||||
|
} else if (selectedTag.format === timeFormats[0].format && !is24Hrs) {
|
||||||
|
setIs24Hrs(true);
|
||||||
|
}
|
||||||
|
}, [selectedTag.format])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTag.label === "temporal") {
|
||||||
|
handlePropertyChange("subType", TEMPORAL.DATE);
|
||||||
|
handlePropertyChange("format", "DD/MM/YY");
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
const handleIs24HrsClick = (checked: boolean) => {
|
||||||
|
if (selectedTag.subType === TEMPORAL.TIME) {
|
||||||
|
if (!checked) {
|
||||||
|
handlePropertyChange("format", timeFormats[1].format)
|
||||||
|
} else {
|
||||||
|
handlePropertyChange("format", timeFormats[0].format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIs24Hrs(checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleIsCurrentTimeStamp = (checked: boolean) => {
|
||||||
|
if (selectedTag.type.toLowerCase() === "temporal") {
|
||||||
|
setIsCurrentTimeStamp(checked)
|
||||||
|
handlePropertyChange("currentTimeStamp", checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddSelectOption = () => {
|
||||||
|
if (selectedOption && selectedOption.length) {
|
||||||
|
setDroppedItems((prev) => (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
[selTagKey]: Array.isArray(prev[selTagKey])
|
||||||
|
? prev[selTagKey].map((item, i) => {
|
||||||
|
if (i === selTagIndx) {
|
||||||
|
return { ...item, options: item.options ? [...item.options, selectedOption] : [selectedOption] };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleSelectOptionDel = (indx: number) => {
|
||||||
|
// setSelectedOptions(prev =>
|
||||||
|
// prev.filter((_, i) => i !== indx)
|
||||||
|
// )
|
||||||
|
|
||||||
|
setDroppedItems((prev) => (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
[selTagKey]: Array.isArray(prev[selTagKey])
|
||||||
|
? prev[selTagKey].map((item, i) => {
|
||||||
|
if (i === selTagIndx) {
|
||||||
|
return { ...item, options: item.options ? item.options.filter((_, i) => i !== indx) : [] };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const handleDecimalPlace = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const val = Number(e.target.value)
|
||||||
|
setDecimalPlace(val)
|
||||||
|
handlePropertyChange("decimalPlace", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ("styles" in selectedTag) {
|
||||||
|
const fontSize = selectedTag?.styles?.textSize;
|
||||||
|
const tagHeight = selectedTag?.dimensions?.height - 18;
|
||||||
|
|
||||||
|
|
||||||
|
console.log(fontSize, tagHeight, "Line 323", selectedTag?.styles)
|
||||||
|
|
||||||
|
if (selectedTag.subType !== "password" && selectedTag.subType !== "paragraph" && tagHeight > (fontSize + fontSize)) {
|
||||||
|
console.log("Change subType Called from if")
|
||||||
|
handlePropertyChange("subType", "paragraph")
|
||||||
|
} else if (selectedTag.subType !== "password" && selectedTag.subType !== "single_line" && tagHeight <= (fontSize + fontSize)) {
|
||||||
|
console.log("Change subType Called from elseif")
|
||||||
|
handlePropertyChange("subType", "single_line")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedTag])
|
||||||
|
|
||||||
|
console.log("RightSideBar - selectedTag", selectedTag)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Right Side Bar Start */}
|
||||||
|
<div className='col-span-2 w-full h-full bg-white text-sm'>
|
||||||
|
<p className="p-2 font-semibold text-md">Properties</p>
|
||||||
|
<hr className="text-slate-200" />
|
||||||
|
|
||||||
|
<div className="p-3 text-sm text-slate-600 flex flex-col gap-2">
|
||||||
|
|
||||||
|
{/* label */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label htmlFor="" className="">Label</label>
|
||||||
|
<input
|
||||||
|
value={selectedTag.label}
|
||||||
|
onChange={(e) => handlePropertyChange('label', e.target.value)}
|
||||||
|
type="text"
|
||||||
|
className="outline-none border-1 rounded px-2 py-1 border-slate-200" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Required */}
|
||||||
|
{(selectedTag.type !== "signature" && selectedTag.type !== "initials") && <div className="flex justify-between items-center">
|
||||||
|
<label htmlFor="required" className="text-xs text-slate-600 transitions-all duration-150">Required</label>
|
||||||
|
<input
|
||||||
|
id="required"
|
||||||
|
checked={selectedTag.required ? selectedTag.required : false}
|
||||||
|
onChange={(e) => handlePropertyChange('required', e.target.checked)}
|
||||||
|
type="checkbox"
|
||||||
|
className="outline-none border-1 rounded px-2 py-1 border-slate-200"
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{/* Use Current TimeStamp */}
|
||||||
|
{selectedTag.type.toLowerCase() === "temporal" && <div className="flex justify-left items-center gap-1.5 mt-0.5">
|
||||||
|
<label
|
||||||
|
className={`text-xs ${isCurrentTimeStamp ? "text-slate-600" : "text-slate-400"} transitions-all duration-150`}
|
||||||
|
htmlFor="use_current_timestamp"
|
||||||
|
>
|
||||||
|
Use Current Timestamp
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
checked={isCurrentTimeStamp ? isCurrentTimeStamp : false}
|
||||||
|
onChange={(e) => handleIsCurrentTimeStamp(e.target.checked)}
|
||||||
|
id="use_current_timestamp"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{/* Type | SubType */}
|
||||||
|
{selectedType && Object.keys(selectedType).length > 0 &&
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label htmlFor="" className="">Type</label>
|
||||||
|
<div
|
||||||
|
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.TYPE ? null : DROPDOWNTOGGLE.TYPE)}
|
||||||
|
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
|
||||||
|
<p className="capitalize cursor-pointer">{selectedTag?.subType ? (selectedTag.subType === "date_time" ? "Date Time" : selectedTag.subType) : selectedType[Object.keys(selectedType)[0] as keyof typeof selectedType]}</p>
|
||||||
|
<MdKeyboardArrowDown />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dropDownToggle === DROPDOWNTOGGLE.TYPE &&
|
||||||
|
<div className="shadow bg-slate-100 mt-1 border-[1px] border-slate-100 rounded">
|
||||||
|
{Object.keys(selectedType).map((val, indx) => (
|
||||||
|
<div
|
||||||
|
key={indx}
|
||||||
|
onClick={() => handlePropertyChange("subType", selectedType[val as keyof typeof selectedType])}
|
||||||
|
className={`capitalize px-2 py-1 hover:bg-slate-200 cursor-pointer`}
|
||||||
|
>
|
||||||
|
{selectedType[val as keyof typeof selectedType] === "date_time" ? "Date Time" : selectedType[val as keyof typeof selectedType]}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Mask Type */}
|
||||||
|
{
|
||||||
|
selectedTag?.subType === TEXT.PASSWORD &&
|
||||||
|
<div>
|
||||||
|
<label htmlFor="" className="">Mask Type</label>
|
||||||
|
<div
|
||||||
|
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.MASK_TYPE ? null : DROPDOWNTOGGLE.MASK_TYPE)}
|
||||||
|
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
|
||||||
|
<p className="capitalize cursor-pointer">{(selectedTag.maskType && selectedTag.maskType.replace("_", " ")) ? selectedTag.maskType : MASKTYPE.ALL}</p>
|
||||||
|
<MdKeyboardArrowDown />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
dropDownToggle === DROPDOWNTOGGLE.MASK_TYPE &&
|
||||||
|
<div className="shadow bg-slate-100 mt-1 border-[1px] border-slate-100 rounded">
|
||||||
|
{(Object.keys(MASKTYPE) as (keyof typeof MASKTYPE)[]).map((val, indx) =>
|
||||||
|
(
|
||||||
|
<div
|
||||||
|
key={indx}
|
||||||
|
onClick={() => handlePropertyChange("maskType", MASKTYPE[val])}
|
||||||
|
className="capitalize px-2 py-1 hover:bg-slate-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
{(MASKTYPE[val]).replace("_", " ")}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Mask Character - START */}
|
||||||
|
{
|
||||||
|
selectedTag?.subType === TEXT.PASSWORD &&
|
||||||
|
<div>
|
||||||
|
<label htmlFor="" className="">Mask Character</label>
|
||||||
|
<div
|
||||||
|
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.MASK_CHARACTER ? null : DROPDOWNTOGGLE.MASK_CHARACTER)}
|
||||||
|
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
|
||||||
|
<p className="capitalize cursor-pointer py-1">
|
||||||
|
{selectedTag?.maskCharacter
|
||||||
|
?
|
||||||
|
selectedTag.maskCharacter === MASKCHARACTER.ASTERISK && <LuAsterisk /> ||
|
||||||
|
selectedTag.maskCharacter === MASKCHARACTER.DOT && <GoDotFill /> ||
|
||||||
|
selectedTag.maskCharacter === MASKCHARACTER.X && <IoClose />
|
||||||
|
:
|
||||||
|
<LuAsterisk />
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<MdKeyboardArrowDown />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
dropDownToggle === DROPDOWNTOGGLE.MASK_CHARACTER &&
|
||||||
|
<div className="shadow bg-slate-100 mt-1 border-[1px] border-slate-100 rounded">
|
||||||
|
{(Object.keys(MASKCHARACTER) as (keyof typeof MASKCHARACTER)[]).map((val, indx) =>
|
||||||
|
(
|
||||||
|
<div
|
||||||
|
key={indx}
|
||||||
|
onClick={() => handlePropertyChange("maskCharacter", MASKCHARACTER[val])}
|
||||||
|
className="capitalize p-2 hover:bg-slate-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
MASKCHARACTER[val] === MASKCHARACTER.ASTERISK &&
|
||||||
|
<LuAsterisk />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
MASKCHARACTER[val] === MASKCHARACTER.DOT &&
|
||||||
|
<GoDotFill />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
MASKCHARACTER[val] === MASKCHARACTER.X &&
|
||||||
|
<IoClose />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{/* Mask Character - END */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* TEMPORAL DATE FORMAT - START */}
|
||||||
|
{
|
||||||
|
(selectedTag?.subType === TEMPORAL.DATE ||
|
||||||
|
selectedTag?.subType === TEMPORAL.TIME ||
|
||||||
|
selectedTag?.subType === TEMPORAL.DATE_TIME) &&
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{/* <div className="flex justify-between">
|
||||||
|
<label htmlFor="" className="">Formats</label>
|
||||||
|
<div className="flex justify-center items-center gap-1.5">
|
||||||
|
<input
|
||||||
|
checked={showFormatEg ? showFormatEg : false}
|
||||||
|
onChange={(e) => setShowFormatEg(e.target.checked)}
|
||||||
|
id="data_time_format"
|
||||||
|
type="checkbox" />
|
||||||
|
<label className="text-xs text-slate-400" htmlFor="data_time_format">
|
||||||
|
Show Example
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<label htmlFor="" className="">Formats</label>
|
||||||
|
{(selectedTag.subType === TEMPORAL.DATE_TIME || selectedTag.subType === TEMPORAL.TIME) &&
|
||||||
|
<div className="flex justify-center items-center gap-1.5">
|
||||||
|
<input
|
||||||
|
checked={is24Hrs ? is24Hrs : false}
|
||||||
|
onChange={(e) => handleIs24HrsClick(e.target.checked)}
|
||||||
|
id="data_time_format"
|
||||||
|
type="checkbox" />
|
||||||
|
<label className={`text-xs ${is24Hrs ? "text-slate-600" : "text-slate-400"} transitions-all duration-150`} htmlFor="data_time_format">
|
||||||
|
24 Hrs
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => setDropDownToggle(dropDownToggle === DROPDOWNTOGGLE.DDMMYY_FORMAT ? null : DROPDOWNTOGGLE.DDMMYY_FORMAT)}
|
||||||
|
className="flex justify-between items-center border-1 rounded px-2 py-1 border-slate-200">
|
||||||
|
<p className="cursor-pointer py-1">
|
||||||
|
{selectedTag.format ? selectedTag.format : dateTimeFormatMap['date'][0].format}
|
||||||
|
{/* {(selectedTag.subType !== TEMPORAL.DATE && selectedTag.subType !== TEMPORAL.TIME) ? (is24Hrs ? " " + timeFormats[0].format : " " + timeFormats[1].format) : ""} */}
|
||||||
|
</p>
|
||||||
|
<MdKeyboardArrowDown />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
dropDownToggle === DROPDOWNTOGGLE.DDMMYY_FORMAT &&
|
||||||
|
<div className="shadow bg-slate-100 mt-1 border-[1px] max-h-[200px] overflow-y-auto border-slate-100 rounded">
|
||||||
|
{dateTimeFormatMap[selectedTag.subType].map((valObj, indx) => {
|
||||||
|
// let val = showFormatEg ? valObj["example"] : valObj["format"]
|
||||||
|
let val = valObj["format"]
|
||||||
|
if (selectedTag.subType === TEMPORAL.DATE_TIME) {
|
||||||
|
if (is24Hrs) {
|
||||||
|
val = val + " " + timeFormats[0].format;
|
||||||
|
} else {
|
||||||
|
val = val + " " + timeFormats[1].format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={indx}
|
||||||
|
onClick={() => handlePropertyChange("format", valObj["format"])}
|
||||||
|
className="px-2 py-1 hover:bg-slate-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
{val}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{selectedTag.format && <div className="pt-4">
|
||||||
|
{/* <p>
|
||||||
|
Example
|
||||||
|
</p> */}
|
||||||
|
<p className="text-center">{dayjs().format(selectedTag.format)}</p>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{/* TEMPORAL DATE FORMAT - END */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* NUMBER - DECIMAL PLACES */}
|
||||||
|
{
|
||||||
|
selectedTag?.subType === NUMBER.DECIMAL &&
|
||||||
|
<div>
|
||||||
|
<label htmlFor="" className="">Decimal Places</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
onChange={(e) => handleDecimalPlace(e)}
|
||||||
|
value={decimalPlace}
|
||||||
|
className="bg-slate-100 w-full outline-none p-2 rounded" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{(selectedTag?.subType === "single select" || selectedTag?.subType === "multi select") &&
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="" className="">Add Options</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
onChange={(e) => setSelectedOption(e.target.value)}
|
||||||
|
value={selectedOption}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (e.code === "Enter") {
|
||||||
|
// if (selectedOption) {
|
||||||
|
// setSelectedOptions((prev) => [...prev, selectedOption]);
|
||||||
|
// setSelectedOption("");
|
||||||
|
// }
|
||||||
|
handleAddSelectOption()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="bg-slate-100 w-full outline-none p-2 rounded" />
|
||||||
|
|
||||||
|
<div className="h-[150px] mt-1 overflow-hidden overflow-y-auto">
|
||||||
|
{
|
||||||
|
selectedTag.options && selectedTag.options.map((val, indx) => (
|
||||||
|
<div
|
||||||
|
key={indx}
|
||||||
|
className="flex justify-between items-center px-2 py-1 my-1 rounded bg-slate-100">
|
||||||
|
<p>{val}</p>
|
||||||
|
<IoMdClose
|
||||||
|
onClick={() =>
|
||||||
|
// setSelectedOptions(prev =>
|
||||||
|
// prev.filter((_, i) => i !== indx)
|
||||||
|
// )
|
||||||
|
handleSelectOptionDel(indx)
|
||||||
|
}
|
||||||
|
className="cursor-pointer text-slate-500 hover:text-slate-600" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Right Side Bar End */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RightSideBar
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import UserAuthUI from "../auth/UsertAuthUI"
|
||||||
|
|
||||||
|
function Register() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-center items-center w-full h-[100vh]">
|
||||||
|
<UserAuthUI pg={"register"} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Register
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import Setting from '../setting/Setting'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
const PageGuard = () => {
|
||||||
|
const allowedPages = ["settings", "contacts"]
|
||||||
|
|
||||||
|
console.log("Allowed Pages: ", allowedPages)
|
||||||
|
|
||||||
|
const {page1} = useParams<{page1: string}>()
|
||||||
|
if (page1 && !allowedPages.includes(page1)) {
|
||||||
|
return <div>Access Denied</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Setting />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageGuard
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { JSX, useLayoutEffect } from 'react';
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { fetchUserDetails } from '../../store/slices/userSlice';
|
||||||
|
|
||||||
|
const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
dispatch(fetchUserDetails());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtectedRoute;
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import SettingNav from "./SettingNav";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
const Setting = () => {
|
||||||
|
const { page1 } = useParams<{ page1: string }>();
|
||||||
|
const renderContent = () => {
|
||||||
|
switch (activeTab) {
|
||||||
|
case "userProfile":
|
||||||
|
return <div>User Profile content goes here...</div>;
|
||||||
|
case "language":
|
||||||
|
return <div>Language settings content goes here...</div>;
|
||||||
|
case "contactAll":
|
||||||
|
return <div>All Contacts content goes here...</div>;
|
||||||
|
case "contactAdd":
|
||||||
|
return <div>Add Contact form goes here...</div>;
|
||||||
|
case "contactRemove":
|
||||||
|
return <div>Remove Contact UI goes here...</div>;
|
||||||
|
case "contactShare":
|
||||||
|
return <div>Share Contact options go here...</div>;
|
||||||
|
default:
|
||||||
|
return <div>Coming Soon...</div>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: "userProfile", label: "User Profile", type: "settings" },
|
||||||
|
{ id: "language", label: "Language", type: "settings" },
|
||||||
|
{ id: "contactAll", label: "All Contacts", type: "contact" },
|
||||||
|
{ id: "contactAdd", label: "Add Contact", type: "contact" },
|
||||||
|
{ id: "contactRemove", label: "Remove Contact", type: "contact" },
|
||||||
|
{ id: "contactShare", label: "Share Contact", type: "contact" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState("userProfile");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingNav />
|
||||||
|
|
||||||
|
<div className="min-h-screen bg-[#f9fafa]">
|
||||||
|
{/* Top Nav Tabs */}
|
||||||
|
<div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-8 pt-4">
|
||||||
|
<div className="flex gap-6">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
tab.type === page1 && <button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`cursor-pointer relative pb-3 text-sm font-medium transition-all duration-200
|
||||||
|
${activeTab === tab.id
|
||||||
|
? "text-blue-600 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-600"
|
||||||
|
: "text-gray-600 hover:text-blue-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<div className="px-8 py-6">{renderContent()}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Setting;
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import { FaCog } from 'react-icons/fa';
|
||||||
|
import { IoIosNotifications } from 'react-icons/io';
|
||||||
|
import { RootState } from '../../store/store';
|
||||||
|
import { UserState } from '../../store/slices/userSlice';
|
||||||
|
|
||||||
|
const SettingNav = () => {
|
||||||
|
let { page1 } = useParams()
|
||||||
|
const user: UserState = useSelector((state: RootState) => state.user);
|
||||||
|
|
||||||
|
const initials = user.username
|
||||||
|
? user.username
|
||||||
|
.split(' ')
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join('')
|
||||||
|
.slice(0, 2)
|
||||||
|
.toUpperCase()
|
||||||
|
: 'U';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="w-full px-6 py-2 flex justify-between items-center bg-white border-b rounded-sm border-gray-200">
|
||||||
|
<h1 className="text-md font-medium text-gray-800 capitalize">{page1}</h1>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* Notification + Settings with smaller gap */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
aria-label="Notifications"
|
||||||
|
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
||||||
|
>
|
||||||
|
<IoIosNotifications className="text-xl" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/settings"
|
||||||
|
aria-label="Settings"
|
||||||
|
className="p-2 rounded-full hover:bg-gray-100 transition-colors text-gray-600"
|
||||||
|
>
|
||||||
|
<FaCog className="text-sm" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Avatar with separate spacing */}
|
||||||
|
<div className="w-9 h-9 ms-3 rounded-full overflow-hidden bg-[var(--primary)] flex items-center justify-center text-white font-semibold text-sm">
|
||||||
|
{user.picture ? (
|
||||||
|
<img
|
||||||
|
src={user.picture}
|
||||||
|
alt="User avatar"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
initials
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingNav;
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { FaCheck } from 'react-icons/fa';
|
||||||
|
import { SaveType, SaveTypeEnum } from '../client/SignDocument';
|
||||||
|
|
||||||
|
interface DrawProps {
|
||||||
|
saved: SaveType;
|
||||||
|
setSaved: React.Dispatch<React.SetStateAction<SaveType>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Draw: React.FC<DrawProps> = ({ saved, setSaved }) => {
|
||||||
|
|
||||||
|
// DRAW
|
||||||
|
// const [drawSignature, setDrawSignature] = useState<string>("");
|
||||||
|
// const [drawInitial, setDrawInitial] = useState<string>("");
|
||||||
|
|
||||||
|
const canvasRef1 = useRef<HTMLCanvasElement>(null);
|
||||||
|
const canvasRef2 = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const [selectedColor, setSelectedColor] = useState<'black' | 'blue' | 'red' | 'green'>('black');
|
||||||
|
const [isDrawing1, setIsDrawing1] = useState(false);
|
||||||
|
const [isDrawing2, setIsDrawing2] = useState(false);
|
||||||
|
|
||||||
|
const [hasDrawn1, setHasDrawn1] = useState(false);
|
||||||
|
const [hasDrawn2, setHasDrawn2] = useState(false);
|
||||||
|
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const startDrawing = (
|
||||||
|
e: React.MouseEvent,
|
||||||
|
canvasRef: React.RefObject<HTMLCanvasElement>,
|
||||||
|
setIsDrawing: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
) => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas?.getContext('2d');
|
||||||
|
if (!canvas || !ctx) return;
|
||||||
|
|
||||||
|
ctx.strokeStyle = selectedColor;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||||
|
setIsDrawing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = (
|
||||||
|
e: React.MouseEvent,
|
||||||
|
canvasRef: React.RefObject<HTMLCanvasElement>,
|
||||||
|
isDrawing: boolean,
|
||||||
|
setHasDrawn: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
) => {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas?.getContext('2d');
|
||||||
|
if (!canvas || !ctx) return;
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
|
||||||
|
ctx.stroke();
|
||||||
|
setHasDrawn(true);
|
||||||
|
// setSaved({
|
||||||
|
// saveType: null,
|
||||||
|
// save: false,
|
||||||
|
// signature: null,
|
||||||
|
// initial: null
|
||||||
|
// });
|
||||||
|
setError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const endDrawing = (setIsDrawing: React.Dispatch<React.SetStateAction<boolean>>) => {
|
||||||
|
setIsDrawing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCanvas = (
|
||||||
|
canvasRef: React.RefObject<HTMLCanvasElement>,
|
||||||
|
setHasDrawn: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
) => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas?.getContext('2d');
|
||||||
|
if (!canvas || !ctx) return;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
setHasDrawn(false);
|
||||||
|
// setSaved({
|
||||||
|
// saveType: null,
|
||||||
|
// save: false,
|
||||||
|
// signature: null,
|
||||||
|
// initial: null
|
||||||
|
// });
|
||||||
|
setError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (!hasDrawn1 || !hasDrawn2) {
|
||||||
|
setError('Add both signature and initials before saving!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(canvasRef1.current?.toDataURL());
|
||||||
|
|
||||||
|
// setDrawSignature(canvasRef1.current?.toDataURL() || '');
|
||||||
|
// setDrawInitial(canvasRef2.current?.toDataURL() || '');
|
||||||
|
|
||||||
|
// Simulate save
|
||||||
|
setSaved({
|
||||||
|
saveType: SaveTypeEnum.DRAW,
|
||||||
|
save: true,
|
||||||
|
signature: canvasRef1.current?.toDataURL() || '',
|
||||||
|
initial: canvasRef2.current?.toDataURL() || ''
|
||||||
|
});
|
||||||
|
setError('');
|
||||||
|
// alert('Both canvases saved!');
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (!saved && (hasDrawn1 || hasDrawn2)) {
|
||||||
|
if (!window.confirm('Unsaved changes will be lost. Continue?')) return;
|
||||||
|
}
|
||||||
|
clearCanvas(canvasRef1, setHasDrawn1);
|
||||||
|
clearCanvas(canvasRef2, setHasDrawn2);
|
||||||
|
// setSaved({
|
||||||
|
// saveType: null,
|
||||||
|
// save: false
|
||||||
|
// });
|
||||||
|
setError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full border-t-1 border-slate-300'>
|
||||||
|
|
||||||
|
<div className="rounded-sm text-sm m-2.5">
|
||||||
|
{/* <div className="flex justify-between items-center mb-4"> */}
|
||||||
|
{/* <h2 className="text-xl font-semibold">Dual Drawing Canvas</h2> */}
|
||||||
|
{/* <button
|
||||||
|
onClick={close}
|
||||||
|
title="Close"
|
||||||
|
className="text-red-500 hover:text-red-700 transition"
|
||||||
|
>
|
||||||
|
<FiX size={24} />
|
||||||
|
</button> */}
|
||||||
|
{/* </div> */}
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row not-first:flex justify-center gap-4 w-full">
|
||||||
|
{[canvasRef1, canvasRef2].map((ref, index) => {
|
||||||
|
const setIsDrawing = index === 0 ? setIsDrawing1 : setIsDrawing2;
|
||||||
|
const isDrawing = index === 0 ? isDrawing1 : isDrawing2;
|
||||||
|
const setHasDrawn = index === 0 ? setHasDrawn1 : setHasDrawn2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
<div className='flex justify-between items-center w-full'>
|
||||||
|
<p className='text-slate-400 font-semibold pointer-events-none'>{index === 0 ? "Signature" : "Initial"}</p>
|
||||||
|
<div className='flex justify-center items-center gap-1'>
|
||||||
|
<div className="flex cursor-pointer">
|
||||||
|
{['black', 'blue', 'red', 'green'].map((color) => (
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-sm shadow mr-1 flex justify-center items-center ${selectedColor === color ? 'scale-110' : 'scale-100'} transition-all`}
|
||||||
|
key={color}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedColor(color as any);
|
||||||
|
clearCanvas(ref, setHasDrawn)
|
||||||
|
}}
|
||||||
|
title={color}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
>
|
||||||
|
{/* */}
|
||||||
|
{selectedColor === color && <FaCheck className='fill-white text-xs p-[1.5px]' />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => clearCanvas(ref, setHasDrawn)}
|
||||||
|
className="py-1 text-sm rounded text-slate-400 cursor-pointer"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<canvas
|
||||||
|
ref={ref}
|
||||||
|
width={280}
|
||||||
|
height={180}
|
||||||
|
className="bg-white h-auto rounded border-1 border-slate-400"
|
||||||
|
onPointerDown={(e) => startDrawing(e, ref, setIsDrawing)}
|
||||||
|
onPointerMove={(e) => draw(e, ref, isDrawing, setHasDrawn)}
|
||||||
|
onPointerUp={() => endDrawing(setIsDrawing)}
|
||||||
|
onPointerLeave={() => endDrawing(setIsDrawing)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="m-3 flex justify-end gap-2">
|
||||||
|
{/* <button
|
||||||
|
onClick={close}
|
||||||
|
className="border-1 border-[var(--primary)] text-[var(--primary)] px-4 text-sm py-1 rounded shadow opacity-70 hover:opacity-100 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button> */}
|
||||||
|
<button
|
||||||
|
onClick={save}
|
||||||
|
className="bg-[var(--primary)] opacity-90 hover:opacity-100 text-white px-4 text-sm py-1 rounded shadow disabled:opacity-50 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* {error && (
|
||||||
|
<div className="mt-4 text-red-500 font-medium animate-pulse">{error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{saved && (
|
||||||
|
<div className="mt-4 text-green-600 font-medium">Canvas saved successfully!</div>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Draw;
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import Draw from './Draw'
|
||||||
|
import TypedSign from './TypedSign';
|
||||||
|
import SignUpload from './SignUpload';
|
||||||
|
import { IoMdClose } from 'react-icons/io';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { store } from '../../store/store';
|
||||||
|
import { SaveType, SaveTypeEnum } from '../client/SignDocument';
|
||||||
|
|
||||||
|
interface SignConProps {
|
||||||
|
setOpenSign: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
recipientId: string;
|
||||||
|
saved: SaveType;
|
||||||
|
setSaved: React.Dispatch<React.SetStateAction<SaveType>>;
|
||||||
|
// save: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignCon: React.FC<SignConProps> = ({ setOpenSign, recipientId, saved, setSaved }) => {
|
||||||
|
|
||||||
|
const [activeSaveType, setActiveSaveType] = React.useState<SaveTypeEnum>(SaveTypeEnum.DRAW);
|
||||||
|
|
||||||
|
const handleSaveSignature = () => {
|
||||||
|
if (!saved.save) return
|
||||||
|
|
||||||
|
console.log("signature:", saved.initial, "initials:", saved.signature, "subType:", saved.saveType, "MIC CHECK")
|
||||||
|
// return
|
||||||
|
|
||||||
|
axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/signature?recipientId=${recipientId}`,
|
||||||
|
{
|
||||||
|
signature: saved.signature,
|
||||||
|
initials: saved.initial,
|
||||||
|
subType: saved.saveType
|
||||||
|
},
|
||||||
|
{ headers: { "ngrok-skip-browser-warning": "69420", } })
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res, "Signature saved response")
|
||||||
|
if (res.status === 200 || res.status === 201) {
|
||||||
|
setOpenSign(false)
|
||||||
|
setActiveSaveType(saved.saveType as SaveTypeEnum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error saving signature:', err);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleSaveSignature();
|
||||||
|
}, [saved])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='absolute top-0 left-0 w-full h-full flex items-center justify-center '>
|
||||||
|
<div className='absolute top-0 left-0 w-full h-full bg-slate-100 opacity-60'></div>
|
||||||
|
|
||||||
|
<div className='relative lg:min-w-[650px] max-w-max mx-auto shadow-md rounded-sm bg-white pt-3 pb-1 px-3'>
|
||||||
|
<IoMdClose
|
||||||
|
onClick={() => setOpenSign(false)}
|
||||||
|
className='absolute right-3 text-slate-400 text-lg hover:text-[var(--primary)] cursor-pointer transition-all' />
|
||||||
|
{/* Signature Save types Nav */}
|
||||||
|
<div className='flex text-sm'>
|
||||||
|
<div className={`px-4 pb-1 cursor-pointer border-b-2 font-medium ${activeSaveType === SaveTypeEnum.DRAW ? "border-[var(--primary)] text-[var(--primary)]" : "border-slate-100"}`}
|
||||||
|
onClick={() => setActiveSaveType(SaveTypeEnum.DRAW)}>
|
||||||
|
<p className='tracking-wide'>Draw</p>
|
||||||
|
</div>
|
||||||
|
<div className={`px-4 pb-1 cursor-pointer border-b-2 font-medium ${activeSaveType === SaveTypeEnum.TYPE ? "border-[var(--primary)] text-[var(--primary)]" : "border-slate-100"}`}
|
||||||
|
onClick={() => setActiveSaveType(SaveTypeEnum.TYPE)}>
|
||||||
|
<p className='tracking-wide'>Type</p>
|
||||||
|
</div>
|
||||||
|
<div className={`px-4 pb-1 cursor-pointer border-b-2 font-medium ${activeSaveType === SaveTypeEnum.UPLOAD ? "border-[var(--primary)] text-[var(--primary)]" : "border-slate-100"}`}
|
||||||
|
onClick={() => setActiveSaveType(SaveTypeEnum.UPLOAD)}>
|
||||||
|
<p className='tracking-wide'>Upload</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeSaveType === SaveTypeEnum.DRAW && <Draw
|
||||||
|
saved={saved}
|
||||||
|
setSaved={setSaved}
|
||||||
|
/>}
|
||||||
|
{activeSaveType === SaveTypeEnum.TYPE && <TypedSign
|
||||||
|
saved={saved}
|
||||||
|
setSaved={setSaved}
|
||||||
|
/>}
|
||||||
|
{activeSaveType === SaveTypeEnum.UPLOAD && <SignUpload
|
||||||
|
saved={saved}
|
||||||
|
setSaved={setSaved}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
{/* <div className="mt-3 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
// onClick={close}
|
||||||
|
className="border-1 border-[var(--primary)] text-[var(--primary)] px-4 text-sm py-1 rounded shadow opacity-70 hover:opacity-100 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
// onClick={save}
|
||||||
|
className="bg-[var(--primary)] opacity-90 hover:opacity-100 text-white px-4 text-sm py-1 rounded shadow disabled:opacity-50 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignCon;
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { SaveType, SaveTypeEnum } from '../client/SignDocument';
|
||||||
|
|
||||||
|
interface SignUploadProps {
|
||||||
|
saved: SaveType;
|
||||||
|
setSaved: React.Dispatch<React.SetStateAction<SaveType>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignUpload: React.FC<SignUploadProps> = ({ saved, setSaved }) => {
|
||||||
|
const [signatureFile, setSignatureFile] = useState<string>("");
|
||||||
|
const [initialFile, setInitialFile] = useState<string>("");
|
||||||
|
|
||||||
|
const signatureInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const initialInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleDrop = (
|
||||||
|
e: React.DragEvent<HTMLDivElement>,
|
||||||
|
type: 'signature' | 'initial'
|
||||||
|
) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
type === 'signature' ? setSignatureFile(file) : setInitialFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (type: 'signature' | 'initial') => {
|
||||||
|
if (type === 'signature') signatureInputRef.current?.click();
|
||||||
|
else initialInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
// const handleFileChange = (
|
||||||
|
// e: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
// type: 'signature' | 'initial'
|
||||||
|
// ) => {
|
||||||
|
// const file = e.target.files?.[0];
|
||||||
|
// if (file && file.type.startsWith('image/')) {
|
||||||
|
// type === 'signature' ? setSignatureFile(file) : setInitialFile(file);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
const toBase64 = (file: File): Promise<string> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
reader.onload = () => {
|
||||||
|
if (typeof reader.result === 'string') {
|
||||||
|
resolve(reader.result);
|
||||||
|
} else {
|
||||||
|
reject(new Error('FileReader result is not a string'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFileChange = async (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
type: 'signature' | 'initial'
|
||||||
|
): Promise<void> => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
try {
|
||||||
|
const base64 = await toBase64(file);
|
||||||
|
type === 'signature' ? setSignatureFile(base64) : setInitialFile(base64);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting file to base64:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (!signatureFile || !initialFile) {
|
||||||
|
alert('Please upload both signature and initials.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaved({
|
||||||
|
saveType: SaveTypeEnum.UPLOAD,
|
||||||
|
signature: signatureFile,
|
||||||
|
initial: initialFile,
|
||||||
|
save: true
|
||||||
|
})
|
||||||
|
|
||||||
|
alert('Files saved as Blob. Check console for details.');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full border-t-1 border-slate-300">
|
||||||
|
<div className="flex gap-2 text-sm m-2.5">
|
||||||
|
{/* Signature Upload */}
|
||||||
|
<div
|
||||||
|
className="border-1 w-full border-dashed border-gray-300 rounded-lg py-8 px-6 flex flex-col items-center justify-center cursor-pointer hover:border-[var(--primary)] transition relative"
|
||||||
|
onClick={() => handleClick('signature')}
|
||||||
|
onDragOver={(e) => e.preventDefault()}
|
||||||
|
onDrop={(e) => handleDrop(e, 'signature')}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={signatureInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => handleFileChange(e, 'signature')}
|
||||||
|
/>
|
||||||
|
<p className="text-gray-400 text-center">
|
||||||
|
{signatureFile ? 'Signature Uploaded' : 'Drop Signature Here or Click to Upload'}
|
||||||
|
</p>
|
||||||
|
{signatureFile && (
|
||||||
|
<img
|
||||||
|
src={signatureFile}
|
||||||
|
alt="Signature Preview"
|
||||||
|
className="mt-4 h-32 object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Initials Upload */}
|
||||||
|
<div
|
||||||
|
className="border-1 w-full border-dashed border-gray-300 rounded-lg py-8 px-6 flex flex-col items-center justify-center cursor-pointer hover:border-[var(--primary)] transition relative"
|
||||||
|
onClick={() => handleClick('initial')}
|
||||||
|
onDragOver={(e) => e.preventDefault()}
|
||||||
|
onDrop={(e) => handleDrop(e, 'initial')}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={initialInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => handleFileChange(e, 'initial')}
|
||||||
|
/>
|
||||||
|
<p className="text-gray-400 text-center">
|
||||||
|
{initialFile ? 'Initials Uploaded' : 'Drop Initials Here or Click to Upload'}
|
||||||
|
</p>
|
||||||
|
{initialFile && (
|
||||||
|
<img
|
||||||
|
src={initialFile}
|
||||||
|
alt="Initial Preview"
|
||||||
|
className="mt-4 h-32 object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <button
|
||||||
|
className="mt-6 bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition"
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
Save as BLOB
|
||||||
|
</button> */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className="m-3 flex justify-end gap-2">
|
||||||
|
{/* <button
|
||||||
|
// onClick={close}
|
||||||
|
className="border-1 border-[var(--primary)] text-[var(--primary)] px-4 text-sm py-1 rounded shadow opacity-70 hover:opacity-100 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button> */}
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
// onClick={save}
|
||||||
|
className="bg-[var(--primary)] opacity-90 hover:opacity-100 text-white px-4 text-sm py-1 rounded shadow disabled:opacity-50 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignUpload;
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { SaveType, SaveTypeEnum } from '../client/SignDocument';
|
||||||
|
|
||||||
|
interface TypedProps {
|
||||||
|
saved: SaveType;
|
||||||
|
setSaved: React.Dispatch<React.SetStateAction<SaveType>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TypedSign: React.FC<TypedProps> = ({ saved, setSaved }) => {
|
||||||
|
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const [typedSign, setTypedSign] = useState<string>("")
|
||||||
|
const [typedInitial, setTypedInitial] = useState<string>("")
|
||||||
|
|
||||||
|
const [hasTyped1, setHasTyped1] = useState(false);
|
||||||
|
const [hasTyped2, setHasTyped2] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typedSign.trim().length > 0) {
|
||||||
|
setHasTyped1(true);
|
||||||
|
} else {
|
||||||
|
if (!hasTyped1) return
|
||||||
|
setHasTyped1(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typedInitial.trim().length > 0) {
|
||||||
|
setHasTyped2(true);
|
||||||
|
} else {
|
||||||
|
if (!hasTyped2) return
|
||||||
|
setHasTyped2(false);
|
||||||
|
}
|
||||||
|
}, [typedSign, typedInitial])
|
||||||
|
|
||||||
|
const handleSaveClick = () => {
|
||||||
|
|
||||||
|
// console.log("Saved CLICK CALLED", SaveTypeEnum.TYPE, hasTyped1, hasTyped2);
|
||||||
|
|
||||||
|
if (!hasTyped1 || !hasTyped2) {
|
||||||
|
setError('Add both signature and initials before saving!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDrawSignature(canvasRef1.current?.toDataURL() || '');
|
||||||
|
// setDrawInitial(canvasRef2.current?.toDataURL() || '');
|
||||||
|
|
||||||
|
// Simulate save
|
||||||
|
setSaved({
|
||||||
|
saveType: SaveTypeEnum.TYPE,
|
||||||
|
signature: typedSign,
|
||||||
|
initial: typedInitial,
|
||||||
|
save: true
|
||||||
|
});
|
||||||
|
|
||||||
|
setError('');
|
||||||
|
alert('Both canvases saved!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full border-t-1 border-slate-300'>
|
||||||
|
<div className='flex gap-2 text-sm m-2.5'>
|
||||||
|
<div className='w-full'>
|
||||||
|
|
||||||
|
<input
|
||||||
|
onChange={(e) => setTypedSign(e.target.value)}
|
||||||
|
className='outline-none border-1 border-slate-200 w-full p-2 text-sm text-slate-500 bg-white rounded'
|
||||||
|
placeholder='Type Signature . . .' type="text" />
|
||||||
|
<p className='text-center py-8 text-lg w-full bg-white border-1 border-slate-200 mt-4 rounded-sm text-slate-400 pointer-events-none italic'>
|
||||||
|
{typedSign ? typedSign : "Signature"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='w-full'>
|
||||||
|
|
||||||
|
<input
|
||||||
|
onChange={(e) => setTypedInitial(e.target.value)}
|
||||||
|
maxLength={3}
|
||||||
|
min={1}
|
||||||
|
className='outline-none border-1 border-slate-200 w-full p-2 text-sm text-slate-500 bg-white rounded'
|
||||||
|
placeholder='Type Initial . . .' type="text" />
|
||||||
|
<p className='text-center py-8 text-lg w-full bg-white border-1 border-slate-200 mt-4 rounded-sm text-slate-400 pointer-events-none italic'>
|
||||||
|
{typedInitial ? typedInitial : "Initial"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="m-3 flex justify-end gap-2">
|
||||||
|
{/* <button
|
||||||
|
// onClick={close}
|
||||||
|
className="border-1 border-[var(--primary)] text-[var(--primary)] px-4 text-sm py-1 rounded shadow opacity-70 hover:opacity-100 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button> */}
|
||||||
|
<button
|
||||||
|
onClick={handleSaveClick}
|
||||||
|
className="bg-[var(--primary)] opacity-90 hover:opacity-100 text-white px-4 text-sm py-1 rounded shadow disabled:opacity-50 transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TypedSign
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import SignaturePad from './SignaturePad'
|
||||||
|
import { IoMdClose } from 'react-icons/io';
|
||||||
|
import InitialPad from './InitialPad';
|
||||||
|
|
||||||
|
interface InitialConProps {
|
||||||
|
openInitial: boolean;
|
||||||
|
setOpenInitial: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
savedInitial: string | null;
|
||||||
|
setSavedInitial: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InitialCon: React.FC<InitialConProps> = ({ openInitial, setOpenInitial, savedInitial, setSavedInitial }) => {
|
||||||
|
|
||||||
|
const handleSaveInitial = (signatureDataUrl: string) => {
|
||||||
|
console.log(signatureDataUrl, "savedInitial ------ 123");
|
||||||
|
setSavedInitial(signatureDataUrl);
|
||||||
|
setOpenInitial(false)
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// const handleSignClose = () => {
|
||||||
|
// setOpenInitial(false)
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 w-full h-screen"
|
||||||
|
style={{
|
||||||
|
display: `${openInitial ? 'block' : 'none'}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-full h-screen bg-slate-100 opacity-40 blur-[15px]"></div>
|
||||||
|
<div className="absolute z-50 top-[50%] left-[50%] translate-[-50%]">
|
||||||
|
{/* <IoMdClose
|
||||||
|
onClick={() => handleSignClose()}
|
||||||
|
className='absolute right-[15px] top-[12px] text-xl text-slate-400 hover:text-slate-600 transition cursor-pointer' /> */}
|
||||||
|
{/* <SignaturePad onSave={handleSaveInitial} /> */}
|
||||||
|
<InitialPad onSave={handleSaveInitial} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InitialCon;
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
/ components/InitialPad.tsx
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
interface InitialPadProps {
|
||||||
|
onSave: (signatureDataUrl: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InitialPad: React.FC<InitialPadProps> = ({ onSave }) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [isDrawing, setIsDrawing] = useState(false);
|
||||||
|
const [initials, setInitials] = useState('');
|
||||||
|
const [isCanvasClear, setIsCanvasClear] = useState(true);
|
||||||
|
const [uploadedImage, setUploadedImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const getPointerPosition = (e: PointerEvent) => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return { x: 0, y: 0 };
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: e.clientX - rect.left,
|
||||||
|
y: e.clientY - rect.top,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDrawing = (e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||||
|
const ctx = canvasRef.current?.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const { x, y } = getPointerPosition(e.nativeEvent);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
setIsDrawing(true);
|
||||||
|
setIsCanvasClear(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = (e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
const ctx = canvasRef.current?.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const { x, y } = getPointerPosition(e.nativeEvent);
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrawing = () => {
|
||||||
|
setIsDrawing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCanvas = () => {
|
||||||
|
const ctx = canvasRef.current?.getContext('2d');
|
||||||
|
if (ctx && canvasRef.current) {
|
||||||
|
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||||
|
}
|
||||||
|
setUploadedImage(null);
|
||||||
|
setIsCanvasClear(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyInitials = () => {
|
||||||
|
const ctx = canvasRef.current?.getContext('2d');
|
||||||
|
if (ctx && canvasRef.current) {
|
||||||
|
clearCanvas();
|
||||||
|
ctx.font = '48px cursive';
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.fillText(initials.toUpperCase(), 50, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const result = reader.result as string;
|
||||||
|
setUploadedImage(result);
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const ctx = canvasRef.current?.getContext('2d');
|
||||||
|
if (ctx && canvasRef.current) {
|
||||||
|
clearCanvas();
|
||||||
|
ctx.drawImage(img, 0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.src = result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSignature = () => {
|
||||||
|
// if (isCanvasClear) {
|
||||||
|
// onSave('');
|
||||||
|
// return;
|
||||||
|
// };
|
||||||
|
const dataURL = canvasRef.current?.toDataURL('image/png');
|
||||||
|
if (dataURL) {
|
||||||
|
onSave(dataURL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white px-6 pb-6 pt-2.5 rounded-md shadow-md w-full max-w-md">
|
||||||
|
<h2 className="text-md text-slate-600 mb-2 text-left">Create Your Initial</h2>
|
||||||
|
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={400}
|
||||||
|
height={200}
|
||||||
|
onPointerDown={startDrawing}
|
||||||
|
onPointerMove={draw}
|
||||||
|
onPointerUp={stopDrawing}
|
||||||
|
onPointerLeave={stopDrawing}
|
||||||
|
className="border border-gray-300 rounded-md mb-4 w-full touch-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mb-3 text-sm">
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Enter Initial</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={initials}
|
||||||
|
onChange={(e) => setInitials(e.target.value)}
|
||||||
|
// maxLength={3}
|
||||||
|
placeholder="e.g. AB"
|
||||||
|
className="flex-1 px-3 py-1 outline-none border-1 border-slate-400 rounded-md"
|
||||||
|
/>
|
||||||
|
{/* <button
|
||||||
|
onClick={applyInitials}
|
||||||
|
className="px-4 py-1.5 bg-[var(--primary)] text-white rounded-md hover:opacity-80 transition cursor-pointer"
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-sm">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Upload PNG Signature</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/png"
|
||||||
|
onChange={handleUpload}
|
||||||
|
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-slate-100 file:text-[var(--primary)] hover:file:bg-slate-200 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 text-sm">
|
||||||
|
<button
|
||||||
|
onClick={clearCanvas}
|
||||||
|
className="flex-1 px-4 py-1.5 text-slate-600 bg-slate-100 rounded-md hover:bg-slate-200 transition outline-none"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={saveSignature}
|
||||||
|
className="flex-1 px-4 py-1.5 bg-[var(--primary)] text-white rounded-md hover:opacity-85 transition outline-none cursor-pointer"
|
||||||
|
>
|
||||||
|
Sign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InitialPad;
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import SignaturePad from './SignaturePad'
|
||||||
|
import { IoMdClose } from 'react-icons/io';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface SignatureConProps {
|
||||||
|
recipientId: string;
|
||||||
|
openSign: boolean;
|
||||||
|
setOpenSign: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
savedSignature: string | null;
|
||||||
|
setSavedSignature: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignatureCon: React.FC<SignatureConProps> = ({ recipientId, openSign, setOpenSign, savedSignature, setSavedSignature }) => {
|
||||||
|
|
||||||
|
const handleSaveSignature = (signatureDataUrl: string) => {
|
||||||
|
console.log('Signature saved:', signatureDataUrl);
|
||||||
|
axios.post(`${import.meta.env.VITE_BASE_URI}/api/sign/signature?recipientId=${recipientId}`,
|
||||||
|
{
|
||||||
|
signature: signatureDataUrl,
|
||||||
|
initials: signatureDataUrl,
|
||||||
|
type: "DRAW",
|
||||||
|
},
|
||||||
|
{ headers: { "ngrok-skip-browser-warning": "69420", } })
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res, "Signature saved response")
|
||||||
|
if (res.status === 200 || res.status === 201) {
|
||||||
|
console.log('Signature saved successfully');
|
||||||
|
setSavedSignature(signatureDataUrl);
|
||||||
|
setOpenSign(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error saving signature:', err);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Optional: you can also upload it to server or download as image
|
||||||
|
};
|
||||||
|
|
||||||
|
// const handleSignClose = () => {
|
||||||
|
// setOpenSign(false)
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 w-full h-screen"
|
||||||
|
style={{
|
||||||
|
display: `${openSign ? 'block' : 'none'}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="w-full h-screen bg-slate-100 opacity-40 blur-[15px]"></div>
|
||||||
|
<div className="absolute z-50 top-[50%] left-[50%] translate-[-50%]">
|
||||||
|
{/* <IoMdClose
|
||||||
|
onClick={() => handleSignClose()}
|
||||||
|
className='absolute right-[15px] top-[12px] text-xl text-slate-400 hover:text-slate-600 transition cursor-pointer' /> */}
|
||||||
|
<SignaturePad onSave={handleSaveSignature} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignatureCon
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
/ components/SignaturePad.tsx
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
interface SignaturePadProps {
|
||||||
|
onSave: (signatureDataUrl: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignaturePad: React.FC<SignaturePadProps> = ({ onSave }) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [isDrawing, setIsDrawing] = useState(false);
|
||||||
|
const [initials, setInitials] = useState('');
|
||||||
|
const [isCanvasClear, setIsCanvasClear] = useState(true);
|
||||||
|
const [uploadedImage, setUploadedImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const getPointerPosition = (e: PointerEvent) => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return { x: 0, y: 0 };
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: e.clientX - rect.left,
|
||||||
|
y: e.clientY - rect.top,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDrawing = (e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||||
|
const ctx = canvasRef.current?.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const { x, y } = getPointerPosition(e.nativeEvent);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
setIsDrawing(true);
|
||||||
|
setIsCanvasClear(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = (e: React.PointerEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
const ctx = canvasRef.current?.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const { x, y } = getPointerPosition(e.nativeEvent);
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
ctx.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrawing = () => {
|
||||||
|
setIsDrawing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCanvas = () => {
|
||||||
|
const ctx = canvasRef.current?.getContext('2d');
|
||||||
|
if (ctx && canvasRef.current) {
|
||||||
|
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||||
|
}
|
||||||
|
setIsCanvasClear(true);
|
||||||
|
setUploadedImage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyInitials = () => {
|
||||||
|
const ctx = canvasRef.current?.getContext('2d');
|
||||||
|
if (ctx && canvasRef.current) {
|
||||||
|
clearCanvas();
|
||||||
|
ctx.font = '48px cursive';
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.fillText(initials.toUpperCase(), 50, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const result = reader.result as string;
|
||||||
|
setUploadedImage(result);
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const ctx = canvasRef.current?.getContext('2d');
|
||||||
|
if (ctx && canvasRef.current) {
|
||||||
|
clearCanvas();
|
||||||
|
ctx.drawImage(img, 0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.src = result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveSignature = () => {
|
||||||
|
// if (isCanvasClear) {
|
||||||
|
// onSave('');
|
||||||
|
// return;
|
||||||
|
// };
|
||||||
|
const dataURL = canvasRef.current?.toDataURL('image/png');
|
||||||
|
if (dataURL) {
|
||||||
|
onSave(dataURL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white px-6 pb-6 pt-2.5 rounded-md shadow-md w-full max-w-md">
|
||||||
|
<h2 className="text-md text-slate-600 mb-2 text-left">Create Your Signature</h2>
|
||||||
|
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={400}
|
||||||
|
height={200}
|
||||||
|
onPointerDown={startDrawing}
|
||||||
|
onPointerMove={draw}
|
||||||
|
onPointerUp={stopDrawing}
|
||||||
|
onPointerLeave={stopDrawing}
|
||||||
|
className="border border-gray-300 rounded-md mb-4 w-full touch-none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mb-3 text-sm">
|
||||||
|
<label className="block font-medium text-gray-700 mb-1">Enter Signature</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={initials}
|
||||||
|
onChange={(e) => setInitials(e.target.value)}
|
||||||
|
// maxLength={3}
|
||||||
|
placeholder="e.g. AB"
|
||||||
|
className="flex-1 px-3 outline-none border-1 border-slate-400 rounded-md"
|
||||||
|
/>
|
||||||
|
{/* <button
|
||||||
|
onClick={applyInitials}
|
||||||
|
className="px-4 py-1.5 bg-[var(--primary)] text-white rounded-md hover:opacity-80 transition cursor-pointer"
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3 text-sm">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Upload PNG Signature</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/png"
|
||||||
|
onChange={handleUpload}
|
||||||
|
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-slate-100 file:text-[var(--primary)] hover:file:bg-slate-200 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 text-sm">
|
||||||
|
<button
|
||||||
|
onClick={clearCanvas}
|
||||||
|
className="flex-1 px-4 py-1.5 text-slate-600 bg-slate-100 rounded-md hover:bg-slate-200 transition outline-none"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={saveSignature}
|
||||||
|
className="flex-1 px-4 py-1.5 bg-[var(--primary)] text-white rounded-md hover:opacity-85 transition outline-none cursor-pointer"
|
||||||
|
>
|
||||||
|
Sign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignaturePad;
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
import React, { useRef, useState, DragEvent, ChangeEvent } from 'react';
|
||||||
|
import { FiUploadCloud } from 'react-icons/fi';
|
||||||
|
import { StampTypeObj } from '../client/SignDocument';
|
||||||
|
|
||||||
|
interface StampUploadType {
|
||||||
|
stamp: string;
|
||||||
|
setStamp: React.Dispatch<React.SetStateAction<StampTypeObj>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StampUpload: React.FC<StampUploadType> = ({ stamp, setStamp }) => {
|
||||||
|
|
||||||
|
console.log(stamp, "stamp upload component");
|
||||||
|
|
||||||
|
|
||||||
|
// const [preview, setStamp] = useState<string | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
|
||||||
|
reader.onload = () => {
|
||||||
|
if (typeof reader.result === 'string') {
|
||||||
|
console.log(reader.result, "reader result");
|
||||||
|
setStamp((prev) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
blob: reader.result
|
||||||
|
} as StampTypeObj
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const file = e.dataTransfer.files?.[0];
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
if (typeof reader.result === 'string') {
|
||||||
|
setStamp((prev) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
blob: reader.result
|
||||||
|
} as StampTypeObj
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = () => {
|
||||||
|
setStamp((prev) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
blob: ""
|
||||||
|
} as StampTypeObj
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||||
|
<div className='absolute w-full h-full bg-white opacity-20 pointer-events-none'></div>
|
||||||
|
<div
|
||||||
|
onClick={handleClick}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
className="relative w-80 p-4 bg-white rounded-lg shadow-lg border border-gray-300 cursor-pointer transition hover:shadow-xl"
|
||||||
|
>
|
||||||
|
{stamp}
|
||||||
|
{stamp.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="relative z-50">
|
||||||
|
<img
|
||||||
|
src={stamp}
|
||||||
|
alt="Preview"
|
||||||
|
className="w-full h-48 object-contain rounded"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemove();
|
||||||
|
}}
|
||||||
|
className="cursor-pointer absolute top-[-12px] right-[-10px] text-slate-400 rounded-md px-2 py-1 text-md transition font-bold hover:text-slate-500"
|
||||||
|
aria-label="Remove"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setStamp((prev) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
open: false
|
||||||
|
} as StampTypeObj
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className='text-sm bg-[var(--primary)] text-white rounded-md px-2 py-1 w-full mt-2.5 hover:bg-[var(--primary)] cursor-pointer'>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center h-40 text-gray-500">
|
||||||
|
<FiUploadCloud className="text-4xl mb-2 text-gray-400" />
|
||||||
|
<p className="text-sm">Click or drag image here</p>
|
||||||
|
<p className="text-xs text-gray-400">(Only image files)</p>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemove();
|
||||||
|
setStamp((prev) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
...prev,
|
||||||
|
open: false
|
||||||
|
} as StampTypeObj
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="cursor-pointer absolute top-[5px] right-[6px] text-slate-400 rounded-md px-2 py-1 text-md transition font-bold hover:text-slate-500"
|
||||||
|
aria-label="Remove"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{stamp.length === 0 && <input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StampUpload;
|
||||||
|
|
@ -0,0 +1,360 @@
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||||
|
|
||||||
|
/* .auth-ui-con * {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: "Poppins", sans-serif;
|
||||||
|
text-decoration: none;
|
||||||
|
list-style: none;
|
||||||
|
} */
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .main-container-l {
|
||||||
|
/* background-image: url("images/login-bg.webp") !important; */
|
||||||
|
background-image: url("images/login-bg2.png") !important;
|
||||||
|
background-position: left bottom !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
background-size: 100%;
|
||||||
|
/* display: flex !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
align-items: center !important; */
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
/* background: linear-gradient(90deg, #e2e2e2, #c9d6ff); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
width: 850px;
|
||||||
|
height: 550px;
|
||||||
|
background: #fff;
|
||||||
|
/* margin-top: 20px; */
|
||||||
|
margin-left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-radius: 30px;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 0, 0, .2);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
margin: -10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l p {
|
||||||
|
font-size: 14.5px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .form-box {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
/* transition: .6s ease-in-out 1.2s, visibility 0s 1s; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l.active .form-box {
|
||||||
|
right: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .form-box.register {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l.active .form-box.register {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.auth-ui-con .input-box {
|
||||||
|
position: relative;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .input-box input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 13px 50px 13px 20px;
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .input-box input::placeholder {
|
||||||
|
color: #888;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .input-box i {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .forgot-link {
|
||||||
|
margin: -15px 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .forgot-link a {
|
||||||
|
font-size: 14.5px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, .1);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .social-icons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .social-icons a {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-box {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-box::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -250%;
|
||||||
|
width: 300%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--primary);
|
||||||
|
/* border: 2px solid red; */
|
||||||
|
border-radius: 100px;
|
||||||
|
z-index: 2;
|
||||||
|
/* transition: 1.8s ease-in-out; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l.active .toggle-box::before {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-panel {
|
||||||
|
position: absolute;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
/* background: seagreen; */
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 2;
|
||||||
|
/* transition: .6s ease-in-out; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-panel.toggle-left {
|
||||||
|
left: 0;
|
||||||
|
/* transition-delay: 1.2s; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l.active .toggle-panel.toggle-left {
|
||||||
|
left: -50%;
|
||||||
|
/* transition-delay: .6s; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-panel.toggle-right {
|
||||||
|
right: -50%;
|
||||||
|
/* transition-delay: .6s; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l.active .toggle-panel.toggle-right {
|
||||||
|
right: 0;
|
||||||
|
/* transition-delay: 1.2s; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-panel p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-panel .btn {
|
||||||
|
width: 160px;
|
||||||
|
height: 46px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-container {
|
||||||
|
margin-top: 25px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-container .input-box {
|
||||||
|
position: static !important;
|
||||||
|
margin: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fl-container .input-box input {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 950px) {
|
||||||
|
.form-box {
|
||||||
|
padding: 0px !important;
|
||||||
|
background-color: tomato;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-box>div {
|
||||||
|
padding: 0rem 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 880px) {
|
||||||
|
.toggle-box {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-l {
|
||||||
|
width: 480px !important;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-box {
|
||||||
|
right: auto !important;
|
||||||
|
left: 50% !important;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-box>div {
|
||||||
|
padding: 0rem 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 550px) {
|
||||||
|
.container-l {
|
||||||
|
width: 400px !important;
|
||||||
|
}
|
||||||
|
.form-box>div {
|
||||||
|
padding: 0rem 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 440px) {
|
||||||
|
.container-l {
|
||||||
|
width: 300px !important;
|
||||||
|
}
|
||||||
|
.form-box>div {
|
||||||
|
padding: 0rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* @media screen and (max-width: 900px) {
|
||||||
|
.auth-ui-con .container-l {
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .form-box {
|
||||||
|
top: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l.active .form-box {
|
||||||
|
right: 0;
|
||||||
|
bottom: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-box::before {
|
||||||
|
left: 0;
|
||||||
|
top: -270%;
|
||||||
|
width: 100%;
|
||||||
|
height: 300%;
|
||||||
|
border-radius: 20vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l.active .toggle-box::before {
|
||||||
|
left: 0;
|
||||||
|
top: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l.active .toggle-panel.toggle-left {
|
||||||
|
left: 0;
|
||||||
|
top: -30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-panel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-panel.toggle-left {
|
||||||
|
display: none !important;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-panel.toggle-right {
|
||||||
|
display: none !important;
|
||||||
|
right: 0;
|
||||||
|
bottom: -30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .container-l.active .toggle-panel.toggle-right {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-box::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
} */
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
.form-box {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-ui-con .toggle-panel h1 {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
interface svgParamTypes {
|
||||||
|
size: {
|
||||||
|
x: string,
|
||||||
|
y: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GoogleSvg({size}: svgParamTypes) {
|
||||||
|
return (
|
||||||
|
<svg width={size.x} height={size.y} viewBox="-3 0 262 262" xmlns="http:/www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" fill="#4285F4" /><path d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" fill="#34A853" /><path d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782" fill="#FBBC05" /><path d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" fill="#EB4335" /></svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SalesforceSvg({size}: svgParamTypes) {
|
||||||
|
return (
|
||||||
|
<svg width={size.x} height={size.y} viewBox="0 -38 256 256" version="1.1" xmlns="http:/www.w3.org/2000/svg" xmlns:xlink="http:/www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||||
|
<g>
|
||||||
|
<path d="M106.553203,159.610976 C114.801129,168.204994 126.284107,173.534815 138.983873,173.534815 C155.865892,173.534815 170.594851,164.121105 178.438396,150.146262 C185.254593,153.191874 192.799405,154.885905 200.737669,154.885905 C231.186506,154.885905 255.871995,129.985475 255.871995,99.2706935 C255.871995,68.5522694 231.186506,43.651839 200.737669,43.651839 C197.021731,43.651839 193.389583,44.0234329 189.877657,44.7338328 C182.970383,32.4129469 169.807947,24.0885163 154.700107,24.0885163 C148.375726,24.0885163 142.393793,25.5493901 137.067615,28.1469039 C130.065621,11.6765534 113.751923,0.127999726 94.7387049,0.127999726 C74.9385822,0.127999726 58.0638501,12.6565411 51.5864595,30.2271008 C48.7557888,29.6259931 45.8231119,29.3126885 42.8139304,29.3126885 C19.23958,29.3126885 0.127998772,48.6209959 0.127998772,72.4430755 C0.127998772,88.4070383 8.71473094,102.34545 21.4727861,109.802829 C18.8461277,115.846693 17.3852539,122.517167 17.3852539,129.53009 C17.3852539,156.926028 39.6262381,179.134225 67.0586069,179.134225 C83.1646497,179.134225 97.4782987,171.476477 106.553203,159.610976" fill="#00A1E0" transform="translate(127.999997, 89.631112) scale(1, -1) translate(-127.999997, -89.631112) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M37.1700744,75.3309413 C37.009779,74.9119875 37.2283636,74.8245536 37.2793667,74.7516921 C37.7602528,74.4019567 38.2484251,74.1505844 38.7402405,73.8700675 C41.3486835,72.4856983 43.8114035,72.0813168 46.3870588,72.0813168 C51.6330895,72.0813168 54.8900002,74.8719136 54.8900002,79.3638274 L54.8900002,79.4512613 C54.8900002,83.6043689 51.2141357,85.1126027 47.7641419,86.2018827 L47.3160434,86.3476058 C44.7148866,87.1927996 42.4707512,87.921415 42.4707512,89.6336611 L42.4707512,89.724738 C42.4707512,91.1892549 43.7822589,92.2676057 45.8150958,92.2676057 C48.0738034,92.2676057 50.755108,91.5171318 52.4819264,90.5626457 C52.4819264,90.5626457 52.9883141,90.2347688 53.174111,90.7265842 C53.2761172,90.9888857 54.1504556,93.3423133 54.2415326,93.5973287 C54.3398956,93.8742026 54.1650279,94.0782149 53.9865172,94.1875072 C52.0156126,95.3860795 49.2905911,96.2057718 46.4708496,96.2057718 L45.9462465,96.2021287 C41.1446712,96.2021287 37.7930405,93.3022395 37.7930405,89.1454888 L37.7930405,89.058055 C37.7930405,84.6754335 41.4907635,83.2546335 44.9553296,82.2637166 L45.5127204,82.092492 C48.0373727,81.3165166 50.2122895,80.6498336 50.2122895,78.872012 L50.2122895,78.7845782 C50.2122895,77.1597659 48.7987757,75.9502644 46.5182096,75.9502644 C45.6329419,75.9502644 42.8095574,75.9684798 39.760302,77.8956674 C39.3923513,78.110609 39.1774097,78.2672613 38.8932497,78.4384859 C38.7438836,78.5332059 38.3686467,78.6971444 38.2047082,78.2016859 L37.1700744,75.3309413 L37.1700744,75.3309413 Z" fill="#FFFFFF" transform="translate(46.001138, 84.143544) scale(1, -1) translate(-46.001138, -84.143544) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M113.969779,75.3309413 C113.809484,74.9119875 114.028068,74.8245536 114.079072,74.7516921 C114.559958,74.4019567 115.04813,74.1505844 115.539945,73.8700675 C118.148388,72.4856983 120.611108,72.0813168 123.186764,72.0813168 C128.432794,72.0813168 131.689705,74.8719136 131.689705,79.3638274 L131.689705,79.4512613 C131.689705,83.6043689 128.013841,85.1126027 124.563847,86.2018827 L124.115748,86.3476058 C121.514591,87.1927996 119.270456,87.921415 119.270456,89.6336611 L119.270456,89.724738 C119.270456,91.1892549 120.581964,92.2676057 122.614801,92.2676057 C124.873508,92.2676057 127.554813,91.5171318 129.281631,90.5626457 C129.281631,90.5626457 129.788019,90.2347688 129.973816,90.7265842 C130.075822,90.9888857 130.95016,93.3423133 131.041237,93.5973287 C131.1396,93.8742026 130.964733,94.0782149 130.786222,94.1875072 C128.815317,95.3860795 126.090296,96.2057718 123.270554,96.2057718 L122.745951,96.2021287 C117.944376,96.2021287 114.592745,93.3022395 114.592745,89.1454888 L114.592745,89.058055 C114.592745,84.6754335 118.290468,83.2546335 121.755034,82.2637166 L122.312425,82.092492 C124.837077,81.3165166 127.015637,80.6498336 127.015637,78.872012 L127.015637,78.7845782 C127.015637,77.1597659 125.598481,75.9502644 123.317914,75.9502644 C122.432647,75.9502644 119.609262,75.9684798 116.560007,77.8956674 C116.192056,78.110609 115.973471,78.2599751 115.696598,78.4384859 C115.601878,78.5004182 115.157422,78.6716428 115.004413,78.2016859 L113.969779,75.3309413 L113.969779,75.3309413 Z" fill="#FFFFFF" transform="translate(122.800843, 84.143544) scale(1, -1) translate(-122.800843, -84.143544) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M166.398769,84.1136744 C166.398769,81.5744499 165.925169,79.5744007 164.992541,78.1608868 C164.070843,76.7619453 162.675544,76.08069 160.730141,76.08069 C158.781095,76.08069 157.393083,76.7583022 156.485956,78.1608868 C155.567901,79.5707576 155.101587,81.5744499 155.101587,84.1136744 C155.101587,86.6492559 155.567901,88.6456621 156.485956,90.0446036 C157.393083,91.4289728 158.781095,92.102942 160.730141,92.102942 C162.675544,92.102942 164.070843,91.4289728 164.996184,90.0446036 C165.925169,88.6456621 166.398769,86.6492559 166.398769,84.1136744 M170.777747,88.8205297 C170.347864,90.2741174 169.677538,91.5564805 168.784984,92.623902 C167.89243,93.6949666 166.763076,94.5547327 165.422424,95.181342 C164.085415,95.8043081 162.504319,96.1212558 160.730141,96.1212558 C158.95232,96.1212558 157.371224,95.8043081 156.034215,95.181342 C154.693563,94.5547327 153.564209,93.6949666 152.668012,92.623902 C151.779101,91.5528374 151.108775,90.2704743 150.675249,88.8205297 C150.249009,87.3742282 150.034067,85.7931329 150.034067,84.1136744 C150.034067,82.434216 150.249009,80.8494776 150.675249,79.4068191 C151.108775,77.9568745 151.775458,76.6745115 152.671655,75.6034469 C153.564209,74.5323823 154.697206,73.6762592 156.034215,73.0678654 C157.374867,72.4594715 158.95232,72.14981 160.730141,72.14981 C162.504319,72.14981 164.081772,72.4594715 165.422424,73.0678654 C166.759433,73.6762592 167.89243,74.5323823 168.784984,75.6034469 C169.677538,76.6708684 170.347864,77.9532315 170.777747,79.4068191 C171.20763,80.8531206 171.422572,82.4378591 171.422572,84.1136744 C171.422572,85.7894898 171.20763,87.3742282 170.777747,88.8205297" fill="#FFFFFF" transform="translate(160.728320, 84.135533) scale(1, -1) translate(-160.728320, -84.135533) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M206.737482,76.7251427 C206.591759,77.1513827 206.180091,76.9910873 206.180091,76.9910873 C205.542553,76.7470012 204.864941,76.5211304 204.143612,76.408195 C203.411353,76.2952596 202.606233,76.2369704 201.742824,76.2369704 C199.622553,76.2369704 197.939452,76.8672227 196.733593,78.113155 C195.524092,79.3590873 194.846479,81.3737088 194.853766,84.0987303 C194.861052,86.5796656 195.458516,88.444921 196.533224,89.8657209 C197.600645,91.2792348 199.225458,92.0042071 201.393088,92.0042071 C203.200055,92.0042071 204.577138,91.7965517 206.019796,91.3411671 C206.019796,91.3411671 206.365888,91.1918009 206.529827,91.6435424 C206.91235,92.7073209 207.19651,93.468724 207.604535,94.6381516 C207.721113,94.9696716 207.436953,95.1117516 207.334947,95.1518255 C206.766627,95.3740532 205.425975,95.7347178 204.413199,95.887727 C203.465999,96.0334501 202.358504,96.1099547 201.127144,96.1099547 C199.28739,96.1099547 197.648005,95.7966501 196.245421,95.1700409 C194.846479,94.5470747 193.658836,93.6873086 192.718922,92.616244 C191.779009,91.5451794 191.064966,90.2628163 190.587723,88.8128717 C190.114123,87.3665702 189.873679,85.7781887 189.873679,84.0987303 C189.873679,80.4665826 190.853667,77.5302627 192.788141,75.3808473 C194.726258,73.2241458 197.637076,72.1275797 201.433162,72.1275797 C203.677298,72.1275797 205.979722,72.5829643 207.633679,73.2350751 C207.633679,73.2350751 207.950627,73.3880843 207.81219,73.756035 L206.737482,76.7251427 L206.737482,76.7251427 Z" fill="#FFFFFF" transform="translate(198.860433, 84.118767) scale(1, -1) translate(-198.860433, -84.118767) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M214.399204,86.5520198 C214.606859,87.9618906 214.996668,89.1349613 215.597776,90.0493736 C216.504902,91.4373859 217.889271,92.198789 219.834675,92.198789 C221.780078,92.198789 223.066084,91.4337428 223.987782,90.0493736 C224.599819,89.1349613 224.865764,87.9108875 224.971413,86.5520198 L214.399204,86.5520198 L214.399204,86.5520198 Z M229.142736,89.6522783 C228.771142,91.0548629 227.849444,92.4720198 227.244693,93.1204874 C226.290207,94.1478351 225.357579,94.8655213 224.432238,95.2662597 C223.222736,95.7835766 221.772791,96.1260258 220.18441,96.1260258 C218.333727,96.1260258 216.654268,95.8163643 215.291758,95.1751828 C213.925604,94.5340013 212.778035,93.6596628 211.878195,92.5703828 C210.978355,91.4847459 210.300742,90.1914536 209.870859,88.7232937 C209.437333,87.2624198 209.218749,85.6703952 209.218749,83.9909368 C209.218749,82.2823338 209.444619,80.6903092 209.892718,79.25858 C210.344459,77.8159215 211.065789,76.5444877 212.042133,75.4916385 C213.014835,74.4315031 214.268053,73.6008816 215.769001,73.0216324 C217.259019,72.4460262 219.069628,72.1472939 221.149825,72.150937 C225.430441,72.1655093 227.685505,73.1199955 228.61449,73.6336693 C228.778428,73.7247462 228.935081,73.8850416 228.738354,74.3440693 L227.769296,77.0581615 C227.623573,77.4625431 227.211905,77.3131769 227.211905,77.3131769 C226.15177,76.9197246 224.643536,76.2129677 221.127967,76.2202539 C218.829185,76.2238969 217.124225,76.9015092 216.056804,77.9616446 C214.960238,79.0472815 214.424705,80.6429492 214.329985,82.8943707 L229.153665,82.8797984 C229.153665,82.8797984 229.543474,82.8870845 229.583548,83.2659645 C229.598121,83.4262599 230.093579,86.3115768 229.142736,89.6522783 L229.142736,89.6522783 Z" fill="#FFFFFF" transform="translate(219.472103, 84.138465) scale(1, -1) translate(-219.472103, -84.138465) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M95.6829901,86.5520198 C95.8942885,87.9618906 96.2804547,89.1349613 96.8815624,90.0493736 C97.7886885,91.4373859 99.1730577,92.198789 101.118461,92.198789 C103.063864,92.198789 104.34987,91.4337428 105.275211,90.0493736 C105.883605,89.1349613 106.14955,87.9108875 106.255199,86.5520198 L95.6829901,86.5520198 L95.6829901,86.5520198 Z M110.422879,89.6522783 C110.051285,91.0548629 109.13323,92.4720198 108.528479,93.1204874 C107.573993,94.1478351 106.641365,94.8655213 105.716024,95.2662597 C104.506522,95.7835766 103.056578,96.1260258 101.468196,96.1260258 C99.6211562,96.1260258 97.9380547,95.8163643 96.5755439,95.1751828 C95.2093901,94.5340013 94.0618209,93.6596628 93.1619809,92.5703828 C92.2621409,91.4847459 91.5845286,90.1914536 91.1546455,88.7232937 C90.7247625,87.2624198 90.5025348,85.6703952 90.5025348,83.9909368 C90.5025348,82.2823338 90.7284055,80.6903092 91.176504,79.25858 C91.6282455,77.8159215 92.3495747,76.5444877 93.3259193,75.4916385 C94.2986209,74.4315031 95.5518393,73.6008816 97.052787,73.0216324 C98.5428054,72.4460262 100.353415,72.1472939 102.433612,72.150937 C106.714227,72.1655093 108.969291,73.1199955 109.898276,73.6336693 C110.062214,73.7247462 110.218867,73.8850416 110.022141,74.3440693 L109.056725,77.0581615 C108.907359,77.4625431 108.495691,77.3131769 108.495691,77.3131769 C107.435556,76.9197246 105.930965,76.2129677 102.40811,76.2202539 C100.112972,76.2238969 98.4080116,76.9015092 97.3405901,77.9616446 C96.2440239,79.0472815 95.7084916,80.6429492 95.6137716,82.8943707 L110.437451,82.8797984 C110.437451,82.8797984 110.827261,82.8870845 110.867334,83.2659645 C110.881907,83.4262599 111.377365,86.3115768 110.422879,89.6522783 L110.422879,89.6522783 Z" fill="#FFFFFF" transform="translate(100.755643, 84.138465) scale(1, -1) translate(-100.755643, -84.138465) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M63.6417638,76.8187739 C63.0625146,77.2814447 62.9823669,77.3980231 62.7856408,77.6967554 C62.4941946,78.15214 62.3448285,78.8006077 62.3448285,79.6239431 C62.3448285,80.9281646 62.7747115,81.8644354 63.6672654,82.4946877 C63.6563361,82.4910446 64.9423423,83.6058261 67.9660961,83.5657523 C70.0900099,83.5366076 71.9880529,83.223303 71.9880529,83.223303 L71.9880529,76.4836108 L71.991696,76.4836108 C71.991696,76.4836108 70.1082253,76.0792293 67.9879545,75.9517216 C64.9714869,75.7695678 63.6308346,76.822417 63.6417638,76.8187739 M69.5399053,87.2343307 C68.9387976,87.2780476 68.1591792,87.3035491 67.2265515,87.3035491 C65.9551176,87.3035491 64.7274007,87.1432537 63.5761885,86.8335922 C62.41769,86.5239307 61.37577,86.0394014 60.4795731,85.3982199 C59.5797331,84.7533953 58.8547608,83.9300599 58.3301578,82.9537153 C57.8055547,81.9773707 57.5396101,80.8261584 57.5396101,79.5365092 C57.5396101,78.2250016 57.7654809,77.0847185 58.2172224,76.1520908 C58.6689639,75.2158201 59.3210747,74.4362016 60.1516962,73.8350939 C60.9750316,73.2339863 61.99145,72.793174 63.1718069,72.5272294 C64.3339484,72.2612847 65.6527423,72.1264909 67.0954007,72.1264909 C68.6145638,72.1264909 70.1300837,72.2503555 71.5982437,72.5017278 C73.0518314,72.749457 74.836939,73.1101217 75.3323975,73.223057 C75.8242129,73.3396355 76.3706744,73.4890016 76.3706744,73.4890016 C76.7386252,73.5800786 76.7094806,73.9735309 76.7094806,73.9735309 L76.7021944,87.5294199 C76.7021944,90.5021706 75.9080036,92.7062321 74.3451237,94.0723859 C72.7895298,95.4348967 70.4980345,96.1234382 67.536213,96.1234382 C66.4250746,96.1234382 64.6363238,95.970429 63.5652592,95.7554874 C63.5652592,95.7554874 60.3265639,95.1288782 58.9931978,94.0869582 C58.9931978,94.0869582 58.7017516,93.9048044 58.862047,93.4967798 L59.9112531,90.6770383 C60.0424039,90.3127306 60.3957824,90.4365952 60.3957824,90.4365952 C60.3957824,90.4365952 60.5087177,90.4803121 60.6398685,90.5568168 C63.4923977,92.1087675 67.0990438,92.0614075 67.0990438,92.0614075 C68.7019976,92.0614075 69.9333576,91.7408167 70.7639791,91.1032783 C71.5727422,90.4839552 71.9844099,89.5476845 71.9844099,87.5731368 L71.9844099,86.9465276 C70.709333,87.1286814 69.5399053,87.2343307 69.5399053,87.2343307" fill="#FFFFFF" transform="translate(67.124818, 84.124965) scale(1, -1) translate(-67.124818, -84.124965) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M189.099133,94.6589745 C189.212069,94.9941376 188.975269,95.1544329 188.876906,95.1908637 C188.625533,95.2892268 187.365029,95.5551714 186.392327,95.6171037 C184.530715,95.7300391 183.496081,95.4167345 182.57074,95.0014237 C181.652684,94.586113 180.632623,93.9157868 180.064303,93.1543837 L180.064303,94.9577068 C180.064303,95.2090791 179.885792,95.4094483 179.638063,95.4094483 L175.838334,95.4094483 C175.590604,95.4094483 175.412094,95.2090791 175.412094,94.9577068 L175.412094,72.8478733 C175.412094,72.6001441 175.616106,72.3961318 175.863835,72.3961318 L179.758284,72.3961318 C180.006014,72.3961318 180.206383,72.6001441 180.206383,72.8478733 L180.206383,83.8936824 C180.206383,85.3764146 180.370321,86.8555038 180.698198,87.7844884 C181.018789,88.7025438 181.455958,89.4384453 181.995134,89.9666915 C182.537952,90.4912946 183.153632,90.8592453 183.827601,91.0669007 C184.516143,91.2781992 185.277546,91.3474176 185.816721,91.3474176 C186.592697,91.3474176 187.445177,91.1470484 187.445177,91.1470484 C187.729337,91.1142607 187.889632,91.2891284 187.984352,91.5477868 C188.239367,92.2253991 188.960697,94.254593 189.099133,94.6589745" fill="#FFFFFF" transform="translate(182.270544, 84.018248) scale(1, -1) translate(-182.270544, -84.018248) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M152.543767,105.761574 C152.070167,105.907297 151.640284,106.00566 151.07925,106.111309 C150.51093,106.213316 149.833318,106.264319 149.064629,106.264319 C146.383324,106.264319 144.270339,105.506559 142.787607,104.012897 C141.312161,102.526522 140.310315,100.264171 139.80757,97.2877773 L139.625416,96.2859312 L136.259213,96.2859312 C136.259213,96.2859312 135.851189,96.3005035 135.763755,95.8560481 L135.21365,92.770362 C135.173577,92.4789159 135.301084,92.2931189 135.694537,92.2931189 L138.969663,92.2931189 L135.647177,73.7425715 C135.388518,72.24891 135.089786,71.0211931 134.758266,70.0885655 C134.434032,69.1705101 134.117084,68.4819686 133.723632,67.9792239 C133.344752,67.4983378 132.98773,67.1413163 132.368407,66.9336609 C131.858377,66.7624363 131.268198,66.6822886 130.623374,66.6822886 C130.266352,66.6822886 129.789109,66.7405778 129.43573,66.8134393 C129.085995,66.8826578 128.900198,66.9591624 128.634254,67.0720978 C128.634254,67.0720978 128.251731,67.2178209 128.098721,66.8352978 C127.9785,66.5183501 127.104161,64.1175625 126.998512,63.8224732 C126.896506,63.527384 127.042229,63.2978702 127.228026,63.2286517 C127.665195,63.0756425 127.989429,62.9736363 128.583251,62.8315563 C129.406586,62.6384733 130.102414,62.627544 130.754524,62.627544 C132.117035,62.627544 133.362967,62.8206271 134.393958,63.1922209 C135.428592,63.5674579 136.332075,64.2195686 137.133552,65.1011932 C137.996961,66.0556794 138.53978,67.0538824 139.057096,68.4200362 C139.57077,69.7679747 140.011583,71.44379 140.361318,73.3964792 L143.702019,92.2931189 L148.583742,92.2931189 C148.583742,92.2931189 148.99541,92.2785466 149.079201,92.7266451 L149.632949,95.8086881 C149.669379,96.1037773 149.545515,96.2859312 149.148419,96.2859312 L144.408776,96.2859312 C144.434278,96.3915804 144.649219,98.0601096 145.192038,99.6302757 C145.425195,100.296959 145.862364,100.839777 146.230315,101.211371 C146.594623,101.575679 147.013576,101.834337 147.472604,101.983703 C147.942561,102.136713 148.478093,102.209574 149.064629,102.209574 C149.509084,102.209574 149.949896,102.158571 150.281416,102.089353 C150.740444,101.99099 150.918955,101.939986 151.039176,101.903556 C151.523706,101.757833 151.589281,101.899913 151.684001,102.13307 L152.816998,105.244257 C152.933576,105.57942 152.645773,105.7215 152.543767,105.761574" fill="#FFFFFF" transform="translate(139.904621, 84.445931) scale(1, -1) translate(-139.904621, -84.445931) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
<path d="M86.3217408,63.553237 C86.3217408,63.3055078 86.14323,63.1051385 85.8955008,63.1051385 L81.9646209,63.1051385 C81.7168916,63.1051385 81.542024,63.3055078 81.542024,63.553237 L81.542024,95.1897165 C81.542024,95.4374457 81.7168916,95.6378149 81.9646209,95.6378149 L85.8955008,95.6378149 C86.14323,95.6378149 86.3217408,95.4374457 86.3217408,95.1897165 L86.3217408,63.553237 L86.3217408,63.553237 Z" fill="#FFFFFF" transform="translate(83.931882, 79.371477) scale(1, -1) translate(-83.931882, -79.371477) ">
|
||||||
|
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
interface TransactionTopNavProps {
|
||||||
|
filterType: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterContent = {
|
||||||
|
"all": "All Transactions",
|
||||||
|
"draft": "Transactions in Draft",
|
||||||
|
"sent": "Sent Transactions",
|
||||||
|
"complete": "Completed Transactions",
|
||||||
|
"declined": "Declined Transactions",
|
||||||
|
"discarded": "Discarded Transactions",
|
||||||
|
"canceled": "Canceled Transactions",
|
||||||
|
"expired": "Expired Transactions"
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransactionTopNav: React.FC<TransactionTopNavProps> = ({ filterType }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`px-3 py-1 mb-2 text-md bg-[var(--compBgCol)] rounded flex justify-between items-center`}>
|
||||||
|
<h1 className='h-[34px] flex items-center max-w-max'>{filterContent[filterType as string]}</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TransactionTopNav
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { AiFillSignature } from 'react-icons/ai';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { LuGitGraph } from 'react-icons/lu';
|
||||||
|
import AccountNav from '../accounts/AccountNav';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import api from '../../config/api';
|
||||||
|
|
||||||
|
// Sample recipient data — you can replace this with actual API data later
|
||||||
|
const recipients = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Shanu.R",
|
||||||
|
email: "shanu.r@example.com",
|
||||||
|
signedAt: "5/1/2025 | 04:24:35 pm",
|
||||||
|
status: "Signed",
|
||||||
|
viewLink: "#"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Amit B",
|
||||||
|
email: "amit.b@example.com",
|
||||||
|
signedAt: "5/1/2025 | 04:24:35 pm",
|
||||||
|
status: "Signed",
|
||||||
|
viewLink: "#"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const TransactionView = () => {
|
||||||
|
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
|
||||||
|
|
||||||
|
if (!accountId || !transactionId) {
|
||||||
|
window.location.href = "/";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [transactionDetails, setTransactionDetails] = useState(null);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const [res1, res2] = await Promise.all([api.get(`/api/accounts/${accountId}/transactions/${transactionId}`),
|
||||||
|
api.get(`/api/accounts/${accountId}/transactions/${transactionId}/recipients`)])
|
||||||
|
setTransactionDetails({ transaction: res1.data, recipients: res2.data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error Gettign Transaction and its Recipient: ", error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData()
|
||||||
|
}, [accountId, transactionId])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{transactionDetails &&
|
||||||
|
<>
|
||||||
|
<div className='mt-2 flex justify-center items-stretch w-full h-full bg-white rounded-sm'>
|
||||||
|
<div className="w-full px-6 py-4 rounded-tl-sm text-xs border-r-2 border-gray-200 overflow-y-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<h1 className='text-lg font-semibold text-gray-800'>
|
||||||
|
Complete with Forms Made Easy: <span className='text-slate-600'>{transactionDetails.transaction.name ? transactionDetails.transaction.name : "Untitled"}</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className='mt-2'>
|
||||||
|
<p className='text-slate-600'>From <span className='text-blue-600'>Shanu.R</span></p>
|
||||||
|
<p className='text-slate-600'>Last Change on <span>12/02/25</span></p>
|
||||||
|
<p className='text-slate-600'>Sent on <span>11/02/25</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p>
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className='my-2 flex gap-2'>
|
||||||
|
{/* <button
|
||||||
|
type="button"
|
||||||
|
aria-label="Sign the document"
|
||||||
|
className='cursor-pointer border border-gray-300 rounded-sm px-4 py-1 bg-[var(--primary)] text-white'
|
||||||
|
>
|
||||||
|
Sign
|
||||||
|
</button> */}
|
||||||
|
<button className='cursor-pointer border border-gray-300 rounded-sm px-4 py-1 hover:bg-gray-100'>
|
||||||
|
More
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className='my-6 border-slate-200' />
|
||||||
|
|
||||||
|
{/* Recipients Section */}
|
||||||
|
<div>
|
||||||
|
<div className='flex justify-between items-center text-md'>
|
||||||
|
<h2 className='text-lg font-semibold text-gray-800 cursor-pointer'>Signers</h2>
|
||||||
|
<p className='font-bold flex items-center gap-1.5 text-slate-800 cursor-pointer'>
|
||||||
|
<LuGitGraph className='text-[0.9rem] text-[var(--primary)]' />
|
||||||
|
Signing Graph
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Recipients List */}
|
||||||
|
{transactionDetails.recipients.map((recipient, index) => (
|
||||||
|
<div key={index}>
|
||||||
|
<div className='mt-2 flex justify-between items-center'>
|
||||||
|
<div className='flex gap-8 items-center'>
|
||||||
|
<p className='text-slate-600'>{recipient.id}</p>
|
||||||
|
<div>
|
||||||
|
<p className='text-slate-600 font-semibold'>{recipient.name}</p>
|
||||||
|
<p className='text-slate-600'>{recipient.email}</p>
|
||||||
|
{recipient.hostEmail !== null && <p className='text-xs'><i><b>Host: {recipient.hostEmail}</b></i></p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex flex-col leading-4.5 items-start text-md text-slate-600 pr-18'>
|
||||||
|
<div className='flex gap-1 items-center relative'>
|
||||||
|
<AiFillSignature className='absolute left-[-30px] text-[1rem]' />
|
||||||
|
<p className='text-black font-semibold'>{recipient.status}</p>
|
||||||
|
</div>
|
||||||
|
{recipient.status !== "PENDING" && <p>on {recipient.completedAt}</p>}
|
||||||
|
{/* <a href={recipient.viewLink} className='text-blue-600 hover:underline'>
|
||||||
|
View signed location
|
||||||
|
</a> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className='my-2 border-slate-200' />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2>
|
||||||
|
<p className='mt-2 text-slate-600'>No Message Found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='w-[210px] text-xs rounded-sm '>
|
||||||
|
|
||||||
|
<div className='p-4 flex flex-col gap-2'>
|
||||||
|
<p>abc.pdf</p>
|
||||||
|
<div className='border-3 rounded-sm border-slate-300 relative text-xs'>
|
||||||
|
<img style={{ width: "200px", height: "160px" }} src="https://images.unsplash.com/photo-1746469535771-71a672e8719f?q=80&w=870&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" alt="" />
|
||||||
|
<div className='bg-slate-100 p-2 absolute w-full bottom-[0px]'>
|
||||||
|
<p className='text-[0.7rem] text-slate-500'>1 of 10</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransactionView;
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import TransactionTopNav from './TransactionTopNav';
|
||||||
|
import api from '../../config/api';
|
||||||
|
|
||||||
|
interface Document {
|
||||||
|
documentId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Recipient {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
type: string;
|
||||||
|
status: string;
|
||||||
|
viewedDateTime?: string;
|
||||||
|
signedDateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Transaction {
|
||||||
|
transactionId: string;
|
||||||
|
name: string;
|
||||||
|
createAt: string;
|
||||||
|
lastModifiedAt: string;
|
||||||
|
status: string;
|
||||||
|
signingOrder: boolean;
|
||||||
|
documents: Document[];
|
||||||
|
recipients: Recipient[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionResponse {
|
||||||
|
content: Transaction[];
|
||||||
|
pageNumber: number;
|
||||||
|
totalPages: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Transactions = () => {
|
||||||
|
const { filterParam } = useParams();
|
||||||
|
const [transactionData, setTransactionData] = useState<Transaction[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.get<TransactionResponse>(
|
||||||
|
`api/transaction?transactionStatus=${filterParam === "all" ? "" : filterParam?.toUpperCase()}`
|
||||||
|
)
|
||||||
|
.then(res => setTransactionData(res.data.content))
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
}, [filterParam]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{filterParam && <TransactionTopNav filterType={filterParam} />}
|
||||||
|
{/* <h2 className="text-cl font-semibold text-gray-800 mb-2.5">Transactions</h2> */}
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
transactionData.length > 0 ?
|
||||||
|
<div className="overflow-x-auto bg-white shadow-md rounded-lg border border-gray-200">
|
||||||
|
<table className="min-w-full text-sm text-gray-700">
|
||||||
|
<thead className="bg-gray-100 text-left text-xs uppercase tracking-wider text-gray-600">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3">Name</th>
|
||||||
|
<th className="px-6 py-3">Created At</th>
|
||||||
|
<th className="px-6 py-3">Status</th>
|
||||||
|
<th className="px-6 py-3">Documents</th>
|
||||||
|
<th className="px-6 py-3">Recipients</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{transactionData.map((tx) => (
|
||||||
|
<tr key={tx.transactionId} className="border-t hover:bg-gray-50 transition duration-150">
|
||||||
|
<td className="px-6 py-4 font-medium text-gray-800">
|
||||||
|
{tx.name?.trim() || 'N/A'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
{new Date(tx.createAt).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<span className="inline-block px-2 py-1 text-xs font-semibold rounded bg-blue-100 text-blue-800">
|
||||||
|
{tx.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<ul className="list-disc list-inside space-y-1">
|
||||||
|
{tx.documents.map((doc) => (
|
||||||
|
<li key={doc.documentId}>{doc.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 space-y-2">
|
||||||
|
{tx.recipients.map((rec) => (
|
||||||
|
<div key={rec.id} className="text-sm">
|
||||||
|
<div className="font-semibold">
|
||||||
|
{rec.firstName} {rec.lastName}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-600">Status: {rec.status}</div>
|
||||||
|
{rec.signedDateTime && (
|
||||||
|
<div className="text-green-600">
|
||||||
|
Signed: {new Date(rec.signedDateTime).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div> :
|
||||||
|
<div className='flex justify-center items-center mt-6 text-slate-400 gap-2'>
|
||||||
|
<hr className='w-full border-dashed' />
|
||||||
|
<p className='whitespace-nowrap text-sm'>No Data Found</p>
|
||||||
|
<hr className='w-full border-dashed' />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Transactions;
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
import { FaSignature } from 'react-icons/fa'
|
||||||
|
import { IoIosDocument } from 'react-icons/io';
|
||||||
|
import { LuTableOfContents } from 'react-icons/lu';
|
||||||
|
import { MdKeyboardArrowDown } from 'react-icons/md'
|
||||||
|
import { RiResetLeftLine } from 'react-icons/ri';
|
||||||
|
import { FaCheck } from "react-icons/fa6";
|
||||||
|
|
||||||
|
|
||||||
|
interface FIT {
|
||||||
|
label: string,
|
||||||
|
value: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fitObj = [
|
||||||
|
{
|
||||||
|
label: "Fit",
|
||||||
|
value: "FIT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "50%",
|
||||||
|
value: "50"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "100%",
|
||||||
|
value: "100"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "150%",
|
||||||
|
value: "150"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const DocView = () => {
|
||||||
|
const [nav2RecToggle, setNav2RecToggle] = useState<boolean>(false);
|
||||||
|
const [nav2DocToggle, setNav2DocToggle] = useState<boolean>(false);
|
||||||
|
const [isFields, setIsFields] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [isFitDd, setIsFitDd] = useState<boolean>(false)
|
||||||
|
const [selFitIndx, setSelFitIndx] = useState<number>(0);
|
||||||
|
|
||||||
|
function zoom(percentage: number, contentClass: string) {
|
||||||
|
|
||||||
|
console.log("calling zoom function", percentage, contentClass);
|
||||||
|
|
||||||
|
const content = document.querySelector(`.${contentClass}`);
|
||||||
|
const scale = percentage // 100;
|
||||||
|
if(content) {
|
||||||
|
console.log("calling zoom function Printing Content", content);
|
||||||
|
content.style.transform = `scale(${scale})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Calling useEffect for zoom", fitObj[selFitIndx].value);
|
||||||
|
let indx = fitObj[selFitIndx].value
|
||||||
|
if (indx === "FIT") {
|
||||||
|
indx = "100";
|
||||||
|
}
|
||||||
|
zoom(Number(indx), 'zoom-con')
|
||||||
|
}, [selFitIndx])
|
||||||
|
|
||||||
|
console.log(isFitDd);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
{/* Nav Two - Oprions - START */}
|
||||||
|
<div className='flex justify-between items-center px-4 py-1 bg-white'>
|
||||||
|
|
||||||
|
{/* Left Side */}
|
||||||
|
<div className='flex justify-start items-center gap-2'>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => setIsFields(true)}
|
||||||
|
className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${isFields ? 'bg-slate-100' : ''}`}>
|
||||||
|
<LuTableOfContents />
|
||||||
|
<p className='tracking-wide'>Fields</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => setIsFields(false)}
|
||||||
|
className={`flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm ${!isFields ? 'bg-slate-100' : ''}`}>
|
||||||
|
<IoIosDocument />
|
||||||
|
<p className='tracking-wide'>Pages</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||||
|
<RiResetLeftLine />
|
||||||
|
<p className='tracking-wide'>Reset</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
tabIndex={1}
|
||||||
|
onBlur={() => setIsFitDd(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => setIsFitDd(!isFitDd)}
|
||||||
|
className='flex items-center gap-1 cursor-pointer text-xs px-2 py-1 rounded-sm'>
|
||||||
|
<p className='tracking-wide'>Fit</p>
|
||||||
|
<MdKeyboardArrowDown style={{transform: `${isFitDd ? "rotate(180deg)" : ""}`}} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFitDd && <div className='absolute bg-white rounded-sm z-50'>
|
||||||
|
{fitObj.map((val, indx) => {
|
||||||
|
return (<div
|
||||||
|
key={indx}
|
||||||
|
onClick={() => setSelFitIndx(indx)}
|
||||||
|
className='flex z-50 items-center justify-between gap-2 w-[70px] p-2 text-xs cursor-default hover:bg-slate-100'>
|
||||||
|
<div
|
||||||
|
|
||||||
|
className='flex items-center justify-between w-full'>
|
||||||
|
<p className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[12ch]'>{val.label}</p>
|
||||||
|
{selFitIndx === indx && <FaCheck className='text-sm text-slate-400' />}
|
||||||
|
</div>
|
||||||
|
</div>)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/* Nav Two - ENDS */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className='grid grid-cols-12 gap-1 mt-1 h-[calc(100vh-38px)] overflow-hidden'>
|
||||||
|
<div className='col-span-2 bg-white h-full p-4 overflow-y-auto'></div>
|
||||||
|
<div className='zoom-con col-span-8 bg-slate-200 h-full p-4 overflow-y-auto'>
|
||||||
|
Test Zoom
|
||||||
|
</div>
|
||||||
|
<div className='col-span-2 bg-white h-full p-4 overflow-y-auto'></div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocView
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
const DocViewCon = () => {
|
||||||
|
return (
|
||||||
|
<div>DocViewCon</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocViewCon
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
// src/api.js
|
||||||
|
import axios from "axios";
|
||||||
|
import { redirect } from "react-router-dom"; // if using react-router
|
||||||
|
// or use your preferred navigation method
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_BASE_URI,
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"ngrok-skip-browser-warning": "69420",
|
||||||
|
"Access-Control-Allow-Origin": "*"
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flag to prevent infinite loop
|
||||||
|
let isRefreshing = false;
|
||||||
|
|
||||||
|
// Response interceptor
|
||||||
|
api.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
if (window.location.pathname === "/login" || window.location.pathname === "/register") {
|
||||||
|
window.location.href = "/";
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
async error => {
|
||||||
|
const originalRequest = error.config;
|
||||||
|
// Prevent multiple refresh attempts
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
originalRequest._retry = true;
|
||||||
|
try {
|
||||||
|
if (!isRefreshing) {
|
||||||
|
isRefreshing = true;
|
||||||
|
// Attempt to refresh the token
|
||||||
|
const refreshResponse = await api.patch("/api/auth/refresh");
|
||||||
|
if (refreshResponse.status === 200) {
|
||||||
|
isRefreshing = false;
|
||||||
|
// Retry the original request
|
||||||
|
if (window.location.pathname === "/login" || window.location.pathname === "/register") {
|
||||||
|
window.location.href = "/"; // Redirect to login page
|
||||||
|
}
|
||||||
|
return api(originalRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (refreshError) {
|
||||||
|
console.error("Refresh token failed:", refreshError);
|
||||||
|
if (window.location.pathname !== "/login" && window.location.pathname !== "/register") {
|
||||||
|
window.location.href = "/login"; // Redirect to login page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default api;
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const createAxiosInstance = () => {
|
||||||
|
|
||||||
|
const persistedRoot = localStorage.getItem('persist:root');
|
||||||
|
const parsedRoot = JSON.parse(persistedRoot);
|
||||||
|
|
||||||
|
if(!persistedRoot || !("user" in parsedRoot)) {
|
||||||
|
return axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_BASE_URI,
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"ngrok-skip-browser-warning": "69420",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = JSON.parse(parsedRoot.user);
|
||||||
|
const accessToken = user.accessToken;
|
||||||
|
|
||||||
|
console.log(accessToken, "accessToken");
|
||||||
|
|
||||||
|
return axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_BASE_URI,
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
"ngrok-skip-browser-warning": "69420",
|
||||||
|
authorization: accessToken,
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../store/store";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/ Create an Axios instance
|
||||||
|
const useAxios = () => {
|
||||||
|
const user = useSelector((state: RootState) => state.user);
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_BASE_URI,
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
authorization: user.accessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useAxios;
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import api from "../config/api";
|
||||||
|
import { AccountData } from "../utlity/types";
|
||||||
|
|
||||||
|
|
||||||
|
const fetchAccounts = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api("/api/user/accounts");
|
||||||
|
const data: AccountData[] = response.data || [];
|
||||||
|
// setAllAccounts(data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching accounts:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchAccountData = async (accountId: string) => {
|
||||||
|
try {
|
||||||
|
if (!accountId) return null;
|
||||||
|
const response = await api.get(`/api/accounts/${accountId}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching account data:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchTransactionData = async (accountId: string, transactionId: string) => {
|
||||||
|
try {
|
||||||
|
if (!accountId || !transactionId) return null;
|
||||||
|
const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching account data:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
fetchAccountData,
|
||||||
|
fetchTransactionData,
|
||||||
|
fetchAccounts
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { RxText } from 'react-icons/rx';
|
||||||
|
import { AiFillSignature } from 'react-icons/ai';
|
||||||
|
import { TbSignature } from 'react-icons/tb';
|
||||||
|
import { CiCalendarDate } from 'react-icons/ci';
|
||||||
|
import { MdConfirmationNumber, MdRadioButtonChecked } from 'react-icons/md';
|
||||||
|
import { FaRegImage, FaFileAlt, FaStamp } from 'react-icons/fa';
|
||||||
|
import { IoMdCheckboxOutline } from 'react-icons/io';
|
||||||
|
import { RiDropdownList } from 'react-icons/ri';
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ icon: <RxText />, label: 'text' },
|
||||||
|
{ icon: <AiFillSignature />, label: 'signature' },
|
||||||
|
{ icon: <TbSignature />, label: 'initials' },
|
||||||
|
{ icon: <CiCalendarDate />, label: 'temporal' },
|
||||||
|
{ icon: <MdConfirmationNumber />, label: 'number' },
|
||||||
|
{ icon: <FaRegImage />, label: 'image' },
|
||||||
|
{ icon: <FaStamp />, label: 'stamp' },
|
||||||
|
{ icon: <IoMdCheckboxOutline />, label: 'checkbox' },
|
||||||
|
{ icon: <RiDropdownList />, label: 'select' },
|
||||||
|
{ icon: <FaFileAlt />, label: 'file' },
|
||||||
|
{ icon: <MdRadioButtonChecked />, label: 'radio' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default fields;
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
const { floor, random } = Math;
|
||||||
|
|
||||||
|
// Define an RGB color type
|
||||||
|
type RGBColor = {
|
||||||
|
r: number;
|
||||||
|
g: number;
|
||||||
|
b: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to generate a random RGB color
|
||||||
|
const generateRandomRGB = (): RGBColor => ({
|
||||||
|
r: floor(random() * 256),
|
||||||
|
g: floor(random() * 256),
|
||||||
|
b: floor(random() * 256),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to calculate Euclidean distance between two RGB colors
|
||||||
|
const colorDistance = (rgb1: RGBColor, rgb2: RGBColor): number => {
|
||||||
|
return Math.sqrt(
|
||||||
|
Math.pow(rgb1.r - rgb2.r, 2) +
|
||||||
|
Math.pow(rgb1.g - rgb2.g, 2) +
|
||||||
|
Math.pow(rgb1.b - rgb2.b, 2)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define a threshold for color difference (higher = more distinct colors)
|
||||||
|
const MIN_COLOR_DIFFERENCE = 100;
|
||||||
|
|
||||||
|
function generateUniqueColors(count: number): string[] {
|
||||||
|
const generatedColors = new Set<RGBColor>(); // Store unique RGB values
|
||||||
|
const colorArray: string[] = []; // Store final hex color codes
|
||||||
|
|
||||||
|
while (colorArray.length < count) {
|
||||||
|
let newColor: RGBColor;
|
||||||
|
let isTooClose: boolean;
|
||||||
|
|
||||||
|
do {
|
||||||
|
newColor = generateRandomRGB();
|
||||||
|
|
||||||
|
// Check if the new color is too close to existing ones
|
||||||
|
isTooClose = [...generatedColors].some(
|
||||||
|
(existingColor) => colorDistance(newColor, existingColor) < MIN_COLOR_DIFFERENCE
|
||||||
|
);
|
||||||
|
|
||||||
|
} while (isTooClose);
|
||||||
|
|
||||||
|
// Store the new RGB color
|
||||||
|
generatedColors.add(newColor);
|
||||||
|
|
||||||
|
// Convert RGB to Hex and store in array
|
||||||
|
const hexColor = `#${newColor.r.toString(16).padStart(2, '0')}`
|
||||||
|
+ `${newColor.g.toString(16).padStart(2, '0')}`
|
||||||
|
+ `${newColor.b.toString(16).padStart(2, '0')}`.toUpperCase();
|
||||||
|
|
||||||
|
colorArray.push(hexColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return colorArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default generateUniqueColors;
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// iconMap.js
|
||||||
|
import { RxText } from 'react-icons/rx';
|
||||||
|
import { AiFillSignature } from 'react-icons/ai';
|
||||||
|
import { TbSignature } from 'react-icons/tb';
|
||||||
|
import { CiCalendarDate } from 'react-icons/ci';
|
||||||
|
import { MdConfirmationNumber, MdRadioButtonChecked } from 'react-icons/md';
|
||||||
|
import { FaRegImage, FaFileAlt, FaStamp } from 'react-icons/fa';
|
||||||
|
import { IoMdCheckboxOutline } from 'react-icons/io';
|
||||||
|
import { RiDropdownList } from 'react-icons/ri';
|
||||||
|
import { MdEmail, MdBusiness } from 'react-icons/md';
|
||||||
|
import { FaUser } from 'react-icons/fa';
|
||||||
|
|
||||||
|
const typeToIconMap = {
|
||||||
|
TEXT: <RxText />,
|
||||||
|
SIGNATURE: <AiFillSignature />,
|
||||||
|
INITIALS: <TbSignature />,
|
||||||
|
TEMPORAL: <CiCalendarDate />,
|
||||||
|
NUMBER: <MdConfirmationNumber />,
|
||||||
|
IMAGE: <FaRegImage />,
|
||||||
|
STAMP: <FaStamp />,
|
||||||
|
CHECKBOX: <IoMdCheckboxOutline />,
|
||||||
|
SELECT: <RiDropdownList />,
|
||||||
|
FILE: <FaFileAlt />,
|
||||||
|
ATTACHMENT: <FaFileAlt />,
|
||||||
|
RADIO: <MdRadioButtonChecked />,
|
||||||
|
EMAIL: <MdEmail />,
|
||||||
|
NAME: <FaUser />,
|
||||||
|
COMPANY: <MdBusiness />,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default typeToIconMap;
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #41644A;
|
||||||
|
--primaryLight: #ffffff;
|
||||||
|
--primaryLight2: #4b7455;
|
||||||
|
--primaryLight3: #dae9de;
|
||||||
|
--dashBoardBg: #ffffff;
|
||||||
|
--mainViewBg: #edeaeb;
|
||||||
|
--highlightBg1: #edeaeb;
|
||||||
|
--menuTextCol: #98a0a8;
|
||||||
|
--menuTextColActive: #41644A;
|
||||||
|
--menuBgColActive: #edeaeb;
|
||||||
|
--menuTextHoverCol: #e7e7e7;
|
||||||
|
|
||||||
|
--greyTextCol: #98a0a8;
|
||||||
|
--lightGreyCol: #e1e1e1;
|
||||||
|
--compBgCol: #ffffff;
|
||||||
|
|
||||||
|
--avatarBgCol: #41644A;
|
||||||
|
--avatarTextCol: #ffffff;
|
||||||
|
|
||||||
|
|
||||||
|
--profileMenuModalBg: #ffffff;
|
||||||
|
--profileMenuModalTextCol: #98a0a8;
|
||||||
|
--profileMenuModalTextColActive: #2d2d2d;
|
||||||
|
--profileMenuModalBgColActive: #edeaeb;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--sendForSignTextCol: #ffffff;
|
||||||
|
--sendForSignBgCol: #d02f56;
|
||||||
|
|
||||||
|
--sendForSignDropDownText: #000000;
|
||||||
|
--sendForSignDropDownBg: #ffffff;
|
||||||
|
--sendForSignDropDownHover: #e7e7e7;
|
||||||
|
|
||||||
|
|
||||||
|
--loaderBg: #ffffff;
|
||||||
|
|
||||||
|
--modalBg: #ffffff;
|
||||||
|
|
||||||
|
--hideModalBg: #2d2d2d;
|
||||||
|
|
||||||
|
--processStepOneTextHead: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Sortable CSS */
|
||||||
|
.sortable-ghost {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(1.05);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropped-item {
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
touch-action: none;
|
||||||
|
-ms-touch-action: none; /* For older IE/Edge */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes textColorChange {
|
||||||
|
0%, 100% {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
color: #41644A; /* Tailwind blue-500 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.animate-text {
|
||||||
|
animation: textColorChange 2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
// import { PersistGate } from 'redux-persist/integration/react'
|
||||||
|
import { store } from './store/store.ts'
|
||||||
|
import { GoogleOAuthProvider } from '@react-oauth/google'
|
||||||
|
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<GoogleOAuthProvider clientId='735904161492-a74bmqsou7o7nulff8f5amd32mhp00rv.apps.googleusercontent.com'>
|
||||||
|
<Provider store={store}>
|
||||||
|
{/* <PersistGate loading={null} persistor={persistor}> */}
|
||||||
|
<App />
|
||||||
|
{/* </PersistGate> */}
|
||||||
|
</Provider>
|
||||||
|
</ GoogleOAuthProvider>
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
// import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { hideLoader, showLoader } from "./loaderSlice";
|
||||||
|
// import { showModal } from "./modalSlice";
|
||||||
|
// import { updateSelectedDocuments } from "./selectedDocsSlice";
|
||||||
|
|
||||||
|
import api from "../../config/api";
|
||||||
|
import { Bounce, toast } from "react-toastify";
|
||||||
|
// import { createAxiosInstance } from "../../config/axiosInstance";
|
||||||
|
|
||||||
|
// const axiosInstance = createAxiosInstance();
|
||||||
|
|
||||||
|
export interface Document {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
original?: boolean;
|
||||||
|
corrupted?: boolean;
|
||||||
|
isCompleted: boolean;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentState {
|
||||||
|
documents: Document[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: DocumentState = {
|
||||||
|
documents: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface uploadArgs {
|
||||||
|
formData: FormData;
|
||||||
|
accountId: string;
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Fetch documents from API
|
||||||
|
export const fetchDocuments = createAsyncThunk<Document[], { accountId: string, transactionId: string }>(
|
||||||
|
"documents/fetchDocuments",
|
||||||
|
async ({ accountId, transactionId }, { rejectWithValue }) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/documents?include_images=true`);
|
||||||
|
return response.data as Document[];
|
||||||
|
} catch (error: any) {
|
||||||
|
// if(error.status === 401) {}
|
||||||
|
return rejectWithValue(error.response?.data || "Failed to fetch documents");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Upload multiple documents using FormData (Key: "document")
|
||||||
|
export const uploadDocuments = createAsyncThunk<Document[], uploadArgs>(
|
||||||
|
"documents/uploadDocuments",
|
||||||
|
async ({ formData, accountId, transactionId }, { dispatch, rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
dispatch(showLoader("Uploading documents..."));
|
||||||
|
console.log("Form Data", formData.getAll("document"))
|
||||||
|
|
||||||
|
const response = await api.post(`/api/accounts/${accountId}/transactions/${transactionId}/documents`, formData, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Form Uploaded Response DocumentSlice Line 185", response.data)
|
||||||
|
|
||||||
|
dispatch(hideLoader());
|
||||||
|
return [response.data];
|
||||||
|
} catch (error: any) {
|
||||||
|
dispatch(hideLoader());
|
||||||
|
toast.error("Error Uploading Document", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 3000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: false,
|
||||||
|
draggable: false,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
transition: Bounce,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Failed Uploading Documents")
|
||||||
|
throw new Error("Failed Uploading Documents")
|
||||||
|
|
||||||
|
// Mock data in case of failure
|
||||||
|
// const failedDocuments = formData.getAll("document").map((file: any) => ({
|
||||||
|
// id: uuidv4(),
|
||||||
|
// name: file.name,
|
||||||
|
// extension: file.name.split(".").pop() || "unknown",
|
||||||
|
// original: Math.random() < 0.5,
|
||||||
|
// corrupted: Math.random() < 0.5,
|
||||||
|
// }));
|
||||||
|
|
||||||
|
// dispatch(updateSelectedDocuments({ documents: failedDocuments }));
|
||||||
|
// return failedDocuments; // Instead of rejecting, return mock data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const documentSlice = createSlice({
|
||||||
|
name: "documents",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setDocuments: (state, action: PayloadAction<Document[]>) => {
|
||||||
|
state.documents = action.payload;
|
||||||
|
},
|
||||||
|
setDocProgComplete: (state, action: PayloadAction<string>) => {
|
||||||
|
const docIndex = state.documents.findIndex(doc => doc.id === action.payload);
|
||||||
|
if (docIndex !== -1) {
|
||||||
|
state.documents[docIndex].isCompleted = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetDocuments: (state) => {
|
||||||
|
state.documents = [];
|
||||||
|
},
|
||||||
|
deleteDocument: (state, action: PayloadAction<string>) => {
|
||||||
|
state.documents = state.documents.filter(doc => doc.id !== action.payload);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
// Fetch documents
|
||||||
|
.addCase(fetchDocuments.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(fetchDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
|
||||||
|
state.loading = false;
|
||||||
|
console.log(action.payload, "action.payload")
|
||||||
|
state.documents = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(fetchDocuments.rejected, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
})
|
||||||
|
|
||||||
|
// Upload multiple documents
|
||||||
|
.addCase(uploadDocuments.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(uploadDocuments.fulfilled, (state, action: PayloadAction<Document[]>) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.documents = [...action.payload, ...state.documents];
|
||||||
|
})
|
||||||
|
.addCase(uploadDocuments.rejected, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { resetDocuments, setDocuments, setDocProgComplete, deleteDocument } = documentSlice.actions;
|
||||||
|
export default documentSlice.reducer;
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { JSX } from 'react';
|
||||||
|
|
||||||
|
// Define the DroppedItem interface
|
||||||
|
export interface DroppedItem {
|
||||||
|
icon: JSX.Element;
|
||||||
|
label?: string;
|
||||||
|
type: string;
|
||||||
|
subType?: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
signerIndex: number;
|
||||||
|
maskType?: string;
|
||||||
|
maskCharacter?: string;
|
||||||
|
required?: boolean;
|
||||||
|
format?: string;
|
||||||
|
radioGroups?: DroppedItem[];
|
||||||
|
documentIndex: number;
|
||||||
|
pageNumber: number;
|
||||||
|
radioPosition?: {
|
||||||
|
topMin: number;
|
||||||
|
topMax: number;
|
||||||
|
leftMin: number;
|
||||||
|
leftMax: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the DroppedItemsObj type
|
||||||
|
interface DroppedItemsObj {
|
||||||
|
[key: string]: DroppedItem[]; // Map each key to an array of DroppedItems
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the initial state
|
||||||
|
interface DroppedItemsState {
|
||||||
|
droppedItems: DroppedItemsObj;
|
||||||
|
droppedItemsCounter: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: DroppedItemsState = {
|
||||||
|
droppedItems: {},
|
||||||
|
droppedItemsCounter: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the slice
|
||||||
|
const droppedItemsSlice = createSlice({
|
||||||
|
name: 'droppedItems',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
// Action to add a dropped item
|
||||||
|
addDroppedItem: (state, action: PayloadAction<DroppedItem>) => {
|
||||||
|
const { documentIndex, pageNumber } = action.payload;
|
||||||
|
const key = `${documentIndex}_${pageNumber}`;
|
||||||
|
|
||||||
|
if (!state.droppedItems[key]) {
|
||||||
|
state.droppedItems[key] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
state.droppedItems[key].push(action.payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Action to update an existing dropped item
|
||||||
|
updateDroppedItem: (state, action: PayloadAction<DroppedItem>) => {
|
||||||
|
const { documentIndex, pageNumber, label } = action.payload;
|
||||||
|
const key = `${documentIndex}_${pageNumber}`;
|
||||||
|
|
||||||
|
if (state.droppedItems[key]) {
|
||||||
|
const index = state.droppedItems[key].findIndex(item => item.label === label);
|
||||||
|
if (index !== -1) {
|
||||||
|
state.droppedItems[key][index] = action.payload; // Replace the existing item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Action to remove a dropped item
|
||||||
|
removeDroppedItem: (state, action: PayloadAction<{ documentIndex: number, pageNumber: number, label: string }>) => {
|
||||||
|
const { documentIndex, pageNumber, label } = action.payload;
|
||||||
|
const key = `${documentIndex}_${pageNumber}`;
|
||||||
|
|
||||||
|
if (state.droppedItems[key]) {
|
||||||
|
state.droppedItems[key] = state.droppedItems[key].filter(item => item.label !== label);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Action to clear all dropped items
|
||||||
|
clearDroppedItems: (state) => {
|
||||||
|
state.droppedItems = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Action to increment the counter for dropped items
|
||||||
|
incrementDroppedItemsCounter: (state) => {
|
||||||
|
state.droppedItemsCounter += 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export actions and reducer
|
||||||
|
export const {
|
||||||
|
addDroppedItem,
|
||||||
|
updateDroppedItem,
|
||||||
|
removeDroppedItem,
|
||||||
|
clearDroppedItems,
|
||||||
|
incrementDroppedItemsCounter,
|
||||||
|
} = droppedItemsSlice.actions;
|
||||||
|
|
||||||
|
export default droppedItemsSlice.reducer;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// const a = {
|
||||||
|
// documentIds: [
|
||||||
|
// {
|
||||||
|
// index: 0,
|
||||||
|
// document: ''
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// recipients: [
|
||||||
|
// {
|
||||||
|
// index: 0,
|
||||||
|
// email: 'abcd@gmail.com',
|
||||||
|
// name: 'suegfu',
|
||||||
|
// type: 'signer' || 'cc' || 'inPerson'
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// replyTo: 'efgh@gmail.com',
|
||||||
|
// replyToName: 'efgh',
|
||||||
|
|
||||||
|
// businessUnit: 'id',
|
||||||
|
// message: 'sdfvsdfc',
|
||||||
|
// subject: 'sdf',
|
||||||
|
// expiry: {}
|
||||||
|
// }
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
interface LoaderState {
|
||||||
|
isLoading: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: LoaderState = {
|
||||||
|
isLoading: false,
|
||||||
|
message: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const loaderSlice = createSlice({
|
||||||
|
name: "loader",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
showLoader: (state, action: PayloadAction<string | undefined>) => {
|
||||||
|
state.isLoading = true;
|
||||||
|
state.message = action.payload;
|
||||||
|
},
|
||||||
|
hideLoader: (state) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
state.message = undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { showLoader, hideLoader } = loaderSlice.actions;
|
||||||
|
export default loaderSlice.reducer;
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
interface ModalState {
|
||||||
|
isOpen: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ModalState = {
|
||||||
|
isOpen: false,
|
||||||
|
message: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalSlice = createSlice({
|
||||||
|
name: "modal",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
showModal: (state, action: PayloadAction<string | undefined>) => {
|
||||||
|
state.isOpen = true;
|
||||||
|
state.message = action.payload;
|
||||||
|
},
|
||||||
|
hideModal: (state) => {
|
||||||
|
state.isOpen = false;
|
||||||
|
state.message = undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { showModal, hideModal } = modalSlice.actions;
|
||||||
|
export default modalSlice.reducer;
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import axios from "axios";
|
||||||
|
import { createAxiosInstance } from "../../config/axiosInstance";
|
||||||
|
|
||||||
|
const axiosInstance = createAxiosInstance();
|
||||||
|
|
||||||
|
export type SignType = "self-sign" | "multiple-sign";
|
||||||
|
|
||||||
|
|
||||||
|
export interface Image {
|
||||||
|
index: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
type: string
|
||||||
|
src: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectedDocument {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
original: boolean;
|
||||||
|
corrupted?: boolean;
|
||||||
|
images?: Image[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayloadSelectedDocument {
|
||||||
|
documentId: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
original: boolean;
|
||||||
|
corrupted?: boolean;
|
||||||
|
images?: Image[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectedDocumentsState {
|
||||||
|
selectedDocuments: SelectedDocument[];
|
||||||
|
signType: SignType | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: SelectedDocumentsState = {
|
||||||
|
selectedDocuments: [],
|
||||||
|
signType: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Thunk to update selected documents and sign type in the backend
|
||||||
|
export const updateSelectedDocuments = createAsyncThunk<
|
||||||
|
{ documents: SelectedDocument[] }, // Return type
|
||||||
|
{ documents: SelectedDocument[] } // Payload type
|
||||||
|
>(
|
||||||
|
"selectedDocuments/updateSelectedDocuments",
|
||||||
|
async ({ documents }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
|
||||||
|
console.log(documents, "From Selected Documents")
|
||||||
|
|
||||||
|
// await axiosInstance.post("/selected-documents",
|
||||||
|
// { documents }
|
||||||
|
// );
|
||||||
|
|
||||||
|
return { documents };
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("From Catch", documents)
|
||||||
|
return { documents };
|
||||||
|
// return rejectWithValue(error.response?.data || "Failed to update selected documents");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedDocumentsSlice = createSlice({
|
||||||
|
name: "selectedDocuments",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setSelectedDocuments: (state, action: PayloadAction<SelectedDocument[]>) => {
|
||||||
|
state.selectedDocuments = action.payload;
|
||||||
|
},
|
||||||
|
setSignType: (state, action: PayloadAction<SignType>) => {
|
||||||
|
state.signType = action.payload;
|
||||||
|
},
|
||||||
|
setImages: (state, action: PayloadAction<PayloadSelectedDocument[]>) => {
|
||||||
|
for (const document of action.payload) {
|
||||||
|
const docId = document.documentId;
|
||||||
|
for (const [index, selDoc] of state.selectedDocuments.entries()) {
|
||||||
|
if (selDoc.id === docId) {
|
||||||
|
state.selectedDocuments[index].images = document.images;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetSelDoc: (state) => {
|
||||||
|
state = initialState
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(updateSelectedDocuments.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
console.log("updateSelectedDocuments.Pending")
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(updateSelectedDocuments.fulfilled, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
console.log("updateSelectedDocuments.fulfilled", action.payload.documents)
|
||||||
|
state.selectedDocuments = action.payload.documents;
|
||||||
|
})
|
||||||
|
.addCase(updateSelectedDocuments.rejected, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
console.log("updateSelectedDocuments.Rejected")
|
||||||
|
state.error = action.payload as string;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setSelectedDocuments, setSignType, setImages, resetSelDoc } = selectedDocumentsSlice.actions;
|
||||||
|
export default selectedDocumentsSlice.reducer;
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import colors from '../../utlity/colorsArr';
|
||||||
|
import api from '../../config/api';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export enum recipientType {
|
||||||
|
SIGNER = "Signer",
|
||||||
|
INPERSONSIGNER = "InPersonSigner",
|
||||||
|
CC = "CC"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Recipient {
|
||||||
|
id?: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
routingOrder: number;
|
||||||
|
email: string;
|
||||||
|
hostName: string;
|
||||||
|
hostEmail: string;
|
||||||
|
type: recipientType;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectedRecipientInt {
|
||||||
|
recipients: Recipient[],
|
||||||
|
signingOrder: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddRecipientArgs {
|
||||||
|
recipientsData: Recipient[];
|
||||||
|
accountId: string;
|
||||||
|
transactionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: SelectedRecipientInt = {
|
||||||
|
recipients: [],
|
||||||
|
signingOrder: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addRecipientAsync = createAsyncThunk<Recipient[], AddRecipientArgs>(
|
||||||
|
'selectedRecipients/addRecipientAsync',
|
||||||
|
async ({ recipientsData, accountId, transactionId }, { rejectWithValue }) => {
|
||||||
|
console.log(recipientsData, accountId, transactionId, "recipientsData Line 46");
|
||||||
|
try {
|
||||||
|
const response = await api.post(
|
||||||
|
`/api/accounts/${accountId}/transactions/${transactionId}/recipients`,
|
||||||
|
recipientsData
|
||||||
|
);
|
||||||
|
console.log(response)
|
||||||
|
return response.data as Recipient[];
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(err.response?.data || "Failed to add recipient");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const removeRecipientAsync = createAsyncThunk<string, string>(
|
||||||
|
'selectedRecipients/removeRecipientAsync',
|
||||||
|
async (email, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/recipients`, { data: { email } });
|
||||||
|
return email; // return email so we can filter from state
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(err.response?.data || "Failed to remove recipient");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchRecipients = createAsyncThunk<Recipient[], { accountId: string, transactionId: string }>(
|
||||||
|
'selectedRecipients/fetchRecipients',
|
||||||
|
async ({ accountId, transactionId }, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}/recipients`);
|
||||||
|
return response.data as Recipient[];
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(err.response?.data || "Failed to fetch recipients");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedRecipientsSlice = createSlice({
|
||||||
|
name: 'selectedRecipients',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
// Add a single recipient
|
||||||
|
addRecipient: (state, action: PayloadAction<Recipient>) => {
|
||||||
|
for (const recipient of state.recipients) {
|
||||||
|
if (recipient.email === action.payload.email) {
|
||||||
|
toast.dismiss();
|
||||||
|
toast.error("Email already exists !", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
});
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding Recipient on top if type is inPersonSigner
|
||||||
|
console.log("Getting Type ", action.payload.type)
|
||||||
|
if (action.payload.type === recipientType.INPERSONSIGNER) {
|
||||||
|
state.signingOrder = true
|
||||||
|
state.recipients = [action.payload, ...state.recipients]
|
||||||
|
} else {
|
||||||
|
state.recipients.push(action.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
resetRecipients: (state) => {
|
||||||
|
state.recipients = [];
|
||||||
|
},
|
||||||
|
// Replace the entire selectedRecipients array
|
||||||
|
setSelectedRecipients: (state, action: PayloadAction<Recipient[]>) => {
|
||||||
|
for (const [index, recipient] of action.payload.entries()) {
|
||||||
|
if ((recipient.type === recipientType.INPERSONSIGNER && index > 0) && action.payload[index - 1].type !== recipientType.INPERSONSIGNER) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.recipients = action.payload;
|
||||||
|
},
|
||||||
|
setSigningOrder: (state, action: PayloadAction<boolean>) => {
|
||||||
|
|
||||||
|
for (const recipient of state.recipients) {
|
||||||
|
if (recipient.type === recipientType.INPERSONSIGNER) {
|
||||||
|
if (state.signingOrder) return state;
|
||||||
|
state.signingOrder = true;
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.signingOrder = action.payload;
|
||||||
|
},
|
||||||
|
// Remove Recipients
|
||||||
|
removeSelectedRecipient: (state, action: PayloadAction<Recipient>) => {
|
||||||
|
console.log("removeSelectedRecipient Called")
|
||||||
|
state.recipients = state.recipients.filter((recipient) => recipient.email !== action.payload.email);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add Color in Recipient
|
||||||
|
addColorsToRecipient: (state) => {
|
||||||
|
// const colors = generateUniqueColors(state.recipients.length);
|
||||||
|
for (const [index, recipient] of state.recipients.entries()) {
|
||||||
|
console.log(state)
|
||||||
|
debugger
|
||||||
|
recipient.color = colors[index]
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
updateRecipientOrder: (state, action: PayloadAction<{ id: string; routingOrder: number }>) => {
|
||||||
|
console.log("recipient order called Line 159")
|
||||||
|
const { id, routingOrder } = action.payload;
|
||||||
|
const recipient = state.recipients.find((r) => r.id === id || r.email === id);
|
||||||
|
if (recipient) {
|
||||||
|
recipient.routingOrder = routingOrder;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateRecipientIds: (state, action: PayloadAction<Recipient[]>) => {
|
||||||
|
console.log("updateRecipientIds", console.table(action.payload));
|
||||||
|
for (const recObj of state.recipients) {
|
||||||
|
const email = recObj.email
|
||||||
|
for (const recObj2 of action.payload) {
|
||||||
|
recObj.id = recObj2.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetSelRecipient: (state) => {
|
||||||
|
state = initialState;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(fetchRecipients.pending, (state) => {
|
||||||
|
console.log("Fetching recipients...");
|
||||||
|
})
|
||||||
|
.addCase(fetchRecipients.fulfilled, (state, action: PayloadAction<Recipient[]>) => {
|
||||||
|
console.log("Fetched recipients successfully:", action.payload);
|
||||||
|
|
||||||
|
// Assign colors to recipients
|
||||||
|
const assignedRecipients = action.payload.map((recipient, index) => ({
|
||||||
|
...recipient,
|
||||||
|
color: colors[index], // Cycle if more recipients than colors
|
||||||
|
}));
|
||||||
|
|
||||||
|
state.recipients = assignedRecipients;
|
||||||
|
})
|
||||||
|
.addCase(fetchRecipients.rejected, (state, action) => {
|
||||||
|
console.error("Failed to fetch recipients:", action.payload);
|
||||||
|
toast.error("Failed to fetch recipients", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
.addCase(addRecipientAsync.pending, (state) => {
|
||||||
|
console.log("Fetching recipients...");
|
||||||
|
})
|
||||||
|
.addCase(addRecipientAsync.fulfilled, (state, action: PayloadAction<Recipient>) => {
|
||||||
|
console.log("Added recipient successfully:", action.payload);
|
||||||
|
// state.recipients.push(action.payload);
|
||||||
|
})
|
||||||
|
.addCase(addRecipientAsync.rejected, (state, action) => {
|
||||||
|
console.error("Failed to fetch recipients:", action.payload);
|
||||||
|
toast.error("Failed to fetch recipients", {
|
||||||
|
position: "bottom-right",
|
||||||
|
autoClose: 5000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: "light",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { addRecipient, setSelectedRecipients, removeSelectedRecipient, setSigningOrder, addColorsToRecipient, updateRecipientIds, resetSelRecipient, updateRecipientOrder, resetRecipients } = selectedRecipientsSlice.actions;
|
||||||
|
export default selectedRecipientsSlice.reducer;
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
interface SelectedTag {
|
||||||
|
key: string;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: SelectedTag | null = null;
|
||||||
|
|
||||||
|
const selectedTagSlice = createSlice({
|
||||||
|
name: 'selectedTag',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
// Set selected tag
|
||||||
|
setSelectedTag: (state, action: PayloadAction<SelectedTag>) => {
|
||||||
|
return action.payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clear selected tag
|
||||||
|
resetSelectedTag: (state) => {
|
||||||
|
state = initialState;
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setSelectedTag, resetSelectedTag } = selectedTagSlice.actions;
|
||||||
|
|
||||||
|
export default selectedTagSlice.reducer;
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
type SignatureType = 'TYPED' | 'DRAW' | 'UPLOADED';
|
||||||
|
|
||||||
|
interface SignatureData {
|
||||||
|
type: SignatureType;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SignatureState {
|
||||||
|
signature: SignatureData | null;
|
||||||
|
initial: SignatureData | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: SignatureState = {
|
||||||
|
signature: null,
|
||||||
|
initial: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const signatureSlice = createSlice({
|
||||||
|
name: 'signature',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setSignature: (state, action: PayloadAction<SignatureData>) => {
|
||||||
|
state.signature = action.payload;
|
||||||
|
},
|
||||||
|
setInitial: (state, action: PayloadAction<SignatureData>) => {
|
||||||
|
state.initial = action.payload;
|
||||||
|
},
|
||||||
|
clearSignature: (state) => {
|
||||||
|
state.signature = null;
|
||||||
|
},
|
||||||
|
clearInitial: (state) => {
|
||||||
|
state.initial = null;
|
||||||
|
},
|
||||||
|
clearAll: (state) => {
|
||||||
|
state.signature = null;
|
||||||
|
state.initial = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setSignature,
|
||||||
|
setInitial,
|
||||||
|
clearSignature,
|
||||||
|
clearInitial,
|
||||||
|
clearAll,
|
||||||
|
} = signatureSlice.actions;
|
||||||
|
|
||||||
|
export default signatureSlice.reducer;
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
// const updatedTagData = {
|
||||||
|
// "id": "id" in dropTag ? dropTag.id : null,
|
||||||
|
// "label": dropTag.label,
|
||||||
|
// "position": position,
|
||||||
|
// mask,
|
||||||
|
// "type": dropTag.type.toUpperCase(),
|
||||||
|
// "subType": dropTag.subType ? dropTag.subType.toUpperCase() : null,
|
||||||
|
// "error": "error" in dropTag ? dropTag.error.toUpperCase() : null,
|
||||||
|
// "regex": "regex" in dropTag ? dropTag.regex : [],
|
||||||
|
// "format": "format" in dropTag ? dropTag.format : "",
|
||||||
|
// "documentIndex": dropTag.documentIndex,
|
||||||
|
// "pageNumber": dropTag.pageNumber,
|
||||||
|
// "signerIndex": dropTag.signerIndex,
|
||||||
|
// "style": "style" in dropTag ? dropTag.style : {},
|
||||||
|
// }
|
||||||
|
|
||||||
|
interface position {
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface mask {
|
||||||
|
maskType: string;
|
||||||
|
maskCharacter: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tag {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
position: position;
|
||||||
|
mask: mask;
|
||||||
|
type: string;
|
||||||
|
subType: string | null;
|
||||||
|
error: string | null;
|
||||||
|
regex: string[] | [];
|
||||||
|
format: string;
|
||||||
|
documentIndex: number;
|
||||||
|
pageNumber: number;
|
||||||
|
signerIndex: number;
|
||||||
|
style: object | {};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemoveTags {
|
||||||
|
recipientId: string;
|
||||||
|
tagId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TagsStateType {
|
||||||
|
mod: Tag[];
|
||||||
|
remove: RemoveTags[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: TagsStateType = {
|
||||||
|
"mod": [],
|
||||||
|
"remove": []
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagsSlice = createSlice({
|
||||||
|
name: 'tags',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
udpateTags: (state, action: PayloadAction<Tag[]>) => {
|
||||||
|
state.mod = action.payload;
|
||||||
|
},
|
||||||
|
removeTags: (state, action: PayloadAction<RemoveTags>) => {
|
||||||
|
// action.payload.index
|
||||||
|
},
|
||||||
|
clearTags: (state) => {
|
||||||
|
Object.assign(state, initialState);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {udpateTags, removeTags} = tagsSlice.actions;
|
||||||
|
|
||||||
|
export default tagsSlice.reducer;
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export enum TRANSACTION_STATUS_ENUMS {
|
||||||
|
TRANSACTION_INITIATED,
|
||||||
|
TRANSACTION_NOT_INITIATED,
|
||||||
|
TRANSACTION_SEND_SUCCESS,
|
||||||
|
TRANSACTION_SEND_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Transaction {
|
||||||
|
transactionId: string | null;
|
||||||
|
status: TRANSACTION_STATUS_ENUMS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: Transaction = {
|
||||||
|
transactionId: null,
|
||||||
|
status: TRANSACTION_STATUS_ENUMS.TRANSACTION_NOT_INITIATED
|
||||||
|
};
|
||||||
|
|
||||||
|
const transactionSlice = createSlice({
|
||||||
|
name: 'tags',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setTransactionId: (state, action: PayloadAction<string>) => {
|
||||||
|
const transactionId = action.payload;
|
||||||
|
console.log("Mic Check 2", transactionId)
|
||||||
|
state.transactionId = transactionId;
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
updateTransactionStatus: (state, action: PayloadAction<TRANSACTION_STATUS_ENUMS>) => {
|
||||||
|
state.status = action.payload;
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
clearTransaction: (state) => {
|
||||||
|
state = initialState
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setTransactionId, updateTransactionStatus, clearTransaction } = transactionSlice.actions;
|
||||||
|
|
||||||
|
export default transactionSlice.reducer;
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import axios from "axios";
|
||||||
|
import api from "../../config/api";
|
||||||
|
|
||||||
|
// --- Types ---
|
||||||
|
export interface Account {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
isDefault: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserState {
|
||||||
|
id: number | null;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
language?: string;
|
||||||
|
account: Account | null;
|
||||||
|
picture?: string;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
authorized?: boolean;
|
||||||
|
accessToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Initial State ---
|
||||||
|
const initialState: UserState = {
|
||||||
|
id: null,
|
||||||
|
email: "",
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
language: "",
|
||||||
|
account: null,
|
||||||
|
picture: undefined,
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
authorized: false,
|
||||||
|
accessToken: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Thunk to fetch user details ---
|
||||||
|
export const fetchUserDetails = createAsyncThunk<UserState, void, { rejectValue: string }>
|
||||||
|
("user/fetchUserDetails", async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get("/api/user/details");
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
return rejectWithValue(
|
||||||
|
error.response?.data?.message || "Failed to fetch user details"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Slice ---
|
||||||
|
const userSlice = createSlice({
|
||||||
|
name: "user",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
logout: () => ({
|
||||||
|
...initialState,
|
||||||
|
}),
|
||||||
|
updateUser: (state, action: PayloadAction<Partial<UserState>>) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...action.payload,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
authorized: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(fetchUserDetails.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(fetchUserDetails.fulfilled, (state, action) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...action.payload,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
authorized: true,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.addCase(fetchUserDetails.rejected, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = action.payload || "Unknown error";
|
||||||
|
state.authorized = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Exports ---
|
||||||
|
export const { logout, updateUser } = userSlice.actions;
|
||||||
|
export default userSlice.reducer;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
// export interface UserState {
|
||||||
|
// id: string;
|
||||||
|
// username: string;
|
||||||
|
// firstName: string;
|
||||||
|
// lastName: string;
|
||||||
|
// email: string;
|
||||||
|
// picture?: string;
|
||||||
|
// loading: boolean;
|
||||||
|
// error: string | null;
|
||||||
|
// authorized?: boolean;
|
||||||
|
// accessToken?: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Load user data from localStorage (if available)
|
||||||
|
// const loadUserFromLocalStorage = (): UserState => {
|
||||||
|
// try {
|
||||||
|
// const userDetails = JSON.parse(localStorage.getItem("user") || "null");
|
||||||
|
// if (!userDetails || !userDetails.id) {
|
||||||
|
// localStorage.removeItem("user"); // Ensure invalid data is cleared
|
||||||
|
// return {
|
||||||
|
// id: "",
|
||||||
|
// username: "",
|
||||||
|
// firstName: "",
|
||||||
|
// lastName: "",
|
||||||
|
// email: "",
|
||||||
|
// picture: undefined,
|
||||||
|
// loading: false,
|
||||||
|
// error: null,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// return {
|
||||||
|
// ...userDetails,
|
||||||
|
// loading: false,
|
||||||
|
// error: null,
|
||||||
|
// };
|
||||||
|
// } catch (e) {
|
||||||
|
// console.log(e.message)
|
||||||
|
// return {
|
||||||
|
// id: "",
|
||||||
|
// username: "",
|
||||||
|
// email: "",
|
||||||
|
// picture: undefined,
|
||||||
|
// loading: false,
|
||||||
|
// error: null,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const initialState: UserState = loadUserFromLocalStorage();
|
||||||
|
|
||||||
|
// const userSlice = createSlice({
|
||||||
|
// name: "user",
|
||||||
|
// initialState,
|
||||||
|
// reducers: {
|
||||||
|
// logout: (state) => {
|
||||||
|
// localStorage.removeItem("user");
|
||||||
|
// return {
|
||||||
|
// id: "",
|
||||||
|
// username: "",
|
||||||
|
// email: "",
|
||||||
|
// picture: undefined,
|
||||||
|
// loading: false,
|
||||||
|
// error: null,
|
||||||
|
// };
|
||||||
|
// },
|
||||||
|
// updateUser: (state, action: PayloadAction<UserState>) => {
|
||||||
|
// localStorage.setItem("user", JSON.stringify(action.payload));
|
||||||
|
// console.log(action.payload, "Line 70")
|
||||||
|
// return { ...state, ...action.payload, loading: false, error: null };
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// export const { logout, updateUser } = userSlice.actions;
|
||||||
|
// export default userSlice.reducer;
|
||||||
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
// import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
||||||
|
// import { persistStore, persistReducer } from 'redux-persist';
|
||||||
|
// import storage from 'redux-persist/lib/storage';
|
||||||
|
|
||||||
|
// // Ensure proper imports
|
||||||
|
// import userReducer from "./slices/userSlice";
|
||||||
|
// import selectedDocsReducer from "./slices/selectedDocsSlice";
|
||||||
|
// import loaderReducer from "./slices/loaderSlice";
|
||||||
|
// import modalReducer from "./slices/modalSlice";
|
||||||
|
// import selectedRecipientsReducer from "./slices/selectedRecipientsSlice";
|
||||||
|
// import documentReducer from "./slices/documentSlice";
|
||||||
|
// import tagsReducer from "./slices/tagsSlice";
|
||||||
|
// import transactionReducer from "./slices/transactionSlice";
|
||||||
|
// import signatureReducer from "./slices/signatureSlice";
|
||||||
|
|
||||||
|
// // Generate a unique key for this tab
|
||||||
|
// const uniqueTabKey = sessionStorage.getItem('tabId') || Math.random().toString(36).substr(2, 9);
|
||||||
|
// if (!sessionStorage.getItem('tabId')) {
|
||||||
|
// sessionStorage.setItem('tabId', uniqueTabKey);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Persist configuration for `user` (shared across all tabs)
|
||||||
|
// const userPersistConfig = {
|
||||||
|
// key: 'root-user',
|
||||||
|
// storage,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Persist configuration for other slices (unique key per tab)
|
||||||
|
// const otherPersistConfig = (key) => ({
|
||||||
|
// key: `root-${key}-${uniqueTabKey}`,
|
||||||
|
// storage,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const rootReducer = combineReducers({
|
||||||
|
// user: persistReducer(userPersistConfig, userReducer),
|
||||||
|
// documents: persistReducer(otherPersistConfig('documents'), documentReducer),
|
||||||
|
// selectedDocuments: persistReducer(otherPersistConfig('selectedDocuments'), selectedDocsReducer),
|
||||||
|
// loader: persistReducer(otherPersistConfig('loader'), loaderReducer),
|
||||||
|
// modal: persistReducer(otherPersistConfig('modal'), modalReducer),
|
||||||
|
// selectedRecipients: persistReducer(otherPersistConfig('selectedRecipients'), selectedRecipientsReducer),
|
||||||
|
// tags: persistReducer(otherPersistConfig('tags'), tagsReducer),
|
||||||
|
// transaction: persistReducer(otherPersistConfig('transaction'), transactionReducer),
|
||||||
|
// signature: persistReducer(otherPersistConfig('signature'), signatureReducer),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const persistedReducer = rootReducer;
|
||||||
|
|
||||||
|
// export const store = configureStore({
|
||||||
|
// reducer: persistedReducer,
|
||||||
|
// devTools: import.meta.env.VITE_NODE_ENV !== "production" || true,
|
||||||
|
// middleware: (getDefaultMiddleware) =>
|
||||||
|
// getDefaultMiddleware({
|
||||||
|
// serializableCheck: false,
|
||||||
|
// }),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// export const persistor = persistStore(store);
|
||||||
|
|
||||||
|
// export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
// export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
||||||
|
|
||||||
|
import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
// Reducers
|
||||||
|
import userReducer from "./slices/userSlice";
|
||||||
|
import selectedDocsReducer from "./slices/selectedDocsSlice";
|
||||||
|
import loaderReducer from "./slices/loaderSlice";
|
||||||
|
import modalReducer from "./slices/modalSlice";
|
||||||
|
import selectedRecipientsReducer from "./slices/selectedRecipientsSlice";
|
||||||
|
import documentReducer from "./slices/documentSlice";
|
||||||
|
import tagsReducer from "./slices/tagsSlice";
|
||||||
|
import transactionReducer from "./slices/transactionSlice";
|
||||||
|
import signatureReducer from "./slices/signatureSlice";
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
user: userReducer,
|
||||||
|
documents: documentReducer,
|
||||||
|
selectedDocuments: selectedDocsReducer,
|
||||||
|
loader: loaderReducer,
|
||||||
|
modal: modalReducer,
|
||||||
|
selectedRecipients: selectedRecipientsReducer,
|
||||||
|
tags: tagsReducer,
|
||||||
|
transaction: transactionReducer,
|
||||||
|
signature: signatureReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: rootReducer,
|
||||||
|
devTools: import.meta.env.VITE_NODE_ENV !== "production" || true,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
serializableCheck: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
const colors = [
|
||||||
|
"#8F87F1",
|
||||||
|
"#BB3E00",
|
||||||
|
"#F7AD45",
|
||||||
|
"#A86523",
|
||||||
|
"#4A102A",
|
||||||
|
"#644A07",
|
||||||
|
"#FE7743",
|
||||||
|
"#FFC6C6",
|
||||||
|
"#547792",
|
||||||
|
"#E9F5BE",
|
||||||
|
"#81E7AF",
|
||||||
|
"#7AE2CF",
|
||||||
|
"#3A59D1",
|
||||||
|
"#F4F8D3",
|
||||||
|
"#222222",
|
||||||
|
"#FFF085",
|
||||||
|
"#27391C",
|
||||||
|
"#BE5985",
|
||||||
|
"#ADB2D4",
|
||||||
|
"#A5BFCC"
|
||||||
|
];
|
||||||
|
|
||||||
|
export default colors;
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
export const dateFormats = [
|
||||||
|
// { format: "MM/DD/YYYY", example: "04/02/2025" },
|
||||||
|
// { format: "MMMM D, YYYY", example: "April 2, 2025" },
|
||||||
|
// { format: "YYYY-MM-DD", example: "2025-04-02" },
|
||||||
|
// { format: "dddd, MMMM D, YYYY", example: "Wednesday, April 2, 2025" },
|
||||||
|
// { format: "D MMM, YYYY", example: "2 Apr, 2025" }
|
||||||
|
|
||||||
|
{ "format": "DD/MM/YY" },
|
||||||
|
{ "format": "MM/DD/YY" },
|
||||||
|
{ "format": "MM/DD/YYYY" },
|
||||||
|
{ "format": "DD/MM/YYYY" },
|
||||||
|
{ "format": "YYYY/MM/DD" },
|
||||||
|
{ "format": "YYYY-MM-DD" },
|
||||||
|
{ "format": "DD MMM YYYY" },
|
||||||
|
{ "format": "dddd, DD MMM YYYY" },
|
||||||
|
{ "format": "MMM DD, YYYY" },
|
||||||
|
{ "format": "DD-MMM-YYYY" },
|
||||||
|
{ "format": "MMM DD, YYYY" }
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export const timeFormats = [
|
||||||
|
{ format: "HH:mm" },
|
||||||
|
{ format: "hh:mm" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const dateTimeFormats = [
|
||||||
|
{ "format": "DD/MM/YY" },
|
||||||
|
{ "format": "MM/DD/YY" },
|
||||||
|
{ "format": "MM/DD/YYYY" },
|
||||||
|
{ "format": "DD/MM/YYYY" },
|
||||||
|
{ "format": "YYYY/MM/DD" },
|
||||||
|
{ "format": "YYYY-MM-DD" },
|
||||||
|
{ "format": "DD MMM YYYY" },
|
||||||
|
{ "format": "dddd, DD MMM YYYY" },
|
||||||
|
{ "format": "MMM DD, YYYY" },
|
||||||
|
{ "format": "DD-MMM-YYYY" },
|
||||||
|
{ "format": "MMM DD, YYYY" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const yearFormats = [
|
||||||
|
{ format: "YYYY", example: "2025" },
|
||||||
|
{ format: "YY", example: "25" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const monthFormats = [
|
||||||
|
{ format: "MMMM", example: "April" },
|
||||||
|
{ format: "MMM", example: "Apr" },
|
||||||
|
{ format: "MM", example: "04" }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const weekFormats = [
|
||||||
|
{ format: "wo", example: "14th week of the year" },
|
||||||
|
{ format: "W", example: "Week 14" },
|
||||||
|
{ format: "YYYY-Www", example: "2025-W14" }
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
const type_all = (str: string, changeWith: string): string => {
|
||||||
|
return str.replace(/./g, changeWith);
|
||||||
|
};
|
||||||
|
|
||||||
|
const type_last_four = (str: string, changeWith: string): string => {
|
||||||
|
str = str.slice(0, -4).replace(/./g, "*") + str.slice(-4);
|
||||||
|
return str.replace(/\*/g, changeWith);
|
||||||
|
};
|
||||||
|
|
||||||
|
const type_credit_card = (str: string, changeWith: string): string => {
|
||||||
|
if (str.length !== 16) return str;
|
||||||
|
// console.log(str.replace(/\d(?=\d{4})/g, "*"), "line 11", str)
|
||||||
|
// return str.replace(/\d(?=\d{4})/g, `${changeWith}`);
|
||||||
|
return `${changeWith.repeat(4)} ${changeWith.repeat(4)} ${changeWith.repeat(4)} ${str.slice(-4)}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const type_national_insurance = (str: string, changeWith: string): string => {
|
||||||
|
// str = str.slice(0, -1).replace(/[A-Za-z0-9]/g, "*") + str.slice(-1);
|
||||||
|
// return str.replace(/\*/g, changeWith);
|
||||||
|
return `${changeWith.repeat(2)} ${changeWith.repeat(2)} ${changeWith.repeat(2)} ${changeWith.repeat(2)} ${str.slice(-1)}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const type_social_security = (str: string, changeWith: string): string => {
|
||||||
|
// Expecting 9-digit string like "123456789"
|
||||||
|
const cleaned = str.replace(/\D/g, ""); // remove non-digit characters
|
||||||
|
if (cleaned.length !== 9) return str;
|
||||||
|
|
||||||
|
const part3 = cleaned.slice(5);
|
||||||
|
return `${changeWith.repeat(3)}-${changeWith.repeat(2)}-${part3}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const type_social_insurance = (str: string, changeWith: string): string => {
|
||||||
|
const cleaned = str.replace(/\D/g, "");
|
||||||
|
if (cleaned.length !== 9) return str;
|
||||||
|
|
||||||
|
const part3 = cleaned.slice(6);
|
||||||
|
return `${changeWith.repeat(3)} ${changeWith.repeat(3)} ${part3}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const maskUtils = (type: string, original: string, changeWith: string): string => {
|
||||||
|
switch (type) {
|
||||||
|
case "ALL":
|
||||||
|
return type_all(original, changeWith);
|
||||||
|
case "LAST_FOUR":
|
||||||
|
return type_last_four(original, changeWith);
|
||||||
|
case "CREDIT_CARD":
|
||||||
|
return type_credit_card(original, changeWith);
|
||||||
|
case "NATIONAL_INSURANCE":
|
||||||
|
return type_national_insurance(original, changeWith);
|
||||||
|
case "SOCIAL_SECURITY":
|
||||||
|
return type_social_security(original, changeWith);
|
||||||
|
case "SOCIAL_INSURANCE":
|
||||||
|
return type_social_insurance(original, changeWith);
|
||||||
|
default:
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default maskUtils;
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { JSX } from "react";
|
||||||
|
|
||||||
|
export interface DroppedItem {
|
||||||
|
id?: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
focusElementIndx?: number;
|
||||||
|
label?: string;
|
||||||
|
type: string;
|
||||||
|
subType?: string;
|
||||||
|
dimensions: {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
recipientId: number;
|
||||||
|
// mask?: {
|
||||||
|
// type: string;
|
||||||
|
// character: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
maskType?: string;
|
||||||
|
maskCharacter?: string;
|
||||||
|
|
||||||
|
required?: boolean;
|
||||||
|
format?: string;
|
||||||
|
radioGroups?: DroppedItem[];
|
||||||
|
// documentIndex: number;
|
||||||
|
documentId: string;
|
||||||
|
pageIndex: number;
|
||||||
|
options?: string[]
|
||||||
|
selectedValue?: string | null;
|
||||||
|
radioPosition?: {
|
||||||
|
topMin: number;
|
||||||
|
topMax: number;
|
||||||
|
leftMin: number;
|
||||||
|
leftMax: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number
|
||||||
|
};
|
||||||
|
value?: string | number;
|
||||||
|
validation?: {
|
||||||
|
minLength: number;
|
||||||
|
maxLength: number;
|
||||||
|
error: string;
|
||||||
|
regex: string[];
|
||||||
|
};
|
||||||
|
isDeleted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DroppedItemsObj {
|
||||||
|
[key: string]: DroppedItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OpenType {
|
||||||
|
TYPED,
|
||||||
|
DRAW,
|
||||||
|
UPLOADED
|
||||||
|
}
|
||||||
|
|
||||||
|
// types/account.ts
|
||||||
|
|
||||||
|
export enum UserRole {
|
||||||
|
OWNER = "OWNER",
|
||||||
|
ADMIN = "ADMIN",
|
||||||
|
EDITOR = "EDITOR",
|
||||||
|
VIEWER = "VIEWER",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AccountStatus {
|
||||||
|
ACTIVE = "ACTIVE",
|
||||||
|
REJECTED = "REJECTED",
|
||||||
|
INVITED = "INVITED",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AccountData {
|
||||||
|
accountId: number;
|
||||||
|
accountName: string;
|
||||||
|
role: UserRole;
|
||||||
|
status: AccountStatus;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounces an effect until after a delay has passed with no changes in dependencies.
|
||||||
|
*
|
||||||
|
* @param effect - The callback function to run after the debounce delay.
|
||||||
|
* @param deps - The dependencies to watch for changes.
|
||||||
|
* @param delay - The debounce delay in milliseconds.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const useDebouncedEffect = (effect: () => void, deps: any[], delay: number) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
effect(); // call the effect after the delay
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(handler); // cancel previous timer on dependency change
|
||||||
|
};
|
||||||
|
}, [...deps, delay]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDebouncedEffect;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue