mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
236 lines
7.1 KiB
TypeScript
236 lines
7.1 KiB
TypeScript
import { useEffect, useMemo, useRef, FormEvent } from 'react';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import { Trans, useTranslation } from 'react-i18next';
|
|
import styles from './home.module.css';
|
|
import { useDirectories, useDirectoryAddresses } from '../../hooks/use-directories';
|
|
import { CommunityStatsCollector, useCommunitiesStatsStore } from '../../hooks/use-communities-stats';
|
|
import PopularThreadsBox from './popular-threads-box';
|
|
import BoardsList from './boards-list';
|
|
import SiteLegalMeta from '../../components/site-legal-meta';
|
|
import LoadingEllipsis from '../../components/loading-ellipsis';
|
|
import useDirectoryModalStore from '../../stores/use-directory-modal-store';
|
|
import DisclaimerModal from '../../components/disclaimer-modal';
|
|
import DirectoryModal from '../../components/directory-modal';
|
|
import { getBoardPath } from '../../lib/utils/route-utils';
|
|
import { isWebRuntime } from '../../lib/media-hosting/show-upload-controls';
|
|
import lowerCase from 'lodash/lowerCase';
|
|
|
|
// https://github.com/bitsocialnet/lists/blob/master/5chan-directories.json
|
|
|
|
const SearchBar = () => {
|
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const directories = useDirectories();
|
|
|
|
const handleSearchSubmit = (e: FormEvent) => {
|
|
e.preventDefault();
|
|
const searchInput = searchInputRef.current?.value;
|
|
if (searchInput) {
|
|
const boardPath = getBoardPath(searchInput, directories);
|
|
navigate(`/${boardPath}`);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={styles.searchBar}>
|
|
<form onSubmit={handleSearchSubmit}>
|
|
<input
|
|
autoCorrect='off'
|
|
autoComplete='off'
|
|
spellCheck='false'
|
|
autoCapitalize='off'
|
|
type='text'
|
|
placeholder={lowerCase(t('enter_board_address'))}
|
|
ref={searchInputRef}
|
|
/>
|
|
<button className={styles.searchButton}>{t('go')}</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const InfoBox = () => {
|
|
const { t } = useTranslation();
|
|
const isWeb = isWebRuntime();
|
|
return (
|
|
<div className={`${styles.box} ${styles.infoBox}`}>
|
|
<div className={styles.infoboxBar}>
|
|
<h2>{t('what_is_5chan')}</h2>
|
|
</div>
|
|
<div className={styles.boxContent}>
|
|
<Trans
|
|
i18nKey='5chan_description'
|
|
shouldUnescape={true}
|
|
components={{
|
|
1: <Link key='rules-link' to='/rules' />,
|
|
2: <Link key='faqs-link' to='/faq' />,
|
|
}}
|
|
/>
|
|
<br />
|
|
<br />
|
|
{isWeb ? (
|
|
<Trans
|
|
i18nKey='no_global_rules_info'
|
|
shouldUnescape={true}
|
|
components={{
|
|
1: <a key='releases-link' href='https://github.com/bitsocialnet/5chan/releases/latest' target='_blank' rel='noopener noreferrer' />,
|
|
}}
|
|
/>
|
|
) : (
|
|
t('app_p2p_info')
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface StatValueProps {
|
|
isLoaded: boolean;
|
|
loadingLabel: string;
|
|
value: number;
|
|
}
|
|
|
|
const StatValue = ({ isLoaded, loadingLabel, value }: StatValueProps) => (isLoaded ? <>{value}</> : <LoadingEllipsis string={loadingLabel} />);
|
|
|
|
const Stats = ({ directoryAddresses }: { directoryAddresses: string[] }) => {
|
|
const { t } = useTranslation();
|
|
const communitiesStats = useCommunitiesStatsStore((state) => state.communityStats);
|
|
|
|
const { totalPosts, currentUsers, boardsTracked, allDirectoryStatsLoaded } = useMemo(() => {
|
|
let totalPosts = 0;
|
|
let currentUsers = 0;
|
|
let boardsTracked = 0;
|
|
let allDirectoryStatsLoaded = directoryAddresses.length > 0;
|
|
|
|
for (const address of directoryAddresses) {
|
|
const stat = communitiesStats[address];
|
|
|
|
if (!stat || stat.allPostCount === undefined) {
|
|
allDirectoryStatsLoaded = false;
|
|
continue;
|
|
}
|
|
|
|
totalPosts += stat.allPostCount || 0;
|
|
currentUsers += stat.weekActiveUserCount || 0;
|
|
boardsTracked++;
|
|
}
|
|
|
|
return { totalPosts, currentUsers, boardsTracked, allDirectoryStatsLoaded };
|
|
}, [communitiesStats, directoryAddresses]);
|
|
|
|
const loadingLabel = t('loading');
|
|
|
|
return (
|
|
<>
|
|
{/* Render collectors to fetch stats for each community */}
|
|
{directoryAddresses.map((address) => (
|
|
<CommunityStatsCollector key={address} communityAddress={address} />
|
|
))}
|
|
<div className={styles.box}>
|
|
<div className={`${styles.boxBar} ${styles.color2ColorBar}`}>
|
|
<h2 className='capitalize'>{t('stats')}</h2>
|
|
</div>
|
|
<div className={`${styles.boxContent} ${styles.stats}`}>
|
|
<div className={styles.stat}>
|
|
<b>{t('total_posts')}</b> <StatValue isLoaded={allDirectoryStatsLoaded} loadingLabel={loadingLabel} value={totalPosts} />
|
|
</div>
|
|
<div className={styles.stat}>
|
|
<b>{t('current_users')}</b> <StatValue isLoaded={allDirectoryStatsLoaded} loadingLabel={loadingLabel} value={currentUsers} />
|
|
</div>
|
|
<div className={styles.stat}>
|
|
<b>{t('boards_tracked')}</b> <StatValue isLoaded={allDirectoryStatsLoaded} loadingLabel={loadingLabel} value={boardsTracked} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const Footer = () => {
|
|
const { t } = useTranslation();
|
|
return (
|
|
<>
|
|
<ul className={styles.footer}>
|
|
<li>
|
|
<a href='https://bitsocial.net' target='_blank' rel='noopener noreferrer'>
|
|
{t('about')}
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<Link to='/faq'>FAQ</Link>
|
|
</li>
|
|
<li>
|
|
<Link to='/rules'>Rules</Link>
|
|
</li>
|
|
<li>
|
|
<a href='https://x.com/5chanapp' target='_blank' rel='noopener noreferrer'>
|
|
Twitter/X
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href='https://t.me/bitsocialnet' target='_blank' rel='noopener noreferrer'>
|
|
Telegram
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<Link to='/pass'>{t('support_5chan')}</Link>
|
|
</li>
|
|
<li>
|
|
<a href='https://github.com/bitsocialnet/5chan' target='_blank' rel='noopener noreferrer'>
|
|
Source Code
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
<div className={styles.footerInfo}>
|
|
<SiteLegalMeta />
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const HomeLogo = () => {
|
|
return (
|
|
<Link to='/'>
|
|
<div className={styles.logo}>
|
|
<img alt='' src='assets/logo/logo-transparent.png' />
|
|
</div>
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
const Home = () => {
|
|
const directories = useDirectories();
|
|
const directoryAddresses = useDirectoryAddresses();
|
|
const { closeDirectoryModal } = useDirectoryModalStore();
|
|
|
|
useEffect(() => {
|
|
document.title = '5chan';
|
|
}, []);
|
|
|
|
// Close directory modal when navigating away from home
|
|
useEffect(() => {
|
|
return () => {
|
|
closeDirectoryModal();
|
|
};
|
|
}, [closeDirectoryModal]);
|
|
|
|
return (
|
|
<>
|
|
<DisclaimerModal />
|
|
<DirectoryModal />
|
|
<div className={styles.content}>
|
|
<HomeLogo />
|
|
<SearchBar />
|
|
<InfoBox />
|
|
<BoardsList multisub={directories} />
|
|
<PopularThreadsBox directories={directories} directoryAddresses={directoryAddresses} />
|
|
<Stats directoryAddresses={directoryAddresses} />
|
|
<Footer />
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Home;
|