6 Commits

Author SHA1 Message Date
towfiqi
08f44911d7 chore(release): 0.3.3 2023-11-12 20:59:08 +06:00
towfiqi
9dce1d5b48 chore: resolves test issues 2023-11-12 20:58:03 +06:00
towfiqi
fbd23ede25 feat: Shows total keywords count in domains page 2023-11-11 12:48:31 +06:00
towfiqi
60c68bd339 feat: Adds ability to visit pages from Insight tab 2023-11-11 11:42:14 +06:00
towfiqi
2339e31af9 feat: Domains now show their favicon.
closes #130
2023-11-11 11:29:59 +06:00
towfiqi
4a60271cac fix: Resolves Website Thumbnail missing issue
You can now set your own thum.io key to fetch screenshot. More in docs.

closes #131
2023-11-11 11:17:44 +06:00
33 changed files with 474 additions and 205 deletions

View File

@@ -12,6 +12,7 @@
"no-await-in-loop": "off",
"arrow-body-style":"off",
"max-len": ["error", {"code": 150, "ignoreComments": true, "ignoreUrls": true}],
"import/no-extraneous-dependencies": "off",
"import/extensions": [
"error",
"ignorePackages",

View File

@@ -2,6 +2,20 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.3.3](https://github.com/towfiqi/serpbear/compare/v0.3.2...v0.3.3) (2023-11-12)
### Features
* Adds ability to visit pages from Insight tab ([60c68bd](https://github.com/towfiqi/serpbear/commit/60c68bd339db7aeed35aea035dd21691702ffee3))
* Domains now show their favicon. ([2339e31](https://github.com/towfiqi/serpbear/commit/2339e31af9e90bf918f5bcd4f23114f38cef0313)), closes [#130](https://github.com/towfiqi/serpbear/issues/130)
* Shows total keywords count in domains page ([fbd23ed](https://github.com/towfiqi/serpbear/commit/fbd23ede256062c72ec2f7e3983a0a02f0240725))
### Bug Fixes
* Resolves Website Thumbnail missing issue ([4a60271](https://github.com/towfiqi/serpbear/commit/4a60271cac1209dc02748c4d31943bb21c9ecaf2)), closes [#131](https://github.com/towfiqi/serpbear/issues/131)
### [0.3.2](https://github.com/towfiqi/serpbear/compare/v0.3.1...v0.3.2) (2023-11-09)

View File

@@ -11,7 +11,7 @@ FROM node:lts-alpine AS builder
WORKDIR /app
COPY --from=deps /app ./
RUN rm -rf /app/data
RUN rm -rf /app/__test__
RUN rm -rf /app/__tests__
RUN npm run build

View File

@@ -2,10 +2,11 @@ export const dummyDomain = {
ID: 1,
domain: 'compressimage.io',
slug: 'compressimage-io',
keywordCount: 0,
keywordCount: 10,
avgPosition: 24,
lastUpdated: '2022-11-11T10:00:32.243',
added: '2022-11-11T10:00:32.244',
tags: [],
tags: '',
notification: true,
notification_interval: 'daily',
notification_emails: '',
@@ -33,7 +34,7 @@ export const dummyKeywords = [
lastResult: [],
sticky: false,
updating: false,
lastUpdateError: 'false',
lastUpdateError: false as false,
},
{
ID: 2,
@@ -56,6 +57,23 @@ export const dummyKeywords = [
lastResult: [],
sticky: false,
updating: false,
lastUpdateError: 'false',
lastUpdateError: false as false,
},
];
export const dummySettings = {
scaping_api: '',
scraper_type: 'none',
notification_interval: 'never',
notification_email: '',
notification_email_from: '',
smtp_server: '',
smtp_port: '',
smtp_username: '',
smtp_password: '',
scrape_retry: false,
search_console_integrated: false,
screenshot_key: '',
available_scapers: [],
failed_queue: [],
};

View File

@@ -1,35 +0,0 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import Modal from '../../components/common/Modal';
// jest.mock('React', () => ({
// ...jest.requireActual('React'),
// useEffect: jest.fn(),
// }));
// const mockedUseEffect = useEffect as jest.Mock<any>;
// jest.mock('../../components/common/Icon', () => () => <div data-testid="icon"/>);
describe('Modal Component', () => {
it('Renders without crashing', async () => {
render(<Modal closeModal={() => console.log() }><div></div></Modal>);
// mockedUseEffect.mock.calls[0]();
expect(document.querySelector('.modal')).toBeInTheDocument();
});
// it('Sets up the escapae key shortcut', async () => {
// render(<Modal closeModal={() => console.log() }><div></div></Modal>);
// expect(mockedUseEffect).toBeCalled();
// });
it('Displays the Given Content', async () => {
render(<Modal closeModal={() => console.log() }>
<div>
<h1>Hello Modal!!</h1>
</div>
</Modal>);
expect(await screen.findByText('Hello Modal!!')).toBeInTheDocument();
});
it('Renders Modal Title', async () => {
render(<Modal closeModal={() => console.log() } title="Sample Modal Title"><p>Some Modal Content</p></Modal>);
expect(await screen.findByText('Sample Modal Title')).toBeInTheDocument();
});
});

View File

@@ -1,11 +0,0 @@
import { render, screen } from '@testing-library/react';
import Sidebar from '../../components/common/Sidebar';
describe('Sidebar Component', () => {
it('renders without crashing', async () => {
render(<Sidebar domains={[]} showAddModal={() => console.log() } />);
expect(
await screen.findByText('SerpBear'),
).toBeInTheDocument();
});
});

View File

@@ -1,21 +0,0 @@
import { waitFor } from '@testing-library/react';
// import { useFetchDomains } from '../../services/domains';
// import { createWrapper } from '../utils';
jest.mock('next/router', () => ({
useRouter: () => ({
query: { slug: 'compressimage-io' },
push: (link:string) => { console.log('Pushed', link); },
}),
}));
describe('DomainHooks', () => {
it('useFetchDomains should fetch the Domains', async () => {
// const { result } = renderHook(() => useFetchDomains(), { wrapper: createWrapper() });
const result = { current: { isSuccess: false, data: '' } };
await waitFor(() => {
console.log('result.current: ', result.current.data);
return expect(result.current.isSuccess).toBe(true);
});
});
});

View File

@@ -0,0 +1,35 @@
import { fireEvent, render } from '@testing-library/react';
import DomainItem from '../../components/domains/DomainItem';
import { dummyDomain } from '../../__mocks__/data';
const updateThumbMock = jest.fn();
const domainItemProps = {
domain: dummyDomain,
selected: false,
isConsoleIntegrated: false,
thumb: '',
updateThumb: updateThumbMock,
};
describe('DomainItem Component', () => {
it('renders without crashing', async () => {
const { container } = render(<DomainItem {...domainItemProps} />);
expect(container.querySelector('.domItem')).toBeInTheDocument();
});
it('renders keywords count', async () => {
const { container } = render(<DomainItem {...domainItemProps} />);
const domStatskeywords = container.querySelector('.dom_stats div:nth-child(1)');
expect(domStatskeywords?.textContent).toBe('Keywords10');
});
it('renders avg position', async () => {
const { container } = render(<DomainItem {...domainItemProps} />);
const domStatsAvg = container.querySelector('.dom_stats div:nth-child(2)');
expect(domStatsAvg?.textContent).toBe('Avg position24');
});
it('updates domain thumbnail on relevant button click', async () => {
const { container } = render(<DomainItem {...domainItemProps} />);
const reloadThumbbBtn = container.querySelector('.domain_thumb button');
if (reloadThumbbBtn) fireEvent.click(reloadThumbbBtn);
expect(updateThumbMock).toHaveBeenCalledWith(dummyDomain.domain);
});
});

View File

@@ -1,8 +1,14 @@
import { fireEvent, render, screen } from '@testing-library/react';
import Keyword from '../../components/keywords/Keyword';
import { dummyKeywords } from '../data';
import { dummyKeywords } from '../../__mocks__/data';
const keywordFunctions = {
const keywordProps = {
keywordData: dummyKeywords[0],
selected: false,
index: 0,
showSCData: false,
scDataType: '',
style: {},
refreshkeyword: jest.fn(),
favoriteKeyword: jest.fn(),
removeKeyword: jest.fn(),
@@ -10,35 +16,37 @@ const keywordFunctions = {
manageTags: jest.fn(),
showKeywordDetails: jest.fn(),
};
jest.mock('react-chartjs-2', () => ({
Line: () => null,
}));
describe('Keyword Component', () => {
it('renders without crashing', async () => {
render(<Keyword keywordData={dummyKeywords[0]} {...keywordFunctions} selected={false} />);
render(<Keyword {...keywordProps} />);
expect(await screen.findByText('compress image')).toBeInTheDocument();
});
it('Should Render Position Correctly', async () => {
render(<Keyword keywordData={dummyKeywords[0]} {...keywordFunctions} selected={false} />);
render(<Keyword {...keywordProps} />);
const positionElement = document.querySelector('.keyword_position');
expect(positionElement?.childNodes[0].nodeValue).toBe('19');
});
it('Should Display Position Change arrow', async () => {
render(<Keyword keywordData={dummyKeywords[0]} {...keywordFunctions} selected={false} />);
render(<Keyword {...keywordProps} />);
const positionElement = document.querySelector('.keyword_position i');
expect(positionElement?.textContent).toBe('▲ 1');
});
it('Should Display the SERP Page URL', async () => {
render(<Keyword keywordData={dummyKeywords[0]} {...keywordFunctions} selected={false} />);
render(<Keyword {...keywordProps} />);
const positionElement = document.querySelector('.keyword_url');
expect(positionElement?.textContent).toBe('/');
});
it('Should Display the Keyword Options on dots Click', async () => {
render(<Keyword keywordData={dummyKeywords[0]} {...keywordFunctions} selected={false} />);
const button = document.querySelector('.keyword .keyword_dots');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
const { container } = render(<Keyword {...keywordProps} />);
const button = container.querySelector('.keyword_dots');
if (button) fireEvent.click(button);
expect(document.querySelector('.keyword_options')).toBeVisible();
});
// it('Should favorite Keywords', async () => {
// render(<Keyword keywordData={dummyKeywords[0]} {...keywordFunctions} selected={false} />);
// render(<Keyword {...keywordProps} />);
// const button = document.querySelector('.keyword .keyword_dots');
// if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
// const option = document.querySelector('.keyword .keyword_options li:nth-child(1) a');

View File

@@ -0,0 +1,28 @@
import { fireEvent, render, screen } from '@testing-library/react';
import Modal from '../../components/common/Modal';
const closeModalMock = jest.fn();
describe('Modal Component', () => {
it('Renders without crashing', async () => {
render(<Modal closeModal={closeModalMock }><div></div></Modal>);
expect(document.querySelector('.modal')).toBeInTheDocument();
});
it('Displays the Given Content', async () => {
render(<Modal closeModal={closeModalMock}>
<div>
<h1>Hello Modal!!</h1>
</div>
</Modal>);
expect(await screen.findByText('Hello Modal!!')).toBeInTheDocument();
});
it('Renders Modal Title', async () => {
render(<Modal closeModal={closeModalMock} title="Sample Modal Title"><p>Some Modal Content</p></Modal>);
expect(await screen.findByText('Sample Modal Title')).toBeInTheDocument();
});
it('Closes the modal on close button click', async () => {
const { container } = render(<Modal closeModal={closeModalMock} title="Sample Modal Title"><p>Some Modal Content</p></Modal>);
const closeBtn = container.querySelector('.modal-close');
if (closeBtn) fireEvent.click(closeBtn);
expect(closeModalMock).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,23 @@
import { fireEvent, render, screen } from '@testing-library/react';
import Sidebar from '../../components/common/Sidebar';
import { dummyDomain } from '../../__mocks__/data';
const addDomainMock = jest.fn();
jest.mock('next/router', () => jest.requireActual('next-router-mock'));
describe('Sidebar Component', () => {
it('renders without crashing', async () => {
render(<Sidebar domains={[dummyDomain]} showAddModal={addDomainMock} />);
expect(screen.getByText('SerpBear')).toBeInTheDocument();
});
it('renders domain list', async () => {
render(<Sidebar domains={[dummyDomain]} showAddModal={addDomainMock} />);
expect(screen.getByText('compressimage.io')).toBeInTheDocument();
});
it('calls showAddModal on Add Domain button click', async () => {
render(<Sidebar domains={[dummyDomain]} showAddModal={addDomainMock} />);
const addDomainBtn = screen.getByTestId('add_domain');
fireEvent.click(addDomainBtn);
expect(addDomainMock).toHaveBeenCalledWith(true);
});
});

View File

@@ -1,9 +1,15 @@
import { render, screen } from '@testing-library/react';
import TopBar from '../../components/common/TopBar';
jest.mock('next/router', () => ({
useRouter: () => ({
pathname: '/',
}),
}));
describe('TopBar Component', () => {
it('renders without crashing', async () => {
render(<TopBar showSettings={() => console.log() } />);
render(<TopBar showSettings={jest.fn} showAddModal={jest.fn} />);
expect(
await screen.findByText('SerpBear'),
).toBeInTheDocument();

View File

@@ -0,0 +1,27 @@
import { renderHook, waitFor } from '@testing-library/react';
import mockRouter from 'next-router-mock';
import { useFetchDomains } from '../../services/domains';
import { createWrapper } from '../../__mocks__/utils';
import { dummyDomain } from '../../__mocks__/data';
jest.mock('next/router', () => jest.requireActual('next-router-mock'));
fetchMock.mockIf(`${window.location.origin}/api/domains`, async () => {
return new Promise((resolve) => {
resolve({
body: JSON.stringify({ domains: [dummyDomain] }),
status: 200,
});
});
});
describe('DomainHooks', () => {
it('useFetchDomains should fetch the Domains', async () => {
const { result } = renderHook(() => useFetchDomains(mockRouter), { wrapper: createWrapper() });
// const result = { current: { isSuccess: false, data: '' } };
await waitFor(() => {
return expect(result.current.isLoading).toBe(false);
});
});
});

View File

@@ -1,17 +1,26 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { MockResponseInitFunction } from 'jest-fetch-mock';
import SingleDomain from '../../pages/domain/[slug]';
import { useAddDomain, useDeleteDomain, useFetchDomains, useUpdateDomain } from '../../services/domains';
import { useAddKeywords, useDeleteKeywords, useFavKeywords, useFetchKeywords, useRefreshKeywords } from '../../services/keywords';
import { dummyDomain, dummyKeywords } from '../data';
import { dummyDomain, dummyKeywords, dummySettings } from '../../__mocks__/data';
import { useFetchSettings } from '../../services/settings';
jest.mock('../../services/domains');
jest.mock('../../services/keywords');
jest.mock('../../services/settings');
jest.mock('next/router', () => ({
useRouter: () => ({
query: { slug: dummyDomain.slug },
}),
}));
jest.mock('react-chartjs-2', () => ({
Line: () => null,
}));
const useFetchDomainsFunc = useFetchDomains as jest.Mock<any>;
const useFetchKeywordsFunc = useFetchKeywords as jest.Mock<any>;
const useDeleteKeywordsFunc = useDeleteKeywords as jest.Mock<any>;
@@ -21,9 +30,12 @@ const useAddDomainFunc = useAddDomain as jest.Mock<any>;
const useAddKeywordsFunc = useAddKeywords as jest.Mock<any>;
const useUpdateDomainFunc = useUpdateDomain as jest.Mock<any>;
const useDeleteDomainFunc = useDeleteDomain as jest.Mock<any>;
const useFetchSettingsFunc = useFetchSettings as jest.Mock<any>;
describe('SingleDomain Page', () => {
const queryClient = new QueryClient();
beforeEach(() => {
useFetchSettingsFunc.mockImplementation(() => ({ data: { settings: dummySettings }, isLoading: false }));
useFetchDomainsFunc.mockImplementation(() => ({ data: { domains: [dummyDomain] }, isLoading: false }));
useFetchKeywordsFunc.mockImplementation(() => ({ keywordsData: { keywords: dummyKeywords }, keywordsLoading: false }));
useDeleteKeywordsFunc.mockImplementation(() => ({ mutate: () => { } }));
@@ -38,158 +50,163 @@ describe('SingleDomain Page', () => {
jest.clearAllMocks();
});
it('Render without crashing.', async () => {
const { getByTestId } = render(<SingleDomain />);
// screen.debug(undefined, Infinity);
expect(getByTestId('domain-header')).toBeInTheDocument();
// expect(await result.findByText(/compressimage/i)).toBeInTheDocument();
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
expect(screen.getByTestId('domain-header')).toBeInTheDocument();
});
it('Should Call the useFetchDomains hook on render.', async () => {
render(<SingleDomain />);
// screen.debug(undefined, Infinity);
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
expect(useFetchDomains).toHaveBeenCalled();
// expect(await result.findByText(/compressimage/i)).toBeInTheDocument();
});
it('Should Render the Keywords', async () => {
render(<SingleDomain />);
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const keywordsCount = document.querySelectorAll('.keyword').length;
expect(keywordsCount).toBe(2);
});
it('Should Display the Keywords Details Sidebar on Keyword Click.', async () => {
render(<SingleDomain />);
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const keywords = document.querySelectorAll('.keyword');
const firstKeyword = keywords && keywords[0].querySelector('a');
if (firstKeyword) fireEvent(firstKeyword, new MouseEvent('click', { bubbles: true }));
if (firstKeyword) fireEvent.click(firstKeyword);
const fn: MockResponseInitFunction = async () => {
return new Promise((resolve) => {
resolve({
body: JSON.stringify({ keyword: dummyKeywords[0] }),
status: 200,
});
});
};
fetchMock.mockIf(`${window.location.origin}/api/keyword?id=${dummyKeywords[0].ID}`, fn);
expect(screen.getByTestId('keywordDetails')).toBeVisible();
});
it('Should Display the AddDomain Modal on Add Domain Button Click.', async () => {
render(<SingleDomain />);
const button = document.querySelector('[data-testid=add_domain]');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const button = screen.getByTestId('add_domain');
if (button) fireEvent.click(button);
expect(screen.getByTestId('adddomain_modal')).toBeVisible();
});
it('Should Display the AddKeywords Modal on Add Keyword Button Click.', async () => {
render(<SingleDomain />);
const button = document.querySelector('[data-testid=add_keyword]');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const button = screen.getByTestId('add_keyword');
if (button) fireEvent.click(button);
expect(screen.getByTestId('addkeywords_modal')).toBeVisible();
});
it('Should display the Domain Settings on Settings Button click.', async () => {
render(<SingleDomain />);
const button = document.querySelector('[data-testid=show_domain_settings]');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const button = screen.getByTestId('show_domain_settings');
if (button) fireEvent.click(button);
expect(screen.getByTestId('domain_settings')).toBeVisible();
});
it('Device Tab change should be functioning.', async () => {
render(<SingleDomain />);
const button = document.querySelector('[data-testid=mobile_tab]');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const button = screen.getByTestId('mobile_tab');
if (button) fireEvent.click(button);
const keywordsCount = document.querySelectorAll('.keyword').length;
expect(keywordsCount).toBe(0);
});
it('Search Filter should function properly', async () => {
render(<SingleDomain />);
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const inputNode = screen.getByTestId('filter_input');
fireEvent.change(inputNode, { target: { value: 'compressor' } }); // triggers onChange event
if (inputNode) fireEvent.change(inputNode, { target: { value: 'compressor' } }); // triggers onChange event
expect(inputNode.getAttribute('value')).toBe('compressor');
const keywordsCount = document.querySelectorAll('.keyword').length;
expect(keywordsCount).toBe(1);
});
it('Country Filter should function properly', async () => {
render(<SingleDomain />);
const button = document.querySelector('[data-testid=filter_button]');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const button = screen.getByTestId('filter_button');
if (button) fireEvent.click(button);
expect(document.querySelector('.country_filter')).toBeVisible();
const countrySelect = document.querySelector('.country_filter .selected');
if (countrySelect) fireEvent(countrySelect, new MouseEvent('click', { bubbles: true }));
if (countrySelect) fireEvent.click(countrySelect);
expect(document.querySelector('.country_filter .select_list')).toBeVisible();
const firstCountry = document.querySelector('.country_filter .select_list ul li:nth-child(1)');
if (firstCountry) fireEvent(firstCountry, new MouseEvent('click', { bubbles: true }));
if (firstCountry) fireEvent.click(firstCountry);
const keywordsCount = document.querySelectorAll('.keyword').length;
expect(keywordsCount).toBe(0);
});
// Tags Filter should function properly
it('Tags Filter should Render & Function properly', async () => {
render(<SingleDomain />);
const button = document.querySelector('[data-testid=filter_button]');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const button = screen.getByTestId('filter_button');
if (button) fireEvent.click(button);
expect(document.querySelector('.tags_filter')).toBeVisible();
const countrySelect = document.querySelector('.tags_filter .selected');
if (countrySelect) fireEvent(countrySelect, new MouseEvent('click', { bubbles: true }));
if (countrySelect) fireEvent.click(countrySelect);
expect(document.querySelector('.tags_filter .select_list')).toBeVisible();
expect(document.querySelectorAll('.tags_filter .select_list ul li').length).toBe(1);
const firstTag = document.querySelector('.tags_filter .select_list ul li:nth-child(1)');
if (firstTag) fireEvent(firstTag, new MouseEvent('click', { bubbles: true }));
if (firstTag) fireEvent.click(firstTag);
expect(document.querySelectorAll('.keyword').length).toBe(1);
});
it('Sort Options Should be visible Sort Button on Click.', async () => {
render(<SingleDomain />);
const button = document.querySelector('[data-testid=sort_button]');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const button = screen.getByTestId('sort_button');
if (button) fireEvent.click(button);
expect(document.querySelector('.sort_options')).toBeVisible();
});
it('Sort: Position should sort keywords accordingly', async () => {
render(<SingleDomain />);
const button = document.querySelector('[data-testid=sort_button]');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const button = screen.getByTestId('sort_button');
if (button) fireEvent.click(button);
// Test Top Position Sort
const topPosSortOption = document.querySelector('ul.sort_options li:nth-child(1)');
if (topPosSortOption) fireEvent(topPosSortOption, new MouseEvent('click', { bubbles: true }));
if (topPosSortOption) fireEvent.click(topPosSortOption);
const firstKeywordTitle = document.querySelector('.domKeywords_keywords .keyword:nth-child(1) a')?.textContent;
expect(firstKeywordTitle).toBe('compress image');
// Test Lowest Position Sort
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
if (button) fireEvent.click(button);
const lowestPosSortOption = document.querySelector('ul.sort_options li:nth-child(2)');
if (lowestPosSortOption) fireEvent(lowestPosSortOption, new MouseEvent('click', { bubbles: true }));
if (lowestPosSortOption) fireEvent.click(lowestPosSortOption);
const secondKeywordTitle = document.querySelector('.domKeywords_keywords .keyword:nth-child(1) a')?.textContent;
expect(secondKeywordTitle).toBe('image compressor');
});
it('Sort: Date Added should sort keywords accordingly', async () => {
render(<SingleDomain />);
const button = document.querySelector('[data-testid=sort_button]');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const button = screen.getByTestId('sort_button');
if (button) fireEvent.click(button);
// Test Top Position Sort
const topPosSortOption = document.querySelector('ul.sort_options li:nth-child(3)');
if (topPosSortOption) fireEvent(topPosSortOption, new MouseEvent('click', { bubbles: true }));
if (topPosSortOption) fireEvent.click(topPosSortOption);
const firstKeywordTitle = document.querySelector('.domKeywords_keywords .keyword:nth-child(1) a')?.textContent;
expect(firstKeywordTitle).toBe('compress image');
// Test Lowest Position Sort
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
if (button) fireEvent.click(button);
const lowestPosSortOption = document.querySelector('ul.sort_options li:nth-child(4)');
if (lowestPosSortOption) fireEvent(lowestPosSortOption, new MouseEvent('click', { bubbles: true }));
if (lowestPosSortOption) fireEvent.click(lowestPosSortOption);
const secondKeywordTitle = document.querySelector('.domKeywords_keywords .keyword:nth-child(1) a')?.textContent;
expect(secondKeywordTitle).toBe('image compressor');
});
it('Sort: Alphabetical should sort keywords accordingly', async () => {
render(<SingleDomain />);
const button = document.querySelector('[data-testid=sort_button]');
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
render(<QueryClientProvider client={queryClient}><SingleDomain /></QueryClientProvider>);
const button = screen.getByTestId('sort_button');
if (button) fireEvent.click(button);
// Test Top Position Sort
const topPosSortOption = document.querySelector('ul.sort_options li:nth-child(5)');
if (topPosSortOption) fireEvent(topPosSortOption, new MouseEvent('click', { bubbles: true }));
if (topPosSortOption) fireEvent.click(topPosSortOption);
const firstKeywordTitle = document.querySelector('.domKeywords_keywords .keyword:nth-child(1) a')?.textContent;
expect(firstKeywordTitle).toBe('compress image');
// Test Lowest Position Sort
if (button) fireEvent(button, new MouseEvent('click', { bubbles: true }));
if (button) fireEvent.click(button);
const lowestPosSortOption = document.querySelector('ul.sort_options li:nth-child(6)');
if (lowestPosSortOption) fireEvent(lowestPosSortOption, new MouseEvent('click', { bubbles: true }));
if (lowestPosSortOption) fireEvent.click(lowestPosSortOption);
const secondKeywordTitle = document.querySelector('.domKeywords_keywords .keyword:nth-child(1) a')?.textContent;
expect(secondKeywordTitle).toBe('image compressor');
});

View File

@@ -0,0 +1,49 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import * as ReactQuery from 'react-query';
import { dummyDomain } from '../../__mocks__/data';
import Domains from '../../pages/domains';
jest.mock('next/router', () => jest.requireActual('next-router-mock'));
jest.spyOn(ReactQuery, 'useQuery').mockImplementation(jest.fn().mockReturnValue(
{ data: { domains: [dummyDomain] }, isLoading: false, isSuccess: true },
));
fetchMock.mockIf(`${window.location.origin}/api/domains`, async () => {
return new Promise((resolve) => {
resolve({
body: JSON.stringify({ domains: [dummyDomain] }),
status: 200,
});
});
});
describe('Domains Page', () => {
const queryClient = new QueryClient();
it('Renders without crashing', async () => {
render(
<QueryClientProvider client={queryClient}>
<Domains />
</QueryClientProvider>,
);
expect(screen.getByTestId('domains')).toBeInTheDocument();
});
it('Renders the Domain Component', async () => {
const { container } = render(
<QueryClientProvider client={queryClient}>
<Domains />
</QueryClientProvider>,
);
expect(container.querySelector('.domItem')).toBeInTheDocument();
});
it('Should Display Add Domain Modal on relveant Button Click.', async () => {
render(<QueryClientProvider client={queryClient}><Domains /></QueryClientProvider>);
const button = screen.getByTestId('addDomainButton');
if (button) fireEvent.click(button);
expect(screen.getByTestId('adddomain_modal')).toBeVisible();
});
it('Should Display the version number in Footer.', async () => {
render(<QueryClientProvider client={queryClient}><Domains /></QueryClientProvider>);
expect(screen.getByText('SerpBear v0.0.0')).toBeVisible();
});
});

View File

@@ -2,21 +2,16 @@ import { render, screen } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from 'react-query';
import Home from '../../pages/index';
const routerPush = jest.fn();
jest.mock('next/router', () => ({
useRouter: () => ({
push: routerPush,
}),
}));
describe('Home Page', () => {
const queryClient = new QueryClient();
it('Renders without crashing', async () => {
// const dummyDomain = {
// ID: 1,
// domain: 'compressimage.io',
// slug: 'compressimage-io',
// keywordCount: 0,
// lastUpdated: '2022-11-11T10:00:32.243',
// added: '2022-11-11T10:00:32.244',
// tags: [],
// notification: true,
// notification_interval: 'daily',
// notification_emails: '',
// };
render(
<QueryClientProvider client={queryClient}>
<Home />
@@ -26,12 +21,12 @@ describe('Home Page', () => {
expect(await screen.findByRole('main')).toBeInTheDocument();
expect(screen.queryByText('Add Domain')).not.toBeInTheDocument();
});
it('Should Display the Add Domain Modal when there are no Domains.', async () => {
it('Should redirect to /domains route.', async () => {
render(
<QueryClientProvider client={queryClient}>
<Home />
</QueryClientProvider>,
);
expect(await screen.findByText('Add Domain')).toBeInTheDocument();
expect(routerPush).toHaveBeenCalledWith('/domains');
});
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable @next/next/no-img-element */
import React from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
@@ -26,11 +27,11 @@ const Sidebar = ({ domains, showAddModal } : SidebarProps) => {
rounded-r-none ${((`/domain/${d.slug}` === router.asPath || `/domain/console/${d.slug}` === router.asPath
|| `/domain/insight/${d.slug}` === router.asPath)
? 'bg-white text-zinc-800 border border-r-0' : 'text-zinc-500')}`}>
<i className={'text-center leading-4 mr-2 inline-block rounded-full w-5 h-5 bg-orange-200 not-italic'}>
{d.domain.charAt(0)}
</i>
<img
className={' inline-block mr-1'}
src={`https://www.google.com/s2/favicons?domain=${d.domain}&sz=16`} alt={d.domain}
/>
{d.domain}
{/* <span>0</span> */}
</a>
</Link>
</li>)

View File

@@ -11,9 +11,10 @@ type DomainItemProps = {
selected: boolean,
isConsoleIntegrated: boolean,
thumb: string,
updateThumb: Function,
}
const DomainItem = ({ domain, selected, isConsoleIntegrated = false, thumb }: DomainItemProps) => {
const DomainItem = ({ domain, selected, isConsoleIntegrated = false, thumb, updateThumb }: DomainItemProps) => {
const { keywordsUpdated, slug, keywordCount = 0, avgPosition = 0, scVisits = 0, scImpressions = 0, scPosition = 0 } = domain;
// const router = useRouter();
return (
@@ -21,8 +22,20 @@ const DomainItem = ({ domain, selected, isConsoleIntegrated = false, thumb }: Do
<Link href={`/domain/${slug}`} passHref={true}>
<a className='flex flex-col lg:flex-row'>
<div className={`flex-1 p-6 flex ${!isConsoleIntegrated ? 'basis-1/3' : ''}`}>
<div className="domain_thumb w-20 h-20 mr-6 bg-slate-100 rounded border border-gray-200 overflow-hidden">
{thumb && <img src={thumb} alt={domain.domain} />}
<div className="group domain_thumb w-20 h-20 mr-6 bg-slate-100 rounded
border border-gray-200 overflow-hidden flex justify-center relative">
<button
className=' absolute right-1 top-0 text-gray-400 p-1 transition-all
invisible opacity-0 group-hover:visible group-hover:opacity-100 hover:text-gray-600 z-10'
title='Reload Website Screenshot'
onClick={(e) => { e.preventDefault(); e.stopPropagation(); updateThumb(domain.domain); }}
>
<Icon type="reload" size={12} />
</button>
<img
className={`self-center ${!thumb ? 'max-w-[50px]' : ''}`}
src={thumb || `https://www.google.com/s2/favicons?domain=${domain.domain}&sz=128`} alt={domain.domain}
/>
</div>
<div className="domain_details flex-1">
<h3 className='font-semibold text-base mb-2'>{domain.domain}</h3>

View File

@@ -12,7 +12,7 @@ type SCInsightProps = {
isConsoleIntegrated: boolean,
}
const SCInsight = ({ insight, isLoading = true, isConsoleIntegrated = true }: SCInsightProps) => {
const SCInsight = ({ insight, isLoading = true, isConsoleIntegrated = true, domain }: SCInsightProps) => {
const [activeTab, setActiveTab] = useState<string>('stats');
const insightItems = insight[activeTab as keyof InsightDataType];
@@ -108,7 +108,7 @@ const SCInsight = ({ insight, isLoading = true, isConsoleIntegrated = true }: SC
(item:SCInsightItem, index: number) => {
const insightItemCount = insight ? insightItems : [];
const lastItem = !!(insightItemCount && (index === insightItemCount.length));
return <InsightItem key={index} item={item} type={activeTab} lastItem={lastItem} />;
return <InsightItem key={index} item={item} type={activeTab} lastItem={lastItem} domain={domain?.domain || ''} />;
},
)
}

View File

@@ -5,10 +5,11 @@ import Icon from '../common/Icon';
type InsightItemProps = {
item: SCInsightItem,
lastItem: boolean,
type: string
type: string,
domain: string
}
const InsightItem = ({ item, lastItem, type }:InsightItemProps) => {
const InsightItem = ({ item, lastItem, type, domain }:InsightItemProps) => {
const { clicks, impressions, ctr, position, country = 'zzz', keyword, page, keywords = 0, countries: cntrs = 0, date } = item;
let firstItem = keyword;
if (type === 'pages') { firstItem = page; } if (type === 'stats') {
@@ -24,7 +25,7 @@ const InsightItem = ({ item, lastItem, type }:InsightItemProps) => {
<div className=' w-3/4 lg:flex-1 lg:basis-20 lg:w-auto font-semibold'>
{type === 'countries' && <span className={`fflag fflag-${country} w-[18px] h-[12px] mr-2`} />}
{firstItem}
{type === 'pages' && domain ? <a href={`https://${domain}${page}`} target='_blank' rel="noreferrer">{firstItem}</a> : firstItem}
</div>
<div className='keyword_pos text-center inline-block mr-3 lg:mr-0 lg:flex-1'>

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Toaster } from 'react-hot-toast';
import useUpdateSettings, { useFetchSettings } from '../../services/settings';
import { useFetchSettings, useUpdateSettings } from '../../services/settings';
import Icon from '../common/Icon';
import NotificationSettings from './NotificationSettings';
import ScraperSettings from './ScraperSettings';

View File

@@ -1,11 +1,26 @@
// eslint-disable-next-line no-unused-vars
import 'isomorphic-fetch';
import './styles/globals.css';
import '@testing-library/jest-dom';
import { enableFetchMocks } from 'jest-fetch-mock';
// Optional: configure or set up a testing framework before each test.
// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`
// Used for __tests__/testing-library.js
// Learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
window.matchMedia = (query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
});
global.ResizeObserver = require('resize-observer-polyfill');
// Enable Fetch Mocking
enableFetchMocks();

41
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "serpbear",
"version": "0.3.2",
"version": "0.3.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "serpbear",
"version": "0.3.2",
"version": "0.3.3",
"dependencies": {
"@googleapis/searchconsole": "^1.0.0",
"@types/react-transition-group": "^4.4.5",
@@ -59,6 +59,8 @@
"eslint-config-next": "12.3.1",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"next-router-mock": "^0.9.10",
"postcss": "^8.4.31",
"prettier": "^2.7.1",
"resize-observer-polyfill": "^1.5.1",
@@ -4152,6 +4154,15 @@
"node": ">=6.0"
}
},
"node_modules/cross-fetch": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
"integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
"dev": true,
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -7829,6 +7840,16 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-fetch-mock": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz",
"integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==",
"dev": true,
"dependencies": {
"cross-fetch": "^3.0.4",
"promise-polyfill": "^8.1.3"
}
},
"node_modules/jest-get-type": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
@@ -9840,6 +9861,16 @@
}
}
},
"node_modules/next-router-mock": {
"version": "0.9.10",
"resolved": "https://registry.npmjs.org/next-router-mock/-/next-router-mock-0.9.10.tgz",
"integrity": "sha512-bK6sRb/xGNFgHVUZuvuApn6KJBAKTPiP36A7a9mO77U4xQO5ukJx9WHlU67Tv8AuySd09pk0+Hu8qMVIAmLO6A==",
"dev": true,
"peerDependencies": {
"next": ">=10.0.0",
"react": ">=17.0.0"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
@@ -10835,6 +10866,12 @@
"integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==",
"optional": true
},
"node_modules/promise-polyfill": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
"integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==",
"dev": true
},
"node_modules/promise-retry": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "serpbear",
"version": "0.3.2",
"version": "0.3.3",
"private": true,
"scripts": {
"dev": "next dev",
@@ -67,6 +67,8 @@
"eslint-config-next": "12.3.1",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"next-router-mock": "^0.9.10",
"postcss": "^8.4.31",
"prettier": "^2.7.1",
"resize-observer-polyfill": "^1.5.1",

View File

@@ -55,6 +55,7 @@ const updateSettings = async (req: NextApiRequest, res: NextApiResponse<Settings
};
export const getAppSettings = async () : Promise<SettingsType> => {
const screenshotAPIKey = process.env.SCREENSHOT_API || '69408-serpbear';
try {
const settingsRaw = await readFile(`${process.cwd()}/data/settings.json`, { encoding: 'utf-8' });
const failedQueueRaw = await readFile(`${process.cwd()}/data/failed_queue.json`, { encoding: 'utf-8' });
@@ -73,6 +74,7 @@ export const getAppSettings = async () : Promise<SettingsType> => {
search_console_integrated: !!(process.env.SEARCH_CONSOLE_PRIVATE_KEY && process.env.SEARCH_CONSOLE_CLIENT_EMAIL),
available_scapers: allScrapers.map((scraper) => ({ label: scraper.name, value: scraper.id })),
failed_queue: failedQueue,
screenshot_key: screenshotAPIKey,
};
} catch (error) {
console.log('Error Decrypting Settings API Keys!');
@@ -81,7 +83,7 @@ export const getAppSettings = async () : Promise<SettingsType> => {
return decryptedSettings;
} catch (error) {
console.log('[ERROR] Getting App Settings. ', error);
const settings = {
const settings: SettingsType = {
scraper_type: 'none',
notification_interval: 'never',
notification_email: '',
@@ -91,6 +93,7 @@ export const getAppSettings = async () : Promise<SettingsType> => {
smtp_username: '',
smtp_password: '',
scrape_retry: false,
screenshot_key: screenshotAPIKey,
};
const otherSettings = {
available_scapers: allScrapers.map((scraper) => ({ label: scraper.name, value: scraper.id })),

View File

@@ -1,19 +1,20 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import type { NextPage } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { CSSTransition } from 'react-transition-group';
import toast, { Toaster } from 'react-hot-toast';
import TopBar from '../../components/common/TopBar';
import AddDomain from '../../components/domains/AddDomain';
import Settings from '../../components/settings/Settings';
import { useFetchSettings } from '../../services/settings';
import { useFetchDomains } from '../../services/domains';
import { fetchDomainScreenshot, useFetchDomains } from '../../services/domains';
import DomainItem from '../../components/domains/DomainItem';
import Icon from '../../components/common/Icon';
type thumbImages = { [domain:string] : string }
const SingleDomain: NextPage = () => {
const Domains: NextPage = () => {
const router = useRouter();
const [noScrapprtError, setNoScrapprtError] = useState(false);
const [showSettings, setShowSettings] = useState(false);
@@ -22,33 +23,28 @@ const SingleDomain: NextPage = () => {
const { data: appSettings } = useFetchSettings();
const { data: domainsData, isLoading } = useFetchDomains(router, true);
const totalKeywords = useMemo(() => {
let keywords = 0;
if (domainsData?.domains) {
domainsData.domains.forEach(async (domain:DomainType) => {
keywords += domain?.keywordCount || 0;
});
}
return keywords;
}, [domainsData]);
useEffect(() => {
// console.log('Domains Data: ', domainsData);
if (domainsData?.domains && domainsData.domains.length > 0) {
const domainThumbsRaw = localStorage.getItem('domainThumbs');
const domThumbs = domainThumbsRaw ? JSON.parse(domainThumbsRaw) : {};
if (domainsData?.domains && domainsData.domains.length > 0 && appSettings?.settings?.screenshot_key) {
domainsData.domains.forEach(async (domain:DomainType) => {
if (domain.domain) {
if (!domThumbs[domain.domain]) {
const domainImageBlob = await fetch(`https://image.thum.io/get/auth/66909-serpbear/maxAge/96/width/200/https://${domain.domain}`).then((res) => res.blob());
if (domainImageBlob) {
const reader = new FileReader();
await new Promise((resolve, reject) => {
reader.onload = resolve;
reader.onerror = reject;
reader.readAsDataURL(domainImageBlob);
});
const imageBase: string = reader.result && typeof reader.result === 'string' ? reader.result : '';
localStorage.setItem('domainThumbs', JSON.stringify({ ...domThumbs, [domain.domain]: imageBase }));
setDomainThumbs((currentThumbs) => ({ ...currentThumbs, [domain.domain]: imageBase }));
}
} else {
setDomainThumbs((currentThumbs) => ({ ...currentThumbs, [domain.domain]: domThumbs[domain.domain] }));
const domainThumb = await fetchDomainScreenshot(domain.domain, appSettings.settings.screenshot_key);
if (domainThumb) {
setDomainThumbs((currentThumbs) => ({ ...currentThumbs, [domain.domain]: domainThumb }));
}
}
});
}
}, [domainsData]);
}, [domainsData, appSettings]);
useEffect(() => {
// console.log('appSettings.settings: ', appSettings && appSettings.settings);
@@ -57,8 +53,20 @@ const SingleDomain: NextPage = () => {
}
}, [appSettings]);
const manuallyUpdateThumb = async (domain: string) => {
if (domain && appSettings?.settings?.screenshot_key) {
const domainThumb = await fetchDomainScreenshot(domain, appSettings.settings.screenshot_key, true);
if (domainThumb) {
toast(`${domain} Screenshot Updated Successfully!`, { icon: '✔️' });
setDomainThumbs((currentThumbs) => ({ ...currentThumbs, [domain]: domainThumb }));
} else {
toast(`Failed to Fetch ${domain} Screenshot!`, { icon: '⚠️' });
}
}
};
return (
<div className="Domain flex flex-col min-h-screen">
<div data-testid="domains" className="Domain flex flex-col min-h-screen">
{noScrapprtError && (
<div className=' p-3 bg-red-600 text-white text-sm text-center'>
A Scrapper/Proxy has not been set up Yet. Open Settings to set it up and start using the app.
@@ -71,9 +79,12 @@ const SingleDomain: NextPage = () => {
<div className="flex flex-col w-full max-w-5xl mx-auto p-6 lg:mt-24 lg:p-0">
<div className='flex justify-between mb-2 items-center'>
<div className=' text-sm'>{domainsData?.domains?.length || 0} Domains</div>
<div className=' text-sm text-gray-600'>
{domainsData?.domains?.length || 0} Domains <span className=' text-gray-300 ml-1 mr-1'>|</span> {totalKeywords} keywords
</div>
<div>
<button
data-testid="addDomainButton"
className={'ml-2 inline-block py-2 text-blue-700 font-bold text-sm'}
onClick={() => setShowAddDomain(true)}>
<span
@@ -90,6 +101,7 @@ const SingleDomain: NextPage = () => {
selected={false}
isConsoleIntegrated={!!(appSettings && appSettings?.settings?.search_console_integrated) }
thumb={domainThumbs[domain.domain]}
updateThumb={manuallyUpdateThumb}
// isConsoleIntegrated={false}
/>;
})}
@@ -115,8 +127,9 @@ const SingleDomain: NextPage = () => {
<footer className='text-center flex flex-1 justify-center pb-5 items-end'>
<span className='text-gray-500 text-xs'><a href='https://github.com/towfiqi/serpbear' target="_blank" rel='noreferrer'>SerpBear v{appSettings?.settings?.version || '0.0.0'}</a></span>
</footer>
<Toaster position='bottom-center' containerClassName="react_toaster" />
</div>
);
};
export default SingleDomain;
export default Domains;

View File

@@ -8,7 +8,7 @@ import Icon from '../components/common/Icon';
const Home: NextPage = () => {
const router = useRouter();
useEffect(() => {
router.push('/domains');
if (router) router.push('/domains');
}, [router]);
return (

View File

@@ -19,6 +19,36 @@ export async function fetchDomains(router: NextRouter, withStats:boolean) {
return res.json();
}
export async function fetchDomainScreenshot(domain: string, screenshot_key:string, forceFetch = false): Promise<string | false> {
const domainThumbsRaw = localStorage.getItem('domainThumbs');
const domThumbs = domainThumbsRaw ? JSON.parse(domainThumbsRaw) : {};
if (!domThumbs[domain] || forceFetch) {
try {
const screenshotURL = `https://image.thum.io/get/auth/${screenshot_key}/maxAge/96/width/200/https://${domain}`;
const domainImageRes = await fetch(screenshotURL);
const domainImageBlob = domainImageRes.status === 200 ? await domainImageRes.blob() : false;
if (domainImageBlob) {
const reader = new FileReader();
await new Promise((resolve, reject) => {
reader.onload = resolve;
reader.onerror = reject;
reader.readAsDataURL(domainImageBlob);
});
const imageBase: string = reader.result && typeof reader.result === 'string' ? reader.result : '';
localStorage.setItem('domainThumbs', JSON.stringify({ ...domThumbs, [domain]: imageBase }));
return imageBase;
}
return false;
} catch (error) {
return false;
}
} else if (domThumbs[domain]) {
return domThumbs[domain];
}
return false;
}
export function useFetchDomains(router: NextRouter, withStats:boolean = false) {
return useQuery('domains', () => fetchDomains(router, withStats));
}

View File

@@ -10,7 +10,7 @@ export function useFetchSettings() {
return useQuery('settings', () => fetchSettings());
}
const useUpdateSettings = (onSuccess:Function|undefined) => {
export const useUpdateSettings = (onSuccess:Function|undefined) => {
const queryClient = useQueryClient();
return useMutation(async (settings: SettingsType) => {
@@ -60,5 +60,3 @@ export function useClearFailedQueue(onSuccess:Function) {
},
});
}
export default useUpdateSettings;

View File

@@ -24,7 +24,8 @@
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"types.d.ts"
"types.d.ts",
"./jest.setup.js"
],
"exclude": [
"node_modules"

3
types.d.ts vendored
View File

@@ -83,7 +83,8 @@ type SettingsType = {
scrape_delay?: string,
scrape_retry?: boolean,
failed_queue?: string[]
version?: string
version?: string,
screenshot_key?: string,
}
type KeywordSCDataChild = {