serpbear/components/keywords/KeywordsTable.tsx
towfiqi 2a1fc0e43d feat: Adds keyword search volume data feature for tracked keywords.
- Adds a volume field in the keyword table.
- Adds a button in the Adwords Integration screen to update all the tracked keywords.
- When a new keyword is added, the volume data is automatically fetched.
- Adds ability to sort keywords based on search volume.
2024-03-01 10:52:45 +06:00

264 lines
14 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { Toaster } from 'react-hot-toast';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { filterKeywords, keywordsByDevice, sortKeywords } from '../../utils/client/sortFilter';
import Icon from '../common/Icon';
import Keyword from './Keyword';
import KeywordDetails from './KeywordDetails';
import KeywordFilters from './KeywordFilter';
import Modal from '../common/Modal';
import { useDeleteKeywords, useFavKeywords, useRefreshKeywords } from '../../services/keywords';
import KeywordTagManager from './KeywordTagManager';
import AddTags from './AddTags';
import useWindowResize from '../../hooks/useWindowResize';
import useIsMobile from '../../hooks/useIsMobile';
type KeywordsTableProps = {
domain: DomainType | null,
keywords: KeywordType[],
isLoading: boolean,
showAddModal: boolean,
setShowAddModal: Function,
isConsoleIntegrated: boolean,
}
const KeywordsTable = (props: KeywordsTableProps) => {
const { keywords = [], isLoading = true, isConsoleIntegrated = false } = props;
const showSCData = isConsoleIntegrated;
const [device, setDevice] = useState<string>('desktop');
const [selectedKeywords, setSelectedKeywords] = useState<number[]>([]);
const [showKeyDetails, setShowKeyDetails] = useState<KeywordType|null>(null);
const [showRemoveModal, setShowRemoveModal] = useState<boolean>(false);
const [showTagManager, setShowTagManager] = useState<null|number>(null);
const [showAddTags, setShowAddTags] = useState<boolean>(false);
const [SCListHeight, setSCListHeight] = useState(500);
const [filterParams, setFilterParams] = useState<KeywordFilters>({ countries: [], tags: [], search: '' });
const [sortBy, setSortBy] = useState<string>('date_asc');
const [scDataType, setScDataType] = useState<string>('threeDays');
const [showScDataTypes, setShowScDataTypes] = useState<boolean>(false);
const { mutate: deleteMutate } = useDeleteKeywords(() => {});
const { mutate: favoriteMutate } = useFavKeywords(() => {});
const { mutate: refreshMutate } = useRefreshKeywords(() => {});
const [isMobile] = useIsMobile();
useWindowResize(() => setSCListHeight(window.innerHeight - (isMobile ? 200 : 400)));
const scDataObject:{ [k:string] : string} = {
threeDays: 'Last Three Days',
sevenDays: 'Last Seven Days',
thirtyDays: 'Last Thirty Days',
avgThreeDays: 'Last Three Days Avg',
avgSevenDays: 'Last Seven Days Avg',
avgThirtyDays: 'Last Thirty Days Avg',
};
const processedKeywords: {[key:string] : KeywordType[]} = useMemo(() => {
const procKeywords = keywords.filter((x) => x.device === device);
const filteredKeywords = filterKeywords(procKeywords, filterParams);
const sortedKeywords = sortKeywords(filteredKeywords, sortBy, scDataType);
return keywordsByDevice(sortedKeywords, device);
}, [keywords, device, sortBy, filterParams, scDataType]);
const allDomainTags: string[] = useMemo(() => {
const allTags = keywords.reduce((acc: string[], keyword) => [...acc, ...keyword.tags], []);
return [...new Set(allTags)];
}, [keywords]);
const selectKeyword = (keywordID: number) => {
console.log('Select Keyword: ', keywordID);
let updatedSelectd = [...selectedKeywords, keywordID];
if (selectedKeywords.includes(keywordID)) {
updatedSelectd = selectedKeywords.filter((keyID) => keyID !== keywordID);
}
setSelectedKeywords(updatedSelectd);
};
const Row = ({ data, index, style }:ListChildComponentProps) => {
const keyword = data[index];
return (
<Keyword
key={keyword.ID}
style={style}
index={index}
selected={selectedKeywords.includes(keyword.ID)}
selectKeyword={selectKeyword}
keywordData={keyword}
refreshkeyword={() => refreshMutate({ ids: [keyword.ID] })}
favoriteKeyword={favoriteMutate}
manageTags={() => setShowTagManager(keyword.ID)}
removeKeyword={() => { setSelectedKeywords([keyword.ID]); setShowRemoveModal(true); }}
showKeywordDetails={() => setShowKeyDetails(keyword)}
lastItem={index === (processedKeywords[device].length - 1)}
showSCData={showSCData}
scDataType={scDataType}
/>
);
};
const selectedAllItems = selectedKeywords.length === processedKeywords[device].length;
return (
<div>
<div className='domKeywords flex flex-col bg-[white] rounded-md text-sm border mb-8'>
{selectedKeywords.length > 0 && (
<div className='font-semibold text-sm py-4 px-8 text-gray-500 '>
<ul className=''>
<li className='inline-block mr-4'>
<a
className='block px-2 py-2 cursor-pointer hover:text-indigo-600'
onClick={() => { refreshMutate({ ids: selectedKeywords }); setSelectedKeywords([]); }}
>
<span className=' bg-indigo-100 text-blue-700 px-1 rounded'><Icon type="reload" size={11} /></span> Refresh Keywords
</a>
</li>
<li className='inline-block mr-4'>
<a
className='block px-2 py-2 cursor-pointer hover:text-indigo-600'
onClick={() => setShowRemoveModal(true)}
>
<span className=' bg-red-100 text-red-600 px-1 rounded'><Icon type="trash" size={14} /></span> Remove Keywords</a>
</li>
<li className='inline-block mr-4'>
<a
className='block px-2 py-2 cursor-pointer hover:text-indigo-600'
onClick={() => setShowAddTags(true)}
>
<span className=' bg-green-100 text-green-500 px-1 rounded'><Icon type="tags" size={14} /></span> Tag Keywords</a>
</li>
</ul>
</div>
)}
{selectedKeywords.length === 0 && (
<KeywordFilters
allTags={allDomainTags}
filterParams={filterParams}
filterKeywords={(params:KeywordFilters) => setFilterParams(params)}
updateSort={(sorted:string) => setSortBy(sorted)}
sortBy={sortBy}
keywords={keywords}
device={device}
setDevice={setDevice}
integratedConsole={isConsoleIntegrated}
/>
)}
<div className={`domkeywordsTable domkeywordsTable--keywords ${showSCData ? 'domkeywordsTable--hasSC' : ''}
styled-scrollbar w-full overflow-auto min-h-[60vh]`}>
<div className=' lg:min-w-[800px]'>
<div className={`domKeywords_head domKeywords_head--${sortBy} hidden lg:flex p-3 px-6 bg-[#FCFCFF]
text-gray-600 justify-between items-center font-semibold border-y`}>
<span className='domKeywords_head_keyword flex-1 basis-[4rem] w-auto '>
{processedKeywords[device].length > 0 && (
<button
className={`p-0 mr-2 leading-[0px] inline-block rounded-sm pt-0 px-[1px] pb-[3px] border border-slate-300
${selectedAllItems ? ' bg-blue-700 border-blue-700 text-white' : 'text-transparent'}`}
onClick={() => setSelectedKeywords(selectedAllItems ? [] : processedKeywords[device].map((k: KeywordType) => k.ID))}
>
<Icon type="check" size={10} />
</button>
)}
Keyword
</span>
<span className='domKeywords_head_position flex-1 basis-24 grow-0 text-center'>Position</span>
<span className='domKeywords_head_best flex-1 basis-16 grow-0 text-center'>Best</span>
<span className='domKeywords_head_history flex-1 basis-20 grow-0'>History (7d)</span>
<span className='domKeywords_head_volume flex-1 basis-24 grow-0 text-center'>Volume</span>
<span className='domKeywords_head_url flex-1'>URL</span>
<span className='domKeywords_head_updated flex-1 relative left-3'>Updated</span>
{showSCData && (
<div className='domKeywords_head_sc flex-1 min-w-[170px] mr-7 text-center'>
{/* Search Console */}
<div>
<div
className=' w-48 select-none cursor-pointer absolute bg-white rounded-full
px-2 py-[2px] mt-[-22px] ml-3 border border-gray-200 z-40'
onClick={() => setShowScDataTypes(!showScDataTypes)}>
<Icon type="google" size={13} /> {scDataObject[scDataType]}
<Icon classes="ml-2" type={showScDataTypes ? 'caret-up' : 'caret-down'} size={10} />
</div>
{showScDataTypes && (
<div className='absolute bg-white border border-gray-200 z-50 w-44 rounded mt-2 ml-5 text-gray-500'>
{Object.keys(scDataObject).map((itemKey) => {
return <span
className={`block p-2 cursor-pointer hover:bg-indigo-50 hover:text-indigo-600
${scDataType === itemKey ? 'bg-indigo-100 text-indigo-600' : ''}`}
key={itemKey}
onClick={() => { setScDataType(itemKey); setShowScDataTypes(false); }}>
{scDataObject[itemKey]}
</span>;
})}
</div>
)}
</div>
<div className='relative top-2 flex justify-between'>
<span className='min-w-[40px]'>Pos</span>
<span className='min-w-[40px]'>Imp</span>
<span className='min-w-[40px]'>Visits</span>
{/* <span>CTR</span> */}
</div>
</div>
)}
</div>
<div className='domKeywords_keywords border-gray-200 min-h-[55vh] relative'>
{processedKeywords[device] && processedKeywords[device].length > 0 && (
<List
innerElementType="div"
itemData={processedKeywords[device]}
itemCount={processedKeywords[device].length}
itemSize={isMobile ? 146 : 57}
height={SCListHeight}
width={'100%'}
className={'styled-scrollbar'}
>
{Row}
</List>
)}
{!isLoading && processedKeywords[device].length === 0 && (
<p className=' p-9 pt-[10%] text-center text-gray-500'>No Keywords Added for this Device Type.</p>
)}
{isLoading && (
<p className=' p-9 pt-[10%] text-center text-gray-500'>Loading Keywords...</p>
)}
</div>
</div>
</div>
</div>
{showKeyDetails && showKeyDetails.ID && (
<KeywordDetails keyword={showKeyDetails} closeDetails={() => setShowKeyDetails(null)} />
)}
{showRemoveModal && selectedKeywords.length > 0 && (
<Modal closeModal={() => { setSelectedKeywords([]); setShowRemoveModal(false); }} title={'Remove Keywords'}>
<div className='text-sm'>
<p>Are you sure you want to remove {selectedKeywords.length > 1 ? 'these' : 'this'} Keyword?</p>
<div className='mt-6 text-right font-semibold'>
<button
className=' py-1 px-5 rounded cursor-pointer bg-indigo-50 text-slate-500 mr-3'
onClick={() => { setSelectedKeywords([]); setShowRemoveModal(false); }}>
Cancel
</button>
<button
className=' py-1 px-5 rounded cursor-pointer bg-red-400 text-white'
onClick={() => { deleteMutate(selectedKeywords); setShowRemoveModal(false); setSelectedKeywords([]); }}>
Remove
</button>
</div>
</div>
</Modal>
)}
{showTagManager && (
<KeywordTagManager
allTags={allDomainTags}
keyword={keywords.find((k) => k.ID === showTagManager)}
closeModal={() => setShowTagManager(null)}
/>
)}
{showAddTags && (
<AddTags
keywords={keywords.filter((k) => selectedKeywords.includes(k.ID))}
closeModal={() => setShowAddTags(false)}
/>
)}
<Toaster position='bottom-center' containerClassName="react_toaster" />
</div>
);
};
export default KeywordsTable;