From fae9c8ae1a325bbe413e7ff511072e3efbac4ecf Mon Sep 17 00:00:00 2001 From: bhushan6 Date: Mon, 14 Aug 2023 21:40:55 +0530 Subject: [PATCH] adds ui for collaborators --- package-lock.json | 110 +++++++++++++++++++ package.json | 2 + src/App.jsx | 2 +- src/components/3D/BorderPlane/index.jsx | 38 +++++-- src/components/3D/Others.jsx | 28 +++-- src/components/UI/BottomBar/index.jsx | 18 ++- src/components/UI/Panel/ColorInput/index.jsx | 15 ++- src/components/UI/PeopleList/index.jsx | 83 ++++++++++++++ src/components/UI/PeopleList/style.css | 104 ++++++++++++++++++ src/store/index.js | 25 ++++- src/utils/constants.js | 23 ++++ src/utils/helpers.js | 43 ++++++++ 12 files changed, 461 insertions(+), 30 deletions(-) create mode 100644 src/components/UI/PeopleList/index.jsx create mode 100644 src/components/UI/PeopleList/style.css diff --git a/package-lock.json b/package-lock.json index 78b8016..33af3c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4fe0818..6b78567 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.jsx b/src/App.jsx index cf0d07f..63e47f5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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"; diff --git a/src/components/3D/BorderPlane/index.jsx b/src/components/3D/BorderPlane/index.jsx index 784f863..9199f47 100644 --- a/src/components/3D/BorderPlane/index.jsx +++ b/src/components/3D/BorderPlane/index.jsx @@ -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 ( - - - + + + + + {/* +

Hello

+ */} +
); }); diff --git a/src/components/3D/Others.jsx b/src/components/3D/Others.jsx index 394a251..aaa11e5 100644 --- a/src/components/3D/Others.jsx +++ b/src/components/3D/Others.jsx @@ -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 ( <> - + ); }); @@ -45,8 +44,13 @@ export const Others = () => { return ( <> {others.map((user) => { + console.log(user); return user.presence?.self ? ( - + ) : null; })} diff --git a/src/components/UI/BottomBar/index.jsx b/src/components/UI/BottomBar/index.jsx index f608dad..3eeadc6 100644 --- a/src/components/UI/BottomBar/index.jsx +++ b/src/components/UI/BottomBar/index.jsx @@ -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 (
- - +
); }; diff --git a/src/components/UI/Panel/ColorInput/index.jsx b/src/components/UI/Panel/ColorInput/index.jsx index 9fc91ee..f709072 100644 --- a/src/components/UI/Panel/ColorInput/index.jsx +++ b/src/components/UI/Panel/ColorInput/index.jsx @@ -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 (
- setColor(color)} /> +
); }; diff --git a/src/components/UI/PeopleList/index.jsx b/src/components/UI/PeopleList/index.jsx new file mode 100644 index 0000000..d84a633 --- /dev/null +++ b/src/components/UI/PeopleList/index.jsx @@ -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 ( +
+
+

{name}

+
+ ); +}; + +const s = { + display: "flex", + justifyContent: "center", + alignItems: "center", +}; + +const PeopleList = () => { + const others = useStore((state) => state.liveblocks.others); + + const NoOfPeoples = others.length; + + return ( + + + {NoOfPeoples < 1 && ( +

+ No one else in room +

+ )} + {others.map((user) => { + return user.presence?.self ? ( + + ) : null; + })} +
+ + + + + + + +
+ ); +}; + +export const PopoverPeopleList = () => ( + + + + + + + + {/* + + */} + {/* */} + + + +); diff --git a/src/components/UI/PeopleList/style.css b/src/components/UI/PeopleList/style.css new file mode 100644 index 0000000..021902b --- /dev/null +++ b/src/components/UI/PeopleList/style.css @@ -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; +} diff --git a/src/store/index.js b/src/store/index.js index 45a698a..e4e4eac 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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 }, } ) ); diff --git a/src/utils/constants.js b/src/utils/constants.js index 864928e..9ed463e 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -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 +]; diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 4c2e001..f34db0e 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -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; +}