Compare commits

..

No commits in common. "1a4ea46c5b8df8da79ba594fa8b0d07c979735ff" and "4a5ede2891f7c353c03fe606d5feb91f209d41bb" have entirely different histories.

16 changed files with 193 additions and 1195 deletions

View File

@ -21,7 +21,6 @@ import TransactionView from "./components/transactions/TransactionView";
import Templates from "./components/templates/Templates";
import DocumentCompletionScreen from "./components/process/CompletionScreen/DocumentCompletionScreen";
import JoyrideController from "./utlity/JoyRideTour";
import Contacts from "./components/contacts/Contacts";
const App = () => {
@ -50,8 +49,6 @@ const App = () => {
<Route path="/templates" element={<Templates />} />
{/* </Route> */}
<Route path="/contacts" element={<Contacts />} />
<Route path="/s/:page1" element={<Setting />} />
</Route>

View File

@ -213,6 +213,10 @@ const Account = () => {
const navigate = useNavigate();
const handleTransactionClick = (id: number) => {
navigate(`/accounts/${activeAccountId}/transactions/${id}`);
};
const handleInvite = async (data: { i_accountName: number; email: string; role: string }) => {
setInviteLoading(true);
try {

View File

@ -2,8 +2,6 @@ import React, { useEffect, useState } from 'react'
import { PiDotsThreeOutlineVerticalFill } from 'react-icons/pi';
import api from '../../config/api';
import { Transaction } from '../transactions/Transactions';
import { useNavigate } from 'react-router-dom';
import { FaCheck } from 'react-icons/fa';
@ -18,12 +16,10 @@ const TransactionDataTable = () => {
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const navigate = useNavigate();
const fetchTransactions = async () => {
console.log("fetchTransactions")
try {
const response = await api.get(`/api/accounts/${activeAccountId}/transactions?sortBy=lastModifiedAt desc`);
const response = await api(`/api/accounts/${activeAccountId}/transactions`);
setTransactions(response.data || []);
} catch (error) {
console.error("Error fetching transactions:", error);
@ -32,10 +28,6 @@ const TransactionDataTable = () => {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const handleTransactionClick = (id: number) => {
navigate(`/accounts/${activeAccountId}/transactions/${id}`);
};
useEffect(() => {
if (activeAccountId) {
fetchTransactions();

View File

@ -1,174 +0,0 @@
import { useEffect, useState, useRef } from 'react';
import { FaEllipsisV } from 'react-icons/fa';
import api from '../../config/api';
import ContactFormModal from './ContactsFormModal';
import { Bounce, toast } from 'react-toastify';
import { IoMdAdd } from 'react-icons/io';
const Contacts = () => {
const activeAccountId = JSON.parse(sessionStorage.getItem('activeAccountId') || 'null');
const [contacts, setContacts] = useState([]);
const [loading, setLoading] = useState(true);
const [openMenuId, setOpenMenuId] = useState(null);
const blurTimeoutRef = useRef(null);
const [createAndInviteLoader, setCreateAndInviteLoader] = useState(false);
const [showModal, setShowModal] = useState(false)
const fetchContacts = async () => {
try {
const res = await api.get(`/api/accounts/${activeAccountId}/contacts`);
setContacts(res.data.content || []);
} catch (error) {
console.error('Failed to fetch contacts:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (activeAccountId) {
fetchContacts();
}
}, [activeAccountId]);
const handleEdit = (contactId) => {
console.log('Edit contact:', contactId);
setOpenMenuId(null);
};
const handleDelete = async(contactId) => {
console.log('Delete contact:', contactId);
setOpenMenuId(null);
try {
let res = await api.delete(`/api/accounts/{accountId}/contacts/{contactId}`)
if (res?.data) {
}
} catch (error) {
console.log(error)
}
};
const toggleMenu = (contactId) => {
// Cancel any pending blur timeout (prevents blinking)
clearTimeout(blurTimeoutRef.current);
setOpenMenuId((prevId) => (prevId === contactId ? null : contactId));
};
const handleBlur = () => {
blurTimeoutRef.current = setTimeout(() => {
setOpenMenuId(null);
}, 100);
};
const handleContactSubmit = async (data: ContactData) => {
try {
setCreateAndInviteLoader(true);
const res = await api.post(`/api/accounts/${activeAccountId}/contacts`, [data]);
if (res.status === 200) {
toast.success('Contact created successfully!', {
position: "top-center",
autoClose: 1500,
transition: Bounce,
});
setShowModal(false)
fetchContacts();
}
// return res;
} catch (err) {
console.error("Error creating contact", err);
toast.error("Failed to create contact", {
position: "top-center",
autoClose: 1500,
transition: Bounce,
});
throw err;
} finally {
setCreateAndInviteLoader(false);
}
};
return (
<>
<div className='flex justify-end my-2 cursor-pointer'>
<div
onClick={() => setShowModal((prev) => !prev)}
className="flex items-center gap-2 bg-[var(--primary)] px-3 py-1.5 rounded-sm hover:opacity-90 ">
<IoMdAdd className="text-white" />
<button
className="text-white rounded text-sm hover:opacity-90 cursor-pointer">
Add Contact
</button>
</div>
</div>
<div className="overflow-y-auto max-h-[79vh] border border-gray-200 rounded shadow-sm no-scrollbar">
<table className="min-w-full text-sm bg-white">
<thead className="bg-gray-100 text-gray-700 uppercase text-xs tracking-wider sticky top-0 z-10">
<tr>
<th className="px-4 py-3 text-left">First Name</th>
<th className="px-4 py-3 text-left">Last Name</th>
<th className="px-4 py-3 text-left">Email</th>
<th className="px-4 py-3 text-center">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{loading ? (
<tr>
<td colSpan="4" className="text-center py-4">Loading...</td>
</tr>
) : contacts.length === 0 ? (
<tr>
<td colSpan="4" className="text-center py-4">No contacts found.</td>
</tr>
) : (
contacts.map((contact) => (
<tr key={contact.id} className="hover:bg-gray-50">
<td className="px-4 py-3">{contact.firstName}</td>
<td className="px-4 py-3">{contact.lastName}</td>
<td className="px-4 py-3">{contact.email}</td>
<td className="px-4 py-3 text-center">
<div
className="relative inline-block"
tabIndex={0}
onBlur={handleBlur}
>
<button
className="text-gray-600 hover:text-black p-1 focus:outline-none"
onClick={() => toggleMenu(contact.id)}
>
<FaEllipsisV />
</button>
{openMenuId === contact.id && (
<div className="absolute top-full right-0 mt-1 bg-white border border-gray-200 rounded shadow-lg z-20 min-w-[100px]">
<button
onClick={() => handleEdit(contact.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-blue-600"
>
Edit
</button>
<button
onClick={() => handleDelete(contact.id)}
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-red-600"
>
Delete
</button>
</div>
)}
</div>
</td>
</tr>
))
)}
</tbody>
</table>
{showModal &&
<ContactFormModal onSubmit={handleContactSubmit}
loading={createAndInviteLoader} setShowModal={setShowModal}/>
}
</div>
</>
);
};
export default Contacts;

View File

@ -1,245 +0,0 @@
// ContactForm.tsx
import React, { useState } from 'react';
import { FaSpinner } from 'react-icons/fa';
export interface ContactData {
contactId?: number;
email: string;
firstName: string;
lastName: string;
title: string;
company: string;
website: string;
phones: string[];
}
interface Props {
onSubmit: (data: ContactData) => void;
loading: boolean;
setShowModal: () => React.Dispatch<React.SetStateAction<boolean>>
}
const ContactFormModal = ({ onSubmit, loading, setShowModal }: Props) => {
const [formData, setFormData] = useState<ContactData>({
email: '',
firstName: '',
lastName: '',
title: '',
company: '',
website: '',
phones: [''],
});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const handleChange = (field: keyof ContactData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: '' }));
}
};
const handlePhoneChange = (index: number, value: string) => {
const numericValue = value.replace(/\D/g, ''); // Remove non-numeric
const updatedPhones = [...formData.phones];
updatedPhones[index] = numericValue;
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
const errorKey = `phones_${index}`;
if (errors[errorKey]) {
setErrors((prev) => ({ ...prev, [errorKey]: '' }));
}
};
const addPhoneField = () => {
setFormData((prev) => ({ ...prev, phones: [...prev.phones, ''] }));
};
const removePhoneField = (index: number) => {
const updatedPhones = formData.phones.filter((_, i) => i !== index);
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
};
const validate = () => {
const newErrors: { [key: string]: string } = {};
const nameRegex = /^[a-zA-Z\s\-']+$/;
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
const phoneRegex = /^\d+$/;
// Email
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Invalid email address';
}
// // First Name
// if (!formData.firstName.trim()) {
// newErrors.firstName = 'First name is required';
// } else if (!nameRegex.test(formData.firstName)) {
// newErrors.firstName = 'First name contains invalid characters';
// }
// Last Name
if (!formData.lastName.trim()) {
newErrors.lastName = 'Last name is required';
} else if (!nameRegex.test(formData.lastName)) {
newErrors.lastName = 'Last name contains invalid characters';
}
// // Title
// if (!formData.title.trim()) {
// newErrors.title = 'Title is required';
// }
// Website (optional but validate if present)
if (formData.website.trim() && !urlRegex.test(formData.website)) {
newErrors.website = 'Invalid website URL';
}
// Phones
// formData.phones.forEach((phone, index) => {
// if (!phone.trim()) {
// newErrors[`phones_${index}`] = 'Phone number is required';
// } else if (!phoneRegex.test(phone)) {
// newErrors[`phones_${index}`] = 'Phone number must contain only digits';
// }
// });
return newErrors;
};
const handleSubmit = async () => {
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
try {
await onSubmit(formData);
setFormData({
email: '',
firstName: '',
lastName: '',
title: '',
company: '',
website: '',
phones: [''],
});
setErrors({});
} catch (err) {
console.error('Submit failed:', err);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/5 backdrop-blur-sm bg-opacity-50">
<div className="bg-white rounded-md p-6 max-w-[700px] w-full shadow-lg relative">
<div className="ms-2 space-y-2 max-w-[60vw]">
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
<div className="grid grid-cols-2 gap-3">
{[
{ label: 'First Name', field: 'firstName' },
{ label: 'Last Name', field: 'lastName' },
{ label: 'Email', field: 'email' },
{ label: 'Title', field: 'title' },
{ label: 'Company', field: 'company' },
{ label: 'Website', field: 'website' },
].map(({ label, field }) => (
<div key={field} className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">{label}</label>
<input
type={field === 'email' ? 'email' : 'text'}
value={formData[field as keyof ContactData] as string}
onChange={(e) => handleChange(field as keyof ContactData, e.target.value)}
required={true}
className="px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
placeholder={`Enter ${label}`}
/>
{errors[field] && <span className="text-sm text-red-500 mt-1">{errors[field]}</span>}
</div>
))}
</div>
<div>
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Numbers</label>
<div className="space-y-2">
{formData.phones.map((phone, index) => (
<div key={index} className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<input
type="text"
value={phone}
onChange={(e) => handlePhoneChange(index, e.target.value)}
className="flex-1 px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
maxLength={15}
placeholder='Enter Phone Numbers'
/>
{formData.phones.length > 1 && (
<button
type="button"
onClick={() => removePhoneField(index)}
className="text-xs text-red-500 hover:underline"
>
Remove
</button>
)}
</div>
{errors[`phones_${index}`] && (
<span className="text-sm text-red-500">{errors[`phones_${index}`]}</span>
)}
</div>
))}
<button
type="button"
onClick={addPhoneField}
className="text-sm text-blue-500 hover:underline mt-1 cursor-pointer"
>
+ Add another phone
</button>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
onClick={() => setShowModal((prev) => !prev)}
disabled={loading}
className="bg-gray-300 px-5 py-1.5 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer me-3"
> Cancel
</button>
<button
type="button"
onClick={handleSubmit}
disabled={loading}
className="bg-gray-300 px-5 py-1.5 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer"
>
{loading ? (
<>
<FaSpinner className="animate-spin inline-block mr-2" />
SAVING...
</>
) : (
'SAVE CONTACT'
)}
</button>
</div>
</div>
</div>
</div>
);
};
export default ContactFormModal;

View File

@ -10,7 +10,7 @@ import { IoSettingsOutline } from "react-icons/io5";
import axios from 'axios';
import { MdManageAccounts } from 'react-icons/md';
import { GrContactInfo, GrTransaction } from 'react-icons/gr';
import { GrTransaction } from 'react-icons/gr';
type SideBarProps = {
showSideBar: boolean;
@ -68,7 +68,6 @@ const SideBar: React.FC<SideBarProps> = ({ showSideBar, setShowSideBar }) => {
<NavLink to="/templates" icon={<HiTemplate />} label="Templates" showSideBar={showSideBar} menuPathMap={menuPathMap} />
{/* <NavLink to="/accounts" icon={<MdManageAccounts />} label="Accounts" showSideBar={showSideBar} menuPathMap={menuPathMap} /> */}
<NavLink to="transactions" icon={<GrTransaction />} label="Transactions" showSideBar={showSideBar} menuPathMap={menuPathMap} />
<NavLink to="/contacts" icon={<GrContactInfo />} label="Contacts" showSideBar={showSideBar} menuPathMap={menuPathMap} />
</div>
</div>

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { ContactTypeObj } from './RecipientAddCard';
import React, { useState } from 'react'
import { ContactTypeObj } from './RecipientAddCard'
import { Recipient } from '../../../store/slices/selectedRecipientsSlice';
interface ContactDataProps {
@ -10,127 +10,66 @@ interface ContactDataProps {
openContactModal: boolean;
setOpenContactModal: React.Dispatch<React.SetStateAction<boolean>>;
setRecipients: React.Dispatch<React.SetStateAction<Recipient>>;
topRef?: React.RefObject<HTMLDivElement>;
setManualFocusTriggered: React.Dispatch<React.SetStateAction<boolean>>
}
// Helper function to highlight matching text
const highlightText = (text: string, searchTerm: string, shouldHighlight: boolean) => {
if (!shouldHighlight || !searchTerm.trim()) {
return <span>{text}</span>;
}
const ContactModal: React.FC<ContactDataProps> = ({ contactData, inpFirstName, inpLastName, inpEmail, openContactModal, setRecipients, setOpenContactModal }) => {
const searchTermLower = searchTerm.toLowerCase();
const textLower = text.toLowerCase();
const index = textLower.indexOf(searchTermLower);
if (index === -1) {
return <span>{text}</span>;
}
const beforeMatch = text.slice(0, index);
const match = text.slice(index, index + searchTerm.length);
const afterMatch = text.slice(index + searchTerm.length);
return (
<span>
{beforeMatch}
<span className="bg-yellow-200 font-medium">{match}</span>
{afterMatch}
</span>
);
};
const ContactModal: React.FC<ContactDataProps> = ({
contactData,
inpFirstName,
inpLastName,
inpEmail,
openContactModal,
setRecipients,
setOpenContactModal ,
setManualFocusTriggered
}) => {
const handleContactClick = (indx: number) => {
if (!contactData.contacts.length) return;
setRecipients((prev) => ({
...prev, // Preserve other properties like uuid, newOne, etc.
setRecipients((prev) => {
return {
firstName: contactData.contacts[indx].firstName,
lastName: contactData.contacts[indx].lastName,
email: contactData.contacts[indx].email,
}));
type: prev.type
}
});
setOpenContactModal(false);
setManualFocusTriggered(false)
};
const handleKeyDown = (event: React.KeyboardEvent, indx: number) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleContactClick(indx);
}
};
if (!openContactModal || !contactData.contacts.length) {
return null;
}
return (
<div className="max-h-60 overflow-y-auto">
{contactData.contacts.map((contact, indx) => (
<>
{
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={contact.id || indx}
tabIndex={0}
onMouseDown={() => handleContactClick(indx)}
onKeyDown={(e) => handleKeyDown(e, indx)}
className="flex items-center px-3 py-2 cursor-pointer hover:bg-gray-50 transition-colors duration-150 focus:bg-gray-100 focus:outline-none rounded-md"
role="button"
aria-label={`Select contact ${contact.firstName} ${contact.lastName}`}
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"}` }}
>
{/* Avatar */}
<div className="w-8 h-8 bg-[var(--primaryLight2)] rounded-full flex items-center justify-center text-white text-sm font-medium">
{(contact.firstName?.[0] || '') + (contact.lastName?.[0] || '')}
</div>
{/* Contact Info */}
<div className="ml-3 flex-1 min-w-0">
<div className="flex gap-1.5 items-center">
<p className="text-sm font-semibold text-gray-900 truncate">
{highlightText(
contact.firstName,
inpFirstName,
contactData.type === "firstName"
)}
{recipient.firstName}
</p>
<p className="text-sm font-semibold text-gray-900 truncate">
{highlightText(
contact.lastName,
inpLastName,
contactData.type === "lastName"
)}
<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 truncate">
{highlightText(
contact.email,
inpEmail,
contactData.type === "email"
)}
<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>
))}
})
}
</>
)
}
{/* No results message */}
{contactData.contacts.length === 0 && (
<div className="px-3 py-4 text-center text-gray-500 text-sm">
No contacts found
</div>
)}
</div>
);
};
export default ContactModal;
// return <p
// key={indx}
// className='w-full hover:bg-slate-100 cursor-pointer px-2 py-1'>
// {val.firstName}
// </p>
export default ContactModal

View File

@ -6,7 +6,7 @@ import SelectedDocuments from "./SelectedDocuments";
import api from "../../../config/api";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../store/store";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { setImages } from "../../../store/slices/selectedDocsSlice";
import { addRecipient, addRecipientAsync, fetchRecipients, recipientType, resetRecipients, toggleToTemplates, TRANSACTION_TEMPLATE_TOGGLE, updateRecipientIds } from "../../../store/slices/selectedRecipientsSlice";
@ -55,9 +55,6 @@ const ProcessStepOne = () => {
const selectedDocuments = useSelector((state: RootState) => state.documents.documents)
const selectedRecipients = useSelector((state: RootState) => state.selectedRecipients.recipients)
const userData = useSelector((state: RootState) => state.user)
// ProcessStepOne.tsx
const topRef = useRef<HTMLDivElement | null>(null);
// const userData = useSelector((state: RootState) => state.user)
const [transactionDetails, setTransactionDetails] = useState<any>([]);
@ -69,7 +66,7 @@ const ProcessStepOne = () => {
// Fetching Transaction Details - START
(async () => {
try {
const res = await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`);
const res = await api.get(`/api/accounts/${accountId}/transactions/${transactionId}`);
setTransactionDetails(res.data)
} catch (e) {
console.log("Error - transactions details")
@ -84,11 +81,6 @@ const ProcessStepOne = () => {
// })()
// >>>>>>> origin/main
// (async () => {
// const res = await api.get(`/api/accounts/${accountId}/${transactionTemplateToggle}/${transactionId}`)
// console.log(res.data);
// })()
selectedRecipients.length === 0 && dispatch(fetchRecipients({ accountId, transactionId }));
selectedDocuments.length === 0 && dispatch(fetchDocuments({ accountId, transactionId }));
}, [])
@ -323,7 +315,7 @@ const ProcessStepOne = () => {
onClick={() => handleNextClick(false)}
className="text-sm px-4 py-2 rounded bg-[var(--primary)] text-white shadow hover:bg-[var(--primaryLight2)] transition"
>
{signType === SignTypeEnum.selfSign ? "Sign" : "Next"}
{signType === SignTypeEnum.selfSign ? "Add Tags" : "Add Tags"}
</button>
</div>
</div>
@ -360,15 +352,7 @@ const ProcessStepOne = () => {
<div className="flex flex-col gap-4 align-start">
<RecipientCards transactionTemplateToggle={transactionTemplateToggle} setEditRecipient={setEditRecipient} setEditCase={setEditCase}/>
{/* {signType !== "self-sign" && <RecipientAddCard />} */}
<div
ref={topRef}
className={`relative w-full bg-[var(--mainViewBg)] ${globalLoader.isLoading ? 'pointer-events-none h-screen overflow-hidden' : ''}`}
>
<RecipientAddCard transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase} topRef={topRef} />
</div>
<RecipientAddCard transactionTemplateToggle={transactionTemplateToggle} editRecipient={editRecipient} editCase={editCase} setEditRecipient={setEditRecipient} setEditCase={setEditCase}/>
</div>
}
</section>

View File

@ -24,11 +24,7 @@ export interface ContactTypeObj {
}
const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase, setEditRecipient, setEditCase, topRef }) => {
const [activeAccountId, setActiveAccountId] = useState<string | null>(
JSON.parse(sessionStorage.getItem('activeAccountId') || 'null')
);
const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase ,setEditRecipient, setEditCase }) => {
const [contactData, setContactData] = useState<ContactTypeObj>({ type: "", contacts: [] });
const [openContactModal, setOpenContactModal] = useState(false);
@ -39,28 +35,8 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
const [recipient, setRecipients] = useState<Recipient>({ uuid:null, newOne:false, firstName: "", lastName: "", email: "", hostName: "", hostEmail: "", roleName: "", type: recipientType.SIGNER, allowSenderToEdit: false, allowSenderToDelete: false });
const [showAdvanceSettings, setShowAdvanceSettings] = useState(false)
const [cachedContacts, setCachedContacts] = useState<ContactType[]>([]);
const [filteredContacts, setFilteredContacts] = useState<ContactType[]>([]);
const [activeField, setActiveField] = useState<"firstName" | "lastName" | "email" | null>(null);
const [hasFetchedInitial, setHasFetchedInitial] = useState(false);
const [lastQuery, setLastQuery] = useState<string | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [manualFocusTriggered, setManualFocusTriggered] = useState(false);
console.log(recipient)
useEffect(() => {
if (openContactModal) {
if (topRef?.current) {
topRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
} else {
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
}, [openContactModal]);
useEffect(() => {
if (editCase === "Edit" && editRecipient) {
setRecipients({
@ -105,6 +81,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
}));
}
// ✅ Reset state and form
setRecipients({
firstName: "",
lastName: "",
@ -121,7 +98,7 @@ const RecipientAddCard = ({ transactionTemplateToggle, editRecipient, editCase,
// setRecipients(null),
setEditCase("")
setManualFocusTriggered(false)
formRef.current?.reset();
};
@ -178,97 +155,7 @@ setManualFocusTriggered(false)
}, 350)
}
const handleFocus = async (field: "firstName" | "lastName" | "email") => {
setManualFocusTriggered(true);
setOpenContactModal(true);
setActiveField(field);
setLastQuery(null); // Reset to allow fresh input fetch if needed
if (!hasFetchedInitial) {
try {
const res = await api.get(`/api/accounts/${activeAccountId}/contacts?pageSize=10`);
const contacts = res.data?.content || [];
setCachedContacts(contacts);
setFilteredContacts(contacts);
setContactData({ type: field, contacts });
setHasFetchedInitial(true);
} catch (err) {
console.error("Initial contact fetch failed:", err);
}
} else {
setFilteredContacts(cachedContacts);
setContactData({ type: field, contacts: cachedContacts });
}
};
useDebouncedEffect(() => {
console.log("useDebouncedEffect")
if (!activeField || !manualFocusTriggered) return;
const inputValue = recipient[activeField].trim().toLowerCase();
if (inputValue.length === 0) {
setFilteredContacts(cachedContacts);
setContactData({ type: activeField, contacts: cachedContacts });
setOpenContactModal(true); // Keep showing modal with all options
return;
}
// Local match
const localMatches = cachedContacts.filter(contact =>
contact[activeField]?.toLowerCase().includes(inputValue)
);
if (localMatches.length > 0) {
setFilteredContacts(localMatches);
setContactData({ type: activeField, contacts: localMatches });
setOpenContactModal(true);
return;
}
if (lastQuery !== inputValue) {
setLastQuery(inputValue);
api
.get(`/api/accounts/${activeAccountId}/contacts?pageSize=5&${activeField}=${inputValue}`)
.then((res) => {
const fetched = res.data?.content || [];
setFilteredContacts(fetched);
setContactData({ type: activeField, contacts: fetched });
setOpenContactModal(true);
})
.catch(console.error);
}
}, [recipient.firstName, recipient.lastName, recipient.email], 300);
useEffect(() => {
const handleClickOutside = (e: MouseEvent | FocusEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setOpenContactModal(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("focusin", handleClickOutside); // Also handles tabbing out
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("focusin", handleClickOutside);
};
}, []);
return (
<div ref={containerRef}>
<form
ref={formRef}
onSubmit={handleRecipientAdd}
@ -405,7 +292,7 @@ useEffect(() => {
<div className="grid grid-cols-12 gap-4 mt-3">
<input
type="text"
onFocus={() => handleFocus("firstName")}
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
value={recipient.firstName}
placeholder="First Name"
@ -415,7 +302,7 @@ useEffect(() => {
<input
required
type="text"
onFocus={() => handleFocus("lastName")}
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
value={recipient.lastName}
placeholder="Last Name"
@ -425,7 +312,7 @@ useEffect(() => {
<input
required
type="email"
onFocus={() => handleFocus("email")}
onFocus={() => setOpenContactModal(true)}
onBlur={handleContactModalBlur}
value={recipient.email}
placeholder="Recipient Email"
@ -459,8 +346,8 @@ useEffect(() => {
</div>}
{/* Contact Modal */}
{openContactModal && contactData.contacts.length > 0 && (
<div className="absolute z-50 mt-1 w-2/3 max-h-[50vh] bg-white shadow-lg rounded-md p-3 border border-gray-100 overflow-y-auto no-scrollbar">
{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}
@ -469,14 +356,10 @@ useEffect(() => {
openContactModal={openContactModal}
setRecipients={setRecipients}
setOpenContactModal={setOpenContactModal}
topRef={topRef}
setManualFocusTriggered={setManualFocusTriggered}
/>
</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">
@ -507,7 +390,7 @@ useEffect(() => {
</button>
</div>
</form>
</div>
);
};

View File

@ -149,79 +149,6 @@ const DirectionalControls: React.FC<DirectionalControlsProps> = ({
<button onClick={() => handleMove('down')} disabled={disabled} className={buttonClass} title="Move Down">
<FaArrowDown />
</button>
{/* Middle Row: Left - Center (Inputs) - Right */}
<div className="flex items-center space-x-1">
{/* Left Button */}
<div className="flex flex-col items-center">
<button
onClick={() => handleMove('left')}
disabled={disabled}
className={buttonClass}
title="Move left"
type="button"
>
<FaArrowLeft size={12} />
</button>
<span className="mt-1">Left</span>
</div>
{/* Center Panel */}
<div className="relative flex items-center justify-center bg-white rounded-lg px-1 py-2 w-24 ">
{/* Floating dimensions tooltip */}
<div className="absolute -top-2 left-1/2 -translate-x-1/2 px-2 py-[1px] text-[10px] font-mono text-black z-10 rounded-sm flex items-center gap-[2px]">
<input
type="number"
value={leftValue}
onChange={(e) => handlePositionInputChange('left', e.target.value)}
onBlur={() => handlePositionInputBlur('left')}
onKeyPress={(e) => handleKeyPress(e, 'left')}
disabled={disabled}
className="w-[40px] h-[16px] text-[10px] text-center border-none focus:outline-none text-black font-mono"
/>
<span className="mx-[1px]">×</span>
<input
type="number"
value={topValue}
onChange={(e) => handlePositionInputChange('top', e.target.value)}
onBlur={() => handlePositionInputBlur('top')}
onKeyPress={(e) => handleKeyPress(e, 'top')}
disabled={disabled}
className="w-[40px] h-[16px] text-[10px] text-center border-none focus:outline-none text-black font-mono"
/>
</div>
</div>
{/* Right Button */}
<div className="flex flex-col items-center">
<button
onClick={() => handleMove('right')}
disabled={disabled}
className={buttonClass}
title="Move right"
type="button"
>
<FaArrowRight size={12} />
</button>
<span className="mt-1">Right</span>
</div>
</div>
{/* Bottom Button */}
<div className="flex justify-center me-4">
<div className="flex flex-col items-center">
<button
onClick={() => handleMove('down')}
disabled={disabled}
className={buttonClass}
title="Move down"
type="button"
>
<FaArrowDown size={12} />
</button>
<span className="mt-1">Bottom</span>
</div>
</div>
</div>
);
};

View File

@ -1,230 +0,0 @@
// ContactForm.tsx
import React, { useState } from 'react';
import { FaSpinner } from 'react-icons/fa';
export interface ContactData {
contactId?: number;
email: string;
firstName: string;
lastName: string;
title: string;
company: string;
website: string;
phones: string[];
}
interface Props {
onSubmit: (data: ContactData) => void;
loading: boolean;
}
const ContactForm = ({ onSubmit, loading }: Props) => {
const [formData, setFormData] = useState<ContactData>({
email: '',
firstName: '',
lastName: '',
title: '',
company: '',
website: '',
phones: [''],
});
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const handleChange = (field: keyof ContactData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
if (errors[field]) {
setErrors((prev) => ({ ...prev, [field]: '' }));
}
};
const handlePhoneChange = (index: number, value: string) => {
const numericValue = value.replace(/\D/g, ''); // Remove non-numeric
const updatedPhones = [...formData.phones];
updatedPhones[index] = numericValue;
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
const errorKey = `phones_${index}`;
if (errors[errorKey]) {
setErrors((prev) => ({ ...prev, [errorKey]: '' }));
}
};
const addPhoneField = () => {
setFormData((prev) => ({ ...prev, phones: [...prev.phones, ''] }));
};
const removePhoneField = (index: number) => {
const updatedPhones = formData.phones.filter((_, i) => i !== index);
setFormData((prev) => ({ ...prev, phones: updatedPhones }));
};
const validate = () => {
const newErrors: { [key: string]: string } = {};
const nameRegex = /^[a-zA-Z\s\-']+$/;
const urlRegex = /^(https?:\/\/)?([\w-]+\.)+[\w-]{2,}(\/.*)?$/;
const phoneRegex = /^\d+$/;
// Email
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Invalid email address';
}
// // First Name
// if (!formData.firstName.trim()) {
// newErrors.firstName = 'First name is required';
// } else if (!nameRegex.test(formData.firstName)) {
// newErrors.firstName = 'First name contains invalid characters';
// }
// Last Name
if (!formData.lastName.trim()) {
newErrors.lastName = 'Last name is required';
} else if (!nameRegex.test(formData.lastName)) {
newErrors.lastName = 'Last name contains invalid characters';
}
// // Title
// if (!formData.title.trim()) {
// newErrors.title = 'Title is required';
// }
// Website (optional but validate if present)
if (formData.website.trim() && !urlRegex.test(formData.website)) {
newErrors.website = 'Invalid website URL';
}
// Phones
// formData.phones.forEach((phone, index) => {
// if (!phone.trim()) {
// newErrors[`phones_${index}`] = 'Phone number is required';
// } else if (!phoneRegex.test(phone)) {
// newErrors[`phones_${index}`] = 'Phone number must contain only digits';
// }
// });
return newErrors;
};
const handleSubmit = async () => {
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
try {
await onSubmit(formData);
setFormData({
email: '',
firstName: '',
lastName: '',
title: '',
company: '',
website: '',
phones: [''],
});
setErrors({});
} catch (err) {
console.error('Submit failed:', err);
}
};
return (
<div className="ms-2 space-y-2 max-w-[60vw]">
<h2 className="text-lg font-medium text-slate-500">Add Contact</h2>
<div className="grid grid-cols-2 gap-3">
{[
{ label: 'First Name', field: 'firstName' },
{ label: 'Last Name', field: 'lastName' },
{ label: 'Email', field: 'email' },
{ label: 'Title', field: 'title' },
{ label: 'Company', field: 'company' },
{ label: 'Website', field: 'website' },
].map(({ label, field }) => (
<div key={field} className="flex flex-col">
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">{label}</label>
<input
type={field === 'email' ? 'email' : 'text'}
value={formData[field as keyof ContactData] as string}
onChange={(e) => handleChange(field as keyof ContactData, e.target.value)}
required={true}
className="px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
placeholder={`Enter ${label}`}
/>
{errors[field] && <span className="text-sm text-red-500 mt-1">{errors[field]}</span>}
</div>
))}
</div>
<div>
<label className="text-sm font-medium text-[var(--greyTextCol)] mb-1">Phone Numbers</label>
<div className="space-y-2">
{formData.phones.map((phone, index) => (
<div key={index} className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<input
type="text"
value={phone}
onChange={(e) => handlePhoneChange(index, e.target.value)}
className="flex-1 px-3 py-1.5 border border-gray-400 rounded-md focus:outline-none focus:ring-1 focus:ring-[var(--primary)] text-sm"
maxLength={15}
placeholder='Enter Phone Numbers'
/>
{formData.phones.length > 1 && (
<button
type="button"
onClick={() => removePhoneField(index)}
className="text-xs text-red-500 hover:underline"
>
Remove
</button>
)}
</div>
{errors[`phones_${index}`] && (
<span className="text-sm text-red-500">{errors[`phones_${index}`]}</span>
)}
</div>
))}
<button
type="button"
onClick={addPhoneField}
className="text-sm text-blue-500 hover:underline mt-1 cursor-pointer"
>
+ Add another phone
</button>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
onClick={handleSubmit}
disabled={loading}
className="bg-gray-300 px-5 py-2 rounded-md text-slate-700 text-sm font-medium hover:bg-opacity-90 transition disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer"
>
{loading ? (
<>
<FaSpinner className="animate-spin inline-block mr-2" />
SAVING...
</>
) : (
'SAVE CONTACT'
)}
</button>
</div>
</div>
);
};
export default ContactForm;

View File

@ -9,7 +9,6 @@ import AsideSection from "./AsideSection";
import Signature from "./Signature/Signature";
import { Bounce, toast } from "react-toastify";
import AccountNav from "../accounts/AccountNav";
import ContactForm , {ContactData} from "./ContactForm";
// Define the shape of state object
interface Account {
@ -213,13 +212,6 @@ const Setting = () => {
accountOptionsLoading={accountOptionsLoading}
/>
);
case "contacts":
return (
<ContactForm
onSubmit={handleContactSubmit}
loading={createAndInviteLoader}
/>
);
default:
return <div className="bg-white p-6 rounded-lg shadow-sm">Coming Soon...</div>;
}
@ -244,7 +236,6 @@ const Setting = () => {
const tabs = [
{ id: "userProfile", label: "User Profile", type: "settings" },
{ id: "create and invite", label: "Create and invite", type: "settings" },
{ id: "contacts", label: "Contacts", type: "settings" },
{ id: "language", label: "Language", type: "settings" },
{ id: "contactAdd", label: "Add Contact", type: "contact" },
{ id: "contactRemove", label: "Remove Contact", type: "contact" },
@ -388,28 +379,6 @@ const Setting = () => {
}
};
const handleContactSubmit = async (data: ContactData) => {
try {
setCreateAndInviteLoader(true);
const res = await api.post(`/api/accounts/${activeAccountId}/contacts`, [data]);
if (res.status === 200) {
toast.success('Contact created successfully!', {
position: "top-center",
autoClose: 1500,
transition: Bounce,
});
}
// return res;
} catch (err) {
console.error("Error creating contact", err);
toast.error("Failed to create contact");
throw err;
} finally {
setCreateAndInviteLoader(false);
}
};
return (
<>
{/* <SettingNav /> */}

View File

@ -30,7 +30,7 @@ const Templates = () => {
console.log("handleAddTemplate called");
try {
const res = await api.post(`/api/accounts/${activeAccountId}/templates`);
navigate(`/process-step-one/${activeAccountId}/template/${res.data.id}`)
navigate(`/process-step-one/${activeAccountId}/template/${res.data}`)
} catch (error) {
console.log(error)
}

View File

@ -1,5 +1,5 @@
import { AiFillSignature } from 'react-icons/ai';
import { Link, useNavigate, useParams } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import { LuGitGraph } from 'react-icons/lu';
import AccountNav from '../accounts/AccountNav';
import { useEffect, useState } from 'react';
@ -7,8 +7,6 @@ import api from '../../config/api';
import SigningOrderDiagram from '../../SigningOrderDiagram ';
import { IoCloseOutline } from 'react-icons/io5';
import { MdEditDocument } from "react-icons/md";
import { useSelector } from 'react-redux';
import { RootState } from '../../store/store';
// Sample recipient data — you can replace this with actual API data later
const recipients = [
@ -32,8 +30,8 @@ const recipients = [
const TransactionView = () => {
const { accountId, transactionId } = useParams<{ accountId: string, transactionId: string }>();
console.log("account" , accountId , transactionId)
const navigate = useNavigate();
if (!accountId || !transactionId) {
window.location.href = "/";
@ -42,8 +40,6 @@ const TransactionView = () => {
const [transactionDetails, setTransactionDetails] = useState(null);
const [signingOrderModal, setSigningOrderModal] = useState({ open: false });
const user = useSelector((state: RootState) => state.user)
console.log(transactionDetails, "line 37");
const fetchData = async () => {
@ -60,11 +56,6 @@ const TransactionView = () => {
fetchData()
}, [accountId, transactionId])
const handleNavigateToSign = (id: number) => {
window.open(`./../../../../sign/${id}`)
}
return (
<>
{transactionDetails &&
@ -78,12 +69,12 @@ const TransactionView = () => {
<div className='mt-2'>
{/* new Date(transactionDetails?.transaction?.lastModifiedAt * 1000) */}
<p className='text-slate-600'>From <span className='text-blue-600'>{transactionDetails?.transaction.createdBy.firstName + " " + transactionDetails?.transaction.createdBy.lastName}</span></p>
<p className='text-slate-600'>From <span className='text-blue-600'>Shanu.R</span></p>
<p className=''><span className='text-slate-600 font-semibold'>Last Change on</span> <span className='text-slate-400'>{new Date(transactionDetails?.transaction?.lastModifiedAt * 1000).toISOString()}</span></p>
<p className='text-slate-600'>Created at <span>{new Date(transactionDetails?.transaction?.createdAt * 1000).toISOString()}</span></p>
</div>
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>{transactionDetails?.transaction.status}</p>
<p className='text-slate-600 text-[0.98rem] mt-2 font-semibold'>Draft</p>
{/* Buttons */}
<div className='my-2 flex gap-2'>
@ -121,27 +112,19 @@ const TransactionView = () => {
<div key={index}>
<div className='mt-2 flex justify-between items-center'>
<div className='flex gap-8 items-center'>
{/* <p className='text-slate-600'>{index+1}</p> */}
<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 && recipient.hostEmail !== "" && <p className='text-xs'><i><b>Host: {recipient.hostEmail}</b></i></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'>
<div className='flex gap-3 items-center relative'>
<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.email === user.email &&
<p
onClick={() => handleNavigateToSign(recipient.id)}
className='text-white rounded-sm bg-[var(--primary)] px-3 py-1 font-semibold'>
Sign
</p>}
</div>
{recipient.status !== "PENDING" && <p>on {recipient.completedAt}</p>}
{/* <a href={recipient.viewLink} className='text-blue-600 hover:underline'>
View signed location
@ -155,17 +138,10 @@ const TransactionView = () => {
}
{transactionDetails?.transaction?.subject !== "" &&
<div>
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Subject</h2>
<p className='mt-2 text-slate-600'>{`${transactionDetails?.transaction?.subject ? transactionDetails?.transaction?.subject : "Not Found"} `}</p>
</div>}
{transactionDetails?.transaction?.message !== "" &&
<div>
<h2 className='text-lg mt-4 font-semibold text-gray-800 cursor-pointer'>Message</h2>
<p className='mt-2 text-slate-600'>{`${transactionDetails?.transaction?.message ? transactionDetails?.transaction?.message : "Not Found"} `}</p>
</div>}
<p className='mt-2 text-slate-600'>{ transactionDetails?.transaction?.message ?? "No Message Found...!"}</p>
</div>
</div>
<div className='w-[210px] text-xs rounded-sm '>

View File

@ -161,9 +161,7 @@ canvas {
/* Animations */
@keyframes ringA {
from,
4% {
from, 4% {
stroke-dasharray: 0 660;
stroke-width: 20;
stroke-dashoffset: -330;
@ -181,8 +179,7 @@ canvas {
stroke-dashoffset: -595;
}
40%,
54% {
40%, 54% {
stroke-dasharray: 0 660;
stroke-width: 20;
stroke-dashoffset: -660;
@ -200,8 +197,7 @@ canvas {
stroke-dashoffset: -925;
}
90%,
to {
90%, to {
stroke-dasharray: 0 660;
stroke-width: 20;
stroke-dashoffset: -990;
@ -209,9 +205,7 @@ canvas {
}
@keyframes ringB {
from,
12% {
from, 12% {
stroke-dasharray: 0 220;
stroke-width: 20;
stroke-dashoffset: -110;
@ -229,8 +223,7 @@ canvas {
stroke-dashoffset: -195;
}
48%,
62% {
48%, 62% {
stroke-dasharray: 0 220;
stroke-width: 20;
stroke-dashoffset: -220;
@ -248,8 +241,7 @@ canvas {
stroke-dashoffset: -305;
}
98%,
to {
98%, to {
stroke-dasharray: 0 220;
stroke-width: 20;
stroke-dashoffset: -330;
@ -275,8 +267,7 @@ canvas {
stroke-dashoffset: -175;
}
36%,
58% {
36%, 58% {
stroke-dasharray: 0 440;
stroke-width: 20;
stroke-dashoffset: -220;
@ -294,8 +285,7 @@ canvas {
stroke-dashoffset: -395;
}
94%,
to {
94%, to {
stroke-dasharray: 0 440;
stroke-width: 20;
stroke-dashoffset: -440;
@ -303,9 +293,7 @@ canvas {
}
@keyframes ringD {
from,
8% {
from, 8% {
stroke-dasharray: 0 440;
stroke-width: 20;
stroke-dashoffset: 0;
@ -323,8 +311,7 @@ canvas {
stroke-dashoffset: -175;
}
44%,
50% {
44%, 50% {
stroke-dasharray: 0 440;
stroke-width: 20;
stroke-dashoffset: -220;
@ -342,14 +329,12 @@ canvas {
stroke-dashoffset: -395;
}
86%,
to {
86%, to {
stroke-dasharray: 0 440;
stroke-width: 20;
stroke-dashoffset: -440;
}
}
.bottom-border>div:hover {
background-color: var(--primaryLight2) ;
opacity: 0.9;
@ -359,11 +344,3 @@ canvas {
/* Group Border - END */
/* JoyRide - START */
.__floater__body > div > button:last-child {
display: none !important;
}
/* JoyRide - END */

View File

@ -144,7 +144,7 @@ const JoyrideController = () => {
target: "body", // This makes the welcome step show in center of screen
content: (
<div>
<p>👋 Hi there! Welcome to SME. Let’s start signing.</p>
<p>👋 Welcome to SME, Let's Start the tour</p>
</div>
),
placement: "center",