mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(rules): use hash links for directory deep links
Switch rules navigation from /rules/:code paths to /rules#code hashes, reject legacy subpaths, and ignore address hashes in markdown links.
This commit is contained in:
@@ -444,6 +444,22 @@ describe('App', () => {
|
||||
expect(container.querySelector('[data-testid="boards-bar"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the rules route as a global static page', async () => {
|
||||
await renderApp('/rules#mu');
|
||||
|
||||
expect(latestLocation).toBe('/rules');
|
||||
expect(container.querySelector('[data-testid="rules-view"]')).toBeTruthy();
|
||||
expect(container.querySelector('[data-testid="boards-bar"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects legacy rules subpaths instead of treating them as board routes', async () => {
|
||||
await renderApp('/rules/music-posting.eth');
|
||||
|
||||
expect(latestLocation).toBe('/not-found');
|
||||
expect(container.querySelector('[data-testid="not-found-view"]')).toBeTruthy();
|
||||
expect(container.querySelector('[data-testid="rules-view"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('canonicalizes board address routes to directory codes while preserving query strings', async () => {
|
||||
await renderApp('/music-posting.eth/thread/comment-1?focus=1');
|
||||
|
||||
|
||||
+2
-1
@@ -292,7 +292,8 @@ const App = () => {
|
||||
<Route path='/' element={<Home />} />
|
||||
<Route path='/faq' element={<Faq />} />
|
||||
<Route path='/pass' element={<Pass />} />
|
||||
<Route path='/rules/:boardIdentifier?' element={<Rules />} />
|
||||
<Route path='/rules' element={<Rules />} />
|
||||
<Route path='/rules/*' element={<Navigate to='/not-found' replace />} />
|
||||
<Route path='/blotter' element={<Blotter />} />
|
||||
<Route
|
||||
path='/settings/account-data'
|
||||
|
||||
@@ -158,7 +158,7 @@ vi.mock('../../../stores/use-post-number-store', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../comment-media', () => ({
|
||||
vi.mock('../../comment-media/comment-media', () => ({
|
||||
default: ({
|
||||
commentMediaInfo,
|
||||
isFloatingEmbed,
|
||||
@@ -183,11 +183,11 @@ vi.mock('../../comment-media', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../embed', () => ({
|
||||
vi.mock('../../embed/embed-utils', () => ({
|
||||
canEmbed: (parsedUrl: URL) => testState.embeddableHosts.has(parsedUrl.host),
|
||||
}));
|
||||
|
||||
vi.mock('../../reply-quote-preview', () => ({
|
||||
vi.mock('../../reply-quote-preview/reply-quote-preview', () => ({
|
||||
default: ({
|
||||
isOP,
|
||||
isQuotelinkUnavailable,
|
||||
@@ -365,14 +365,14 @@ describe('Markdown', () => {
|
||||
|
||||
it('renders inline Markdown links for app routes and inert empty hrefs', async () => {
|
||||
await renderMarkdown({
|
||||
content: '[Rules](/rules/biz) [AI moderation]( )',
|
||||
content: '[Rules](/rules#biz) [Old rules](/rules/biz) [Address rules](/rules#custom-board.bso) [AI moderation]( )',
|
||||
});
|
||||
|
||||
const links = Array.from(container.querySelectorAll('a'));
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links[0]?.getAttribute('href')).toBe('/rules/biz');
|
||||
expect(links[0]?.getAttribute('href')).toBe('/rules#biz');
|
||||
expect(links[0]?.textContent).toBe('Rules');
|
||||
expect(container.textContent).toBe('Rules AI moderation');
|
||||
expect(container.textContent).toBe('Rules Old rules Address rules AI moderation');
|
||||
});
|
||||
|
||||
it('does not render unsafe inline Markdown hrefs', async () => {
|
||||
|
||||
@@ -5,18 +5,18 @@ import { useDismiss, useFloating, useFocus, useHover, useInteractions, offset, s
|
||||
import { getLinkMediaInfo, getHasThumbnail } from '../../lib/utils/media-utils';
|
||||
import { isCatalogView } from '../../lib/utils/view-utils';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import CommentMedia from '../comment-media';
|
||||
import CodeBlock from '../code-block';
|
||||
import CommentMedia from '../comment-media/comment-media';
|
||||
import CodeBlock from '../code-block/code-block';
|
||||
import styles from './markdown.module.css';
|
||||
import { Link, useLocation, useParams } from 'react-router-dom';
|
||||
import { canEmbed } from '../embed';
|
||||
import { canEmbed } from '../embed/embed-utils';
|
||||
import { is5chanLink, transform5chanLinkToInternal, isValidCrossboardPattern } from '../../lib/utils/url-utils';
|
||||
import { CROSSBOARD_NUMBER_QUOTE_TOKEN_REGEX, type ExternalQuoteReference } from '../../lib/utils/external-quote-utils';
|
||||
import { isUnavailableQuoteTarget } from '../../lib/utils/quote-link-utils';
|
||||
import usePostNumberStore, { getCidForPostNumber } from '../../stores/use-post-number-store';
|
||||
import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||
import { useComment } from '@bitsocial/bitsocial-react-hooks';
|
||||
import ReplyQuotePreview from '../reply-quote-preview';
|
||||
import ReplyQuotePreview from '../reply-quote-preview/reply-quote-preview';
|
||||
import ExternalNumberQuoteLink from './external-number-quote-link';
|
||||
import { findDirectoryByAddress, useDirectories, type DirectoryCommunity } from '../../hooks/use-directories';
|
||||
import { getDirectoryCodeForBoardAddress } from '../../lib/utils/directory-list-lookup-utils';
|
||||
@@ -216,6 +216,7 @@ const COMBINED_REGEX_WITHOUT_SPOILER = new RegExp(
|
||||
);
|
||||
|
||||
const makeTokenKey = (prefix: string, type: Token['type'], start: number, end: number): string => `${prefix}${type}:${start}:${end}`;
|
||||
const RULES_HASH_ROUTE_REGEX = /^\/rules#[A-Za-z0-9_-]+$/;
|
||||
|
||||
const isGreentextLine = (line: string): boolean => {
|
||||
if (line === '>') return true;
|
||||
@@ -233,6 +234,10 @@ function normalizeInternalRouteHref(href: string): string {
|
||||
return href;
|
||||
}
|
||||
|
||||
function isUnsupportedRulesRouteHref(href: string): boolean {
|
||||
return href.startsWith('/rules/') || (href.startsWith('/rules#') && !RULES_HASH_ROUTE_REGEX.test(href));
|
||||
}
|
||||
|
||||
function splitUrlTrailingText(rawHref: string): { href: string; trailingText: string } {
|
||||
let href = rawHref;
|
||||
let trailingText = '';
|
||||
@@ -475,6 +480,9 @@ const AnchorLink = ({ href, text }: { href: string; text: string }) => {
|
||||
const internalPath = transform5chanLinkToInternal(href);
|
||||
if (internalPath) {
|
||||
const internalRoute = normalizeInternalRouteHref(internalPath);
|
||||
if (isUnsupportedRulesRouteHref(internalRoute)) {
|
||||
return <span>{text}</span>;
|
||||
}
|
||||
let displayText: React.ReactNode = text;
|
||||
const isAutolinkedUrl = text.startsWith('http');
|
||||
|
||||
@@ -493,16 +501,21 @@ const AnchorLink = ({ href, text }: { href: string; text: string }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedHref = normalizeInternalRouteHref(href);
|
||||
if (isUnsupportedRulesRouteHref(normalizedHref)) {
|
||||
return <span>{text}</span>;
|
||||
}
|
||||
|
||||
if (
|
||||
href.startsWith('#/') ||
|
||||
href.startsWith('/#/') ||
|
||||
href.startsWith('/p/') ||
|
||||
href.match(/^\/p\/[^/]+(\/c\/[^/]+)?$/) ||
|
||||
href.match(/^\/rules\/[^/]+$/) ||
|
||||
href.match(/^\/[^/]+(\/thread\/[^/]+)?$/) ||
|
||||
href.match(/^\/[^/]+\/(catalog|description|rules)(\/settings)?$/)
|
||||
normalizedHref.startsWith('/p/') ||
|
||||
normalizedHref.match(/^\/p\/[^/]+(\/c\/[^/]+)?$/) ||
|
||||
normalizedHref.match(RULES_HASH_ROUTE_REGEX) ||
|
||||
normalizedHref.match(/^\/[^/#]+(\/thread\/[^/]+)?$/) ||
|
||||
normalizedHref.match(/^\/[^/]+\/(catalog|description|rules)(\/settings)?$/)
|
||||
) {
|
||||
return <Link to={normalizeInternalRouteHref(href)}>{text}</Link>;
|
||||
return <Link to={normalizedHref}>{text}</Link>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1389,7 +1389,20 @@ describe('PostForm', () => {
|
||||
expect(promptRow?.className).toBe('rules');
|
||||
expect(promptRow?.querySelector('ul')?.className).toBe('rules');
|
||||
expect(links.map((link) => link.textContent)).toEqual(['Rules', 'FAQ']);
|
||||
expect(links.map((link) => link.getAttribute('href'))).toEqual(['/rules/mu', '/faq']);
|
||||
expect(links.map((link) => link.getAttribute('href'))).toEqual(['/rules#mu', '/faq']);
|
||||
});
|
||||
|
||||
it('uses the plain rules page link for custom boards without a directory hash', async () => {
|
||||
testState.resolvedCommunityAddress = 'custom-board.bso';
|
||||
|
||||
await renderPostForm('/custom-board.bso');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const promptRow = Array.from(container.querySelectorAll('tr')).find((row) => row.textContent === 'Please read the Rules and FAQ before posting.');
|
||||
const links = Array.from(promptRow?.querySelectorAll('a') || []);
|
||||
|
||||
expect(links.map((link) => link.textContent)).toEqual(['Rules', 'FAQ']);
|
||||
expect(links.map((link) => link.getAttribute('href'))).toEqual(['/rules', '/faq']);
|
||||
});
|
||||
|
||||
it('shortens long pasted file-link filenames next to the upload button', async () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
|
||||
import { isValidPublishURL, isValidURL } from '../../lib/utils/url-utils';
|
||||
import { getModerationPostingRoleLabel } from '../../lib/utils/author-display-utils';
|
||||
import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
|
||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||
import { getBoardPath, isDirectoryRoute } from '../../lib/utils/route-utils';
|
||||
import { isAllView, isCatalogView, isModQueueView, isModView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
|
||||
import { getCommentFlagOptionsForDirectory, getCommentFlagPublishOptionsForDirectory, type CommentFlagSelectOption } from '../../lib/comment-flag-selection';
|
||||
import { FLASH_TAG_OPTIONS, getFlashTagPublishOptionsForDirectoryCode, isFlashDirectoryCode, type FlashTagOption } from '../../lib/flash-tags';
|
||||
@@ -545,8 +545,9 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const subscriptions = account?.subscriptions || [];
|
||||
const directories = useDirectories();
|
||||
const directoryEntry = useDirectoryEntry(effectiveBoardAddress, params?.boardIdentifier);
|
||||
const pendingPostBoardPath = effectiveBoardAddress ? getBoardPath(effectiveBoardAddress, directories) : undefined;
|
||||
const rulesPath = effectiveBoardAddress ? `/rules/${getBoardPath(effectiveBoardAddress, directories)}` : '/rules';
|
||||
const effectiveBoardPath = effectiveBoardAddress ? getBoardPath(effectiveBoardAddress, directories) : undefined;
|
||||
const pendingPostBoardPath = effectiveBoardPath;
|
||||
const rulesPath = effectiveBoardPath && isDirectoryRoute(effectiveBoardPath, directories) ? `/rules#${effectiveBoardPath}` : '/rules';
|
||||
const showSpoilerForPost = directoryEntry?.features?.noSpoilers !== true;
|
||||
const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true;
|
||||
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
||||
|
||||
@@ -9,7 +9,6 @@ import Rules from '../rules';
|
||||
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
boardIdentifier: undefined as string | undefined,
|
||||
communities: {} as Record<string, { rules?: string[]; shortAddress?: string; state?: string; title?: string }>,
|
||||
directories: [
|
||||
{ address: 'anime-posting.eth', title: '/a/ - Anime & Manga' },
|
||||
@@ -31,16 +30,6 @@ vi.mock('react-i18next', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom');
|
||||
return {
|
||||
...actual,
|
||||
useParams: () => ({
|
||||
boardIdentifier: testState.boardIdentifier,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
||||
useClientsStates: () => ({
|
||||
states: {},
|
||||
@@ -60,12 +49,12 @@ vi.mock('../../../hooks/use-directories', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../home', () => ({
|
||||
vi.mock('../../home/home', () => ({
|
||||
Footer: () => createElement('div', { 'data-testid': 'footer' }, 'footer'),
|
||||
HomeLogo: () => createElement('div', { 'data-testid': 'home-logo' }, 'home-logo'),
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/markdown', () => ({
|
||||
vi.mock('../../../components/markdown/markdown', () => ({
|
||||
default: ({ content }: { content: string }) => createElement('div', { 'data-testid': 'markdown' }, content),
|
||||
}));
|
||||
|
||||
@@ -81,9 +70,9 @@ let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
let scrollIntoViewMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
const renderRules = async () => {
|
||||
const renderRules = async (initialEntry = '/rules') => {
|
||||
await act(async () => {
|
||||
root.render(createElement(MemoryRouter, null, createElement(Rules)));
|
||||
root.render(createElement(MemoryRouter, { initialEntries: [initialEntry] }, createElement(Rules)));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -110,7 +99,6 @@ const submitBoardAddress = async (address: string) => {
|
||||
describe('Rules', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
testState.boardIdentifier = undefined;
|
||||
testState.communities = {};
|
||||
testState.directories = [
|
||||
{ address: 'anime-posting.eth', title: '/a/ - Anime & Manga' },
|
||||
@@ -141,9 +129,9 @@ describe('Rules', () => {
|
||||
it('renders a quick-jump nav link and a rules section for every directory', async () => {
|
||||
await renderRules();
|
||||
|
||||
// Quick-jump nav links use the directory name (like 4chan's board list) and point at the per-directory route.
|
||||
expect(container.querySelector('a[href="/rules/a"]')?.textContent).toBe('Anime & Manga');
|
||||
expect(container.querySelector('a[href="/rules/b"]')?.textContent).toBe('Random');
|
||||
// Quick-jump nav links use the directory name (like 4chan's board list) and point at per-directory hash links.
|
||||
expect(container.querySelector('a[href="/rules#a"]')?.textContent).toBe('Anime & Manga');
|
||||
expect(container.querySelector('a[href="/rules#b"]')?.textContent).toBe('Random');
|
||||
|
||||
// One anchored rules section per directory.
|
||||
expect(container.querySelector('#a')).toBeTruthy();
|
||||
@@ -162,14 +150,28 @@ describe('Rules', () => {
|
||||
expect(container.textContent).not.toContain('Rules for:');
|
||||
});
|
||||
|
||||
it('insta-scrolls to a directory section when deep-linked via /rules/:code', async () => {
|
||||
testState.boardIdentifier = 'a';
|
||||
|
||||
await renderRules();
|
||||
it('insta-scrolls to a directory section when deep-linked via /rules#code', async () => {
|
||||
await renderRules('/rules#a');
|
||||
|
||||
expect(scrollIntoViewMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores address hashes instead of resolving them to directory rules or P2P rules', async () => {
|
||||
testState.communities = {
|
||||
'anime-posting.eth': {
|
||||
rules: ['P2P address rules should not render from a hash.'],
|
||||
shortAddress: 'anime-posting.eth',
|
||||
state: 'succeeded',
|
||||
},
|
||||
};
|
||||
|
||||
await renderRules('/rules#anime-posting.eth');
|
||||
|
||||
expect(scrollIntoViewMock).not.toHaveBeenCalled();
|
||||
expect(container.textContent).not.toContain('Rules for: anime-posting.eth');
|
||||
expect(container.textContent).not.toContain('P2P address rules should not render from a hash.');
|
||||
});
|
||||
|
||||
it('loads a board over P2P when an address is submitted in the loader', async () => {
|
||||
testState.communities = {
|
||||
'custom-board.eth': {
|
||||
@@ -186,7 +188,7 @@ describe('Rules', () => {
|
||||
expect(container.textContent).toContain('No spamming.');
|
||||
});
|
||||
|
||||
it('clears a loaded P2P rules box when navigating to a directory route', async () => {
|
||||
it('clears a loaded P2P rules box when navigating to a directory hash link', async () => {
|
||||
testState.communities = {
|
||||
'custom-board.eth': {
|
||||
rules: ['No spamming.'],
|
||||
@@ -199,8 +201,10 @@ describe('Rules', () => {
|
||||
await submitBoardAddress('custom-board.eth');
|
||||
expect(container.textContent).toContain('Rules for: custom-board.eth');
|
||||
|
||||
testState.boardIdentifier = 'a';
|
||||
await renderRules();
|
||||
const link = container.querySelector('a[href="/rules#a"]') as HTMLAnchorElement;
|
||||
await act(async () => {
|
||||
link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
||||
});
|
||||
|
||||
expect(container.textContent).not.toContain('Rules for: custom-board.eth');
|
||||
expect(scrollIntoViewMock).toHaveBeenCalled();
|
||||
|
||||
+26
-23
@@ -1,12 +1,12 @@
|
||||
import { Fragment, useEffect, useRef, useState, FormEvent } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useCommunity } from '@bitsocial/bitsocial-react-hooks';
|
||||
import { Footer, HomeLogo } from '../home';
|
||||
import { Footer, HomeLogo } from '../home/home';
|
||||
import { useDirectories, useDirectoryDefaults, DirectoryCommunity, DirectoryDefaultsData } from '../../hooks/use-directories';
|
||||
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
|
||||
import { getCommunityAddress, getBoardPath, isDirectoryRoute } from '../../lib/utils/route-utils';
|
||||
import Markdown from '../../components/markdown';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import { getCommunityAddress, isDirectoryRoute } from '../../lib/utils/route-utils';
|
||||
import Markdown from '../../components/markdown/markdown';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis/loading-ellipsis';
|
||||
import useStateString from '../../hooks/use-state-string';
|
||||
import styles from './rules.module.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -59,13 +59,16 @@ const groupDirectoriesByCategory = (directories: DirectoryCommunity[], defaults:
|
||||
}))
|
||||
.filter((group) => group.communities.length > 0);
|
||||
|
||||
// Resolve a /rules/:boardIdentifier segment (directory code or board address) to a directory code.
|
||||
const resolveDirectoryCode = (identifier: string, directories: DirectoryCommunity[]): string | null => {
|
||||
if (isDirectoryRoute(identifier, directories)) {
|
||||
return identifier;
|
||||
const getRulesHashCode = (hash: string): string => {
|
||||
if (!hash) {
|
||||
return '';
|
||||
}
|
||||
const rawHash = hash.startsWith('#') ? hash.slice(1) : hash;
|
||||
try {
|
||||
return decodeURIComponent(rawHash);
|
||||
} catch {
|
||||
return rawHash;
|
||||
}
|
||||
const code = getBoardPath(getCommunityAddress(identifier, directories), directories);
|
||||
return isDirectoryRoute(code, directories) ? code : null;
|
||||
};
|
||||
|
||||
// A single directory's rules (h3 title + ordered rules), anchored by code for deep-link scrolling.
|
||||
@@ -109,7 +112,7 @@ const CategoryRulesBox = ({ group, defaults }: { group: CategoryGroup; defaults:
|
||||
</div>
|
||||
);
|
||||
|
||||
// Quick-jump nav (left column) grouped by category; clicking a directory insta-scrolls to its rules via /rules/:code.
|
||||
// Quick-jump nav (left column) grouped by category; clicking a directory insta-scrolls to its rules via /rules#code.
|
||||
const DirectoryNav = ({ groups }: { groups: CategoryGroup[] }) => (
|
||||
<div className={`${styles.box} ${styles.selectorBox}`}>
|
||||
<div className={styles.boxBar}>
|
||||
@@ -129,7 +132,7 @@ const DirectoryNav = ({ groups }: { groups: CategoryGroup[] }) => (
|
||||
const code = getDirectoryCode(community);
|
||||
return (
|
||||
<li key={community.address}>
|
||||
<Link to={`/rules/${code}`}>{getBoardName(community.title) || getDirectoryDisplayTitle(community)}</Link>
|
||||
<Link to={`/rules#${code}`}>{getBoardName(community.title) || getDirectoryDisplayTitle(community)}</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
@@ -233,11 +236,12 @@ const LoadBoardRules = ({ onLoad, onClear, isLoaded }: { onLoad: (address: strin
|
||||
};
|
||||
|
||||
const Rules = () => {
|
||||
const { boardIdentifier } = useParams();
|
||||
const { hash } = useLocation();
|
||||
const directories = useDirectories();
|
||||
const directoryDefaults = useDirectoryDefaults();
|
||||
const [loadedAddress, setLoadedAddress] = useState('');
|
||||
const scrolledForRef = useRef<string | null>(null);
|
||||
const hashCode = getRulesHashCode(hash);
|
||||
|
||||
// Order directories alphabetically by directory code (e.g. /3/, /a/, /aco/...), like 4chan, not by title.
|
||||
const directoriesWithCode = directories.filter((community) => getDirectoryCode(community)).toSorted((a, b) => getDirectoryCode(a).localeCompare(getDirectoryCode(b)));
|
||||
@@ -253,24 +257,23 @@ const Rules = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setLoadedAddress('');
|
||||
if (!boardIdentifier) {
|
||||
scrolledForRef.current = null;
|
||||
scrolledForRef.current = null;
|
||||
if (!hashCode) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}, [boardIdentifier]);
|
||||
}, [hashCode]);
|
||||
|
||||
// Deep-link: /rules/:code insta-scrolls to that directory's rules once the matching section is rendered.
|
||||
// Deep-link: /rules#code insta-scrolls to a known directory's rules once the matching section is rendered.
|
||||
useEffect(() => {
|
||||
if (!boardIdentifier || scrolledForRef.current === boardIdentifier) {
|
||||
if (!hashCode || scrolledForRef.current === hashCode || !isDirectoryRoute(hashCode, directories)) {
|
||||
return;
|
||||
}
|
||||
const code = resolveDirectoryCode(boardIdentifier, directories);
|
||||
const element = code ? document.getElementById(code) : null;
|
||||
const element = document.getElementById(hashCode);
|
||||
if (element) {
|
||||
element.scrollIntoView();
|
||||
scrolledForRef.current = boardIdentifier;
|
||||
scrolledForRef.current = hashCode;
|
||||
}
|
||||
}, [boardIdentifier, directories]);
|
||||
}, [hashCode, directories]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
|
||||
Reference in New Issue
Block a user