mirror of
https://github.com/towfiqi/serpbear
synced 2025-06-26 18:15:54 +00:00
- Integrates Google Adwords API to generate keywords for a domain. - Adds a New Adwords Integration ui inside the App settings > Integrations screen to integrate Google Adwords. - Adds a New Ideas tab under each domain. - Adds ability to automatically generate keyword ideas based on website content, currently tracked keywords, Currently Ranking keywords or custom keywords - The Keyword Ideas are not saved in database, they are saved in a local file inside the data folder. File naming convention: IDEAS_domain.com.json - The keywords can be marked as favorites, and each time a keyword is favorited, they are added in the IDEAS_domain.com.json file.
140 lines
7.7 KiB
TypeScript
140 lines
7.7 KiB
TypeScript
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<HTMLDivElement>(null);
|
|
const searchResultFound = useRef<HTMLDivElement>(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 (
|
|
<div className="IdeaDetails fixed w-full h-screen top-0 left-0 z-[99999]" onClick={closeOnBGClick} data-testid="IdeaDetails">
|
|
<div className="IdeaDetails absolute w-full lg:w-5/12 bg-white customShadow top-0 right-0 h-screen" >
|
|
<div className='IdeaDetails__header p-6 border-b border-b-slate-200 text-slate-500'>
|
|
<h3 className=' text-lg font-bold'>
|
|
<span title={countries[keyword.country][0]}
|
|
className={`fflag fflag-${keyword.country} w-[18px] h-[12px] mr-2`} /> {keyword.keyword}
|
|
<span className='py-1 px-2 ml-2 rounded bg-blue-50 text-blue-700 text-xs font-bold'>
|
|
{formattedNum(keyword.avgMonthlySearches)}/month
|
|
</span>
|
|
</h3>
|
|
<button
|
|
className='absolute top-2 right-2 p-2 px-3 text-gray-400 hover:text-gray-700 transition-all hover:rotate-90'
|
|
onClick={() => closeDetails()}>
|
|
<Icon type='close' size={24} />
|
|
</button>
|
|
</div>
|
|
<div className='IdeaDetails__content p-6'>
|
|
|
|
<div className='IdeaDetails__section'>
|
|
<div className="IdeaDetails__section__head flex justify-between mb-5">
|
|
<h3 className=' font-bold text-gray-700 text-lg'>Search Volume Trend</h3>
|
|
</div>
|
|
<div className='IdeaDetails__section__chart h-64'>
|
|
<Chart labels={chartData.labels} sreies={chartData.sreies} noMaxLimit={true} reverse={false} />
|
|
</div>
|
|
</div>
|
|
<div className='IdeaDetails__section mt-10'>
|
|
<div className="IdeaDetails__section__head flex justify-between items-center pb-4 mb-4 border-b border-b-slate-200">
|
|
<h3 className=' font-bold text-gray-700 lg:text-lg'>Google Search Result
|
|
<a className='text-gray-400 hover:text-indigo-600 inline-block ml-1 px-2 py-1'
|
|
href={`https://www.google.com/search?q=${encodeURI(keyword.keyword)}`}
|
|
target="_blank"
|
|
rel='noreferrer'>
|
|
<Icon type='link' size={14} />
|
|
</a>
|
|
</h3>
|
|
<span className=' text-xs text-gray-500'>{dayjs(updatedDate).format('MMMM D, YYYY')}</span>
|
|
</div>
|
|
<div className={'keywordDetails__section__results styled-scrollbar overflow-y-auto relative'} ref={searchResultContainer}>
|
|
{!searchResultsFetched && (
|
|
<div className=' absolute flex w-full h-full justify-center items-center flex-col z-50 font-semibold'>
|
|
<p>View Google Search Results for "{keyword.keyword}"</p>
|
|
<button
|
|
onClick={() => fetchKeywordSearchResults()}
|
|
className='mt-4 text-sm font-semibold py-2 px-5 rounded cursor-pointer bg-blue-700 text-white '>
|
|
<Icon type={fetchingResult ? 'loading' : 'google'} /> {fetchingResult ? 'Performing' : 'Perform'} Google Search
|
|
</button>
|
|
</div>
|
|
)}
|
|
<div className={`${!searchResultsFetched ? ' blur-sm ' : ''}`}>
|
|
{keywordSearchResult && Array.isArray(keywordSearchResult) && keywordSearchResult.length > 0 && (
|
|
keywordSearchResult.map((item, index) => {
|
|
const { position } = keyword;
|
|
const domainExist = position < 100 && index === (position - 1);
|
|
return (
|
|
<div
|
|
ref={domainExist ? searchResultFound : null}
|
|
className={`leading-6 mb-4 mr-3 p-3 text-sm break-all pr-3 rounded
|
|
${domainExist ? ' bg-amber-50 border border-amber-200' : ''}`}
|
|
key={item.url + item.position}>
|
|
<h4 className='font-semibold text-blue-700'>
|
|
<a href={item.url} target="_blank" rel='noreferrer'>{`${index + 1}. ${item.title}`}</a>
|
|
</h4>
|
|
{/* <p>{item.description}</p> */}
|
|
<a className=' text-green-900' href={item.url} target="_blank" rel='noreferrer'>{item.url}</a>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default IdeaDetails;
|