sets up liveblock for collaboration feat

This commit is contained in:
bhushan6
2023-08-14 04:33:19 +05:30
parent c960f1786a
commit fe58e5c01f
12 changed files with 353 additions and 57 deletions
+2
View File
@@ -22,3 +22,5 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.env
+73
View File
@@ -8,6 +8,9 @@
"name": "lego-builder", "name": "lego-builder",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@liveblocks/client": "^1.2.2",
"@liveblocks/react": "^1.2.2",
"@liveblocks/zustand": "^1.2.2",
"@radix-ui/colors": "^2.0.1", "@radix-ui/colors": "^2.0.1",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
@@ -981,6 +984,44 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@liveblocks/client": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-1.2.2.tgz",
"integrity": "sha512-cav7NIoZNGmOC07p/spaCiNwLL/eeKKTAscLb2wN5myXG+xWlf6sj6zIxMOSA1eTDcDDvDMsnkLUviUUK0QGWA==",
"dependencies": {
"@liveblocks/core": "1.2.2"
}
},
"node_modules/@liveblocks/core": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-1.2.2.tgz",
"integrity": "sha512-1WALetZ600EN3D4VaSpXse2Plp5+7os78B1kAANlOmf/NtiQKIoEBE40xYBVd4QAJgX6YIYGjOsQQja0CUqs7g=="
},
"node_modules/@liveblocks/react": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@liveblocks/react/-/react-1.2.2.tgz",
"integrity": "sha512-OEPnS4C+cl1RgKOBs8RSYct6EbkzhQcSALIO1j+CSwJqYMy9vJxSd8nrqRqn+Nz9Uanmrgp01HTGrbHw+vp0gg==",
"dependencies": {
"@liveblocks/client": "1.2.2",
"@liveblocks/core": "1.2.2",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": "^16.14.0 || ^17 || ^18"
}
},
"node_modules/@liveblocks/zustand": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@liveblocks/zustand/-/zustand-1.2.2.tgz",
"integrity": "sha512-F8xTtR3yE2SJ5FYHMpdAu6mheSBubjc4AekNaoePg2uV8BfZljNT1VrU+1SKCCEddV1e96OOYQqP+iBJcryYhg==",
"dependencies": {
"@liveblocks/client": "1.2.2",
"@liveblocks/core": "1.2.2"
},
"peerDependencies": {
"zustand": "^4.1.3"
}
},
"node_modules/@mediapipe/tasks-vision": { "node_modules/@mediapipe/tasks-vision": {
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.2.tgz",
@@ -6163,6 +6204,38 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"@liveblocks/client": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-1.2.2.tgz",
"integrity": "sha512-cav7NIoZNGmOC07p/spaCiNwLL/eeKKTAscLb2wN5myXG+xWlf6sj6zIxMOSA1eTDcDDvDMsnkLUviUUK0QGWA==",
"requires": {
"@liveblocks/core": "1.2.2"
}
},
"@liveblocks/core": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-1.2.2.tgz",
"integrity": "sha512-1WALetZ600EN3D4VaSpXse2Plp5+7os78B1kAANlOmf/NtiQKIoEBE40xYBVd4QAJgX6YIYGjOsQQja0CUqs7g=="
},
"@liveblocks/react": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@liveblocks/react/-/react-1.2.2.tgz",
"integrity": "sha512-OEPnS4C+cl1RgKOBs8RSYct6EbkzhQcSALIO1j+CSwJqYMy9vJxSd8nrqRqn+Nz9Uanmrgp01HTGrbHw+vp0gg==",
"requires": {
"@liveblocks/client": "1.2.2",
"@liveblocks/core": "1.2.2",
"use-sync-external-store": "^1.2.0"
}
},
"@liveblocks/zustand": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@liveblocks/zustand/-/zustand-1.2.2.tgz",
"integrity": "sha512-F8xTtR3yE2SJ5FYHMpdAu6mheSBubjc4AekNaoePg2uV8BfZljNT1VrU+1SKCCEddV1e96OOYQqP+iBJcryYhg==",
"requires": {
"@liveblocks/client": "1.2.2",
"@liveblocks/core": "1.2.2"
}
},
"@mediapipe/tasks-vision": { "@mediapipe/tasks-vision": {
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.2.tgz",
+3
View File
@@ -10,6 +10,9 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@liveblocks/client": "^1.2.2",
"@liveblocks/react": "^1.2.2",
"@liveblocks/zustand": "^1.2.2",
"@radix-ui/colors": "^2.0.1", "@radix-ui/colors": "^2.0.1",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
+19
View File
@@ -1,7 +1,26 @@
import "./App.css"; import "./App.css";
import { useEffect } from "react";
import { LegoRoom } from "./components/3D"; import { LegoRoom } from "./components/3D";
import { useStore } from "./store";
import { Loader } from "./components/UI/Loader";
const CONNECTED = "connected";
function App() { function App() {
const enterRoom = useStore((state) => state.liveblocks.enterRoom);
const leaveRoom = useStore((state) => state.liveblocks.leaveRoom);
const status = useStore((state) => state.liveblocks.status);
useEffect(() => {
enterRoom("lego-test-room");
return () => leaveRoom("lego-test-room", { bricks: [] });
}, [enterRoom, leaveRoom]);
if (status !== CONNECTED) {
return <Loader />;
}
return ( return (
<div className="App"> <div className="App">
<LegoRoom /> <LegoRoom />
+22 -15
View File
@@ -1,7 +1,13 @@
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
/* eslint-disable react/no-unknown-property */ /* eslint-disable react/no-unknown-property */
import { useLayoutEffect, useMemo, useRef } from "react"; import {
useLayoutEffect,
useMemo,
useRef,
forwardRef,
useImperativeHandle,
} from "react";
import { useFrame, useThree } from "@react-three/fiber"; import { useFrame, useThree } from "@react-three/fiber";
import { DoubleSide, RawShaderMaterial, Color, Vector2 } from "three"; import { DoubleSide, RawShaderMaterial, Color, Vector2 } from "three";
@@ -45,14 +51,13 @@ void main() {
} }
`; `;
export const BorderPlane = ({ export const BorderPlane = forwardRef(function BorderPlane(
planeSize = [10, 10], { planeSize = [10, 10], color = "#ffffff", ...props },
color = "#ffffff", ref
...props ) {
}) => {
const { size } = useThree(); const { size } = useThree();
const ref = useRef(); const mesh = useRef();
const uniforms = useMemo(() => { const uniforms = useMemo(() => {
return { return {
@@ -60,22 +65,22 @@ export const BorderPlane = ({
uBorderColor: { value: new Color(0xffffff) }, uBorderColor: { value: new Color(0xffffff) },
uBorderWidth: { value: 10 }, uBorderWidth: { value: 10 },
uResolution: { value: new Vector2() }, uResolution: { value: new Vector2() },
uSize: { value: new Vector2(20, 20) }, uSize: { value: new Vector2(25, 25) },
}; };
}, []); }, []);
useLayoutEffect(() => { useLayoutEffect(() => {
ref.current.material.uniforms.uResolution.value.x = size.width; mesh.current.material.uniforms.uResolution.value.x = size.width;
ref.current.material.uniforms.uResolution.value.y = size.height; mesh.current.material.uniforms.uResolution.value.y = size.height;
}, [size]); }, [size]);
useLayoutEffect(() => { useLayoutEffect(() => {
ref.current.material.uniforms.uSize.value.x = planeSize[0]; mesh.current.material.uniforms.uSize.value.x = planeSize[0];
ref.current.material.uniforms.uSize.value.y = planeSize[1]; mesh.current.material.uniforms.uSize.value.y = planeSize[1];
}, [planeSize]); }, [planeSize]);
useLayoutEffect(() => { useLayoutEffect(() => {
// ref.current.material.uniforms.uBorderColor.value = color; mesh.current.material.uniforms.uBorderColor.value.set(color);
}, [color]); }, [color]);
const planeMaterial = useMemo(() => { const planeMaterial = useMemo(() => {
@@ -94,14 +99,16 @@ export const BorderPlane = ({
// planeMaterial.uniforms.uTime.value = elapsedTime; // planeMaterial.uniforms.uTime.value = elapsedTime;
// }); // });
useImperativeHandle(ref, () => mesh.current);
return ( return (
<mesh <mesh
rotation={[-Math.PI / 2, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}
ref={ref} ref={mesh}
material={planeMaterial} material={planeMaterial}
{...props} {...props}
> >
<planeGeometry args={planeSize} /> <planeGeometry args={planeSize} />
</mesh> </mesh>
); );
}; });
+3
View File
@@ -94,6 +94,9 @@ export const Brick = ({
initial={{ opacity: 0, scale: 0.8 }} initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ type: "spring", stiffness: 250, duration: 2 }} transition={{ type: "spring", stiffness: 250, duration: 2 }}
userData={{
uID,
}}
> >
<mesh <mesh
castShadow castShadow
+46
View File
@@ -0,0 +1,46 @@
/* eslint-disable react/no-unknown-property */
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
import { useStore } from "../../store";
import { memo, useRef, useEffect } from "react";
import { BorderPlane } from "./BorderPlane";
import { base } from "../../utils";
const Other = memo(function Other({ id, otherCurors }) {
const room = useStore((state) => state.liveblocks.room);
const ref = useRef();
useEffect(() => {
room.subscribe("event", ({ event }) => {
if (event.type === id) {
const d = event.data;
ref.current.position.x = d.x;
ref.current.position.z = d.z;
ref.current.position.y = d.y - 31 / 2;
ref.current.scale.set(d.w, d.d);
}
});
}, [id, room]);
return (
<>
<BorderPlane planeSize={[25, 25]} ref={ref} color={"#ff0000"} />
</>
);
});
export const Others = () => {
const others = useStore((state) => state.liveblocks.others);
return (
<>
{others.map((user) => {
return user.presence?.self ? (
<Other key={user.presence.self.id} id={user.presence.self.id} />
) : null;
})}
</>
);
};
+18 -7
View File
@@ -21,10 +21,10 @@ import {
} from "../../utils"; } from "../../utils";
import { ChangeColor } from "./ChangeColor"; import { ChangeColor } from "./ChangeColor";
import { useStore } from "../../store"; import { useStore } from "../../store";
import { BorderPlane } from "./BorderPlane"; // import { BorderPlane } from "./BorderPlane";
import { Others } from "./Others";
export const Scene = () => { export const Scene = () => {
// const [bricks, setBricks] = useState([]);
const bricks = useStore((state) => state.bricks); const bricks = useStore((state) => state.bricks);
const setBricks = useStore((state) => state.setBricks); const setBricks = useStore((state) => state.setBricks);
@@ -42,6 +42,10 @@ export const Scene = () => {
const anchorZ = useStore((state) => state.anchorZ); const anchorZ = useStore((state) => state.anchorZ);
const rotate = useStore((state) => state.rotate); const rotate = useStore((state) => state.rotate);
const color = useStore((state) => state.color); const color = useStore((state) => state.color);
const room = useStore((state) => state.liveblocks.room);
const self = useStore((state) => state.self);
// useAnchorShorcuts(anchorX, anchorZ, set); // useAnchorShorcuts(anchorX, anchorZ, set);
// const setSelection = useStore(state => state.setSelectedBricks) // const setSelection = useStore(state => state.setSelectedBricks)
@@ -143,6 +147,17 @@ export const Scene = () => {
evenDepth ? base : base / 2 evenDepth ? base : base / 2
) )
); );
room.broadcastEvent({
type: self.id,
data: {
x: brickCursorRef.current.position.x,
y: brickCursorRef.current.position.y,
z: brickCursorRef.current.position.z,
w: width,
d: depth,
},
});
}; };
const onClick = (e) => { const onClick = (e) => {
@@ -208,11 +223,7 @@ export const Scene = () => {
dimensions={{ x: width, z: depth }} dimensions={{ x: width, z: depth }}
translation={{ x: anchorX, z: anchorZ }} translation={{ x: anchorX, z: anchorZ }}
/> />
{/* <BorderPlane <Others />
planeSize={[25, 100]}
color="#ff0000"
position={[25 / 2, 0.1, 0]}
/> */}
</> </>
); );
}; };
+5
View File
@@ -36,6 +36,8 @@ export const BottomBar = () => {
setSelection({}); setSelection({});
}; };
const clearBricks = useStore((state) => state.clearBricks);
return ( return (
<div className="BottomBar"> <div className="BottomBar">
<button className="Button violet"> <button className="Button violet">
@@ -51,6 +53,9 @@ export const BottomBar = () => {
<button onClick={deleteSelectedBricks} className="Button violet"> <button onClick={deleteSelectedBricks} className="Button violet">
<TrashIcon className="Icon" color="black" /> <TrashIcon className="Icon" color="black" />
</button> </button>
<button onClick={clearBricks} className="Button violet">
<TrashIcon className="Icon" color="black" />
</button>
</div> </div>
); );
}; };
+20
View File
@@ -0,0 +1,20 @@
export const Loader = () => {
return (
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "black",
color: "white",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
Loading...
</div>
);
};
+34
View File
@@ -0,0 +1,34 @@
import { createClient } from "@liveblocks/client";
import { createRoomContext } from "@liveblocks/react";
const client = createClient({
publicApiKey: import.meta.env.VITE_LIVEBLOCKS_API_KEY,
});
export const {
suspense: {
RoomProvider,
useRoom,
useMyPresence,
useUpdateMyPresence,
useSelf,
useOthers,
useOthersMapped,
useOthersConnectionIds,
useOther,
useBroadcastEvent,
useEventListener,
useErrorListener,
useStorage,
useObject,
useMap,
useList,
useBatch,
useHistory,
useUndo,
useRedo,
useCanUndo,
useCanRedo,
useMutation,
},
} = createRoomContext(client);
+76 -3
View File
@@ -1,7 +1,70 @@
import { create } from "zustand"; import { create } from "zustand";
import { CREATE_MODE, defaultAnchor, defaultWidth } from "../utils"; import { CREATE_MODE, defaultAnchor, defaultWidth, uID } from "../utils";
import { createClient } from "@liveblocks/client";
import { liveblocks } from "@liveblocks/zustand";
export const useStore = create((set) => ({ const client = createClient({
publicApiKey: import.meta.env.VITE_LIVEBLOCKS_API_KEY,
});
// export const useStore = create((set) => ({
// mode: CREATE_MODE,
// setMode: (newMode) => set({ mode: newMode }),
// width: defaultWidth,
// setWidth: (newWidth) => set({ width: newWidth }),
// depth: defaultWidth,
// setDepth: (newDepth) => set({ depth: newDepth }),
// anchorX: defaultAnchor,
// setAnchorX: (newAnchorPoint) => set({ anchorX: newAnchorPoint }),
// anchorZ: defaultAnchor,
// setAnchorZ: (newAnchorPoint) => set({ anchorZ: newAnchorPoint }),
// rotate: false,
// setRotate: (bool) => set({ rotate: bool }),
// color: "#ff0000",
// setColor: (newColor) => set({ color: newColor }),
// selectedBricks: [],
// setSelectedBricks: ({ object, shift }) =>
// set((state) => {
// if (object === undefined) return { selectedBricks: [] };
// else if (Array.isArray(object)) return { selectedBricks: object };
// else if (!shift)
// return state.selectedBricks[0] === object
// ? { selectedBricks: [] }
// : { selectedBricks: [object] };
// else if (state.selectedBricks.includes(object))
// return {
// selectedBricks: state.selectedBricks.filter((o) => o !== object),
// };
// else return { selectedBricks: [object, ...state.selectedBricks] };
// }),
// bricks: [],
// setBricks: (getBricks) =>
// set((state) => ({ bricks: getBricks(state.bricks) })),
// clearBricks: () => set({ bricks: [] }),
// cursor: {
// x: 0,
// z: 0,
// w: 0,
// d: 0,
// },
// setCursor: (newState) => set({ cursor: newState }),
// }));
const id = uID();
export const useStore = create(
liveblocks(
(set) => ({
mode: CREATE_MODE, mode: CREATE_MODE,
setMode: (newMode) => set({ mode: newMode }), setMode: (newMode) => set({ mode: newMode }),
@@ -42,4 +105,14 @@ export const useStore = create((set) => ({
bricks: [], bricks: [],
setBricks: (getBricks) => setBricks: (getBricks) =>
set((state) => ({ bricks: getBricks(state.bricks) })), set((state) => ({ bricks: getBricks(state.bricks) })),
})); clearBricks: () => set({ bricks: [] }),
self: { id: id },
}),
{
client,
presenceMapping: { self: true },
storageMapping: { bricks: true },
}
)
);