mirror of
https://github.com/towfiqi/serpbear
synced 2025-06-26 18:15:54 +00:00
196 lines
10 KiB
TypeScript
196 lines
10 KiB
TypeScript
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
import Icon from '../common/Icon';
|
|
import Modal from '../common/Modal';
|
|
import SelectField from '../common/SelectField';
|
|
import countries from '../../utils/countries';
|
|
import { useAddKeywords } from '../../services/keywords';
|
|
|
|
type AddKeywordsProps = {
|
|
keywords: KeywordType[],
|
|
scraperName: string,
|
|
allowsCity: boolean,
|
|
closeModal: Function,
|
|
domain: string
|
|
}
|
|
|
|
type KeywordsInput = {
|
|
keywords: string,
|
|
device: string,
|
|
country: string,
|
|
domain: string,
|
|
tags: string,
|
|
city?:string,
|
|
}
|
|
|
|
const AddKeywords = ({ closeModal, domain, keywords, scraperName = '', allowsCity = false }: AddKeywordsProps) => {
|
|
const inputRef = useRef(null);
|
|
const defCountry = localStorage.getItem('default_country') || 'US';
|
|
|
|
const [error, setError] = useState<string>('');
|
|
const [showTagSuggestions, setShowTagSuggestions] = useState(false);
|
|
const [newKeywordsData, setNewKeywordsData] = useState<KeywordsInput>({ keywords: '', device: 'desktop', country: defCountry, domain, tags: '' });
|
|
const { mutate: addMutate, isLoading: isAdding } = useAddKeywords(() => closeModal(false));
|
|
|
|
const existingTags: string[] = useMemo(() => {
|
|
const allTags = keywords.reduce((acc: string[], keyword) => [...acc, ...keyword.tags], []).filter((t) => t && t.trim() !== '');
|
|
return [...new Set(allTags)];
|
|
}, [keywords]);
|
|
|
|
const setDeviceType = useCallback((input:string) => {
|
|
let updatedDevice = '';
|
|
if (newKeywordsData.device.includes(input)) {
|
|
updatedDevice = newKeywordsData.device.replace(',', '').replace(input, '');
|
|
} else {
|
|
updatedDevice = newKeywordsData.device ? `${newKeywordsData.device},${input}` : input;
|
|
}
|
|
setNewKeywordsData({ ...newKeywordsData, device: updatedDevice });
|
|
}, [newKeywordsData]);
|
|
|
|
const addKeywords = () => {
|
|
const nkwrds = newKeywordsData;
|
|
if (nkwrds.keywords) {
|
|
const devices = nkwrds.device.split(',');
|
|
const multiDevice = nkwrds.device.includes(',') && devices.length > 1;
|
|
const keywordsArray = [...new Set(nkwrds.keywords.split('\n').map((item) => item.trim()).filter((item) => !!item))];
|
|
const currentKeywords = keywords.map((k) => `${k.keyword}-${k.device}-${k.country}${k.city ? `-${k.city}` : ''}`);
|
|
|
|
const keywordExist = keywordsArray.filter((k) =>
|
|
devices.some((device) => currentKeywords.includes(`${k}-${device}-${nkwrds.country}${nkwrds.city ? `-${nkwrds.city}` : ''}`)),
|
|
);
|
|
|
|
if (!multiDevice && (keywordsArray.length === 1 || currentKeywords.length === keywordExist.length) && keywordExist.length > 0) {
|
|
setError(`Keywords ${keywordExist.join(',')} already Exist`);
|
|
setTimeout(() => { setError(''); }, 3000);
|
|
} else {
|
|
const newKeywords = keywordsArray.flatMap((k) =>
|
|
devices.filter((device) =>
|
|
!currentKeywords.includes(`${k}-${device}-${nkwrds.country}${nkwrds.city ? `-${nkwrds.city}` : ''}`),
|
|
).map((device) => ({
|
|
keyword: k,
|
|
device,
|
|
country: nkwrds.country,
|
|
domain: nkwrds.domain,
|
|
tags: nkwrds.tags,
|
|
city: nkwrds.city,
|
|
})),
|
|
);
|
|
addMutate(newKeywords);
|
|
}
|
|
} else {
|
|
setError('Please Insert a Keyword');
|
|
setTimeout(() => { setError(''); }, 3000);
|
|
}
|
|
};
|
|
|
|
const deviceTabStyle = 'cursor-pointer px-2 py-2 rounded';
|
|
|
|
return (
|
|
<Modal closeModal={() => { closeModal(false); }} title={'Add New Keywords'} width="[420px]">
|
|
<div data-testid="addkeywords_modal">
|
|
<div>
|
|
<div>
|
|
<textarea
|
|
className='w-full h-40 border rounded border-gray-200 p-4 outline-none focus:border-indigo-300'
|
|
placeholder="Type or Paste Keywords here. Insert Each keyword in a New line."
|
|
value={newKeywordsData.keywords}
|
|
onChange={(e) => setNewKeywordsData({ ...newKeywordsData, keywords: e.target.value })}>
|
|
</textarea>
|
|
</div>
|
|
|
|
<div className=' my-3 flex justify-between text-sm'>
|
|
<div>
|
|
<SelectField
|
|
multiple={false}
|
|
selected={[newKeywordsData.country]}
|
|
options={Object.keys(countries).map((countryISO:string) => { return { label: countries[countryISO][0], value: countryISO }; })}
|
|
defaultLabel='All Countries'
|
|
updateField={(updated:string[]) => {
|
|
setNewKeywordsData({ ...newKeywordsData, country: updated[0] });
|
|
localStorage.setItem('default_country', updated[0]);
|
|
}}
|
|
rounded='rounded'
|
|
maxHeight={48}
|
|
flags={true}
|
|
/>
|
|
</div>
|
|
<ul className='flex text-xs font-semibold text-gray-500'>
|
|
<li
|
|
className={`${deviceTabStyle} mr-2 ${newKeywordsData.device.includes('desktop') ? ' bg-indigo-50 text-indigo-700' : ''}`}
|
|
onClick={() => setDeviceType('desktop')}>
|
|
<Icon type='desktop' classes={'top-[3px]'} size={15} /> <i className='not-italic hidden lg:inline-block'>Desktop</i>
|
|
<Icon type='check' classes={'pl-1'} size={12} color={newKeywordsData.device.includes('desktop') ? '#4338ca' : '#bbb'} />
|
|
</li>
|
|
<li
|
|
className={`${deviceTabStyle} ${newKeywordsData.device.includes('mobile') ? ' bg-indigo-50 text-indigo-700' : ''}`}
|
|
onClick={() => setDeviceType('mobile')}>
|
|
<Icon type='mobile' /> <i className='not-italic hidden lg:inline-block'>Mobile</i>
|
|
<Icon type='check' classes={'pl-1'} size={12} color={newKeywordsData.device.includes('mobile') ? '#4338ca' : '#bbb'} />
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div className='relative'>
|
|
<input
|
|
className='w-full border rounded border-gray-200 py-2 px-4 pl-12 outline-none focus:border-indigo-300'
|
|
placeholder='Insert Tags (Optional)'
|
|
value={newKeywordsData.tags}
|
|
onChange={(e) => setNewKeywordsData({ ...newKeywordsData, tags: e.target.value })}
|
|
/>
|
|
<span className='absolute text-gray-400 top-3 left-2 cursor-pointer' onClick={() => setShowTagSuggestions(!showTagSuggestions)}>
|
|
<Icon type="tags" size={16} color={showTagSuggestions ? '#777' : '#aaa'} />
|
|
<Icon type={showTagSuggestions ? 'caret-up' : 'caret-down'} size={14} color={showTagSuggestions ? '#666' : '#aaa'} />
|
|
</span>
|
|
{showTagSuggestions && (
|
|
<ul className={`absolute z-50
|
|
bg-white border border-t-0 border-gray-200 rounded rounded-t-none w-full`}>
|
|
{existingTags.length > 0 && existingTags.map((tag, index) => {
|
|
return newKeywordsData.tags.split(',').map((t) => t.trim()).includes(tag) === false && <li
|
|
className=' p-2 cursor-pointer hover:text-indigo-600 hover:bg-indigo-50 transition'
|
|
key={index}
|
|
onClick={() => {
|
|
const tagInput = newKeywordsData.tags;
|
|
// eslint-disable-next-line no-nested-ternary
|
|
const tagToInsert = tagInput + (tagInput.trim().slice(-1) === ',' ? '' : (tagInput.trim() ? ', ' : '')) + tag;
|
|
setNewKeywordsData({ ...newKeywordsData, tags: tagToInsert });
|
|
setShowTagSuggestions(false);
|
|
if (inputRef?.current) (inputRef.current as HTMLInputElement).focus();
|
|
}}>
|
|
<Icon type='tags' size={14} color='#bbb' /> {tag}
|
|
</li>;
|
|
})}
|
|
{existingTags.length === 0 && <p>No Existing Tags Found... </p>}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
<div className='relative mt-2'>
|
|
<input
|
|
className={`w-full border rounded border-gray-200 py-2 px-4 pl-8
|
|
outline-none focus:border-indigo-300 ${!allowsCity ? ' cursor-not-allowed' : ''} `}
|
|
disabled={!allowsCity}
|
|
title={!allowsCity ? `Your scraper ${scraperName} doesn't have city level scraping feature.` : ''}
|
|
placeholder={`City (Optional)${!allowsCity ? `. Not avaialable for ${scraperName}.` : ''}`}
|
|
value={newKeywordsData.city}
|
|
onChange={(e) => setNewKeywordsData({ ...newKeywordsData, city: e.target.value })}
|
|
/>
|
|
<span className='absolute text-gray-400 top-2 left-2'><Icon type="city" size={16} /></span>
|
|
</div>
|
|
</div>
|
|
{error && <div className='w-full mt-4 p-3 text-sm bg-red-50 text-red-700'>{error}</div>}
|
|
<div className='mt-6 text-right text-sm font-semibold flex justify-between'>
|
|
<button
|
|
className=' py-2 px-5 rounded cursor-pointer bg-indigo-50 text-slate-500 mr-3'
|
|
onClick={() => closeModal(false)}>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
className=' py-2 px-5 rounded cursor-pointer bg-blue-700 text-white'
|
|
onClick={() => !isAdding && addKeywords()}>
|
|
{isAdding ? 'Adding....' : 'Add Keywords'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default AddKeywords;
|