refactor(ui): rename topbar to boardsbar and add desktop footer for board, thread, and catalog

This commit is contained in:
plebeius
2026-02-23 16:47:51 +08:00
parent a7721fc252
commit 4433834c1c
39 changed files with 672 additions and 294 deletions
+4 -4
View File
@@ -35,8 +35,8 @@ import FeedCacheContainer from './components/feed-cache-container';
import ReplyModal from './components/reply-modal'; import ReplyModal from './components/reply-modal';
import PostForm from './components/post-form'; import PostForm from './components/post-form';
import BoardBlotter from './components/board-blotter'; import BoardBlotter from './components/board-blotter';
import TopBar from './components/topbar'; import BoardsBar from './components/boardsbar';
import TopbarEditModal from './components/topbar-edit-modal'; import BoardsBarEditModal from './components/boardsbar-edit-modal';
import DirectoryModal from './components/directory-modal'; import DirectoryModal from './components/directory-modal';
import DisclaimerModal from './components/disclaimer-modal'; import DisclaimerModal from './components/disclaimer-modal';
import SettingsModal from './components/settings-modal'; import SettingsModal from './components/settings-modal';
@@ -96,9 +96,9 @@ const BoardLayout = () => {
return ( return (
<div className={styles.boardLayout}> <div className={styles.boardLayout}>
<TopBar /> <BoardsBar />
<CreateBoardModal /> <CreateBoardModal />
<TopbarEditModal /> <BoardsBarEditModal />
<DirectoryModal /> <DirectoryModal />
<DisclaimerModal /> <DisclaimerModal />
<BoardHeader /> <BoardHeader />
+17 -5
View File
@@ -32,7 +32,7 @@ interface BoardButtonsProps {
isTopbar?: boolean; isTopbar?: boolean;
} }
const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => { export const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams(); const params = useParams();
const directories = useDirectories(); const directories = useDirectories();
@@ -68,7 +68,7 @@ const SubscribeButton = ({ address }: BoardButtonsProps) => {
); );
}; };
const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView, isInModQueueView }: BoardButtonsProps) => { export const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView, isInModQueueView }: BoardButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams(); const params = useParams();
const directories = useDirectories(); const directories = useDirectories();
@@ -133,7 +133,7 @@ const RefreshButton = () => {
); );
}; };
const UpdateButton = () => { export const UpdateButton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const reset = useFeedResetStore((state) => state.reset); const reset = useFeedResetStore((state) => state.reset);
return ( return (
@@ -143,7 +143,7 @@ const UpdateButton = () => {
); );
}; };
const AutoButton = () => { export const AutoButton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const handleAutoClick = () => { const handleAutoClick = () => {
@@ -181,6 +181,18 @@ const BottomButton = () => {
); );
}; };
export const TopButton = () => {
const { t } = useTranslation();
const handleClick = () => {
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
};
return (
<button className='button' onClick={handleClick}>
{t('top')}
</button>
);
};
const SortOptions = () => { const SortOptions = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { sortType, setSortType } = useSortingStore(); const { sortType, setSortType } = useSortingStore();
@@ -442,7 +454,7 @@ export const MobileBoardButtons = () => {
); );
}; };
const PostPageStats = () => { export const PostPageStats = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams(); const params = useParams();
+1 -1
View File
@@ -1 +1 @@
export { MobileBoardButtons, DesktopBoardButtons } from './board-buttons'; export { MobileBoardButtons, DesktopBoardButtons, PostPageStats, ReturnButton, CatalogButton, UpdateButton, AutoButton, TopButton } from './board-buttons';
@@ -34,3 +34,21 @@
font-weight: bold; font-weight: bold;
color: var(--button-desktop-text-color-hover); color: var(--button-desktop-text-color-hover);
} }
/* Footer-style compact pagination (layout in footer-first-row.module.css) */
.footerPageLink {
padding: 2px 6px;
font-size: 12px;
text-decoration: var(--button-text-decoration);
color: var(--button-desktop-text-color);
}
.footerPageLink:hover {
color: var(--button-desktop-text-color-hover);
text-decoration: var(--button-text-decoration);
}
.footerPageCurrent {
font-weight: bold;
color: var(--button-desktop-text-color-hover);
}
@@ -1,24 +1,75 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import StyleSelector from '../style-selector/style-selector';
import footerStyles from '../footer-first-row/footer-first-row.module.css';
import styles from './board-pagination.module.css'; import styles from './board-pagination.module.css';
export interface BoardPaginationProps { export interface BoardPaginationProps {
basePath: string; basePath: string;
currentPage: number; currentPage: number;
totalPages: number; totalPages: number;
/** When true, renders compact footer-style row with [1] [2] ... [10] Next Catalog + Style select */
footerStyle?: boolean;
} }
const BoardPagination = ({ basePath, currentPage, totalPages }: BoardPaginationProps) => { const BoardPagination = ({ basePath, currentPage, totalPages, footerStyle = false }: BoardPaginationProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const pageHref = (page: number) => (page === 1 ? basePath : `${basePath}/${page}`); const pageHref = (page: number) => (page === 1 ? basePath : `${basePath}/${page}`);
const prevHref = currentPage > 1 ? pageHref(currentPage - 1) : undefined; const prevHref = currentPage > 1 ? pageHref(currentPage - 1) : undefined;
const nextHref = currentPage < totalPages ? pageHref(currentPage + 1) : undefined; const nextHref = currentPage < totalPages ? pageHref(currentPage + 1) : undefined;
const catalogHref = `${basePath}/catalog`;
if (totalPages <= 1) { if (totalPages <= 1 && !footerStyle) {
return null; return null;
} }
if (footerStyle) {
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
const ellipsisThreshold = 7;
const showEllipsis = totalPages > ellipsisThreshold;
const visiblePages = showEllipsis ? [1, 2, currentPage, totalPages].filter((p, i, arr) => arr.indexOf(p) === i).sort((a, b) => a - b) : pageNumbers;
return (
<div className={footerStyles.footerRow}>
<div className={footerStyles.footerLeft}>
{visiblePages.map((page, idx) => {
const isCurrent = page === currentPage;
const prevPage = visiblePages[idx - 1];
const showLeadingEllipsis = showEllipsis && prevPage !== undefined && page - prevPage > 1;
return (
<span key={page}>
{showLeadingEllipsis && <span> ... </span>}
{isCurrent ? (
<span className={styles.footerPageCurrent}>[{page}]</span>
) : (
<Link to={pageHref(page)} className={styles.footerPageLink}>
[{page}]
</Link>
)}
</span>
);
})}
{nextHref && (
<>
{' '}
<Link to={nextHref} className={styles.footerPageLink}>
{t('next')}
</Link>
</>
)}{' '}
<Link to={catalogHref} className={styles.footerPageLink}>
{t('catalog')}
</Link>
</div>
<div className={footerStyles.footerRight}>
<span className={footerStyles.styleLabel}>{t('style')}:</span>
<StyleSelector />
</div>
</div>
);
}
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1); const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
return ( return (
@@ -8,7 +8,7 @@
z-index: 999; z-index: 999;
} }
.topbarEditDialog { .boardsbarEditDialog {
position: absolute; position: absolute;
width: 400px; width: 400px;
max-height: 80vh; max-height: 80vh;
@@ -31,7 +31,7 @@
} }
@media (max-width: 600px) { @media (max-width: 600px) {
.topbarEditDialog { .boardsbarEditDialog {
width: calc(100% - 20px); width: calc(100% - 20px);
margin-left: -50%; margin-left: -50%;
left: 50%; left: 50%;
@@ -154,7 +154,7 @@
text-decoration: var(--post-content-link-text-decoration-hover); text-decoration: var(--post-content-link-text-decoration-hover);
} }
.topbarEditFooter { .boardsbarEditFooter {
padding: 0; padding: 0;
margin: 0; margin: 0;
text-align: center; text-align: center;
@@ -1,10 +1,10 @@
import { useState, useMemo } from 'react'; import { useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { useAccount } from '@plebbit/plebbit-react-hooks'; import { useAccount } from '@plebbit/plebbit-react-hooks';
import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store'; import useBoardsBarEditModalStore from '../../stores/use-boardsbar-edit-modal-store';
import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store'; import useBoardsBarVisibilityStore from '../../stores/use-boardsbar-visibility-store';
import { getAllBoardCodes } from '../../constants/board-codes'; import { getAllBoardCodes } from '../../constants/board-codes';
import styles from './topbar-edit-modal.module.css'; import styles from './boardsbar-edit-modal.module.css';
const directoriesToString = (dirs: Set<string>): string => Array.from(dirs).sort().join(' '); const directoriesToString = (dirs: Set<string>): string => Array.from(dirs).sort().join(' ');
@@ -18,20 +18,20 @@ const stringToDirectories = (str: string): Set<string> => {
}; };
// Form component keyed by store values so it remounts with fresh state when modal reopens // Form component keyed by store values so it remounts with fresh state when modal reopens
const TopbarEditModalForm = ({ const BoardsBarEditModalForm = ({
visibleDirectories, visibleDirectories,
showSubscriptionsInTopbar, showSubscriptionsInBoardsBar,
setDirectoryVisibility, setDirectoryVisibility,
setShowSubscriptionsInTopbar, setShowSubscriptionsInBoardsBar,
closeTopbarEditModal, closeBoardsBarEditModal,
subscriptions, subscriptions,
location, location,
}: { }: {
visibleDirectories: Set<string>; visibleDirectories: Set<string>;
showSubscriptionsInTopbar: boolean; showSubscriptionsInBoardsBar: boolean;
setDirectoryVisibility: (code: string, visible: boolean) => void; setDirectoryVisibility: (code: string, visible: boolean) => void;
setShowSubscriptionsInTopbar: (show: boolean) => void; setShowSubscriptionsInBoardsBar: (show: boolean) => void;
closeTopbarEditModal: () => void; closeBoardsBarEditModal: () => void;
subscriptions: string[]; subscriptions: string[];
location: { pathname: string }; location: { pathname: string };
}) => { }) => {
@@ -39,7 +39,7 @@ const TopbarEditModalForm = ({
const allVisible = allBoardCodes.every((code) => visibleDirectories.has(code)); const allVisible = allBoardCodes.every((code) => visibleDirectories.has(code));
const [localDirectoryInput, setLocalDirectoryInput] = useState(() => (allVisible ? '' : directoriesToString(visibleDirectories))); const [localDirectoryInput, setLocalDirectoryInput] = useState(() => (allVisible ? '' : directoriesToString(visibleDirectories)));
const [showSubscriptions, setShowSubscriptions] = useState(() => showSubscriptionsInTopbar); const [showSubscriptions, setShowSubscriptions] = useState(() => showSubscriptionsInBoardsBar);
const handleSave = () => { const handleSave = () => {
if (localDirectoryInput.trim() === '') { if (localDirectoryInput.trim() === '') {
@@ -48,8 +48,8 @@ const TopbarEditModalForm = ({
const inputDirectories = stringToDirectories(localDirectoryInput); const inputDirectories = stringToDirectories(localDirectoryInput);
allBoardCodes.forEach((code) => setDirectoryVisibility(code, inputDirectories.has(code))); allBoardCodes.forEach((code) => setDirectoryVisibility(code, inputDirectories.has(code)));
} }
setShowSubscriptionsInTopbar(showSubscriptions); setShowSubscriptionsInBoardsBar(showSubscriptions);
closeTopbarEditModal(); closeBoardsBarEditModal();
}; };
return ( return (
@@ -76,7 +76,7 @@ const TopbarEditModalForm = ({
className={styles.editSubscriptionsLink} className={styles.editSubscriptionsLink}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
closeTopbarEditModal(); closeBoardsBarEditModal();
}} }}
> >
edit subscriptions edit subscriptions
@@ -86,16 +86,16 @@ const TopbarEditModalForm = ({
</div> </div>
</div> </div>
)} )}
<div className={styles.topbarEditFooter}> <div className={styles.boardsbarEditFooter}>
<button onClick={handleSave}>Save</button> <button onClick={handleSave}>Save</button>
</div> </div>
</> </>
); );
}; };
const TopbarEditModal = () => { const BoardsBarEditModal = () => {
const { showModal, closeTopbarEditModal } = useTopbarEditModalStore(); const { showModal, closeBoardsBarEditModal } = useBoardsBarEditModalStore();
const { visibleDirectories, showSubscriptionsInTopbar, setDirectoryVisibility, setShowSubscriptionsInTopbar } = useTopbarVisibilityStore(); const { visibleDirectories, showSubscriptionsInBoardsBar, setDirectoryVisibility, setShowSubscriptionsInBoardsBar } = useBoardsBarVisibilityStore();
const account = useAccount(); const account = useAccount();
const subscriptions = useMemo(() => account?.subscriptions || [], [account?.subscriptions]); const subscriptions = useMemo(() => account?.subscriptions || [], [account?.subscriptions]);
const location = useLocation(); const location = useLocation();
@@ -106,27 +106,27 @@ const TopbarEditModal = () => {
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => { const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
closeTopbarEditModal(); closeBoardsBarEditModal();
} }
}; };
const formKey = `${directoriesToString(visibleDirectories)}-${showSubscriptionsInTopbar}`; const formKey = `${directoriesToString(visibleDirectories)}-${showSubscriptionsInBoardsBar}`;
return ( return (
<div className={styles.backdrop} onClick={handleBackdropClick}> <div className={styles.backdrop} onClick={handleBackdropClick}>
<div className={styles.topbarEditDialog}> <div className={styles.boardsbarEditDialog}>
<div className={styles.hd}> <div className={styles.hd}>
<h2>Custom Board List</h2> <h2>Custom Board List</h2>
<button className={styles.closeButton} onClick={closeTopbarEditModal} title='Close' /> <button className={styles.closeButton} onClick={closeBoardsBarEditModal} title='Close' />
</div> </div>
<div className={styles.bd}> <div className={styles.bd}>
<TopbarEditModalForm <BoardsBarEditModalForm
key={formKey} key={formKey}
visibleDirectories={visibleDirectories} visibleDirectories={visibleDirectories}
showSubscriptionsInTopbar={showSubscriptionsInTopbar} showSubscriptionsInBoardsBar={showSubscriptionsInBoardsBar}
setDirectoryVisibility={setDirectoryVisibility} setDirectoryVisibility={setDirectoryVisibility}
setShowSubscriptionsInTopbar={setShowSubscriptionsInTopbar} setShowSubscriptionsInBoardsBar={setShowSubscriptionsInBoardsBar}
closeTopbarEditModal={closeTopbarEditModal} closeBoardsBarEditModal={closeBoardsBarEditModal}
subscriptions={subscriptions} subscriptions={subscriptions}
location={location} location={location}
/> />
@@ -136,4 +136,4 @@ const TopbarEditModal = () => {
); );
}; };
export default TopbarEditModal; export default BoardsBarEditModal;
@@ -1,4 +1,4 @@
// Types for topbar edit modal component // Types for boardsbar edit modal component
export interface BoardCodeCheckbox { export interface BoardCodeCheckbox {
code: string; code: string;
@@ -0,0 +1 @@
export { default } from './boardsbar-edit-modal';
@@ -1,29 +1,29 @@
.boardNavDesktop { .boardNavDesktop {
font-size: var(--topbar-font-size); font-size: var(--boardsbar-font-size);
color: var(--topbar-separator-color); color: var(--boardsbar-separator-color);
position: relative; position: relative;
} }
.checkingSubscriptions { .checkingSubscriptions {
font-size: var(--topbar-font-size); font-size: var(--boardsbar-font-size);
color: var(--topbar-desktop-link-text-color); color: var(--boardsbar-desktop-link-text-color);
text-transform: lowercase; text-transform: lowercase;
} }
.boardNavDesktop a, .navTopRight span { .boardNavDesktop a, .navTopRight span {
color: var(--topbar-desktop-link-text-color); color: var(--boardsbar-desktop-link-text-color);
text-decoration: var(--topbar-desktop-link-text-decoration); text-decoration: var(--boardsbar-desktop-link-text-decoration);
} }
.boardNavDesktop a:hover, .navTopRight span:hover { .boardNavDesktop a:hover, .navTopRight span:hover {
color: var(--topbar-desktop-link-text-color-hover); color: var(--boardsbar-desktop-link-text-color-hover);
text-decoration: var(--topbar-desktop-link-text-decoration-hover); text-decoration: var(--boardsbar-desktop-link-text-decoration-hover);
cursor: pointer; cursor: pointer;
} }
.placeholder { .placeholder {
color: var(--topbar-separator-color); color: var(--boardsbar-separator-color);
} }
.navTopRight { .navTopRight {
@@ -42,14 +42,14 @@
} }
.temporaryButton { .temporaryButton {
color: var(--topbar-desktop-link-text-color); color: var(--boardsbar-desktop-link-text-color);
text-decoration: var(--topbar-desktop-link-text-decoration); text-decoration: var(--boardsbar-desktop-link-text-decoration);
text-transform: capitalize; text-transform: capitalize;
} }
.temporaryButton:hover { .temporaryButton:hover {
color: var(--topbar-desktop-link-text-color-hover); color: var(--boardsbar-desktop-link-text-color-hover);
text-decoration: var(--topbar-desktop-link-text-decoration-hover); text-decoration: var(--boardsbar-desktop-link-text-decoration-hover);
cursor: pointer; cursor: pointer;
} }
@@ -63,9 +63,9 @@
.boardNavMobile { .boardNavMobile {
padding: 2px 4px; padding: 2px 4px;
background-color: var(--topbar-mobile-background-color); background-color: var(--boardsbar-mobile-background-color);
border-bottom: var(--topbar-mobile-border-bottom); border-bottom: var(--boardsbar-mobile-border-bottom);
font-size: var(--topbar-mobile-font-size); font-size: var(--boardsbar-mobile-font-size);
position: fixed; position: fixed;
width: 100%; width: 100%;
z-index: 3; z-index: 3;
@@ -77,7 +77,7 @@
} }
.boardNavMobile .pageJump a, .boardNavMobile .pageJump span { .boardNavMobile .pageJump a, .boardNavMobile .pageJump span {
color: var(--topbar-mobile-button-text-color); color: var(--boardsbar-mobile-button-text-color);
text-decoration: var(--button-text-decoration-mobile); text-decoration: var(--button-text-decoration-mobile);
} }
@@ -91,7 +91,7 @@
} }
.boardNavMobile select { .boardNavMobile select {
font-size: var(--topbar-mobile-font-size); font-size: var(--boardsbar-mobile-font-size);
} }
.boardSelect { .boardSelect {
@@ -120,4 +120,4 @@
.boardNavMobile { .boardNavMobile {
display: none; display: none;
} }
} }
@@ -9,11 +9,11 @@ import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories'
import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
import { getBoardPath, extractDirectoryFromTitle } from '../../lib/utils/route-utils'; import { getBoardPath, extractDirectoryFromTitle } from '../../lib/utils/route-utils';
import useCreateBoardModalStore from '../../stores/use-create-board-modal-store'; import useCreateBoardModalStore from '../../stores/use-create-board-modal-store';
import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store'; import useBoardsBarEditModalStore from '../../stores/use-boardsbar-edit-modal-store';
import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store'; import useBoardsBarVisibilityStore from '../../stores/use-boardsbar-visibility-store';
import useDirectoryModalStore from '../../stores/use-directory-modal-store'; import useDirectoryModalStore from '../../stores/use-directory-modal-store';
import { BOARD_CODE_GROUPS, getAllBoardCodes } from '../../constants/board-codes'; import { BOARD_CODE_GROUPS, getAllBoardCodes } from '../../constants/board-codes';
import styles from './topbar.module.css'; import styles from './boardsbar.module.css';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import lowerCase from 'lodash/lowerCase'; import lowerCase from 'lodash/lowerCase';
@@ -87,7 +87,7 @@ const findBoardAddressByCode = (code: string, directories: DirectoryCommunity[])
return entry?.address || null; return entry?.address || null;
}; };
const TopBarDesktop = () => { const BoardsBarDesktop = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
const params = useParams(); const params = useParams();
@@ -95,9 +95,9 @@ const TopBarDesktop = () => {
const [showSearchBar, setShowSearchBar] = useState(false); const [showSearchBar, setShowSearchBar] = useState(false);
const [showAllTemporarily, setShowAllTemporarily] = useState(false); const [showAllTemporarily, setShowAllTemporarily] = useState(false);
const { openCreateBoardModal } = useCreateBoardModalStore(); const { openCreateBoardModal } = useCreateBoardModalStore();
const { openTopbarEditModal } = useTopbarEditModalStore(); const { openBoardsBarEditModal } = useBoardsBarEditModalStore();
const { openDirectoryModal } = useDirectoryModalStore(); const { openDirectoryModal } = useDirectoryModalStore();
const { visibleDirectories, showSubscriptionsInTopbar } = useTopbarVisibilityStore(); const { visibleDirectories, showSubscriptionsInBoardsBar } = useBoardsBarVisibilityStore();
const directories = useDirectories(); const directories = useDirectories();
// Memoize allBoardCodes since it's derived from a constant // Memoize allBoardCodes since it's derived from a constant
@@ -129,7 +129,7 @@ const TopBarDesktop = () => {
); );
// Show all subscriptions when enabled; no separate per-address tracking (avoids drift when subscribing from board-buttons) // Show all subscriptions when enabled; no separate per-address tracking (avoids drift when subscribing from board-buttons)
const visibleSubscriptionAddresses = showSubscriptionsInTopbar ? subscriptions : []; const visibleSubscriptionAddresses = showSubscriptionsInBoardsBar ? subscriptions : [];
// Check if any directories are hidden // Check if any directories are hidden
const hasHiddenDirectories = useMemo(() => { const hasHiddenDirectories = useMemo(() => {
@@ -146,7 +146,7 @@ const TopBarDesktop = () => {
// Initialize visibility store on mount // Initialize visibility store on mount
useEffect(() => { useEffect(() => {
useTopbarVisibilityStore.getState().initialize(); useBoardsBarVisibilityStore.getState().initialize();
}, []); }, []);
// Render a board code link or placeholder // Render a board code link or placeholder
@@ -229,7 +229,7 @@ const TopBarDesktop = () => {
<>[{visibleSubscriptionAddresses.map((address: string, index: number) => renderSubscription(address, index, visibleSubscriptionAddresses.length))}] </> <>[{visibleSubscriptionAddresses.map((address: string, index: number) => renderSubscription(address, index, visibleSubscriptionAddresses.length))}] </>
)} )}
[ [
<span className={styles.temporaryButton} onClick={() => openTopbarEditModal()} style={{ cursor: 'pointer' }}> <span className={styles.temporaryButton} onClick={() => openBoardsBarEditModal()} style={{ cursor: 'pointer' }}>
{capitalize(t('edit'))} {capitalize(t('edit'))}
</span> </span>
] [ ] [
@@ -247,7 +247,7 @@ const TopBarDesktop = () => {
); );
}; };
const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => { const BoardsBarMobile = ({ subplebbitAddress }: { subplebbitAddress?: string }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const directories = useDirectories(); const directories = useDirectories();
@@ -302,7 +302,7 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
{directoryBoards.map((board, index) => { {directoryBoards.map((board, index) => {
const directoryCode = extractDirectoryFromTitle(board.title!); const directoryCode = extractDirectoryFromTitle(board.title!);
return ( return (
<option key={index} value={directoryCode!}> <option key={board.address} value={directoryCode!}>
{board.title} {board.title}
</option> </option>
); );
@@ -344,7 +344,7 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
); );
}; };
const TopBar = () => { const BoardsBar = () => {
const params = useParams(); const params = useParams();
const commentIndex = params?.accountCommentIndex ? parseInt(params.accountCommentIndex) : undefined; const commentIndex = params?.accountCommentIndex ? parseInt(params.accountCommentIndex) : undefined;
const accountComment = useAccountComment({ commentIndex }); const accountComment = useAccountComment({ commentIndex });
@@ -353,10 +353,10 @@ const TopBar = () => {
return ( return (
<> <>
<TopBarDesktop /> <BoardsBarDesktop />
<TopBarMobile subplebbitAddress={subplebbitAddress} /> <BoardsBarMobile subplebbitAddress={subplebbitAddress} />
</> </>
); );
}; };
export default TopBar; export default BoardsBar;
+1
View File
@@ -0,0 +1 @@
export { default } from './boardsbar';
@@ -0,0 +1,20 @@
import { useTranslation } from 'react-i18next';
import StyleSelector from '../style-selector/style-selector';
import styles from '../footer-first-row/footer-first-row.module.css';
/** Catalog footer first row: Style selector on right only (no pagination; catalog shows all pages at once). */
const CatalogFooterFirstRow = () => {
const { t } = useTranslation();
return (
<div className={styles.footerRow}>
<div className={styles.footerLeft} />
<div className={styles.footerRight}>
<span className={styles.styleLabel}>{t('style')}:</span>
<StyleSelector />
</div>
</div>
);
};
export default CatalogFooterFirstRow;
@@ -0,0 +1 @@
export { default } from './catalog-footer-first-row';
@@ -0,0 +1,27 @@
/* Shared footer first-row layout styles (catalog footer, board pagination footer mode) */
.footerRow {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
width: 100%;
}
.footerLeft {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.footerRight {
display: flex;
align-items: center;
gap: 6px;
}
.styleLabel {
font-size: 12px;
text-transform: capitalize;
}
@@ -0,0 +1,2 @@
export { default } from './page-footer-desktop';
export type { PageFooterDesktopProps } from './page-footer-desktop';
@@ -0,0 +1,38 @@
.footer {
margin-top: 16px;
padding-bottom: 24px;
}
.footer hr {
width: 90%;
margin: 16px auto;
border: none;
border-top: 1px solid var(--border-color, #ccc);
}
.firstRow {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
padding: 8px 0;
text-align: center;
}
.boardsBarRow {
padding: 8px 0;
}
.legalMeta {
padding: 12px 0 0;
font-size: 11px;
color: var(--body-font-color);
text-align: center;
}
@media (max-width: 640px) {
.footer {
display: none;
}
}
@@ -0,0 +1,25 @@
import BoardsBar from '../boardsbar';
import SiteLegalMeta from '../site-legal-meta';
import styles from './page-footer-desktop.module.css';
export interface PageFooterDesktopProps {
/** Mode-specific first row content (e.g. board pagination or thread controls) */
firstRow: React.ReactNode;
}
const PageFooterDesktop = ({ firstRow }: PageFooterDesktopProps) => {
return (
<footer className={styles.footer}>
<hr />
<div className={styles.firstRow}>{firstRow}</div>
<div className={styles.boardsBarRow}>
<BoardsBar />
</div>
<div className={styles.legalMeta}>
<SiteLegalMeta order='license-first' />
</div>
</footer>
);
};
export default PageFooterDesktop;
@@ -16,22 +16,10 @@ vi.mock('react-i18next', () => ({
}), }),
})); }));
vi.mock('../../../../hooks/use-theme', () => ({
default: () => ['yotsuba', vi.fn()],
}));
vi.mock('../../../../stores/use-expanded-media-store', () => ({ vi.mock('../../../../stores/use-expanded-media-store', () => ({
default: () => ({ fitExpandedImagesToScreen: false, setFitExpandedImagesToScreen: vi.fn() }), default: () => ({ fitExpandedImagesToScreen: false, setFitExpandedImagesToScreen: vi.fn() }),
})); }));
vi.mock('../../../../stores/use-special-theme-store', () => ({
default: () => ({ isEnabled: false, setIsEnabled: vi.fn() }),
}));
vi.mock('../../../../lib/utils/time-utils', () => ({
isChristmas: () => false,
}));
vi.mock('../../version', () => ({ vi.mock('../../version', () => ({
default: () => null, default: () => null,
})); }));
@@ -1,13 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import useTheme from '../../../hooks/use-theme';
import packageJson from '../../../../package.json'; import packageJson from '../../../../package.json';
import styles from './interface-settings.module.css'; import styles from './interface-settings.module.css';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import useExpandedMediaStore from '../../../stores/use-expanded-media-store'; import useExpandedMediaStore from '../../../stores/use-expanded-media-store';
import useFeedViewSettingsStore from '../../../stores/use-feed-view-settings-store'; import useFeedViewSettingsStore from '../../../stores/use-feed-view-settings-store';
import useSpecialThemeStore from '../../../stores/use-special-theme-store';
import { isChristmas } from '../../../lib/utils/time-utils';
import Version from '../../version'; import Version from '../../version';
const commitRef = process.env.VITE_COMMIT_REF; const commitRef = process.env.VITE_COMMIT_REF;
@@ -68,36 +65,6 @@ const CheckForUpdates = () => {
); );
}; };
const Style = () => {
const [theme, setTheme] = useTheme();
const { isEnabled, setIsEnabled } = useSpecialThemeStore();
const isChristmasTime = isChristmas();
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newTheme = e.target.value;
if (newTheme === 'special') {
setIsEnabled(true);
setTheme('tomorrow');
} else {
setIsEnabled(false);
setTheme(newTheme);
}
};
return (
<select className={styles.themeSettings} value={isEnabled ? 'special' : theme} onChange={handleThemeChange}>
<option value='yotsuba'>Yotsuba</option>
<option value='yotsuba-b'>Yotsuba B</option>
<option value='futaba'>Futaba</option>
<option value='burichan'>Burichan</option>
<option value='tomorrow'>Tomorrow</option>
<option value='photon'>Photon</option>
{isChristmasTime && <option value='special'>Special</option>}
</select>
);
};
// prettier-ignore // prettier-ignore
const availableLanguages = ['ar', 'bn', 'cs', 'da', 'de', 'el', 'en', 'es', 'fa', 'fi', 'fil', 'fr', 'he', 'hi', 'hu', 'id', 'it', 'ja', 'ko', 'mr', 'nl', 'no', 'pl', 'pt', 'ro', 'ru', 'sq', 'sv', 'te', 'th', 'tr', 'uk', 'ur', 'vi', 'zh']; const availableLanguages = ['ar', 'bn', 'cs', 'da', 'de', 'el', 'en', 'es', 'fa', 'fi', 'fil', 'fr', 'he', 'hi', 'hu', 'id', 'it', 'ja', 'ko', 'mr', 'nl', 'no', 'pl', 'pt', 'ro', 'ru', 'sq', 'sv', 'te', 'th', 'tr', 'uk', 'ur', 'vi', 'zh'];
@@ -135,9 +102,6 @@ const InterfaceSettings = () => {
<div className={styles.setting}> <div className={styles.setting}>
{capitalize(t('update'))}: <CheckForUpdates /> {capitalize(t('update'))}: <CheckForUpdates />
</div> </div>
<div className={styles.setting}>
{capitalize(t('style'))}: <Style />
</div>
<div className={styles.setting}> <div className={styles.setting}>
{capitalize(t('interface_language'))}: <InterfaceLanguage /> {capitalize(t('interface_language'))}: <InterfaceLanguage />
</div> </div>
+2
View File
@@ -0,0 +1,2 @@
export { default } from './site-legal-meta';
export type { SiteLegalMetaOrder, SiteLegalMetaProps } from './site-legal-meta';
@@ -0,0 +1,41 @@
import Version from '../version';
export type SiteLegalMetaOrder = 'version-first' | 'license-first';
export type SiteLegalMetaProps = {
/** Order of blocks: version-first (homepage) or license-first (board/post footer) */
order?: SiteLegalMetaOrder;
};
const LicenseText = () => <span>5chan is free and open source software under GPLv2 license.</span>;
const VersionFeedbackContact = () => (
<>
<Version /> {' '}
<a href='https://github.com/bitsocialhq/5chan/issues/new' target='_blank' rel='noopener noreferrer'>
Feedback
</a>{' '}
{' '}
<a href='https://github.com/bitsocialhq/5chan/graphs/contributors' target='_blank' rel='noopener noreferrer'>
Contact
</a>
</>
);
const SiteLegalMeta = ({ order = 'version-first' }: SiteLegalMetaProps) => {
const first = order === 'version-first' ? <VersionFeedbackContact /> : <LicenseText />;
const second = order === 'version-first' ? <LicenseText /> : <VersionFeedbackContact />;
return (
<>
<br />
{first}
<br />
<br />
<br />
{second}
</>
);
};
export default SiteLegalMeta;
@@ -0,0 +1,3 @@
.select {
display: inline-block;
}
@@ -0,0 +1,36 @@
import useTheme from '../../hooks/use-theme';
import useSpecialThemeStore from '../../stores/use-special-theme-store';
import { isChristmas } from '../../lib/utils/time-utils';
import styles from './style-selector.module.css';
const StyleSelector = () => {
const [theme, setTheme] = useTheme();
const { isEnabled, setIsEnabled } = useSpecialThemeStore();
const isChristmasTime = isChristmas();
const handleThemeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newTheme = e.target.value;
if (newTheme === 'special') {
setIsEnabled(true);
setTheme('tomorrow');
} else {
setIsEnabled(false);
setTheme(newTheme);
}
};
return (
<select className={styles.select} value={isEnabled ? 'special' : theme} onChange={handleThemeChange}>
<option value='yotsuba'>Yotsuba</option>
<option value='yotsuba-b'>Yotsuba B</option>
<option value='futaba'>Futaba</option>
<option value='burichan'>Burichan</option>
<option value='tomorrow'>Tomorrow</option>
<option value='photon'>Photon</option>
{isChristmasTime && <option value='special'>Special</option>}
</select>
);
};
export default StyleSelector;
@@ -0,0 +1,2 @@
export { default } from './thread-footer-first-row';
export type { ThreadFooterFirstRowProps } from './thread-footer-first-row';
@@ -0,0 +1,31 @@
.row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
width: 100%;
text-transform: capitalize;
}
.left {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
.right {
display: flex;
align-items: center;
}
.row .button {
text-transform: capitalize;
}
@@ -0,0 +1,55 @@
import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
import { useDirectories } from '../../hooks/use-directories';
import { getBoardPath } from '../../lib/utils/route-utils';
import useReplyModalStore from '../../stores/use-reply-modal-store';
import { ReturnButton, CatalogButton, TopButton, UpdateButton, AutoButton, PostPageStats } from '../board-buttons/board-buttons';
import styles from './thread-footer-first-row.module.css';
export interface ThreadFooterFirstRowProps {
postCid: string;
threadNumber: number | undefined;
subplebbitAddress: string;
/** Thread closed - disable Post a Reply */
isThreadClosed?: boolean;
}
const ThreadFooterFirstRow = ({ postCid, threadNumber, subplebbitAddress, isThreadClosed = false }: ThreadFooterFirstRowProps) => {
const { t } = useTranslation();
const location = useLocation();
const params = useParams();
const directories = useDirectories();
const { openReplyModalEmpty } = useReplyModalStore();
const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const isInModView = isModView(location.pathname);
const handlePostReplyClick = () => {
if (isThreadClosed) return;
openReplyModalEmpty(postCid, threadNumber, subplebbitAddress);
};
return (
<div className={styles.row}>
<div className={styles.left}>
[<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] [
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] [
<TopButton />] [<UpdateButton />] [<AutoButton />]
</div>
<div className={styles.center}>
[
<button type='button' className='button' onClick={handlePostReplyClick} disabled={isThreadClosed} aria-label={t('post_a_reply')}>
{t('post_a_reply')}
</button>
]
</div>
<div className={styles.right}>
<PostPageStats />
</div>
</div>
);
};
export default ThreadFooterFirstRow;
@@ -1 +0,0 @@
export { default } from './topbar-edit-modal';
-1
View File
@@ -1 +0,0 @@
export { default } from './topbar';
+1 -1
View File
@@ -1,6 +1,6 @@
/** /**
* Mapping of 4chan board codes to 5chan directory names * Mapping of 4chan board codes to 5chan directory names
* Used for displaying board links in the topbar * Used for displaying board links in the boardsbar
*/ */
export const BOARD_CODE_TO_DIRECTORY: Record<string, string> = { export const BOARD_CODE_TO_DIRECTORY: Record<string, string> = {
a: 'Anime & Manga', a: 'Anime & Manga',
@@ -0,0 +1,25 @@
import { create } from 'zustand';
interface BoardsBarEditModalState {
showModal: boolean;
openBoardsBarEditModal: () => void;
closeBoardsBarEditModal: () => void;
}
const useBoardsBarEditModalStore = create<BoardsBarEditModalState>((set) => ({
showModal: false,
openBoardsBarEditModal: () => {
set({
showModal: true,
});
},
closeBoardsBarEditModal: () => {
set({
showModal: false,
});
},
}));
export default useBoardsBarEditModalStore;
@@ -1,29 +1,36 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { getAllBoardCodes } from '../constants/board-codes'; import { getAllBoardCodes } from '../constants/board-codes';
const LOCALSTORAGE_KEY_DIRECTORIES = '5chan-topbar-directories-visible'; const LOCALSTORAGE_KEY_DIRECTORIES = '5chan-boardsbar-directories-visible';
const LOCALSTORAGE_KEY_SUBSCRIPTIONS = '5chan-topbar-subscriptions-visible'; const LOCALSTORAGE_KEY_SUBSCRIPTIONS = '5chan-boardsbar-subscriptions-visible';
const LOCALSTORAGE_KEY_DIRECTORIES_OLD = '5chan-topbar-directories-visible';
const LOCALSTORAGE_KEY_SUBSCRIPTIONS_OLD = '5chan-topbar-subscriptions-visible';
interface TopbarVisibilityState { interface BoardsBarVisibilityState {
// Directory codes that are visible (all visible by default) // Directory codes that are visible (all visible by default)
visibleDirectories: Set<string>; visibleDirectories: Set<string>;
// If true, show all account subscriptions in topbar (default: false) // If true, show all account subscriptions in boardsbar (default: false)
showSubscriptionsInTopbar: boolean; showSubscriptionsInBoardsBar: boolean;
// Actions // Actions
toggleDirectory: (code: string) => void; toggleDirectory: (code: string) => void;
setDirectoryVisibility: (code: string, visible: boolean) => void; setDirectoryVisibility: (code: string, visible: boolean) => void;
setShowSubscriptionsInTopbar: (show: boolean) => void; setShowSubscriptionsInBoardsBar: (show: boolean) => void;
// Initialize from localStorage // Initialize from localStorage
initialize: () => void; initialize: () => void;
} }
const loadFromLocalStorage = (key: string, defaultValue: Set<string>): Set<string> => { const loadFromLocalStorage = (key: string, fallbackKey: string, defaultValue: Set<string>): Set<string> => {
try { try {
const stored = localStorage.getItem(key); const stored = localStorage.getItem(key);
if (stored) { if (stored) {
const array = JSON.parse(stored); const array = JSON.parse(stored);
return new Set(array); return new Set(array);
} }
const fallbackStored = localStorage.getItem(fallbackKey);
if (fallbackStored) {
const array = JSON.parse(fallbackStored);
return new Set(array);
}
} catch (e) { } catch (e) {
console.warn(`Failed to load ${key} from localStorage:`, e); console.warn(`Failed to load ${key} from localStorage:`, e);
} }
@@ -42,11 +49,17 @@ const saveToLocalStorage = (key: string, set: Set<string>) => {
const loadShowSubscriptionsFromStorage = (): boolean => { const loadShowSubscriptionsFromStorage = (): boolean => {
try { try {
const stored = localStorage.getItem(LOCALSTORAGE_KEY_SUBSCRIPTIONS); const stored = localStorage.getItem(LOCALSTORAGE_KEY_SUBSCRIPTIONS);
if (!stored) return false; if (stored !== null) {
const parsed = JSON.parse(stored); const parsed = JSON.parse(stored);
// Migrate from old format (array of addresses) if (Array.isArray(parsed)) return parsed.length > 0;
if (Array.isArray(parsed)) return parsed.length > 0; if (typeof parsed === 'boolean') return parsed;
if (typeof parsed === 'boolean') return parsed; }
const fallbackStored = localStorage.getItem(LOCALSTORAGE_KEY_SUBSCRIPTIONS_OLD);
if (fallbackStored !== null) {
const parsed = JSON.parse(fallbackStored);
if (Array.isArray(parsed)) return parsed.length > 0;
if (typeof parsed === 'boolean') return parsed;
}
} catch (e) { } catch (e) {
console.warn('Failed to load subscriptions visibility from localStorage:', e); console.warn('Failed to load subscriptions visibility from localStorage:', e);
} }
@@ -61,14 +74,14 @@ const saveShowSubscriptionsToStorage = (show: boolean) => {
} }
}; };
const useTopbarVisibilityStore = create<TopbarVisibilityState>((set, _get) => { const useBoardsBarVisibilityStore = create<BoardsBarVisibilityState>((set, _get) => {
// Initialize with all directories visible by default // Initialize with all directories visible by default
const allBoardCodes = getAllBoardCodes(); const allBoardCodes = getAllBoardCodes();
const defaultVisibleDirectories = new Set(allBoardCodes); const defaultVisibleDirectories = new Set(allBoardCodes);
return { return {
visibleDirectories: loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, defaultVisibleDirectories), visibleDirectories: loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, LOCALSTORAGE_KEY_DIRECTORIES_OLD, defaultVisibleDirectories),
showSubscriptionsInTopbar: loadShowSubscriptionsFromStorage(), showSubscriptionsInBoardsBar: loadShowSubscriptionsFromStorage(),
toggleDirectory: (code: string) => { toggleDirectory: (code: string) => {
set((state) => { set((state) => {
@@ -96,20 +109,20 @@ const useTopbarVisibilityStore = create<TopbarVisibilityState>((set, _get) => {
}); });
}, },
setShowSubscriptionsInTopbar: (show: boolean) => { setShowSubscriptionsInBoardsBar: (show: boolean) => {
saveShowSubscriptionsToStorage(show); saveShowSubscriptionsToStorage(show);
set({ showSubscriptionsInTopbar: show }); set({ showSubscriptionsInBoardsBar: show });
}, },
initialize: () => { initialize: () => {
const directories = loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, defaultVisibleDirectories); const directories = loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, LOCALSTORAGE_KEY_DIRECTORIES_OLD, defaultVisibleDirectories);
const showSubscriptions = loadShowSubscriptionsFromStorage(); const showSubscriptions = loadShowSubscriptionsFromStorage();
set({ set({
visibleDirectories: directories, visibleDirectories: directories,
showSubscriptionsInTopbar: showSubscriptions, showSubscriptionsInBoardsBar: showSubscriptions,
}); });
}, },
}; };
}); });
export default useTopbarVisibilityStore; export default useBoardsBarVisibilityStore;
+19
View File
@@ -14,6 +14,8 @@ interface ReplyModalState {
quoteInsertSelectedText: string | null; quoteInsertSelectedText: string | null;
closeModal: () => void; closeModal: () => void;
openReplyModal: (parentCid: string, parentNumber: number | undefined, postCid: string, threadNumber: number | undefined, subplebbitAddress: string) => void; openReplyModal: (parentCid: string, parentNumber: number | undefined, postCid: string, threadNumber: number | undefined, subplebbitAddress: string) => void;
/** Open reply modal with empty textarea, no prefilled quote. Use for "Post a Reply" footer button. */
openReplyModalEmpty: (postCid: string, threadNumber: number | undefined, subplebbitAddress: string) => void;
} }
const getQuotedSelection = () => { const getQuotedSelection = () => {
@@ -87,6 +89,23 @@ const useReplyModalStore = create<ReplyModalState>((set, get) => ({
scrollY, scrollY,
}); });
}, },
openReplyModalEmpty: (postCid, threadNumber, subplebbitAddress) => {
useSelectedTextStore.getState().resetSelectedText();
const isMobile = window.innerWidth <= 768;
const scrollY = isMobile ? window.scrollY : 0;
set({
activeCid: postCid,
parentNumber: null,
threadNumber: threadNumber ?? null,
threadCid: postCid,
showReplyModal: true,
subplebbitAddress,
scrollY,
quoteInsertNumber: null,
quoteInsertSelectedText: null,
});
},
})); }));
export default useReplyModalStore; export default useReplyModalStore;
-25
View File
@@ -1,25 +0,0 @@
import { create } from 'zustand';
interface TopbarEditModalState {
showModal: boolean;
openTopbarEditModal: () => void;
closeTopbarEditModal: () => void;
}
const useTopbarEditModalStore = create<TopbarEditModalState>((set) => ({
showModal: false,
openTopbarEditModal: () => {
set({
showModal: true,
});
},
closeTopbarEditModal: () => {
set({
showModal: false,
});
},
}));
export default useTopbarEditModalStore;
+68 -68
View File
@@ -209,17 +209,17 @@
/* tooltip */ /* tooltip */
--tooltip-background-color: #181f24; --tooltip-background-color: #181f24;
/* topbar */ /* boardsbar */
--topbar-font-size: 9pt; --boardsbar-font-size: 9pt;
--topbar-separator-color: #b86; --boardsbar-separator-color: #b86;
--topbar-desktop-link-text-color: #800; --boardsbar-desktop-link-text-color: #800;
--topbar-desktop-link-text-color-hover: #e00; --boardsbar-desktop-link-text-color-hover: #e00;
--topbar-desktop-link-text-decoration: none; --boardsbar-desktop-link-text-decoration: none;
--topbar-desktop-link-text-decoration-hover: none; --boardsbar-desktop-link-text-decoration-hover: none;
--topbar-mobile-background-color: #f0e0d6; --boardsbar-mobile-background-color: #f0e0d6;
--topbar-mobile-border-bottom: 2px solid #d9c5b7; --boardsbar-mobile-border-bottom: 2px solid #d9c5b7;
--topbar-mobile-font-size: 10px; --boardsbar-mobile-font-size: 10px;
--topbar-mobile-button-text-color: navy; --boardsbar-mobile-button-text-color: navy;
/* mod queue */ /* mod queue */
--mod-queue-table-border: 1px solid #d9bfb7; --mod-queue-table-border: 1px solid #d9bfb7;
@@ -446,17 +446,17 @@
/* tooltip */ /* tooltip */
--tooltip-background-color: #181f24; --tooltip-background-color: #181f24;
/* topbar */ /* boardsbar */
--topbar-font-size: 9pt; --boardsbar-font-size: 9pt;
--topbar-separator-color: #89a; --boardsbar-separator-color: #89a;
--topbar-desktop-link-text-color: #34345c; --boardsbar-desktop-link-text-color: #34345c;
--topbar-desktop-link-text-color-hover: #d00; --boardsbar-desktop-link-text-color-hover: #d00;
--topbar-desktop-link-text-decoration: none; --boardsbar-desktop-link-text-decoration: none;
--topbar-desktop-link-text-decoration-hover: none; --boardsbar-desktop-link-text-decoration-hover: none;
--topbar-mobile-background-color: #d6daf0; --boardsbar-mobile-background-color: #d6daf0;
--topbar-mobile-border-bottom: 2px solid #b7c5d9; --boardsbar-mobile-border-bottom: 2px solid #b7c5d9;
--topbar-mobile-font-size: 10px; --boardsbar-mobile-font-size: 10px;
--topbar-mobile-button-text-color: #34345c; --boardsbar-mobile-button-text-color: #34345c;
/* mod queue */ /* mod queue */
--mod-queue-table-border: 1px solid #b7c5d9; --mod-queue-table-border: 1px solid #b7c5d9;
@@ -654,17 +654,17 @@
/* tooltip */ /* tooltip */
--tooltip-background-color: #181f24; --tooltip-background-color: #181f24;
/* topbar */ /* boardsbar */
--topbar-font-size: 11pt; --boardsbar-font-size: 11pt;
--topbar-separator-color: none; --boardsbar-separator-color: none;
--topbar-desktop-link-text-color: #00e; --boardsbar-desktop-link-text-color: #00e;
--topbar-desktop-link-text-color-hover: red; --boardsbar-desktop-link-text-color-hover: red;
--topbar-desktop-link-text-decoration: underline; --boardsbar-desktop-link-text-decoration: underline;
--topbar-desktop-link-text-decoration-hover: underline; --boardsbar-desktop-link-text-decoration-hover: underline;
--topbar-mobile-background-color: #f0e0d6; --boardsbar-mobile-background-color: #f0e0d6;
--topbar-mobile-border-bottom: 2px solid #d9c5b7; --boardsbar-mobile-border-bottom: 2px solid #d9c5b7;
--topbar-mobile-font-size: 9pt; --boardsbar-mobile-font-size: 9pt;
--topbar-mobile-button-text-color: #00f; --boardsbar-mobile-button-text-color: #00f;
/* mod queue */ /* mod queue */
--mod-queue-table-border: 1px solid rgba(0, 0, 0, 0.20); --mod-queue-table-border: 1px solid rgba(0, 0, 0, 0.20);
@@ -866,17 +866,17 @@
/* tooltip */ /* tooltip */
--tooltip-background-color: #181f24; --tooltip-background-color: #181f24;
/* topbar */ /* boardsbar */
--topbar-font-size: 11pt; --boardsbar-font-size: 11pt;
--topbar-separator-color: none; --boardsbar-separator-color: none;
--topbar-desktop-link-text-color: #34345c; --boardsbar-desktop-link-text-color: #34345c;
--topbar-desktop-link-text-color-hover: #d00; --boardsbar-desktop-link-text-color-hover: #d00;
--topbar-desktop-link-text-decoration: underline; --boardsbar-desktop-link-text-decoration: underline;
--topbar-desktop-link-text-decoration-hover: underline; --boardsbar-desktop-link-text-decoration-hover: underline;
--topbar-mobile-background-color: #d6daf0; --boardsbar-mobile-background-color: #d6daf0;
--topbar-mobile-border-bottom: 2px solid #b7c5d9; --boardsbar-mobile-border-bottom: 2px solid #b7c5d9;
--topbar-mobile-font-size: 9pt; --boardsbar-mobile-font-size: 9pt;
--topbar-mobile-button-text-color: #34345c; --boardsbar-mobile-button-text-color: #34345c;
/* mod queue */ /* mod queue */
--mod-queue-table-border: 1px solid rgba(0, 0, 0, 0.20); --mod-queue-table-border: 1px solid rgba(0, 0, 0, 0.20);
@@ -1112,18 +1112,18 @@
/* tooltip */ /* tooltip */
--tooltip-background-color: #000; --tooltip-background-color: #000;
/* topbar */ /* boardsbar */
--topbar-font-size: 9pt; --boardsbar-font-size: 9pt;
--topbar-separator-color: #c5c8c6; --boardsbar-separator-color: #c5c8c6;
--topbar-desktop-link-text-color: #81a2be; --boardsbar-desktop-link-text-color: #81a2be;
--topbar-desktop-link-text-color-hover: #5f89ac; --boardsbar-desktop-link-text-color-hover: #5f89ac;
--topbar-desktop-link-text-decoration: none; --boardsbar-desktop-link-text-decoration: none;
--topbar-desktop-link-text-decoration-hover: none; --boardsbar-desktop-link-text-decoration-hover: none;
--topbar-mobile-background-color: #1d1f21; --boardsbar-mobile-background-color: #1d1f21;
--topbar-mobile-border-bottom: 2px solid #282a2e; --boardsbar-mobile-border-bottom: 2px solid #282a2e;
--topbar-mobile-font-size: 10px; --boardsbar-mobile-font-size: 10px;
--topbar-mobile-button-text-color: #81a2be; --boardsbar-mobile-button-text-color: #81a2be;
--topbar-mobile-button-color: #81a2be; --boardsbar-mobile-button-color: #81a2be;
/* mod queue */ /* mod queue */
--mod-queue-table-border: 1px solid #111; --mod-queue-table-border: 1px solid #111;
@@ -1341,18 +1341,18 @@
/* tooltip */ /* tooltip */
--tooltip-background-color: #181f24; --tooltip-background-color: #181f24;
/* topbar */ /* boardsbar */
--topbar-font-size: 9pt; --boardsbar-font-size: 9pt;
--topbar-separator-color: #333; --boardsbar-separator-color: #333;
--topbar-desktop-link-text-color: #f60; --boardsbar-desktop-link-text-color: #f60;
--topbar-desktop-link-text-color-hover: #f30; --boardsbar-desktop-link-text-color-hover: #f30;
--topbar-desktop-link-text-decoration: none; --boardsbar-desktop-link-text-decoration: none;
--topbar-desktop-link-text-decoration-hover: none; --boardsbar-desktop-link-text-decoration-hover: none;
--topbar-mobile-background-color: #ddd; --boardsbar-mobile-background-color: #ddd;
--topbar-mobile-border-bottom: 2px solid #ccc; --boardsbar-mobile-border-bottom: 2px solid #ccc;
--topbar-mobile-font-size: 10px; --boardsbar-mobile-font-size: 10px;
--topbar-mobile-button-text-color: #f60; --boardsbar-mobile-button-text-color: #f60;
--topbar-mobile-button-color: #f60; --boardsbar-mobile-button-color: #f60;
/* mod queue */ /* mod queue */
--mod-queue-table-border: 1px solid #ccc; --mod-queue-table-border: 1px solid #ccc;
+23 -16
View File
@@ -19,6 +19,7 @@ import { getPageFromFeedPath, getSubplebbitAddress, isDirectoryBoard, normalizeM
import ErrorDisplay from '../../components/error-display/error-display'; import ErrorDisplay from '../../components/error-display/error-display';
import LoadingEllipsis from '../../components/loading-ellipsis'; import LoadingEllipsis from '../../components/loading-ellipsis';
import BoardPagination from '../../components/board-pagination'; import BoardPagination from '../../components/board-pagination';
import PageFooterDesktop from '../../components/page-footer-desktop';
import { Post } from '../post'; import { Post } from '../post';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
@@ -270,20 +271,23 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
const footerComponents = useMemo( const footerComponents = useMemo(
() => ({ () => ({
Footer: () => ( Footer: () => (
<BoardFooter <>
subplebbitAddresses={subplebbitAddresses} <BoardFooter
hasMore={hasMore} subplebbitAddresses={subplebbitAddresses}
combinedFeedLength={combinedFeed.length} hasMore={hasMore}
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts} combinedFeedLength={combinedFeed.length}
onNewerPostsClick={handleNewerPostsButtonClick} subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
isInAllView={isInAllView} onNewerPostsClick={handleNewerPostsButtonClick}
isInSubscriptionsView={isInSubscriptionsView} isInAllView={isInAllView}
isInModView={isInModView} isInSubscriptionsView={isInSubscriptionsView}
subplebbitState={subplebbitState} isInModView={isInModView}
subscriptionsLength={subscriptions?.length || 0} subplebbitState={subplebbitState}
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0} subscriptionsLength={subscriptions?.length || 0}
showLoadingEllipsis={effectiveInfiniteScroll || combinedFeed.length === 0} accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
/> showLoadingEllipsis={effectiveInfiniteScroll || combinedFeed.length === 0}
/>
<PageFooterDesktop firstRow={<BoardPagination basePath={paginationBasePath} currentPage={currentPage} totalPages={totalPages} footerStyle />} />
</>
), ),
}), }),
[ [
@@ -299,7 +303,9 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
subscriptions?.length, subscriptions?.length,
accountSubplebbitAddresses?.length, accountSubplebbitAddresses?.length,
effectiveInfiniteScroll, effectiveInfiniteScroll,
combinedFeed.length, paginationBasePath,
currentPage,
totalPages,
], ],
); );
@@ -399,6 +405,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0} accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
showLoadingEllipsis={true} showLoadingEllipsis={true}
/> />
<PageFooterDesktop firstRow={<BoardPagination basePath={paginationBasePath} currentPage={currentPage} totalPages={totalPages} footerStyle />} />
</> </>
) )
) : ( ) : (
@@ -407,7 +414,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
{currentPageFeed.map((post, index) => ( {currentPageFeed.map((post, index) => (
<Post key={post.cid} index={index} post={post} /> <Post key={post.cid} index={index} post={post} />
))} ))}
<BoardPagination basePath={paginationBasePath} currentPage={currentPage} totalPages={totalPages} />
<BoardFooter <BoardFooter
subplebbitAddresses={subplebbitAddresses} subplebbitAddresses={subplebbitAddresses}
hasMore={hasMore} hasMore={hasMore}
@@ -422,6 +428,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0} accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
showLoadingEllipsis={combinedFeed.length === 0} showLoadingEllipsis={combinedFeed.length === 0}
/> />
<PageFooterDesktop firstRow={<BoardPagination basePath={paginationBasePath} currentPage={currentPage} totalPages={totalPages} footerStyle />} />
</> </>
)} )}
</div> </div>
+51 -23
View File
@@ -17,8 +17,10 @@ import useSortingStore from '../../stores/use-sorting-store';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import { getSubplebbitAddress, isDirectoryBoard, normalizeMultiboardFeedPath } from '../../lib/utils/route-utils'; import { getSubplebbitAddress, isDirectoryBoard, normalizeMultiboardFeedPath } from '../../lib/utils/route-utils';
import CatalogRow from '../../components/catalog-row'; import CatalogRow from '../../components/catalog-row';
import CatalogFooterFirstRow from '../../components/catalog-footer-first-row';
import LoadingEllipsis from '../../components/loading-ellipsis'; import LoadingEllipsis from '../../components/loading-ellipsis';
import ErrorDisplay from '../../components/error-display/error-display'; import ErrorDisplay from '../../components/error-display/error-display';
import PageFooterDesktop from '../../components/page-footer-desktop';
import styles from './catalog.module.css'; import styles from './catalog.module.css';
import { commentMatchesPattern } from '../../lib/utils/pattern-utils'; import { commentMatchesPattern } from '../../lib/utils/pattern-utils';
@@ -391,17 +393,20 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
const footerComponents = useMemo( const footerComponents = useMemo(
() => ({ () => ({
Footer: () => ( Footer: () => (
<CatalogFooter <>
subplebbitAddresses={subplebbitAddresses} <CatalogFooter
hasMore={hasMore} subplebbitAddresses={subplebbitAddresses}
combinedFeedLength={cappedFeed.length} hasMore={hasMore}
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts} combinedFeedLength={cappedFeed.length}
onNewerPostsClick={handleNewerPostsButtonClick} subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
isInAllView={isInAllView} onNewerPostsClick={handleNewerPostsButtonClick}
isInSubscriptionsView={isInSubscriptionsView} isInAllView={isInAllView}
isInModView={isInModView} isInSubscriptionsView={isInSubscriptionsView}
showLoadingEllipsis={effectiveInfiniteScroll} isInModView={isInModView}
/> showLoadingEllipsis={effectiveInfiniteScroll}
/>
<PageFooterDesktop firstRow={<CatalogFooterFirstRow />} />
</>
), ),
}), }),
[ [
@@ -556,7 +561,16 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
) : ( ) : (
<> <>
{rows.map((row, index) => ( {rows.map((row, index) => (
<CatalogRow key={index} index={index} row={row} /> <CatalogRow
key={
row
.map((p) => p?.cid ?? (p?.timestamp != null ? `t${p.timestamp}` : ''))
.filter(Boolean)
.join('-') || 'row-no-ids'
}
index={index}
row={row}
/>
))} ))}
<CatalogFooter <CatalogFooter
subplebbitAddresses={subplebbitAddresses} subplebbitAddresses={subplebbitAddresses}
@@ -569,12 +583,22 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
isInModView={isInModView} isInModView={isInModView}
showLoadingEllipsis={true} showLoadingEllipsis={true}
/> />
<PageFooterDesktop firstRow={<CatalogFooterFirstRow />} />
</> </>
) )
) : ( ) : (
<> <>
{rows.map((row, index) => ( {rows.map((row, index) => (
<CatalogRow key={index} index={index} row={row} /> <CatalogRow
key={
row
.map((p) => p?.cid ?? (p?.timestamp != null ? `t${p.timestamp}` : ''))
.filter(Boolean)
.join('-') || 'row-no-ids'
}
index={index}
row={row}
/>
))} ))}
<CatalogFooter <CatalogFooter
subplebbitAddresses={subplebbitAddresses} subplebbitAddresses={subplebbitAddresses}
@@ -587,20 +611,24 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
isInModView={isInModView} isInModView={isInModView}
showLoadingEllipsis={false} showLoadingEllipsis={false}
/> />
<PageFooterDesktop firstRow={<CatalogFooterFirstRow />} />
</> </>
)} )}
</> </>
) : ( ) : (
<div className={styles.footer}> <>
<CatalogLoading <div className={styles.footer}>
subplebbitAddresses={subplebbitAddresses} <CatalogLoading
hasMore={hasMore} subplebbitAddresses={subplebbitAddresses}
combinedFeedLength={cappedFeed.length} hasMore={hasMore}
state={state} combinedFeedLength={cappedFeed.length}
subscriptionsLength={isInSubscriptionsView ? subscriptions?.length || 0 : 1} state={state}
error={error} subscriptionsLength={isInSubscriptionsView ? subscriptions?.length || 0 : 1}
/> error={error}
</div> />
</div>
<PageFooterDesktop firstRow={<CatalogFooterFirstRow />} />
</>
)} )}
</div> </div>
</div> </div>
+2 -14
View File
@@ -7,7 +7,7 @@ import { useDirectories, useDirectoryAddresses } from '../../hooks/use-directori
import { SubplebbitStatsCollector, useSubplebbitsStatsStore } from '../../hooks/use-subplebbits-stats'; import { SubplebbitStatsCollector, useSubplebbitsStatsStore } from '../../hooks/use-subplebbits-stats';
import PopularThreadsBox from './popular-threads-box'; import PopularThreadsBox from './popular-threads-box';
import BoardsList from './boards-list'; import BoardsList from './boards-list';
import Version from '../../components/version'; import SiteLegalMeta from '../../components/site-legal-meta';
import useDirectoryModalStore from '../../stores/use-directory-modal-store'; import useDirectoryModalStore from '../../stores/use-directory-modal-store';
import DisclaimerModal from '../../components/disclaimer-modal'; import DisclaimerModal from '../../components/disclaimer-modal';
import DirectoryModal from '../../components/directory-modal'; import DirectoryModal from '../../components/directory-modal';
@@ -163,19 +163,7 @@ export const Footer = () => {
</li> </li>
</ul> </ul>
<div className={styles.footerInfo}> <div className={styles.footerInfo}>
<br /> <SiteLegalMeta />
<Version /> {' '}
<a href='https://github.com/bitsocialhq/5chan/issues/new' target='_blank' rel='noopener noreferrer'>
Feedback
</a>{' '}
{' '}
<a href='https://github.com/bitsocialhq/5chan/graphs/contributors' target='_blank' rel='noopener noreferrer'>
Contact
</a>
<br />
<br />
<br />
<span>5chan is free and open source software under GPLv2 license.</span>
</div> </div>
</> </>
); );
+7
View File
@@ -9,8 +9,10 @@ import { useDirectories } from '../../hooks/use-directories';
import { isDirectoryBoard } from '../../lib/utils/route-utils'; import { isDirectoryBoard } from '../../lib/utils/route-utils';
import useIsMobile from '../../hooks/use-is-mobile'; import useIsMobile from '../../hooks/use-is-mobile';
import ErrorDisplay from '../../components/error-display/error-display'; import ErrorDisplay from '../../components/error-display/error-display';
import PageFooterDesktop from '../../components/page-footer-desktop';
import PostDesktop from '../../components/post-desktop'; import PostDesktop from '../../components/post-desktop';
import PostMobile from '../../components/post-mobile'; import PostMobile from '../../components/post-mobile';
import ThreadFooterFirstRow from '../../components/thread-footer-first-row';
import styles from './post.module.css'; import styles from './post.module.css';
export interface PostProps { export interface PostProps {
@@ -174,6 +176,11 @@ const PostPage = () => {
<ErrorDisplay error={comment?.error} /> <ErrorDisplay error={comment?.error} />
</div> </div>
)} )}
{post?.cid && subplebbitAddress ? (
<PageFooterDesktop
firstRow={<ThreadFooterFirstRow postCid={post.cid} threadNumber={post?.number} subplebbitAddress={subplebbitAddress} isThreadClosed={!!post?.locked} />}
/>
) : null}
</div> </div>
); );
}; };