[dyad] Add collapse/expand all to headlines - wrote 1 file(s)
This commit is contained in:
@@ -6,24 +6,48 @@ import type { HeadlineNode } from "@/app/actions";
|
|||||||
import { KeywordHighlighter } from "./keyword-highlighter";
|
import { KeywordHighlighter } from "./keyword-highlighter";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
interface HeadlineTreeProps {
|
interface HeadlineTreeProps {
|
||||||
headlines: HeadlineNode[];
|
headlines: HeadlineNode[];
|
||||||
keyword?: string | null;
|
keyword?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCollapsiblePaths = (
|
||||||
|
nodes: HeadlineNode[],
|
||||||
|
parentPath = ""
|
||||||
|
): string[] => {
|
||||||
|
let paths: string[] = [];
|
||||||
|
nodes.forEach((node, index) => {
|
||||||
|
const currentPath = parentPath ? `${parentPath}-${index}` : `${index}`;
|
||||||
|
if (node.level === 2 && node.children && node.children.length > 0) {
|
||||||
|
paths.push(currentPath);
|
||||||
|
}
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
paths = paths.concat(getCollapsiblePaths(node.children, currentPath));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return paths;
|
||||||
|
};
|
||||||
|
|
||||||
const HeadlineNodeDisplay = ({
|
const HeadlineNodeDisplay = ({
|
||||||
node,
|
node,
|
||||||
level,
|
level,
|
||||||
|
path,
|
||||||
keyword,
|
keyword,
|
||||||
|
collapsedStates,
|
||||||
|
onToggle,
|
||||||
}: {
|
}: {
|
||||||
node: HeadlineNode;
|
node: HeadlineNode;
|
||||||
level: number;
|
level: number;
|
||||||
|
path: string;
|
||||||
keyword?: string | null;
|
keyword?: string | null;
|
||||||
|
collapsedStates: Record<string, boolean>;
|
||||||
|
onToggle: (path: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const isCollapsible =
|
const isCollapsible =
|
||||||
node.level === 2 && node.children && node.children.length > 0;
|
node.level === 2 && node.children && node.children.length > 0;
|
||||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
const isCollapsed = collapsedStates[path];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -33,7 +57,7 @@ const HeadlineNodeDisplay = ({
|
|||||||
isCollapsible && "cursor-pointer hover:bg-muted/50"
|
isCollapsible && "cursor-pointer hover:bg-muted/50"
|
||||||
)}
|
)}
|
||||||
style={{ paddingLeft: `${16 + level * 24}px` }}
|
style={{ paddingLeft: `${16 + level * 24}px` }}
|
||||||
onClick={() => isCollapsible && setIsCollapsed(!isCollapsed)}
|
onClick={() => isCollapsible && onToggle(path)}
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -63,7 +87,10 @@ const HeadlineNodeDisplay = ({
|
|||||||
key={index}
|
key={index}
|
||||||
node={child}
|
node={child}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
|
path={`${path}-${index}`}
|
||||||
keyword={keyword}
|
keyword={keyword}
|
||||||
|
collapsedStates={collapsedStates}
|
||||||
|
onToggle={onToggle}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@@ -71,15 +98,64 @@ const HeadlineNodeDisplay = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function HeadlineTree({ headlines, keyword }: HeadlineTreeProps) {
|
export function HeadlineTree({ headlines, keyword }: HeadlineTreeProps) {
|
||||||
|
const [collapsedStates, setCollapsedStates] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>(() => {
|
||||||
|
const initialStates: Record<string, boolean> = {};
|
||||||
|
const paths = getCollapsiblePaths(headlines);
|
||||||
|
paths.forEach((path) => {
|
||||||
|
initialStates[path] = true;
|
||||||
|
});
|
||||||
|
return initialStates;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggle = (path: string) => {
|
||||||
|
setCollapsedStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[path]: !prev[path],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const collapseAll = () => {
|
||||||
|
const newStates: Record<string, boolean> = {};
|
||||||
|
Object.keys(collapsedStates).forEach((path) => {
|
||||||
|
newStates[path] = true;
|
||||||
|
});
|
||||||
|
setCollapsedStates(newStates);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandAll = () => {
|
||||||
|
const newStates: Record<string, boolean> = {};
|
||||||
|
Object.keys(collapsedStates).forEach((path) => {
|
||||||
|
newStates[path] = false;
|
||||||
|
});
|
||||||
|
setCollapsedStates(newStates);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasCollapsibleNodes = Object.keys(collapsedStates).length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full rounded-lg overflow-hidden">
|
<div className="w-full rounded-lg overflow-hidden">
|
||||||
|
{hasCollapsibleNodes && (
|
||||||
|
<div className="flex items-center gap-2 p-4 border-b bg-muted/50">
|
||||||
|
<Button variant="outline" size="sm" onClick={expandAll}>
|
||||||
|
Expand All
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={collapseAll}>
|
||||||
|
Collapse All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="-mt-px">
|
<div className="-mt-px">
|
||||||
{headlines.map((headline, index) => (
|
{headlines.map((headline, index) => (
|
||||||
<HeadlineNodeDisplay
|
<HeadlineNodeDisplay
|
||||||
key={index}
|
key={index}
|
||||||
node={headline}
|
node={headline}
|
||||||
level={0}
|
level={0}
|
||||||
|
path={`${index}`}
|
||||||
keyword={keyword}
|
keyword={keyword}
|
||||||
|
collapsedStates={collapsedStates}
|
||||||
|
onToggle={handleToggle}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user