Skip to content

Commit

Permalink
🔄 Merge pull request #15 from steeeee0223/feature/worxpace
Browse files Browse the repository at this point in the history
Setup `liveblocks`
  • Loading branch information
steeeee0223 authored Feb 7, 2024
2 parents 80b918f + 1177747 commit 616bab9
Show file tree
Hide file tree
Showing 16 changed files with 863 additions and 188 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ DB_WORXPACE=''

# EDGESTORE
EDGE_STORE_ACCESS_KEY=''
EDGE_STORE_SECRET_KEY=''
EDGE_STORE_SECRET_KEY=''

# LIVEBLOCKS
LIVEBLOCKS_SECRET_KEY=''
10 changes: 10 additions & 0 deletions apps/worxpace/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,23 @@
"@clerk/nextjs": "^4.29.4",
"@edgestore/react": "^0.1.6",
"@edgestore/server": "^0.1.6",
"@liveblocks/client": "^1.9.7",
"@liveblocks/node": "^1.9.7",
"@liveblocks/react": "^1.9.7",
"@liveblocks/yjs": "^1.9.7",
"@t3-oss/env-nextjs": "^0.7.1",
"@tiptap/extension-collaboration": "^2.2.1",
"@tiptap/extension-collaboration-cursor": "^2.2.1",
"@tiptap/pm": "^2.2.1",
"@tiptap/react": "^2.2.1",
"@tiptap/starter-kit": "^2.2.1",
"next": "^14.0.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-textarea-autosize": "^8.5.3",
"sonner": "^1.3.1",
"superjson": "2.2.1",
"yjs": "^13.6.11",
"zod": "^3.22.4",
"zustand": "^4.5.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,41 @@
"use client";

import { type PropsWithChildren } from "react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, type PropsWithChildren } from "react";
import { useParams, usePathname, useRouter } from "next/navigation";

import { Document } from "@acme/prisma";
import { TreeProvider } from "@acme/ui/components";
import { useNavControl } from "@acme/ui/hooks";

import { Room } from "~/components";
import { fetchUrl } from "~/lib";
import Navbar from "./navbar";
import SearchCommand from "./search-command";
import { Sidebar } from "./sidebar";

const DocsProvider = ({ children }: PropsWithChildren) => {
const router = useRouter();
const params = useParams();
const pathname = usePathname();
/** Sidebar & Navbar */
const {
isMobile,
sidebarRef,
navbarRef,
isResetting,
isCollapsed,
handleMouseDown,
collapse,
resetWidth,
} = useNavControl();

useEffect(() => {
isMobile ? collapse() : resetWidth();
}, [isMobile]);

useEffect(() => {
if (isMobile) collapse();
}, [pathname, isMobile]);
/** Docs */
const onClickItem = (id: string) => router.push(`/documents/${id}`);
const isItemActive = (id: string) => params.documentId === id;
Expand All @@ -32,13 +55,41 @@ const DocsProvider = ({ children }: PropsWithChildren) => {
onClickItem={onClickItem}
isItemActive={isItemActive}
>
<Sidebar />
<main className="h-full flex-1 overflow-y-auto">
<SearchCommand />
{children}
</main>
<RoomWrapper documentId={params.documentId as string | undefined}>
<Sidebar
ref={sidebarRef}
isResetting={isResetting}
isMobile={isMobile}
handleMouseDown={handleMouseDown}
resetWidth={resetWidth}
collapse={collapse}
/>
<Navbar
ref={navbarRef}
isCollapsed={isCollapsed}
isResetting={isResetting}
isMobile={isMobile}
onResetWidth={resetWidth}
/>
<main className="h-full flex-1 overflow-y-auto">
<SearchCommand />
{children}
</main>
</RoomWrapper>
</TreeProvider>
);
};

export default DocsProvider;

const RoomWrapper = ({
children,
documentId,
}: PropsWithChildren<{ documentId?: string | null }>) => {
if (!documentId) return <>{children}</>;
return (
<Room roomId={documentId} fallback={null}>
{children}
</Room>
);
};
51 changes: 31 additions & 20 deletions apps/worxpace/src/app/(platform)/(tools)/_components/navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { ForwardedRef, forwardRef } from "react";
import { useParams } from "next/navigation";
import { MenuIcon } from "lucide-react";

Expand All @@ -9,29 +10,34 @@ import { cn } from "@acme/ui/lib";
import { theme } from "~/constants/theme";
import Banner from "./banner";
import Menu from "./menu";
import Participants from "./participants";
import Publish from "./publish";
import Title from "./title";

interface NavbarProps {
isResetting: boolean;
isMobile: boolean;
isCollapsed: boolean;
onResetWidth: () => void;
}

