mirror of
https://github.com/towfiqi/serpbear
synced 2025-06-26 18:15:54 +00:00
feat: Adds the ability to view the changelog and displays the latest version number.
- Adds a new Footer component. - Adds a new Changelog component that displays the changelog.
This commit is contained in:
32
components/common/Footer.tsx
Normal file
32
components/common/Footer.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useState } from 'react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import { useFetchChangelog } from '../../services/misc';
|
||||
import ChangeLog from '../settings/Changelog';
|
||||
|
||||
interface FooterProps {
|
||||
currentVersion: string
|
||||
}
|
||||
|
||||
const Footer = ({ currentVersion = '' }: FooterProps) => {
|
||||
const [showChangelog, setShowChangelog] = useState(false);
|
||||
const { data: changeLogs } = useFetchChangelog();
|
||||
const latestVersionNum = changeLogs && Array.isArray(changeLogs) && changeLogs[0] ? changeLogs[0].name : '';
|
||||
|
||||
return (
|
||||
<footer className='text-center flex flex-1 justify-center pb-5 items-end'>
|
||||
<span className='text-gray-500 text-xs'>
|
||||
<a className='cursor-pointer' onClick={() => setShowChangelog(true)}>SerpBear v{currentVersion || '0.0.0'}</a>
|
||||
{currentVersion && latestVersionNum && `v${currentVersion}` !== latestVersionNum && (
|
||||
<a className='cursor-pointer text-indigo-700 font-semibold' onClick={() => setShowChangelog(true)}>
|
||||
{' '}| Update to Version {latestVersionNum} (latest)
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
<CSSTransition in={showChangelog} timeout={300} classNames="settings_anim" unmountOnExit mountOnEnter>
|
||||
<ChangeLog closeChangeLog={() => setShowChangelog(false)} />
|
||||
</CSSTransition>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
37
components/common/SidePanel.tsx
Normal file
37
components/common/SidePanel.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Icon from './Icon';
|
||||
import useOnKey from '../../hooks/useOnKey';
|
||||
|
||||
type SidePanelProps = {
|
||||
children: React.ReactNode,
|
||||
closePanel: Function,
|
||||
title?: string,
|
||||
width?: 'large' | 'medium' | 'small',
|
||||
position?: 'left' | 'right'
|
||||
}
|
||||
const SidePanel = ({ children, closePanel, width, position = 'right', title = '' }:SidePanelProps) => {
|
||||
useOnKey('Escape', closePanel);
|
||||
const closeOnBGClick = (e:React.SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
if (e.target === e.currentTarget) { closePanel(); }
|
||||
};
|
||||
return (
|
||||
<div className="SidePanel fixed w-full h-screen top-0 left-0 z-50" onClick={closeOnBGClick}>
|
||||
<div className={`absolute w-full max-w-md border-l border-l-gray-400 bg-white customShadow top-0
|
||||
${position === 'left' ? 'left-0' : 'right-0'} h-screen`}>
|
||||
<div className='SidePanel__header px-5 py-4 text-slate-500 border-b border-b-gray-100'>
|
||||
<h3 className=' text-black text-lg font-bold'>{title}</h3>
|
||||
<button
|
||||
className=' absolute top-2 right-2 p-2 px- text-gray-400 hover:text-gray-700 transition-all hover:rotate-90'
|
||||
onClick={() => closePanel()}>
|
||||
<Icon type='close' size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidePanel;
|
||||
80
components/settings/Changelog.tsx
Normal file
80
components/settings/Changelog.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { useLayoutEffect, useMemo } from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import dayjs from 'dayjs';
|
||||
import SidePanel from '../common/SidePanel';
|
||||
import { useFetchChangelog } from '../../services/misc';
|
||||
import Icon from '../common/Icon';
|
||||
|
||||
const Markdown = React.lazy(() => import('react-markdown'));
|
||||
|
||||
type ChangeLogProps = {
|
||||
closeChangeLog: Function,
|
||||
}
|
||||
|
||||
const ChangeLogloader = () => {
|
||||
return (
|
||||
<div className='w-full h-full absolute flex justify-center items-center'>
|
||||
<Icon type="loading" size={36} color='#999' />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ChangeLog = ({ closeChangeLog }: ChangeLogProps) => {
|
||||
const { data: changeLogData, isLoading } = useFetchChangelog();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.body.style.overflow = 'hidden';
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
() => {
|
||||
console.log('run CleanUp !');
|
||||
document.body.style.overflow = 'auto';
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onClose = () => {
|
||||
document.body.style.overflow = 'auto';
|
||||
closeChangeLog();
|
||||
};
|
||||
|
||||
const changeLogs = useMemo(() => {
|
||||
if (changeLogData && Array.isArray(changeLogData)) {
|
||||
return changeLogData.map(({ name = '', body, published_at }:{name: string, body: string, published_at: string}) => ({
|
||||
version: name,
|
||||
major: !!(name.match(/v\d+\.0+\.0/)),
|
||||
date: published_at,
|
||||
content: body.replaceAll(/^(##|###) \[([^\]]+)\]\(([^)]+)\) \(([^)]+)\)/g, '')
|
||||
.replaceAll(/\(\[(.*?)\]\((https:\/\/github\.com\/towfiqi\/serpbear\/commit\/([a-f0-9]{40}))\)\)/g, ''),
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}, [changeLogData]);
|
||||
|
||||
return <SidePanel title='SerpBear Changelog' closePanel={onClose}>
|
||||
<React.Suspense fallback={<ChangeLogloader />}>
|
||||
{!isLoading && changeLogs.length > 0 && (
|
||||
<div className='changelog-body bg-[#f8f9ff] px-6 pt-4 pb-10 overflow-y-auto styled-scrollbar'>
|
||||
{changeLogs.map(({ version, content, date, major }) => {
|
||||
return (
|
||||
<div
|
||||
key={version}
|
||||
className={`domKeywords bg-white rounded mb-6 border ${major ? ' border-indigo-400' : 'border-transparent'}`}>
|
||||
<h4 className=' px-5 py-3 border-b border-b-gray-100 flex justify-between text-indigo-700 font-semibold'>
|
||||
<a href={`https://github.com/towfiqi/serpbear/releases/tag/${version}`}>
|
||||
{version} {major && <span className=' text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded ml-2'>Major</span>}
|
||||
</a>
|
||||
<span className=' text-sm text-gray-500'>
|
||||
<TimeAgo title={dayjs(date).format('DD-MMM-YYYY, hh:mm:ss A')} date={date} />
|
||||
</span>
|
||||
</h4>
|
||||
<div className='changelog-content px-5 py-3 text-sm text-left'><Markdown>{content}</Markdown></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{isLoading && <ChangeLogloader />}
|
||||
</React.Suspense>
|
||||
</SidePanel>;
|
||||
};
|
||||
|
||||
export default ChangeLog;
|
||||
1082
package-lock.json
generated
1082
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@
|
||||
"react-chartjs-2": "^4.3.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-query": "^3.39.2",
|
||||
"react-timeago": "^7.1.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useFetchDomains } from '../../../services/domains';
|
||||
import { useFetchKeywords } from '../../../services/keywords';
|
||||
import { useFetchSettings } from '../../../services/settings';
|
||||
import AddKeywords from '../../../components/keywords/AddKeywords';
|
||||
import Footer from '../../../components/common/Footer';
|
||||
|
||||
const SingleDomain: NextPage = () => {
|
||||
const router = useRouter();
|
||||
@@ -104,6 +105,7 @@ const SingleDomain: NextPage = () => {
|
||||
closeModal={() => setShowAddKeywords(false)}
|
||||
/>
|
||||
</CSSTransition>
|
||||
<Footer currentVersion={appSettings?.version ? appSettings.version : ''} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useFetchDomains } from '../../../../services/domains';
|
||||
import { useFetchSCKeywords } from '../../../../services/searchConsole';
|
||||
import SCKeywordsTable from '../../../../components/keywords/SCKeywordsTable';
|
||||
import { useFetchSettings } from '../../../../services/settings';
|
||||
import Footer from '../../../../components/common/Footer';
|
||||
|
||||
const DiscoverPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
@@ -88,6 +89,7 @@ const DiscoverPage: NextPage = () => {
|
||||
<CSSTransition in={showSettings} timeout={300} classNames="settings_anim" unmountOnExit mountOnEnter>
|
||||
<Settings closeSettings={() => setShowSettings(false)} />
|
||||
</CSSTransition>
|
||||
<Footer currentVersion={appSettings?.settings?.version ? appSettings.settings.version : ''} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import KeywordIdeasTable from '../../../../components/ideas/KeywordIdeasTable';
|
||||
import { useFetchKeywordIdeas } from '../../../../services/adwords';
|
||||
import KeywordIdeasUpdater from '../../../../components/ideas/KeywordIdeasUpdater';
|
||||
import Modal from '../../../../components/common/Modal';
|
||||
import Footer from '../../../../components/common/Footer';
|
||||
|
||||
const DiscoverPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
@@ -104,7 +105,7 @@ const DiscoverPage: NextPage = () => {
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<Footer currentVersion={appSettings?.settings?.version ? appSettings.settings.version : ''} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useFetchDomains } from '../../../../services/domains';
|
||||
import { useFetchSCInsight } from '../../../../services/searchConsole';
|
||||
import SCInsight from '../../../../components/insight/Insight';
|
||||
import { useFetchSettings } from '../../../../services/settings';
|
||||
import Footer from '../../../../components/common/Footer';
|
||||
|
||||
const InsightPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
@@ -88,6 +89,7 @@ const InsightPage: NextPage = () => {
|
||||
<CSSTransition in={showSettings} timeout={300} classNames="settings_anim" unmountOnExit mountOnEnter>
|
||||
<Settings closeSettings={() => setShowSettings(false)} />
|
||||
</CSSTransition>
|
||||
<Footer currentVersion={appSettings?.settings?.version ? appSettings.settings.version : ''} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useCheckMigrationStatus, useFetchSettings } from '../../services/settin
|
||||
import { fetchDomainScreenshot, useFetchDomains } from '../../services/domains';
|
||||
import DomainItem from '../../components/domains/DomainItem';
|
||||
import Icon from '../../components/common/Icon';
|
||||
import Footer from '../../components/common/Footer';
|
||||
|
||||
type thumbImages = { [domain:string] : string }
|
||||
|
||||
@@ -23,7 +24,6 @@ const Domains: NextPage = () => {
|
||||
const { data: appSettingsData, isLoading: isAppSettingsLoading } = useFetchSettings();
|
||||
const { data: domainsData, isLoading } = useFetchDomains(router, true);
|
||||
const { data: migrationStatus } = useCheckMigrationStatus();
|
||||
// const { mutate: updateDatabaseMutate, isLoading: isUpdatingDB } = useMigrateDatabase((res:Object) => { window.location.reload(); });
|
||||
|
||||
const appSettings:SettingsType = appSettingsData?.settings || {};
|
||||
const { scraper_type = '' } = appSettings;
|
||||
@@ -139,9 +139,7 @@ const Domains: NextPage = () => {
|
||||
<CSSTransition in={showSettings} timeout={300} classNames="settings_anim" unmountOnExit mountOnEnter>
|
||||
<Settings closeSettings={() => setShowSettings(false)} />
|
||||
</CSSTransition>
|
||||
<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.version || '0.0.0'}</a></span>
|
||||
</footer>
|
||||
<Footer currentVersion={appSettings?.version ? appSettings.version : ''} />
|
||||
<Toaster position='bottom-center' containerClassName="react_toaster" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useFetchSettings } from '../../services/settings';
|
||||
import Settings from '../../components/settings/Settings';
|
||||
import SelectField from '../../components/common/SelectField';
|
||||
import allCountries, { adwordsLanguages } from '../../utils/countries';
|
||||
import Footer from '../../components/common/Footer';
|
||||
|
||||
const Research: NextPage = () => {
|
||||
const router = useRouter();
|
||||
@@ -141,6 +142,7 @@ const Research: NextPage = () => {
|
||||
<CSSTransition in={showSettings} timeout={300} classNames="settings_anim" unmountOnExit mountOnEnter>
|
||||
<Settings closeSettings={() => setShowSettings(false)} />
|
||||
</CSSTransition>
|
||||
<Footer currentVersion={appSettings?.settings?.version ? appSettings.settings.version : ''} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
10
services/misc.tsx
Normal file
10
services/misc.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
export async function fetchChangelog() {
|
||||
const res = await fetch('https://api.github.com/repos/towfiqi/serpbear/releases', { method: 'GET' });
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function useFetchChangelog() {
|
||||
return useQuery('changelog', () => fetchChangelog(), { cacheTime: 60 * 60 * 1000 });
|
||||
}
|
||||
52
styles/changelog.css
Normal file
52
styles/changelog.css
Normal file
@@ -0,0 +1,52 @@
|
||||
.changelog-body{
|
||||
max-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.changelog-content h1{
|
||||
margin: 1.2rem 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.changelog-content h2{
|
||||
margin: 1.2rem 0;
|
||||
font-size: 1.6rem;
|
||||
font-weight: bold;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.changelog-content h2:after {
|
||||
content: "🌟";
|
||||
font-size: 1.2rem;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.changelog-content h3{
|
||||
margin: 1.2rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.changelog-content h4{
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
.changelog-content ul{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 20px;
|
||||
list-style-type: disc;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
|
||||
.changelog-content a:link{
|
||||
color: rgb(75, 75, 241)
|
||||
}
|
||||
.changelog-content a:visited{
|
||||
color: rgb(116, 116, 121)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
@import url("./fflag.css");
|
||||
|
||||
@import url("./changelog.css");
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
Reference in New Issue
Block a user