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:
towfiqi
2024-03-02 20:48:22 +06:00
parent 407ab8db83
commit bb4a6844b5
14 changed files with 1306 additions and 7 deletions

View 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;

View 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;

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);

View File

@@ -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
View 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
View 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)
}

View File

@@ -1,5 +1,5 @@
@import url("./fflag.css");
@import url("./changelog.css");
@tailwind base;
@tailwind components;
@tailwind utilities;