2026-02-23 20:50:30 +08:00
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
import { useLocation, useParams } from 'react-router-dom';
|
2026-04-20 22:11:36 +07:00
|
|
|
import { useComment } from '@bitsocial/bitsocial-react-hooks';
|
2026-03-02 19:17:59 +08:00
|
|
|
import BoardsBar from '../boards-bar';
|
2026-02-23 20:50:30 +08:00
|
|
|
import SiteLegalMeta from '../site-legal-meta';
|
|
|
|
|
import StyleSelector from '../style-selector/style-selector';
|
2026-05-19 15:38:44 +07:00
|
|
|
import {
|
|
|
|
|
CatalogSearchResultsLabel,
|
|
|
|
|
ReturnButton,
|
|
|
|
|
CatalogButton,
|
|
|
|
|
TopButton,
|
|
|
|
|
UpdateButton,
|
|
|
|
|
AutoButton,
|
|
|
|
|
PostPageStats,
|
|
|
|
|
RefreshButton,
|
|
|
|
|
} from '../board-buttons/board-buttons';
|
2026-02-23 20:50:30 +08:00
|
|
|
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
|
|
|
|
|
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
2026-03-20 16:48:57 +08:00
|
|
|
import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-store';
|
2026-03-03 15:07:14 +08:00
|
|
|
import useCountLinksInReplies from '../../hooks/use-count-links-in-replies';
|
|
|
|
|
import { usePostPageNumber } from '../../hooks/use-post-page-number';
|
|
|
|
|
import { useDirectoryByAddress } from '../../hooks/use-directories';
|
2026-05-16 13:44:44 +07:00
|
|
|
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
|
2026-03-03 15:07:14 +08:00
|
|
|
import capitalize from 'lodash/capitalize';
|
2026-02-23 20:50:30 +08:00
|
|
|
import styles from './footer.module.css';
|
|
|
|
|
|
|
|
|
|
/* -----------------------------------------------------------------------------
|
|
|
|
|
* PageFooterDesktop
|
|
|
|
|
* Main page footer wrapper: hr, first row slot, BoardsBar, SiteLegalMeta.
|
|
|
|
|
* -------------------------------------------------------------------------- */
|
|
|
|
|
|
2026-03-08 16:37:34 +08:00
|
|
|
interface PageFooterDesktopProps {
|
2026-02-23 20:50:30 +08:00
|
|
|
/** Mode-specific first row content (e.g. board pagination or thread controls) */
|
2026-05-19 15:38:44 +07:00
|
|
|
firstRow?: React.ReactNode;
|
2026-02-24 16:36:13 +08:00
|
|
|
/** Optional row between first row and BoardsBar (e.g. style selector on thread page) */
|
|
|
|
|
styleRow?: React.ReactNode;
|
2026-05-19 15:38:44 +07:00
|
|
|
/** Catalog pages omit the default footer top margin */
|
|
|
|
|
variant?: 'catalog';
|
2026-02-23 20:50:30 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-19 15:38:44 +07:00
|
|
|
export const PageFooterDesktop = ({ firstRow, styleRow, variant }: PageFooterDesktopProps) => (
|
|
|
|
|
<footer className={variant === 'catalog' ? `${styles.footer} ${styles.footerCatalog}` : styles.footer}>
|
2026-02-23 20:50:30 +08:00
|
|
|
<hr />
|
2026-05-19 15:38:44 +07:00
|
|
|
{firstRow != null ? <div className={styles.firstRow}>{firstRow}</div> : null}
|
2026-02-24 16:36:13 +08:00
|
|
|
{styleRow != null ? <div className={styles.styleRow}>{styleRow}</div> : null}
|
2026-02-23 20:50:30 +08:00
|
|
|
<div className={styles.boardsBarRow}>
|
|
|
|
|
<BoardsBar />
|
|
|
|
|
</div>
|
|
|
|
|
<div className={styles.legalMeta}>
|
|
|
|
|
<SiteLegalMeta order='license-first' />
|
|
|
|
|
</div>
|
|
|
|
|
</footer>
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-07 18:50:24 +08:00
|
|
|
/* -----------------------------------------------------------------------------
|
|
|
|
|
* StyleOnlyFooterFirstRow
|
|
|
|
|
* Footer first row with only the style selector (right-aligned). Used by mod queue.
|
|
|
|
|
* -------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
export const StyleOnlyFooterFirstRow = () => {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
return (
|
|
|
|
|
<div className={`${styles.footerRow} ${styles.footerRowRightOnly}`}>
|
|
|
|
|
<div className={styles.footerRight}>
|
|
|
|
|
<span className={styles.styleLabel}>{t('style')}:</span>
|
|
|
|
|
<StyleSelector />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-23 20:50:30 +08:00
|
|
|
/* -----------------------------------------------------------------------------
|
|
|
|
|
* CatalogFooterFirstRow
|
2026-05-19 15:38:44 +07:00
|
|
|
* Catalog footer nav row: Return, Catalog, Top, Refresh, plus search/filter label.
|
2026-02-23 20:50:30 +08:00
|
|
|
* -------------------------------------------------------------------------- */
|
|
|
|
|
|
2026-03-08 16:37:34 +08:00
|
|
|
interface CatalogFooterFirstRowProps {
|
2026-03-13 13:25:10 +08:00
|
|
|
communityAddress?: string;
|
2026-02-24 16:36:13 +08:00
|
|
|
isInAllView?: boolean;
|
|
|
|
|
isInSubscriptionsView?: boolean;
|
|
|
|
|
isInModView?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 13:25:10 +08:00
|
|
|
export const CatalogFooterFirstRow = ({ communityAddress, isInAllView = false, isInSubscriptionsView = false, isInModView = false }: CatalogFooterFirstRowProps) => {
|
2026-02-23 20:50:30 +08:00
|
|
|
return (
|
|
|
|
|
<div className={styles.footerRow}>
|
2026-02-24 16:36:13 +08:00
|
|
|
<div className={styles.footerLeft}>
|
|
|
|
|
<span>
|
2026-03-13 13:25:10 +08:00
|
|
|
[<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
|
2026-02-24 16:36:13 +08:00
|
|
|
</span>
|
|
|
|
|
<span>
|
2026-03-13 13:25:10 +08:00
|
|
|
[<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
|
2026-02-24 16:36:13 +08:00
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
[<TopButton />]
|
|
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
[<RefreshButton />]
|
|
|
|
|
</span>
|
2026-05-19 15:38:44 +07:00
|
|
|
<CatalogSearchResultsLabel />
|
2026-02-23 20:50:30 +08:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-19 15:38:44 +07:00
|
|
|
/* -----------------------------------------------------------------------------
|
|
|
|
|
* CatalogFooterStyleRow
|
|
|
|
|
* Catalog style selector on its own row (below nav, above BoardsBar).
|
|
|
|
|
* -------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
export const CatalogFooterStyleRow = () => {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
return (
|
|
|
|
|
<span className={styles.styleRowContent}>
|
|
|
|
|
<span className={styles.styleLabel}>{t('style')}:</span>
|
|
|
|
|
<StyleSelector />
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-24 16:36:13 +08:00
|
|
|
/* -----------------------------------------------------------------------------
|
|
|
|
|
* ThreadFooterStyleRow
|
|
|
|
|
* Style selector on its own row (thread page only, below first row, above BoardsBar).
|
|
|
|
|
* -------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
export const ThreadFooterStyleRow = () => {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
return (
|
|
|
|
|
<span className={styles.styleRowContent}>
|
|
|
|
|
<span className={styles.styleLabel}>{t('style')}:</span>
|
|
|
|
|
<StyleSelector />
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-23 20:50:30 +08:00
|
|
|
/* -----------------------------------------------------------------------------
|
|
|
|
|
* ThreadFooterFirstRow
|
|
|
|
|
* Thread page footer first row: Return, Catalog, Top, Update, Auto, Post a Reply, stats.
|
|
|
|
|
* -------------------------------------------------------------------------- */
|
|
|
|
|
|
2026-03-08 16:37:34 +08:00
|
|
|
interface ThreadFooterFirstRowProps {
|
2026-02-23 20:50:30 +08:00
|
|
|
postCid: string;
|
|
|
|
|
threadNumber: number | undefined;
|
2026-03-13 13:25:10 +08:00
|
|
|
communityAddress: string;
|
2026-02-23 20:50:30 +08:00
|
|
|
/** Thread closed - disable Post a Reply */
|
|
|
|
|
isThreadClosed?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 13:25:10 +08:00
|
|
|
export const ThreadFooterFirstRow = ({ postCid, threadNumber, communityAddress, isThreadClosed = false }: ThreadFooterFirstRowProps) => {
|
2026-02-23 20:50:30 +08:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const params = useParams();
|
|
|
|
|
const { openReplyModalEmpty } = useReplyModalStore();
|
|
|
|
|
|
|
|
|
|
const isInAllView = isAllView(location.pathname);
|
|
|
|
|
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
|
|
|
|
const isInModView = isModView(location.pathname);
|
|
|
|
|
|
|
|
|
|
const handlePostReplyClick = () => {
|
|
|
|
|
if (isThreadClosed) return;
|
2026-03-13 13:25:10 +08:00
|
|
|
openReplyModalEmpty(postCid, threadNumber, communityAddress);
|
2026-02-23 20:50:30 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={styles.threadRow}>
|
|
|
|
|
<div className={styles.threadLeft}>
|
2026-02-24 16:36:13 +08:00
|
|
|
<span>
|
2026-03-13 13:25:10 +08:00
|
|
|
[<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
|
2026-02-24 16:36:13 +08:00
|
|
|
</span>
|
|
|
|
|
<span>
|
2026-03-13 13:25:10 +08:00
|
|
|
[<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]
|
2026-02-24 16:36:13 +08:00
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
[<TopButton />]
|
|
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
[<UpdateButton />]
|
|
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
[<AutoButton />]
|
|
|
|
|
</span>
|
2026-02-23 20:50:30 +08:00
|
|
|
</div>
|
|
|
|
|
<div className={styles.threadCenter}>
|
2026-02-24 16:36:13 +08:00
|
|
|
<span>
|
|
|
|
|
[
|
|
|
|
|
<button type='button' className={styles.button} onClick={handlePostReplyClick} disabled={isThreadClosed} aria-label={t('post_a_reply')}>
|
|
|
|
|
{t('post_a_reply')}
|
|
|
|
|
</button>
|
|
|
|
|
]
|
|
|
|
|
</span>
|
2026-02-23 20:50:30 +08:00
|
|
|
</div>
|
|
|
|
|
<div className={styles.threadRight}>
|
|
|
|
|
<PostPageStats />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
2026-03-01 20:45:26 +08:00
|
|
|
|
|
|
|
|
/* -----------------------------------------------------------------------------
|
|
|
|
|
* PageFooterMobile
|
|
|
|
|
* Mobile-only page footer with content slot and footer links.
|
|
|
|
|
* -------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
export const PageFooterMobile = ({ children }: { children: React.ReactNode }) => (
|
|
|
|
|
<footer className={styles.mobileFooter}>
|
|
|
|
|
<hr />
|
|
|
|
|
{children}
|
|
|
|
|
<hr />
|
|
|
|
|
<div className={styles.mobileFooterLinks}>
|
|
|
|
|
<SiteLegalMeta order='license-first' />
|
|
|
|
|
</div>
|
|
|
|
|
</footer>
|
|
|
|
|
);
|
2026-03-03 15:07:14 +08:00
|
|
|
|
|
|
|
|
/* -----------------------------------------------------------------------------
|
|
|
|
|
* ThreadFooterMobile
|
|
|
|
|
* Mobile thread page footer: Post a Reply, Return/Catalog/Top, Update/Auto, stats.
|
|
|
|
|
* -------------------------------------------------------------------------- */
|
|
|
|
|
|
2026-03-08 16:37:34 +08:00
|
|
|
interface ThreadFooterMobileProps {
|
2026-03-03 15:07:14 +08:00
|
|
|
postCid: string;
|
|
|
|
|
threadNumber: number | undefined;
|
2026-03-13 13:25:10 +08:00
|
|
|
communityAddress: string;
|
2026-03-03 15:07:14 +08:00
|
|
|
isThreadClosed?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 13:25:10 +08:00
|
|
|
export const ThreadFooterMobile = ({ postCid, threadNumber, communityAddress, isThreadClosed = false }: ThreadFooterMobileProps) => {
|
2026-03-03 15:07:14 +08:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const params = useParams();
|
|
|
|
|
const { openReplyModalEmpty } = useReplyModalStore();
|
2026-03-20 16:48:57 +08:00
|
|
|
const autoUpdateEnabled = useThreadLiveUpdatesStore((state) => state.enabled);
|
2026-03-03 15:07:14 +08:00
|
|
|
|
|
|
|
|
const isInAllView = isAllView(location.pathname);
|
|
|
|
|
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
|
|
|
|
const isInModView = isModView(location.pathname);
|
2026-05-16 13:44:44 +07:00
|
|
|
const communityIdentifier = useCommunityIdentifier(communityAddress);
|
2026-03-03 15:07:14 +08:00
|
|
|
|
2026-05-16 13:44:44 +07:00
|
|
|
const post = useComment({ commentCid: postCid, autoUpdate: autoUpdateEnabled, community: communityIdentifier });
|
2026-03-03 15:07:14 +08:00
|
|
|
const { replyCount } = post || {};
|
|
|
|
|
const linkCount = useCountLinksInReplies(post);
|
2026-03-13 13:25:10 +08:00
|
|
|
const directoryEntry = useDirectoryByAddress(communityAddress);
|
2026-03-03 15:07:14 +08:00
|
|
|
const requirePostLinkIsMedia = directoryEntry?.features?.requirePostLinkIsMedia === true;
|
2026-04-17 10:48:27 +07:00
|
|
|
const pageNumber = usePostPageNumber({ communityAddress: communityAddress, postCid, enabled: true });
|
2026-03-03 15:07:14 +08:00
|
|
|
|
|
|
|
|
const handlePostReplyClick = () => {
|
|
|
|
|
if (isThreadClosed) return;
|
2026-03-13 13:25:10 +08:00
|
|
|
openReplyModalEmpty(postCid, threadNumber, communityAddress);
|
2026-03-03 15:07:14 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<PageFooterMobile>
|
|
|
|
|
<div className={styles.threadMobileFooterContent}>
|
|
|
|
|
<div className={styles.mobileFooterButtons}>
|
|
|
|
|
<button className='button' onClick={handlePostReplyClick} disabled={isThreadClosed}>
|
|
|
|
|
{t('post_a_reply')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className={styles.mobileFooterButtons}>
|
2026-03-13 13:25:10 +08:00
|
|
|
<ReturnButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
|
|
|
|
|
<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
|
2026-03-03 15:07:14 +08:00
|
|
|
<TopButton />
|
|
|
|
|
</div>
|
|
|
|
|
<div className={styles.mobileFooterButtons}>
|
|
|
|
|
<UpdateButton />
|
|
|
|
|
<AutoButton />
|
|
|
|
|
</div>
|
|
|
|
|
<div className={styles.mobileFooterStats}>
|
|
|
|
|
{capitalize(t('replies'))}: {replyCount ?? '?'} / {capitalize(requirePostLinkIsMedia ? t('images') : t('links'))}: {linkCount ?? '?'} /{' '}
|
|
|
|
|
{t('pagination.pageLabel')}: {pageNumber ?? '?'}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</PageFooterMobile>
|
|
|
|
|
);
|
|
|
|
|
};
|