mirror of
https://github.com/towfiqi/serpbear
synced 2025-06-26 18:15:54 +00:00
refactor: improves Performance & Code Readability
- Replaces useEffet with useMemo & useLayoutEffect where necessary. - Converts some useEffects to separate hooks. - Moves the functions defined within components to utils. - Splits the Keywords renderPosition function to its own component.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
import Icon from './Icon';
|
import Icon from './Icon';
|
||||||
|
import useOnKey from '../../hooks/useOnKey';
|
||||||
|
|
||||||
type ModalProps = {
|
type ModalProps = {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
@@ -9,17 +10,7 @@ type ModalProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Modal = ({ children, width = '1/2', closeModal, title }:ModalProps) => {
|
const Modal = ({ children, width = '1/2', closeModal, title }:ModalProps) => {
|
||||||
useEffect(() => {
|
useOnKey('Escape', closeModal);
|
||||||
const closeModalonEsc = (event:KeyboardEvent) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', closeModalonEsc, false);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', closeModalonEsc, false);
|
|
||||||
};
|
|
||||||
}, [closeModal]);
|
|
||||||
|
|
||||||
const closeOnBGClick = (e:React.SyntheticEvent) => {
|
const closeOnBGClick = (e:React.SyntheticEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Icon from '../common/Icon';
|
import Icon from '../common/Icon';
|
||||||
import Modal from '../common/Modal';
|
import Modal from '../common/Modal';
|
||||||
import { useDeleteDomain, useUpdateDomain } from '../../services/domains';
|
import { useDeleteDomain, useUpdateDomain } from '../../services/domains';
|
||||||
@@ -18,7 +18,10 @@ const DomainSettings = ({ domain, closeModal }: DomainSettingsProps) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [showRemoveDomain, setShowRemoveDomain] = useState<boolean>(false);
|
const [showRemoveDomain, setShowRemoveDomain] = useState<boolean>(false);
|
||||||
const [settingsError, setSettingsError] = useState<DomainSettingsError>({ type: '', msg: '' });
|
const [settingsError, setSettingsError] = useState<DomainSettingsError>({ type: '', msg: '' });
|
||||||
const [domainSettings, setDomainSettings] = useState<DomainSettings>({ notification_interval: 'never', notification_emails: '' });
|
const [domainSettings, setDomainSettings] = useState<DomainSettings>(() => ({
|
||||||
|
notification_interval: domain && domain.notification_interval ? domain.notification_interval : 'never',
|
||||||
|
notification_emails: domain && domain.notification_emails ? domain.notification_emails : '',
|
||||||
|
}));
|
||||||
|
|
||||||
const { mutate: updateMutate } = useUpdateDomain(() => closeModal(false));
|
const { mutate: updateMutate } = useUpdateDomain(() => closeModal(false));
|
||||||
const { mutate: deleteMutate } = useDeleteDomain(() => {
|
const { mutate: deleteMutate } = useDeleteDomain(() => {
|
||||||
@@ -26,12 +29,6 @@ const DomainSettings = ({ domain, closeModal }: DomainSettingsProps) => {
|
|||||||
router.push('/domains');
|
router.push('/domains');
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (domain) {
|
|
||||||
setDomainSettings({ notification_interval: domain.notification_interval, notification_emails: domain.notification_emails });
|
|
||||||
}
|
|
||||||
}, [domain]);
|
|
||||||
|
|
||||||
const updateNotiEmails = (event:React.FormEvent<HTMLInputElement>) => {
|
const updateNotiEmails = (event:React.FormEvent<HTMLInputElement>) => {
|
||||||
setDomainSettings({ ...domainSettings, notification_emails: event.currentTarget.value });
|
setDomainSettings({ ...domainSettings, notification_emails: event.currentTarget.value });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import countries from '../../utils/countries';
|
import countries from '../../utils/countries';
|
||||||
import Icon from '../common/Icon';
|
import Icon from '../common/Icon';
|
||||||
|
import { formattedNum } from '../../utils/client/helpers';
|
||||||
|
|
||||||
type InsightItemProps = {
|
type InsightItemProps = {
|
||||||
item: SCInsightItem,
|
item: SCInsightItem,
|
||||||
@@ -16,7 +17,6 @@ const InsightItem = ({ item, lastItem, type, domain }:InsightItemProps) => {
|
|||||||
firstItem = date && new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(new Date(date));
|
firstItem = date && new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' }).format(new Date(date));
|
||||||
}
|
}
|
||||||
if (type === 'countries') { firstItem = countries[country] && countries[country][0]; }
|
if (type === 'countries') { firstItem = countries[country] && countries[country][0]; }
|
||||||
const formattedNum = (num:number) => new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(num);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -35,7 +35,6 @@ const InsightItem = ({ item, lastItem, type, domain }:InsightItemProps) => {
|
|||||||
{Math.round(position)}
|
{Math.round(position)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div className='keyword_imp text-center inline-block lg:flex-1'>{formattedNum(clicks)}</div> */}
|
|
||||||
<div className={`keyword_position absolute bg-[#f8f9ff] w-fit min-w-[50px] h-14 p-2 text-base mt-[-55px] rounded right-5 lg:relative
|
<div className={`keyword_position absolute bg-[#f8f9ff] w-fit min-w-[50px] h-14 p-2 text-base mt-[-55px] rounded right-5 lg:relative
|
||||||
lg:bg-transparent lg:w-auto lg:h-auto lg:mt-0 lg:p-0 lg:text-sm lg:flex-1 lg:basis-40 lg:grow-0 lg:right-0 text-center font-semibold`}>
|
lg:bg-transparent lg:w-auto lg:h-auto lg:mt-0 lg:p-0 lg:text-sm lg:flex-1 lg:basis-40 lg:grow-0 lg:right-0 text-center font-semibold`}>
|
||||||
{formattedNum(clicks)}
|
{formattedNum(clicks)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useMemo, useState, useEffect } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js';
|
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js';
|
||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
|
import { formattedNum } from '../../utils/client/helpers';
|
||||||
|
|
||||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||||
|
|
||||||
@@ -12,21 +13,15 @@ type InsightStatsProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const InsightStats = ({ stats = [], totalKeywords = 0, totalPages = 0 }:InsightStatsProps) => {
|
const InsightStats = ({ stats = [], totalKeywords = 0, totalPages = 0 }:InsightStatsProps) => {
|
||||||
const formattedNum = (num:number) => new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(num);
|
const totalStat = useMemo(() => {
|
||||||
const [totalStat, setTotalStat] = useState({ impressions: 0, clicks: 0, ctr: 0, position: 0 });
|
return stats.reduce((acc, item) => {
|
||||||
|
return {
|
||||||
useEffect(() => {
|
impressions: item.impressions + acc.impressions,
|
||||||
if (stats.length > 0) {
|
clicks: item.clicks + acc.clicks,
|
||||||
const totalStats = stats.reduce((acc, item) => {
|
ctr: item.ctr + acc.ctr,
|
||||||
return {
|
position: item.position + acc.position,
|
||||||
impressions: item.impressions + acc.impressions,
|
};
|
||||||
clicks: item.clicks + acc.clicks,
|
}, { impressions: 0, clicks: 0, ctr: 0, position: 0 });
|
||||||
ctr: item.ctr + acc.ctr,
|
|
||||||
position: item.position + acc.position,
|
|
||||||
};
|
|
||||||
}, { impressions: 0, clicks: 0, ctr: 0, position: 0 });
|
|
||||||
setTotalStat(totalStats);
|
|
||||||
}
|
|
||||||
}, [stats]);
|
}, [stats]);
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import dayjs from 'dayjs';
|
|||||||
import Icon from '../common/Icon';
|
import Icon from '../common/Icon';
|
||||||
import countries from '../../utils/countries';
|
import countries from '../../utils/countries';
|
||||||
import ChartSlim from '../common/ChartSlim';
|
import ChartSlim from '../common/ChartSlim';
|
||||||
import { generateTheChartData } from '../common/generateChartData';
|
import KeywordPosition from './KeywordPosition';
|
||||||
|
import { generateTheChartData } from '../../utils/client/generateChartData';
|
||||||
|
|
||||||
type KeywordProps = {
|
type KeywordProps = {
|
||||||
keywordData: KeywordType,
|
keywordData: KeywordType,
|
||||||
@@ -82,16 +83,6 @@ const Keyword = (props: KeywordProps) => {
|
|||||||
|
|
||||||
const optionsButtonStyle = 'block px-2 py-2 cursor-pointer hover:bg-indigo-50 hover:text-blue-700';
|
const optionsButtonStyle = 'block px-2 py-2 cursor-pointer hover:bg-indigo-50 hover:text-blue-700';
|
||||||
|
|
||||||
const renderPosition = (pos:number, type?:string) => {
|
|
||||||
if (!updating && pos === 0) {
|
|
||||||
return <span className='text-gray-400' title='Not in Top 100'>{'>100'}</span>;
|
|
||||||
}
|
|
||||||
if (updating && type !== 'sc') {
|
|
||||||
return <span title='Updating Keyword Position'><Icon type="loading" /></span>;
|
|
||||||
}
|
|
||||||
return pos;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={keyword}
|
key={keyword}
|
||||||
@@ -123,7 +114,7 @@ const Keyword = (props: KeywordProps) => {
|
|||||||
<div
|
<div
|
||||||
className={`keyword_position absolute bg-[#f8f9ff] w-fit min-w-[50px] h-12 p-2 text-base mt-[-20px] rounded right-5 lg:relative
|
className={`keyword_position absolute bg-[#f8f9ff] w-fit min-w-[50px] h-12 p-2 text-base mt-[-20px] rounded right-5 lg:relative
|
||||||
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 font-semibold`}>
|
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 font-semibold`}>
|
||||||
{renderPosition(position)}
|
<KeywordPosition position={position} />
|
||||||
{!updating && positionChange > 0 && <i className=' not-italic ml-1 text-xs text-[#5ed7c3]'>▲ {positionChange}</i>}
|
{!updating && positionChange > 0 && <i className=' not-italic ml-1 text-xs text-[#5ed7c3]'>▲ {positionChange}</i>}
|
||||||
{!updating && positionChange < 0 && <i className=' not-italic ml-1 text-xs text-red-300'>▼ {positionChange}</i>}
|
{!updating && positionChange < 0 && <i className=' not-italic ml-1 text-xs text-red-300'>▼ {positionChange}</i>}
|
||||||
</div>
|
</div>
|
||||||
@@ -164,7 +155,10 @@ const Keyword = (props: KeywordProps) => {
|
|||||||
relative flex justify-between text-center lg:flex-1 lg:text-sm lg:m-0 lg:mt-0 lg:border-t-0 lg:pt-0 lg:top-0'>
|
relative flex justify-between text-center lg:flex-1 lg:text-sm lg:m-0 lg:mt-0 lg:border-t-0 lg:pt-0 lg:top-0'>
|
||||||
<span className='min-w-[40px]'>
|
<span className='min-w-[40px]'>
|
||||||
<span className='lg:hidden'>SC Position: </span>
|
<span className='lg:hidden'>SC Position: </span>
|
||||||
{renderPosition(keywordData?.scData?.position[scDataType as keyof KeywordSCDataChild] || 0, 'sc')}
|
<KeywordPosition
|
||||||
|
position={keywordData?.scData?.position[scDataType as keyof KeywordSCDataChild] || 0}
|
||||||
|
type='sc'
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span className='min-w-[40px]'>
|
<span className='min-w-[40px]'>
|
||||||
<span className='lg:hidden'>Impressions: </span>{keywordData?.scData?.impressions[scDataType as keyof KeywordSCDataChild] || 0}
|
<span className='lg:hidden'>Impressions: </span>{keywordData?.scData?.impressions[scDataType as keyof KeywordSCDataChild] || 0}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Icon from '../common/Icon';
|
import Icon from '../common/Icon';
|
||||||
import countries from '../../utils/countries';
|
import countries from '../../utils/countries';
|
||||||
import Chart from '../common/Chart';
|
import Chart from '../common/Chart';
|
||||||
import SelectField from '../common/SelectField';
|
import SelectField from '../common/SelectField';
|
||||||
import { generateTheChartData } from '../common/generateChartData';
|
import { useFetchSingleKeyword } from '../../services/keywords';
|
||||||
|
import useOnKey from '../../hooks/useOnKey';
|
||||||
|
import { generateTheChartData } from '../../utils/client/generateChartData';
|
||||||
|
|
||||||
type KeywordDetailsProps = {
|
type KeywordDetailsProps = {
|
||||||
keyword: KeywordType,
|
keyword: KeywordType,
|
||||||
@@ -13,11 +15,12 @@ type KeywordDetailsProps = {
|
|||||||
|
|
||||||
const KeywordDetails = ({ keyword, closeDetails }:KeywordDetailsProps) => {
|
const KeywordDetails = ({ keyword, closeDetails }:KeywordDetailsProps) => {
|
||||||
const updatedDate = new Date(keyword.lastUpdated);
|
const updatedDate = new Date(keyword.lastUpdated);
|
||||||
const [keywordHistory, setKeywordHistory] = useState<KeywordHistory>(keyword.history);
|
|
||||||
const [keywordSearchResult, setKeywordSearchResult] = useState<KeywordLastResult[]>([]);
|
|
||||||
const [chartTime, setChartTime] = useState<string>('30');
|
const [chartTime, setChartTime] = useState<string>('30');
|
||||||
const searchResultContainer = useRef<HTMLDivElement>(null);
|
const searchResultContainer = useRef<HTMLDivElement>(null);
|
||||||
const searchResultFound = useRef<HTMLDivElement>(null);
|
const searchResultFound = useRef<HTMLDivElement>(null);
|
||||||
|
const { data: keywordData } = useFetchSingleKeyword(keyword.ID);
|
||||||
|
const keywordHistory: KeywordHistory = keywordData?.history || keyword.history;
|
||||||
|
const keywordSearchResult: KeywordLastResult = keywordData?.searchResult || keyword.history;
|
||||||
const dateOptions = [
|
const dateOptions = [
|
||||||
{ label: 'Last 7 Days', value: '7' },
|
{ label: 'Last 7 Days', value: '7' },
|
||||||
{ label: 'Last 30 Days', value: '30' },
|
{ label: 'Last 30 Days', value: '30' },
|
||||||
@@ -26,39 +29,9 @@ const KeywordDetails = ({ keyword, closeDetails }:KeywordDetailsProps) => {
|
|||||||
{ label: 'All Time', value: 'all' },
|
{ label: 'All Time', value: 'all' },
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useOnKey('Escape', closeDetails);
|
||||||
const fetchFullKeyword = async () => {
|
|
||||||
try {
|
|
||||||
const fetchURL = `${window.location.origin}/api/keyword?id=${keyword.ID}`;
|
|
||||||
const res = await fetch(fetchURL, { method: 'GET' }).then((result) => result.json());
|
|
||||||
if (res.keyword) {
|
|
||||||
console.log(res.keyword, new Date().getTime());
|
|
||||||
setKeywordHistory(res.keyword.history || []);
|
|
||||||
setKeywordSearchResult(res.keyword.lastResult || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (keyword.lastResult.length === 0) {
|
|
||||||
fetchFullKeyword();
|
|
||||||
}
|
|
||||||
}, [keyword]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const closeModalonEsc = (event:KeyboardEvent) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
console.log(event.key);
|
|
||||||
closeDetails();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', closeModalonEsc, false);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', closeModalonEsc, false);
|
|
||||||
};
|
|
||||||
}, [closeDetails]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (keyword.position < 100 && keyword.position > 0 && searchResultFound?.current) {
|
if (keyword.position < 100 && keyword.position > 0 && searchResultFound?.current) {
|
||||||
searchResultFound.current.scrollIntoView({
|
searchResultFound.current.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import Icon from '../common/Icon';
|
import Icon from '../common/Icon';
|
||||||
import SelectField, { SelectionOption } from '../common/SelectField';
|
import SelectField, { SelectionOption } from '../common/SelectField';
|
||||||
import countries from '../../utils/countries';
|
import countries from '../../utils/countries';
|
||||||
@@ -17,11 +17,6 @@ type KeywordFilterProps = {
|
|||||||
SCcountries?: string[];
|
SCcountries?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeywordCountState = {
|
|
||||||
desktop: number,
|
|
||||||
mobile: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const KeywordFilters = (props: KeywordFilterProps) => {
|
const KeywordFilters = (props: KeywordFilterProps) => {
|
||||||
const {
|
const {
|
||||||
device,
|
device,
|
||||||
@@ -36,20 +31,14 @@ const KeywordFilters = (props: KeywordFilterProps) => {
|
|||||||
integratedConsole = false,
|
integratedConsole = false,
|
||||||
SCcountries = [],
|
SCcountries = [],
|
||||||
} = props;
|
} = props;
|
||||||
const [keywordCounts, setKeywordCounts] = useState<KeywordCountState>({ desktop: 0, mobile: 0 });
|
|
||||||
const [sortOptions, showSortOptions] = useState(false);
|
const [sortOptions, showSortOptions] = useState(false);
|
||||||
const [filterOptions, showFilterOptions] = useState(false);
|
const [filterOptions, showFilterOptions] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
const keywordCounts = useMemo(() => {
|
||||||
const keyWordCount = { desktop: 0, mobile: 0 };
|
return keywords.reduce((acc, k) => ({
|
||||||
keywords.forEach((k) => {
|
desktop: k.device === 'desktop' ? acc.desktop + 1 : acc.desktop,
|
||||||
if (k.device === 'desktop') {
|
mobile: k.device !== 'desktop' ? acc.mobile + 1 : acc.mobile,
|
||||||
keyWordCount.desktop += 1;
|
}), { desktop: 0, mobile: 0 });
|
||||||
} else {
|
|
||||||
keyWordCount.mobile += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setKeywordCounts(keyWordCount);
|
|
||||||
}, [keywords]);
|
}, [keywords]);
|
||||||
|
|
||||||
const filterCountry = (cntrs:string[]) => filterKeywords({ ...filterParams, countries: cntrs });
|
const filterCountry = (cntrs:string[]) => filterKeywords({ ...filterParams, countries: cntrs });
|
||||||
|
|||||||
19
components/keywords/KeywordPosition.tsx
Normal file
19
components/keywords/KeywordPosition.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Icon from '../common/Icon';
|
||||||
|
|
||||||
|
type KeywordPositionProps = {
|
||||||
|
position: number,
|
||||||
|
updating?: boolean,
|
||||||
|
type?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeywordPosition = ({ position = 0, type = '', updating = false }:KeywordPositionProps) => {
|
||||||
|
if (!updating && position === 0) {
|
||||||
|
return <span className='text-gray-400' title='Not in Top 100'>{'>100'}</span>;
|
||||||
|
}
|
||||||
|
if (updating && type !== 'sc') {
|
||||||
|
return <span title='Updating Keyword Position'><Icon type="loading" /></span>;
|
||||||
|
}
|
||||||
|
return <>{Math.round(position)}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeywordPosition;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from 'react-hot-toast';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import { CSSTransition } from 'react-transition-group';
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||||
@@ -12,6 +12,8 @@ import Modal from '../common/Modal';
|
|||||||
import { useDeleteKeywords, useFavKeywords, useRefreshKeywords } from '../../services/keywords';
|
import { useDeleteKeywords, useFavKeywords, useRefreshKeywords } from '../../services/keywords';
|
||||||
import KeywordTagManager from './KeywordTagManager';
|
import KeywordTagManager from './KeywordTagManager';
|
||||||
import AddTags from './AddTags';
|
import AddTags from './AddTags';
|
||||||
|
import useWindowResize from '../../hooks/useWindowResize';
|
||||||
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
|
|
||||||
type KeywordsTableProps = {
|
type KeywordsTableProps = {
|
||||||
domain: DomainType | null,
|
domain: DomainType | null,
|
||||||
@@ -31,7 +33,6 @@ const KeywordsTable = (props: KeywordsTableProps) => {
|
|||||||
const [showRemoveModal, setShowRemoveModal] = useState<boolean>(false);
|
const [showRemoveModal, setShowRemoveModal] = useState<boolean>(false);
|
||||||
const [showTagManager, setShowTagManager] = useState<null|number>(null);
|
const [showTagManager, setShowTagManager] = useState<null|number>(null);
|
||||||
const [showAddTags, setShowAddTags] = useState<boolean>(false);
|
const [showAddTags, setShowAddTags] = useState<boolean>(false);
|
||||||
const [isMobile, setIsMobile] = useState<boolean>(false);
|
|
||||||
const [SCListHeight, setSCListHeight] = useState(500);
|
const [SCListHeight, setSCListHeight] = useState(500);
|
||||||
const [filterParams, setFilterParams] = useState<KeywordFilters>({ countries: [], tags: [], search: '' });
|
const [filterParams, setFilterParams] = useState<KeywordFilters>({ countries: [], tags: [], search: '' });
|
||||||
const [sortBy, setSortBy] = useState<string>('date_asc');
|
const [sortBy, setSortBy] = useState<string>('date_asc');
|
||||||
@@ -40,6 +41,8 @@ const KeywordsTable = (props: KeywordsTableProps) => {
|
|||||||
const { mutate: deleteMutate } = useDeleteKeywords(() => {});
|
const { mutate: deleteMutate } = useDeleteKeywords(() => {});
|
||||||
const { mutate: favoriteMutate } = useFavKeywords(() => {});
|
const { mutate: favoriteMutate } = useFavKeywords(() => {});
|
||||||
const { mutate: refreshMutate } = useRefreshKeywords(() => {});
|
const { mutate: refreshMutate } = useRefreshKeywords(() => {});
|
||||||
|
const [isMobile] = useIsMobile();
|
||||||
|
useWindowResize(() => setSCListHeight(window.innerHeight - (isMobile ? 200 : 400)));
|
||||||
|
|
||||||
const scDataObject:{ [k:string] : string} = {
|
const scDataObject:{ [k:string] : string} = {
|
||||||
threeDays: 'Last Three Days',
|
threeDays: 'Last Three Days',
|
||||||
@@ -50,16 +53,6 @@ const KeywordsTable = (props: KeywordsTableProps) => {
|
|||||||
avgThirtyDays: 'Last Thirty Days Avg',
|
avgThirtyDays: 'Last Thirty Days Avg',
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsMobile(!!(window.matchMedia('only screen and (max-width: 760px)').matches));
|
|
||||||
const resizeList = () => setSCListHeight(window.innerHeight - (isMobile ? 200 : 400));
|
|
||||||
resizeList();
|
|
||||||
window.addEventListener('resize', resizeList);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', resizeList);
|
|
||||||
};
|
|
||||||
}, [isMobile]);
|
|
||||||
|
|
||||||
const processedKeywords: {[key:string] : KeywordType[]} = useMemo(() => {
|
const processedKeywords: {[key:string] : KeywordType[]} = useMemo(() => {
|
||||||
const procKeywords = keywords.filter((x) => x.device === device);
|
const procKeywords = keywords.filter((x) => x.device === device);
|
||||||
const filteredKeywords = filterKeywords(procKeywords, filterParams);
|
const filteredKeywords = filterKeywords(procKeywords, filterParams);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from '../common/Icon';
|
import Icon from '../common/Icon';
|
||||||
import countries from '../../utils/countries';
|
import countries from '../../utils/countries';
|
||||||
|
import KeywordPosition from './KeywordPosition';
|
||||||
|
import { formattedNum } from '../../utils/client/helpers';
|
||||||
|
|
||||||
type SCKeywordProps = {
|
type SCKeywordProps = {
|
||||||
keywordData: SearchAnalyticsItem,
|
keywordData: SearchAnalyticsItem,
|
||||||
@@ -15,13 +17,6 @@ const SCKeyword = (props: SCKeywordProps) => {
|
|||||||
const { keywordData, selected, lastItem, selectKeyword, style, isTracked = false } = props;
|
const { keywordData, selected, lastItem, selectKeyword, style, isTracked = false } = props;
|
||||||
const { keyword, uid, position, country, impressions, ctr, clicks } = keywordData;
|
const { keyword, uid, position, country, impressions, ctr, clicks } = keywordData;
|
||||||
|
|
||||||
const renderPosition = () => {
|
|
||||||
if (position === 0) {
|
|
||||||
return <span className='text-gray-400' title='Not in Top 100'>{'>100'}</span>;
|
|
||||||
}
|
|
||||||
return Math.round(position);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={keyword}
|
key={keyword}
|
||||||
@@ -45,7 +40,7 @@ const SCKeyword = (props: SCKeywordProps) => {
|
|||||||
|
|
||||||
<div className={`keyword_position absolute bg-[#f8f9ff] w-fit min-w-[50px] h-15 p-2 text-base mt-[-20px] rounded right-5 lg:relative
|
<div className={`keyword_position absolute bg-[#f8f9ff] w-fit min-w-[50px] h-15 p-2 text-base mt-[-20px] rounded right-5 lg:relative
|
||||||
lg:bg-transparent lg:w-auto lg:h-auto lg:mt-0 lg:p-0 lg:text-sm lg:flex-1 lg:basis-40 lg:grow-0 lg:right-0 text-center font-semibold`}>
|
lg:bg-transparent lg:w-auto lg:h-auto lg:mt-0 lg:p-0 lg:text-sm lg:flex-1 lg:basis-40 lg:grow-0 lg:right-0 text-center font-semibold`}>
|
||||||
{renderPosition()}
|
<KeywordPosition position={position} />
|
||||||
<span className='block text-xs text-gray-500 lg:hidden'>Position</span>
|
<span className='block text-xs text-gray-500 lg:hidden'>Position</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,14 +48,14 @@ const SCKeyword = (props: SCKeywordProps) => {
|
|||||||
<span className='mr-3 lg:hidden'>
|
<span className='mr-3 lg:hidden'>
|
||||||
<Icon type="eye" size={14} color="#999" />
|
<Icon type="eye" size={14} color="#999" />
|
||||||
</span>
|
</span>
|
||||||
{new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(impressions)}
|
{formattedNum(impressions)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'keyword_visits text-center inline-block mt-4 mr-5 ml-5 lg:flex-1 lg:m-0 max-w-[70px] lg:max-w-none lg:pr-5'}>
|
<div className={'keyword_visits text-center inline-block mt-4 mr-5 ml-5 lg:flex-1 lg:m-0 max-w-[70px] lg:max-w-none lg:pr-5'}>
|
||||||
<span className='mr-3 lg:hidden'>
|
<span className='mr-3 lg:hidden'>
|
||||||
<Icon type="cursor" size={14} color="#999" />
|
<Icon type="cursor" size={14} color="#999" />
|
||||||
</span>
|
</span>
|
||||||
{new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(clicks)}
|
{formattedNum(clicks)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='keyword_ctr text-center inline-block mt-4 relative lg:flex-1 lg:m-0 '>
|
<div className='keyword_ctr text-center inline-block mt-4 relative lg:flex-1 lg:m-0 '>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from 'react-hot-toast';
|
||||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||||
import { useAddKeywords, useFetchKeywords } from '../../services/keywords';
|
import { useAddKeywords, useFetchKeywords } from '../../services/keywords';
|
||||||
@@ -7,6 +7,9 @@ import { SCfilterKeywords, SCkeywordsByDevice, SCsortKeywords } from '../../util
|
|||||||
import Icon from '../common/Icon';
|
import Icon from '../common/Icon';
|
||||||
import KeywordFilters from './KeywordFilter';
|
import KeywordFilters from './KeywordFilter';
|
||||||
import SCKeyword from './SCKeyword';
|
import SCKeyword from './SCKeyword';
|
||||||
|
import useWindowResize from '../../hooks/useWindowResize';
|
||||||
|
import useIsMobile from '../../hooks/useIsMobile';
|
||||||
|
import { formattedNum } from '../../utils/client/helpers';
|
||||||
|
|
||||||
type SCKeywordsTableProps = {
|
type SCKeywordsTableProps = {
|
||||||
domain: DomainType | null,
|
domain: DomainType | null,
|
||||||
@@ -27,11 +30,13 @@ const SCKeywordsTable = ({ domain, keywords = [], isLoading = true, isConsoleInt
|
|||||||
const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]);
|
const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]);
|
||||||
const [filterParams, setFilterParams] = useState<KeywordFilters>({ countries: [], tags: [], search: '' });
|
const [filterParams, setFilterParams] = useState<KeywordFilters>({ countries: [], tags: [], search: '' });
|
||||||
const [sortBy, setSortBy] = useState<string>('imp_desc');
|
const [sortBy, setSortBy] = useState<string>('imp_desc');
|
||||||
const [isMobile, setIsMobile] = useState<boolean>(false);
|
|
||||||
const [SCListHeight, setSCListHeight] = useState(500);
|
const [SCListHeight, setSCListHeight] = useState(500);
|
||||||
const { keywordsData } = useFetchKeywords(router);
|
const { keywordsData } = useFetchKeywords(router);
|
||||||
const addedkeywords: string[] = keywordsData?.keywords?.map((key: KeywordType) => `${key.keyword}:${key.country}:${key.device}`) || [];
|
const addedkeywords: string[] = keywordsData?.keywords?.map((key: KeywordType) => `${key.keyword}:${key.country}:${key.device}`) || [];
|
||||||
const { mutate: addKeywords } = useAddKeywords(() => { if (domain && domain.slug) router.push(`/domain/${domain.slug}`); });
|
const { mutate: addKeywords } = useAddKeywords(() => { if (domain && domain.slug) router.push(`/domain/${domain.slug}`); });
|
||||||
|
const [isMobile] = useIsMobile();
|
||||||
|
useWindowResize(() => setSCListHeight(window.innerHeight - (isMobile ? 200 : 400)));
|
||||||
|
|
||||||
const finalKeywords: {[key:string] : SCKeywordType[] } = useMemo(() => {
|
const finalKeywords: {[key:string] : SCKeywordType[] } = useMemo(() => {
|
||||||
const procKeywords = keywords.filter((x) => x.device === device);
|
const procKeywords = keywords.filter((x) => x.device === device);
|
||||||
const filteredKeywords = SCfilterKeywords(procKeywords, filterParams);
|
const filteredKeywords = SCfilterKeywords(procKeywords, filterParams);
|
||||||
@@ -71,16 +76,6 @@ const SCKeywordsTable = ({ domain, keywords = [], isLoading = true, isConsoleInt
|
|||||||
};
|
};
|
||||||
}, [finalKeywords, device]);
|
}, [finalKeywords, device]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsMobile(!!(window.matchMedia('only screen and (max-width: 760px)').matches));
|
|
||||||
const resizeList = () => setSCListHeight(window.innerHeight - (isMobile ? 200 : 400));
|
|
||||||
resizeList();
|
|
||||||
window.addEventListener('resize', resizeList);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', resizeList);
|
|
||||||
};
|
|
||||||
}, [isMobile]);
|
|
||||||
|
|
||||||
const selectKeyword = (keywordID: string) => {
|
const selectKeyword = (keywordID: string) => {
|
||||||
console.log('Select Keyword: ', keywordID);
|
console.log('Select Keyword: ', keywordID);
|
||||||
let updatedSelectd = [...selectedKeywords, keywordID];
|
let updatedSelectd = [...selectedKeywords, keywordID];
|
||||||
@@ -194,10 +189,10 @@ const SCKeywordsTable = ({ domain, keywords = [], isLoading = true, isConsoleInt
|
|||||||
</span>
|
</span>
|
||||||
<span className='domKeywords_head_position flex-1 basis-40 grow-0 text-center'>{viewSummary.position}</span>
|
<span className='domKeywords_head_position flex-1 basis-40 grow-0 text-center'>{viewSummary.position}</span>
|
||||||
<span className='domKeywords_head_imp flex-1 text-center'>
|
<span className='domKeywords_head_imp flex-1 text-center'>
|
||||||
{new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(viewSummary.impressions)}
|
{formattedNum(viewSummary.impressions)}
|
||||||
</span>
|
</span>
|
||||||
<span className='domKeywords_head_visits flex-1 text-center'>
|
<span className='domKeywords_head_visits flex-1 text-center'>
|
||||||
{new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(viewSummary.visits)}
|
{formattedNum(viewSummary.visits)}
|
||||||
</span>
|
</span>
|
||||||
<span className='domKeywords_head_ctr flex-1 text-center'>
|
<span className='domKeywords_head_ctr flex-1 text-center'>
|
||||||
{new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(viewSummary.ctr)}%
|
{new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(viewSummary.ctr)}%
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useFetchSettings, useUpdateSettings } from '../../services/settings';
|
|||||||
import Icon from '../common/Icon';
|
import Icon from '../common/Icon';
|
||||||
import NotificationSettings from './NotificationSettings';
|
import NotificationSettings from './NotificationSettings';
|
||||||
import ScraperSettings from './ScraperSettings';
|
import ScraperSettings from './ScraperSettings';
|
||||||
|
import useOnKey from '../../hooks/useOnKey';
|
||||||
|
|
||||||
type SettingsProps = {
|
type SettingsProps = {
|
||||||
closeSettings: Function,
|
closeSettings: Function,
|
||||||
@@ -34,6 +35,7 @@ const Settings = ({ closeSettings }:SettingsProps) => {
|
|||||||
const [settingsError, setSettingsError] = useState<SettingsError|null>(null);
|
const [settingsError, setSettingsError] = useState<SettingsError|null>(null);
|
||||||
const { mutate: updateMutate, isLoading: isUpdating } = useUpdateSettings(() => console.log(''));
|
const { mutate: updateMutate, isLoading: isUpdating } = useUpdateSettings(() => console.log(''));
|
||||||
const { data: appSettings, isLoading } = useFetchSettings();
|
const { data: appSettings, isLoading } = useFetchSettings();
|
||||||
|
useOnKey('Escape', closeSettings);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appSettings && appSettings.settings) {
|
if (appSettings && appSettings.settings) {
|
||||||
@@ -41,19 +43,6 @@ const Settings = ({ closeSettings }:SettingsProps) => {
|
|||||||
}
|
}
|
||||||
}, [appSettings]);
|
}, [appSettings]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const closeModalonEsc = (event:KeyboardEvent) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
console.log(event.key);
|
|
||||||
closeSettings();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('keydown', closeModalonEsc, false);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', closeModalonEsc, false);
|
|
||||||
};
|
|
||||||
}, [closeSettings]);
|
|
||||||
|
|
||||||
const closeOnBGClick = (e:React.SyntheticEvent) => {
|
const closeOnBGClick = (e:React.SyntheticEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.nativeEvent.stopImmediatePropagation();
|
e.nativeEvent.stopImmediatePropagation();
|
||||||
|
|||||||
12
hooks/useIsMobile.tsx
Normal file
12
hooks/useIsMobile.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const useIsMobile = () => {
|
||||||
|
const [isMobile, setIsMobile] = useState<boolean>(false);
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMobile(!!(window.matchMedia('only screen and (max-width: 760px)').matches));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return [isMobile];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useIsMobile;
|
||||||
17
hooks/useOnKey.tsx
Normal file
17
hooks/useOnKey.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const useOnKey = (key:string, onPress: Function) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const closeModalonEsc = (event:KeyboardEvent) => {
|
||||||
|
if (event.key === key) {
|
||||||
|
onPress();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', closeModalonEsc, false);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', closeModalonEsc, false);
|
||||||
|
};
|
||||||
|
}, [key, onPress]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useOnKey;
|
||||||
13
hooks/useWindowResize.tsx
Normal file
13
hooks/useWindowResize.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
const useWindowResize = (onResize: () => void) => {
|
||||||
|
useEffect(() => {
|
||||||
|
onResize();
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
};
|
||||||
|
}, [onResize]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useWindowResize;
|
||||||
@@ -153,3 +153,23 @@ export function useRefreshKeywords(onSuccess:Function) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useFetchSingleKeyword(keywordID:number) {
|
||||||
|
return useQuery(['keyword', keywordID], async () => {
|
||||||
|
try {
|
||||||
|
const fetchURL = `${window.location.origin}/api/keyword?id=${keywordID}`;
|
||||||
|
const res = await fetch(fetchURL, { method: 'GET' }).then((result) => result.json());
|
||||||
|
if (res.status >= 400 && res.status < 600) {
|
||||||
|
throw new Error('Bad response from server');
|
||||||
|
}
|
||||||
|
return { history: res.keyword.history || [], searchResult: res.keyword.lastResult || [] };
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Error Loading Keyword Details');
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
onError: () => {
|
||||||
|
console.log('Error Loading Keyword Data!!!');
|
||||||
|
toast('Error Loading Keyword Details.', { icon: '⚠️' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
2
utils/client/helpers.ts
Normal file
2
utils/client/helpers.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
export const formattedNum = (num:number) => new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(num);
|
||||||
Reference in New Issue
Block a user