Add breadcrumbs
This commit is contained in:
@@ -7,13 +7,21 @@ import { Repertoire } from "@/routes/Repertoire";
|
||||
import { Repertoires } from "@/routes/Repertoires";
|
||||
import { Root } from "@/routes/Root";
|
||||
import { Settings } from "@/routes/Settings";
|
||||
import { lazy, StrictMode } from "react";
|
||||
import { AttachmentId, PieceId, RepertoireId } from "common";
|
||||
import { Effect, pipe } from "effect";
|
||||
import { ListMusic, Music3, SettingsIcon, Users } from "lucide-react";
|
||||
import { lazy, ReactNode, StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import { createBrowserRouter, Params, RouterProvider } from "react-router-dom";
|
||||
import { pieceCache, repertoireCache } from "./cache";
|
||||
import "./style.css";
|
||||
|
||||
const Attachment = lazy(() => import("@/routes/Attachment"));
|
||||
|
||||
export interface Handle {
|
||||
readonly breadcrumb?: (params: Params) => Effect.Effect<ReactNode>;
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
@@ -25,6 +33,12 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "piece",
|
||||
handle: {
|
||||
breadcrumb: () => Effect.succeed(<>
|
||||
<Music3 className="size-4" absoluteStrokeWidth />
|
||||
<div className="self-baseline">Utwory</div>
|
||||
</>),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -32,6 +46,13 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: ":pieceId",
|
||||
handle: {
|
||||
breadcrumb: (params: Params) => pipe(
|
||||
PieceId.make(params.pieceId!),
|
||||
pieceCache.get,
|
||||
Effect.map((piece) => piece.name),
|
||||
),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -39,6 +60,17 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "attachment/:attachmentId",
|
||||
handle: {
|
||||
breadcrumb: (params: Params) => Effect.gen(function* () {
|
||||
const pieceId = PieceId.make(params.pieceId!);
|
||||
const attachmentId = AttachmentId.make(params.attachmentId!);
|
||||
|
||||
const piece = yield* pieceCache.get(pieceId);
|
||||
const attachment = piece.attachments.find((attachment) => attachment.attachmentId === attachmentId)!;
|
||||
|
||||
return attachment.filename;
|
||||
}),
|
||||
},
|
||||
Component: Attachment,
|
||||
},
|
||||
],
|
||||
@@ -47,6 +79,12 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: "repertoire",
|
||||
handle: {
|
||||
breadcrumb: () => Effect.succeed(<>
|
||||
<ListMusic className="size-4" absoluteStrokeWidth />
|
||||
<div className="self-baseline">Repertuary</div>
|
||||
</>),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -54,16 +92,35 @@ const router = createBrowserRouter([
|
||||
},
|
||||
{
|
||||
path: ":repertoireId",
|
||||
handle: {
|
||||
breadcrumb: (params: Params) => pipe(
|
||||
RepertoireId.make(params.repertoireId!),
|
||||
repertoireCache.get,
|
||||
Effect.map((repertoire) => repertoire.name),
|
||||
),
|
||||
},
|
||||
Component: Repertoire,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
handle: {
|
||||
breadcrumb: () => Effect.succeed(<>
|
||||
<SettingsIcon className="size-4" absoluteStrokeWidth />
|
||||
<div className="self-baseline">Ustawienia</div>
|
||||
</>),
|
||||
},
|
||||
Component: Settings,
|
||||
},
|
||||
{
|
||||
path: "admin",
|
||||
handle: {
|
||||
breadcrumb: () => Effect.succeed(<>
|
||||
<Users className="size-4" absoluteStrokeWidth />
|
||||
<div className="self-baseline">Administracja</div>
|
||||
</>),
|
||||
},
|
||||
Component: Admin,
|
||||
}
|
||||
],
|
||||
|
||||
111
packages/frontend/src/components/ui/breadcrumb.tsx
Normal file
111
packages/frontend/src/components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"nav"> & {
|
||||
separator?: React.ReactNode
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
|
||||
Breadcrumb.displayName = "Breadcrumb";
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<"ol">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbList.displayName = "BreadcrumbList";
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem";
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & {
|
||||
asChild?: boolean
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink";
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) => (
|
||||
<li
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
|
||||
|
||||
export {
|
||||
Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem,
|
||||
BreadcrumbLink, BreadcrumbList, BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
};
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { type Handle } from "@/app";
|
||||
import { API_URL_PREFIX, client } from "@/client";
|
||||
import { Avatar, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { useLoading } from "@/hooks/useLoading";
|
||||
import { setUser, useStore } from "@/hooks/useStore";
|
||||
import { Role } from "common/the_api";
|
||||
import { Effect, HashSet, Option, pipe } from "effect";
|
||||
import { Array, Effect, HashSet, Option, pipe } from "effect";
|
||||
import { apply } from "effect/Function";
|
||||
import { LogOut, Settings, Users } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { Link, Outlet, useNavigate } from "react-router-dom";
|
||||
import { Fragment, useEffect, useMemo } from "react";
|
||||
import { Link, Outlet, UIMatch, useMatches, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
export function Root() {
|
||||
|
||||
@@ -49,9 +53,12 @@ export function Root() {
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden flex flex-col items-stretch">
|
||||
<div className="flex p-4 justify-between items-center border-b">
|
||||
<Link className={buttonVariants({ variant: "outline" })} to="/">
|
||||
Repozytorium muzyczne
|
||||
</Link>
|
||||
<div className="flex gap-4 items-baseline">
|
||||
<Link className={buttonVariants({ variant: "outline" })} to="/">
|
||||
Repozytorium muzyczne
|
||||
</Link>
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Avatar className="shadow">
|
||||
@@ -81,3 +88,44 @@ export function Root() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Breadcrumbs() {
|
||||
|
||||
const params = useParams();
|
||||
const matches = useMatches() as UIMatch<unknown, Handle | undefined>[];
|
||||
|
||||
const breadcrumbEffect = useMemo(() => pipe(
|
||||
matches,
|
||||
Array.filterMap(({ handle, pathname }) => pipe(
|
||||
handle,
|
||||
Option.fromNullable,
|
||||
Option.flatMapNullable((handle) => handle.breadcrumb),
|
||||
Option.map(apply(params)),
|
||||
Option.map(Effect.map((title) => ({ title, pathname })))
|
||||
)),
|
||||
Effect.allWith({ concurrency: "unbounded" })
|
||||
), [params, matches]);
|
||||
|
||||
const { data: breadcrumbs } = useLoading(breadcrumbEffect, [breadcrumbEffect]);
|
||||
|
||||
return (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{breadcrumbs !== null && breadcrumbs?.map(({ title, pathname }, i) => (
|
||||
<Fragment key={i}>
|
||||
<BreadcrumbItem>
|
||||
{i < breadcrumbs.length - 1 ? (
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={pathname} className="flex items-center gap-2">{title}</Link>
|
||||
</BreadcrumbLink>
|
||||
) : (
|
||||
<BreadcrumbPage className="flex items-center gap-2">{title}</BreadcrumbPage>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
{i < breadcrumbs.length - 1 && <BreadcrumbSeparator />}
|
||||
</Fragment>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user