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 (
+
+ );
+};
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"