adds ui for collaborators

This commit is contained in:
bhushan6
2023-08-14 21:40:55 +05:30
parent f5b3ed6756
commit fae9c8ae1a
12 changed files with 461 additions and 30 deletions
+110
View File
@@ -16,6 +16,8 @@
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-scroll-area": "^1.0.4",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.0.4",
"@react-three/drei": "^9.80.2",
@@ -1416,6 +1418,43 @@
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.6.tgz",
"integrity": "sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.4",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.3",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.2",
"@radix-ui/react-portal": "1.0.3",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.1",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz",
@@ -1549,6 +1588,37 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.4.tgz",
"integrity": "sha512-OIClwBkwPG+FKvC4OMTRaa/3cfD069nkKFFL/TQzRzaO42Ce5ivKU9VMKgT7UU6UIkjcQqKBrDOIzWtPGw6e6w==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/number": "1.0.1",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.2.tgz",
@@ -6462,6 +6532,29 @@
"@radix-ui/react-use-controllable-state": "1.0.1"
}
},
"@radix-ui/react-popover": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.6.tgz",
"integrity": "sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-dismissable-layer": "1.0.4",
"@radix-ui/react-focus-guards": "1.0.1",
"@radix-ui/react-focus-scope": "1.0.3",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-popper": "1.1.2",
"@radix-ui/react-portal": "1.0.3",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-use-controllable-state": "1.0.1",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
}
},
"@radix-ui/react-popper": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz",
@@ -6525,6 +6618,23 @@
"@radix-ui/react-use-controllable-state": "1.0.1"
}
},
"@radix-ui/react-scroll-area": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.4.tgz",
"integrity": "sha512-OIClwBkwPG+FKvC4OMTRaa/3cfD069nkKFFL/TQzRzaO42Ce5ivKU9VMKgT7UU6UIkjcQqKBrDOIzWtPGw6e6w==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/number": "1.0.1",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-layout-effect": "1.0.1"
}
},
"@radix-ui/react-slider": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.1.2.tgz",
+2
View File
@@ -18,6 +18,8 @@
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-scroll-area": "^1.0.4",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.0.4",
"@react-three/drei": "^9.80.2",
+1 -1
View File
@@ -1,5 +1,5 @@
import "./App.css";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { LegoRoom } from "./components/3D";
import { useStore } from "./store";
import { Loader } from "./components/UI/Loader";
+28 -10
View File
@@ -10,6 +10,15 @@ import {
} from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { DoubleSide, RawShaderMaterial, Color, Vector2 } from "three";
import { Html } from "@react-three/drei";
import { cursorColors } from "../../../utils";
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]]; // Swap elements
}
}
const vertexShader = `
varying vec2 vUv;
@@ -70,6 +79,7 @@ export const BorderPlane = forwardRef(function BorderPlane(
const { size } = useThree();
const mesh = useRef();
const group = useRef();
const uniforms = useMemo(() => {
return {
@@ -91,7 +101,7 @@ export const BorderPlane = forwardRef(function BorderPlane(
mesh.current.material.uniforms.uSize.value.x = planeSize[0];
mesh.current.material.uniforms.uSize.value.y = planeSize[1];
mesh.current.material.uniforms.uBorderWidth.value =
(Math.min(planeSize[0], planeSize[1]) / 50) * 5;
(Math.min(planeSize[0], planeSize[1]) / 50) * 10;
}, [planeSize]);
useLayoutEffect(() => {
@@ -114,16 +124,24 @@ export const BorderPlane = forwardRef(function BorderPlane(
// planeMaterial.uniforms.uTime.value = elapsedTime;
// });
useImperativeHandle(ref, () => mesh.current);
useImperativeHandle(ref, () => ({
plane: mesh.current,
container: group.current,
}));
return (
<mesh
rotation={[-Math.PI / 2, 0, 0]}
ref={mesh}
material={planeMaterial}
{...props}
>
<planeGeometry args={planeSize} />
</mesh>
<group ref={group}>
<mesh
rotation={[-Math.PI / 2, 0, 0]}
ref={mesh}
material={planeMaterial}
{...props}
>
<planeGeometry args={planeSize} />
</mesh>
{/* <Html>
<h1>Hello</h1>
</Html> */}
</group>
);
});
+16 -12
View File
@@ -4,9 +4,8 @@
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 Other = memo(function Other({ id, color }) {
const room = useStore((state) => state.liveblocks.room);
const ref = useRef();
@@ -15,15 +14,15 @@ const Other = memo(function Other({ id, otherCurors }) {
const unsubscribe = 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.container.position.x = d.x;
ref.current.container.position.z = d.z;
ref.current.container.position.y = d.y - 31 / 2;
ref.current.scale.set(d.w, d.d);
ref.current.material.uniforms.uSize.value.x = d.w;
ref.current.material.uniforms.uSize.value.y = d.d;
ref.current.material.uniforms.uBorderWidth.value =
(Math.min(d.w, d.d) / 50) * 5;
ref.current.plane.scale.set(d.w, d.d);
ref.current.plane.material.uniforms.uSize.value.x = d.w;
ref.current.plane.material.uniforms.uSize.value.y = d.d;
ref.current.plane.material.uniforms.uBorderWidth.value =
(Math.min(d.w, d.d) / 50) * 10;
}
});
@@ -34,7 +33,7 @@ const Other = memo(function Other({ id, otherCurors }) {
return (
<>
<BorderPlane planeSize={[25, 25]} ref={ref} color={"#ff0000"} />
<BorderPlane planeSize={[25, 25]} ref={ref} color={color} />
</>
);
});
@@ -45,8 +44,13 @@ export const Others = () => {
return (
<>
{others.map((user) => {
console.log(user);
return user.presence?.self ? (
<Other key={user.presence.self.id} id={user.presence.self.id} />
<Other
key={user.presence.self.id}
id={user.presence.self.id}
color={user.presence.self.color}
/>
) : null;
})}
</>
+16 -2
View File
@@ -3,6 +3,7 @@ import React from "react";
import { TrashIcon, ResetIcon } from "@radix-ui/react-icons";
import "./styles.css";
import { useStore } from "../../../store";
import { PopoverPeopleList } from "../PeopleList";
// import { EDIT_MODE } from "../../../utils";
export const BottomBar = () => {
@@ -16,6 +17,8 @@ export const BottomBar = () => {
const setBricks = useStore((state) => state.setBricks);
const setSelection = useStore((state) => state.setSelectedBricks);
const room = useStore((state) => state.liveblocks.room);
const deleteSelectedBricks = () => {
setBricks((bricks) => {
const newBricks = bricks.filter((brick) => {
@@ -38,12 +41,22 @@ export const BottomBar = () => {
const clearBricks = useStore((state) => state.clearBricks);
const undo = () => {
room.history.undo();
setSelection({});
};
const redo = () => {
room.history.redo();
setSelection({});
};
return (
<div className="BottomBar">
<button className="Button violet">
<button className="Button violet" onClick={undo}>
<ResetIcon className="Icon" color="black" />
</button>
<button className="Button violet">
<button className="Button violet" onClick={redo}>
<ResetIcon
style={{ transform: "scaleX(-1)" }}
className="Icon"
@@ -56,6 +69,7 @@ export const BottomBar = () => {
<button onClick={clearBricks} className="Button violet">
<TrashIcon className="Icon" color="black" />
</button>
<PopoverPeopleList />
</div>
);
};
+13 -2
View File
@@ -1,16 +1,27 @@
/* eslint-disable no-unused-vars */
import React from "react";
import React, { useRef } from "react";
import { HexColorPicker } from "react-colorful";
import "./styles.css";
import { useStore } from "../../../../store";
// import useDebounce from "use-debouncy";
export const ColorInput = () => {
const setColor = useStore((state) => state.setColor);
const tId = useRef();
const debouncesetColor = (color) => {
if (tId.current) clearTimeout(tId.current);
tId.current = setTimeout(() => {
setColor(color);
}, 200);
};
return (
<div className="SliderInputContainer">
<label className="SliderLabel">Color</label>
<HexColorPicker color="#000000" onChange={(color) => setColor(color)} />
<HexColorPicker color="#000000" onChange={debouncesetColor} />
</div>
);
};
+83
View File
@@ -0,0 +1,83 @@
/* eslint-disable react/prop-types */
import "./style.css";
import * as Popover from "@radix-ui/react-popover";
import { PersonIcon } from "@radix-ui/react-icons";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useStore } from "../../../store";
const Person = ({ color = "#ff0000", name = "" }) => {
return (
<div className="PeopleListItem">
<div className="ColorBadge" style={{ background: color }}></div>
<h3 className="name">{name}</h3>
</div>
);
};
const s = {
display: "flex",
justifyContent: "center",
alignItems: "center",
};
const PeopleList = () => {
const others = useStore((state) => state.liveblocks.others);
const NoOfPeoples = others.length;
return (
<ScrollArea.Root className="PeopleList ScrollAreaRoot">
<ScrollArea.Viewport
className="ScrollAreaViewport"
style={NoOfPeoples < 1 ? s : {}}
>
{NoOfPeoples < 1 && (
<h3 style={{ textAlign: "center", padding: "6px", fontSize: "14px" }}>
No one else in room
</h3>
)}
{others.map((user) => {
return user.presence?.self ? (
<Person
key={user.presence.self.id}
name={"Person"}
color={user.presence.self.color}
/>
) : null;
})}
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="ScrollAreaScrollbar"
orientation="vertical"
>
<ScrollArea.Thumb className="ScrollAreaThumb" />
</ScrollArea.Scrollbar>
<ScrollArea.Scrollbar
className="ScrollAreaScrollbar"
orientation="horizontal"
>
<ScrollArea.Thumb className="ScrollAreaThumb" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="ScrollAreaCorner" />
</ScrollArea.Root>
);
};
export const PopoverPeopleList = () => (
<Popover.Root>
<Popover.Trigger asChild>
<button className="Button violet" aria-label="Connected People List">
<PersonIcon className="Icon" color="black" />
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content className="PopoverContent" sideOffset={5}>
<PeopleList />
{/* <Popover.Close className="PopoverClose" aria-label="Close">
<Cross2Icon />
</Popover.Close> */}
{/* <Popover.Arrow className="PopoverArrow" /> */}
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
+104
View File
@@ -0,0 +1,104 @@
@import "@radix-ui/colors/black-alpha.css";
@import "@radix-ui/colors/mauve.css";
@import "@radix-ui/colors/violet.css";
.ScrollAreaRoot {
max-height: 225px;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 10px var(--black-a7);
background-color: white;
--scrollbar-size: 10px;
}
.ScrollAreaViewport {
width: 100%;
height: 100%;
border-radius: inherit;
}
.ScrollAreaScrollbar {
display: flex;
user-select: none;
touch-action: none;
padding: 2px;
background-color: var(--black-a6);
transition: background-color 160ms ease-out;
}
.ScrollAreaScrollbar:hover {
background: var(--black-a8);
}
.ScrollAreaScrollbar[data-orientation="vertical"] {
width: var(--scrollbar-size);
}
.ScrollAreaScrollbar[data-orientation="horizontal"] {
flex-direction: column;
height: var(--scrollbar-size);
}
.ScrollAreaThumb {
flex: 1;
background: var(--mauve-10);
border-radius: var(--scrollbar-size);
position: relative;
}
/* increase target size for touch devices https://www.w3.org/WAI/WCAG21/Understanding/target-size.html */
.ScrollAreaThumb::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
min-width: 44px;
min-height: 44px;
}
.ScrollAreaCorner {
background: var(--black-a8);
}
.PeopleList .PeopleListItem {
line-height: 18px;
border-top: 1px solid var(--mauve-6);
}
.PeopleList {
display: flex;
flex-direction: column;
gap: 10px;
justify-content: center;
align-items: center;
width: 200px;
}
.PeopleList .PeopleListItem {
display: flex;
justify-content: start;
align-items: center;
gap: 10px;
width: 200px;
padding: 8px;
}
.PeopleList .PeopleListItem .ColorBadge {
width: 25px;
height: 25px;
border-radius: 50%;
box-sizing: border-box;
border: 1px solid black;
}
.PeopleList .PeopleListItem .name {
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 157px;
}
.PopoverContent {
background-color: white;
border-radius: 4px;
}
+22 -3
View File
@@ -1,5 +1,11 @@
import { create } from "zustand";
import { CREATE_MODE, defaultAnchor, defaultWidth, uID } from "../utils";
import {
CREATE_MODE,
defaultAnchor,
defaultWidth,
generateSoftColors,
uID,
} from "../utils";
import { createClient } from "@liveblocks/client";
import { liveblocks } from "@liveblocks/zustand";
@@ -61,6 +67,7 @@ const client = createClient({
// }));
const id = uID();
const color = generateSoftColors();
export const useStore = create(
liveblocks(
@@ -107,12 +114,24 @@ export const useStore = create(
set((state) => ({ bricks: getBricks(state.bricks) })),
clearBricks: () => set({ bricks: [] }),
self: { id: id },
self: { id: id, color: color },
cursorColors: {},
setCursorColors: (newCursorColor) =>
set((state) => ({
cursorColors: { ...state.cursorColors, ...newCursorColor },
})),
removeCursorColor: (id) =>
set((state) => {
delete state.cursorColors[id];
return { cursorColors: { ...state.cursorColors } };
}),
}),
{
client,
presenceMapping: { self: true },
storageMapping: { bricks: true },
storageMapping: { bricks: true, cursorColors: true },
}
)
);
+23
View File
@@ -30,3 +30,26 @@ export const colors = [
"#000000",
"#652A0C",
];
export const cursorColors = [
"#FFA6B1", // Soft Pink
"#FFB6C1", // Light Pink
"#FFD1A8", // Peach
"#FFE4B5", // Moccasin
"#FFDEAD", // Navajo White
"#FFF0E1", // Misty Rose
"#D8BFD8", // Thistle
"#E6E6FA", // Lavender
"#B0E0E6", // Powder Blue
"#ADD8E6", // Light Blue
"#B0C4DE", // Light Steel Blue
"#98FB98", // Pale Green
"#F0E68C", // Khaki
"#FFEBD6", // Almond
"#FFE5B4", // Mellow Apricot
"#FFDAB9", // Peach Puff
"#EEDD82", // Light Goldenrod
"#D3D3D3", // Light Gray
"#FFC0CB", // Pink
"#FFB347", // Pastel Orange
];
+43
View File
@@ -84,3 +84,46 @@ export function uID(length = 8) {
return result;
}
function hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function rgbToHex(r, g, b) {
return (
"#" +
((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()
);
}
export function generateSoftColors() {
const hue = Math.random();
const saturation = Math.random() * 0.2 + 0.4; // 40% to 60%
const lightness = Math.random() * 0.3 + 0.5; // 50% to 80%
const [r, g, b] = hslToRgb(hue, saturation, lightness);
const hexColor = rgbToHex(r, g, b);
return hexColor;
}