diff --git a/src/app.tsx b/src/app.tsx
index 92936f47..9002eaf7 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -35,8 +35,8 @@ import FeedCacheContainer from './components/feed-cache-container';
import ReplyModal from './components/reply-modal';
import PostForm from './components/post-form';
import BoardBlotter from './components/board-blotter';
-import TopBar from './components/topbar';
-import TopbarEditModal from './components/topbar-edit-modal';
+import BoardsBar from './components/boardsbar';
+import BoardsBarEditModal from './components/boardsbar-edit-modal';
import DirectoryModal from './components/directory-modal';
import DisclaimerModal from './components/disclaimer-modal';
import SettingsModal from './components/settings-modal';
@@ -96,9 +96,9 @@ const BoardLayout = () => {
return (
-
+
-
+
diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx
index 239712e0..fac1c350 100644
--- a/src/components/board-buttons/board-buttons.tsx
+++ b/src/components/board-buttons/board-buttons.tsx
@@ -32,7 +32,7 @@ interface BoardButtonsProps {
isTopbar?: boolean;
}
-const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
+export const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const { t } = useTranslation();
const params = useParams();
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 params = useParams();
const directories = useDirectories();
@@ -133,7 +133,7 @@ const RefreshButton = () => {
);
};
-const UpdateButton = () => {
+export const UpdateButton = () => {
const { t } = useTranslation();
const reset = useFeedResetStore((state) => state.reset);
return (
@@ -143,7 +143,7 @@ const UpdateButton = () => {
);
};
-const AutoButton = () => {
+export const AutoButton = () => {
const { t } = useTranslation();
const isMobile = useIsMobile();
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 (
+
+ );
+};
+
const SortOptions = () => {
const { t } = useTranslation();
const { sortType, setSortType } = useSortingStore();
@@ -442,7 +454,7 @@ export const MobileBoardButtons = () => {
);
};
-const PostPageStats = () => {
+export const PostPageStats = () => {
const { t } = useTranslation();
const params = useParams();
diff --git a/src/components/board-buttons/index.ts b/src/components/board-buttons/index.ts
index 067ee43b..7cbc008b 100644
--- a/src/components/board-buttons/index.ts
+++ b/src/components/board-buttons/index.ts
@@ -1 +1 @@
-export { MobileBoardButtons, DesktopBoardButtons } from './board-buttons';
+export { MobileBoardButtons, DesktopBoardButtons, PostPageStats, ReturnButton, CatalogButton, UpdateButton, AutoButton, TopButton } from './board-buttons';
diff --git a/src/components/board-pagination/board-pagination.module.css b/src/components/board-pagination/board-pagination.module.css
index 24b35289..c5ae676e 100644
--- a/src/components/board-pagination/board-pagination.module.css
+++ b/src/components/board-pagination/board-pagination.module.css
@@ -34,3 +34,21 @@
font-weight: bold;
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);
+}
diff --git a/src/components/board-pagination/board-pagination.tsx b/src/components/board-pagination/board-pagination.tsx
index 7c93a8ad..09ec0bfa 100644
--- a/src/components/board-pagination/board-pagination.tsx
+++ b/src/components/board-pagination/board-pagination.tsx
@@ -1,24 +1,75 @@
import { Link } from 'react-router-dom';
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';
export interface BoardPaginationProps {
basePath: string;
currentPage: 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 pageHref = (page: number) => (page === 1 ? basePath : `${basePath}/${page}`);
const prevHref = currentPage > 1 ? 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;
}
+ 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 (
+
+
+ {visiblePages.map((page, idx) => {
+ const isCurrent = page === currentPage;
+ const prevPage = visiblePages[idx - 1];
+ const showLeadingEllipsis = showEllipsis && prevPage !== undefined && page - prevPage > 1;
+ return (
+
+ {showLeadingEllipsis && ... }
+ {isCurrent ? (
+ [{page}]
+ ) : (
+
+ [{page}]
+
+ )}
+
+ );
+ })}
+ {nextHref && (
+ <>
+ {' '}
+
+ {t('next')}
+
+ >
+ )}{' '}
+
+ {t('catalog')}
+
+
+
+ {t('style')}:
+
+
+
+ );
+ }
+
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
return (
diff --git a/src/components/topbar-edit-modal/topbar-edit-modal.module.css b/src/components/boardsbar-edit-modal/boardsbar-edit-modal.module.css
similarity index 97%
rename from src/components/topbar-edit-modal/topbar-edit-modal.module.css
rename to src/components/boardsbar-edit-modal/boardsbar-edit-modal.module.css
index 078c1847..fc806fc7 100644
--- a/src/components/topbar-edit-modal/topbar-edit-modal.module.css
+++ b/src/components/boardsbar-edit-modal/boardsbar-edit-modal.module.css
@@ -8,7 +8,7 @@
z-index: 999;
}
-.topbarEditDialog {
+.boardsbarEditDialog {
position: absolute;
width: 400px;
max-height: 80vh;
@@ -31,7 +31,7 @@
}
@media (max-width: 600px) {
- .topbarEditDialog {
+ .boardsbarEditDialog {
width: calc(100% - 20px);
margin-left: -50%;
left: 50%;
@@ -154,7 +154,7 @@
text-decoration: var(--post-content-link-text-decoration-hover);
}
-.topbarEditFooter {
+.boardsbarEditFooter {
padding: 0;
margin: 0;
text-align: center;
diff --git a/src/components/topbar-edit-modal/topbar-edit-modal.tsx b/src/components/boardsbar-edit-modal/boardsbar-edit-modal.tsx
similarity index 71%
rename from src/components/topbar-edit-modal/topbar-edit-modal.tsx
rename to src/components/boardsbar-edit-modal/boardsbar-edit-modal.tsx
index 3b7d29b3..2dbcbdab 100644
--- a/src/components/topbar-edit-modal/topbar-edit-modal.tsx
+++ b/src/components/boardsbar-edit-modal/boardsbar-edit-modal.tsx
@@ -1,10 +1,10 @@
import { useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useAccount } from '@plebbit/plebbit-react-hooks';
-import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store';
-import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store';
+import useBoardsBarEditModalStore from '../../stores/use-boardsbar-edit-modal-store';
+import useBoardsBarVisibilityStore from '../../stores/use-boardsbar-visibility-store';
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 => Array.from(dirs).sort().join(' ');
@@ -18,20 +18,20 @@ const stringToDirectories = (str: string): Set => {
};
// Form component keyed by store values so it remounts with fresh state when modal reopens
-const TopbarEditModalForm = ({
+const BoardsBarEditModalForm = ({
visibleDirectories,
- showSubscriptionsInTopbar,
+ showSubscriptionsInBoardsBar,
setDirectoryVisibility,
- setShowSubscriptionsInTopbar,
- closeTopbarEditModal,
+ setShowSubscriptionsInBoardsBar,
+ closeBoardsBarEditModal,
subscriptions,
location,
}: {
visibleDirectories: Set;
- showSubscriptionsInTopbar: boolean;
+ showSubscriptionsInBoardsBar: boolean;
setDirectoryVisibility: (code: string, visible: boolean) => void;
- setShowSubscriptionsInTopbar: (show: boolean) => void;
- closeTopbarEditModal: () => void;
+ setShowSubscriptionsInBoardsBar: (show: boolean) => void;
+ closeBoardsBarEditModal: () => void;
subscriptions: string[];
location: { pathname: string };
}) => {
@@ -39,7 +39,7 @@ const TopbarEditModalForm = ({
const allVisible = allBoardCodes.every((code) => visibleDirectories.has(code));
const [localDirectoryInput, setLocalDirectoryInput] = useState(() => (allVisible ? '' : directoriesToString(visibleDirectories)));
- const [showSubscriptions, setShowSubscriptions] = useState(() => showSubscriptionsInTopbar);
+ const [showSubscriptions, setShowSubscriptions] = useState(() => showSubscriptionsInBoardsBar);
const handleSave = () => {
if (localDirectoryInput.trim() === '') {
@@ -48,8 +48,8 @@ const TopbarEditModalForm = ({
const inputDirectories = stringToDirectories(localDirectoryInput);
allBoardCodes.forEach((code) => setDirectoryVisibility(code, inputDirectories.has(code)));
}
- setShowSubscriptionsInTopbar(showSubscriptions);
- closeTopbarEditModal();
+ setShowSubscriptionsInBoardsBar(showSubscriptions);
+ closeBoardsBarEditModal();
};
return (
@@ -76,7 +76,7 @@ const TopbarEditModalForm = ({
className={styles.editSubscriptionsLink}
onClick={(e) => {
e.stopPropagation();
- closeTopbarEditModal();
+ closeBoardsBarEditModal();
}}
>
edit subscriptions
@@ -86,16 +86,16 @@ const TopbarEditModalForm = ({
)}
-
+
>
);
};
-const TopbarEditModal = () => {
- const { showModal, closeTopbarEditModal } = useTopbarEditModalStore();
- const { visibleDirectories, showSubscriptionsInTopbar, setDirectoryVisibility, setShowSubscriptionsInTopbar } = useTopbarVisibilityStore();
+const BoardsBarEditModal = () => {
+ const { showModal, closeBoardsBarEditModal } = useBoardsBarEditModalStore();
+ const { visibleDirectories, showSubscriptionsInBoardsBar, setDirectoryVisibility, setShowSubscriptionsInBoardsBar } = useBoardsBarVisibilityStore();
const account = useAccount();
const subscriptions = useMemo(() => account?.subscriptions || [], [account?.subscriptions]);
const location = useLocation();
@@ -106,27 +106,27 @@ const TopbarEditModal = () => {
const handleBackdropClick = (e: React.MouseEvent
) => {
if (e.target === e.currentTarget) {
- closeTopbarEditModal();
+ closeBoardsBarEditModal();
}
};
- const formKey = `${directoriesToString(visibleDirectories)}-${showSubscriptionsInTopbar}`;
+ const formKey = `${directoriesToString(visibleDirectories)}-${showSubscriptionsInBoardsBar}`;
return (
-
+
Custom Board List
-
+
-
@@ -136,4 +136,4 @@ const TopbarEditModal = () => {
);
};
-export default TopbarEditModal;
+export default BoardsBarEditModal;
diff --git a/src/components/topbar-edit-modal/topbar-edit-modal.types.ts b/src/components/boardsbar-edit-modal/boardsbar-edit-modal.types.ts
similarity index 81%
rename from src/components/topbar-edit-modal/topbar-edit-modal.types.ts
rename to src/components/boardsbar-edit-modal/boardsbar-edit-modal.types.ts
index f215e160..0682959d 100644
--- a/src/components/topbar-edit-modal/topbar-edit-modal.types.ts
+++ b/src/components/boardsbar-edit-modal/boardsbar-edit-modal.types.ts
@@ -1,4 +1,4 @@
-// Types for topbar edit modal component
+// Types for boardsbar edit modal component
export interface BoardCodeCheckbox {
code: string;
diff --git a/src/components/boardsbar-edit-modal/index.ts b/src/components/boardsbar-edit-modal/index.ts
new file mode 100644
index 00000000..ce2ac21f
--- /dev/null
+++ b/src/components/boardsbar-edit-modal/index.ts
@@ -0,0 +1 @@
+export { default } from './boardsbar-edit-modal';
diff --git a/src/components/topbar/topbar.module.css b/src/components/boardsbar/boardsbar.module.css
similarity index 61%
rename from src/components/topbar/topbar.module.css
rename to src/components/boardsbar/boardsbar.module.css
index 17d00e8a..dac588fb 100644
--- a/src/components/topbar/topbar.module.css
+++ b/src/components/boardsbar/boardsbar.module.css
@@ -1,29 +1,29 @@
.boardNavDesktop {
- font-size: var(--topbar-font-size);
- color: var(--topbar-separator-color);
+ font-size: var(--boardsbar-font-size);
+ color: var(--boardsbar-separator-color);
position: relative;
}
.checkingSubscriptions {
- font-size: var(--topbar-font-size);
- color: var(--topbar-desktop-link-text-color);
+ font-size: var(--boardsbar-font-size);
+ color: var(--boardsbar-desktop-link-text-color);
text-transform: lowercase;
}
.boardNavDesktop a, .navTopRight span {
- color: var(--topbar-desktop-link-text-color);
- text-decoration: var(--topbar-desktop-link-text-decoration);
+ color: var(--boardsbar-desktop-link-text-color);
+ text-decoration: var(--boardsbar-desktop-link-text-decoration);
}
.boardNavDesktop a:hover, .navTopRight span:hover {
- color: var(--topbar-desktop-link-text-color-hover);
- text-decoration: var(--topbar-desktop-link-text-decoration-hover);
+ color: var(--boardsbar-desktop-link-text-color-hover);
+ text-decoration: var(--boardsbar-desktop-link-text-decoration-hover);
cursor: pointer;
}
.placeholder {
- color: var(--topbar-separator-color);
+ color: var(--boardsbar-separator-color);
}
.navTopRight {
@@ -42,14 +42,14 @@
}
.temporaryButton {
- color: var(--topbar-desktop-link-text-color);
- text-decoration: var(--topbar-desktop-link-text-decoration);
+ color: var(--boardsbar-desktop-link-text-color);
+ text-decoration: var(--boardsbar-desktop-link-text-decoration);
text-transform: capitalize;
}
.temporaryButton:hover {
- color: var(--topbar-desktop-link-text-color-hover);
- text-decoration: var(--topbar-desktop-link-text-decoration-hover);
+ color: var(--boardsbar-desktop-link-text-color-hover);
+ text-decoration: var(--boardsbar-desktop-link-text-decoration-hover);
cursor: pointer;
}
@@ -63,9 +63,9 @@
.boardNavMobile {
padding: 2px 4px;
- background-color: var(--topbar-mobile-background-color);
- border-bottom: var(--topbar-mobile-border-bottom);
- font-size: var(--topbar-mobile-font-size);
+ background-color: var(--boardsbar-mobile-background-color);
+ border-bottom: var(--boardsbar-mobile-border-bottom);
+ font-size: var(--boardsbar-mobile-font-size);
position: fixed;
width: 100%;
z-index: 3;
@@ -77,7 +77,7 @@
}
.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);
}
@@ -91,7 +91,7 @@
}
.boardNavMobile select {
- font-size: var(--topbar-mobile-font-size);
+ font-size: var(--boardsbar-mobile-font-size);
}
.boardSelect {
@@ -120,4 +120,4 @@
.boardNavMobile {
display: none;
}
-}
\ No newline at end of file
+}
diff --git a/src/components/topbar/topbar.tsx b/src/components/boardsbar/boardsbar.tsx
similarity index 93%
rename from src/components/topbar/topbar.tsx
rename to src/components/boardsbar/boardsbar.tsx
index d73f9040..33c6610b 100644
--- a/src/components/topbar/topbar.tsx
+++ b/src/components/boardsbar/boardsbar.tsx
@@ -9,11 +9,11 @@ import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories'
import { useBoardPath, useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
import { getBoardPath, extractDirectoryFromTitle } from '../../lib/utils/route-utils';
import useCreateBoardModalStore from '../../stores/use-create-board-modal-store';
-import useTopbarEditModalStore from '../../stores/use-topbar-edit-modal-store';
-import useTopbarVisibilityStore from '../../stores/use-topbar-visibility-store';
+import useBoardsBarEditModalStore from '../../stores/use-boardsbar-edit-modal-store';
+import useBoardsBarVisibilityStore from '../../stores/use-boardsbar-visibility-store';
import useDirectoryModalStore from '../../stores/use-directory-modal-store';
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 debounce from 'lodash/debounce';
import lowerCase from 'lodash/lowerCase';
@@ -87,7 +87,7 @@ const findBoardAddressByCode = (code: string, directories: DirectoryCommunity[])
return entry?.address || null;
};
-const TopBarDesktop = () => {
+const BoardsBarDesktop = () => {
const { t } = useTranslation();
const location = useLocation();
const params = useParams();
@@ -95,9 +95,9 @@ const TopBarDesktop = () => {
const [showSearchBar, setShowSearchBar] = useState(false);
const [showAllTemporarily, setShowAllTemporarily] = useState(false);
const { openCreateBoardModal } = useCreateBoardModalStore();
- const { openTopbarEditModal } = useTopbarEditModalStore();
+ const { openBoardsBarEditModal } = useBoardsBarEditModalStore();
const { openDirectoryModal } = useDirectoryModalStore();
- const { visibleDirectories, showSubscriptionsInTopbar } = useTopbarVisibilityStore();
+ const { visibleDirectories, showSubscriptionsInBoardsBar } = useBoardsBarVisibilityStore();
const directories = useDirectories();
// 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)
- const visibleSubscriptionAddresses = showSubscriptionsInTopbar ? subscriptions : [];
+ const visibleSubscriptionAddresses = showSubscriptionsInBoardsBar ? subscriptions : [];
// Check if any directories are hidden
const hasHiddenDirectories = useMemo(() => {
@@ -146,7 +146,7 @@ const TopBarDesktop = () => {
// Initialize visibility store on mount
useEffect(() => {
- useTopbarVisibilityStore.getState().initialize();
+ useBoardsBarVisibilityStore.getState().initialize();
}, []);
// Render a board code link or placeholder
@@ -229,7 +229,7 @@ const TopBarDesktop = () => {
<>[{visibleSubscriptionAddresses.map((address: string, index: number) => renderSubscription(address, index, visibleSubscriptionAddresses.length))}] >
)}
[
-
openTopbarEditModal()} style={{ cursor: 'pointer' }}>
+ openBoardsBarEditModal()} style={{ cursor: 'pointer' }}>
{capitalize(t('edit'))}
] [
@@ -247,7 +247,7 @@ const TopBarDesktop = () => {
);
};
-const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
+const BoardsBarMobile = ({ subplebbitAddress }: { subplebbitAddress?: string }) => {
const { t } = useTranslation();
const navigate = useNavigate();
const directories = useDirectories();
@@ -302,7 +302,7 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
{directoryBoards.map((board, index) => {
const directoryCode = extractDirectoryFromTitle(board.title!);
return (
-
);
@@ -344,7 +344,7 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => {
);
};
-const TopBar = () => {
+const BoardsBar = () => {
const params = useParams();
const commentIndex = params?.accountCommentIndex ? parseInt(params.accountCommentIndex) : undefined;
const accountComment = useAccountComment({ commentIndex });
@@ -353,10 +353,10 @@ const TopBar = () => {
return (
<>
-
-
+
+
>
);
};
-export default TopBar;
+export default BoardsBar;
diff --git a/src/components/boardsbar/index.ts b/src/components/boardsbar/index.ts
new file mode 100644
index 00000000..7a4e1192
--- /dev/null
+++ b/src/components/boardsbar/index.ts
@@ -0,0 +1 @@
+export { default } from './boardsbar';
diff --git a/src/components/catalog-footer-first-row/catalog-footer-first-row.tsx b/src/components/catalog-footer-first-row/catalog-footer-first-row.tsx
new file mode 100644
index 00000000..9df6f278
--- /dev/null
+++ b/src/components/catalog-footer-first-row/catalog-footer-first-row.tsx
@@ -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 (
+
+
+
+ {t('style')}:
+
+
+
+ );
+};
+
+export default CatalogFooterFirstRow;
diff --git a/src/components/catalog-footer-first-row/index.ts b/src/components/catalog-footer-first-row/index.ts
new file mode 100644
index 00000000..46fb1a18
--- /dev/null
+++ b/src/components/catalog-footer-first-row/index.ts
@@ -0,0 +1 @@
+export { default } from './catalog-footer-first-row';
diff --git a/src/components/footer-first-row/footer-first-row.module.css b/src/components/footer-first-row/footer-first-row.module.css
new file mode 100644
index 00000000..7724873e
--- /dev/null
+++ b/src/components/footer-first-row/footer-first-row.module.css
@@ -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;
+}
diff --git a/src/components/page-footer-desktop/index.ts b/src/components/page-footer-desktop/index.ts
new file mode 100644
index 00000000..56b45e60
--- /dev/null
+++ b/src/components/page-footer-desktop/index.ts
@@ -0,0 +1,2 @@
+export { default } from './page-footer-desktop';
+export type { PageFooterDesktopProps } from './page-footer-desktop';
diff --git a/src/components/page-footer-desktop/page-footer-desktop.module.css b/src/components/page-footer-desktop/page-footer-desktop.module.css
new file mode 100644
index 00000000..158841cd
--- /dev/null
+++ b/src/components/page-footer-desktop/page-footer-desktop.module.css
@@ -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;
+ }
+}
diff --git a/src/components/page-footer-desktop/page-footer-desktop.tsx b/src/components/page-footer-desktop/page-footer-desktop.tsx
new file mode 100644
index 00000000..789af537
--- /dev/null
+++ b/src/components/page-footer-desktop/page-footer-desktop.tsx
@@ -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 (
+
+ );
+};
+
+export default PageFooterDesktop;
diff --git a/src/components/settings-modal/interface-settings/__tests__/interface-settings.test.tsx b/src/components/settings-modal/interface-settings/__tests__/interface-settings.test.tsx
index 0a114a84..66de998b 100644
--- a/src/components/settings-modal/interface-settings/__tests__/interface-settings.test.tsx
+++ b/src/components/settings-modal/interface-settings/__tests__/interface-settings.test.tsx
@@ -16,22 +16,10 @@ vi.mock('react-i18next', () => ({
}),
}));
-vi.mock('../../../../hooks/use-theme', () => ({
- default: () => ['yotsuba', vi.fn()],
-}));
-
vi.mock('../../../../stores/use-expanded-media-store', () => ({
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', () => ({
default: () => null,
}));
diff --git a/src/components/settings-modal/interface-settings/interface-settings.tsx b/src/components/settings-modal/interface-settings/interface-settings.tsx
index a372833b..87e3aa89 100644
--- a/src/components/settings-modal/interface-settings/interface-settings.tsx
+++ b/src/components/settings-modal/interface-settings/interface-settings.tsx
@@ -1,13 +1,10 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
-import useTheme from '../../../hooks/use-theme';
import packageJson from '../../../../package.json';
import styles from './interface-settings.module.css';
import capitalize from 'lodash/capitalize';
import useExpandedMediaStore from '../../../stores/use-expanded-media-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';
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) => {
- const newTheme = e.target.value;
-
- if (newTheme === 'special') {
- setIsEnabled(true);
- setTheme('tomorrow');
- } else {
- setIsEnabled(false);
- setTheme(newTheme);
- }
- };
-
- return (
-
- );
-};
-
// 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'];
@@ -135,9 +102,6 @@ const InterfaceSettings = () => {
{capitalize(t('update'))}:
-
- {capitalize(t('style'))}:
-
{capitalize(t('interface_language'))}:
diff --git a/src/components/site-legal-meta/index.ts b/src/components/site-legal-meta/index.ts
new file mode 100644
index 00000000..c764ad8c
--- /dev/null
+++ b/src/components/site-legal-meta/index.ts
@@ -0,0 +1,2 @@
+export { default } from './site-legal-meta';
+export type { SiteLegalMetaOrder, SiteLegalMetaProps } from './site-legal-meta';
diff --git a/src/components/site-legal-meta/site-legal-meta.tsx b/src/components/site-legal-meta/site-legal-meta.tsx
new file mode 100644
index 00000000..710e5b79
--- /dev/null
+++ b/src/components/site-legal-meta/site-legal-meta.tsx
@@ -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 = () => 5chan is free and open source software under GPLv2 license.;
+
+const VersionFeedbackContact = () => (
+ <>
+ •{' '}
+
+ Feedback
+ {' '}
+ •{' '}
+
+ Contact
+
+ >
+);
+
+const SiteLegalMeta = ({ order = 'version-first' }: SiteLegalMetaProps) => {
+ const first = order === 'version-first' ? : ;
+ const second = order === 'version-first' ? : ;
+
+ return (
+ <>
+
+ {first}
+
+
+
+ {second}
+ >
+ );
+};
+
+export default SiteLegalMeta;
diff --git a/src/components/style-selector/style-selector.module.css b/src/components/style-selector/style-selector.module.css
new file mode 100644
index 00000000..a5115757
--- /dev/null
+++ b/src/components/style-selector/style-selector.module.css
@@ -0,0 +1,3 @@
+.select {
+ display: inline-block;
+}
diff --git a/src/components/style-selector/style-selector.tsx b/src/components/style-selector/style-selector.tsx
new file mode 100644
index 00000000..2fd0b16c
--- /dev/null
+++ b/src/components/style-selector/style-selector.tsx
@@ -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) => {
+ const newTheme = e.target.value;
+
+ if (newTheme === 'special') {
+ setIsEnabled(true);
+ setTheme('tomorrow');
+ } else {
+ setIsEnabled(false);
+ setTheme(newTheme);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default StyleSelector;
diff --git a/src/components/thread-footer-first-row/index.ts b/src/components/thread-footer-first-row/index.ts
new file mode 100644
index 00000000..33f1aea1
--- /dev/null
+++ b/src/components/thread-footer-first-row/index.ts
@@ -0,0 +1,2 @@
+export { default } from './thread-footer-first-row';
+export type { ThreadFooterFirstRowProps } from './thread-footer-first-row';
diff --git a/src/components/thread-footer-first-row/thread-footer-first-row.module.css b/src/components/thread-footer-first-row/thread-footer-first-row.module.css
new file mode 100644
index 00000000..e0d5a376
--- /dev/null
+++ b/src/components/thread-footer-first-row/thread-footer-first-row.module.css
@@ -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;
+}
diff --git a/src/components/thread-footer-first-row/thread-footer-first-row.tsx b/src/components/thread-footer-first-row/thread-footer-first-row.tsx
new file mode 100644
index 00000000..97a9c616
--- /dev/null
+++ b/src/components/thread-footer-first-row/thread-footer-first-row.tsx
@@ -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 (
+
+
+
+ [
+
+ ]
+
+
+
+ );
+};
+
+export default ThreadFooterFirstRow;
diff --git a/src/components/topbar-edit-modal/index.ts b/src/components/topbar-edit-modal/index.ts
deleted file mode 100644
index 48862555..00000000
--- a/src/components/topbar-edit-modal/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './topbar-edit-modal';
diff --git a/src/components/topbar/index.ts b/src/components/topbar/index.ts
deleted file mode 100644
index d778b02a..00000000
--- a/src/components/topbar/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './topbar';
diff --git a/src/constants/board-codes.ts b/src/constants/board-codes.ts
index 90113fa2..c0cae910 100644
--- a/src/constants/board-codes.ts
+++ b/src/constants/board-codes.ts
@@ -1,6 +1,6 @@
/**
* 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 = {
a: 'Anime & Manga',
diff --git a/src/stores/use-boardsbar-edit-modal-store.ts b/src/stores/use-boardsbar-edit-modal-store.ts
new file mode 100644
index 00000000..5a610362
--- /dev/null
+++ b/src/stores/use-boardsbar-edit-modal-store.ts
@@ -0,0 +1,25 @@
+import { create } from 'zustand';
+
+interface BoardsBarEditModalState {
+ showModal: boolean;
+ openBoardsBarEditModal: () => void;
+ closeBoardsBarEditModal: () => void;
+}
+
+const useBoardsBarEditModalStore = create((set) => ({
+ showModal: false,
+
+ openBoardsBarEditModal: () => {
+ set({
+ showModal: true,
+ });
+ },
+
+ closeBoardsBarEditModal: () => {
+ set({
+ showModal: false,
+ });
+ },
+}));
+
+export default useBoardsBarEditModalStore;
diff --git a/src/stores/use-topbar-visibility-store.ts b/src/stores/use-boardsbar-visibility-store.ts
similarity index 59%
rename from src/stores/use-topbar-visibility-store.ts
rename to src/stores/use-boardsbar-visibility-store.ts
index 7d05b8fe..3fcded94 100644
--- a/src/stores/use-topbar-visibility-store.ts
+++ b/src/stores/use-boardsbar-visibility-store.ts
@@ -1,29 +1,36 @@
import { create } from 'zustand';
import { getAllBoardCodes } from '../constants/board-codes';
-const LOCALSTORAGE_KEY_DIRECTORIES = '5chan-topbar-directories-visible';
-const LOCALSTORAGE_KEY_SUBSCRIPTIONS = '5chan-topbar-subscriptions-visible';
+const LOCALSTORAGE_KEY_DIRECTORIES = '5chan-boardsbar-directories-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)
visibleDirectories: Set;
- // If true, show all account subscriptions in topbar (default: false)
- showSubscriptionsInTopbar: boolean;
+ // If true, show all account subscriptions in boardsbar (default: false)
+ showSubscriptionsInBoardsBar: boolean;
// Actions
toggleDirectory: (code: string) => void;
setDirectoryVisibility: (code: string, visible: boolean) => void;
- setShowSubscriptionsInTopbar: (show: boolean) => void;
+ setShowSubscriptionsInBoardsBar: (show: boolean) => void;
// Initialize from localStorage
initialize: () => void;
}
-const loadFromLocalStorage = (key: string, defaultValue: Set): Set => {
+const loadFromLocalStorage = (key: string, fallbackKey: string, defaultValue: Set): Set => {
try {
const stored = localStorage.getItem(key);
if (stored) {
const array = JSON.parse(stored);
return new Set(array);
}
+ const fallbackStored = localStorage.getItem(fallbackKey);
+ if (fallbackStored) {
+ const array = JSON.parse(fallbackStored);
+ return new Set(array);
+ }
} catch (e) {
console.warn(`Failed to load ${key} from localStorage:`, e);
}
@@ -42,11 +49,17 @@ const saveToLocalStorage = (key: string, set: Set) => {
const loadShowSubscriptionsFromStorage = (): boolean => {
try {
const stored = localStorage.getItem(LOCALSTORAGE_KEY_SUBSCRIPTIONS);
- if (!stored) return false;
- const parsed = JSON.parse(stored);
- // Migrate from old format (array of addresses)
- if (Array.isArray(parsed)) return parsed.length > 0;
- if (typeof parsed === 'boolean') return parsed;
+ if (stored !== null) {
+ const parsed = JSON.parse(stored);
+ if (Array.isArray(parsed)) return parsed.length > 0;
+ 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) {
console.warn('Failed to load subscriptions visibility from localStorage:', e);
}
@@ -61,14 +74,14 @@ const saveShowSubscriptionsToStorage = (show: boolean) => {
}
};
-const useTopbarVisibilityStore = create((set, _get) => {
+const useBoardsBarVisibilityStore = create((set, _get) => {
// Initialize with all directories visible by default
const allBoardCodes = getAllBoardCodes();
const defaultVisibleDirectories = new Set(allBoardCodes);
return {
- visibleDirectories: loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, defaultVisibleDirectories),
- showSubscriptionsInTopbar: loadShowSubscriptionsFromStorage(),
+ visibleDirectories: loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, LOCALSTORAGE_KEY_DIRECTORIES_OLD, defaultVisibleDirectories),
+ showSubscriptionsInBoardsBar: loadShowSubscriptionsFromStorage(),
toggleDirectory: (code: string) => {
set((state) => {
@@ -96,20 +109,20 @@ const useTopbarVisibilityStore = create((set, _get) => {
});
},
- setShowSubscriptionsInTopbar: (show: boolean) => {
+ setShowSubscriptionsInBoardsBar: (show: boolean) => {
saveShowSubscriptionsToStorage(show);
- set({ showSubscriptionsInTopbar: show });
+ set({ showSubscriptionsInBoardsBar: show });
},
initialize: () => {
- const directories = loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, defaultVisibleDirectories);
+ const directories = loadFromLocalStorage(LOCALSTORAGE_KEY_DIRECTORIES, LOCALSTORAGE_KEY_DIRECTORIES_OLD, defaultVisibleDirectories);
const showSubscriptions = loadShowSubscriptionsFromStorage();
set({
visibleDirectories: directories,
- showSubscriptionsInTopbar: showSubscriptions,
+ showSubscriptionsInBoardsBar: showSubscriptions,
});
},
};
});
-export default useTopbarVisibilityStore;
+export default useBoardsBarVisibilityStore;
diff --git a/src/stores/use-reply-modal-store.ts b/src/stores/use-reply-modal-store.ts
index 94afed88..1eb9b1b2 100644
--- a/src/stores/use-reply-modal-store.ts
+++ b/src/stores/use-reply-modal-store.ts
@@ -14,6 +14,8 @@ interface ReplyModalState {
quoteInsertSelectedText: string | null;
closeModal: () => 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 = () => {
@@ -87,6 +89,23 @@ const useReplyModalStore = create((set, get) => ({
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;
diff --git a/src/stores/use-topbar-edit-modal-store.ts b/src/stores/use-topbar-edit-modal-store.ts
deleted file mode 100644
index 3e2e6272..00000000
--- a/src/stores/use-topbar-edit-modal-store.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { create } from 'zustand';
-
-interface TopbarEditModalState {
- showModal: boolean;
- openTopbarEditModal: () => void;
- closeTopbarEditModal: () => void;
-}
-
-const useTopbarEditModalStore = create((set) => ({
- showModal: false,
-
- openTopbarEditModal: () => {
- set({
- showModal: true,
- });
- },
-
- closeTopbarEditModal: () => {
- set({
- showModal: false,
- });
- },
-}));
-
-export default useTopbarEditModalStore;
diff --git a/src/themes.css b/src/themes.css
index ef659eb5..78882aef 100644
--- a/src/themes.css
+++ b/src/themes.css
@@ -209,17 +209,17 @@
/* tooltip */
--tooltip-background-color: #181f24;
- /* topbar */
- --topbar-font-size: 9pt;
- --topbar-separator-color: #b86;
- --topbar-desktop-link-text-color: #800;
- --topbar-desktop-link-text-color-hover: #e00;
- --topbar-desktop-link-text-decoration: none;
- --topbar-desktop-link-text-decoration-hover: none;
- --topbar-mobile-background-color: #f0e0d6;
- --topbar-mobile-border-bottom: 2px solid #d9c5b7;
- --topbar-mobile-font-size: 10px;
- --topbar-mobile-button-text-color: navy;
+ /* boardsbar */
+ --boardsbar-font-size: 9pt;
+ --boardsbar-separator-color: #b86;
+ --boardsbar-desktop-link-text-color: #800;
+ --boardsbar-desktop-link-text-color-hover: #e00;
+ --boardsbar-desktop-link-text-decoration: none;
+ --boardsbar-desktop-link-text-decoration-hover: none;
+ --boardsbar-mobile-background-color: #f0e0d6;
+ --boardsbar-mobile-border-bottom: 2px solid #d9c5b7;
+ --boardsbar-mobile-font-size: 10px;
+ --boardsbar-mobile-button-text-color: navy;
/* mod queue */
--mod-queue-table-border: 1px solid #d9bfb7;
@@ -446,17 +446,17 @@
/* tooltip */
--tooltip-background-color: #181f24;
- /* topbar */
- --topbar-font-size: 9pt;
- --topbar-separator-color: #89a;
- --topbar-desktop-link-text-color: #34345c;
- --topbar-desktop-link-text-color-hover: #d00;
- --topbar-desktop-link-text-decoration: none;
- --topbar-desktop-link-text-decoration-hover: none;
- --topbar-mobile-background-color: #d6daf0;
- --topbar-mobile-border-bottom: 2px solid #b7c5d9;
- --topbar-mobile-font-size: 10px;
- --topbar-mobile-button-text-color: #34345c;
+ /* boardsbar */
+ --boardsbar-font-size: 9pt;
+ --boardsbar-separator-color: #89a;
+ --boardsbar-desktop-link-text-color: #34345c;
+ --boardsbar-desktop-link-text-color-hover: #d00;
+ --boardsbar-desktop-link-text-decoration: none;
+ --boardsbar-desktop-link-text-decoration-hover: none;
+ --boardsbar-mobile-background-color: #d6daf0;
+ --boardsbar-mobile-border-bottom: 2px solid #b7c5d9;
+ --boardsbar-mobile-font-size: 10px;
+ --boardsbar-mobile-button-text-color: #34345c;
/* mod queue */
--mod-queue-table-border: 1px solid #b7c5d9;
@@ -654,17 +654,17 @@
/* tooltip */
--tooltip-background-color: #181f24;
- /* topbar */
- --topbar-font-size: 11pt;
- --topbar-separator-color: none;
- --topbar-desktop-link-text-color: #00e;
- --topbar-desktop-link-text-color-hover: red;
- --topbar-desktop-link-text-decoration: underline;
- --topbar-desktop-link-text-decoration-hover: underline;
- --topbar-mobile-background-color: #f0e0d6;
- --topbar-mobile-border-bottom: 2px solid #d9c5b7;
- --topbar-mobile-font-size: 9pt;
- --topbar-mobile-button-text-color: #00f;
+ /* boardsbar */
+ --boardsbar-font-size: 11pt;
+ --boardsbar-separator-color: none;
+ --boardsbar-desktop-link-text-color: #00e;
+ --boardsbar-desktop-link-text-color-hover: red;
+ --boardsbar-desktop-link-text-decoration: underline;
+ --boardsbar-desktop-link-text-decoration-hover: underline;
+ --boardsbar-mobile-background-color: #f0e0d6;
+ --boardsbar-mobile-border-bottom: 2px solid #d9c5b7;
+ --boardsbar-mobile-font-size: 9pt;
+ --boardsbar-mobile-button-text-color: #00f;
/* mod queue */
--mod-queue-table-border: 1px solid rgba(0, 0, 0, 0.20);
@@ -866,17 +866,17 @@
/* tooltip */
--tooltip-background-color: #181f24;
- /* topbar */
- --topbar-font-size: 11pt;
- --topbar-separator-color: none;
- --topbar-desktop-link-text-color: #34345c;
- --topbar-desktop-link-text-color-hover: #d00;
- --topbar-desktop-link-text-decoration: underline;
- --topbar-desktop-link-text-decoration-hover: underline;
- --topbar-mobile-background-color: #d6daf0;
- --topbar-mobile-border-bottom: 2px solid #b7c5d9;
- --topbar-mobile-font-size: 9pt;
- --topbar-mobile-button-text-color: #34345c;
+ /* boardsbar */
+ --boardsbar-font-size: 11pt;
+ --boardsbar-separator-color: none;
+ --boardsbar-desktop-link-text-color: #34345c;
+ --boardsbar-desktop-link-text-color-hover: #d00;
+ --boardsbar-desktop-link-text-decoration: underline;
+ --boardsbar-desktop-link-text-decoration-hover: underline;
+ --boardsbar-mobile-background-color: #d6daf0;
+ --boardsbar-mobile-border-bottom: 2px solid #b7c5d9;
+ --boardsbar-mobile-font-size: 9pt;
+ --boardsbar-mobile-button-text-color: #34345c;
/* mod queue */
--mod-queue-table-border: 1px solid rgba(0, 0, 0, 0.20);
@@ -1112,18 +1112,18 @@
/* tooltip */
--tooltip-background-color: #000;
- /* topbar */
- --topbar-font-size: 9pt;
- --topbar-separator-color: #c5c8c6;
- --topbar-desktop-link-text-color: #81a2be;
- --topbar-desktop-link-text-color-hover: #5f89ac;
- --topbar-desktop-link-text-decoration: none;
- --topbar-desktop-link-text-decoration-hover: none;
- --topbar-mobile-background-color: #1d1f21;
- --topbar-mobile-border-bottom: 2px solid #282a2e;
- --topbar-mobile-font-size: 10px;
- --topbar-mobile-button-text-color: #81a2be;
- --topbar-mobile-button-color: #81a2be;
+ /* boardsbar */
+ --boardsbar-font-size: 9pt;
+ --boardsbar-separator-color: #c5c8c6;
+ --boardsbar-desktop-link-text-color: #81a2be;
+ --boardsbar-desktop-link-text-color-hover: #5f89ac;
+ --boardsbar-desktop-link-text-decoration: none;
+ --boardsbar-desktop-link-text-decoration-hover: none;
+ --boardsbar-mobile-background-color: #1d1f21;
+ --boardsbar-mobile-border-bottom: 2px solid #282a2e;
+ --boardsbar-mobile-font-size: 10px;
+ --boardsbar-mobile-button-text-color: #81a2be;
+ --boardsbar-mobile-button-color: #81a2be;
/* mod queue */
--mod-queue-table-border: 1px solid #111;
@@ -1341,18 +1341,18 @@
/* tooltip */
--tooltip-background-color: #181f24;
- /* topbar */
- --topbar-font-size: 9pt;
- --topbar-separator-color: #333;
- --topbar-desktop-link-text-color: #f60;
- --topbar-desktop-link-text-color-hover: #f30;
- --topbar-desktop-link-text-decoration: none;
- --topbar-desktop-link-text-decoration-hover: none;
- --topbar-mobile-background-color: #ddd;
- --topbar-mobile-border-bottom: 2px solid #ccc;
- --topbar-mobile-font-size: 10px;
- --topbar-mobile-button-text-color: #f60;
- --topbar-mobile-button-color: #f60;
+ /* boardsbar */
+ --boardsbar-font-size: 9pt;
+ --boardsbar-separator-color: #333;
+ --boardsbar-desktop-link-text-color: #f60;
+ --boardsbar-desktop-link-text-color-hover: #f30;
+ --boardsbar-desktop-link-text-decoration: none;
+ --boardsbar-desktop-link-text-decoration-hover: none;
+ --boardsbar-mobile-background-color: #ddd;
+ --boardsbar-mobile-border-bottom: 2px solid #ccc;
+ --boardsbar-mobile-font-size: 10px;
+ --boardsbar-mobile-button-text-color: #f60;
+ --boardsbar-mobile-button-color: #f60;
/* mod queue */
--mod-queue-table-border: 1px solid #ccc;
diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx
index 34ebb6d8..b406c59a 100644
--- a/src/views/board/board.tsx
+++ b/src/views/board/board.tsx
@@ -19,6 +19,7 @@ import { getPageFromFeedPath, getSubplebbitAddress, isDirectoryBoard, normalizeM
import ErrorDisplay from '../../components/error-display/error-display';
import LoadingEllipsis from '../../components/loading-ellipsis';
import BoardPagination from '../../components/board-pagination';
+import PageFooterDesktop from '../../components/page-footer-desktop';
import { Post } from '../post';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
@@ -270,20 +271,23 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
const footerComponents = useMemo(
() => ({
Footer: () => (
-
+ <>
+
+ } />
+ >
),
}),
[
@@ -299,7 +303,9 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
subscriptions?.length,
accountSubplebbitAddresses?.length,
effectiveInfiniteScroll,
- combinedFeed.length,
+ paginationBasePath,
+ currentPage,
+ totalPages,
],
);
@@ -399,6 +405,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
showLoadingEllipsis={true}
/>
+ } />
>
)
) : (
@@ -407,7 +414,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
{currentPageFeed.map((post, index) => (
))}
-
+ } />
>
)}
diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx
index cf6be8cb..a2db9fd4 100644
--- a/src/views/catalog/catalog.tsx
+++ b/src/views/catalog/catalog.tsx
@@ -17,8 +17,10 @@ import useSortingStore from '../../stores/use-sorting-store';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import { getSubplebbitAddress, isDirectoryBoard, normalizeMultiboardFeedPath } from '../../lib/utils/route-utils';
import CatalogRow from '../../components/catalog-row';
+import CatalogFooterFirstRow from '../../components/catalog-footer-first-row';
import LoadingEllipsis from '../../components/loading-ellipsis';
import ErrorDisplay from '../../components/error-display/error-display';
+import PageFooterDesktop from '../../components/page-footer-desktop';
import styles from './catalog.module.css';
import { commentMatchesPattern } from '../../lib/utils/pattern-utils';
@@ -391,17 +393,20 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
const footerComponents = useMemo(
() => ({
Footer: () => (
-
+ <>
+
+
} />
+ >
),
}),
[
@@ -556,7 +561,16 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
) : (
<>
{rows.map((row, index) => (
-
+
p?.cid ?? (p?.timestamp != null ? `t${p.timestamp}` : ''))
+ .filter(Boolean)
+ .join('-') || 'row-no-ids'
+ }
+ index={index}
+ row={row}
+ />
))}
+ } />
>
)
) : (
<>
{rows.map((row, index) => (
-
+ p?.cid ?? (p?.timestamp != null ? `t${p.timestamp}` : ''))
+ .filter(Boolean)
+ .join('-') || 'row-no-ids'
+ }
+ index={index}
+ row={row}
+ />
))}
+ } />
>
)}
>
) : (
-
-
-
+ <>
+
+
+
+ } />
+ >
)}
diff --git a/src/views/home/home.tsx b/src/views/home/home.tsx
index 0e5adc43..4337a63e 100644
--- a/src/views/home/home.tsx
+++ b/src/views/home/home.tsx
@@ -7,7 +7,7 @@ import { useDirectories, useDirectoryAddresses } from '../../hooks/use-directori
import { SubplebbitStatsCollector, useSubplebbitsStatsStore } from '../../hooks/use-subplebbits-stats';
import PopularThreadsBox from './popular-threads-box';
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 DisclaimerModal from '../../components/disclaimer-modal';
import DirectoryModal from '../../components/directory-modal';
@@ -163,19 +163,7 @@ export const Footer = () => {
-
-
•{' '}
-
- Feedback
- {' '}
- •{' '}
-
- Contact
-
-
-
-
-
5chan is free and open source software under GPLv2 license.
+
>
);
diff --git a/src/views/post/post.tsx b/src/views/post/post.tsx
index cebcea9b..462f4523 100644
--- a/src/views/post/post.tsx
+++ b/src/views/post/post.tsx
@@ -9,8 +9,10 @@ import { useDirectories } from '../../hooks/use-directories';
import { isDirectoryBoard } from '../../lib/utils/route-utils';
import useIsMobile from '../../hooks/use-is-mobile';
import ErrorDisplay from '../../components/error-display/error-display';
+import PageFooterDesktop from '../../components/page-footer-desktop';
import PostDesktop from '../../components/post-desktop';
import PostMobile from '../../components/post-mobile';
+import ThreadFooterFirstRow from '../../components/thread-footer-first-row';
import styles from './post.module.css';
export interface PostProps {
@@ -174,6 +176,11 @@ const PostPage = () => {
)}
+ {post?.cid && subplebbitAddress ? (
+ }
+ />
+ ) : null}
);
};