mirror of
https://github.com/ahmadk953/tasko.git
synced 2025-05-01 03:09:34 +00:00
Added Very Basic Liveblocks Implimentation and README Updates
This commit is contained in:
parent
5a76ac0611
commit
0190fad583
12 changed files with 336 additions and 15 deletions
|
@ -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 (
|
||||
<LiveblocksProvider
|
||||
authEndpoint={async (room) => {
|
||||
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}
|
||||
>
|
||||
<RoomProvider id={`${boardId}`} initialPresence={{ cursor: null }}>
|
||||
<ClientSideSuspense fallback={<BoardLiveblocks.Skeleton />}>
|
||||
{children}
|
||||
</ClientSideSuspense>
|
||||
</RoomProvider>
|
||||
</LiveblocksProvider>
|
||||
);
|
||||
};
|
||||
|
||||
BoardLiveblocks.Skeleton = function SkeletonBoardLiveblocks() {
|
||||
return (
|
||||
<div className='h-full overflow-x-auto p-4'>
|
||||
<ol className='flex h-full gap-x-3'>
|
||||
<li className='h-full w-[272px] shrink-0 select-none'>
|
||||
<div className='w-full rounded-md bg-[#f1f2f4] pb-2 shadow-md'>
|
||||
<div className='flex items-start justify-between gap-x-2 px-2 pt-2 text-sm font-semibold'>
|
||||
<Skeleton className='h-7 truncate border-transparent bg-transparent px-[7px] py-1 text-sm font-medium' />
|
||||
</div>
|
||||
<ol className='mx-1 mt-2 flex flex-col gap-y-2 px-1 py-0.5'>
|
||||
<Skeleton className='h-12 space-y-2 truncate rounded-md border-2 border-transparent bg-white px-3 py-2 text-sm shadow-sm' />
|
||||
<Skeleton className='h-24 space-y-2 truncate rounded-md border-2 border-transparent bg-white px-3 py-2 text-sm shadow-sm' />
|
||||
<Skeleton className='h-16 space-y-2 truncate rounded-md border-2 border-transparent bg-white px-3 py-2 text-sm shadow-sm' />
|
||||
</ol>
|
||||
</div>
|
||||
</li>
|
||||
<div className='w-1 flex-shrink-0' />
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -7,7 +7,7 @@ interface BoardNavbarProps {
|
|||
data: Board;
|
||||
}
|
||||
|
||||
export const BoardNavbar = async ({ data }: BoardNavbarProps) => {
|
||||
export const BoardNavbar = ({ data }: BoardNavbarProps) => {
|
||||
return (
|
||||
<div className='fixed top-14 z-[40] flex h-14 w-full items-center gap-x-4 bg-black/50 px-6 text-white'>
|
||||
<BoardTitleForm data={data} />
|
||||
|
|
|
@ -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<HTMLDivElement>(null);
|
||||
|
||||
const [numbers, setNumbers] = useState<number[]>([]);
|
||||
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<HTMLDivElement>) {
|
||||
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<HTMLDivElement>) {
|
||||
updateMyPresence({ cursor: null });
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className='relative h-full w-full'
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerLeave={handlePointerLeave}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
{others.map(({ connectionId, presence }) => (
|
||||
<Cursor
|
||||
key={connectionId}
|
||||
x={(presence.cursor?.x as number) * width}
|
||||
y={(presence.cursor?.y as number) * height}
|
||||
color={colors[connectionId % colors.length]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
interface CursorProps {
|
||||
x: number | undefined;
|
||||
y: number | undefined;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export function Cursor({ x, y, color }: CursorProps) {
|
||||
return (
|
||||
<svg
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: `translateX(${x}px) translateY(${y}px)`,
|
||||
}}
|
||||
width='16'
|
||||
height='16'
|
||||
viewBox='0 0 16 16'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
d='m13.67 6.03-11-4a.5.5 0 0 0-.64.64l4 11a.5.5 0 0 0 .935.015l1.92-4.8 4.8-1.92a.5.5 0 0 0 0-.935h-.015Z'
|
||||
fill={`${color}`}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -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: {
|
|||
>
|
||||
<BoardNavbar data={board} />
|
||||
<div className='absolute inset-0 bg-black/10' />
|
||||
<main className='relative h-full pt-28'>{children}</main>
|
||||
<main className='relative h-full pt-28'>
|
||||
<BoardLiveblocks boardId={params.boardId}>{children}</BoardLiveblocks>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<div className='h-full overflow-x-auto p-4'>
|
||||
<ListContainer boardId={params.boardId} data={lists} />
|
||||
</div>
|
||||
<BoardRoomWrapper>
|
||||
<div className='h-full overflow-x-auto p-4'>
|
||||
<ListContainer boardId={params.boardId} data={lists} />
|
||||
</div>
|
||||
</BoardRoomWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue