Fuma studio

PageTree

Installation

npx shadcn@latest add http://fumastudio/com/r/page-tree.json
components/fumastudio/page-tree.tsx
"use client";

import { colors } from "@/components/fumastudio/colors";
import { Tree, TreeItem, TreeItemLabel } from "@/components/fumastudio/tree";
import {
	Sidebar,
	SidebarContent,
	SidebarGroup,
	SidebarHeader,
	SidebarMenu,
	SidebarMenuItem,
	SidebarRail,
} from "@/components/ui/sidebar";
import { syncDataLoaderFeature } from "@headless-tree/core";
import { useTree } from "@headless-tree/react";
import { type ClientFiletree, ClientSidebarItem } from "@trythis/nextjs";
import { type PageEntry } from "@trythis/nextjs/utils";
import { Loader2 } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React, { CSSProperties } from "react";

const PAGE_TREE_INDENT = 15;
const SIDEBAR_WIDTH = "17rem";

const Loader = () => (
	<div className="flex items-center w-full bg-red-400">
		<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
	</div>
);

export type PageTreeProps = React.ComponentProps<typeof Sidebar> & {
	tree: ClientFiletree;
};

export function PageTree({ ...props }: PageTreeProps) {
	const items = props?.tree;
	const pathName = usePathname();
	const rootItemId = "/";
	const tree = useTree<ClientSidebarItem>({
		rootItemId,
		initialState: { expandedItems: Object.keys(items) },
		getItemName: (item) => item.getItemData().name,
		isItemFolder: (item) => item.getItemData().type === "folder",
		dataLoader: {
			getItem: (slug) => items![slug] || {},
			getChildren: (itemId) => {
				if (!itemId || !items) return [];

				const entry = items![itemId];
				return entry?.type === "folder" ? entry.children : [];
			},
		},
		features: [syncDataLoaderFeature],
	});

	return (
		<Sidebar
			style={
				{
					"--sidebar-width": SIDEBAR_WIDTH,
				} as CSSProperties
			}
			{...props}>
			{!props.tree && <Loader />}

			{props.tree && (
				<>
					<SidebarHeader className="bg-whitex">
						<SidebarMenu>
							<SidebarMenuItem>
								<div className="flex items-center h-16 p-3 gap-x-3">
									<div className="flex items-center justify-center rounded-xl bg-white/5 p-1.5 shadow-sm">
										<Image
											src="https://github.com/usebruno/bruno/blob/main/assets/images/logo.png?raw=true"
											alt="Bruno Logo"
											width={40}
											height={40}
											className="object-contain"
										/>
									</div>

									<div className="flex flex-col leading-tight">
										<span className="text-lg font-semibold tracking-tight">
											Bruno
										</span>
										<span className="text-xs text-muted-foreground">
											Reinventing
											the API Client
										</span>
									</div>
								</div>
							</SidebarMenuItem>
						</SidebarMenu>
					</SidebarHeader>
					<SidebarContent className="flex flex-col">
						<SidebarGroup className="overflow-y-scroll scrollbar-hide">
							<div className="flex h-full scroll-auto flex-col gap-2 first:*:grow font-inter">
								<Tree
									indent={PAGE_TREE_INDENT}
									tree={tree}
									className="before:-ms-1 relative before:absolute before:inset-0 before:bg-[repeating-linear-gradient(to_right,transparent_0,transparent_calc(var(--tree-indent)-1px),var(--border)_calc(var(--tree-indent)-1px),var(--border)_calc(var(--tree-indent)))]">
									{tree
										.getItems()
										.map((item) => {
											const name =
												item.getItemName();
											const data =
												item.getItemData();

											const slug =
												data.slug ||
												"/";

											const isActive =
												(slug ===
													pathName ||
													pathName.includes(
														slug,
													) ||
													pathName.endsWith(
														slug,
													)) &&
												!item.isFolder();

											const showLabel =
												item.isLoading() ||
												!!name;

											const method =
												(
													data as PageEntry
												)
													?.method;

											let truncatedMethod:
												| string
												| null =
												"";

											if (
												typeof method ===
													"string" &&
												method ===
													"DELETE"
											) {
												truncatedMethod =
													"DEL";
											} else {
												truncatedMethod =
													method;
											}

											return (
												<TreeItem
													item={
														item
													}
													key={item.getId()}>
													<TreeItemLabel
														chevronPosition="right"
														className={`${isActive ? "bg-accent" : ""} ${
															showLabel
																? "before:-inset-y-0.5 before:-z-10 relative before:absolute before:inset-x-0 before:bg-background"
																: "hidden"
														}`}>
														<div className="flex items-center w-full -order-1 text-muted-foreground">
															{name &&
																item.isFolder() && (
																	<div className="flex items-center gap-2">
																		{
																			name
																		}
																	</div>
																)}

															{name &&
																!item.isFolder() && (
																	<Link
																		href={
																			slug
																		}
																		className="w-full">
																		<div className="flex items-center justify-between gap-2 w-full">
																			<span className="text-left">
																				{
																					name
																				}
																			</span>

																			<span
																				className={`flex items-center justify-center w-9 h-4 px-1 rounded-md text-xs leading-tight font-bold ${colors(method, { tryit: true })}`}>
																				{
																					truncatedMethod
																				}
																			</span>
																		</div>
																	</Link>
																)}
														</div>
													</TreeItemLabel>
												</TreeItem>
											);
										})}
								</Tree>
							</div>
						</SidebarGroup>
					</SidebarContent>
					<SidebarRail />
				</>
			)}
		</Sidebar>
	);
}

Props

On this page