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:
parent
2783de5c65
commit
4a47cedad8
@ -1,5 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import useOnKey from '../../hooks/useOnKey';
|
||||
|
||||
type ModalProps = {
|
||||
children: React.ReactNode,
|
||||
@ -9,17 +10,7 @@ type ModalProps = {
|
||||
}
|
||||
|
||||
const Modal = ({ children, width = '1/2', closeModal, title }:ModalProps) => {
|
||||
useEffect(() => {
|
||||
const closeModalonEsc = (event:KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', closeModalonEsc, false);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', closeModalonEsc, false);
|
||||
};
|
||||
}, [closeModal]);
|
||||
useOnKey('Escape', closeModal);
|
||||
|
||||
const closeOnBGClick = (e:React.SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import Icon from '../common/Icon';
|
||||
import Modal from '../common/Modal';
|
||||
import { useDeleteDomain, useUpdateDomain } from '../../services/domains';
|
||||
@ -18,7 +18,10 @@ const DomainSettings = ({ domain, closeModal }: DomainSettingsProps) => {
|
||||
const router = useRouter();
|
||||
const [showRemoveDomain, setShowRemoveDomain] = useState<boolean>(false);
|
||||
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: deleteMutate } = useDeleteDomain(() => {
|
||||
@ -26,12 +29,6 @@ const DomainSettings = ({ domain, closeModal }: DomainSettingsProps) => {
|
||||
router.push('/domains');
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (domain) {
|
||||
setDomainSettings({ notification_interval: domain.notification_interval, notification_emails: domain.notification_emails });
|
||||
}
|
||||
}, [domain]);
|
||||
|
||||
const updateNotiEmails = (event:React.FormEvent<HTMLInputElement>) => {
|
||||
setDomainSettings({ ...domainSettings, notification_emails: event.currentTarget.value });
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import countries from '../../utils/countries';
|
||||
import Icon from '../common/Icon';
|
||||
import { formattedNum } from '../../utils/client/helpers';
|
||||
|
||||
type InsightItemProps = {
|
||||
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));
|
||||
}
|
||||
if (type === 'countries') { firstItem = countries[country] && countries[country][0]; }
|
||||
const formattedNum = (num:number) => new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(num);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -35,7 +35,6 @@ const InsightItem = ({ item, lastItem, type, domain }:InsightItemProps) => {
|
||||
{Math.round(position)}
|
||||
</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
|
||||
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)}
|
||||
|
@ -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 { Line } from 'react-chartjs-2';
|
||||
import { formattedNum } from '../../utils/client/helpers';
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||
|
||||
@ -12,21 +13,15 @@ type InsightStatsProps = {
|
||||
}
|
||||
|
||||
const InsightStats = ({ stats = [], totalKeywords = 0, totalPages = 0 }:InsightStatsProps) => {
|
||||
const formattedNum = (num:number) => new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(num);
|
||||
const [totalStat, setTotalStat] = useState({ impressions: 0, clicks: 0, ctr: 0, position: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (stats.length > 0) {
|
||||
const totalStats = stats.reduce((acc, item) => {
|
||||
return {
|
||||
impressions: item.impressions + acc.impressions,
|
||||
clicks: item.clicks + acc.clicks,
|
||||
ctr: item.ctr + acc.ctr,
|
||||
position: item.position + acc.position,
|
||||
};
|
||||
}, { impressions: 0, clicks: 0, ctr: 0, position: 0 });
|
||||
setTotalStat(totalStats);
|
||||
}
|
||||
const totalStat = useMemo(() => {
|
||||
return stats.reduce((acc, item) => {
|
||||
return {
|
||||
impressions: item.impressions + acc.impressions,
|
||||
clicks: item.clicks + acc.clicks,
|
||||
ctr: item.ctr + acc.ctr,
|
||||
position: item.position + acc.position,
|
||||
};
|
||||
}, { impressions: 0, clicks: 0, ctr: 0, position: 0 });
|
||||
}, [stats]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
|
@ -4,7 +4,8 @@ import dayjs from 'dayjs';
|
||||
import Icon from '../common/Icon';
|
||||
import countries from '../../utils/countries';
|
||||
import ChartSlim from '../common/ChartSlim';
|
||||
import { generateTheChartData } from '../common/generateChartData';
|
||||
import KeywordPosition from './KeywordPosition';
|
||||
import { generateTheChartData } from '../../utils/client/generateChartData';
|
||||
|
||||
type KeywordProps = {
|
||||
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 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 (
|
||||
<div
|
||||
key={keyword}
|
||||
@ -123,7 +114,7 @@ const Keyword = (props: KeywordProps) => {
|
||||
<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
|
||||
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-red-300'>▼ {positionChange}</i>}
|
||||
</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'>
|
||||
<span className='min-w-[40px]'>
|
||||
<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 className='min-w-[40px]'>
|
||||
<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 Icon from '../common/Icon';
|
||||
import countries from '../../utils/countries';
|
||||
import Chart from '../common/Chart';
|
||||
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 = {
|
||||
keyword: KeywordType,
|
||||
@ -13,11 +15,12 @@ type KeywordDetailsProps = {
|
||||
|
||||
const KeywordDetails = ({ keyword, closeDetails }:KeywordDetailsProps) => {
|
||||
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 searchResultContainer = 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 = [
|
||||
{ label: 'Last 7 Days', value: '7' },
|
||||
{ label: 'Last 30 Days', value: '30' },
|
||||
@ -26,39 +29,9 @@ const KeywordDetails = ({ keyword, closeDetails }:KeywordDetailsProps) => {
|
||||
{ label: 'All Time', value: 'all' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
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]);
|
||||
useOnKey('Escape', closeDetails);
|
||||
|
||||
useEffect(() => {
|
||||
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(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (keyword.position < 100 && keyword.position > 0 && searchResultFound?.current) {
|
||||
searchResultFound.current.scrollIntoView({
|
||||
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 SelectField, { SelectionOption } from '../common/SelectField';
|
||||
import countries from '../../utils/countries';
|
||||
@ -17,11 +17,6 @@ type KeywordFilterProps = {
|
||||
SCcountries?: string[];
|
||||
}
|
||||
|
||||
type KeywordCountState = {
|
||||
desktop: number,
|
||||
mobile: number
|
||||
}
|
||||
|
||||
const KeywordFilters = (props: KeywordFilterProps) => {
|
||||
const {
|
||||
device,
|
||||
@ -36,20 +31,14 @@ const KeywordFilters = (props: KeywordFilterProps) => {
|
||||
integratedConsole = false,
|
||||
SCcountries = [],
|
||||
} = props;
|
||||
const [keywordCounts, setKeywordCounts] = useState<KeywordCountState>({ desktop: 0, mobile: 0 });
|
||||
const [sortOptions, showSortOptions] = useState(false);
|
||||
const [filterOptions, showFilterOptions] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const keyWordCount = { desktop: 0, mobile: 0 };
|
||||
keywords.forEach((k) => {
|
||||
if (k.device === 'desktop') {
|
||||
keyWordCount.desktop += 1;
|
||||
} else {
|
||||
keyWordCount.mobile += 1;
|
||||
}
|
||||
});
|
||||
setKeywordCounts(keyWordCount);
|
||||
const keywordCounts = useMemo(() => {
|
||||
return keywords.reduce((acc, k) => ({
|
||||
desktop: k.device === 'desktop' ? acc.desktop + 1 : acc.desktop,
|
||||
mobile: k.device !== 'desktop' ? acc.mobile + 1 : acc.mobile,
|
||||
}), { desktop: 0, mobile: 0 });
|
||||
}, [keywords]);
|
||||
|
||||
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 { CSSTransition } from 'react-transition-group';
|
||||
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 KeywordTagManager from './KeywordTagManager';
|
||||
import AddTags from './AddTags';
|
||||
import useWindowResize from '../../hooks/useWindowResize';
|
||||
import useIsMobile from '../../hooks/useIsMobile';
|
||||
|
||||
type KeywordsTableProps = {
|
||||
domain: DomainType | null,
|
||||
@ -31,7 +33,6 @@ const KeywordsTable = (props: KeywordsTableProps) => {
|
||||
const [showRemoveModal, setShowRemoveModal] = useState<boolean>(false);
|
||||
const [showTagManager, setShowTagManager] = useState<null|number>(null);
|
||||
const [showAddTags, setShowAddTags] = useState<boolean>(false);
|
||||
const [isMobile, setIsMobile] = useState<boolean>(false);
|
||||
const [SCListHeight, setSCListHeight] = useState(500);
|
||||
const [filterParams, setFilterParams] = useState<KeywordFilters>({ countries: [], tags: [], search: '' });
|
||||
const [sortBy, setSortBy] = useState<string>('date_asc');
|
||||
@ -40,6 +41,8 @@ const KeywordsTable = (props: KeywordsTableProps) => {
|
||||
const { mutate: deleteMutate } = useDeleteKeywords(() => {});
|
||||
const { mutate: favoriteMutate } = useFavKeywords(() => {});
|
||||
const { mutate: refreshMutate } = useRefreshKeywords(() => {});
|
||||
const [isMobile] = useIsMobile();
|
||||
useWindowResize(() => setSCListHeight(window.innerHeight - (isMobile ? 200 : 400)));
|
||||
|
||||
const scDataObject:{ [k:string] : string} = {
|
||||
threeDays: 'Last Three Days',
|
||||
@ -50,16 +53,6 @@ const KeywordsTable = (props: KeywordsTableProps) => {
|
||||
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 procKeywords = keywords.filter((x) => x.device === device);
|
||||
const filteredKeywords = filterKeywords(procKeywords, filterParams);
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import Icon from '../common/Icon';
|
||||
import countries from '../../utils/countries';
|
||||
import KeywordPosition from './KeywordPosition';
|
||||
import { formattedNum } from '../../utils/client/helpers';
|
||||
|
||||
type SCKeywordProps = {
|
||||
keywordData: SearchAnalyticsItem,
|
||||
@ -15,13 +17,6 @@ const SCKeyword = (props: SCKeywordProps) => {
|
||||
const { keywordData, selected, lastItem, selectKeyword, style, isTracked = false } = props;
|
||||
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 (
|
||||
<div
|
||||
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
|
||||
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>
|
||||
</div>
|
||||
|
||||
@ -53,14 +48,14 @@ const SCKeyword = (props: SCKeywordProps) => {
|
||||
<span className='mr-3 lg:hidden'>
|
||||
<Icon type="eye" size={14} color="#999" />
|
||||
</span>
|
||||
{new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(impressions)}
|
||||
{formattedNum(impressions)}
|
||||
</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'}>
|
||||
<span className='mr-3 lg:hidden'>
|
||||
<Icon type="cursor" size={14} color="#999" />
|
||||
</span>
|
||||
{new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(clicks)}
|
||||
{formattedNum(clicks)}
|
||||
</div>
|
||||
|
||||
<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 React, { useState, useMemo, useEffect } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import { useAddKeywords, useFetchKeywords } from '../../services/keywords';
|
||||
@ -7,6 +7,9 @@ import { SCfilterKeywords, SCkeywordsByDevice, SCsortKeywords } from '../../util
|
||||
import Icon from '../common/Icon';
|
||||
import KeywordFilters from './KeywordFilter';
|
||||
import SCKeyword from './SCKeyword';
|
||||
import useWindowResize from '../../hooks/useWindowResize';
|
||||
import useIsMobile from '../../hooks/useIsMobile';
|
||||
import { formattedNum } from '../../utils/client/helpers';
|
||||
|
||||
type SCKeywordsTableProps = {
|
||||
domain: DomainType | null,
|
||||
@ -27,11 +30,13 @@ const SCKeywordsTable = ({ domain, keywords = [], isLoading = true, isConsoleInt
|
||||
const [selectedKeywords, setSelectedKeywords] = useState<string[]>([]);
|
||||
const [filterParams, setFilterParams] = useState<KeywordFilters>({ countries: [], tags: [], search: '' });
|
||||
const [sortBy, setSortBy] = useState<string>('imp_desc');
|
||||
const [isMobile, setIsMobile] = useState<boolean>(false);
|
||||
const [SCListHeight, setSCListHeight] = useState(500);
|
||||
const { keywordsData } = useFetchKeywords(router);
|
||||
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 [isMobile] = useIsMobile();
|
||||
useWindowResize(() => setSCListHeight(window.innerHeight - (isMobile ? 200 : 400)));
|
||||
|
||||
const finalKeywords: {[key:string] : SCKeywordType[] } = useMemo(() => {
|
||||
const procKeywords = keywords.filter((x) => x.device === device);
|
||||
const filteredKeywords = SCfilterKeywords(procKeywords, filterParams);
|
||||
@ -71,16 +76,6 @@ const SCKeywordsTable = ({ domain, keywords = [], isLoading = true, isConsoleInt
|
||||
};
|
||||
}, [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) => {
|
||||
console.log('Select Keyword: ', keywordID);
|
||||
let updatedSelectd = [...selectedKeywords, keywordID];
|
||||
@ -194,10 +189,10 @@ const SCKeywordsTable = ({ domain, keywords = [], isLoading = true, isConsoleInt
|
||||
</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'>
|
||||
{new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(viewSummary.impressions)}
|
||||
{formattedNum(viewSummary.impressions)}
|
||||
</span>
|
||||
<span className='domKeywords_head_visits flex-1 text-center'>
|
||||
{new Intl.NumberFormat('en-IN', { maximumSignificantDigits: 3 }).format(viewSummary.visits)}
|
||||
{formattedNum(viewSummary.visits)}
|
||||
</span>
|
||||
<span className='domKeywords_head_ctr flex-1 text-center'>
|
||||
{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 NotificationSettings from './NotificationSettings';
|
||||
import ScraperSettings from './ScraperSettings';
|
||||
import useOnKey from '../../hooks/useOnKey';
|
||||
|
||||
type SettingsProps = {
|
||||
closeSettings: Function,
|
||||
@ -34,6 +35,7 @@ const Settings = ({ closeSettings }:SettingsProps) => {
|
||||
const [settingsError, setSettingsError] = useState<SettingsError|null>(null);
|
||||
const { mutate: updateMutate, isLoading: isUpdating } = useUpdateSettings(() => console.log(''));
|
||||
const { data: appSettings, isLoading } = useFetchSettings();
|
||||
useOnKey('Escape', closeSettings);
|
||||
|
||||
useEffect(() => {
|
||||
if (appSettings && appSettings.settings) {
|
||||
@ -41,19 +43,6 @@ const Settings = ({ closeSettings }:SettingsProps) => {
|
||||
}
|
||||
}, [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) => {
|
||||
e.stopPropagation();
|
||||
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);
|
Loading…
Reference in New Issue
Block a user