const Navbar = ({ isCollapsed, onResetWidth }: NavbarProps) => {
const Navbar = forwardRef(function Navbar(
{ isCollapsed, isResetting, isMobile, onResetWidth }: NavbarProps,
ref: ForwardedRef<HTMLDivElement>,
) {
const params = useParams();
const { treeItems } = useTree();
const { treeItems, isLoading } = useTree();
const document = treeItems.find(({ id }) => params.documentId === id);

if (!document)
return (
<nav
className={cn(theme.bg.navbar, theme.flex.center, "w-full px-3 py-2")}
>
<Title.Skeleton />
</nav>
);
return (
<>
<div
ref={ref}
className={cn(
"absolute left-60 top-0 z-[99999] w-[calc(100%-240px)]",
isResetting && "transition-all duration-300 ease-in-out",
isMobile && "left-0 w-full",
)}
>
<nav
className={cn(
theme.bg.navbar,
Expand All @@ -46,17 +52,22 @@ const Navbar = ({ isCollapsed, onResetWidth }: NavbarProps) => {
className="h-6 w-6 text-muted-foreground"
/>
)}
<div className={cn(theme.flex.center, "w-full justify-between")}>
<Title initialData={document} />
<div className={theme.flex.gap2}>
<Publish documentId={document.id} />
<Menu documentId={document.id} />
{!document || isLoading ? (
<Title.Skeleton />
) : (
<div className={cn(theme.flex.center, "w-full justify-between")}>
<Title initialData={document} />
<div className={theme.flex.gap2}>
<Participants />
<Publish documentId={document.id} />
<Menu documentId={document.id} />
</div>
</div>
</div>
)}
</nav>
{document.isArchived && <Banner documentId={document.id} />}
</>
{document?.isArchived && <Banner documentId={document.id} />}
</div>
);
};
});

export default Navbar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { MAX_SHOWN_USERS } from "~/constants/site";
import { connectionIdToColor } from "~/lib";
import { useOthers, useSelf } from "~/liveblocks.config";
import UserAvatar from "./user-avatar";

const Participants = () => {
const currentUser = useSelf();
const otherUsers = useOthers();
const hasMoreUsers = otherUsers.length > MAX_SHOWN_USERS;

return (
<div className="flex">
{currentUser && (
<UserAvatar
borderColor={connectionIdToColor(currentUser.connectionId)}
src={currentUser.info?.picture}
name={`${currentUser.info?.name} (You)`}
fallback={currentUser.info?.name?.[0]}
className="ml-[-8px]"
/>
)}
{otherUsers.slice(0, MAX_SHOWN_USERS).map(({ connectionId, info }) => (
<UserAvatar
borderColor={connectionIdToColor(connectionId)}
key={connectionId}
src={info?.picture}
name={info?.name}
fallback={info?.name?.[0] ?? "T"}
className="ml-[-8px]"
/>
))}
{hasMoreUsers && (
<UserAvatar
name={`${otherUsers.length - MAX_SHOWN_USERS} more`}
fallback={`+${otherUsers.length - MAX_SHOWN_USERS}`}
className="ml-[-8px]"
/>
)}
</div>
);
};

export default Participants;
67 changes: 19 additions & 48 deletions apps/worxpace/src/app/(platform)/(tools)/_components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,36 @@
/* eslint-disable jsx-a11y/interactive-supports-focus */
"use client";

import { useEffect } from "react";
import { useParams, usePathname } from "next/navigation";
import { ChevronsLeft, MenuIcon } from "lucide-react";
import { forwardRef, type ForwardedRef, type MouseEventHandler } from "react";
import { ChevronsLeft } from "lucide-react";

import { useNavControl } from "@acme/ui/hooks";
import { cn } from "@acme/ui/lib";

import { theme } from "~/constants/theme";
import DocList from "./doc-list";
import Navbar from "./navbar";

export const Sidebar = () => {
const pathname = usePathname();
const params = useParams();
const {
isMobile,
sidebarRef,
navbarRef,
interface SidebarProps {
isResetting: boolean;
isMobile: boolean;
handleMouseDown: MouseEventHandler<HTMLDivElement>;
resetWidth: () => void;
collapse: () => void;
}

export const Sidebar = forwardRef(function Sidebar(
{
isResetting,
isCollapsed,
isMobile,
handleMouseDown,
collapse,
resetWidth,
} = useNavControl();

useEffect(() => {
isMobile ? collapse() : resetWidth();
}, [isMobile]);

useEffect(() => {
if (isMobile) collapse();
}, [pathname, isMobile]);

collapse,
}: SidebarProps,
ref: ForwardedRef<HTMLElement>,
) {
return (
<>
<aside
ref={sidebarRef}
ref={ref}
className={cn(
"group/sidebar relative z-[99999] flex h-full w-60 flex-col overflow-y-auto bg-secondary",
isResetting && "transition-all duration-300 ease-in-out",
Expand All @@ -64,28 +57,6 @@ export const Sidebar = () => {
className="absolute right-0 top-0 h-full w-1 cursor-ew-resize bg-primary/10 opacity-0 transition group-hover/sidebar:opacity-100"
/>
</aside>
<div
ref={navbarRef}
className={cn(
"absolute left-60 top-0 z-[99999] w-[calc(100%-240px)]",
isResetting && "transition-all duration-300 ease-in-out",
isMobile && "left-0 w-full",
)}
>
{params.documentId ? (
<Navbar isCollapsed={isCollapsed} onResetWidth={resetWidth} />
) : (
<nav className="w-full bg-transparent px-3 py-2">
{isCollapsed && (
<MenuIcon
onClick={resetWidth}
role="button"
className="h-6 w-6 text-muted-foreground"
/>
)}
</nav>
)}
</div>
</>
);
};
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Avatar, AvatarFallback, AvatarImage, Hint } from "@acme/ui/components";
import { cn } from "@acme/ui/lib";

interface UserAvatarProps {
src?: string;
name?: string;
fallback?: string;
borderColor?: string;
className?: string;
}

const UserAvatar = ({
src,
name,
fallback,
borderColor,
className,
}: UserAvatarProps) => {
return (
<Hint
asChild
description={name ?? "Teammate"}
side="bottom"
sideOffset={18}
>
<Avatar
className={cn("h-6 w-6 border-2", className)}
style={{ borderColor }}
>
<AvatarImage src={src} />
<AvatarFallback className="text-xs font-semibold">
{fallback}
</AvatarFallback>
</Avatar>
</Hint>
);
};

export default UserAvatar;
Loading

0 comments on commit 616bab9

Please sign in to comment.