Skip to content

Commit e2a8c8f

Browse files
authored
store package overview file tab in query, small tweaks and fixes (#2279)
1 parent 3a974eb commit e2a8c8f

8 files changed

Lines changed: 188 additions & 119 deletions

File tree

bun.lock

Lines changed: 54 additions & 52 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/Package/ReadmeCodeBlock.tsx renamed to components/Package/MarkdownContentBox/MarkdownCodeBlock.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type Props = {
1111

1212
const SHIKI_OPTS = { langAlias: { gradle: 'groovy' } } as const;
1313

14-
export default function ReadmeCodeBlock({ code, theme, lang }: Props) {
14+
export default function MarkdownCodeBlock({ code, theme, lang }: Props) {
1515
const highlighter = useShikiHighlighter(code, lang, theme, SHIKI_OPTS);
1616

1717
const copyButton = <CopyButton data={code} tooltip="Copy code" label="Copy code to clipboard" />;
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Dispatch, type SetStateAction, useState } from 'react';
1+
import { useState } from 'react';
22
import { Pressable, View } from 'react-native';
33

44
import { P } from '~/common/styleguide';
@@ -8,25 +8,26 @@ import tw from '~/util/tailwind';
88
type Props = {
99
tab: MarkdownTab;
1010
activeTab: MarkdownTabsType;
11-
setActiveTab: Dispatch<SetStateAction<MarkdownTabsType>>;
11+
onPress?: (tab: MarkdownTabsType) => void;
1212
};
1313

14-
export default function MarkdownContentTab({ tab, activeTab, setActiveTab }: Props) {
14+
export default function MarkdownContentTab({ tab, activeTab, onPress }: Props) {
1515
const [isHovered, setHovered] = useState<boolean>(false);
16+
const Element = onPress ? Pressable : View;
1617
return (
17-
<Pressable
18-
onPress={() => setActiveTab(tab.title)}
19-
onPointerEnter={() => setHovered(true)}
20-
onPointerLeave={() => setHovered(false)}
18+
<Element
19+
onPress={() => onPress?.(tab.title)}
20+
onPointerEnter={() => onPress && setHovered(true)}
21+
onPointerLeave={() => onPress && setHovered(false)}
2122
style={[
22-
tw`relative my-1.5 flex-row items-center gap-2 rounded px-2 py-1.5`,
23+
tw`relative my-1.5 select-none flex-row items-center gap-2 rounded px-2 py-1.5`,
2324
isHovered && tw`bg-palette-gray1 dark:bg-palette-gray7`,
2425
]}>
2526
<tab.Icon style={tw`size-5 text-tertiary dark:text-pewter`} />
2627
<P style={tw`text-[14px]`}>{tab.title}</P>
2728
{tab.title === activeTab && (
2829
<View style={tw`absolute -bottom-1.5 left-0 h-0.5 w-full rounded bg-primary-dark`} />
2930
)}
30-
</Pressable>
31+
</Element>
3132
);
3233
}

components/Package/ReadmeHeading.tsx renamed to components/Package/MarkdownContentBox/MarkdownHeading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type Props = PropsWithChildren<{
99
tagName: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
1010
}>;
1111

12-
export default function ReadmeHeading({ children, tagName }: Props) {
12+
export default function MarkdownHeading({ children, tagName }: Props) {
1313
const Heading = tagName;
1414
const slug = typeof children === 'string' ? kebabCase(children) : undefined;
1515

components/Package/MarkdownContentBox.tsx renamed to components/Package/MarkdownContentBox/index.tsx

Lines changed: 93 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Md } from '@m2d/react-markdown/client';
22
import { capitalize } from 'es-toolkit/string';
3-
import { useEffect, useState } from 'react';
3+
import { useRouter } from 'next/router';
4+
import { useEffect, useMemo, useState } from 'react';
45
import { View } from 'react-native';
56
import { type Theme } from 'react-shiki';
67
import rehypeRaw from 'rehype-raw';
@@ -12,8 +13,7 @@ import useSWR from 'swr';
1213
import { A, P } from '~/common/styleguide';
1314
import { CCFile, ChangelogFile, Check, ContributingFile, ReadmeFile } from '~/components/Icons';
1415
import CopyButton from '~/components/Package/CopyButton';
15-
import MarkdownContentTab from '~/components/Package/MarkdownContentTab';
16-
import ReadmeHeading from '~/components/Package/ReadmeHeading';
16+
import ThreeDotsLoader from '~/components/Package/ThreeDotsLoader';
1717
import rndDark from '~/styles/shiki/rnd-dark.json';
1818
import rndLight from '~/styles/shiki/rnd-light.json';
1919
import { type LibraryType, type MarkdownTab, type MarkdownTabsType } from '~/types';
@@ -23,8 +23,10 @@ import { getReadmeAssetURL } from '~/util/getReadmeAssetUrl';
2323
import { parseGitHubUrl } from '~/util/parseGitHubUrl';
2424
import tw from '~/util/tailwind';
2525

26-
import ReadmeCodeBlock from './ReadmeCodeBlock';
27-
import ThreeDotsLoader from './ThreeDotsLoader';
26+
import MarkdownCodeBlock from './MarkdownCodeBlock';
27+
import MarkdownContentTab from './MarkdownContentTab';
28+
import MarkdownHeading from './MarkdownHeading';
29+
import { DEFAULT_MARKDOWN_TAB, MARKDOWN_CONTENT_QUERY_PARAM, parseMarkdownTab } from './utils';
2830

2931
type Props = {
3032
packageName?: string;
@@ -33,45 +35,63 @@ type Props = {
3335
};
3436

3537
export default function MarkdownContentBox({ packageName, library, loader = false }: Props) {
36-
const [activeTab, setActiveTab] = useState<MarkdownTabsType>('Readme');
38+
const router = useRouter();
3739
const repoUrl = library?.github.urls.repo;
3840

39-
const contentTabs: MarkdownTab[] = [
40-
{
41-
title: 'Readme' as const,
42-
Icon: ReadmeFile,
43-
url: library?.template
44-
? getTabContentUrl(library, 'README.md')
45-
: `https://unpkg.com/${packageName}/README.md`,
46-
},
47-
...(library?.github?.hasChangelog
48-
? [
49-
{
50-
title: 'Changelog' as const,
51-
Icon: ChangelogFile,
52-
url: getTabContentUrl(library, 'CHANGELOG.md'),
53-
},
54-
]
55-
: []),
56-
...(library?.github?.hasContributing
57-
? [
58-
{
59-
title: 'Contributing' as const,
60-
Icon: ContributingFile,
61-
url: getTabContentUrl(library, 'CONTRIBUTING.md'),
62-
},
63-
]
64-
: []),
65-
...(library?.github?.hasCC
66-
? [
67-
{
68-
title: 'Code of Conduct' as const,
69-
Icon: CCFile,
70-
url: getTabContentUrl(library, 'CODE_OF_CONDUCT.md'),
71-
},
72-
]
73-
: []),
74-
].flat();
41+
const contentTabs = useMemo<MarkdownTab[]>(
42+
() =>
43+
[
44+
{
45+
title: 'Readme' as const,
46+
Icon: ReadmeFile,
47+
url: library?.template
48+
? getTabContentUrl(library, 'README.md')
49+
: `https://unpkg.com/${packageName}/README.md`,
50+
},
51+
...(library?.github?.hasChangelog
52+
? [
53+
{
54+
title: 'Changelog' as const,
55+
Icon: ChangelogFile,
56+
url: getTabContentUrl(library, 'CHANGELOG.md'),
57+
},
58+
]
59+
: []),
60+
...(library?.github?.hasContributing
61+
? [
62+
{
63+
title: 'Contributing' as const,
64+
Icon: ContributingFile,
65+
url: getTabContentUrl(library, 'CONTRIBUTING.md'),
66+
},
67+
]
68+
: []),
69+
...(library?.github?.hasCC
70+
? [
71+
{
72+
title: 'Code of Conduct' as const,
73+
Icon: CCFile,
74+
url: getTabContentUrl(library, 'CODE_OF_CONDUCT.md'),
75+
},
76+
]
77+
: []),
78+
].flat(),
79+
[library, packageName]
80+
);
81+
82+
const availableTabs = useMemo<MarkdownTabsType[]>(
83+
() => contentTabs.map(({ title }) => title),
84+
[contentTabs]
85+
);
86+
const routeTab = useMemo(
87+
() => parseMarkdownTab(router.query[MARKDOWN_CONTENT_QUERY_PARAM], availableTabs),
88+
[availableTabs, router.query]
89+
);
90+
const [activeTab, setActiveTab] = useState<MarkdownTabsType>(routeTab);
91+
92+
useEffect(() => {
93+
setActiveTab(currentTab => (currentTab === routeTab ? currentTab : routeTab));
94+
}, [routeTab]);
7595

7696
const { data, error, isLoading } = useSWR(
7797
contentTabs.find(({ title }) => title === activeTab)?.url,
@@ -111,6 +131,29 @@ export default function MarkdownContentBox({ packageName, library, loader = fals
111131
}
112132
}, [noData]);
113133

134+
function handleTabChange(nextTab: MarkdownTabsType) {
135+
if (nextTab === activeTab) {
136+
return;
137+
}
138+
139+
setActiveTab(nextTab);
140+
141+
const url = new URL(window.location.href);
142+
143+
if (nextTab === DEFAULT_MARKDOWN_TAB) {
144+
url.searchParams.delete(MARKDOWN_CONTENT_QUERY_PARAM);
145+
} else {
146+
url.searchParams.set(MARKDOWN_CONTENT_QUERY_PARAM, nextTab);
147+
}
148+
149+
url.hash = '';
150+
151+
void router.replace(`${url.pathname}${url.search}`, undefined, {
152+
shallow: true,
153+
scroll: false,
154+
});
155+
}
156+
114157
return (
115158
<View
116159
style={tw`my-2 rounded-xl border border-palette-gray2 text-black dark:border-default dark:text-white`}>
@@ -120,7 +163,7 @@ export default function MarkdownContentBox({ packageName, library, loader = fals
120163
<MarkdownContentTab
121164
tab={tab}
122165
activeTab={activeTab}
123-
setActiveTab={setActiveTab}
166+
onPress={availableTabs.length > 1 ? handleTabChange : undefined}
124167
key={`tab-${tab.title.toLocaleLowerCase()}`}
125168
/>
126169
))}
@@ -144,22 +187,22 @@ export default function MarkdownContentBox({ packageName, library, loader = fals
144187
id="markdownContentContainer"
145188
components={{
146189
h1: ({ children, node }: any) => (
147-
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
190+
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
148191
),
149192
h2: ({ children, node }: any) => (
150-
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
193+
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
151194
),
152195
h3: ({ children, node }: any) => (
153-
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
196+
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
154197
),
155198
h4: ({ children, node }: any) => (
156-
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
199+
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
157200
),
158201
h5: ({ children, node }: any) => (
159-
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
202+
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
160203
),
161204
h6: ({ children, node }: any) => (
162-
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
205+
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
163206
),
164207
br: () => null,
165208
hr: () => null,
@@ -217,7 +260,7 @@ export default function MarkdownContentBox({ packageName, library, loader = fals
217260
const langClass = children?.props?.className;
218261
if (langClass) {
219262
return (
220-
<ReadmeCodeBlock
263+
<MarkdownCodeBlock
221264
code={children.props.children}
222265
theme={(tw.prefixMatch('dark') ? rndDark : rndLight) as Theme}
223266
lang={langClass ? (langClass.split('-')[1] ?? 'sh').toLowerCase() : 'sh'}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { type MarkdownTabsType } from '~/types';
2+
3+
export const MARKDOWN_CONTENT_QUERY_PARAM = 'tab';
4+
export const MARKDOWN_TABS = ['Readme', 'Changelog', 'Contributing', 'Code of Conduct'] as const;
5+
export const DEFAULT_MARKDOWN_TAB: MarkdownTabsType = 'Readme';
6+
7+
export function isValidMarkdownTab(
8+
value?: string | string[],
9+
availableTabs: readonly MarkdownTabsType[] = MARKDOWN_TABS
10+
): value is MarkdownTabsType {
11+
return typeof value === 'string' && availableTabs.includes(value as MarkdownTabsType);
12+
}
13+
14+
export function parseMarkdownTab(
15+
value?: string | string[],
16+
availableTabs: readonly MarkdownTabsType[] = MARKDOWN_TABS
17+
): MarkdownTabsType {
18+
if (isValidMarkdownTab(value, availableTabs)) {
19+
return value;
20+
}
21+
22+
return availableTabs.includes(DEFAULT_MARKDOWN_TAB)
23+
? DEFAULT_MARKDOWN_TAB
24+
: (availableTabs[0] ?? DEFAULT_MARKDOWN_TAB);
25+
}

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@
6868
"browserslist": "^4.28.1",
6969
"cheerio": "^1.2.0",
7070
"dotenv": "^17.3.1",
71-
"lint-staged": "^16.3.3",
71+
"lint-staged": "^16.4.0",
7272
"next-compose-plugins": "^2.2.1",
7373
"next-fonts": "^1.5.1",
7474
"next-images": "^1.8.5",
75-
"oxfmt": "^0.39.0",
76-
"oxlint": "^1.54.0",
75+
"oxfmt": "^0.40.0",
76+
"oxlint": "^1.55.0",
7777
"oxlint-tsgolint": "^0.16.0",
7878
"simple-git-hooks": "^2.13.1",
7979
"typescript": "^5.9.3",

util/queryParams.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { omit } from 'es-toolkit/object';
22
import { type NextRouter } from 'next/router';
33

4-
import { CHART_MODE_QUERY_PARAM } from '~/components/Package/VersionDownloadsChart/utils';
5-
64
export function parseQueryParams(params: Partial<Record<string, string | string[]>>) {
75
return Object.fromEntries(
86
Object.entries(params).map(([key, val]) => [
@@ -13,7 +11,7 @@ export function parseQueryParams(params: Partial<Record<string, string | string[
1311
}
1412

1513
export function replaceQueryParam(router: NextRouter, paramName: string, paramValue?: string) {
16-
const queryParams = omit(router.query, [CHART_MODE_QUERY_PARAM, paramName]);
14+
const queryParams = omit(router.query, [paramName]);
1715

1816
void router.replace(
1917
{

0 commit comments

Comments
 (0)