Skip to content

Commit

Permalink
feat: space 화면 Header UI 구현 (#144)
Browse files Browse the repository at this point in the history
* chore: breadcrumb 컴포넌트 설치

* chore: dropdown menu 컴포넌트 설치

* feat: space breadcrumb 컴포넌트 작성

- 데이터를 가져오는 것에 따라 추가 수정 필요

* feat: space 페이지의 header도 yjs connection에 접근할 수 있도록 yjs provider 끌어올림

* chore: lockfile 재생성

* fix: 중복 로직 삭제

* refactor: breadcrumb 보여줄 아이템 계산 로직 가독성 개선
  • Loading branch information
hoqn authored Nov 28, 2024
1 parent 0e1ec50 commit aa044d8
Show file tree
Hide file tree
Showing 9 changed files with 752 additions and 45 deletions.
1 change: 1 addition & 0 deletions packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@milkdown/theme-nord": "^7.5.0",
"@prosemirror-adapter/react": "^0.2.6",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function App() {
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/space/:entrySpaceId" element={<SpacePage />} />
<Route path="/space/:spaceId" element={<SpacePage />} />
<Route path="/note/:noteId" element={<Editor />} />
</Routes>
</BrowserRouter>
Expand Down
124 changes: 124 additions & 0 deletions packages/frontend/src/components/SpaceBreadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Link } from "react-router-dom";

import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "./ui/breadcrumb";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";

type SpacePath = {
name: string;
urlPath: string;
};

function splitSpacePaths(spacePaths: SpacePath[], itemCountToDisplay: number) {
// 처음 스페이스는 무조건 보여준다.
const firstSpacePath = spacePaths[0];

// 중간 스페이스들은 ...으로 표시하고, 클릭 시 드롭다운 메뉴로 보여준다.
const hiddenSpacePaths = spacePaths.slice(1, -2);

// 마지막 (n-1)개 스페이스는 무조건 보여준다.
const lastItemCount = Math.min(spacePaths.length, itemCountToDisplay - 1);
const shownSpacePaths = spacePaths.slice(-lastItemCount);

return [firstSpacePath, hiddenSpacePaths, shownSpacePaths] as const;
}

type HiddenItemsProps = {
spacePaths: SpacePath[];
};

function HiddenItems({ spacePaths }: HiddenItemsProps) {
return (
<>
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger>
<BreadcrumbEllipsis />
</DropdownMenuTrigger>
<DropdownMenuContent>
{spacePaths.map(({ name, urlPath }) => (
<DropdownMenuItem key={urlPath} asChild>
<Link to={`/space/${urlPath}`}>{name}</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
<BreadcrumbSeparator />
</>
);
}

type SpaceBreadcrumbItemProps = {
spacePath: SpacePath;
isPage?: boolean;
};

function SpaceBreadcrumbItem({ spacePath, isPage }: SpaceBreadcrumbItemProps) {
if (isPage) {
return (
<BreadcrumbItem>
<BreadcrumbPage className="truncate max-w-20">
{spacePath.name}
</BreadcrumbPage>
</BreadcrumbItem>
);
}

return (
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link className="truncate max-w-20" to={`/space/${spacePath.urlPath}`}>
{spacePath.name}
</Link>
</BreadcrumbLink>
<BreadcrumbSeparator />
</BreadcrumbItem>
);
}

type SpaceBreadcrumbProps = {
spacePaths: SpacePath[];
itemCountToDisplay?: number;
};

export default function SpaceBreadcrumb({
spacePaths,
itemCountToDisplay = 3,
}: SpaceBreadcrumbProps) {
// [처음, (...중간...), 직전, 현재]
const [firstSpacePath, hiddenSpacePaths, shownSpacePaths] = splitSpacePaths(
spacePaths,
itemCountToDisplay,
);

return (
<Breadcrumb>
<BreadcrumbList>
{firstSpacePath && <SpaceBreadcrumbItem spacePath={firstSpacePath} />}
{hiddenSpacePaths.length > 0 && (
<HiddenItems spacePaths={hiddenSpacePaths} />
)}
{shownSpacePaths.map((spacePath, index) => (
<SpaceBreadcrumbItem
key={spacePath.urlPath}
spacePath={spacePath}
isPage={index === shownSpacePaths.length - 1}
/>
))}
</BreadcrumbList>
</Breadcrumb>
);
}
33 changes: 33 additions & 0 deletions packages/frontend/src/components/space/SpacePageHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import SpaceBreadcrumb from "../SpaceBreadcrumb";
import { Button } from "../ui/button";

export default function SpacePageHeader() {
return (
<header className="fixed z-20 top-0 inset-x-0 h-16 bg-background/50 backdrop-blur-lg">
<div className="container mx-auto px-6 h-full flex flex-row items-center justify-between">
<div className="flex-1">
<SpaceBreadcrumb
spacePaths={[
{ name: "하나", urlPath: "1" },
{ name: "셋", urlPath: "3" },
{ name: "넷", urlPath: "4" },
{ name: "다섯", urlPath: "5" },
{ name: "여섯", urlPath: "6" },
{ name: "일곱", urlPath: "7" },
{ name: "여덟", urlPath: "8" },
{ name: "아홉", urlPath: "9" },
{
name: "엄청 긴 제목을 가진 스페이스다아아아아아아아아아아아아아",
urlPath: "2",
},
{ name: "열", urlPath: "10" },
]}
/>
</div>
<div className="flex-0">
<Button variant="outline">공유</Button>
</div>
</div>
</header>
);
}
24 changes: 0 additions & 24 deletions packages/frontend/src/components/space/YjsSpaceView.tsx

This file was deleted.

115 changes: 115 additions & 0 deletions packages/frontend/src/components/ui/breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"

import { cn } from "@/lib/utils"

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,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
Loading

0 comments on commit aa044d8

Please sign in to comment.