Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import styles from "./CategoryTreeView.module.css";
|
||||
|
||||
export type CategoryTreeNode = {
|
||||
id: string;
|
||||
name: string;
|
||||
hint?: string;
|
||||
children: CategoryTreeNode[];
|
||||
};
|
||||
|
||||
export type CategoryTreeActions = {
|
||||
onAddChild?: (id: string) => void;
|
||||
onEdit?: (id: string) => void;
|
||||
onRemove?: (id: string) => void;
|
||||
};
|
||||
|
||||
export default function CategoryTreeView({
|
||||
nodes,
|
||||
emptyLabel = "No categories yet",
|
||||
actions,
|
||||
}: {
|
||||
nodes: CategoryTreeNode[];
|
||||
emptyLabel?: string;
|
||||
actions?: CategoryTreeActions;
|
||||
}) {
|
||||
if (!nodes.length) {
|
||||
return <p className={styles.empty}>{emptyLabel}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={styles.list}>
|
||||
{nodes.map((node) => (
|
||||
<TreeBranch key={node.id} node={node} depth={0} actions={actions} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function TreeBranch({
|
||||
node,
|
||||
depth,
|
||||
actions,
|
||||
}: {
|
||||
node: CategoryTreeNode;
|
||||
depth: number;
|
||||
actions?: CategoryTreeActions;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const childCount = node.children.length;
|
||||
const previousChildCount = useRef(childCount);
|
||||
const hasChildren = childCount > 0;
|
||||
const hasActions = Boolean(
|
||||
actions?.onAddChild || actions?.onEdit || actions?.onRemove,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (childCount > previousChildCount.current) {
|
||||
setOpen(true);
|
||||
}
|
||||
previousChildCount.current = childCount;
|
||||
}, [childCount]);
|
||||
|
||||
return (
|
||||
<li className={styles.node}>
|
||||
<div
|
||||
className={styles.row}
|
||||
style={{ paddingInlineStart: `${12 + depth * 56}px` }}
|
||||
>
|
||||
{hasChildren ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.expand} ${open ? styles.expandOpen : ""}`}
|
||||
aria-expanded={open}
|
||||
aria-label={open ? "Collapse" : "Expand"}
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
<span aria-hidden>{open ? "−" : "+"}</span>
|
||||
</button>
|
||||
) : (
|
||||
<span className={styles.expandSpacer} aria-hidden />
|
||||
)}
|
||||
<span className={styles.name} dir="auto">
|
||||
{node.name}
|
||||
</span>
|
||||
{node.hint ? <span className={styles.hint}>{node.hint}</span> : null}
|
||||
{hasActions ? (
|
||||
<div className={styles.actions}>
|
||||
{actions?.onAddChild ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => actions.onAddChild?.(node.id)}
|
||||
>
|
||||
Add sub
|
||||
</button>
|
||||
) : null}
|
||||
{actions?.onEdit ? (
|
||||
<button type="button" onClick={() => actions.onEdit?.(node.id)}>
|
||||
Edit
|
||||
</button>
|
||||
) : null}
|
||||
{actions?.onRemove ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.danger}
|
||||
onClick={() => actions.onRemove?.(node.id)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{hasChildren && open ? (
|
||||
<ul className={styles.list}>
|
||||
{node.children.map((child) => (
|
||||
<TreeBranch
|
||||
key={child.id}
|
||||
node={child}
|
||||
depth={depth + 1}
|
||||
actions={actions}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user