mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
452 lines
16 KiB
TypeScript
452 lines
16 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import styles from './home.module.css';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import { Trans, useTranslation } from 'react-i18next';
|
|
import { Comment, Subplebbit, useAccount, useAccountSubplebbits, useSubplebbits } from '@plebbit/plebbit-react-hooks';
|
|
import packageJson from '../../../package.json';
|
|
import useDefaultSubplebbits, { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
|
|
import usePopularPosts from '../../hooks/use-popular-posts';
|
|
import { getCommentMediaInfo } from '../../lib/utils/media-utils';
|
|
import { CatalogPostMedia } from '../../components/catalog-row';
|
|
import LoadingEllipsis from '../../components/loading-ellipsis';
|
|
import { shuffle } from 'lodash';
|
|
|
|
// for temporary hook to fetch subplebbit stats
|
|
import { useMemo } from 'react';
|
|
import { create } from 'zustand';
|
|
|
|
const isValidAddress = (address: string): boolean => {
|
|
if (address.includes('/') || address.includes('\\') || address.includes(' ')) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const SearchBar = () => {
|
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
|
|
const handleSearchSubmit = () => {
|
|
const inputValue = searchInputRef.current?.value;
|
|
if (inputValue && isValidAddress(inputValue)) {
|
|
navigate(`/p/${inputValue}`);
|
|
} else {
|
|
alert('invalid address');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={styles.searchBar}>
|
|
<input
|
|
autoCorrect='off'
|
|
autoComplete='off'
|
|
spellCheck='false'
|
|
autoCapitalize='off'
|
|
type='text'
|
|
placeholder={`"board.eth/.sol" ${t('or')} "12D3KooW..."`}
|
|
ref={searchInputRef}
|
|
onSubmit={handleSearchSubmit}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
handleSearchSubmit();
|
|
}
|
|
}}
|
|
/>
|
|
<button onClick={handleSearchSubmit}>{t('go')}</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const InfoBox = () => {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<div className={styles.box}>
|
|
<div className={styles.infoboxBar}>
|
|
<h2>{t('what_is_plebchan')}</h2>
|
|
</div>
|
|
<div className={styles.boxContent}>
|
|
<Trans i18nKey='plebchan_description' shouldUnescape={true} components={{ 1: <Link to='https://plebbit.com' target='_blank' rel='noopener noreferrer' /> }} />
|
|
<br />
|
|
<br />
|
|
{t('no_global_rules_info')}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const Board = ({ isOffline, subplebbit }: { isOffline: boolean; subplebbit: Subplebbit }) => {
|
|
const { t } = useTranslation();
|
|
const { address, title, tags } = subplebbit;
|
|
const nsfwTags = ['adult', 'gore'];
|
|
const nsfwTag = tags.find((tag: string) => nsfwTags.includes(tag));
|
|
|
|
return (
|
|
<div className={styles.subplebbit} key={address}>
|
|
<Link to={`/p/${address}`}>{title || address}</Link>
|
|
{nsfwTag && <span className={styles.nsfw}> ({t(nsfwTag)})</span>}
|
|
{isOffline && <span className={styles.offlineIcon} />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const Boards = ({ multisub, subplebbits }: { multisub: Subplebbit[]; subplebbits: any }) => {
|
|
const { t } = useTranslation();
|
|
const account = useAccount();
|
|
const subscriptions = account?.subscriptions || [];
|
|
const { accountSubplebbits } = useAccountSubplebbits();
|
|
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
|
|
|
|
const plebbitSubs = multisub.filter((sub) => sub.tags.includes('plebbit'));
|
|
const interestsSubs = multisub.filter(
|
|
(sub) => sub.tags.includes('topic') && !sub.tags.includes('plebbit') && !sub.tags.includes('country') && !sub.tags.includes('international'),
|
|
);
|
|
const randomSubs = multisub.filter((sub) => sub.tags.includes('random') && !sub.tags.includes('plebbit'));
|
|
const internationalSubs = multisub.filter((sub) => sub.tags.includes('international') || sub.tags.includes('country'));
|
|
const projectsSubs = multisub.filter((sub) => sub.tags.includes('project') && !sub.tags.includes('plebbit') && !sub.tags.includes('topic'));
|
|
|
|
const isSubOffline = (address: string) => {
|
|
const subplebbit = subplebbits && subplebbits.find((sub: Subplebbit) => sub?.address === address);
|
|
const isOffline = subplebbit?.updatedAt && subplebbit.updatedAt < Date.now() / 1000 - 60 * 60;
|
|
return isOffline;
|
|
};
|
|
|
|
return (
|
|
<div className={`${styles.box} ${styles.boardsBox}`}>
|
|
<div className={styles.boxBar}>
|
|
<h2 className={styles.capitalize}>{t('boards')}</h2>
|
|
<span>{t('options')} ▼</span>
|
|
</div>
|
|
<div className={styles.boardsBoxContent}>
|
|
<div className={styles.column}>
|
|
<h3>Plebbit</h3>
|
|
<div className={styles.list}>
|
|
{plebbitSubs.map((sub) => (
|
|
<Board key={sub.address} subplebbit={sub} isOffline={sub.address && isSubOffline(sub.address)} />
|
|
))}
|
|
</div>
|
|
<h3>{t('projects')}</h3>
|
|
<div className={styles.list}>
|
|
{projectsSubs.map((sub) => (
|
|
<Board key={sub.address} subplebbit={sub} isOffline={sub.address && isSubOffline(sub.address)} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className={styles.column}>
|
|
<h3>{t('interests')}</h3>
|
|
<div className={styles.list}>
|
|
{interestsSubs.map((sub) => (
|
|
<Board key={sub.address} subplebbit={sub} isOffline={sub.address && isSubOffline(sub.address)} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className={styles.column}>
|
|
<h3>{t('random')}</h3>
|
|
<div className={styles.list}>
|
|
{randomSubs.map((sub) => (
|
|
<Board key={sub.address} subplebbit={sub} isOffline={sub.address && isSubOffline(sub.address)} />
|
|
))}
|
|
</div>
|
|
<h3>{t('international')}</h3>
|
|
<div className={styles.list}>
|
|
{internationalSubs.map((sub) => (
|
|
<Board key={sub.address} subplebbit={sub} isOffline={sub.address && isSubOffline(sub.address)} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className={styles.column}>
|
|
<div className={styles.list}>
|
|
<h3>{t('subscriptions')}</h3>
|
|
{subscriptions.length > 0
|
|
? subscriptions.map((address: string, index: number) => (
|
|
<div className={styles.subplebbit} key={index}>
|
|
<Link to={`/p/${address}`}>{address}</Link>
|
|
</div>
|
|
))
|
|
: t('not_subscribed')}
|
|
</div>
|
|
<div className={styles.list}>
|
|
<h3>{t('moderating')}</h3>
|
|
{accountSubplebbitAddresses.length > 0
|
|
? accountSubplebbitAddresses.map((address: string, index: number) => (
|
|
<div className={styles.subplebbit} key={index}>
|
|
<Link to={`/p/${address}`}>p/{address}</Link>
|
|
</div>
|
|
))
|
|
: t('not_moderating')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface PopularThreadProps {
|
|
post: Comment;
|
|
boardTitle: string | undefined;
|
|
boardShortAddress: string;
|
|
}
|
|
|
|
const PopularThreadCard = ({ post, boardTitle, boardShortAddress }: PopularThreadProps) => {
|
|
const { cid, content, subplebbitAddress, title } = post || {};
|
|
const commentMediaInfo = getCommentMediaInfo(post);
|
|
|
|
return (
|
|
<div className={styles.popularThread} key={cid}>
|
|
<div className={styles.title}>{boardTitle || boardShortAddress}</div>
|
|
<div className={styles.mediaContainer}>
|
|
<Link to={`/p/${subplebbitAddress}/c/${cid}`}>
|
|
<CatalogPostMedia commentMediaInfo={commentMediaInfo} />
|
|
</Link>
|
|
</div>
|
|
<div className={styles.threadContent}>
|
|
{title && <b>{title}</b>}
|
|
{content && (content.length > 99 ? `: ${content.substring(0, 99)}...` : `: ${content}`)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const PopularThreads = ({ subplebbits }: { subplebbits: any }) => {
|
|
const { t } = useTranslation();
|
|
const popularPosts = usePopularPosts(subplebbits);
|
|
|
|
return (
|
|
<div className={styles.box}>
|
|
<div className={`${styles.boxBar} ${styles.color2ColorBar}`}>
|
|
<h2 className={styles.capitalize}>{t('popular_threads')}</h2>
|
|
<span>{t('options')} ▼</span>
|
|
</div>
|
|
<div className={`${styles.boxContent} ${popularPosts.length === 8 ? styles.popularThreads : ''}`}>
|
|
{popularPosts.length < 8 ? (
|
|
<LoadingEllipsis string={t('loading')} />
|
|
) : (
|
|
popularPosts.map((post: any) => (
|
|
<PopularThreadCard key={post.cid} post={post} boardTitle={post.subplebbitTitle || post.subplebbitAddress} boardShortAddress={post.subplebbitAddress} />
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// temporary hook to fetch subplebbit stats
|
|
function useSubplebbitsStats(options: any) {
|
|
const { subplebbitAddresses, accountName } = options || {};
|
|
const account = useAccount({ accountName });
|
|
const { subplebbits } = useSubplebbits({ subplebbitAddresses });
|
|
|
|
const { setSubplebbitStats, subplebbitsStats } = useSubplebbitsStatsStore();
|
|
|
|
useEffect(() => {
|
|
if (!subplebbitAddresses || subplebbitAddresses.length === 0 || !account) {
|
|
return;
|
|
}
|
|
|
|
subplebbits.forEach((subplebbit) => {
|
|
if (subplebbit && subplebbit.statsCid && !subplebbitsStats[subplebbit.address]) {
|
|
account.plebbit
|
|
.fetchCid(subplebbit.statsCid)
|
|
.then((fetchedStats: any) => {
|
|
setSubplebbitStats(subplebbit.address, JSON.parse(fetchedStats));
|
|
})
|
|
.catch((error: any) => {
|
|
console.error('Fetching subplebbit stats failed', { subplebbitAddress: subplebbit.address, error });
|
|
});
|
|
}
|
|
});
|
|
}, [account, subplebbits, setSubplebbitStats, subplebbitsStats, subplebbitAddresses]);
|
|
|
|
return useMemo(() => {
|
|
return subplebbitAddresses.reduce((acc: any, address: any) => {
|
|
acc[address] = subplebbitsStats[address] || { loading: true };
|
|
return acc;
|
|
}, {});
|
|
}, [subplebbitsStats, subplebbitAddresses]);
|
|
}
|
|
|
|
export type SubplebbitsStatsState = {
|
|
subplebbitsStats: { [subplebbitAddress: string]: any };
|
|
setSubplebbitStats: Function;
|
|
};
|
|
|
|
const useSubplebbitsStatsStore = create<SubplebbitsStatsState>((set) => ({
|
|
subplebbitsStats: {},
|
|
setSubplebbitStats: (subplebbitAddress: string, subplebbitStats: any) =>
|
|
set((state) => ({
|
|
subplebbitsStats: { ...state.subplebbitsStats, [subplebbitAddress]: subplebbitStats },
|
|
})),
|
|
}));
|
|
|
|
const Stats = ({ multisub, subplebbitAddresses }: { multisub: any; subplebbitAddresses: string[] }) => {
|
|
const { t } = useTranslation();
|
|
const stats = useSubplebbitsStats({ subplebbitAddresses });
|
|
|
|
const allStatsLoaded = useMemo(() => {
|
|
return subplebbitAddresses.every((address) => stats[address]);
|
|
}, [stats, subplebbitAddresses]);
|
|
|
|
const { totalPosts, currentUsers } = useMemo(() => {
|
|
let totalPosts = 0;
|
|
let currentUsers = 0;
|
|
|
|
if (allStatsLoaded) {
|
|
Object.values(stats).forEach((stat: any) => {
|
|
totalPosts += stat.allPostCount || 0;
|
|
currentUsers += stat.weekActiveUserCount || 0;
|
|
});
|
|
}
|
|
|
|
return { totalPosts, currentUsers };
|
|
}, [stats, allStatsLoaded]);
|
|
|
|
const boardsTracked = Object.values(stats).filter((stat: any) => stat && !stat.loading).length;
|
|
const enoughStats = boardsTracked >= multisub.length / 2;
|
|
|
|
return (
|
|
<div className={styles.box}>
|
|
<div className={`${styles.boxBar} ${styles.color2ColorBar}`}>
|
|
<h2 className={styles.capitalize}>{t('stats')}</h2>
|
|
</div>
|
|
<div className={`${styles.boxContent} ${enoughStats ? styles.stats : ''}`}>
|
|
{/* <div className={styles.statsInfo}>
|
|
<p>{t('stats_info')}</p>
|
|
</div> */}
|
|
{enoughStats ? (
|
|
<>
|
|
<div className={styles.stat}>
|
|
<b>{t('total_posts')}</b> {totalPosts}
|
|
</div>
|
|
<div className={styles.stat}>
|
|
<b>{t('current_users')}</b> {currentUsers}
|
|
</div>
|
|
<div className={styles.stat}>
|
|
<b>{t('boards_tracked')}</b> {boardsTracked}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<LoadingEllipsis string={t('loading')} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const { version } = packageJson;
|
|
const downloadAppLink = (() => {
|
|
const platform = navigator.platform;
|
|
if (platform === 'Linux' || platform === 'Linux x86_64' || platform === 'Linux i686' || platform === 'Linux aarch64') {
|
|
return `https://github.com/plebbit/plebchan/releases/download/v${version}/plebchan-${version}.AppImage`;
|
|
} else if (platform === 'Win32' || platform === 'Win64' || platform === 'Windows') {
|
|
return `https://github.com/plebbit/plebchan/releases/download/v${version}/plebchan.Portable.${version}.exe`;
|
|
} else if (platform === 'MacIntel' || platform === 'Macintosh') {
|
|
return `https://github.com/plebbit/plebchan/releases/download/v${version}/plebchan-${version}.dmg`;
|
|
} else if (platform === 'Android') {
|
|
return undefined;
|
|
} else if (platform === 'iPhone' || platform === 'iPad') {
|
|
return undefined;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
})();
|
|
|
|
const isElectron = window.isElectron === true;
|
|
const commitRef = process.env.REACT_APP_COMMIT_REF;
|
|
|
|
const Footer = () => {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<>
|
|
<ul className={styles.footer}>
|
|
<li>
|
|
<a href='https://plebbit.com' target='_blank' rel='noopener noreferrer'>
|
|
{t('about')}
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href='https://twitter.com/plebchan_eth' target='_blank' rel='noopener noreferrer'>
|
|
Twitter
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href='https://t.me/plebbit' target='_blank' rel='noopener noreferrer'>
|
|
Telegram
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href='https://discord.gg/E7ejphwzGW' target='_blank' rel='noopener noreferrer'>
|
|
Discord
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href='https://github.com/plebbit/plebchan' target='_blank' rel='noopener noreferrer'>
|
|
GitHub
|
|
</a>
|
|
</li>
|
|
{downloadAppLink && (
|
|
<li>
|
|
<a href={downloadAppLink} target='_blank' rel='noopener noreferrer'>
|
|
{t('download_app')}
|
|
</a>
|
|
</li>
|
|
)}
|
|
<li>
|
|
<a href='https://etherscan.io/token/0xEA81DaB2e0EcBc6B5c4172DE4c22B6Ef6E55Bd8f' target='_blank' rel='noopener noreferrer'>
|
|
{t('token')}
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href='https://github.com/plebbit/whitepaper/discussions/2' target='_blank' rel='noopener noreferrer'>
|
|
{t('whitepaper')}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
<div className={styles.version}>
|
|
<a href={`https://github.com/plebbit/plebchan/releases/tag/v${packageJson.version}`} target='_blank' rel='noopener noreferrer'>
|
|
v{packageJson.version}
|
|
</a>
|
|
{isElectron && (
|
|
<a className={styles.fullNodeStats} href='http://localhost:5001/webui/' target='_blank' rel='noreferrer'>
|
|
node stats
|
|
</a>
|
|
)}
|
|
{commitRef && (
|
|
<a href={`https://github.com/plebbit/plebchan/commit/${commitRef}`} target='_blank' rel='noopener noreferrer'>
|
|
#{commitRef.slice(0, 7)}
|
|
</a>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const HomeLogo = () => {
|
|
return (
|
|
<Link to='/'>
|
|
<div className={styles.logo}>
|
|
<img alt='' src='/assets/logo/logo-transparent.png' />
|
|
</div>
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
const Home = () => {
|
|
const defaultSubplebbits = useDefaultSubplebbits();
|
|
const subplebbitAddresses = useDefaultSubplebbitAddresses();
|
|
const { subplebbits } = useSubplebbits({ subplebbitAddresses });
|
|
|
|
return (
|
|
<div className={styles.content}>
|
|
<HomeLogo />
|
|
<SearchBar />
|
|
<InfoBox />
|
|
<Boards multisub={defaultSubplebbits} subplebbits={subplebbits} />
|
|
<PopularThreads subplebbits={subplebbits} />
|
|
<Stats multisub={defaultSubplebbits} subplebbitAddresses={subplebbitAddresses} />
|
|
<Footer />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Home;
|