Skip to content

Commit 2a1fc0e

Browse files
committed
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.
1 parent 4d15989 commit 2a1fc0e

File tree

16 files changed

+341
-21
lines changed

16 files changed

+341
-21
lines changed

__mocks__/data.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const dummyKeywords = [
2222
lastUpdated: '2022-11-15T10:49:53.113',
2323
added: '2022-11-11T10:01:06.951',
2424
position: 19,
25+
volume: 10000,
2526
history: {
2627
'2022-11-11': 21,
2728
'2022-11-12': 24,
@@ -45,6 +46,7 @@ export const dummyKeywords = [
4546
lastUpdated: '2022-11-15T10:49:53.119',
4647
added: '2022-11-15T10:01:06.951',
4748
position: 29,
49+
volume: 1200,
4850
history: {
4951
'2022-11-11': 33,
5052
'2022-11-12': 34,

components/common/ChartSlim.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const ChartSlim = ({ labels, sreies, noMaxLimit = false }:ChartProps) => {
3636
},
3737
};
3838

39-
return <div className='w-[100px] h-[30px] rounded border border-gray-200'>
39+
return <div className='w-[80px] h-[30px] rounded border border-gray-200'>
4040
<Line
4141
datasetIdKey='XXX'
4242
options={options}

components/keywords/Keyword.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import countries from '../../utils/countries';
66
import ChartSlim from '../common/ChartSlim';
77
import KeywordPosition from './KeywordPosition';
88
import { generateTheChartData } from '../../utils/client/generateChartData';
9+
import { formattedNum } from '../../utils/client/helpers';
910

1011
type KeywordProps = {
1112
keywordData: KeywordType,
@@ -40,7 +41,7 @@ const Keyword = (props: KeywordProps) => {
4041
scDataType = 'threeDays',
4142
} = props;
4243
const {
43-
keyword, domain, ID, city, position, url = '', lastUpdated, country, sticky, history = {}, updating = false, lastUpdateError = false,
44+
keyword, domain, ID, city, position, url = '', lastUpdated, country, sticky, history = {}, updating = false, lastUpdateError = false, volume,
4445
} = keywordData;
4546
const [showOptions, setShowOptions] = useState(false);
4647
const [showPositionError, setPositionError] = useState(false);
@@ -99,7 +100,7 @@ const Keyword = (props: KeywordProps) => {
99100
<Icon type="check" size={10} />
100101
</button>
101102
<a
102-
className='py-2 hover:text-blue-600 lg:flex lg:items-center lg:w-full lg:max-w-[200px]'
103+
className={`py-2 hover:text-blue-600 lg:flex lg:items-center lg:w-full ${showSCData ? 'lg:max-w-[180px]' : 'lg:max-w-[240px]'}`}
103104
onClick={() => showKeywordDetails()}>
104105
<span className={`fflag fflag-${country} w-[18px] h-[12px] mr-2`} title={countries[country][0]} />
105106
<span className=' text-ellipsis overflow-hidden whitespace-nowrap w-[calc(100%-30px)]'>{keyword}{city ? ` (${city})` : ''}</span>
@@ -131,12 +132,18 @@ const Keyword = (props: KeywordProps) => {
131132

132133
{chartData.labels.length > 0 && (
133134
<div
134-
className='hidden basis-32 grow-0 cursor-pointer lg:block'
135+
className='hidden basis-20 grow-0 cursor-pointer lg:block'
135136
onClick={() => showKeywordDetails()}>
136137
<ChartSlim labels={chartData.labels} sreies={chartData.sreies} />
137138
</div>
138139
)}
139140

141+
<div
142+
className={`keyword_best hidden bg-[#f8f9ff] w-fit min-w-[50px] h-12 p-2 text-base mt-[-20px] rounded right-5 lg:relative lg:block
143+
lg:bg-transparent lg:w-auto lg:h-auto lg:mt-0 lg:p-0 lg:text-sm lg:flex-1 lg:basis-24 lg:grow-0 lg:right-0 text-center`}>
144+
{formattedNum(volume)}
145+
</div>
146+
140147
<div
141148
className={`keyword_url inline-block mt-4 mr-5 ml-5 lg:flex-1 text-gray-400 lg:m-0 max-w-[70px]
142149
overflow-hidden text-ellipsis whitespace-nowrap lg:max-w-none lg:pr-5`}>

components/keywords/KeywordFilter.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ const KeywordFilters = (props: KeywordFilterProps) => {
7676
{ value: 'date_desc', label: 'Oldest' },
7777
{ value: 'alpha_asc', label: 'Alphabetically(A-Z)' },
7878
{ value: 'alpha_desc', label: 'Alphabetically(Z-A)' },
79+
{ value: 'vol_asc', label: 'Lowest Search Volume' },
80+
{ value: 'vol_desc', label: 'Highest Search Volume' },
7981
];
8082
if (integratedConsole) {
8183
sortOptionChoices.push({ value: 'imp_desc', label: `Most Viewed${isConsole ? ' (Default)' : ''}` });
@@ -170,8 +172,8 @@ const KeywordFilters = (props: KeywordFilterProps) => {
170172
{sortOptions && (
171173
<ul
172174
data-testid="sort_options"
173-
className='sort_options mt-2 border absolute min-w-[0] right-0 rounded-lg
174-
max-h-96 bg-white z-[9999] w-44 overflow-y-auto styled-scrollbar'>
175+
className='sort_options mt-2 border absolute w-48 min-w-[0] right-0 rounded-lg
176+
max-h-96 bg-white z-[9999] overflow-y-auto styled-scrollbar'>
175177
{sortOptionChoices.map((sortOption) => {
176178
return <li
177179
key={sortOption.value}

components/keywords/KeywordsTable.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,10 @@ const KeywordsTable = (props: KeywordsTableProps) => {
158158
</span>
159159
<span className='domKeywords_head_position flex-1 basis-24 grow-0 text-center'>Position</span>
160160
<span className='domKeywords_head_best flex-1 basis-16 grow-0 text-center'>Best</span>
161-
<span className='domKeywords_head_history flex-1 basis-32 grow-0 '>History (7d)</span>
161+
<span className='domKeywords_head_history flex-1 basis-20 grow-0'>History (7d)</span>
162+
<span className='domKeywords_head_volume flex-1 basis-24 grow-0 text-center'>Volume</span>
162163
<span className='domKeywords_head_url flex-1'>URL</span>
163-
<span className='domKeywords_head_updated flex-1'>Updated</span>
164+
<span className='domKeywords_head_updated flex-1 relative left-3'>Updated</span>
164165
{showSCData && (
165166
<div className='domKeywords_head_sc flex-1 min-w-[170px] mr-7 text-center'>
166167
{/* Search Console */}

components/settings/AdWordsSettings.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { useTestAdwordsIntegration } from '../../services/adwords';
2+
import { useMutateKeywordsVolume, useTestAdwordsIntegration } from '../../services/adwords';
33
import Icon from '../common/Icon';
44
import SecretField from '../common/SecretField';
55

@@ -24,7 +24,10 @@ const AdWordsSettings = ({ settings, settingsError, updateSettings, performUpdat
2424
} = settings || {};
2525

2626
const { mutate: testAdWordsIntegration, isLoading: isTesting } = useTestAdwordsIntegration();
27+
const { mutate: getAllVolumeData, isLoading: isUpdatingVolume } = useMutateKeywordsVolume();
28+
2729
const cloudProjectIntegrated = adwords_client_id && adwords_client_secret && adwords_refresh_token;
30+
const hasAllCredentials = adwords_client_id && adwords_client_secret && adwords_refresh_token && adwords_developer_token && adwords_account_id;
2831

2932
const udpateAndAuthenticate = () => {
3033
if (adwords_client_id && adwords_client_secret) {
@@ -40,11 +43,17 @@ const AdWordsSettings = ({ settings, settingsError, updateSettings, performUpdat
4043
};
4144

4245
const testIntegration = () => {
43-
if (adwords_client_id && adwords_client_secret && adwords_refresh_token && adwords_developer_token && adwords_account_id) {
46+
if (hasAllCredentials) {
4447
testAdWordsIntegration({ developer_token: adwords_developer_token, account_id: adwords_account_id });
4548
}
4649
};
4750

51+
const updateVolumeData = () => {
52+
if (hasAllCredentials) {
53+
getAllVolumeData({ domain: 'all' });
54+
}
55+
};
56+
4857
return (
4958
<div>
5059
<div>
@@ -98,16 +107,34 @@ const AdWordsSettings = ({ settings, settingsError, updateSettings, performUpdat
98107
<div className="settings__section__input mb-4 flex justify-between items-center w-full">
99108
<button
100109
className={`py-2 px-5 w-full text-sm font-semibold rounded bg-indigo-50 text-blue-700 border border-indigo-100
101-
${adwords_client_id && adwords_client_secret && adwords_refresh_token ? 'cursor-pointer' : 'cursor-not-allowed opacity-40'}
110+
${hasAllCredentials ? 'cursor-pointer' : 'cursor-not-allowed opacity-40'}
102111
hover:bg-blue-700 hover:text-white transition`}
103-
title='Insert All the data in the above fields to Authenticate'
112+
title={hasAllCredentials ? '' : 'Insert All the data in the above fields to Test the Integration'}
104113
onClick={testIntegration}>
105114
{isTesting && <Icon type='loading' />}
106115
<Icon type='adwords' size={14} /> Test AdWords Integration
107116
</button>
108117
</div>
109118
</div>
110119
</div>
120+
<div className='mt-4 mb-4 border-b border-gray-100 pt-4 pb-0 relative'>
121+
{!hasAllCredentials && <div className=' absolute w-full h-full z-50' />}
122+
<h4 className=' mb-3 font-semibold text-blue-700'>Update Keyword Volume Data</h4>
123+
<div className={!hasAllCredentials ? 'opacity-40' : ''}>
124+
<div className="settings__section__input mb-4 flex justify-between items-center w-full">
125+
<p>Update Volume data for all your Tracked Keywords.</p>
126+
</div>
127+
<div className="settings__section__input mb-4 flex justify-between items-center w-full">
128+
<button
129+
className={`py-2 px-5 w-full text-sm font-semibold rounded bg-indigo-50 text-blue-700 border border-indigo-100
130+
${hasAllCredentials ? 'cursor-pointer' : 'cursor-not-allowed opacity-40'}
131+
hover:bg-blue-700 hover:text-white transition`}
132+
onClick={updateVolumeData}>
133+
<Icon type={isUpdatingVolume ? 'loading' : 'reload'} size={isUpdatingVolume ? 16 : 12} /> Update Keywords Volume
134+
</button>
135+
</div>
136+
</div>
137+
</div>
111138
<p className='mb-4 text-xs'>
112139
<a target='_blank' rel='noreferrer' href='https://docs.serpbear.com/keyword-research' className=' underline text-blue-600'>Integrate Google Adwords</a> to get Keyword Ideas & Search Volume.{' '}
113140
</p>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Migration: Adds volume field to the keyword table.
2+
3+
// CLI Migration
4+
module.exports = {
5+
up: async (queryInterface, Sequelize) => {
6+
return queryInterface.sequelize.transaction(async (t) => {
7+
try {
8+
const keywordTableDefinition = await queryInterface.describeTable('keyword');
9+
if (keywordTableDefinition) {
10+
if (!keywordTableDefinition.volume) {
11+
await queryInterface.addColumn('keyword', 'volume', {
12+
type: Sequelize.DataTypes.STRING, allowNull: false, defaultValue: 0,
13+
}, { transaction: t });
14+
}
15+
}
16+
} catch (error) {
17+
console.log('error :', error);
18+
}
19+
});
20+
},
21+
down: (queryInterface) => {
22+
return queryInterface.sequelize.transaction(async (t) => {
23+
try {
24+
const keywordTableDefinition = await queryInterface.describeTable('keyword');
25+
if (keywordTableDefinition) {
26+
if (keywordTableDefinition.volume) {
27+
await queryInterface.removeColumn('keyword', 'volume', { transaction: t });
28+
}
29+
}
30+
} catch (error) {
31+
console.log('error :', error);
32+
}
33+
});
34+
},
35+
};

database/models/keyword.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class Keyword extends Model {
4747
@Column({ type: DataType.STRING, allowNull: true, defaultValue: JSON.stringify([]) })
4848
history!: string;
4949

50+
@Column({ type: DataType.INTEGER, allowNull: false, defaultValue: 0 })
51+
volume!: number;
52+
5053
@Column({ type: DataType.STRING, allowNull: true, defaultValue: JSON.stringify([]) })
5154
url!: string;
5255

pages/api/keywords.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import verifyUser from '../../utils/verifyUser';
77
import parseKeywords from '../../utils/parseKeywords';
88
import { integrateKeywordSCData, readLocalSCData } from '../../utils/searchConsole';
99
import refreshAndUpdateKeywords from '../../utils/refresh';
10+
import { getKeywordsVolume, updateKeywordsVolumeData } from '../../utils/adwords';
1011

1112
type KeywordsGetResponse = {
1213
keywords?: KeywordType[],
@@ -103,8 +104,20 @@ const addKeywords = async (req: NextApiRequest, res: NextApiResponse<KeywordsGet
103104
const newKeywords:Keyword[] = await Keyword.bulkCreate(keywordsToAdd);
104105
const formattedkeywords = newKeywords.map((el) => el.get({ plain: true }));
105106
const keywordsParsed: KeywordType[] = parseKeywords(formattedkeywords);
107+
108+
// Queue the SERP Scraping Process
106109
const settings = await getAppSettings();
107-
refreshAndUpdateKeywords(newKeywords, settings); // Queue the SERP Scraping Process
110+
refreshAndUpdateKeywords(newKeywords, settings);
111+
112+
// Update the Keyword Volume
113+
const { adwords_account_id, adwords_client_id, adwords_client_secret, adwords_developer_token } = settings;
114+
if (adwords_account_id && adwords_client_id && adwords_client_secret && adwords_developer_token) {
115+
const keywordsVolumeData = await getKeywordsVolume(keywordsParsed);
116+
if (keywordsVolumeData.volumes !== false) {
117+
await updateKeywordsVolumeData(keywordsVolumeData.volumes);
118+
}
119+
}
120+
108121
return res.status(201).json({ keywords: keywordsParsed });
109122
} catch (error) {
110123
console.log('[ERROR] Adding New Keywords ', error);

pages/api/refresh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ const getKeywordSearchResults = async (req: NextApiRequest, res: NextApiResponse
9393
country: req.query.country as string,
9494
domain: '',
9595
lastUpdated: '',
96+
volume: 0,
9697
added: '',
9798
position: 111,
9899
sticky: false,

0 commit comments

Comments
 (0)