Skip to content

Commit e7c3052

Browse files
refactor(blog): extract CollapsibleTagList to eliminate duplicated tag expand/collapse logic
Co-authored-by: Ollie <olliethedev@users.noreply.github.com>
1 parent 565cfa3 commit e7c3052

3 files changed

Lines changed: 82 additions & 108 deletions

File tree

packages/stack/src/plugins/blog/client/components/pages/post-page.internal.tsx

Lines changed: 4 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { usePluginOverrides, useBasePath } from "@btst/stack/context";
3+
import { usePluginOverrides } from "@btst/stack/context";
44
import { formatDate } from "date-fns";
55
import {
66
useSuspensePost,
@@ -12,22 +12,18 @@ import { MarkdownContent } from "../shared/markdown-content";
1212
import { PageHeader } from "../shared/page-header";
1313
import { PageWrapper } from "../shared/page-wrapper";
1414
import type { BlogPluginOverrides } from "../../overrides";
15-
import { DefaultImage, DefaultLink } from "../shared/defaults";
15+
import { DefaultImage } from "../shared/defaults";
1616
import { BLOG_LOCALIZATION } from "../../localization";
1717
import { PostNavigation } from "../shared/post-navigation";
1818
import { RecentPostsCarousel } from "../shared/recent-posts-carousel";
19-
import { Badge } from "@workspace/ui/components/badge";
2019
import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
2120
import { OnThisPage, OnThisPageSelect } from "../shared/on-this-page";
2221
import type { SerializedPost } from "../../../types";
2322
import { useRegisterPageAIContext } from "@btst/stack/plugins/ai-chat/client/context";
2423
import { WhenVisible } from "@workspace/ui/components/when-visible";
2524
import { PostNavigationSkeleton } from "../loading/post-navigation-skeleton";
2625
import { RecentPostsCarouselSkeleton } from "../loading/recent-posts-carousel-skeleton";
27-
import { ChevronDown, ChevronUp } from "lucide-react";
28-
import { useState } from "react";
29-
30-
const MAX_VISIBLE_POST_TAGS = 15;
26+
import { CollapsibleTagList } from "../shared/collapsible-tag-list";
3127

3228
// Internal component with actual page content
3329
export function PostPage({ slug }: { slug: string }) {
@@ -155,58 +151,12 @@ export function PostPage({ slug }: { slug: string }) {
155151
}
156152

157153
function PostHeaderTop({ post }: { post: SerializedPost }) {
158-
const { Link, localization } = usePluginOverrides<
159-
BlogPluginOverrides,
160-
Partial<BlogPluginOverrides>
161-
>("blog", {
162-
Link: DefaultLink,
163-
localization: BLOG_LOCALIZATION,
164-
});
165-
const basePath = useBasePath();
166-
const [showAll, setShowAll] = useState(false);
167-
168-
const allTags = post.tags ?? [];
169-
const hasMore = allTags.length > MAX_VISIBLE_POST_TAGS;
170-
const visibleTags =
171-
showAll || !hasMore ? allTags : allTags.slice(0, MAX_VISIBLE_POST_TAGS);
172-
173154
return (
174155
<div className="flex flex-row items-center justify-center gap-2 flex-wrap mt-8">
175156
<span className="font-light text-muted-foreground text-sm">
176157
{formatDate(post.createdAt, "MMMM d, yyyy")}
177158
</span>
178-
{visibleTags.map((tag) => (
179-
<Link key={tag.id} href={`${basePath}/blog/tag/${tag.slug}`}>
180-
<Badge variant="secondary" className="text-xs">
181-
{tag.name}
182-
</Badge>
183-
</Link>
184-
))}
185-
{hasMore && (
186-
<Badge asChild variant="secondary" className="text-xs cursor-pointer">
187-
<button
188-
type="button"
189-
onClick={() => setShowAll((prev) => !prev)}
190-
aria-expanded={showAll}
191-
aria-label={
192-
showAll
193-
? localization.BLOG_TAGS_SHOW_LESS
194-
: localization.BLOG_TAGS_SHOW_ALL
195-
}
196-
title={
197-
showAll
198-
? localization.BLOG_TAGS_SHOW_LESS
199-
: localization.BLOG_TAGS_SHOW_ALL
200-
}
201-
>
202-
{showAll ? (
203-
<ChevronUp aria-hidden="true" />
204-
) : (
205-
<ChevronDown aria-hidden="true" />
206-
)}
207-
</button>
208-
</Badge>
209-
)}
159+
<CollapsibleTagList tags={post.tags ?? []} />
210160
</div>
211161
);
212162
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"use client";
2+
3+
import { usePluginOverrides, useBasePath } from "@btst/stack/context";
4+
import type { BlogPluginOverrides } from "../../overrides";
5+
import { DefaultLink } from "./defaults";
6+
import { Badge } from "@workspace/ui/components/badge";
7+
import { BLOG_LOCALIZATION } from "../../localization";
8+
import { ChevronDown, ChevronUp } from "lucide-react";
9+
import { useState } from "react";
10+
import type { SerializedTag } from "../../../types";
11+
12+
const MAX_VISIBLE_TAGS = 15;
13+
14+
interface CollapsibleTagListProps {
15+
tags: SerializedTag[];
16+
maxVisible?: number;
17+
}
18+
19+
export function CollapsibleTagList({
20+
tags,
21+
maxVisible = MAX_VISIBLE_TAGS,
22+
}: CollapsibleTagListProps) {
23+
const { Link, localization } = usePluginOverrides<
24+
BlogPluginOverrides,
25+
Partial<BlogPluginOverrides>
26+
>("blog", {
27+
Link: DefaultLink,
28+
localization: BLOG_LOCALIZATION,
29+
});
30+
const basePath = useBasePath();
31+
const [showAll, setShowAll] = useState(false);
32+
33+
if (!tags || tags.length === 0) {
34+
return null;
35+
}
36+
37+
const hasMore = tags.length > maxVisible;
38+
const visibleTags = showAll || !hasMore ? tags : tags.slice(0, maxVisible);
39+
40+
return (
41+
<>
42+
{visibleTags.map((tag) => (
43+
<Link key={tag.id} href={`${basePath}/blog/tag/${tag.slug}`}>
44+
<Badge variant="secondary" className="text-xs">
45+
{tag.name}
46+
</Badge>
47+
</Link>
48+
))}
49+
{hasMore && (
50+
<Badge asChild variant="secondary" className="text-xs cursor-pointer">
51+
<button
52+
type="button"
53+
onClick={() => setShowAll((prev) => !prev)}
54+
aria-expanded={showAll}
55+
aria-label={
56+
showAll
57+
? localization.BLOG_TAGS_SHOW_LESS
58+
: localization.BLOG_TAGS_SHOW_ALL
59+
}
60+
title={
61+
showAll
62+
? localization.BLOG_TAGS_SHOW_LESS
63+
: localization.BLOG_TAGS_SHOW_ALL
64+
}
65+
>
66+
{showAll ? (
67+
<ChevronUp aria-hidden="true" />
68+
) : (
69+
<ChevronDown aria-hidden="true" />
70+
)}
71+
</button>
72+
</Badge>
73+
)}
74+
</>
75+
);
76+
}
Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,18 @@
11
"use client";
22

3-
import { usePluginOverrides, useBasePath } from "@btst/stack/context";
4-
import type { BlogPluginOverrides } from "../../overrides";
5-
import { DefaultLink } from "./defaults";
6-
import { Badge } from "@workspace/ui/components/badge";
73
import { useSuspenseTags } from "../../hooks/blog-hooks";
8-
import { BLOG_LOCALIZATION } from "../../localization";
9-
import { ChevronDown, ChevronUp } from "lucide-react";
10-
import { useState } from "react";
11-
12-
const MAX_VISIBLE_TAGS = 15;
4+
import { CollapsibleTagList } from "./collapsible-tag-list";
135

146
export function TagsList() {
157
const { tags } = useSuspenseTags();
16-
const { Link, localization } = usePluginOverrides<
17-
BlogPluginOverrides,
18-
Partial<BlogPluginOverrides>
19-
>("blog", {
20-
Link: DefaultLink,
21-
localization: BLOG_LOCALIZATION,
22-
});
23-
const basePath = useBasePath();
24-
const [showAll, setShowAll] = useState(false);
258

269
if (!tags || tags.length === 0) {
2710
return null;
2811
}
2912

30-
const hasMore = tags.length > MAX_VISIBLE_TAGS;
31-
const visibleTags =
32-
showAll || !hasMore ? tags : tags.slice(0, MAX_VISIBLE_TAGS);
33-
3413
return (
3514
<div className="flex flex-wrap gap-2 justify-center">
36-
{visibleTags.map((tag) => (
37-
<Link key={tag.id} href={`${basePath}/blog/tag/${tag.slug}`}>
38-
<Badge variant="secondary" className="text-xs">
39-
{tag.name}
40-
</Badge>
41-
</Link>
42-
))}
43-
{hasMore && (
44-
<Badge asChild variant="secondary" className="text-xs cursor-pointer">
45-
<button
46-
type="button"
47-
onClick={() => setShowAll((prev) => !prev)}
48-
aria-expanded={showAll}
49-
aria-label={
50-
showAll
51-
? localization.BLOG_TAGS_SHOW_LESS
52-
: localization.BLOG_TAGS_SHOW_ALL
53-
}
54-
title={
55-
showAll
56-
? localization.BLOG_TAGS_SHOW_LESS
57-
: localization.BLOG_TAGS_SHOW_ALL
58-
}
59-
>
60-
{showAll ? (
61-
<ChevronUp aria-hidden="true" />
62-
) : (
63-
<ChevronDown aria-hidden="true" />
64-
)}
65-
</button>
66-
</Badge>
67-
)}
15+
<CollapsibleTagList tags={tags} />
6816
</div>
6917
);
7018
}

0 commit comments

Comments
 (0)