mirror of
https://github.com/towfiqi/serpbear
synced 2025-06-26 18:15:54 +00:00
feat: Adds a Keyword Research Section.
- Adds a /research page to the app that lets users generate keyword ideas based on given keywords. - Allows the ability to export keywords.
This commit is contained in:
parent
5650645b58
commit
4d15989b28
@ -294,6 +294,20 @@ const Icon = ({ type, color = 'currentColor', size = 16, title = '', classes = '
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
{type === 'research'
|
||||
&& <svg width={size} viewBox="0 0 48 48" {...xmlnsProps}>
|
||||
<g fill="none" stroke={color} strokeWidth={4}>
|
||||
<path strokeLinecap="round" d="M4 7h40M4 23h11M4 39h11"></path>
|
||||
<path d="M31.5 34a8.5 8.5 0 1 0 0-17a8.5 8.5 0 0 0 0 17Z"></path>
|
||||
<path strokeLinecap="round" d="m37 32l7 7.05"></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
{type === 'domains'
|
||||
&& <svg {...xmlnsProps} width={size} viewBox="0 0 56 56">
|
||||
<path fill={color} d="M7.328 43.504c.445 0 .914-.14 1.383-.469V17.957c0-.844.164-1.172.914-1.57L31.352 3.87c.07-1.547-.915-2.508-2.25-2.508c-.61 0-1.266.164-1.97.586L7.493 13.246c-2.297 1.336-2.578 1.758-2.578 4.43v22.43c0 2.015.96 3.398 2.414 3.398m9.375 5.414c.422 0 .89-.14 1.383-.469V23.371c0-.914.117-1.148.89-1.57L40.703 9.26c.07-1.523-.89-2.507-2.25-2.507c-.586 0-1.266.187-1.945.562L16.82 18.636c-2.297 1.313-2.555 1.805-2.555 4.43V45.52c0 2.015 1.008 3.398 2.438 3.398m10.031 5.719c.82 0 1.805-.328 2.977-.985l18.375-10.547c2.156-1.242 3-2.53 3-5.156l-.047-21.234c0-2.813-1.008-4.242-2.766-4.242c-.773 0-1.758.304-2.859.937L26.992 24.027c-2.203 1.29-2.977 2.602-2.977 5.157v21.234c0 2.719.961 4.219 2.72 4.219M28 50.067c-.117-.024-.164-.094-.164-.258L28 29.254c0-.89.258-1.36 1.055-1.805l17.742-10.43c.07-.046.14-.046.234-.023c.094.024.164.094.164.258l-.07 20.625c0 .773-.281 1.36-1.055 1.828L28.234 50.043a.284.284 0 0 1-.234.023"></path>
|
||||
</svg>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ type SelectFieldProps = {
|
||||
maxHeight?: number|string,
|
||||
rounded?: string,
|
||||
flags?: boolean,
|
||||
inline?: boolean,
|
||||
emptyMsg?: string
|
||||
}
|
||||
const SelectField = (props: SelectFieldProps) => {
|
||||
@ -30,6 +31,7 @@ const SelectField = (props: SelectFieldProps) => {
|
||||
maxHeight = 96,
|
||||
fullWidth = false,
|
||||
rounded = 'rounded-3xl',
|
||||
inline = false,
|
||||
flags = false,
|
||||
label = '',
|
||||
emptyMsg = '' } = props;
|
||||
@ -70,7 +72,7 @@ const SelectField = (props: SelectFieldProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="select font-semibold text-gray-500 relative flex justify-between items-center">
|
||||
<div className={`select font-semibold text-gray-500 relative ${inline ? 'inline-block' : 'flex'} justify-between items-center"`}>
|
||||
{label && <label className='mb-2 font-semibold inline-block text-sm text-gray-700 capitalize'>{label}</label>}
|
||||
<div
|
||||
className={`selected flex border ${rounded} p-1.5 px-4 cursor-pointer select-none ${fullWidth ? 'w-full' : 'w-[210px]'}
|
||||
@ -104,7 +106,7 @@ const SelectField = (props: SelectFieldProps) => {
|
||||
return (
|
||||
<li
|
||||
key={opt.value}
|
||||
className={`select-none cursor-pointer px-3 py-2 hover:bg-[#FCFCFF] capitalize
|
||||
className={`select-none cursor-pointer px-3 py-2 hover:bg-[#FCFCFF] capitalize text-ellipsis overflow-hidden
|
||||
${itemActive ? ' bg-indigo-50 text-indigo-600 hover:bg-indigo-50' : ''} `}
|
||||
onClick={() => selectItem(opt)}
|
||||
>
|
||||
|
@ -37,7 +37,7 @@ const TopBar = ({ showSettings, showAddModal }:TopbarProps) => {
|
||||
<span className=' relative top-[3px] mr-1'><Icon type="logo" size={24} color="#364AFF" /></span> SerpBear
|
||||
<button className='px-3 py-1 font-bold text-blue-700 lg:hidden ml-3 text-lg' onClick={() => showAddModal()}>+</button>
|
||||
</h3>
|
||||
{!isDomainsPage && (
|
||||
{!isDomainsPage && router.asPath !== '/research' && (
|
||||
<Link href={'/domains'} passHref={true}>
|
||||
<a className=' right-14 top-2 px-2 py-1 cursor-pointer bg-[#ecf2ff] hover:bg-indigo-100 transition-all
|
||||
absolute lg:top-3 lg:right-auto lg:left-8 lg:px-3 lg:py-2 rounded-full'>
|
||||
@ -52,16 +52,30 @@ const TopBar = ({ showSettings, showAddModal }:TopbarProps) => {
|
||||
<ul
|
||||
className={`text-sm font-semibold text-gray-500 absolute mt-[-10px] right-3 bg-white
|
||||
border border-gray-200 lg:mt-2 lg:relative lg:block lg:border-0 lg:bg-transparent ${showMobileMenu ? 'block' : 'hidden'}`}>
|
||||
<li className='block lg:inline-block lg:ml-5'>
|
||||
<a className='block px-3 py-2 cursor-pointer' href='https://docs.serpbear.com/' target="_blank" rel='noreferrer'>
|
||||
<Icon type="question" color={'#888'} size={14} /> Help
|
||||
</a>
|
||||
<li className={`block lg:inline-block lg:ml-5 ${router.asPath === '/domains' ? ' text-blue-700' : ''}`}>
|
||||
<Link href={'/domains'} passHref={true}>
|
||||
<a className='block px-3 py-2 cursor-pointer'>
|
||||
<Icon type="domains" color={router.asPath === '/domains' ? '#1d4ed8' : '#888'} size={14} /> Domains
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={`block lg:inline-block lg:ml-5 ${router.asPath === '/research' ? ' text-blue-700' : ''}`}>
|
||||
<Link href={'/research'} passHref={true}>
|
||||
<a className='block px-3 py-2 cursor-pointer'>
|
||||
<Icon type="research" color={router.asPath === '/research' ? '#1d4ed8' : '#888'} size={14} /> Research
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className='block lg:inline-block lg:ml-5'>
|
||||
<a className='block px-3 py-2 cursor-pointer' onClick={() => showSettings()}>
|
||||
<Icon type="settings-alt" color={'#888'} size={14} /> Settings
|
||||
</a>
|
||||
</li>
|
||||
<li className='block lg:inline-block lg:ml-5'>
|
||||
<a className='block px-3 py-2 cursor-pointer' href='https://docs.serpbear.com/' target="_blank" rel='noreferrer'>
|
||||
<Icon type="question" color={'#888'} size={14} /> Help
|
||||
</a>
|
||||
</li>
|
||||
<li className='block lg:inline-block lg:ml-5'>
|
||||
<a className='block px-3 py-2 cursor-pointer' onClick={() => logoutUser()}>
|
||||
<Icon type="logout" color={'#888'} size={14} /> Logout
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { useQuery } from 'react-query';
|
||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import { useAddKeywords } from '../../services/keywords';
|
||||
import Icon from '../common/Icon';
|
||||
@ -11,6 +12,8 @@ import { IdeasSortKeywords, IdeasfilterKeywords } from '../../utils/client/Ideas
|
||||
import IdeasFilters from './IdeasFilter';
|
||||
import { useMutateFavKeywordIdeas } from '../../services/adwords';
|
||||
import IdeaDetails from './IdeaDetails';
|
||||
import { fetchDomains } from '../../services/domains';
|
||||
import SelectField from '../common/SelectField';
|
||||
|
||||
type IdeasKeywordsTableProps = {
|
||||
domain: DomainType | null,
|
||||
@ -33,9 +36,15 @@ const IdeasKeywordsTable = ({
|
||||
const [sortBy, setSortBy] = useState<string>('imp_desc');
|
||||
const [listHeight, setListHeight] = useState(500);
|
||||
const [addKeywordDevice, setAddKeywordDevice] = useState<'desktop'|'mobile'>('desktop');
|
||||
const [addKeywordDomain, setAddKeywordDomain] = useState('');
|
||||
const { mutate: addKeywords } = useAddKeywords(() => { if (domain && domain.slug) router.push(`/domain/${domain.slug}`); });
|
||||
const { mutate: faveKeyword, isLoading: isFaving } = useMutateFavKeywordIdeas(router);
|
||||
const [isMobile] = useIsMobile();
|
||||
const isResearchPage = router.pathname === '/research';
|
||||
|
||||
const { data: domainsData } = useQuery('domains', () => fetchDomains(router, false), { enabled: selectedKeywords.length > 0, retry: false });
|
||||
const theDomains: DomainType[] = (domainsData && domainsData.domains) || [];
|
||||
|
||||
useWindowResize(() => setListHeight(window.innerHeight - (isMobile ? 200 : 400)));
|
||||
|
||||
const finalKeywords: IdeaKeyword[] = useMemo(() => {
|
||||
@ -78,7 +87,7 @@ const IdeasKeywordsTable = ({
|
||||
|
||||
const favoriteKeyword = (keywordID: string) => {
|
||||
if (!isFaving) {
|
||||
faveKeyword({ keywordID, domain: domain?.slug });
|
||||
faveKeyword({ keywordID, domain: isResearchPage ? 'research' : domain?.slug });
|
||||
}
|
||||
};
|
||||
|
||||
@ -87,7 +96,13 @@ const IdeasKeywordsTable = ({
|
||||
keywords.forEach((kitem:IdeaKeyword) => {
|
||||
if (selectedKeywords.includes(kitem.uid)) {
|
||||
const { keyword, country } = kitem;
|
||||
selectedkeywords.push({ keyword, device: addKeywordDevice, country, domain: domain?.domain || '', tags: '' });
|
||||
selectedkeywords.push({
|
||||
keyword,
|
||||
device: addKeywordDevice,
|
||||
country,
|
||||
domain: isResearchPage ? addKeywordDomain : (domain?.domain || ''),
|
||||
tags: '',
|
||||
});
|
||||
}
|
||||
});
|
||||
addKeywords(selectedkeywords);
|
||||
@ -118,7 +133,19 @@ const IdeasKeywordsTable = ({
|
||||
<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 '>
|
||||
<div className='inline-block'>Add Keywords to Tracker</div>
|
||||
<div className={`inline-block ${isResearchPage ? ' mr-2' : ''}`}>Add Keywords to Tracker</div>
|
||||
{isResearchPage && (
|
||||
<SelectField
|
||||
selected={[]}
|
||||
options={theDomains.map((d) => ({ label: d.domain, value: d.domain }))}
|
||||
defaultLabel={'Select a Domain'}
|
||||
updateField={(updated:string[]) => updated[0] && setAddKeywordDomain(updated[0])}
|
||||
emptyMsg="No Domains Found"
|
||||
multiple={false}
|
||||
inline={true}
|
||||
rounded='rounded'
|
||||
/>
|
||||
)}
|
||||
<div className='inline-block ml-2'>
|
||||
<button
|
||||
className={`inline-block px-2 py-1 rounded-s
|
||||
|
@ -87,9 +87,10 @@ const favoriteKeywords = async (req: NextApiRequest, res: NextApiResponse<keywor
|
||||
|
||||
try {
|
||||
const keywordsDatabase = await getLocalKeywordIdeas(domain);
|
||||
if (keywordsDatabase && keywordsDatabase.keywords && keywordsDatabase.favorites) {
|
||||
if (keywordsDatabase && keywordsDatabase.keywords) {
|
||||
const theKeyword = keywordsDatabase.keywords.find((kw) => kw.uid === keywordID);
|
||||
const newFavorites = [...keywordsDatabase.favorites];
|
||||
const existingKeywords = keywordsDatabase.favorites || [];
|
||||
const newFavorites = [...existingKeywords];
|
||||
const existingKeywordIndex = newFavorites.findIndex((kw) => kw.uid === keywordID);
|
||||
if (existingKeywordIndex > -1) {
|
||||
newFavorites.splice(existingKeywordIndex, 1);
|
||||
@ -98,7 +99,7 @@ const favoriteKeywords = async (req: NextApiRequest, res: NextApiResponse<keywor
|
||||
const updated = await updateLocalKeywordIdeas(domain, { favorites: newFavorites });
|
||||
|
||||
if (updated) {
|
||||
return res.status(200).json({ keywords: keywordsDatabase.favorites, error: '' });
|
||||
return res.status(200).json({ keywords: newFavorites, error: '' });
|
||||
}
|
||||
}
|
||||
|
||||
|
148
pages/research/index.tsx
Normal file
148
pages/research/index.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import type { NextPage } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import Icon from '../../components/common/Icon';
|
||||
import TopBar from '../../components/common/TopBar';
|
||||
import KeywordIdeasTable from '../../components/ideas/KeywordIdeasTable';
|
||||
import { exportKeywordIdeas } from '../../utils/client/exportcsv';
|
||||
import { useFetchKeywordIdeas, useMutateKeywordIdeas } from '../../services/adwords';
|
||||
import { useFetchSettings } from '../../services/settings';
|
||||
import Settings from '../../components/settings/Settings';
|
||||
import SelectField from '../../components/common/SelectField';
|
||||
import allCountries, { adwordsLanguages } from '../../utils/countries';
|
||||
|
||||
const Research: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [showFavorites, setShowFavorites] = useState(false);
|
||||
const [language, setLanguage] = useState('1000');
|
||||
const [country, setCountry] = useState('US');
|
||||
const [seedKeywords, setSeedKeywords] = useState('');
|
||||
|
||||
const { data: appSettings } = useFetchSettings();
|
||||
const adwordsConnected = !!(appSettings && appSettings?.settings?.adwords_refresh_token
|
||||
&& appSettings?.settings?.adwords_developer_token, appSettings?.settings?.adwords_account_id);
|
||||
const { data: keywordIdeasData, isLoading: isLoadingIdeas, isError: errorLoadingIdeas } = useFetchKeywordIdeas(router, adwordsConnected);
|
||||
const { mutate: updateKeywordIdeas, isLoading: isUpdatingIdeas } = useMutateKeywordIdeas(router);
|
||||
|
||||
const keywordIdeas:IdeaKeyword[] = keywordIdeasData?.data?.keywords || [];
|
||||
const favorites:IdeaKeyword[] = keywordIdeasData?.data?.favorites || [];
|
||||
const keywordIdeasSettings = keywordIdeasData?.data?.settings || undefined;
|
||||
const { country: previousCountry, language: previousLang, keywords: previousSeedKeywords } = keywordIdeasSettings || {};
|
||||
|
||||
useEffect(() => {
|
||||
if (previousCountry) { setCountry(previousCountry); }
|
||||
if (previousLang) { setLanguage(previousLang.toString()); }
|
||||
if (previousSeedKeywords) { setSeedKeywords(previousSeedKeywords.join(',')); }
|
||||
}, [previousCountry, previousLang, previousSeedKeywords]);
|
||||
|
||||
const reloadKeywordIdeas = () => {
|
||||
const keywordPaylod = seedKeywords ? seedKeywords.split(',').map((key) => key.trim()) : undefined;
|
||||
updateKeywordIdeas({ seedType: 'custom', language, domain: 'research', keywords: keywordPaylod, country });
|
||||
};
|
||||
|
||||
const countryOptions = useMemo(() => {
|
||||
return Object.keys(allCountries)
|
||||
.filter((countryISO) => allCountries[countryISO][3] !== 0)
|
||||
.map((countryISO) => ({ label: allCountries[countryISO][0], value: countryISO }));
|
||||
}, []);
|
||||
|
||||
const languageOPtions = useMemo(() => Object.entries(adwordsLanguages).map(([value, label]) => ({ label, value })), []);
|
||||
|
||||
const buttonStyle = 'leading-6 inline-block px-2 py-2 text-gray-500 hover:text-gray-700';
|
||||
const buttonLabelStyle = 'ml-2 text-sm not-italic lg:invisible lg:opacity-0';
|
||||
const labelStyle = 'mb-2 font-semibold inline-block text-sm text-gray-700 capitalize w-full';
|
||||
|
||||
return (
|
||||
<div className={'Login'}>
|
||||
<Head>
|
||||
<title>Research Keywords - SerpBear</title>
|
||||
</Head>
|
||||
<TopBar showSettings={() => setShowSettings(true)} showAddModal={() => null } />
|
||||
<div className=" w-full max-w-7xl mx-auto lg:flex lg:flex-row">
|
||||
<div className="sidebar w-full p-6 lg:pt-44 lg:w-1/5 lg:block lg:pr-0" data-testid="sidebar">
|
||||
<h3 className="hidden py-7 text-base font-bold text-blue-700 lg:block">
|
||||
<span className=' relative top-[3px] mr-1'><Icon type="logo" size={24} color="#364AFF" /></span> SerpBear
|
||||
</h3>
|
||||
<div className={`sidebar_menu domKeywords max-h-96 overflow-auto styled-scrollbar p-4
|
||||
bg-white border border-gray-200 rounded lg:rounded-none lg:rounded-s lg:border-r-0`}>
|
||||
<div className={'mb-3'}>
|
||||
<label className={labelStyle}>Generate Ideas from given Keywords (Max 20)</label>
|
||||
<textarea
|
||||
className='w-full border border-solid border-gray-300 focus:border-blue-100 p-3 rounded outline-none text-sm'
|
||||
value={seedKeywords}
|
||||
onChange={(event) => setSeedKeywords(event.target.value)}
|
||||
placeholder="keyword1, keyword2.."
|
||||
/>
|
||||
</div>
|
||||
<div className={'mb-3'}>
|
||||
<label className={labelStyle}>Country</label>
|
||||
<SelectField
|
||||
selected={[country]}
|
||||
options={countryOptions}
|
||||
defaultLabel='All Countries'
|
||||
updateField={(updated:string[]) => setCountry(updated[0])}
|
||||
flags={true}
|
||||
multiple={false}
|
||||
fullWidth={true}
|
||||
maxHeight={48}
|
||||
rounded='rounded'
|
||||
/>
|
||||
</div>
|
||||
<div className={'mb-3'}>
|
||||
<label className={labelStyle}>Language</label>
|
||||
<SelectField
|
||||
selected={[language]}
|
||||
options={languageOPtions}
|
||||
defaultLabel='All Languages'
|
||||
updateField={(updated:string[]) => setLanguage(updated[0])}
|
||||
rounded='rounded'
|
||||
multiple={false}
|
||||
fullWidth={true}
|
||||
maxHeight={48}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={`w-full py-2 px-5 mt-2 rounded bg-blue-700 text-white
|
||||
font-semibold ${!adwordsConnected ? ' cursor-not-allowed opacity-40' : 'cursor-pointer'}`}
|
||||
title={!adwordsConnected ? 'Please Connect Adwords account to generate Keyword Ideas..' : ''}
|
||||
onClick={() => !isUpdatingIdeas && adwordsConnected && reloadKeywordIdeas()}>
|
||||
<Icon type={isUpdatingIdeas ? 'loading' : 'download'} size={14} /> {isUpdatingIdeas ? 'Loading....' : 'Load Ideas'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="domain_kewywords px-5 lg:px-0 lg:pt-8 w-full">
|
||||
<div className='domain_kewywords_head w-full '>
|
||||
<div className=' flex mt-12 mb-0 justify-between'>
|
||||
<h1 className=" font-bold mb-0 mt-0 pt-2 lg:text-xl lg:mb-6" data-testid="domain-header">Research Keywords</h1>
|
||||
<button
|
||||
className={`domheader_action_button relative mb-3
|
||||
${buttonStyle} ${keywordIdeas.length === 0 ? 'cursor-not-allowed opacity-60' : ''}`}
|
||||
aria-pressed="false"
|
||||
onClick={() => exportKeywordIdeas(showFavorites ? favorites : keywordIdeas, 'research')}>
|
||||
<Icon type='download' size={20} /><i className={`${buttonLabelStyle}`}>Export as csv</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<KeywordIdeasTable
|
||||
isLoading={isLoadingIdeas}
|
||||
noIdeasDatabase={errorLoadingIdeas}
|
||||
domain={null}
|
||||
keywords={keywordIdeas}
|
||||
favorites={favorites}
|
||||
isAdwordsIntegrated={adwordsConnected}
|
||||
showFavorites={showFavorites}
|
||||
setShowFavorites={setShowFavorites}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CSSTransition in={showSettings} timeout={300} classNames="settings_anim" unmountOnExit mountOnEnter>
|
||||
<Settings closeSettings={() => setShowSettings(false)} />
|
||||
</CSSTransition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Research;
|
@ -26,9 +26,9 @@ export function useTestAdwordsIntegration(onSuccess?: Function) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchAdwordsKeywordIdeas(router: NextRouter) {
|
||||
export async function fetchAdwordsKeywordIdeas(router: NextRouter, domainSlug: string) {
|
||||
// if (!router.query.slug) { throw new Error('Invalid Domain Name'); }
|
||||
const res = await fetch(`${window.location.origin}/api/ideas?domain=${router.query.slug}`, { method: 'GET' });
|
||||
const res = await fetch(`${window.location.origin}/api/ideas?domain=${domainSlug}`, { method: 'GET' });
|
||||
if (res.status >= 400 && res.status < 600) {
|
||||
if (res.status === 401) {
|
||||
console.log('Unauthorized!!');
|
||||
@ -40,15 +40,15 @@ export async function fetchAdwordsKeywordIdeas(router: NextRouter) {
|
||||
}
|
||||
|
||||
export function useFetchKeywordIdeas(router: NextRouter, adwordsConnected = false) {
|
||||
return useQuery(
|
||||
`keywordIdeas-${router.query.slug}`,
|
||||
() => router.query.slug && fetchAdwordsKeywordIdeas(router),
|
||||
{ enabled: adwordsConnected, retry: false },
|
||||
);
|
||||
const isResearch = router.pathname === '/research';
|
||||
const domainSlug = isResearch ? 'research' : (router.query.slug as string);
|
||||
const enabled = !!(adwordsConnected && domainSlug);
|
||||
return useQuery(`keywordIdeas-${domainSlug}`, () => domainSlug && fetchAdwordsKeywordIdeas(router, domainSlug), { enabled, retry: false });
|
||||
}
|
||||
|
||||
export function useMutateKeywordIdeas(router:NextRouter, onSuccess?: Function) {
|
||||
const queryClient = useQueryClient();
|
||||
const domainSlug = router.pathname === '/research' ? 'research' : router.query.slug as string;
|
||||
return useMutation(async (data:Record<string, any>) => {
|
||||
const headers = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
|
||||
const fetchOpts = { method: 'POST', headers, body: JSON.stringify({ ...data }) };
|
||||
@ -64,7 +64,7 @@ export function useMutateKeywordIdeas(router:NextRouter, onSuccess?: Function) {
|
||||
if (onSuccess) {
|
||||
onSuccess(false);
|
||||
}
|
||||
queryClient.invalidateQueries([`keywordIdeas-${router.query.slug}`]);
|
||||
queryClient.invalidateQueries([`keywordIdeas-${domainSlug}`]);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log('Error Loading Keyword Ideas!!!', error);
|
||||
@ -75,6 +75,7 @@ export function useMutateKeywordIdeas(router:NextRouter, onSuccess?: Function) {
|
||||
|
||||
export function useMutateFavKeywordIdeas(router:NextRouter, onSuccess?: Function) {
|
||||
const queryClient = useQueryClient();
|
||||
const domainSlug = router.pathname === '/research' ? 'research' : router.query.slug as string;
|
||||
return useMutation(async (payload:Record<string, any>) => {
|
||||
const headers = new Headers({ 'Content-Type': 'application/json', Accept: 'application/json' });
|
||||
const fetchOpts = { method: 'PUT', headers, body: JSON.stringify({ ...payload }) };
|
||||
@ -90,7 +91,7 @@ export function useMutateFavKeywordIdeas(router:NextRouter, onSuccess?: Function
|
||||
if (onSuccess) {
|
||||
onSuccess(false);
|
||||
}
|
||||
queryClient.invalidateQueries([`keywordIdeas-${router.query.slug}`]);
|
||||
queryClient.invalidateQueries([`keywordIdeas-${domainSlug}`]);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log('Error Favorating Keywords', error);
|
||||
|
@ -101,7 +101,7 @@ export const getAdwordsAccessToken = async (credentials:AdwordsCredentials) => {
|
||||
console.log('[Error] Getting Google Account Access Token:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The function `getAdwordsKeywordIdeas` retrieves keyword ideas from Google AdWords API based on
|
||||
@ -225,24 +225,25 @@ const extractAdwordskeywordIdeas = (keywordIdeas:keywordIdeasResponseItem[], opt
|
||||
const { competition, competitionIndex = '0', avgMonthlySearches = '0', monthlySearchVolumes = [] } = keywordIdeaMetrics || {};
|
||||
if (keywordIdeaMetrics?.avgMonthlySearches) {
|
||||
const searchVolumeTrend: Record<string, string> = {};
|
||||
|
||||
const searchVolume = parseInt(avgMonthlySearches, 10);
|
||||
monthlySearchVolumes.forEach((item) => {
|
||||
searchVolumeTrend[`${item.month}-${item.year}`] = item.monthlySearches;
|
||||
});
|
||||
|
||||
keywords.push({
|
||||
uid: `${country.toLowerCase()}:${text.replaceAll(' ', '-')}`,
|
||||
keyword: text,
|
||||
competition,
|
||||
competitionIndex: competitionIndex !== null ? parseInt(competitionIndex, 10) : 0,
|
||||
monthlySearchVolumes: searchVolumeTrend,
|
||||
avgMonthlySearches: parseInt(avgMonthlySearches, 10),
|
||||
added: new Date().getTime(),
|
||||
updated: new Date().getTime(),
|
||||
country,
|
||||
domain,
|
||||
position: 999,
|
||||
});
|
||||
if (searchVolume > 100) {
|
||||
keywords.push({
|
||||
uid: `${country.toLowerCase()}:${text.replaceAll(' ', '-')}`,
|
||||
keyword: text,
|
||||
competition,
|
||||
competitionIndex: competitionIndex !== null ? parseInt(competitionIndex, 10) : 0,
|
||||
monthlySearchVolumes: searchVolumeTrend,
|
||||
avgMonthlySearches: searchVolume,
|
||||
added: new Date().getTime(),
|
||||
updated: new Date().getTime(),
|
||||
country,
|
||||
domain,
|
||||
position: 999,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import countries from '../countries';
|
||||
* @returns {void}
|
||||
*/
|
||||
const exportCSV = (keywords: KeywordType[] | SCKeywordType[], domain:string, scDataDuration = 'lastThreeDays') => {
|
||||
if (!keywords || (keywords && Array.isArray(keywords) && keywords.length === 0)) { return; }
|
||||
const isSCKeywords = !!(keywords && keywords[0] && keywords[0].uid);
|
||||
let csvHeader = 'ID,Keyword,Position,URL,Country,Device,Updated,Added,Tags\r\n';
|
||||
let csvBody = '';
|
||||
@ -34,7 +35,14 @@ const exportCSV = (keywords: KeywordType[] | SCKeywordType[], domain:string, scD
|
||||
downloadCSV(csvHeader, csvBody, fileName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates CSV File form the given keyword Ideas, and automatically downloads it.
|
||||
* @param {IdeaKeyword[]} keywords - The keyword Ideas to export
|
||||
* @param {string} domainName - The domain name.
|
||||
* @returns {void}
|
||||
*/
|
||||
export const exportKeywordIdeas = (keywords: IdeaKeyword[], domainName:string) => {
|
||||
if (!keywords || (keywords && Array.isArray(keywords) && keywords.length === 0)) { return; }
|
||||
const csvHeader = 'Keyword,Volume,Competition,CompetitionScore,Country,Added\r\n';
|
||||
let csvBody = '';
|
||||
const fileName = `${domainName}-keyword_ideas.csv`;
|
||||
@ -47,6 +55,12 @@ export const exportKeywordIdeas = (keywords: IdeaKeyword[], domainName:string) =
|
||||
downloadCSV(csvHeader, csvBody, fileName);
|
||||
};
|
||||
|
||||
/**
|
||||
* generates a CSV file with a specified header and body content and automatically downloads it.
|
||||
* @param {string} csvHeader - The `csvHeader` file header. A comma speperated csv header.
|
||||
* @param {string} csvBody - The content of the csv file.
|
||||
* @param {string} fileName - The file Name for the downlaoded csv file.
|
||||
*/
|
||||
const downloadCSV = (csvHeader:string, csvBody:string, fileName:string) => {
|
||||
const blob = new Blob([csvHeader + csvBody], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
Loading…
Reference in New Issue
Block a user