About U

New+++
Gallery view
 
import React, { useEffect, useState, Fragment, useCallback, ChangeEvent, FormEvent, useRef, } from 'react'; import { useRequireAuth } from 'hooks/useRequireAuth'; import SiteLayout from 'components/site/SiteLayout'; import { useRouter } from 'next/router'; import { useSite } from 'hooks/useSite'; import { classNames } from 'utils/joinClassName'; import { Dialog, Tab, Transition } from '@headlessui/react'; import { useToast } from 'hooks/useToast'; import { addMembershipSegment, defaultSegment, getMembershipSegments, membership, } from 'services/membership'; import { v4 } from 'uuid'; import Spinner from 'components/icons/Spinner'; import MultiSelect from 'components/shared/input/MultiSelect'; import { MembershipSegmentInterface } from 'interfaces/membership'; import moment from 'moment'; import ConfirmModal from 'components/dashboard/ConfirmModal'; import { Page, Site } from 'interfaces/site'; import MembershipUserTable from 'components/site/Membership/MembersTable'; import MembershipUsers from 'components/site/Membership/MembersHeader'; import MembershipSegments from 'components/site/Membership/MembersSegments'; import ProtectedPages from 'components/site/Membership/ProtectedPages'; import Webhook from 'components/site/Membership/Webhook'; import InitialMembershipSettings from 'components/site/Membership/MemberInitialSettings'; import { Tooltip } from 'react-tooltip'; const usersPerPage = 20; const UserDetails: React.FC = () => { const { user } = useRequireAuth(); const { query } = useRouter(); const { addToast } = useToast(); const { getSite, fetchAndSetSitePageSettings, isPageSettingFetching, restrictPagesFetch, } = useSite(); const [site, setSite] = useState<Site>(null); const { siteId } = query; const [siteDefaultSegment, setSiteDefaultSegment] = useState(defaultSegment); const [isSegmentsFetched, setIsSegmentsFetched] = useState(false); const [segments, setSegments] = useState([]); const [newSegment, setNewSegment] = useState(''); // const [manageSegment, setManageSegment] = useState(false); const [selectedSegment, setSelectedSegment] = useState([]); const [membershipUsers, setMembershipUsers] = useState([]); const [isMembershipFetched, setIsMembershipFetched] = useState(false); const [membershipUser, setMembershipUser] = useState({ name: '', email: '', allowedSegments: [siteDefaultSegment.id], }); const [showUserDeleteModal, setUserShowDeleteModal] = useState(false); const [isDeleting, setIsDeleting] = useState<boolean>(false); const [isUpdate, setIsUpdate] = useState(false); const [userDoc, setUserDoc] = useState(''); const [loading, setLoading] = useState(false); const [showUserModal, setShowUserModal] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [currentTabIndex, setCurrentTabIndex] = useState(0); const [lockPagesEmpty, setLockPagesEmpty] = useState(false); const [isFirstTimeSiteChange, setIsFirstTimeSiteChange] = useState(true); const [showClickIcon, setShowClickIcon] = useState(true); const [errors, setErrors] = useState({ name: '', email: '', segment: '', addSegment: '', }); const [restrictPages, setRestrictPages] = useState<Page[]>([]); const [isRestrictedPagesFetched, setIsRestrictedPagesFetched] = useState(false); const initialFetchSegments = useRef(null); const sortSegmentOptions = useCallback( (options: Array<MembershipSegmentInterface>) => { options.sort((a, b) => { return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1; }); setSegments(options); }, [] ); const fetchSegments = useCallback(async () => { if ( site && site.id && !isSegmentsFetched && !initialFetchSegments.current ) { initialFetchSegments.current = true; const { defaultSegment, options } = await getMembershipSegments(site.id); setSiteDefaultSegment(defaultSegment); sortSegmentOptions(options); setSelectedSegment([ { name: defaultSegment.name, value: defaultSegment.id, }, ]); setSiteDefaultSegment(defaultSegment); setIsSegmentsFetched(true); } }, [isSegmentsFetched, site, sortSegmentOptions, initialFetchSegments]); const fetchMembershipUsers = useCallback(async () => { if (site?.id && !isMembershipFetched) { const data = { siteId: site.id, action: 'get', }; await membership(data) .then((res) => { if (res.data.status === 200) { setMembershipUsers(res?.data?.data); } setIsMembershipFetched(true); }) .catch((err) => { setIsMembershipFetched(true); console.log(err); }); } }, [isMembershipFetched, site?.id]); useEffect(() => { if (siteId && user) { const data = getSite(siteId.toString()); setSite(data); } }, [getSite, siteId, user]); useEffect(() => { // TO SET DEFAULT TAB INDEX if (isFirstTimeSiteChange && site) { if (!site?.passwordProtection?.enabled) { setCurrentTabIndex(4); } else { setCurrentTabIndex(0); setShowClickIcon(false); } setIsFirstTimeSiteChange(false); return; } }, [isFirstTimeSiteChange, site]); const handleSetRestrictedPages = useCallback(() => { if ( site?.pages && site?.pageSettings && Object.keys(site?.pages)?.length && Object.keys(site?.pageSettings)?.length && false ) { const restrictPages = Object.values(site?.pages).filter((page: Page) => { const pageSettings = site.pageSettings[page.id]; return ( pageSettings?.restrict?.type && pageSettings?.restrict?.type !== 'none' ); }); setRestrictPages(restrictPages); }else{ setLockPagesEmpty(true); } }, [site]); useEffect(() => { if ( site && !site?.pageSettings && !isRestrictedPagesFetched && !isPageSettingFetching ) { fetchAndSetSitePageSettings(site.id).then((res) => { if (res) { setIsRestrictedPagesFetched(true); handleSetRestrictedPages(); } }); } if (site?.pageSettings && !isRestrictedPagesFetched) { setIsRestrictedPagesFetched(true); handleSetRestrictedPages(); } }, [ site, isPageSettingFetching, fetchAndSetSitePageSettings, isRestrictedPagesFetched, handleSetRestrictedPages, ]); useEffect(() => { !isMembershipFetched && fetchMembershipUsers(); }, [fetchMembershipUsers, isMembershipFetched]); useEffect(() => { !isSegmentsFetched && fetchSegments(); }, [fetchSegments, isSegmentsFetched]); useEffect(() => { if ( restrictPagesFetch?.pages && restrictPagesFetch?.pageSettings && Object.keys(restrictPagesFetch?.pages)?.length && Object.keys(restrictPagesFetch?.pageSettings)?.length && restrictPagesFetch?.loading && lockPagesEmpty ) { console.log(restrictPagesFetch,'sdf') const restrictPages: any[] = Object.values( restrictPagesFetch?.pages ).filter((page: Page) => { const pageSettings: any = restrictPagesFetch.pageSettings[page.id]; return ( pageSettings?.restrict?.type && pageSettings?.restrict?.type !== 'none' ); }); console.log(restrictPages, 'restrinbed'); } }, [restrictPagesFetch, lockPagesEmpty]); if (!user) return null; const endIndex = currentPage * usersPerPage; const startIndex = endIndex - usersPerPage; const currentPageUsers = membershipUsers.slice(startIndex, endIndex); const nPages = Math.ceil(membershipUsers.length / usersPerPage); const pageNumbers = [...Array(nPages + 1).keys()].slice(1); const isFinalPage = pageNumbers[pageNumbers.length - 1] === currentPage; const nextPage = () => { if (currentPage !== nPages) setCurrentPage(currentPage + 1); }; const prevPage = () => { if (currentPage !== 1) setCurrentPage(currentPage - 1); }; const updateRestrictedPages = () => { fetchAndSetSitePageSettings(site.id).then((res) => { if (res) { setIsRestrictedPagesFetched(true); handleSetRestrictedPages(); } }); }; if(site?.pages && restrictPagesFetch?.pages){ console.log( Object.keys(site?.pages)?.length, Object.keys(restrictPagesFetch?.pages)?.length) } const handleChangeEvent = (e: ChangeEvent<HTMLInputElement>) => { const { name, value } = e.target; setMembershipUser((prev) => ({ ...prev, [name]: value, })); }; const segmentOptions = segments.map((item) => { return { name: item.name, value: item.id }; }); const handleSegmentSelection = (value: { name: string; value: string }[]) => { setSelectedSegment(value); setMembershipUser((prev) => ({ ...prev, allowedSegments: value.map((item) => item.value), })); }; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const isValidEmail = emailRegex.test(membershipUser.email); const saveUser = async () => { // check empty fields if (membershipUser.email.trim() === '') { setErrors((prev) => ({ ...prev, email: 'This field is required!', })); return; } if (membershipUser.name.trim() === '') { setErrors((prev) => ({ ...prev, name: 'This field is required!', })); return; } // check valid email if (!isValidEmail) { setErrors((prev) => ({ ...prev, email: 'Please enter a valid email!', })); return; } // check if email already exists const isEmailExists = membershipUsers.filter( (item) => item.email.toLowerCase() === membershipUser.email.trim().toLowerCase() ).length; if (isEmailExists) { setErrors((prev) => ({ ...prev, email: 'This email already exists!', })); return; } if (!membershipUser.allowedSegments.length) { setErrors((prev) => ({ ...prev, segment: 'Please select atleast one segment!', })); return; } setErrors({ email: '', name: '', segment: '', addSegment: '' }); if (site?.id) { const data = { siteId: site.id, action: 'post', ...membershipUser, }; setLoading(true); await membership(data) .then((res) => { setLoading(false); if (res.data.status === 201) { setMembershipUsers((prev) => [...prev, { ...res.data.data }]); handleModelClose(); addToast({ title: 'Membership user added', description: 'Membership user added successfully', type: 'success', }); } else { addToast({ title: 'Membership user add failed', description: res?.data?.details?.error || 'Membership user add failed. Please try again!', type: 'error', }); } }) .catch(() => { addToast({ title: 'Membership user add failed', description: 'Something went wrong. Please try again!', type: 'error', }); setLoading(false); }); } }; const deleteMember = async () => { setIsDeleting(true); const updatedMembershipUsers = membershipUsers.filter( (user) => user.email !== membershipUser.email ); if (site?.id) { const data = { siteId: site.id, action: 'delete', email: membershipUser.email, }; await membership(data) .then((res) => { if (res.data.status === 204) { handleModelClose(); setMembershipUsers(updatedMembershipUsers); addToast({ title: 'Membership user deleted', description: 'Membership user deleted successfully', type: 'success', }); } else { addToast({ title: 'Membership user deletion failed', description: res?.data?.details?.error || 'Membership user deletion failed. Please try again!', type: 'error', }); } }) .catch(() => { addToast({ title: 'Membership user deletion failed', description: 'Something went wrong. Please try again!', type: 'error', }); }); setIsDeleting(false); setUserShowDeleteModal(false); setLoading(false); } }; const handleMembershipUpdate = (member) => { setIsUpdate(true); setShowUserModal(true); if (member?.allowedSegments?.length) { setSelectedSegment( member?.allowedSegments ?.map((item) => { const segmentItem = segmentOptions.find( (segment) => segment.value === item ); return segmentItem; }) ?.filter(Boolean) ); } setMembershipUser({ name: member.name, email: member.email, allowedSegments: member.allowedSegments, }); setUserDoc(member.id); }; const updateUser = async () => { if (membershipUser.name.trim() === '') { setErrors((prev) => ({ ...prev, name: 'This field is required!', })); return; } if (!membershipUser.allowedSegments.length) { setErrors((prev) => ({ ...prev, segment: 'Please select atleast one segment!', })); return; } setErrors({ email: '', name: '', segment: '', addSegment: '' }); const data = { action: 'put', siteId: site.id, userId: userDoc, update: { name: membershipUser.name, allowedSegments: membershipUser.allowedSegments, }, }; setLoading(true); await membership(data) .then((res) => { setLoading(false); if (res.data.status === 200) { membershipUsers.map((item) => { if (item.id === userDoc) { item.name = membershipUser.name; item.allowedSegments = membershipUser.allowedSegments; } }); handleModelClose(); addToast({ title: 'Membership user updated', description: 'Membership user updated successfully', type: 'success', }); } else { addToast({ title: 'Membership user update failed', description: res?.data?.details?.error || 'Membership user update failed. Please try again!', type: 'error', }); } }) .catch(() => { setLoading(false); addToast({ title: 'Membership user update failed', description: 'Membership user update failed. Please try again!', type: 'error', }); }); }; const createNewSegment = (newSegment: string) => { if (newSegment.trim() === '') { setErrors((prev) => ({ ...prev, addSegment: 'Segment name required.', })); return; } const newSegmentData = { name: newSegment, id: v4(), isDefault: false, }; const isSegmentExists = segmentOptions.some( (segment) => segment.name.toLowerCase().trim() === newSegment.toLowerCase().trim() ); if (isSegmentExists) { setErrors((prev) => ({ ...prev, addSegment: 'Segment already exists.', })); return; } else { addMembershipSegment(site.id, newSegmentData); sortSegmentOptions([...segments, newSegmentData]); setNewSegment(''); return newSegmentData; } }; // console.log(site.pageSettings, site.pages) const addNewSegment = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); setErrors((prev) => ({ ...prev, addSegment: '', })); createNewSegment(newSegment); }; const handleModelClose = () => { setShowUserModal(false); setIsUpdate(false); setErrors({ email: '', name: '', segment: '', addSegment: '' }); setTimeout(() => { setMembershipUser({ name: '', email: '', allowedSegments: [siteDefaultSegment.id], }); setSelectedSegment([ { name: siteDefaultSegment.name, value: siteDefaultSegment.id, }, ]); }, 100); }; const getTime = (epoch: number) => { return epoch ? moment(epoch).format('ll') : '-'; }; const getSegmentNames = (segmentIds: string[]) => { const segmentNames = []; segmentIds?.map((segmentId) => { const segmentItem = segments.find((item) => item.id === segmentId); segmentItem && segmentNames.push(segmentItem.name); }); return segmentNames; }; const tableHeaders = [ 'name and email', // 'email', 'allowed segments', 'last login', 'created date', // 'signed up', 'edit', ]; const categories = [ { label: 'Protected pages', tab: ( <ProtectedPages pageSettings={site?.pageSettings} restrictPages={restrictPages} site={site} getSegmentNames={getSegmentNames} createNewSegment={createNewSegment} isRestrictedPagesFetched={isRestrictedPagesFetched} updateRestrictedPages={updateRestrictedPages} /> ), }, { label: 'Members', tab: ( <> <MembershipUsers segments={segments} site={site} setIsSegmentsFetched={setIsSegmentsFetched} membershipUsers={membershipUsers} setMembershipUsers={setMembershipUsers} setShowUserModal={setShowUserModal} /> <MembershipUserTable isMembershipFetched={isMembershipFetched} membershipUsers={membershipUsers} tableHeaders={tableHeaders} currentPageUsers={currentPageUsers} getSegmentNames={getSegmentNames} getTime={getTime} handleMembershipUpdate={handleMembershipUpdate} setMembershipUser={setMembershipUser} setUserShowDeleteModal={setUserShowDeleteModal} currentPage={currentPage} pageNumbers={pageNumbers} prevPage={prevPage} setCurrentPage={setCurrentPage} isFinalPage={isFinalPage} nextPage={nextPage} setShowUserModal={setShowUserModal} /> </> ), }, { label: 'Segments', tab: ( <MembershipSegments segments={segments} addNewSegment={addNewSegment} setNewSegment={setNewSegment} newSegment={newSegment} error={errors?.addSegment} siteId={siteId?.toString()} sortSegment={sortSegmentOptions} /> ), }, { label: 'Webhooks', tab: <Webhook site={site} siteId={siteId?.toString()} />, }, { label: 'Settings', tab: <InitialMembershipSettings setShowIcon={setShowClickIcon} />, }, ]; const isMembershipEnabled = site?.passwordProtection?.enabled; return ( <SiteLayout siteId={siteId?.toString()} site={site} page={{ title: 'Membership', slug: 'membership', }} > {!isSegmentsFetched && ( <div className="flex flex-col w-full items-center justify-center min-h-[100vh]"> <Spinner width="30" className="m-auto flex justify-center items-center w-6 h-6 mx-5 animate-spin text-royal-blue-600" /> </div> )} {/* <div className="mt-3"> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-20 my-5"> */} {isSegmentsFetched && ( <div className="mt-10"> <div className="max-w-6xl xl:max-w-5xl px-9 pt-5 pb-10 rounded-md mx-auto bg-white"> <Tab.Group onChange={(index) => { setCurrentTabIndex(index); if (index === 0) { setShowClickIcon(false); } }} selectedIndex={currentTabIndex} > <div className=""> <div className="border-b !focus-visible:none border-gray-200"> <Tab.List className={`-mb-px sm:flex ${ isMembershipEnabled ? 'space-x-8' : 'space-x-0' }`} > {categories.map((category, idx) => ( <Tab key={`${category.label}-${idx}`} className={({ selected }) => classNames( 'whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm', selected ? 'border-royal-blue-500 text-royal-blue-600' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700', isMembershipEnabled || category.label === 'Settings' ? 'block' : 'hidden' ) } {...(idx === 0 ? { 'data-tooltip-id': 'protected-pages-tooltip', 'data-tooltip-content': '✨ Click here to lock selected pages👇', } : {})} > <div className="flex"> {isMembershipEnabled && idx === 0 && showClickIcon && ( <div className="w-full flex justify-end"> <span className="relative flex h-4 w-4"> {idx === 0 && ( <Tooltip id="protected-pages-tooltip" className="bg-white text-gray-400 p-2 rounded shadow absolute mb-2" isOpen={isMembershipEnabled && idx === 0} ></Tooltip> )} </span> </div> )} {category.label}{' '} </div> </Tab> ))} </Tab.List> </div> <Tab.Panels> {categories.map((category, idx) => ( <Tab.Panel key={idx}>{category.tab}</Tab.Panel> ))} </Tab.Panels> </div> </Tab.Group> </div> </div> )} <Transition.Root show={showUserModal} as={Fragment}> <Dialog as="div" className="fixed z-10 inset-0 overflow-y-auto" onClose={handleModelClose} > <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <Transition.Child as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0" > <Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> </Transition.Child> <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true" > &#8203; </span> <Transition.Child as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enterTo="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > <div className="inline-block p-3 sm:p-10 align-bottom bg-gray-100 rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-xl md:max-w-2xl lg:max-w-3xl xl:max-w-4xl sm:w-full"> <div className="p-2 sm:p-8 bg-white rounded"> <h3 className="text-lg leading-6 font-medium text-gray-900 px-2 py-3 sm:p-0 sm:pb-5"> {isUpdate ? 'Edit User' : 'Add new user'} </h3> <div className="grid grid-cols-2 gap-6"> <div className="col-span-2 md:col-span-1 my-2"> <h2 className="text-sm pb-1 text-gray-600">Name</h2> <input type="text" name="name" placeholder="Enter Name" value={membershipUser.name} className="block w-full border border-gray-300 rounded-md shadow-sm py-2 pl-3 pr-9 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm" onChange={handleChangeEvent} /> {errors.name && ( <p className="mb-0 mt-1 text-sm text-red-600" id="url-error" > This field is required! </p> )} </div> <div className="col-span-2 md:col-span-1 my-2"> <h2 className="text-sm pb-1 text-gray-600">Email</h2> {isUpdate ? ( <p className="block w-full border turncate overflow-hidden truncate border-gray-300 rounded-md shadow-sm py-2 pl-3 pr-9 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm"> {membershipUser.email} </p> ) : ( <input type="text" name="email" placeholder="Enter Email" value={membershipUser.email} className="block w-full border border-gray-300 rounded-md shadow-sm py-2 pl-3 pr-9 focus:outline-none focus:ring-gray-900 focus:border-gray-900 sm:text-sm" onChange={handleChangeEvent} /> )} {errors.email && ( <p className="mb-0 mt-1 text-sm text-red-600"> {errors.email} </p> )} </div> <div className="col-span-2 md:col-span-1 my-2"> <h2 className="text-sm pb-1 text-gray-600"> Allowed Segments </h2> <MultiSelect selected={selectedSegment} setSelected={handleSegmentSelection} options={segmentOptions} menuHeightClass="max-h-96" emptyStateMsg="Select Segment" /> {errors.segment && ( <p className="mb-0 mt-1 text-sm text-red-600"> {errors.segment} </p> )} {/* <Select options={segments} selected={membershipUser.catagorie} setSelected={(value) => handleCatagories(value)} /> */} </div> <div className="col-span-2 flex gap-4 sm:place-self-end sm:w-1/7 w-full justify-between items-end"> {isUpdate ? ( <button className="flex items-center h-fit justify-center px-3 py-2 text-sm text-white transition duration-150 ease-in-out bg-red-600 border border-transparent rounded-md hover:bg-red-500 focus:outline-none focus:border-red-700 focus:shadow-outline-red sm:text-sm sm:leading-5" onClick={() => { setUserShowDeleteModal(true); setShowUserModal(false); }} > Delete </button> ) : ( <div></div> )} <div className="col-span-2 flex gap-4 sm:place-self-end sm:w-1/7"> <button onClick={handleModelClose} className="sm:mt-2 w-fit flex justify-end py-2 px-4 border border-gray-5 text-sm font-medium rounded-md text-gray-800 bg-gray-50 focus:outline-none focus:border-gray-600 focus:shadow-outline-royal-blue transition duration-150 ease-in-out disabled:opacity-75 disabled:bg-royal-blue-600" > Cancel </button> <button onClick={isUpdate ? updateUser : saveUser} className="sm:mt-2 w-fit flex justify-end py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-royal-blue-600 hover:bg-royal-blue-500 focus:outline-none focus:border-royal-blue-600 focus:shadow-outline-royal-blue active:bg-royal-blue-700 transition duration-150 ease-in-out disabled:opacity-75 disabled:bg-royal-blue-600" disabled={loading} > {loading ? ( <Spinner width="10" className="m-auto w-4 h-4 mx-5 animate-spin text-white" /> ) : isUpdate ? ( 'Update Member' ) : ( 'Add Member' )} </button> </div> </div> </div> </div> </div> </Transition.Child> </div> </Dialog> </Transition.Root> {showUserDeleteModal && ( <ConfirmModal closeModal={() => { setMembershipUser({ name: '', email: '', allowedSegments: [siteDefaultSegment.id], }); setUserShowDeleteModal(false); }} title={`Delete ${membershipUser.name} ?`} text={`${membershipUser.name} will be removed permanently from this site.`} confirmText={isDeleting ? 'Deleting...' : 'Delete member'} confirmAction={deleteMember} isLoading={isDeleting} /> )} </SiteLayout> ); }; export default UserDetails;