diff --git a/src/app/hack/[slug]/page.tsx b/src/app/hack/[slug]/page.tsx index ed20e7a..8eb5835 100644 --- a/src/app/hack/[slug]/page.tsx +++ b/src/app/hack/[slug]/page.tsx @@ -22,6 +22,7 @@ import { TbProgressCheck } from "react-icons/tb"; import { isInformationalArchiveHack, isDownloadableArchiveHack, isArchiveHack, checkEditPermission } from "@/utils/hack"; import Avatar from "@/components/Account/Avatar"; import CollapsibleCard from "@/components/Primitives/CollapsibleCard"; +import CollapsibleTags from "@/components/Hack/CollapsibleTags"; import { getHackMetadata, getHackDownloads } from "@/app/hack/[slug]/actions"; interface HackDetailProps { @@ -327,14 +328,8 @@ export default async function HackDetail({ params }: HackDetailProps) { )}

{hack.summary}

-
-
- {tags.map((t) => ( - - {t} - - ))} -
+
+
{!isArchive && (
diff --git a/src/components/Hack/CollapsibleTags.tsx b/src/components/Hack/CollapsibleTags.tsx new file mode 100644 index 0000000..1aa95c7 --- /dev/null +++ b/src/components/Hack/CollapsibleTags.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useState, useRef, useLayoutEffect, useEffect, useCallback } from "react"; +import { FaChevronDown } from "react-icons/fa6"; + +interface CollapsibleTagsProps { + tags: string[]; +} + +// Tag height: ~28px (px-2.5 py-1 text-xs), gap: 8px +// Max height: 2.5 × 28px + 2 × 8px = 86px (rounded to 88px for safety) +const MAX_HEIGHT = 72; + +export default function CollapsibleTags({ tags }: CollapsibleTagsProps) { + const [isExpanded, setIsExpanded] = useState(false); + const [needsExpansion, setNeedsExpansion] = useState(false); + const [naturalHeight, setNaturalHeight] = useState(null); + const contentRef = useRef(null); + + const checkIfExpansionNeeded = useCallback(() => { + if (!contentRef.current) return; + const height = contentRef.current.scrollHeight; + setNaturalHeight(height); + setNeedsExpansion(height > MAX_HEIGHT); + }, []); + + // Use useLayoutEffect to check synchronously before paint + useLayoutEffect(() => { + checkIfExpansionNeeded(); + }, [tags, checkIfExpansionNeeded]); + + // Also check on window resize + useEffect(() => { + window.addEventListener("resize", checkIfExpansionNeeded); + return () => { + window.removeEventListener("resize", checkIfExpansionNeeded); + }; + }, [checkIfExpansionNeeded]); + + if (tags.length === 0) return null; + + return ( +
+
+
+
+ {tags.map((t) => ( + + {t} + + ))} +
+
+
+ {needsExpansion && ( + + )} +
+ ); +}