mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
refactor(ui): rename topbar to boardsbar and add desktop footer for board, thread, and catalog
This commit is contained in:
+4
-4
@@ -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 />
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
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 (
|
||||||
|
|||||||
+3
-3
@@ -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;
|
||||||
+27
-27
@@ -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
-1
@@ -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';
|
||||||
+19
-19
@@ -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;
|
||||||
@@ -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;
|
||||||
-12
@@ -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>
|
||||||
|
|||||||
@@ -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 +0,0 @@
|
|||||||
export { default } from './topbar';
|
|
||||||
@@ -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;
|
||||||
+33
-20
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user