{label &&
}
setShowOptions(!showOptions)}>
{selected.length > 0 ? (selectedLabels.slice(0, 2).join(', ')) : defaultLabel}
@@ -83,7 +85,7 @@ const SelectField = (props: SelectFieldProps) => {
{showOptions && (
{options.length > 20 && (
diff --git a/components/common/Sidebar.tsx b/components/common/Sidebar.tsx
index e75b187..e7a9cb0 100644
--- a/components/common/Sidebar.tsx
+++ b/components/common/Sidebar.tsx
@@ -25,7 +25,7 @@ const Sidebar = ({ domains, showAddModal } : SidebarProps) => {
void ;
classNames?: string;
}
-const ToggleField = ({ label = '', value = '', onChange, classNames = '' }: ToggleFieldProps) => {
+const ToggleField = ({ label = '', value = false, onChange, classNames = '' }: ToggleFieldProps) => {
return (
-
-
+
+
-
+
{!isInsight &&
@@ -89,7 +107,7 @@ const DomainHeader = ({ domain, showAddModal, showSettingsModal, exportCsv, doma
Export as csv
)}
- {!isConsole && !isInsight && (
+ {!isConsole && !isInsight && !isIdeas && (
- {!isConsole && !isInsight && (
+ {!isConsole && !isInsight && !isIdeas && (
)}
+ {isIdeas && (
+
+ )}
diff --git a/components/ideas/IdeaDetails.tsx b/components/ideas/IdeaDetails.tsx
new file mode 100644
index 0000000..43caf2f
--- /dev/null
+++ b/components/ideas/IdeaDetails.tsx
@@ -0,0 +1,139 @@
+import React, { useMemo, useRef } from 'react';
+import dayjs from 'dayjs';
+import { useQuery } from 'react-query';
+import { useRouter } from 'next/router';
+import Icon from '../common/Icon';
+import countries from '../../utils/countries';
+import Chart from '../common/Chart';
+import useOnKey from '../../hooks/useOnKey';
+import { formattedNum } from '../../utils/client/helpers';
+import { fetchSearchResults } from '../../services/keywords';
+
+type IdeaDetailsProps = {
+ keyword: IdeaKeyword,
+ closeDetails: Function
+}
+
+const dummySearchResults = [
+ { position: 1, url: 'https://google.com/?search=dummy+text', title: 'Google Search Result One' },
+ { position: 1, url: 'https://yahoo.com/?search=dummy+text', title: 'Yahoo Results | Sample Dummy' },
+ { position: 1, url: 'https://gamespot.com/?search=dummy+text', title: 'GameSpot | Dummy Search Results' },
+ { position: 1, url: 'https://compressimage.com/?search=dummy+text', title: 'Compress Images Online' },
+];
+
+const IdeaDetails = ({ keyword, closeDetails }:IdeaDetailsProps) => {
+ const router = useRouter();
+ const updatedDate = new Date(keyword.updated);
+ const searchResultContainer = useRef
(null);
+ const searchResultFound = useRef(null);
+ const searchResultReqPayload = { keyword: keyword.keyword, country: keyword.country, device: 'desktop' };
+ const { data: keywordSearchResultData, refetch: fetchKeywordSearchResults, isLoading: fetchingResult } = useQuery(
+ `ideas:${keyword.uid}`,
+ () => fetchSearchResults(router, searchResultReqPayload),
+ { refetchOnWindowFocus: false, enabled: false },
+ );
+ const { monthlySearchVolumes } = keyword;
+
+ useOnKey('Escape', closeDetails);
+
+ const chartData = useMemo(() => {
+ const chartDataObj: { labels: string[], sreies: number[] } = { labels: [], sreies: [] };
+ Object.keys(monthlySearchVolumes).forEach((dateKey:string) => {
+ const dateKeyArr = dateKey.split('-');
+ const labelDate = `${dateKeyArr[0].slice(0, 1).toUpperCase()}${dateKeyArr[0].slice(1, 3).toLowerCase()}, ${dateKeyArr[1].slice(2)}`;
+ chartDataObj.labels.push(labelDate);
+ chartDataObj.sreies.push(parseInt(monthlySearchVolumes[dateKey], 10));
+ });
+ return chartDataObj;
+ }, [monthlySearchVolumes]);
+
+ const closeOnBGClick = (e:React.SyntheticEvent) => {
+ e.stopPropagation();
+ e.nativeEvent.stopImmediatePropagation();
+ if (e.target === e.currentTarget) { closeDetails(); }
+ };
+
+ const searchResultsFetched = !!keywordSearchResultData?.searchResult?.results;
+ const keywordSearchResult = searchResultsFetched ? keywordSearchResultData?.searchResult?.results : dummySearchResults;
+
+ return (
+
+
+
+
+ {keyword.keyword}
+
+ {formattedNum(keyword.avgMonthlySearches)}/month
+
+
+
+
+
+
+
+
+
Search Volume Trend
+
+
+
+
+
+
+
+
Google Search Result
+
+
+
+
+
{dayjs(updatedDate).format('MMMM D, YYYY')}
+
+
+ {!searchResultsFetched && (
+
+
View Google Search Results for "{keyword.keyword}"
+
+
+ )}
+
+ {keywordSearchResult && Array.isArray(keywordSearchResult) && keywordSearchResult.length > 0 && (
+ keywordSearchResult.map((item, index) => {
+ const { position } = keyword;
+ const domainExist = position < 100 && index === (position - 1);
+ return (
+
+ );
+ })
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default IdeaDetails;
diff --git a/components/ideas/IdeasFilter.tsx b/components/ideas/IdeasFilter.tsx
new file mode 100644
index 0000000..65b2f9c
--- /dev/null
+++ b/components/ideas/IdeasFilter.tsx
@@ -0,0 +1,135 @@
+import React, { useState } from 'react';
+import Icon from '../common/Icon';
+import SelectField, { SelectionOption } from '../common/SelectField';
+
+type IdeasFilterProps = {
+ allTags: string[],
+ filterParams: KeywordFilters,
+ filterKeywords: Function,
+ keywords: IdeaKeyword[],
+ favorites: IdeaKeyword[],
+ updateSort: Function,
+ showFavorites: Function,
+ sortBy: string,
+}
+
+const IdeasFilters = (props: IdeasFilterProps) => {
+ const { filterKeywords, allTags = [], updateSort, showFavorites, sortBy, filterParams, keywords = [], favorites = [] } = props;
+ const [keywordType, setKeywordType] = useState<'all'|'favorites'>('all');
+ const [sortOptions, showSortOptions] = useState(false);
+ const [filterOptions, showFilterOptions] = useState(false);
+
+ const filterTags = (tags:string[]) => filterKeywords({ ...filterParams, tags });
+
+ const searchKeywords = (event:React.FormEvent) => {
+ const filtered = filterKeywords({ ...filterParams, search: event.currentTarget.value });
+ return filtered;
+ };
+
+ const sortOptionChoices: SelectionOption[] = [
+ { value: 'alpha_asc', label: 'Alphabetically(A-Z)' },
+ { value: 'alpha_desc', label: 'Alphabetically(Z-A)' },
+ { value: 'vol_asc', label: 'Lowest Search Volume' },
+ { value: 'vol_desc', label: 'Highest Search Volume' },
+ { value: 'competition_asc', label: 'High Competition' },
+ { value: 'competition_desc', label: 'Low Competition' },
+ ];
+
+ const sortItemStyle = (sortType:string) => {
+ return `cursor-pointer py-2 px-3 hover:bg-[#FCFCFF] ${sortBy === sortType ? 'bg-indigo-50 text-indigo-600 hover:bg-indigo-50' : ''}`;
+ };
+
+ const deviceTabStyle = 'select-none cursor-pointer px-3 py-2 rounded-3xl mr-2';
+ const deviceTabCountStyle = 'px-2 py-0 rounded-3xl bg-[#DEE1FC] text-[0.7rem] font-bold ml-1';
+ const mobileFilterOptionsStyle = 'visible mt-8 border absolute min-w-[0] rounded-lg max-h-96 bg-white z-50 w-52 right-2 p-4';
+
+ return (
+
+
+
+ - { setKeywordType('all'); showFavorites(false); }}>
+
+ All Keywords
+ {keywords.length}
+
+ - { setKeywordType('favorites'); showFavorites(true); }}>
+
+ Favorites
+ {favorites.length}
+
+
+
+
+
+
+
+
+
+
+ {keywordType === 'all' && (
+
+ ({ label: tag, value: tag }))}
+ defaultLabel={`All Groups (${allTags.length})`}
+ updateField={(updated:string[]) => filterTags(updated)}
+ emptyMsg="No Groups Found for this Domain"
+ minWidth={270}
+ />
+
+ )}
+
+
+
+
+
+
+ {sortOptions && (
+
+ {sortOptionChoices.map((sortOption) => {
+ return - { updateSort(sortOption.value); showSortOptions(false); }}>
+ {sortOption.label}
+
;
+ })}
+
+ )}
+
+
+
+
+ );
+};
+
+export default IdeasFilters;
diff --git a/components/ideas/KeywordIdea.tsx b/components/ideas/KeywordIdea.tsx
new file mode 100644
index 0000000..d2cb7f6
--- /dev/null
+++ b/components/ideas/KeywordIdea.tsx
@@ -0,0 +1,77 @@
+import React, { useMemo } from 'react';
+import Icon from '../common/Icon';
+import countries from '../../utils/countries';
+import { formattedNum } from '../../utils/client/helpers';
+import ChartSlim from '../common/ChartSlim';
+
+type KeywordIdeaProps = {
+ keywordData: IdeaKeyword,
+ selected: boolean,
+ lastItem?:boolean,
+ isFavorite: boolean,
+ style: Object,
+ selectKeyword: Function,
+ favoriteKeyword:Function,
+ showKeywordDetails: Function
+}
+
+const KeywordIdea = (props: KeywordIdeaProps) => {
+ const { keywordData, selected, lastItem, selectKeyword, style, isFavorite = false, favoriteKeyword, showKeywordDetails } = props;
+ const { keyword, uid, position, country, monthlySearchVolumes, avgMonthlySearches, competition, competitionIndex } = keywordData;
+
+ const chartData = useMemo(() => {
+ const chartDataObj: { labels: string[], sreies: number[] } = { labels: [], sreies: [] };
+ Object.keys(monthlySearchVolumes).forEach((dateKey:string) => {
+ chartDataObj.labels.push(dateKey);
+ chartDataObj.sreies.push(parseInt(monthlySearchVolumes[dateKey], 10));
+ });
+ return chartDataObj;
+ }, [monthlySearchVolumes]);
+
+ return (
+
+
+
+
+
+ {formattedNum(avgMonthlySearches)}/month
+
+
+
showKeywordDetails()}
+ className={`keyword_visits text-center hidden mt-4 mr-5 ml-5 cursor-pointer
+ lg:flex-1 lg:m-0 lg:ml-10 max-w-[70px] lg:max-w-none lg:pr-5 lg:flex justify-center`}>
+ {chartData.labels.length > 0 && }
+
+
+
+
+ {competitionIndex}/100
+ {competition}
+
+
+
+ );
+ };
+
+ export default KeywordIdea;
diff --git a/components/ideas/KeywordIdeasTable.tsx b/components/ideas/KeywordIdeasTable.tsx
new file mode 100644
index 0000000..0d49f8b
--- /dev/null
+++ b/components/ideas/KeywordIdeasTable.tsx
@@ -0,0 +1,220 @@
+import { useRouter } from 'next/router';
+import React, { useState, useMemo } from 'react';
+import { Toaster } from 'react-hot-toast';
+import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
+import { useAddKeywords } from '../../services/keywords';
+import Icon from '../common/Icon';
+import KeywordIdea from './KeywordIdea';
+import useWindowResize from '../../hooks/useWindowResize';
+import useIsMobile from '../../hooks/useIsMobile';
+import { IdeasSortKeywords, IdeasfilterKeywords } from '../../utils/client/IdeasSortFilter';
+import IdeasFilters from './IdeasFilter';
+import { useMutateFavKeywordIdeas } from '../../services/adwords';
+import IdeaDetails from './IdeaDetails';
+
+type IdeasKeywordsTableProps = {
+ domain: DomainType | null,
+ keywords: IdeaKeyword[],
+ favorites: IdeaKeyword[],
+ noIdeasDatabase: boolean,
+ isLoading: boolean,
+ showFavorites: boolean,
+ setShowFavorites: Function,
+ isAdwordsIntegrated: boolean,
+}
+
+const IdeasKeywordsTable = ({
+ domain, keywords = [], favorites = [], isLoading = true, isAdwordsIntegrated = true, setShowFavorites,
+ showFavorites = false, noIdeasDatabase = false }: IdeasKeywordsTableProps) => {
+ const router = useRouter();
+ const [selectedKeywords, setSelectedKeywords] = useState([]);
+ const [showKeyDetails, setShowKeyDetails] = useState(null);
+ const [filterParams, setFilterParams] = useState({ countries: [], tags: [], search: '' });
+ const [sortBy, setSortBy] = useState('imp_desc');
+ const [listHeight, setListHeight] = useState(500);
+ const [addKeywordDevice, setAddKeywordDevice] = useState<'desktop'|'mobile'>('desktop');
+ const { mutate: addKeywords } = useAddKeywords(() => { if (domain && domain.slug) router.push(`/domain/${domain.slug}`); });
+ const { mutate: faveKeyword, isLoading: isFaving } = useMutateFavKeywordIdeas(router);
+ const [isMobile] = useIsMobile();
+ useWindowResize(() => setListHeight(window.innerHeight - (isMobile ? 200 : 400)));
+
+ const finalKeywords: IdeaKeyword[] = useMemo(() => {
+ const filteredKeywords = IdeasfilterKeywords(showFavorites ? favorites : keywords, filterParams);
+ const sortedKeywords = IdeasSortKeywords(filteredKeywords, sortBy);
+ return sortedKeywords;
+ }, [keywords, showFavorites, favorites, filterParams, sortBy]);
+
+ const favoriteIDs: string[] = useMemo(() => favorites.map((fav) => fav.uid), [favorites]);
+
+ const allTags:string[] = useMemo(() => {
+ const wordTags: Map = new Map();
+ keywords.forEach((k) => {
+ const keywordsArray = k.keyword.split(' ');
+ const keywordFirstTwoWords = keywordsArray.slice(0, 2).join(' ');
+ const keywordFirstTwoWordsReversed = keywordFirstTwoWords.split(' ').reverse().join(' ');
+ if (!wordTags.has(keywordFirstTwoWordsReversed)) {
+ wordTags.set(keywordFirstTwoWords, 0);
+ }
+ });
+ [...wordTags].forEach((tag) => {
+ const foundTags = keywords.filter((kw) => kw.keyword.includes(tag[0]) || kw.keyword.includes(tag[0].split(' ').reverse().join(' ')));
+ if (foundTags.length < 3) {
+ wordTags.delete(tag[0]);
+ } else {
+ wordTags.set(tag[0], foundTags.length);
+ }
+ });
+ const finalWordTags = [...wordTags].sort((a, b) => (a[1] > b[1] ? -1 : 1)).map((t) => `${t[0]} (${t[1]})`);
+ return finalWordTags;
+ }, [keywords]);
+
+ const selectKeyword = (keywordID: string) => {
+ let updatedSelectd = [...selectedKeywords, keywordID];
+ if (selectedKeywords.includes(keywordID)) {
+ updatedSelectd = selectedKeywords.filter((keyID) => keyID !== keywordID);
+ }
+ setSelectedKeywords(updatedSelectd);
+ };
+
+ const favoriteKeyword = (keywordID: string) => {
+ if (!isFaving) {
+ faveKeyword({ keywordID, domain: domain?.slug });
+ }
+ };
+
+ const addKeywordIdeasToTracker = () => {
+ const selectedkeywords:KeywordAddPayload[] = [];
+ keywords.forEach((kitem:IdeaKeyword) => {
+ if (selectedKeywords.includes(kitem.uid)) {
+ const { keyword, country } = kitem;
+ selectedkeywords.push({ keyword, device: addKeywordDevice, country, domain: domain?.domain || '', tags: '' });
+ }
+ });
+ addKeywords(selectedkeywords);
+ setSelectedKeywords([]);
+ };
+
+ const selectedAllItems = selectedKeywords.length === finalKeywords.length;
+
+ const Row = ({ data, index, style }:ListChildComponentProps) => {
+ const keyword: IdeaKeyword = data[index];
+ return (
+ favoriteKeyword(keyword.uid)}
+ showKeywordDetails={() => setShowKeyDetails(keyword)}
+ isFavorite={favoriteIDs.includes(keyword.uid)}
+ keywordData={keyword}
+ lastItem={index === (finalKeywords.length - 1)}
+ />
+ );
+ };
+
+ return (
+
+
+ {selectedKeywords.length > 0 && (
+
+
Add Keywords to Tracker
+
+
+
+
+
addKeywordIdeasToTracker()}
+ >
+ + Add Keywords
+
+
+ )}
+ {selectedKeywords.length === 0 && (
+
setFilterParams(params)}
+ updateSort={(sorted:string) => setSortBy(sorted)}
+ sortBy={sortBy}
+ keywords={keywords}
+ favorites={favorites}
+ showFavorites={(show:boolean) => { setShowFavorites(show); }}
+ />
+ )}
+
+
+
+
+ {finalKeywords.length > 0 && (
+
+ )}
+ Keyword
+
+ Monthly Search
+ Search Trend
+ Competition
+
+
+ {!isLoading && finalKeywords && finalKeywords.length > 0 && (
+
+ {Row}
+
+ )}
+
+ {isAdwordsIntegrated && isLoading && (
+
Loading Keywords Ideas...
+ )}
+ {isAdwordsIntegrated && noIdeasDatabase && !isLoading && (
+
+ {'No keyword Ideas has been generated for this domain yet. Click the "Load Ideas" button to generate keyword ideas.'}
+
+ )}
+ {isAdwordsIntegrated && !isLoading && finalKeywords.length === 0 && !noIdeasDatabase && (
+
+ {'No Keyword Ideas found. Please try generating Keyword Ideas again by clicking the "Load Ideas" button.'}
+
+ )}
+ {!isAdwordsIntegrated && (
+
+ Google Adwords has not been Integrated yet. Please follow These Steps to integrate Google Adwords.
+
+ )}
+
+
+
+
+ {showKeyDetails && showKeyDetails.uid && (
+
setShowKeyDetails(null)} />
+ )}
+
+
+ );
+ };
+
+ export default IdeasKeywordsTable;
diff --git a/components/ideas/KeywordIdeasUpdater.tsx b/components/ideas/KeywordIdeasUpdater.tsx
new file mode 100644
index 0000000..2f0c5e9
--- /dev/null
+++ b/components/ideas/KeywordIdeasUpdater.tsx
@@ -0,0 +1,139 @@
+import { useMemo, useState } from 'react';
+import { useRouter } from 'next/router';
+import { useMutateKeywordIdeas } from '../../services/adwords';
+import allCountries, { adwordsLanguages } from '../../utils/countries';
+import SelectField from '../common/SelectField';
+import Icon from '../common/Icon';
+
+interface KeywordIdeasUpdaterProps {
+ onUpdate?: Function,
+ domain?: DomainType,
+ searchConsoleConnected: boolean,
+ adwordsConnected: boolean,
+ settings?: {
+ seedSCKeywords: boolean,
+ seedCurrentKeywords: boolean,
+ seedDomain: boolean,
+ language: string,
+ countries: string[],
+ keywords: string,
+ seedType: string
+ }
+}
+
+const KeywordIdeasUpdater = ({ onUpdate, settings, domain, searchConsoleConnected = false, adwordsConnected = false }: KeywordIdeasUpdaterProps) => {
+ const router = useRouter();
+ const [seedType, setSeedType] = useState(() => settings?.seedType || 'auto');
+ const [language, setLanguage] = useState(() => settings?.language.toString() || '1000');
+ const [countries, setCoutries] = useState(() => settings?.countries || ['US']);
+ const [keywords, setKeywords] = useState(() => (settings?.keywords && Array.isArray(settings?.keywords) ? settings?.keywords.join(',') : ''));
+ const { mutate: updateKeywordIdeas, isLoading: isUpdatingIdeas } = useMutateKeywordIdeas(router, () => onUpdate && onUpdate());
+
+ const seedTypeOptions = useMemo(() => {
+ const options = [
+ { label: 'Automatically from Website Content', value: 'auto' },
+ { label: 'Based on currently tracked keywords', value: 'tracking' },
+ { label: 'From Custom Keywords', value: 'custom' },
+ ];
+
+ if (searchConsoleConnected) {
+ options.splice(-2, 0, { label: 'Based on already ranking keywords (GSC)', value: 'searchconsole' });
+ }
+
+ return options;
+ }, [searchConsoleConnected]);
+
+ const reloadKeywordIdeas = () => {
+ const keywordPaylod = seedType !== 'auto' && keywords ? keywords.split(',').map((key) => key.trim()) : undefined;
+ console.log('keywordPaylod :', keywords, keywordPaylod);
+ updateKeywordIdeas({
+ seedType,
+ language,
+ domain: domain?.domain,
+ domainSlug: domain?.slug,
+ keywords: keywordPaylod,
+ country: countries[0],
+ });
+ };
+
+ 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 labelStyle = 'mb-2 font-semibold inline-block text-sm text-gray-700 capitalize w-full';
+ return (
+
+
+
+
+
+ setSeedType(updated[0])}
+ fullWidth={true}
+ multiple={false}
+ rounded='rounded'
+ />
+
+ {seedType === 'custom' && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+
+ setCoutries(updated)}
+ flags={true}
+ multiple={false}
+ fullWidth={true}
+ maxHeight={48}
+ rounded='rounded'
+ />
+
+
+
+ setLanguage(updated[0])}
+ rounded='rounded'
+ multiple={false}
+ fullWidth={true}
+ maxHeight={48}
+ />
+
+
+
+
+
+ );
+};
+
+export default KeywordIdeasUpdater;
diff --git a/components/settings/AdWordsSettings.tsx b/components/settings/AdWordsSettings.tsx
new file mode 100644
index 0000000..12c4e98
--- /dev/null
+++ b/components/settings/AdWordsSettings.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { useTestAdwordsIntegration } from '../../services/adwords';
+import Icon from '../common/Icon';
+import SecretField from '../common/SecretField';
+
+type AdWordsSettingsProps = {
+ settings: SettingsType,
+ settingsError: null | {
+ type: string,
+ msg: string
+ },
+ updateSettings: Function,
+ performUpdate: Function,
+ closeSettings: Function
+}
+
+const AdWordsSettings = ({ settings, settingsError, updateSettings, performUpdate, closeSettings }:AdWordsSettingsProps) => {
+ const {
+ adwords_client_id = '',
+ adwords_client_secret = '',
+ adwords_developer_token = '',
+ adwords_account_id = '',
+ adwords_refresh_token = '',
+ } = settings || {};
+
+ const { mutate: testAdWordsIntegration, isLoading: isTesting } = useTestAdwordsIntegration();
+ const cloudProjectIntegrated = adwords_client_id && adwords_client_secret && adwords_refresh_token;
+
+ const udpateAndAuthenticate = () => {
+ if (adwords_client_id && adwords_client_secret) {
+ const link = document.createElement('a');
+ link.href = `https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fadwords&response_type=code&client_id=${adwords_client_id}&redirect_uri=${`${encodeURIComponent(window.location.origin)}/api/adwords`}&service=lso&o2v=2&theme=glif&flowName=GeneralOAuthFlow`;
+ link.target = '_blank';
+ link.click();
+ if (performUpdate) {
+ performUpdate();
+ closeSettings();
+ }
+ }
+ };
+
+ const testIntegration = () => {
+ if (adwords_client_id && adwords_client_secret && adwords_refresh_token && adwords_developer_token && adwords_account_id) {
+ testAdWordsIntegration({ developer_token: adwords_developer_token, account_id: adwords_account_id });
+ }
+ };
+
+ return (
+
+
+
+
Step 1: Connect Google Cloud Project
+
+ updateSettings('adwords_client_id', client_id)}
+ value={adwords_client_id}
+ placeholder='3943006-231f65cjm.apps.googleusercontent.com'
+ />
+
+
+ updateSettings('adwords_client_secret', client_secret)}
+ value={adwords_client_secret}
+ placeholder='GTXSPX-45asaf-u1s252sd6qdE9yc8T'
+ />
+
+
+
+
+ {!cloudProjectIntegrated &&
}
+
Step 2: Connect Google AdWords
+
+
+ updateSettings('adwords_developer_token', developer_token)}
+ value={adwords_developer_token}
+ placeholder='4xr6jY94kAxtXk4rfcgc4w'
+ />
+
+
+ updateSettings('adwords_account_id', account_id)}
+ value={adwords_account_id}
+ placeholder='590-948-9101'
+ />
+
+
+
+
+
+
+
+ Integrate Google Adwords to get Keyword Ideas & Search Volume.{' '}
+
+
+
+ );
+};
+
+export default AdWordsSettings;
diff --git a/components/settings/IntegrationSettings.tsx b/components/settings/IntegrationSettings.tsx
new file mode 100644
index 0000000..dea92d0
--- /dev/null
+++ b/components/settings/IntegrationSettings.tsx
@@ -0,0 +1,53 @@
+import React, { useState } from 'react';
+import SearchConsoleSettings from './SearchConsoleSettings';
+import AdWordsSettings from './AdWordsSettings';
+import Icon from '../common/Icon';
+
+type IntegrationSettingsProps = {
+ settings: SettingsType,
+ settingsError: null | {
+ type: string,
+ msg: string
+ },
+ updateSettings: Function,
+ performUpdate: Function,
+ closeSettings: Function
+}
+const IntegrationSettings = ({ settings, settingsError, updateSettings, performUpdate, closeSettings }:IntegrationSettingsProps) => {
+ const [currentTab, setCurrentTab] = useState('searchconsole');
+ const tabStyle = 'inline-block px-4 py-1 rounded-full mr-3 cursor-pointer text-sm';
+ return (
+
+
+
+ - setCurrentTab('searchconsole')}>
+ Search Console
+
+ - setCurrentTab('adwords')}>
+ Adwords
+
+
+
+
+ {currentTab === 'searchconsole' && settings && (
+
+ )}
+ {currentTab === 'adwords' && settings && (
+
+ )}
+
+
+ );
+};
+
+export default IntegrationSettings;
diff --git a/components/settings/ScraperSettings.tsx b/components/settings/ScraperSettings.tsx
index ba72c1a..ff4ae6b 100644
--- a/components/settings/ScraperSettings.tsx
+++ b/components/settings/ScraperSettings.tsx
@@ -57,13 +57,15 @@ const ScraperSettings = ({ settings, settingsError, updateSettings }:ScraperSett
/>
{settings.scraper_type !== 'none' && settings.scraper_type !== 'proxy' && (
- updateSettings('scaping_api', value)}
- />
+
+ updateSettings('scaping_api', value)}
+ />
+
)}
{settings.scraper_type === 'proxy' && (
@@ -111,7 +113,7 @@ const ScraperSettings = ({ settings, settingsError, updateSettings }:ScraperSett
updateSettings('scrape_retry', val)}
/>
diff --git a/components/settings/SearchConsoleSettings.tsx b/components/settings/SearchConsoleSettings.tsx
index a9f4638..8cd36a2 100644
--- a/components/settings/SearchConsoleSettings.tsx
+++ b/components/settings/SearchConsoleSettings.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import ToggleField from '../common/ToggleField';
import InputField from '../common/InputField';
type SearchConsoleSettingsProps = {
@@ -14,7 +13,7 @@ type SearchConsoleSettingsProps = {
const SearchConsoleSettings = ({ settings, settingsError, updateSettings }:SearchConsoleSettingsProps) => {
return (
-
+
{/*
{
Notification
setCurrentTab('searchconsole')}>
- Search Console
+ className={`${tabStyle} ${currentTab === 'integrations' ? tabStyleActive : 'border-transparent'}`}
+ onClick={() => setCurrentTab('integrations')}>
+ Integrations
@@ -131,8 +131,14 @@ const Settings = ({ closeSettings }:SettingsProps) => {
{currentTab === 'notification' && settings && (
)}
- {currentTab === 'searchconsole' && settings && (
-
+ {currentTab === 'integrations' && settings && (
+
)}