From 0190fad583573c2597b6ef988913996e1ed6f7bf Mon Sep 17 00:00:00 2001 From: Ahmad <103906421+ahmadk953@users.noreply.github.com> Date: Sat, 11 Jan 2025 01:04:43 -0500 Subject: [PATCH] Added Very Basic Liveblocks Implimentation and README Updates --- README.md | 23 +++--- .../_components/board-liveblocks.tsx | 68 +++++++++++++++++ .../[boardId]/_components/board-navbar.tsx | 2 +- .../_components/board-room-wrapper.tsx | 74 +++++++++++++++++++ .../board/[boardId]/_components/cursor.tsx | 28 +++++++ .../(dashboard)/board/[boardId]/layout.tsx | 11 ++- .../(dashboard)/board/[boardId]/page.tsx | 9 ++- app/api/liveblocks-auth/route.ts | 39 ++++++++++ constants/colors.ts | 10 +++ liveblocks.config.ts | 25 +++++++ package.json | 3 + yarn.lock | 59 ++++++++++++++- 12 files changed, 336 insertions(+), 15 deletions(-) create mode 100644 app/(platform)/(dashboard)/board/[boardId]/_components/board-liveblocks.tsx create mode 100644 app/(platform)/(dashboard)/board/[boardId]/_components/board-room-wrapper.tsx create mode 100644 app/(platform)/(dashboard)/board/[boardId]/_components/cursor.tsx create mode 100644 app/api/liveblocks-auth/route.ts create mode 100644 constants/colors.ts create mode 100644 liveblocks.config.ts diff --git a/README.md b/README.md index cdb7f57..bd354f7 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,20 @@ Documentation can be found [here](https://docs.tasko.ahmadk953.org/). This will be published on the site some time soon but for now, the roadmap will be listed here. -- [x] Finish adding started at date field -- [x] Pagination for Audit Logs page - [ ] Board sorting options (Boards Page) -- [ ] Add real-time collaboration +- [ ] Add real-time collaboration _In Progress - Liveblocks with presence implemented roughly_ - [ ] Add end-to-end Database encryption (for customer data such as card titles and descriptions, and subscription information) -- [ ] Add dark mode -- [ ] Markdown Support in Card Descriptions +- [ ] Add dark mode _In Progress_ +- [ ] Rich Text Support in Card Descriptions - [ ] Self-Hosted Version +- [ ] Move Roadmap to Website _In Progress - Starting off With a Basic MDX Page_ ## Contributing and Development ### Development Commands +Install Dependencies: ``yarn install --immutable`` + Start Dev Server: ``yarn dev`` Production Build: ``yarn build`` @@ -47,12 +48,16 @@ Check Formatting: ``yarn format`` Fix Formatting: ``yarn format:fix`` -Please make sure to lint and check formatting (using the commands listed above) before submitting a Pull Request. +Testing: ``yarn test`` + +Coverage: ``yarn coverage`` + +Please make sure to lint, check formatting, and test (using the commands listed above) before submitting a Pull Request. ## Legal -Privacy Policy _Temporarily removed to revamp it._ +Privacy Policy _Will be added back soon_ -Terms of Service _The Terms of Service will be added soon!_ +Terms of Service _Will be added along with privacy policy_ -[License](https://github.com/ahmadk953/tasko/blob/main/LICENCE) _Will be located on website at some point in time._ +[License](https://github.com/ahmadk953/tasko/blob/main/LICENCE) _Will be added onto the website with privacy policy and terms of service_ diff --git a/app/(platform)/(dashboard)/board/[boardId]/_components/board-liveblocks.tsx b/app/(platform)/(dashboard)/board/[boardId]/_components/board-liveblocks.tsx new file mode 100644 index 0000000..9d4f282 --- /dev/null +++ b/app/(platform)/(dashboard)/board/[boardId]/_components/board-liveblocks.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { + ClientSideSuspense, + LiveblocksProvider, + RoomProvider, +} from '@liveblocks/react'; +import { Skeleton } from '@/components/ui/skeleton'; + +export const BoardLiveblocks = ({ + children, + boardId, +}: { + children: React.ReactNode; + boardId: string; +}) => { + return ( + { + const headers = { + 'Content-Type': 'application/json', + BoardId: `${boardId}`, + }; + + const body = JSON.stringify({ + room, + }); + + const response = await fetch('/api/liveblocks-auth', { + method: 'POST', + headers, + body, + }); + + return await response.json(); + }} + throttle={16} + > + + }> + {children} + + + + ); +}; + +BoardLiveblocks.Skeleton = function SkeletonBoardLiveblocks() { + return ( +
+
    +
  1. +
    +
    + +
    +
      + + + +
    +
    +
  2. +
    +
+
+ ); +}; diff --git a/app/(platform)/(dashboard)/board/[boardId]/_components/board-navbar.tsx b/app/(platform)/(dashboard)/board/[boardId]/_components/board-navbar.tsx index 7083cd2..807c642 100644 --- a/app/(platform)/(dashboard)/board/[boardId]/_components/board-navbar.tsx +++ b/app/(platform)/(dashboard)/board/[boardId]/_components/board-navbar.tsx @@ -7,7 +7,7 @@ interface BoardNavbarProps { data: Board; } -export const BoardNavbar = async ({ data }: BoardNavbarProps) => { +export const BoardNavbar = ({ data }: BoardNavbarProps) => { return (
diff --git a/app/(platform)/(dashboard)/board/[boardId]/_components/board-room-wrapper.tsx b/app/(platform)/(dashboard)/board/[boardId]/_components/board-room-wrapper.tsx new file mode 100644 index 0000000..3b53b77 --- /dev/null +++ b/app/(platform)/(dashboard)/board/[boardId]/_components/board-room-wrapper.tsx @@ -0,0 +1,74 @@ +'use client'; + +import { useOthers, useMyPresence } from '@liveblocks/react/suspense'; +import { Cursor } from './cursor'; +import { colors } from '@/constants/colors'; +import { useEffect, useLayoutEffect, useRef, useState } from 'react'; + +export const BoardRoomWrapper = ({ + children, +}: { + children: React.ReactNode; +}) => { + const others = useOthers(); + const ref = useRef(null); + + const [numbers, setNumbers] = useState([]); + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + const [myPresence, updateMyPresence] = useMyPresence(); + + useLayoutEffect(() => { + if (ref.current) { + setWidth(ref.current.clientWidth); + setHeight(ref.current.clientHeight); + } + }, [numbers]); + + useEffect(() => { + function handleWindowResize() { + if (ref.current) { + setWidth(ref.current.clientWidth); + setHeight(ref.current.clientHeight); + } + } + + window.addEventListener('resize', handleWindowResize); + + return () => { + window.removeEventListener('resize', handleWindowResize); + }; + }, []); + + function handlePointerMove(e: React.PointerEvent) { + if (!ref.current) return; + const rect = ref.current.getBoundingClientRect(); + const normalizedCursorX = (e.clientX - rect.left) / rect.width; + const normalizedCursorY = (e.clientY - rect.top) / rect.height; + updateMyPresence({ + cursor: { x: normalizedCursorX, y: normalizedCursorY }, + }); + } + + function handlePointerLeave(e: React.PointerEvent) { + updateMyPresence({ cursor: null }); + } + return ( +
+ {children} + {others.map(({ connectionId, presence }) => ( + + ))} +
+ ); +}; diff --git a/app/(platform)/(dashboard)/board/[boardId]/_components/cursor.tsx b/app/(platform)/(dashboard)/board/[boardId]/_components/cursor.tsx new file mode 100644 index 0000000..181d9cc --- /dev/null +++ b/app/(platform)/(dashboard)/board/[boardId]/_components/cursor.tsx @@ -0,0 +1,28 @@ +interface CursorProps { + x: number | undefined; + y: number | undefined; + color: string; +} + +export function Cursor({ x, y, color }: CursorProps) { + return ( + + + + ); +} diff --git a/app/(platform)/(dashboard)/board/[boardId]/layout.tsx b/app/(platform)/(dashboard)/board/[boardId]/layout.tsx index e1e5e5c..0c5d647 100644 --- a/app/(platform)/(dashboard)/board/[boardId]/layout.tsx +++ b/app/(platform)/(dashboard)/board/[boardId]/layout.tsx @@ -1,8 +1,15 @@ import { auth } from '@clerk/nextjs/server'; import { notFound, redirect } from 'next/navigation'; +import { + LiveblocksProvider, + RoomProvider, + ClientSideSuspense, +} from '@liveblocks/react/suspense'; import { db } from '@/lib/db'; import { BoardNavbar } from './_components/board-navbar'; +import { Skeleton } from '@/components/ui/skeleton'; +import { BoardLiveblocks } from './_components/board-liveblocks'; export async function generateMetadata(props: { params: Promise<{ boardId: string }>; @@ -53,7 +60,9 @@ const BoardIdLayout = async (props: { >
-
{children}
+
+ {children} +
); }; diff --git a/app/(platform)/(dashboard)/board/[boardId]/page.tsx b/app/(platform)/(dashboard)/board/[boardId]/page.tsx index cabb896..6552124 100644 --- a/app/(platform)/(dashboard)/board/[boardId]/page.tsx +++ b/app/(platform)/(dashboard)/board/[boardId]/page.tsx @@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'; import { db } from '@/lib/db'; import { ListContainer } from './_components/list-container'; +import { BoardRoomWrapper } from './_components/board-room-wrapper'; interface BoardIdPageProps { params: Promise<{ @@ -38,9 +39,11 @@ const BoardIdPage = async (props: BoardIdPageProps) => { }); return ( -
- -
+ +
+ +
+
); }; diff --git a/app/api/liveblocks-auth/route.ts b/app/api/liveblocks-auth/route.ts new file mode 100644 index 0000000..15791d1 --- /dev/null +++ b/app/api/liveblocks-auth/route.ts @@ -0,0 +1,39 @@ +import { db } from '@/lib/db'; + +import { auth, currentUser } from '@clerk/nextjs/server'; +import { Liveblocks } from '@liveblocks/node'; +import { headers } from 'next/headers'; + +export async function POST(req: Request) { + const { sessionClaims } = await auth(); + const user = await currentUser(); + if (!sessionClaims || !user) { + return new Response('Not authorized', { status: 401 }); + } + + const { room } = await req.json(); + const boardId = (await headers()).get('BoardId') as string; + const board = await db.board.findUnique({ + where: { + id: boardId, + orgId: sessionClaims.org_id, + }, + }); + if (!board || board.orgId !== sessionClaims.org_id) { + return new Response('Not authorized', { status: 401 }); + } + + const liveblocks = new Liveblocks({ + secret: process.env.LIVEBLOCKS_SECRET_KEY!, + }); + const session = liveblocks.prepareSession(user.id, { + userInfo: { + name: user.fullName!, + avatar: user.imageUrl, + }, + }); + session.allow(room, session.FULL_ACCESS); + const { body, status } = await session.authorize(); + + return new Response(body, { status }); +} diff --git a/constants/colors.ts b/constants/colors.ts new file mode 100644 index 0000000..6b0d07c --- /dev/null +++ b/constants/colors.ts @@ -0,0 +1,10 @@ +export const colors = [ + '#E57373', + '#9575CD', + '#4FC3F7', + '#81C784', + '#FFF176', + '#FF8A65', + '#F06292', + '#7986CB', +]; diff --git a/liveblocks.config.ts b/liveblocks.config.ts new file mode 100644 index 0000000..cc0cb31 --- /dev/null +++ b/liveblocks.config.ts @@ -0,0 +1,25 @@ +declare global { + interface Liveblocks { + Presence: { + cursor: { x: number; y: number } | null; + }; + + Storage: {}; + + UserMeta: { + id: string; + info: { + name: string; + avatar: string; + }; + }; + + RoomEvent: {}; + + ThreadMetadata: {}; + + RoomInfo: {}; + } +} + +export {}; diff --git a/package.json b/package.json index 3e23d42..0589267 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "@arcjet/next": "^1.0.0-alpha.34", "@clerk/nextjs": "^6.9.7", "@hello-pangea/dnd": "^17.0.0", + "@liveblocks/client": "^2.15.1", + "@liveblocks/node": "^2.15.1", + "@liveblocks/react": "^2.15.1", "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "^15.1.3", diff --git a/yarn.lock b/yarn.lock index f8288d5..ae358d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1773,6 +1773,46 @@ __metadata: languageName: node linkType: hard +"@liveblocks/client@npm:2.15.2, @liveblocks/client@npm:^2.15.1": + version: 2.15.2 + resolution: "@liveblocks/client@npm:2.15.2" + dependencies: + "@liveblocks/core": "npm:2.15.2" + checksum: 10c0/a9ea5ab865b0d1afada8670e3f10ab605f03da2e106e35dc40edbacec257462e723f48f1a0ce8cd1d74182b673fb934f13b14ad900805630072a0952f62d41c8 + languageName: node + linkType: hard + +"@liveblocks/core@npm:2.15.2": + version: 2.15.2 + resolution: "@liveblocks/core@npm:2.15.2" + checksum: 10c0/eadef786899051ab6ceaa9f33020ef6f422bcc1abf4e504bbd8fb724d7ad4f3a97a4e9420cfc9b2988fa6ed71c17781a030dc69b81cb1d4420469d41d2b2fb48 + languageName: node + linkType: hard + +"@liveblocks/node@npm:^2.15.1": + version: 2.15.2 + resolution: "@liveblocks/node@npm:2.15.2" + dependencies: + "@liveblocks/core": "npm:2.15.2" + "@stablelib/base64": "npm:^1.0.1" + fast-sha256: "npm:^1.3.0" + node-fetch: "npm:^2.6.1" + checksum: 10c0/a5b13f2351218f1ba088c867963fd81669b3c13948651177f7186f84a7f66551246c7d8d002938a490c7a3e3ca2f4f2669c405c2141d92a5e64515166e8338b4 + languageName: node + linkType: hard + +"@liveblocks/react@npm:^2.15.1": + version: 2.15.2 + resolution: "@liveblocks/react@npm:2.15.2" + dependencies: + "@liveblocks/client": "npm:2.15.2" + "@liveblocks/core": "npm:2.15.2" + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + checksum: 10c0/ae1ba5473bd555e60e8c8ede1d62f38cdd029cc97f216c3bf765085f2c6f679ca61e5870b06407ac35be52e026af54b19bee7d3d9ccbfc49bde459a5c43e6e2f + languageName: node + linkType: hard + "@mdx-js/esbuild@npm:^3.0.0": version: 3.1.0 resolution: "@mdx-js/esbuild@npm:3.1.0" @@ -3885,6 +3925,13 @@ __metadata: languageName: node linkType: hard +"@stablelib/base64@npm:^1.0.1": + version: 1.0.1 + resolution: "@stablelib/base64@npm:1.0.1" + checksum: 10c0/6330720f021819d19cecfe274111b79a256caa81df478d6b0ae7effc8842b96915b6aeed85926ff05b4d48ec1fc78ad043d928b730ee4e6cc6e8cba6aa097bed + languageName: node + linkType: hard + "@swc/counter@npm:0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -7183,6 +7230,13 @@ __metadata: languageName: node linkType: hard +"fast-sha256@npm:^1.3.0": + version: 1.3.0 + resolution: "fast-sha256@npm:1.3.0" + checksum: 10c0/87f9e4baa7639576cf60a2b6235c9f436e1a1c52323abbd8a705b5bea8355500acf176f2aed0c14f2ecd6d6007e26151461bab2f27b8953bcca8d9d6b76a86e4 + languageName: node + linkType: hard + "fastq@npm:^1.6.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" @@ -10198,7 +10252,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.6.7": +"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -12539,6 +12593,9 @@ __metadata: "@eslint/eslintrc": "npm:^3.1.0" "@eslint/js": "npm:^9.16.0" "@hello-pangea/dnd": "npm:^17.0.0" + "@liveblocks/client": "npm:^2.15.1" + "@liveblocks/node": "npm:^2.15.1" + "@liveblocks/react": "npm:^2.15.1" "@mdx-js/loader": "npm:^3.1.0" "@mdx-js/react": "npm:^3.1.0" "@microsoft/eslint-formatter-sarif": "npm:^3.1.0"