[
  {
    "path": ".eslintignore",
    "content": "node_modules\nout\nbuild\n.next\n\ntailwind.config.js\npostcss.config.js"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  // Configuration for JavaScript files\n  \"extends\": [\n    \"airbnb-base\",\n    \"next/core-web-vitals\",\n    \"plugin:prettier/recommended\"\n  ],\n  \"rules\": {\n    \"prettier/prettier\": [\n      \"error\",\n      {\n        \"singleQuote\": true,\n        \"semi\": true\n      }\n    ]\n  },\n  \"overrides\": [\n    // Configuration for TypeScript files\n    {\n      \"files\": [\"**/*.ts\", \"**/*.tsx\"],\n      \"plugins\": [\"@typescript-eslint\", \"unused-imports\", \"tailwindcss\"],\n      \"extends\": [\n        \"plugin:tailwindcss/recommended\",\n        \"airbnb-typescript\",\n        \"next/core-web-vitals\",\n        \"plugin:prettier/recommended\"\n      ],\n      \"parserOptions\": {\n        \"project\": \"./tsconfig.json\"\n      },\n      \"rules\": {\n        \"prettier/prettier\": [\n          \"error\",\n          {\n            \"singleQuote\": true,\n            \"endOfLine\": \"auto\",\n            \"semi\": true\n          }\n        ],\n        \"react/destructuring-assignment\": \"off\", // Vscode doesn't support automatically destructuring, it's a pain to add a new variable\n        \"jsx-a11y/anchor-is-valid\": \"off\", // Next.js use his own internal link system\n        \"react/require-default-props\": \"off\", // Allow non-defined react props as undefined\n        \"react/jsx-props-no-spreading\": \"off\", // _app.tsx uses spread operator and also, react-hook-form\n        \"@next/next/no-img-element\": \"off\", // We currently not using next/image because it isn't supported with SSG mode\n        \"import/order\": [\n          \"error\",\n          {\n            \"groups\": [\"builtin\", \"external\", \"internal\"],\n            \"pathGroups\": [\n              {\n                \"pattern\": \"react\",\n                \"group\": \"external\",\n                \"position\": \"before\"\n              }\n            ],\n            \"pathGroupsExcludedImportTypes\": [\"react\"],\n            \"newlines-between\": \"always\",\n            \"alphabetize\": {\n              \"order\": \"asc\",\n              \"caseInsensitive\": true\n            }\n          }\n        ],\n        \"@typescript-eslint/comma-dangle\": \"off\", // Avoid conflict rule between Eslint and Prettier\n        \"import/prefer-default-export\": \"off\", // Named export is easier to refactor automatically\n        \"class-methods-use-this\": \"off\", // _document.tsx use render method without `this` keyword\n        \"tailwindcss/classnames-order\": [\n          \"warn\",\n          {\n            \"officialSorting\": true\n          }\n        ], // Follow the same ordering as the official plugin `prettier-plugin-tailwindcss`\n        \"@typescript-eslint/no-unused-vars\": \"off\",\n        \"unused-imports/no-unused-imports\": \"error\",\n        \"unused-imports/no-unused-vars\": [\n          \"error\",\n          { \"argsIgnorePattern\": \"^_\" }\n        ]\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/codesee-arch-diagram.yml",
    "content": "on:\n  push:\n    branches:\n      - main\n  pull_request_target:\n    types: [opened, synchronize, reopened]\n\nname: CodeSee Map\n\njobs:\n  test_map_action:\n    runs-on: ubuntu-latest\n    continue-on-error: true\n    name: Run CodeSee Map Analysis\n    steps:\n      - name: checkout\n        id: checkout\n        uses: actions/checkout@v2\n        with:\n          repository: ${{ github.event.pull_request.head.repo.full_name }}\n          ref: ${{ github.event.pull_request.head.ref }}\n          fetch-depth: 0\n\n      # codesee-detect-languages has an output with id languages.\n      - name: Detect Languages\n        id: detect-languages\n        uses: Codesee-io/codesee-detect-languages-action@latest\n\n      - name: Configure JDK 16\n        uses: actions/setup-java@v2\n        if: ${{ fromJSON(steps.detect-languages.outputs.languages).java }}\n        with:\n          java-version: '16'\n          distribution: 'zulu'\n\n      # CodeSee Maps Go support uses a static binary so there's no setup step required.\n\n      - name: Configure Node.js 14\n        uses: actions/setup-node@v2\n        if: ${{ fromJSON(steps.detect-languages.outputs.languages).javascript }}\n        with:\n          node-version: '14'\n\n      - name: Configure Python 3.x\n        uses: actions/setup-python@v2\n        if: ${{ fromJSON(steps.detect-languages.outputs.languages).python }}\n        with:\n          python-version: '3.10'\n          architecture: 'x64'\n\n      - name: Configure Ruby '3.x'\n        uses: ruby/setup-ruby@v1\n        if: ${{ fromJSON(steps.detect-languages.outputs.languages).ruby }}\n        with:\n          ruby-version: '3.0'\n\n      # We need the rust toolchain because it uses rustc and cargo to inspect the package\n      - name: Configure Rust 1.x stable\n        uses: actions-rs/toolchain@v1\n        if: ${{ fromJSON(steps.detect-languages.outputs.languages).rust }} \n        with:\n          toolchain: stable\n\n      - name: Generate Map\n        id: generate-map\n        uses: Codesee-io/codesee-map-action@latest\n        with:\n          step: map\n          api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}\n          github_ref: ${{ github.ref }}\n          languages: ${{ steps.detect-languages.outputs.languages }}\n\n      - name: Upload Map\n        id: upload-map\n        uses: Codesee-io/codesee-map-action@latest\n        with:\n          step: mapUpload\n          api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}\n          github_ref: ${{ github.ref }}\n\n      - name: Insights\n        id: insights\n        uses: Codesee-io/codesee-map-action@latest\n        with:\n          step: insights\n          api_token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}\n          github_ref: ${{ github.ref }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\n"
  },
  {
    "path": "README.md",
    "content": "# Collabio | Online whiteboard\n\nReal-time whiteboard made with Next.JS and Socket.IO\n## Features\n\n- Drawing lines, circles and rectangles\n- Eraser\n- Undo/Redo\n- Real-time mouse tracking\n- Chatting\n- Placing images\n- Moving selected area\n- Saving canvas\n- Changing backgrounds\n- Sharing\n## Made using\n- Next.JS\n- Recoil\n- TailwindCSS\n- Framer Motion\n- Socket.IO\n## Demo\n\nLIVE DEMO: https://collabio-kriziu.herokuapp.com\n\n\n## Installation\nClone repository, install all npm packages and run like normal Next.JS application.\n## Screenshots\n\n#### Home page\n![home page](https://i.imgur.com/00CZlrR.png)\n\n#### Board page\n![Board page](https://i.imgur.com/0v4Y8XP.png)\n"
  },
  {
    "path": "common/components/portal/components/Portal.ts",
    "content": "import { useEffect, useState } from 'react';\n\nimport { createPortal } from 'react-dom';\n\nconst Portal = ({ children }: { children: JSX.Element | JSX.Element[] }) => {\n  const [portal, setPortal] = useState<HTMLElement>();\n\n  useEffect(() => {\n    const node = document.getElementById('portal');\n    if (node) setPortal(node);\n  }, []);\n\n  if (!portal) return null;\n\n  return createPortal(children, portal);\n};\n\nexport default Portal;\n"
  },
  {
    "path": "common/constants/canvasSize.ts",
    "content": "export const CANVAS_SIZE = {\n  width: 3500,\n  height: 2000,\n};\n"
  },
  {
    "path": "common/constants/colors.ts",
    "content": "export const COLORS = {\n  PURPLE: '#6B32F3',\n  BLUE: '#408FF8',\n  RED: '#F32D27',\n  GREEN: '#6FCB12',\n  GOLD: '#A89D6C',\n  PINK: '#EB29DA',\n  MINT: '#19CB87',\n  RED_LIGHT: '#ED7878',\n  CYAN: '#02CBF6',\n  RED_DARK: '#BA1555',\n  ORANGE: '#FF7300',\n};\n\nexport const COLORS_ARRAY = [...Object.values(COLORS)];\n"
  },
  {
    "path": "common/constants/defaultMove.ts",
    "content": "import { Move } from '../types/global';\n\nexport const DEFAULT_MOVE: Move = {\n  circle: {\n    cX: 0,\n    cY: 0,\n    radiusX: 0,\n    radiusY: 0,\n  },\n  rect: {\n    width: 0,\n    height: 0,\n  },\n  path: [],\n  options: {\n    shape: 'line',\n    mode: 'draw',\n    lineWidth: 1,\n    lineColor: { r: 0, g: 0, b: 0, a: 0 },\n    fillColor: { r: 0, g: 0, b: 0, a: 0 },\n    selection: null,\n  },\n  id: '',\n  img: {\n    base64: '',\n  },\n  timestamp: 0,\n};\n"
  },
  {
    "path": "common/constants/easings.ts",
    "content": "export const DEFAULT_EASE = [0.6, 0.01, -0.05, 0.9];\n"
  },
  {
    "path": "common/hooks/useViewportSize.ts",
    "content": "import { useEffect, useState } from 'react';\n\nexport const useViewportSize = () => {\n  const [width, setWidth] = useState(0);\n  const [height, setHeight] = useState(0);\n\n  useEffect(() => {\n    const handleResize = () => {\n      setWidth(window.innerWidth);\n      setHeight(window.innerHeight);\n    };\n\n    handleResize();\n\n    window.addEventListener('resize', handleResize);\n\n    return () => {\n      window.removeEventListener('resize', handleResize);\n    };\n  }, []);\n\n  return { width, height };\n};\n"
  },
  {
    "path": "common/lib/getNextColor.ts",
    "content": "import { COLORS_ARRAY } from '../constants/colors';\n\nexport const getNextColor = (color?: string) => {\n  const index = COLORS_ARRAY.findIndex((colorArr) => colorArr === color);\n\n  if (index === -1) return COLORS_ARRAY[0];\n\n  return COLORS_ARRAY[(index + 1) % COLORS_ARRAY.length];\n};\n"
  },
  {
    "path": "common/lib/getPos.ts",
    "content": "import { MotionValue } from 'framer-motion';\n\nexport const getPos = (pos: number, motionValue: MotionValue) =>\n  pos - motionValue.get();\n"
  },
  {
    "path": "common/lib/optimizeImage.ts",
    "content": "import FileResizer from 'react-image-file-resizer';\n\nexport const optimizeImage = (file: File, callback: (uri: string) => void) => {\n  FileResizer.imageFileResizer(\n    file,\n    700,\n    700,\n    'WEBP',\n    100,\n    0,\n    (uri) => {\n      callback(uri.toString());\n    },\n    'base64'\n  );\n};\n"
  },
  {
    "path": "common/lib/rgba.ts",
    "content": "import { RgbaColor } from 'react-colorful';\n\nexport const getStringFromRgba = (rgba: RgbaColor) =>\n  `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`;\n"
  },
  {
    "path": "common/lib/socket.ts",
    "content": "import { io, Socket } from 'socket.io-client';\n\nimport { ClientToServerEvents, ServerToClientEvents } from '../types/global';\n\nexport const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();\n"
  },
  {
    "path": "common/recoil/background/background.atom.ts",
    "content": "import { atom } from 'recoil';\n\nexport const backgroundAtom = atom<{ mode: 'dark' | 'light'; lines: boolean }>({\n  key: 'bg',\n  default: {\n    mode: 'light',\n    lines: true,\n  },\n});\n"
  },
  {
    "path": "common/recoil/background/background.hooks.ts",
    "content": "import { useEffect } from 'react';\n\nimport { useRecoilValue, useSetRecoilState } from 'recoil';\n\nimport { backgroundAtom } from './background.atom';\n\nexport const useBackground = () => {\n  const bg = useRecoilValue(backgroundAtom);\n\n  useEffect(() => {\n    const root = window.document.documentElement;\n\n    if (bg.mode === 'dark') {\n      root.classList.remove('light');\n      root.classList.add('dark');\n    } else {\n      root.classList.remove('dark');\n      root.classList.add('light');\n    }\n  }, [bg.mode]);\n\n  return bg;\n};\n\nexport const useSetBackground = () => {\n  const setBg = useSetRecoilState(backgroundAtom);\n\n  const setBackground = (mode: 'dark' | 'light', lines: boolean) => {\n    setBg({\n      mode,\n      lines,\n    });\n  };\n\n  return setBackground;\n};\n"
  },
  {
    "path": "common/recoil/background/index.ts",
    "content": "import { backgroundAtom } from './background.atom';\nimport { useBackground, useSetBackground } from './background.hooks';\n\nexport default backgroundAtom;\n\nexport { useBackground, useSetBackground };\n"
  },
  {
    "path": "common/recoil/options/index.ts",
    "content": "/* eslint-disable import/no-cycle */\nimport { optionsAtom } from './options.atom';\nimport {\n  useOptions,\n  useSetOptions,\n  useOptionsValue,\n  useSetSelection,\n} from './options.hooks';\n\nexport default optionsAtom;\n\nexport { useOptions, useOptionsValue, useSetOptions, useSetSelection };\n"
  },
  {
    "path": "common/recoil/options/options.atom.ts",
    "content": "import { atom } from 'recoil';\n\nimport { CtxOptions } from '@/common/types/global';\n\nexport const optionsAtom = atom<CtxOptions>({\n  key: 'options',\n  default: {\n    lineColor: { r: 0, g: 0, b: 0, a: 1 },\n    fillColor: { r: 0, g: 0, b: 0, a: 0 },\n    lineWidth: 5,\n    mode: 'draw',\n    shape: 'line',\n    selection: null,\n  },\n});\n"
  },
  {
    "path": "common/recoil/options/options.hooks.ts",
    "content": "import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';\n\nimport { optionsAtom } from './options.atom';\n\nexport const useOptionsValue = () => {\n  const options = useRecoilValue(optionsAtom);\n\n  return options;\n};\n\nexport const useSetOptions = () => {\n  const setOptions = useSetRecoilState(optionsAtom);\n\n  return setOptions;\n};\n\nexport const useOptions = () => {\n  const options = useRecoilState(optionsAtom);\n\n  return options;\n};\n\nexport const useSetSelection = () => {\n  const setOptions = useSetOptions();\n\n  const setSelection = (rect: {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  }) => {\n    setOptions((prev) => ({ ...prev, selection: rect }));\n  };\n\n  const clearSelection = () => {\n    setOptions((prev) => ({ ...prev, selection: null }));\n  };\n\n  return { setSelection, clearSelection };\n};\n"
  },
  {
    "path": "common/recoil/room/index.ts",
    "content": "import { roomAtom } from './room.atom';\nimport { useRoom, useSetRoomId, useSetUsers, useMyMoves } from './room.hooks';\n\nexport default roomAtom;\n\nexport { useRoom, useSetRoomId, useSetUsers, useMyMoves };\n"
  },
  {
    "path": "common/recoil/room/room.atom.ts",
    "content": "import { atom } from 'recoil';\n\nimport { ClientRoom } from '@/common/types/global';\n\nexport const DEFAULT_ROOM = {\n  id: '',\n  users: new Map(),\n  usersMoves: new Map(),\n  movesWithoutUser: [],\n  myMoves: [],\n};\n\nexport const roomAtom = atom<ClientRoom>({\n  key: 'room',\n  default: DEFAULT_ROOM,\n});\n"
  },
  {
    "path": "common/recoil/room/room.hooks.ts",
    "content": "import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';\n\nimport { getNextColor } from '@/common/lib/getNextColor';\nimport { Move } from '@/common/types/global';\n\nimport { DEFAULT_ROOM, roomAtom } from './room.atom';\n\nexport const useRoom = () => {\n  const room = useRecoilValue(roomAtom);\n\n  return room;\n};\n\nexport const useSetRoom = () => {\n  const setRoom = useSetRecoilState(roomAtom);\n\n  return setRoom;\n};\n\nexport const useSetRoomId = () => {\n  const setRoomId = useSetRecoilState(roomAtom);\n\n  const handleSetRoomId = (id: string) => {\n    setRoomId({ ...DEFAULT_ROOM, id });\n  };\n\n  return handleSetRoomId;\n};\n\nexport const useSetUsers = () => {\n  const setRoom = useSetRecoilState(roomAtom);\n\n  const handleAddUser = (userId: string, name: string) => {\n    setRoom((prev) => {\n      const newUsers = prev.users;\n      const newUsersMoves = prev.usersMoves;\n\n      const color = getNextColor([...newUsers.values()].pop()?.color);\n\n      newUsers.set(userId, {\n        name,\n        color,\n      });\n      newUsersMoves.set(userId, []);\n\n      return { ...prev, users: newUsers, usersMoves: newUsersMoves };\n    });\n  };\n\n  const handleRemoveUser = (userId: string) => {\n    setRoom((prev) => {\n      const newUsers = prev.users;\n      const newUsersMoves = prev.usersMoves;\n\n      const userMoves = newUsersMoves.get(userId);\n\n      newUsers.delete(userId);\n      newUsersMoves.delete(userId);\n      return {\n        ...prev,\n        users: newUsers,\n        usersMoves: newUsersMoves,\n        movesWithoutUser: [...prev.movesWithoutUser, ...(userMoves || [])],\n      };\n    });\n  };\n\n  const handleAddMoveToUser = (userId: string, moves: Move) => {\n    setRoom((prev) => {\n      const newUsersMoves = prev.usersMoves;\n      const oldMoves = prev.usersMoves.get(userId);\n\n      newUsersMoves.set(userId, [...(oldMoves || []), moves]);\n      return { ...prev, usersMoves: newUsersMoves };\n    });\n  };\n\n  const handleRemoveMoveFromUser = (userId: string) => {\n    setRoom((prev) => {\n      const newUsersMoves = prev.usersMoves;\n      const oldMoves = prev.usersMoves.get(userId);\n      oldMoves?.pop();\n\n      newUsersMoves.set(userId, oldMoves || []);\n      return { ...prev, usersMoves: newUsersMoves };\n    });\n  };\n\n  return {\n    handleAddUser,\n    handleRemoveUser,\n    handleAddMoveToUser,\n    handleRemoveMoveFromUser,\n  };\n};\n\nexport const useMyMoves = () => {\n  const [room, setRoom] = useRecoilState(roomAtom);\n\n  const handleAddMyMove = (move: Move) => {\n    setRoom((prev) => {\n      if (prev.myMoves[prev.myMoves.length - 1]?.options.mode === 'select')\n        return {\n          ...prev,\n          myMoves: [...prev.myMoves.slice(0, prev.myMoves.length - 1), move],\n        };\n\n      return { ...prev, myMoves: [...prev.myMoves, move] };\n    });\n  };\n\n  const handleRemoveMyMove = () => {\n    const newMoves = [...room.myMoves];\n    const move = newMoves.pop();\n\n    setRoom((prev) => ({ ...prev, myMoves: newMoves }));\n\n    return move;\n  };\n\n  return { handleAddMyMove, handleRemoveMyMove, myMoves: room.myMoves };\n};\n"
  },
  {
    "path": "common/recoil/savedMoves/index.ts",
    "content": "import { savedMovesAtom } from './savedMoves.atom';\nimport { useSavedMoves, useSetSavedMoves } from './savedMoves.hooks';\n\nexport default savedMovesAtom;\n\nexport { useSavedMoves, useSetSavedMoves };\n"
  },
  {
    "path": "common/recoil/savedMoves/savedMoves.atom.ts",
    "content": "import { atom } from 'recoil';\n\nimport { Move } from '@/common/types/global';\n\nexport const savedMovesAtom = atom<Move[]>({\n  key: 'saved_moves',\n  default: [],\n});\n"
  },
  {
    "path": "common/recoil/savedMoves/savedMoves.hooks.ts",
    "content": "import { useRecoilValue, useSetRecoilState } from 'recoil';\n\nimport { Move } from '@/common/types/global';\n\nimport { savedMovesAtom } from './savedMoves.atom';\n\nexport const useSetSavedMoves = () => {\n  const setSavedMoves = useSetRecoilState(savedMovesAtom);\n\n  const addSavedMove = (move: Move) => {\n    if (move.options.mode === 'select') return;\n\n    setSavedMoves((prevMoves) => [move, ...prevMoves]);\n  };\n\n  const removeSavedMove = () => {\n    let move: Move | undefined;\n\n    setSavedMoves((prevMoves) => {\n      move = prevMoves.at(0);\n\n      return prevMoves.slice(1);\n    });\n\n    return move;\n  };\n\n  const clearSavedMoves = () => {\n    setSavedMoves([]);\n  };\n\n  return { addSavedMove, removeSavedMove, clearSavedMoves };\n};\n\nexport const useSavedMoves = () => {\n  const savedMoves = useRecoilValue(savedMovesAtom);\n\n  return savedMoves;\n};\n"
  },
  {
    "path": "common/styles/global.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n* {\n  @apply font-montserrat font-medium focus:outline-none focus:ring focus:ring-red-500;\n}\n\n@layer components {\n  .btn-icon {\n    @apply flex h-7 w-7 items-center justify-center rounded-md text-xl text-white transition-all hover:scale-125 active:scale-100 disabled:opacity-25;\n  }\n\n  .btn {\n    @apply rounded-xl bg-black p-5 py-1 text-white transition-all hover:scale-105 active:scale-100;\n  }\n\n  .input {\n    @apply rounded-xl border p-5 py-1;\n  }\n\n  .overflow-overlay {\n    overflow-y: scroll;\n    overflow-y: overlay;\n  }\n}\n\n.drag {\n  cursor: grab;\n}\n\nbody {\n  min-height: 100vh;\n  min-height: -webkit-fill-available;\n  overscroll-behavior: contain;\n  cursor: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" style=\"transform: rotate(-90deg); stroke: white; stroke-width: 1px;\" version=\"1.1\" width=\"16\" height=\"16\"><path d=\"M14.082 2.182a.5.5 0 0 1 .103.557L8.528 15.467a.5.5 0 0 1-.917-.007L5.57 10.694.803 8.652a.5.5 0 0 1-.006-.916l12.728-5.657a.5.5 0 0 1 .556.103z\"></path></svg>'),\n    auto;\n}\nhtml {\n  height: -webkit-fill-available;\n}\n\n#__next,\n#portal {\n  z-index: 0;\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  overflow-x: hidden;\n  overflow-y: scroll;\n  overflow-y: overlay;\n}\n\n#portal {\n  z-index: 1;\n}\n\ninput[type='range']::-webkit-slider-thumb {\n  -webkit-appearance: none;\n  height: 1.2rem;\n  width: 1.2rem;\n  border-radius: 50%;\n  background: #000;\n  cursor: pointer;\n}\n\n/* All the same stuff for Firefox */\ninput[type='range']::-moz-range-thumb {\n  height: 1.2rem;\n  width: 1.2rem;\n  border-radius: 50%;\n  background: #000;\n  cursor: pointer;\n}\n\n/* All the same stuff for IE */\ninput[type='range']::-ms-thumb {\n  height: 1.2rem;\n  width: 1.2rem;\n  border-radius: 50%;\n  background: #000;\n  cursor: pointer;\n}\n\n*::-webkit-scrollbar-track {\n  -webkit-box-shadow: none !important;\n  background-color: transparent !important;\n}\n\n*::-webkit-scrollbar {\n  width: 6px !important;\n  position: absolute;\n  background-color: transparent;\n}\n\n*::-webkit-scrollbar-thumb {\n  @apply bg-black/75;\n}\n"
  },
  {
    "path": "common/types/global.ts",
    "content": "import { RgbaColor } from 'react-colorful';\n\nexport type Shape = 'line' | 'circle' | 'rect' | 'image';\nexport type CtxMode = 'eraser' | 'draw' | 'select';\n\nexport interface CtxOptions {\n  lineWidth: number;\n  lineColor: RgbaColor;\n  fillColor: RgbaColor;\n  shape: Shape;\n  mode: CtxMode;\n  selection: {\n    x: number;\n    y: number;\n    width: number;\n    height: number;\n  } | null;\n}\n\nexport interface Move {\n  circle: {\n    cX: number;\n    cY: number;\n    radiusX: number;\n    radiusY: number;\n  };\n  rect: {\n    width: number;\n    height: number;\n  };\n  img: {\n    base64: string;\n  };\n  path: [number, number][];\n  options: CtxOptions;\n  timestamp: number;\n  id: string;\n}\n\nexport type Room = {\n  usersMoves: Map<string, Move[]>;\n  drawed: Move[];\n  users: Map<string, string>;\n};\n\nexport interface User {\n  name: string;\n  color: string;\n}\n\nexport interface ClientRoom {\n  id: string;\n  usersMoves: Map<string, Move[]>;\n  movesWithoutUser: Move[];\n  myMoves: Move[];\n  users: Map<string, User>;\n}\n\nexport interface MessageType {\n  userId: string;\n  username: string;\n  color: string;\n  msg: string;\n  id: number;\n}\n\nexport interface ServerToClientEvents {\n  room_exists: (exists: boolean) => void;\n  joined: (roomId: string, failed?: boolean) => void;\n  room: (room: Room, usersMovesToParse: string, usersToParse: string) => void;\n  created: (roomId: string) => void;\n  your_move: (move: Move) => void;\n  user_draw: (move: Move, userId: string) => void;\n  user_undo(userId: string): void;\n  mouse_moved: (x: number, y: number, userId: string) => void;\n  new_user: (userId: string, username: string) => void;\n  user_disconnected: (userId: string) => void;\n  new_msg: (userId: string, msg: string) => void;\n}\n\nexport interface ClientToServerEvents {\n  check_room: (roomId: string) => void;\n  draw: (move: Move) => void;\n  mouse_move: (x: number, y: number) => void;\n  undo: () => void;\n  create_room: (username: string) => void;\n  join_room: (room: string, username: string) => void;\n  joined_room: () => void;\n  leave_room: () => void;\n  send_msg: (msg: string) => void;\n}\n"
  },
  {
    "path": "modules/home/components/Home.tsx",
    "content": "import { FormEvent, useEffect, useState } from 'react';\n\nimport { useRouter } from 'next/router';\n\nimport { socket } from '@/common/lib/socket';\nimport { useSetRoomId } from '@/common/recoil/room';\nimport { useModal } from '@/modules/modal';\n\nimport NotFoundModal from '../modals/NotFound';\n\nconst Home = () => {\n  const { openModal } = useModal();\n  const setAtomRoomId = useSetRoomId();\n\n  const [roomId, setRoomId] = useState('');\n  const [username, setUsername] = useState('');\n\n  const router = useRouter();\n\n  useEffect(() => {\n    document.body.style.backgroundColor = 'white';\n  }, []);\n\n  useEffect(() => {\n    socket.on('created', (roomIdFromServer) => {\n      setAtomRoomId(roomIdFromServer);\n      router.push(roomIdFromServer);\n    });\n\n    const handleJoinedRoom = (roomIdFromServer: string, failed?: boolean) => {\n      if (!failed) {\n        setAtomRoomId(roomIdFromServer);\n        router.push(roomIdFromServer);\n      } else {\n        openModal(<NotFoundModal id={roomId} />);\n      }\n    };\n\n    socket.on('joined', handleJoinedRoom);\n\n    return () => {\n      socket.off('created');\n      socket.off('joined', handleJoinedRoom);\n    };\n  }, [openModal, roomId, router, setAtomRoomId]);\n\n  useEffect(() => {\n    socket.emit('leave_room');\n    setAtomRoomId('');\n  }, [setAtomRoomId]);\n\n  const handleCreateRoom = () => {\n    socket.emit('create_room', username);\n  };\n\n  const handleJoinRoom = (e: FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n\n    if (roomId) socket.emit('join_room', roomId, username);\n  };\n\n  return (\n    <div className=\"flex flex-col items-center py-24\">\n      <h1 className=\"text-5xl font-extrabold leading-tight sm:text-extra\">\n        Collabio\n      </h1>\n      <h3 className=\"text-xl sm:text-2xl\">Real-time whiteboard</h3>\n\n      <div className=\"mt-10 flex flex-col gap-2\">\n        <label className=\"self-start font-bold leading-tight\">\n          Enter your name\n        </label>\n        <input\n          className=\"input\"\n          id=\"room-id\"\n          placeholder=\"Username...\"\n          value={username}\n          onChange={(e) => setUsername(e.target.value.slice(0, 15))}\n        />\n      </div>\n\n      <div className=\"my-8 h-px w-96 bg-zinc-200\" />\n\n      <form\n        className=\"flex flex-col items-center gap-3\"\n        onSubmit={handleJoinRoom}\n      >\n        <label htmlFor=\"room-id\" className=\"self-start font-bold leading-tight\">\n          Enter room id\n        </label>\n        <input\n          className=\"input\"\n          id=\"room-id\"\n          placeholder=\"Room id...\"\n          value={roomId}\n          onChange={(e) => setRoomId(e.target.value)}\n        />\n        <button className=\"btn\" type=\"submit\">\n          Join\n        </button>\n      </form>\n\n      <div className=\"my-8 flex w-96 items-center gap-2\">\n        <div className=\"h-px w-full bg-zinc-200\" />\n        <p className=\"text-zinc-400\">or</p>\n        <div className=\"h-px w-full bg-zinc-200\" />\n      </div>\n\n      <div className=\"flex flex-col items-center gap-2\">\n        <h5 className=\"self-start font-bold leading-tight\">Create new room</h5>\n\n        <button className=\"btn\" onClick={handleCreateRoom}>\n          Create\n        </button>\n      </div>\n    </div>\n  );\n};\n\nexport default Home;\n"
  },
  {
    "path": "modules/home/index.ts",
    "content": "import Home from './components/Home';\n\nexport default Home;\n"
  },
  {
    "path": "modules/home/modals/NotFound.tsx",
    "content": "import { AiOutlineClose } from 'react-icons/ai';\n\nimport { useModal } from '@/modules/modal';\n\nconst NotFoundModal = ({ id }: { id: string }) => {\n  const { closeModal } = useModal();\n\n  return (\n    <div className=\"relative flex flex-col items-center rounded-md bg-white p-10 \">\n      <button onClick={closeModal} className=\"absolute top-5 right-5\">\n        <AiOutlineClose />\n      </button>\n      <h2 className=\"text-lg font-bold\">\n        Room with id &quot;{id}&quot; does not exist or is full!\n      </h2>\n      <h3>Try to join room later.</h3>\n    </div>\n  );\n};\n\nexport default NotFoundModal;\n"
  },
  {
    "path": "modules/modal/animations/ModalManager.animations.ts",
    "content": "export const bgAnimation = {\n  closed: { opacity: 0 },\n  opened: { opacity: 1 },\n};\n\nexport const modalAnimation = {\n  closed: { y: -100 },\n  opened: { y: 0 },\n  exited: { y: 100 },\n};\n"
  },
  {
    "path": "modules/modal/components/ModalManager.tsx",
    "content": "import { useEffect, useState } from 'react';\n\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { useRecoilState } from 'recoil';\n\nimport Portal from '@/common/components/portal/components/Portal';\n\nimport {\n  bgAnimation,\n  modalAnimation,\n} from '../animations/ModalManager.animations';\nimport { modalAtom } from '../recoil/modal.atom';\n\nconst ModalManager = () => {\n  const [{ opened, modal }, setModal] = useRecoilState(modalAtom);\n\n  const [portalNode, setPortalNode] = useState<HTMLElement>();\n\n  useEffect(() => {\n    if (!portalNode) {\n      const node = document.getElementById('portal');\n      if (node) setPortalNode(node);\n      return;\n    }\n\n    if (opened) {\n      portalNode.style.pointerEvents = 'all';\n    } else {\n      portalNode.style.pointerEvents = 'none';\n    }\n  }, [opened, portalNode]);\n\n  return (\n    <Portal>\n      <motion.div\n        className=\"absolute z-40 flex min-h-full w-full items-center justify-center bg-black/80\"\n        onClick={() => setModal({ modal: <></>, opened: false })}\n        variants={bgAnimation}\n        initial=\"closed\"\n        animate={opened ? 'opened' : 'closed'}\n      >\n        <AnimatePresence>\n          {opened && (\n            <motion.div\n              variants={modalAnimation}\n              initial=\"closed\"\n              animate=\"opened\"\n              exit=\"exited\"\n              onClick={(e) => e.stopPropagation()}\n              className=\"p-6\"\n            >\n              {modal}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.div>\n    </Portal>\n  );\n};\n\nexport default ModalManager;\n"
  },
  {
    "path": "modules/modal/index.ts",
    "content": "import ModalManager from './components/ModalManager';\nimport { useModal } from './recoil/modal.hooks';\n\nexport { ModalManager, useModal };\n"
  },
  {
    "path": "modules/modal/recoil/modal.atom.tsx",
    "content": "import { atom } from 'recoil';\n\nexport const modalAtom = atom<{\n  modal: JSX.Element | JSX.Element[];\n  opened: boolean;\n}>({\n  key: 'modal',\n  default: {\n    modal: <></>,\n    opened: false,\n  },\n});\n"
  },
  {
    "path": "modules/modal/recoil/modal.hooks.tsx",
    "content": "import { useSetRecoilState } from 'recoil';\n\nimport { modalAtom } from './modal.atom';\n\nconst useModal = () => {\n  const setModal = useSetRecoilState(modalAtom);\n\n  const openModal = (modal: JSX.Element | JSX.Element[]) =>\n    setModal({ modal, opened: true });\n\n  const closeModal = () => setModal({ modal: <></>, opened: false });\n\n  return { openModal, closeModal };\n};\n\nexport { useModal };\n"
  },
  {
    "path": "modules/room/components/NameInput.tsx",
    "content": "import { FormEvent, useEffect, useState } from 'react';\n\nimport { useRouter } from 'next/router';\n\nimport { socket } from '@/common/lib/socket';\nimport { useSetRoomId } from '@/common/recoil/room';\nimport NotFoundModal from '@/modules/home/modals/NotFound';\nimport { useModal } from '@/modules/modal';\n\nconst NameInput = () => {\n  const setRoomId = useSetRoomId();\n  const { openModal } = useModal();\n\n  const [name, setName] = useState('');\n\n  const router = useRouter();\n  const roomId = (router.query.roomId || '').toString();\n\n  useEffect(() => {\n    if (!roomId) return;\n\n    socket.emit('check_room', roomId);\n\n    socket.on('room_exists', (exists) => {\n      if (!exists) {\n        router.push('/');\n      }\n    });\n\n    // eslint-disable-next-line consistent-return\n    return () => {\n      socket.off('room_exists');\n    };\n  }, [roomId, router]);\n\n  useEffect(() => {\n    const handleJoined = (roomIdFromServer: string, failed?: boolean) => {\n      if (failed) {\n        router.push('/');\n        openModal(<NotFoundModal id={roomIdFromServer} />);\n      } else setRoomId(roomIdFromServer);\n    };\n\n    socket.on('joined', handleJoined);\n\n    return () => {\n      socket.off('joined', handleJoined);\n    };\n  }, [openModal, router, setRoomId]);\n\n  const handleJoinRoom = (e: FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n\n    socket.emit('join_room', roomId, name);\n  };\n\n  return (\n    <form\n      className=\"my-24 flex flex-col items-center\"\n      onSubmit={handleJoinRoom}\n    >\n      <h1 className=\"text-5xl font-extrabold leading-tight sm:text-extra\">\n        Collabio\n      </h1>\n      <h3 className=\"text-xl sm:text-2xl\">Real-time whiteboard</h3>\n\n      <div className=\"mt-10 mb-3 flex flex-col gap-2\">\n        <label className=\"self-start font-bold leading-tight\">\n          Enter your name\n        </label>\n        <input\n          className=\"rounded-xl border p-5 py-1\"\n          id=\"room-id\"\n          placeholder=\"Username...\"\n          value={name}\n          onChange={(e) => setName(e.target.value.slice(0, 15))}\n        />\n      </div>\n\n      <button className=\"btn\" type=\"submit\">\n        Enter room\n      </button>\n    </form>\n  );\n};\n\nexport default NameInput;\n"
  },
  {
    "path": "modules/room/components/Room.tsx",
    "content": "import { useRoom } from '@/common/recoil/room';\n\nimport RoomContextProvider from '../context/Room.context';\nimport Board from '../modules/board';\nimport Chat from '../modules/chat';\nimport ToolBar from '../modules/toolbar';\nimport NameInput from './NameInput';\nimport UserList from './UserList';\n\nconst Room = () => {\n  const room = useRoom();\n\n  if (!room.id) return <NameInput />;\n\n  return (\n    <RoomContextProvider>\n      <div className=\"relative h-full w-full overflow-hidden\">\n        <UserList />\n        <ToolBar />\n        <Board />\n        <Chat />\n      </div>\n    </RoomContextProvider>\n  );\n};\n\nexport default Room;\n"
  },
  {
    "path": "modules/room/components/UserList.tsx",
    "content": "import { useRoom } from '@/common/recoil/room';\n\nconst UserList = () => {\n  const { users } = useRoom();\n\n  return (\n    <div className=\"pointer-events-none absolute z-30 flex p-5\">\n      {[...users.keys()].map((userId, index) => {\n        return (\n          <div\n            key={userId}\n            className=\"flex h-5 w-5 select-none items-center justify-center rounded-full text-xs text-white md:h-8 md:w-8 md:text-base lg:h-12 lg:w-12\"\n            style={{\n              backgroundColor: users.get(userId)?.color || 'black',\n              marginLeft: index !== 0 ? '-0.5rem' : 0,\n            }}\n          >\n            {users.get(userId)?.name.split('')[0] || 'A'}\n          </div>\n        );\n      })}\n    </div>\n  );\n};\n\nexport default UserList;\n"
  },
  {
    "path": "modules/room/context/Room.context.tsx",
    "content": "import {\n  createContext,\n  Dispatch,\n  ReactChild,\n  RefObject,\n  SetStateAction,\n  useEffect,\n  useRef,\n  useState,\n} from 'react';\n\nimport { MotionValue, useMotionValue } from 'framer-motion';\nimport { toast } from 'react-toastify';\n\nimport { COLORS_ARRAY } from '@/common/constants/colors';\nimport { socket } from '@/common/lib/socket';\nimport { useSetUsers } from '@/common/recoil/room';\nimport { useSetRoom, useRoom } from '@/common/recoil/room/room.hooks';\nimport { Move, User } from '@/common/types/global';\n\nexport const roomContext = createContext<{\n  x: MotionValue<number>;\n  y: MotionValue<number>;\n  undoRef: RefObject<HTMLButtonElement>;\n  redoRef: RefObject<HTMLButtonElement>;\n  canvasRef: RefObject<HTMLCanvasElement>;\n  bgRef: RefObject<HTMLCanvasElement>;\n  selectionRefs: RefObject<HTMLButtonElement[]>;\n  minimapRef: RefObject<HTMLCanvasElement>;\n  moveImage: { base64: string; x?: number; y?: number };\n  setMoveImage: Dispatch<\n    SetStateAction<{\n      base64: string;\n      x?: number | undefined;\n      y?: number | undefined;\n    }>\n  >;\n}>(null!);\n\nconst RoomContextProvider = ({ children }: { children: ReactChild }) => {\n  const setRoom = useSetRoom();\n  const { users } = useRoom();\n  const { handleAddUser, handleRemoveUser } = useSetUsers();\n\n  const undoRef = useRef<HTMLButtonElement>(null);\n  const redoRef = useRef<HTMLButtonElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const bgRef = useRef<HTMLCanvasElement>(null);\n  const minimapRef = useRef<HTMLCanvasElement>(null);\n  const selectionRefs = useRef<HTMLButtonElement[]>([]);\n\n  const [moveImage, setMoveImage] = useState<{\n    base64: string;\n    x?: number;\n    y?: number;\n  }>({ base64: '' });\n\n  useEffect(() => {\n    if (moveImage.base64 && !moveImage.x && !moveImage.y)\n      setMoveImage({ base64: moveImage.base64, x: 50, y: 50 });\n  }, [moveImage]);\n\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n\n  useEffect(() => {\n    socket.on('room', (room, usersMovesToParse, usersToParse) => {\n      const usersMoves = new Map<string, Move[]>(JSON.parse(usersMovesToParse));\n      const usersParsed = new Map<string, string>(JSON.parse(usersToParse));\n\n      const newUsers = new Map<string, User>();\n\n      usersParsed.forEach((name, id) => {\n        if (id === socket.id) return;\n\n        const index = [...usersParsed.keys()].indexOf(id);\n\n        const color = COLORS_ARRAY[index % COLORS_ARRAY.length];\n\n        newUsers.set(id, {\n          name,\n          color,\n        });\n      });\n\n      setRoom((prev) => ({\n        ...prev,\n        users: newUsers,\n        usersMoves,\n        movesWithoutUser: room.drawed,\n      }));\n    });\n\n    socket.on('new_user', (userId, username) => {\n      toast(`${username} has joined the room.`, {\n        position: 'top-center',\n        theme: 'colored',\n      });\n\n      handleAddUser(userId, username);\n    });\n\n    socket.on('user_disconnected', (userId) => {\n      toast(`${users.get(userId)?.name || 'Anonymous'} has left the room.`, {\n        position: 'top-center',\n        theme: 'colored',\n      });\n\n      handleRemoveUser(userId);\n    });\n\n    return () => {\n      socket.off('room');\n      socket.off('new_user');\n      socket.off('user_disconnected');\n    };\n  }, [handleAddUser, handleRemoveUser, setRoom, users]);\n\n  return (\n    <roomContext.Provider\n      value={{\n        x,\n        y,\n        bgRef,\n        undoRef,\n        redoRef,\n        canvasRef,\n        setMoveImage,\n        moveImage,\n        minimapRef,\n        selectionRefs,\n      }}\n    >\n      {children}\n    </roomContext.Provider>\n  );\n};\n\nexport default RoomContextProvider;\n"
  },
  {
    "path": "modules/room/hooks/useMoveImage.ts",
    "content": "import { useContext } from 'react';\n\nimport { roomContext } from '../context/Room.context';\n\nexport const useMoveImage = () => {\n  const { moveImage, setMoveImage } = useContext(roomContext);\n\n  return { moveImage, setMoveImage };\n};\n"
  },
  {
    "path": "modules/room/hooks/useMovesHandlers.ts",
    "content": "import { useEffect, useMemo } from 'react';\n\nimport { getStringFromRgba } from '@/common/lib/rgba';\nimport { socket } from '@/common/lib/socket';\nimport { useBackground } from '@/common/recoil/background';\nimport { useSetSelection } from '@/common/recoil/options';\nimport { useMyMoves, useRoom } from '@/common/recoil/room';\nimport { useSetSavedMoves } from '@/common/recoil/savedMoves';\nimport { Move } from '@/common/types/global';\n\nimport { useCtx } from '../modules/board/hooks/useCtx';\nimport { useRefs } from './useRefs';\nimport { useSelection } from '../modules/board/hooks/useSelection';\n\nlet prevMovesLength = 0;\n\nexport const useMovesHandlers = (clearOnYourMove: () => void) => {\n  const { canvasRef, minimapRef, bgRef } = useRefs();\n  const room = useRoom();\n  const { handleAddMyMove, handleRemoveMyMove } = useMyMoves();\n  const { addSavedMove, removeSavedMove } = useSetSavedMoves();\n  const ctx = useCtx();\n  const bg = useBackground();\n  const { clearSelection } = useSetSelection();\n\n  const sortedMoves = useMemo(() => {\n    const { usersMoves, movesWithoutUser, myMoves } = room;\n\n    const moves = [...movesWithoutUser, ...myMoves];\n\n    usersMoves.forEach((userMoves) => moves.push(...userMoves));\n\n    moves.sort((a, b) => a.timestamp - b.timestamp);\n\n    return moves;\n  }, [room]);\n\n  const copyCanvasToSmall = () => {\n    if (canvasRef.current && minimapRef.current && bgRef.current) {\n      const smallCtx = minimapRef.current.getContext('2d');\n      if (smallCtx) {\n        smallCtx.clearRect(0, 0, smallCtx.canvas.width, smallCtx.canvas.height);\n        smallCtx.drawImage(\n          bgRef.current,\n          0,\n          0,\n          smallCtx.canvas.width,\n          smallCtx.canvas.height\n        );\n        smallCtx.drawImage(\n          canvasRef.current,\n          0,\n          0,\n          smallCtx.canvas.width,\n          smallCtx.canvas.height\n        );\n      }\n    }\n  };\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  useEffect(() => copyCanvasToSmall(), [bg]);\n\n  const drawMove = (move: Move, image?: HTMLImageElement) => {\n    const { path } = move;\n\n    if (!ctx || !path.length) return;\n\n    const moveOptions = move.options;\n\n    if (moveOptions.mode === 'select') return;\n\n    ctx.lineWidth = moveOptions.lineWidth;\n    ctx.strokeStyle = getStringFromRgba(moveOptions.lineColor);\n    ctx.fillStyle = getStringFromRgba(moveOptions.fillColor);\n    if (moveOptions.mode === 'eraser')\n      ctx.globalCompositeOperation = 'destination-out';\n    else ctx.globalCompositeOperation = 'source-over';\n\n    if (moveOptions.shape === 'image' && image)\n      ctx.drawImage(image, path[0][0], path[0][1]);\n\n    switch (moveOptions.shape) {\n      case 'line': {\n        ctx.beginPath();\n        path.forEach(([x, y]) => {\n          ctx.lineTo(x, y);\n        });\n\n        ctx.stroke();\n        ctx.closePath();\n        break;\n      }\n\n      case 'circle': {\n        const { cX, cY, radiusX, radiusY } = move.circle;\n\n        ctx.beginPath();\n        ctx.ellipse(cX, cY, radiusX, radiusY, 0, 0, 2 * Math.PI);\n        ctx.stroke();\n        ctx.fill();\n        ctx.closePath();\n        break;\n      }\n\n      case 'rect': {\n        const { width, height } = move.rect;\n\n        ctx.beginPath();\n\n        ctx.rect(path[0][0], path[0][1], width, height);\n        ctx.stroke();\n        ctx.fill();\n\n        ctx.closePath();\n        break;\n      }\n\n      default:\n        break;\n    }\n\n    copyCanvasToSmall();\n  };\n\n  const drawAllMoves = async () => {\n    if (!ctx) return;\n\n    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);\n\n    const images = await Promise.all(\n      sortedMoves\n        .filter((move) => move.options.shape === 'image')\n        .map((move) => {\n          return new Promise<HTMLImageElement>((resolve) => {\n            const img = new Image();\n            img.src = move.img.base64;\n            img.id = move.id;\n            img.addEventListener('load', () => resolve(img));\n          });\n        })\n    );\n\n    sortedMoves.forEach((move) => {\n      if (move.options.shape === 'image') {\n        const img = images.find((image) => image.id === move.id);\n        if (img) drawMove(move, img);\n      } else drawMove(move);\n    });\n\n    copyCanvasToSmall();\n  };\n\n  useSelection(drawAllMoves);\n\n  useEffect(() => {\n    socket.on('your_move', (move) => {\n      clearOnYourMove();\n      handleAddMyMove(move);\n      setTimeout(clearSelection, 100);\n    });\n\n    return () => {\n      socket.off('your_move');\n    };\n  }, [clearOnYourMove, clearSelection, handleAddMyMove]);\n\n  useEffect(() => {\n    if (prevMovesLength >= sortedMoves.length || !prevMovesLength) {\n      drawAllMoves();\n    } else {\n      const lastMove = sortedMoves[sortedMoves.length - 1];\n\n      if (lastMove.options.shape === 'image') {\n        const img = new Image();\n        img.src = lastMove.img.base64;\n        img.addEventListener('load', () => drawMove(lastMove, img));\n      } else drawMove(lastMove);\n    }\n\n    return () => {\n      prevMovesLength = sortedMoves.length;\n    };\n\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [sortedMoves]);\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const handleUndo = () => {\n    if (ctx) {\n      const move = handleRemoveMyMove();\n\n      if (move?.options.mode === 'select') clearSelection();\n      else if (move) {\n        addSavedMove(move);\n        socket.emit('undo');\n      }\n    }\n  };\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const handleRedo = () => {\n    if (ctx) {\n      const move = removeSavedMove();\n\n      if (move) {\n        socket.emit('draw', move);\n      }\n    }\n  };\n\n  useEffect(() => {\n    const handleUndoRedoKeyboard = (e: KeyboardEvent) => {\n      if (e.key === 'z' && e.ctrlKey) {\n        handleUndo();\n      } else if (e.key === 'y' && e.ctrlKey) {\n        handleRedo();\n      }\n    };\n\n    document.addEventListener('keydown', handleUndoRedoKeyboard);\n\n    return () => {\n      document.removeEventListener('keydown', handleUndoRedoKeyboard);\n    };\n  }, [handleUndo, handleRedo]);\n\n  return { handleUndo, handleRedo };\n};\n"
  },
  {
    "path": "modules/room/hooks/useRefs.ts",
    "content": "import { useContext } from 'react';\n\nimport { roomContext } from '../context/Room.context';\n\nexport const useRefs = () => {\n  const { undoRef, bgRef, canvasRef, minimapRef, redoRef, selectionRefs } =\n    useContext(roomContext);\n\n  return {\n    undoRef,\n    redoRef,\n    bgRef,\n    canvasRef,\n    minimapRef,\n    selectionRefs,\n  };\n};\n"
  },
  {
    "path": "modules/room/index.ts",
    "content": "import Room from './components/Room';\n\nexport default Room;\n"
  },
  {
    "path": "modules/room/modules/board/components/Background.tsx",
    "content": "import { RefObject, useEffect } from 'react';\n\nimport { motion } from 'framer-motion';\n\nimport { CANVAS_SIZE } from '@/common/constants/canvasSize';\nimport { useBackground } from '@/common/recoil/background';\n\nimport { useBoardPosition } from '../hooks/useBoardPosition';\n\nconst Background = ({ bgRef }: { bgRef: RefObject<HTMLCanvasElement> }) => {\n  const bg = useBackground();\n  const { x, y } = useBoardPosition();\n\n  useEffect(() => {\n    const ctx = bgRef.current?.getContext('2d');\n\n    if (ctx) {\n      ctx.fillStyle = bg.mode === 'dark' ? '#222' : '#fff';\n      ctx.fillRect(0, 0, CANVAS_SIZE.width, CANVAS_SIZE.height);\n\n      document.body.style.backgroundColor =\n        bg.mode === 'dark' ? '#222' : '#fff';\n\n      if (bg.lines) {\n        ctx.lineWidth = 1;\n        ctx.strokeStyle = bg.mode === 'dark' ? '#444' : '#ddd';\n        for (let i = 0; i < CANVAS_SIZE.height; i += 25) {\n          ctx.beginPath();\n          ctx.moveTo(0, i);\n          ctx.lineTo(ctx.canvas.width, i);\n          ctx.stroke();\n        }\n\n        for (let i = 0; i < CANVAS_SIZE.width; i += 25) {\n          ctx.beginPath();\n          ctx.moveTo(i, 0);\n          ctx.lineTo(i, ctx.canvas.height);\n          ctx.stroke();\n        }\n      }\n    }\n  }, [bgRef, bg]);\n\n  return (\n    <motion.canvas\n      ref={bgRef}\n      width={CANVAS_SIZE.width}\n      height={CANVAS_SIZE.height}\n      className=\"absolute top-0 bg-zinc-100\"\n      style={{ x, y }}\n    />\n  );\n};\n\nexport default Background;\n"
  },
  {
    "path": "modules/room/modules/board/components/Canvas.tsx",
    "content": "import { useEffect, useState } from 'react';\n\nimport { motion, useDragControls } from 'framer-motion';\nimport { BsArrowsMove } from 'react-icons/bs';\n\nimport { CANVAS_SIZE } from '@/common/constants/canvasSize';\nimport { useViewportSize } from '@/common/hooks/useViewportSize';\nimport { socket } from '@/common/lib/socket';\n\nimport { useMovesHandlers } from '../../../hooks/useMovesHandlers';\nimport { useRefs } from '../../../hooks/useRefs';\nimport { useBoardPosition } from '../hooks/useBoardPosition';\nimport { useCtx } from '../hooks/useCtx';\nimport { useDraw } from '../hooks/useDraw';\nimport { useSocketDraw } from '../hooks/useSocketDraw';\nimport Background from './Background';\nimport MiniMap from './Minimap';\n\nconst Canvas = () => {\n  const { canvasRef, bgRef, undoRef, redoRef } = useRefs();\n  const { width, height } = useViewportSize();\n  const { x, y } = useBoardPosition();\n  const ctx = useCtx();\n\n  const [dragging, setDragging] = useState(true);\n\n  const {\n    handleEndDrawing,\n    handleDraw,\n    handleStartDrawing,\n    drawing,\n    clearOnYourMove,\n  } = useDraw(dragging);\n  useSocketDraw(drawing);\n\n  const { handleUndo, handleRedo } = useMovesHandlers(clearOnYourMove);\n\n  const dragControls = useDragControls();\n\n  useEffect(() => {\n    setDragging(false);\n  }, []);\n\n  // SETUP\n  useEffect(() => {\n    const undoBtn = undoRef.current;\n    const redoBtn = redoRef.current;\n\n    undoBtn?.addEventListener('click', handleUndo);\n    redoBtn?.addEventListener('click', handleRedo);\n\n    return () => {\n      undoBtn?.removeEventListener('click', handleUndo);\n      redoBtn?.removeEventListener('click', handleRedo);\n    };\n  }, [canvasRef, dragging, handleRedo, handleUndo, redoRef, undoRef]);\n\n  useEffect(() => {\n    if (ctx) socket.emit('joined_room');\n  }, [ctx]);\n\n  return (\n    <div className=\"relative h-full w-full overflow-hidden\">\n      <motion.canvas\n        // SETTINGS\n        ref={canvasRef}\n        width={CANVAS_SIZE.width}\n        height={CANVAS_SIZE.height}\n        className={`absolute top-0 z-10 ${dragging && 'cursor-move'}`}\n        style={{ x, y }}\n        // DRAG\n        drag={dragging}\n        dragConstraints={{\n          left: -(CANVAS_SIZE.width - width),\n          right: 0,\n          top: -(CANVAS_SIZE.height - height),\n          bottom: 0,\n        }}\n        dragControls={dragControls}\n        dragElastic={0}\n        dragTransition={{ power: 0, timeConstant: 0 }}\n        // HANDLERS\n        onContextMenu={(e) => {\n          e.preventDefault();\n          e.stopPropagation();\n        }}\n        onMouseDown={(e) => {\n          if (e.button === 2) {\n            setDragging(true);\n            dragControls.start(e);\n          } else handleStartDrawing(e.clientX, e.clientY);\n        }}\n        onMouseUp={(e) => {\n          if (e.button === 2) setDragging(false);\n          else handleEndDrawing();\n        }}\n        onMouseMove={(e) => {\n          handleDraw(e.clientX, e.clientY, e.shiftKey);\n        }}\n        onTouchStart={(e) =>\n          handleStartDrawing(\n            e.changedTouches[0].clientX,\n            e.changedTouches[0].clientY\n          )\n        }\n        onTouchEnd={handleEndDrawing}\n        onTouchMove={(e) =>\n          handleDraw(e.changedTouches[0].clientX, e.changedTouches[0].clientY)\n        }\n      />\n      <Background bgRef={bgRef} />\n\n      <MiniMap dragging={dragging} />\n\n      <button\n        className={`absolute bottom-14 right-5 z-10 rounded-xl md:bottom-5 ${\n          dragging ? 'bg-green-500' : 'bg-zinc-300 text-black'\n        } p-3 text-lg text-white`}\n        onClick={() => setDragging((prev) => !prev)}\n      >\n        <BsArrowsMove />\n      </button>\n    </div>\n  );\n};\n\nexport default Canvas;\n"
  },
  {
    "path": "modules/room/modules/board/components/Minimap.tsx",
    "content": "import { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { motion, useMotionValue } from 'framer-motion';\n\nimport { CANVAS_SIZE } from '@/common/constants/canvasSize';\nimport { useViewportSize } from '@/common/hooks/useViewportSize';\n\nimport { useRefs } from '../../../hooks/useRefs';\nimport { useBoardPosition } from '../hooks/useBoardPosition';\n\nconst MiniMap = ({ dragging }: { dragging: boolean }) => {\n  const { minimapRef } = useRefs();\n  const boardPos = useBoardPosition();\n  const { width, height } = useViewportSize();\n\n  const [x, setX] = useState(0);\n  const [y, setY] = useState(0);\n\n  const [draggingMinimap, setDraggingMinimap] = useState(false);\n\n  useEffect(() => {\n    if (!draggingMinimap) {\n      const unsubscribe = boardPos.x.onChange(setX);\n      return unsubscribe;\n    }\n\n    return () => {};\n  }, [boardPos.x, draggingMinimap]);\n\n  useEffect(() => {\n    if (!draggingMinimap) {\n      const unsubscribe = boardPos.y.onChange(setY);\n      return unsubscribe;\n    }\n\n    return () => {};\n  }, [boardPos.y, draggingMinimap]);\n\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  const miniX = useMotionValue(0);\n  const miniY = useMotionValue(0);\n\n  const divider = useMemo(() => {\n    if (width > 1600) return 7;\n    if (width > 1000) return 10;\n    if (width > 600) return 14;\n    return 20;\n  }, [width]);\n\n  useEffect(() => {\n    miniX.onChange((newX) => {\n      if (!dragging) boardPos.x.set(Math.floor(-newX * divider));\n    });\n    miniY.onChange((newY) => {\n      if (!dragging) boardPos.y.set(Math.floor(-newY * divider));\n    });\n\n    return () => {\n      miniX.clearListeners();\n      miniY.clearListeners();\n    };\n  }, [boardPos.x, boardPos.y, divider, dragging, miniX, miniY]);\n\n  return (\n    <div\n      className=\"absolute right-10 top-10 z-30 overflow-hidden rounded-lg shadow-lg\"\n      style={{\n        width: CANVAS_SIZE.width / divider,\n        height: CANVAS_SIZE.height / divider,\n      }}\n      ref={containerRef}\n    >\n      <canvas\n        ref={minimapRef}\n        width={CANVAS_SIZE.width}\n        height={CANVAS_SIZE.height}\n        className=\"h-full w-full\"\n      />\n      <motion.div\n        drag\n        dragConstraints={containerRef}\n        dragElastic={0}\n        dragTransition={{ power: 0, timeConstant: 0 }}\n        onDragStart={() => setDraggingMinimap(true)}\n        onDragEnd={() => setDraggingMinimap(false)}\n        className=\"absolute top-0 left-0 cursor-grab rounded-lg border-2 border-red-500\"\n        style={{\n          width: width / divider,\n          height: height / divider,\n          x: miniX,\n          y: miniY,\n        }}\n        animate={{ x: -x / divider, y: -y / divider }}\n        transition={{ duration: 0 }}\n      />\n    </div>\n  );\n};\n\nexport default MiniMap;\n"
  },
  {
    "path": "modules/room/modules/board/components/MousePosition.tsx",
    "content": "import { useRef } from 'react';\n\nimport { motion } from 'framer-motion';\nimport { useInterval, useMouse } from 'react-use';\n\nimport { getPos } from '@/common/lib/getPos';\nimport { socket } from '@/common/lib/socket';\n\nimport { useBoardPosition } from '../hooks/useBoardPosition';\n\nconst MousePosition = () => {\n  const { x, y } = useBoardPosition();\n\n  const prevPosition = useRef({ x: 0, y: 0 });\n\n  const ref = useRef<HTMLDivElement>(null);\n\n  const { docX, docY } = useMouse(ref);\n\n  const touchDevice = window.matchMedia('(pointer: coarse)').matches;\n\n  useInterval(() => {\n    if (\n      (prevPosition.current.x !== docX || prevPosition.current.y !== docY) &&\n      !touchDevice\n    ) {\n      socket.emit('mouse_move', getPos(docX, x), getPos(docY, y));\n      prevPosition.current = { x: docX, y: docY };\n    }\n  }, 150);\n\n  if (touchDevice) return null;\n\n  return (\n    <motion.div\n      ref={ref}\n      className=\"pointer-events-none absolute top-0 left-0 z-50 select-none transition-colors dark:text-white\"\n      animate={{ x: docX + 15, y: docY + 15 }}\n      transition={{ duration: 0.05, ease: 'linear' }}\n    >\n      {getPos(docX, x).toFixed(0)} | {getPos(docY, y).toFixed(0)}\n    </motion.div>\n  );\n};\n\nexport default MousePosition;\n"
  },
  {
    "path": "modules/room/modules/board/components/MousesRenderer.tsx",
    "content": "import { socket } from '@/common/lib/socket';\nimport { useRoom } from '@/common/recoil/room';\n\nimport UserMouse from './UserMouse';\n\nconst MousesRenderer = () => {\n  const { users } = useRoom();\n\n  return (\n    <>\n      {[...users.keys()].map((userId) => {\n        if (userId === socket.id) return null;\n        return <UserMouse userId={userId} key={userId} />;\n      })}\n    </>\n  );\n};\n\nexport default MousesRenderer;\n"
  },
  {
    "path": "modules/room/modules/board/components/MoveImage.tsx",
    "content": "import { useEffect } from 'react';\n\nimport { motion, useMotionValue } from 'framer-motion';\nimport { AiOutlineCheck, AiOutlineClose } from 'react-icons/ai';\n\nimport { DEFAULT_MOVE } from '@/common/constants/defaultMove';\nimport { getPos } from '@/common/lib/getPos';\nimport { socket } from '@/common/lib/socket';\nimport { Move } from '@/common/types/global';\n\nimport { useMoveImage } from '../../../hooks/useMoveImage';\nimport { useBoardPosition } from '../hooks/useBoardPosition';\n\nconst MoveImage = () => {\n  const { x, y } = useBoardPosition();\n  const { moveImage, setMoveImage } = useMoveImage();\n\n  const imageX = useMotionValue(moveImage.x || 50);\n  const imageY = useMotionValue(moveImage.y || 50);\n\n  useEffect(() => {\n    if (moveImage.x) imageX.set(moveImage.x);\n    else imageX.set(50);\n    if (moveImage.y) imageY.set(moveImage.y);\n    else imageY.set(50);\n  }, [imageX, imageY, moveImage.x, moveImage.y]);\n\n  const handlePlaceImage = () => {\n    const [finalX, finalY] = [getPos(imageX.get(), x), getPos(imageY.get(), y)];\n\n    const move: Move = {\n      ...DEFAULT_MOVE,\n      img: { base64: moveImage.base64 },\n      path: [[finalX, finalY]],\n      options: {\n        ...DEFAULT_MOVE.options,\n        selection: null,\n        shape: 'image',\n      },\n    };\n\n    socket.emit('draw', move);\n\n    setMoveImage({ base64: '' });\n    imageX.set(50);\n    imageY.set(50);\n  };\n\n  if (!moveImage.base64) return null;\n\n  return (\n    <motion.div\n      drag\n      dragElastic={0}\n      dragTransition={{ power: 0.03, timeConstant: 50 }}\n      className=\"absolute top-0 z-20 cursor-grab\"\n      style={{ x: imageX, y: imageY }}\n    >\n      <div className=\"absolute bottom-full mb-2 flex gap-3\">\n        <button\n          className=\"rounded-full bg-gray-200 p-2\"\n          onClick={handlePlaceImage}\n        >\n          <AiOutlineCheck />\n        </button>\n        <button\n          className=\"rounded-full bg-gray-200 p-2\"\n          onClick={() => setMoveImage({ base64: '' })}\n        >\n          <AiOutlineClose />\n        </button>\n      </div>\n      <img\n        className=\"pointer-events-none\"\n        alt=\"image to place\"\n        src={moveImage.base64}\n      />\n    </motion.div>\n  );\n};\n\nexport default MoveImage;\n"
  },
  {
    "path": "modules/room/modules/board/components/SelectionBtns.tsx",
    "content": "import { useEffect, useState } from 'react';\n\nimport { AiOutlineDelete } from 'react-icons/ai';\nimport { BsArrowsMove } from 'react-icons/bs';\nimport { FiCopy } from 'react-icons/fi';\n\nimport { useOptionsValue } from '@/common/recoil/options';\n\nimport { useRefs } from '../../../hooks/useRefs';\nimport { useBoardPosition } from '../hooks/useBoardPosition';\n\nconst SelectionBtns = () => {\n  const { selection } = useOptionsValue();\n  const { selectionRefs } = useRefs();\n  const boardPos = useBoardPosition();\n\n  const [boardX, setX] = useState(0);\n  const [boardY, setY] = useState(0);\n\n  useEffect(() => {\n    const unsubscribe = boardPos.x.onChange(setX);\n    return unsubscribe;\n  }, [boardPos.x]);\n\n  useEffect(() => {\n    const unsubscribe = boardPos.y.onChange(setY);\n    return unsubscribe;\n  }, [boardPos.y]);\n\n  let top = -40;\n  let left = -40;\n\n  if (selection) {\n    const { x, y, width, height } = selection;\n    top = Math.min(y, y + height) - 40 + boardY;\n    left = Math.min(x, x + width) + boardX;\n  }\n\n  return (\n    <div\n      className=\"absolute top-0 left-0 z-50 flex items-center justify-center gap-2\"\n      style={{ top, left }}\n    >\n      <button\n        className=\"rounded-full bg-gray-200 p-2\"\n        ref={(ref) => {\n          if (ref && selectionRefs.current) selectionRefs.current[0] = ref;\n        }}\n      >\n        <BsArrowsMove />\n      </button>\n      <button\n        className=\"rounded-full bg-gray-200 p-2\"\n        ref={(ref) => {\n          if (ref && selectionRefs.current) selectionRefs.current[1] = ref;\n        }}\n      >\n        <FiCopy />\n      </button>\n      <button\n        className=\"rounded-full bg-gray-200 p-2\"\n        ref={(ref) => {\n          if (ref && selectionRefs.current) selectionRefs.current[2] = ref;\n        }}\n      >\n        <AiOutlineDelete />\n      </button>\n    </div>\n  );\n};\n\nexport default SelectionBtns;\n"
  },
  {
    "path": "modules/room/modules/board/components/UserMouse.tsx",
    "content": "import { useEffect, useState } from 'react';\n\nimport { motion } from 'framer-motion';\nimport { BsCursorFill } from 'react-icons/bs';\n\nimport { socket } from '@/common/lib/socket';\nimport { useRoom } from '@/common/recoil/room';\n\nimport { useBoardPosition } from '../hooks/useBoardPosition';\n\nconst UserMouse = ({ userId }: { userId: string }) => {\n  const { users } = useRoom();\n  const boardPos = useBoardPosition();\n\n  const [msg, setMsg] = useState('');\n  const [x, setX] = useState(boardPos.x.get());\n  const [y, setY] = useState(boardPos.y.get());\n  const [pos, setPos] = useState({ x: -1, y: -1 });\n\n  useEffect(() => {\n    socket.on('mouse_moved', (newX, newY, socketIdMoved) => {\n      if (socketIdMoved === userId) {\n        setPos({ x: newX, y: newY });\n      }\n    });\n\n    const handleNewMsg = (msgUserId: string, newMsg: string) => {\n      if (msgUserId === userId) {\n        setMsg(newMsg);\n\n        setTimeout(() => {\n          setMsg('');\n        }, 3000);\n      }\n    };\n    socket.on('new_msg', handleNewMsg);\n\n    return () => {\n      socket.off('mouse_moved');\n      socket.off('new_msg', handleNewMsg);\n    };\n  }, [userId]);\n\n  useEffect(() => {\n    const unsubscribe = boardPos.x.onChange(setX);\n    return unsubscribe;\n  }, [boardPos.x]);\n\n  useEffect(() => {\n    const unsubscribe = boardPos.y.onChange(setY);\n    return unsubscribe;\n  }, [boardPos.y]);\n\n  return (\n    <motion.div\n      className={`pointer-events-none absolute top-0 left-0 z-20 text-blue-800 ${\n        pos.x === -1 && 'hidden'\n      }`}\n      style={{ color: users.get(userId)?.color }}\n      animate={{ x: pos.x + x, y: pos.y + y }}\n      transition={{ duration: 0.2, ease: 'linear' }}\n    >\n      <BsCursorFill className=\"-rotate-90\" />\n      {msg && (\n        <p className=\"absolute top-full left-5 max-h-20 max-w-[15rem] overflow-hidden text-ellipsis rounded-md bg-zinc-900 p-1 px-3 text-white\">\n          {msg}\n        </p>\n      )}\n      <p className=\"ml-2\">{users.get(userId)?.name || 'Anonymous'}</p>\n    </motion.div>\n  );\n};\n\nexport default UserMouse;\n"
  },
  {
    "path": "modules/room/modules/board/helpers/Canvas.helpers.ts",
    "content": "const getWidthAndHeight = (\n  x: number,\n  y: number,\n  from: [number, number],\n  shift?: boolean\n) => {\n  let width = x - from[0];\n  let height = y - from[1];\n\n  if (shift) {\n    if (Math.abs(width) > Math.abs(height)) {\n      if ((width > 0 && height < 0) || (width < 0 && height > 0))\n        width = -height;\n      else width = height;\n    } else if ((height > 0 && width < 0) || (height < 0 && width > 0))\n      height = -width;\n    else height = width;\n  } else {\n    width = x - from[0];\n    height = y - from[1];\n  }\n\n  return { width, height };\n};\n\nexport const drawCircle = (\n  ctx: CanvasRenderingContext2D,\n  from: [number, number],\n  x: number,\n  y: number,\n  shift?: boolean\n) => {\n  ctx.beginPath();\n\n  const { width, height } = getWidthAndHeight(x, y, from, shift);\n\n  const cX = from[0] + width / 2;\n  const cY = from[1] + height / 2;\n  const radiusX = Math.abs(width / 2);\n  const radiusY = Math.abs(height / 2);\n\n  ctx.ellipse(cX, cY, radiusX, radiusY, 0, 0, 2 * Math.PI);\n\n  ctx.stroke();\n  ctx.fill();\n  ctx.closePath();\n\n  return { cX, cY, radiusX, radiusY };\n};\n\nexport const drawRect = (\n  ctx: CanvasRenderingContext2D,\n  from: [number, number],\n  x: number,\n  y: number,\n  shift?: boolean,\n  fill?: boolean\n) => {\n  ctx.beginPath();\n\n  const { width, height } = getWidthAndHeight(x, y, from, shift);\n\n  if (fill) ctx.fillRect(from[0], from[1], width, height);\n  else ctx.rect(from[0], from[1], width, height);\n\n  ctx.stroke();\n  ctx.fill();\n  ctx.closePath();\n\n  return { width, height };\n};\n\nexport const drawLine = (\n  ctx: CanvasRenderingContext2D,\n  from: [number, number],\n  x: number,\n  y: number,\n  shift?: boolean\n) => {\n  if (shift) {\n    ctx.beginPath();\n    ctx.lineTo(from[0], from[1]);\n    ctx.lineTo(x, y);\n    ctx.stroke();\n    ctx.closePath();\n\n    return;\n  }\n\n  ctx.lineTo(x, y);\n  ctx.stroke();\n};\n"
  },
  {
    "path": "modules/room/modules/board/hooks/useBoardPosition.ts",
    "content": "import { useContext } from 'react';\n\nimport { roomContext } from '../../../context/Room.context';\n\nexport const useBoardPosition = () => {\n  const { x, y } = useContext(roomContext);\n\n  return { x, y };\n};\n"
  },
  {
    "path": "modules/room/modules/board/hooks/useCtx.ts",
    "content": "import { useEffect, useState } from 'react';\n\nimport { useRefs } from '../../../hooks/useRefs';\n\nexport const useCtx = () => {\n  const { canvasRef } = useRefs();\n\n  const [ctx, setCtx] = useState<CanvasRenderingContext2D>();\n\n  useEffect(() => {\n    const newCtx = canvasRef.current?.getContext('2d');\n\n    if (newCtx) {\n      newCtx.lineJoin = 'round';\n      newCtx.lineCap = 'round';\n      setCtx(newCtx);\n    }\n  }, [canvasRef]);\n\n  return ctx;\n};\n"
  },
  {
    "path": "modules/room/modules/board/hooks/useDraw.ts",
    "content": "import { useState } from 'react';\n\nimport { DEFAULT_MOVE } from '@/common/constants/defaultMove';\nimport { useViewportSize } from '@/common/hooks/useViewportSize';\nimport { getPos } from '@/common/lib/getPos';\nimport { getStringFromRgba } from '@/common/lib/rgba';\nimport { socket } from '@/common/lib/socket';\nimport { useOptionsValue } from '@/common/recoil/options';\nimport { useSetSelection } from '@/common/recoil/options/options.hooks';\nimport { useMyMoves } from '@/common/recoil/room';\nimport { useSetSavedMoves } from '@/common/recoil/savedMoves';\nimport { Move } from '@/common/types/global';\n\nimport { drawRect, drawCircle, drawLine } from '../helpers/Canvas.helpers';\nimport { useBoardPosition } from './useBoardPosition';\nimport { useCtx } from './useCtx';\n\nlet tempMoves: [number, number][] = [];\nlet tempCircle = { cX: 0, cY: 0, radiusX: 0, radiusY: 0 };\nlet tempSize = { width: 0, height: 0 };\nlet tempImageData: ImageData | undefined;\n\nexport const useDraw = (blocked: boolean) => {\n  const options = useOptionsValue();\n  const boardPosition = useBoardPosition();\n  const { clearSavedMoves } = useSetSavedMoves();\n  const { handleAddMyMove } = useMyMoves();\n  const { setSelection, clearSelection } = useSetSelection();\n  const vw = useViewportSize();\n\n  const movedX = boardPosition.x;\n  const movedY = boardPosition.y;\n\n  const [drawing, setDrawing] = useState(false);\n  const ctx = useCtx();\n\n  const setupCtxOptions = () => {\n    if (ctx) {\n      ctx.lineWidth = options.lineWidth;\n      ctx.strokeStyle = getStringFromRgba(options.lineColor);\n      ctx.fillStyle = getStringFromRgba(options.fillColor);\n      if (options.mode === 'eraser')\n        ctx.globalCompositeOperation = 'destination-out';\n      else ctx.globalCompositeOperation = 'source-over';\n    }\n  };\n\n  const drawAndSet = () => {\n    if (!tempImageData)\n      tempImageData = ctx?.getImageData(\n        movedX.get() * -1,\n        movedY.get() * -1,\n        vw.width,\n        vw.height\n      );\n\n    if (tempImageData)\n      ctx?.putImageData(tempImageData, movedX.get() * -1, movedY.get() * -1);\n  };\n\n  const handleStartDrawing = (x: number, y: number) => {\n    if (!ctx || blocked || blocked) return;\n\n    const [finalX, finalY] = [getPos(x, movedX), getPos(y, movedY)];\n\n    setDrawing(true);\n    setupCtxOptions();\n    drawAndSet();\n\n    if (options.shape === 'line' && options.mode !== 'select') {\n      ctx.beginPath();\n      ctx.lineTo(finalX, finalY);\n      ctx.stroke();\n    }\n\n    tempMoves.push([finalX, finalY]);\n  };\n\n  const handleDraw = (x: number, y: number, shift?: boolean) => {\n    if (!ctx || !drawing || blocked) return;\n\n    const [finalX, finalY] = [getPos(x, movedX), getPos(y, movedY)];\n\n    drawAndSet();\n\n    if (options.mode === 'select') {\n      ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';\n      drawRect(ctx, tempMoves[0], finalX, finalY, false, true);\n      tempMoves.push([finalX, finalY]);\n\n      setupCtxOptions();\n\n      return;\n    }\n\n    switch (options.shape) {\n      case 'line':\n        if (shift) tempMoves = tempMoves.slice(0, 1);\n\n        drawLine(ctx, tempMoves[0], finalX, finalY, shift);\n\n        tempMoves.push([finalX, finalY]);\n        break;\n\n      case 'circle':\n        tempCircle = drawCircle(ctx, tempMoves[0], finalX, finalY, shift);\n        break;\n\n      case 'rect':\n        tempSize = drawRect(ctx, tempMoves[0], finalX, finalY, shift);\n        break;\n\n      default:\n        break;\n    }\n  };\n\n  const clearOnYourMove = () => {\n    drawAndSet();\n    tempImageData = undefined;\n  };\n\n  const handleEndDrawing = () => {\n    if (!ctx || blocked) return;\n\n    setDrawing(false);\n\n    ctx.closePath();\n\n    let addMove = true;\n    if (options.mode === 'select' && tempMoves.length) {\n      clearOnYourMove();\n      let x = tempMoves[0][0];\n      let y = tempMoves[0][1];\n      let width = tempMoves[tempMoves.length - 1][0] - x;\n      let height = tempMoves[tempMoves.length - 1][1] - y;\n\n      if (width < 0) {\n        width -= 4;\n        x += 2;\n      } else {\n        width += 4;\n        x -= 2;\n      }\n      if (height < 0) {\n        height -= 4;\n        y += 2;\n      } else {\n        height += 4;\n        y -= 2;\n      }\n\n      if ((width < 4 || width > 4) && (height < 4 || height > 4))\n        setSelection({ x, y, width, height });\n      else {\n        clearSelection();\n        addMove = false;\n      }\n    }\n\n    const move: Move = {\n      ...DEFAULT_MOVE,\n      rect: {\n        ...tempSize,\n      },\n      circle: {\n        ...tempCircle,\n      },\n      path: tempMoves,\n      options,\n    };\n\n    tempMoves = [];\n    tempCircle = { cX: 0, cY: 0, radiusX: 0, radiusY: 0 };\n    tempSize = { width: 0, height: 0 };\n\n    if (options.mode !== 'select') {\n      socket.emit('draw', move);\n      clearSavedMoves();\n    } else if (addMove) handleAddMyMove(move);\n  };\n\n  return {\n    handleEndDrawing,\n    handleDraw,\n    handleStartDrawing,\n    drawing,\n    clearOnYourMove,\n  };\n};\n"
  },
  {
    "path": "modules/room/modules/board/hooks/useSelection.ts",
    "content": "import { useEffect, useMemo } from 'react';\n\nimport { toast } from 'react-toastify';\n\nimport { DEFAULT_MOVE } from '@/common/constants/defaultMove';\nimport { socket } from '@/common/lib/socket';\nimport { useOptionsValue } from '@/common/recoil/options';\nimport { Move } from '@/common/types/global';\n\nimport { useMoveImage } from '../../../hooks/useMoveImage';\nimport { useRefs } from '../../../hooks/useRefs';\nimport { useCtx } from './useCtx';\n\nlet tempSelection = {\n  x: 0,\n  y: 0,\n  width: 0,\n  height: 0,\n};\n\nexport const useSelection = (drawAllMoves: () => Promise<void>) => {\n  const ctx = useCtx();\n  const options = useOptionsValue();\n  const { selection } = options;\n  const { bgRef, selectionRefs } = useRefs();\n  const { setMoveImage } = useMoveImage();\n\n  useEffect(() => {\n    const callback = async () => {\n      await drawAllMoves();\n\n      if (ctx && selection) {\n        setTimeout(() => {\n          const { x, y, width, height } = selection;\n\n          ctx.lineWidth = 2;\n          ctx.strokeStyle = '#000';\n          ctx.setLineDash([5, 10]);\n          ctx.globalCompositeOperation = 'source-over';\n\n          ctx.beginPath();\n          ctx.rect(x, y, width, height);\n          ctx.stroke();\n          ctx.closePath();\n\n          ctx.setLineDash([]);\n        }, 10);\n      }\n    };\n\n    if (\n      tempSelection.width !== selection?.width ||\n      tempSelection.height !== selection?.height ||\n      tempSelection.x !== selection?.x ||\n      tempSelection.y !== selection?.y\n    )\n      callback();\n\n    return () => {\n      if (selection) tempSelection = selection;\n    };\n\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [selection, ctx]);\n\n  const dimension = useMemo(() => {\n    if (selection) {\n      let { x, y, width, height } = selection;\n\n      if (width < 0) {\n        width += 4;\n        x -= 2;\n      } else {\n        width -= 4;\n        x += 2;\n      }\n      if (height < 0) {\n        height += 4;\n        y -= 2;\n      } else {\n        height -= 4;\n        y += 2;\n      }\n\n      return { x, y, width, height };\n    }\n\n    return {\n      width: 0,\n      height: 0,\n      x: 0,\n      y: 0,\n    };\n  }, [selection]);\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const makeBlob = async (withBg?: boolean) => {\n    if (!selection) return null;\n\n    const { x, y, width, height } = dimension;\n\n    const imageData = ctx?.getImageData(x, y, width, height);\n\n    if (imageData) {\n      const tempCanvas = document.createElement('canvas');\n      tempCanvas.width = width;\n      tempCanvas.height = height;\n      const canvas = document.createElement('canvas');\n      canvas.width = width;\n      canvas.height = height;\n      const tempCtx = canvas.getContext('2d');\n\n      if (tempCtx && bgRef.current) {\n        const bgImage = bgRef.current\n          .getContext('2d')\n          ?.getImageData(x, y, width, height);\n\n        if (bgImage && withBg) tempCtx.putImageData(bgImage, 0, 0);\n\n        const sTempCtx = tempCanvas.getContext('2d');\n        sTempCtx?.putImageData(imageData, 0, 0);\n\n        tempCtx.drawImage(tempCanvas, 0, 0);\n\n        const blob: Blob = await new Promise((resolve) => {\n          canvas.toBlob((blobGenerated) => {\n            if (blobGenerated) resolve(blobGenerated);\n          });\n        });\n\n        return blob;\n      }\n    }\n\n    return null;\n  };\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const createDeleteMove = () => {\n    if (!selection) return null;\n\n    let { x, y, width, height } = dimension;\n\n    if (width < 0) {\n      width += 4;\n      x -= 2;\n    } else {\n      width -= 4;\n      x += 2;\n    }\n    if (height < 0) {\n      height += 4;\n      y -= 2;\n    } else {\n      height -= 4;\n      y += 2;\n    }\n\n    const move: Move = {\n      ...DEFAULT_MOVE,\n      rect: {\n        width,\n        height,\n      },\n      path: [[x, y]],\n      options: {\n        ...options,\n        shape: 'rect',\n        mode: 'eraser',\n        fillColor: { r: 0, g: 0, b: 0, a: 1 },\n      },\n    };\n\n    socket.emit('draw', move);\n\n    return move;\n  };\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const handleCopy = async () => {\n    const blob = await makeBlob(true);\n\n    if (blob)\n      navigator.clipboard\n        .write([\n          new ClipboardItem({\n            'image/png': blob,\n          }),\n        ])\n        .then(() => {\n          toast('Copied to clipboard!', {\n            position: 'top-center',\n            theme: 'colored',\n          });\n        });\n  };\n\n  useEffect(() => {\n    const handleSelection = async (e: KeyboardEvent) => {\n      if (e.key === 'c' && e.ctrlKey) handleCopy();\n      if (e.key === 'Delete' && selection) createDeleteMove();\n    };\n\n    document.addEventListener('keydown', handleSelection);\n\n    return () => {\n      document.removeEventListener('keydown', handleSelection);\n    };\n  }, [bgRef, createDeleteMove, ctx, handleCopy, makeBlob, options, selection]);\n\n  useEffect(() => {\n    const handleSelectionMove = async () => {\n      if (selection) {\n        const blob = await makeBlob();\n        if (!blob) return;\n\n        const { x, y, width, height } = dimension;\n\n        const reader = new FileReader();\n        reader.readAsDataURL(blob);\n        reader.addEventListener('loadend', () => {\n          const base64 = reader.result?.toString();\n\n          if (base64) {\n            createDeleteMove();\n            setMoveImage({\n              base64,\n              x: Math.min(x, x + width),\n              y: Math.min(y, y + height),\n            });\n          }\n        });\n      }\n    };\n\n    if (selectionRefs.current) {\n      const moveBtn = selectionRefs.current[0];\n      const copyBtn = selectionRefs.current[1];\n      const deleteBtn = selectionRefs.current[2];\n\n      moveBtn.addEventListener('click', handleSelectionMove);\n      copyBtn.addEventListener('click', handleCopy);\n      deleteBtn.addEventListener('click', createDeleteMove);\n\n      return () => {\n        moveBtn?.removeEventListener('click', handleSelectionMove);\n        copyBtn?.removeEventListener('click', handleCopy);\n        deleteBtn?.removeEventListener('click', createDeleteMove);\n      };\n    }\n\n    return () => {};\n  }, [\n    createDeleteMove,\n    dimension,\n    handleCopy,\n    makeBlob,\n    selection,\n    selectionRefs,\n    setMoveImage,\n  ]);\n};\n"
  },
  {
    "path": "modules/room/modules/board/hooks/useSocketDraw.ts",
    "content": "import { useEffect } from 'react';\n\nimport { socket } from '@/common/lib/socket';\nimport { useSetUsers } from '@/common/recoil/room';\nimport { Move } from '@/common/types/global';\n\nexport const useSocketDraw = (drawing: boolean) => {\n  const { handleAddMoveToUser, handleRemoveMoveFromUser } = useSetUsers();\n\n  useEffect(() => {\n    let moveToDrawLater: Move | undefined;\n    let userIdLater = '';\n\n    socket.on('user_draw', (move, userId) => {\n      if (!drawing) {\n        handleAddMoveToUser(userId, move);\n      } else {\n        moveToDrawLater = move;\n        userIdLater = userId;\n      }\n    });\n\n    return () => {\n      socket.off('user_draw');\n\n      if (moveToDrawLater && userIdLater) {\n        handleAddMoveToUser(userIdLater, moveToDrawLater);\n      }\n    };\n  }, [drawing, handleAddMoveToUser]);\n\n  useEffect(() => {\n    socket.on('user_undo', (userId) => {\n      handleRemoveMoveFromUser(userId);\n    });\n\n    return () => {\n      socket.off('user_undo');\n    };\n  }, [handleRemoveMoveFromUser]);\n};\n"
  },
  {
    "path": "modules/room/modules/board/index.tsx",
    "content": "import Canvas from './components/Canvas';\nimport MousePosition from './components/MousePosition';\nimport MousesRenderer from './components/MousesRenderer';\nimport MoveImage from './components/MoveImage';\nimport SelectionBtns from './components/SelectionBtns';\n\nconst Board = () => (\n  <>\n    <Canvas />\n    <MousePosition />\n    <MousesRenderer />\n    <MoveImage />\n    <SelectionBtns />\n  </>\n);\n\nexport default Board;\n"
  },
  {
    "path": "modules/room/modules/chat/components/Chat.tsx",
    "content": "import { useEffect, useRef, useState } from 'react';\n\nimport { motion } from 'framer-motion';\nimport { BsFillChatFill } from 'react-icons/bs';\nimport { FaChevronDown } from 'react-icons/fa';\nimport { useList } from 'react-use';\n\nimport { socket } from '@/common/lib/socket';\nimport { useRoom } from '@/common/recoil/room';\nimport { MessageType } from '@/common/types/global';\n\nimport ChatInput from './ChatInput';\nimport Message from './Message';\n\nconst Chat = () => {\n  const room = useRoom();\n\n  const msgList = useRef<HTMLDivElement>(null);\n\n  const [newMsg, setNewMsg] = useState(false);\n  const [opened, setOpened] = useState(false);\n  const [msgs, handleMsgs] = useList<MessageType>([]);\n\n  useEffect(() => {\n    const handleNewMsg = (userId: string, msg: string) => {\n      const user = room.users.get(userId);\n\n      handleMsgs.push({\n        userId,\n        msg,\n        id: msgs.length + 1,\n        username: user?.name || 'Anonymous',\n        color: user?.color || '#000',\n      });\n\n      msgList.current?.scroll({ top: msgList.current?.scrollHeight });\n\n      if (!opened) setNewMsg(true);\n    };\n\n    socket.on('new_msg', handleNewMsg);\n\n    return () => {\n      socket.off('new_msg', handleNewMsg);\n    };\n  }, [handleMsgs, msgs, opened, room.users]);\n\n  return (\n    <motion.div\n      className=\"absolute bottom-0 z-50 flex h-[300px] w-full flex-col overflow-hidden rounded-t-md sm:left-36 sm:w-[30rem]\"\n      animate={{ y: opened ? 0 : 260 }}\n      transition={{ duration: 0.2 }}\n    >\n      <button\n        className=\"flex w-full cursor-pointer items-center justify-between bg-zinc-900 py-2 px-10 font-semibold text-white\"\n        onClick={() => {\n          setOpened((prev) => !prev);\n          setNewMsg(false);\n        }}\n      >\n        <div className=\"flex items-center gap-2\">\n          <BsFillChatFill className=\"mt-[-2px]\" />\n          Chat\n          {newMsg && (\n            <p className=\"rounded-md bg-green-500 px-1 font-semibold text-green-900\">\n              New!\n            </p>\n          )}\n        </div>\n\n        <motion.div\n          animate={{ rotate: opened ? 0 : 180 }}\n          transition={{ duration: 0.2 }}\n        >\n          <FaChevronDown />\n        </motion.div>\n      </button>\n      <div className=\"flex flex-1 flex-col justify-between bg-white p-3\">\n        <div className=\"h-[190px] overflow-y-scroll pr-2\" ref={msgList}>\n          {msgs.map((msg) => (\n            <Message key={msg.id} {...msg} />\n          ))}\n        </div>\n        <ChatInput />\n      </div>\n    </motion.div>\n  );\n};\n\nexport default Chat;\n"
  },
  {
    "path": "modules/room/modules/chat/components/ChatInput.tsx",
    "content": "import { FormEvent, useState } from 'react';\n\nimport { AiOutlineSend } from 'react-icons/ai';\n\nimport { socket } from '@/common/lib/socket';\n\nconst ChatInput = () => {\n  const [msg, setMsg] = useState('');\n\n  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n\n    socket.emit('send_msg', msg);\n\n    setMsg('');\n  };\n\n  return (\n    <form className=\"flex w-full items-center gap-2\" onSubmit={handleSubmit}>\n      <input\n        className=\"w-full rounded-xl border border-zinc-300 p-5 py-1\"\n        value={msg}\n        onChange={(e) => setMsg(e.target.value)}\n      />\n      <button className=\"btn-icon h-full w-10 bg-black\" type=\"submit\">\n        <AiOutlineSend />\n      </button>\n    </form>\n  );\n};\n\nexport default ChatInput;\n"
  },
  {
    "path": "modules/room/modules/chat/components/Message.tsx",
    "content": "import { socket } from '@/common/lib/socket';\nimport { MessageType } from '@/common/types/global';\n\nconst Message = ({ userId, msg, username, color }: MessageType) => {\n  const me = socket.id === userId;\n\n  return (\n    <div\n      className={`my-2 flex gap-2 text-clip ${me && 'justify-end text-right'}`}\n    >\n      {!me && (\n        <h5 style={{ color }} className=\"font-bold\">\n          {username}\n        </h5>\n      )}\n      <p style={{ wordBreak: 'break-all' }}>{msg}</p>\n    </div>\n  );\n};\n\nexport default Message;\n"
  },
  {
    "path": "modules/room/modules/chat/index.ts",
    "content": "import Chat from './components/Chat';\n\nexport default Chat;\n"
  },
  {
    "path": "modules/room/modules/toolbar/animations/Entry.animations.ts",
    "content": "export const EntryAnimation = {\n  from: {\n    y: -30,\n    opacity: 0,\n    transition: {\n      duration: 0.2,\n    },\n  },\n\n  to: {\n    y: 0,\n    opacity: 1,\n    transition: {\n      duration: 0.2,\n    },\n  },\n};\n"
  },
  {
    "path": "modules/room/modules/toolbar/components/BackgoundPicker.tsx",
    "content": "import { CgScreen } from 'react-icons/cg';\n\nimport { useModal } from '@/modules/modal';\n\nimport BackgroundModal from '../modals/BackgroundModal';\n\nconst BackgroundPicker = () => {\n  const { openModal } = useModal();\n\n  return (\n    <button className=\"btn-icon\" onClick={() => openModal(<BackgroundModal />)}>\n      <CgScreen />\n    </button>\n  );\n};\n\nexport default BackgroundPicker;\n"
  },
  {
    "path": "modules/room/modules/toolbar/components/ColorPicker.tsx",
    "content": "import { useRef, useState } from 'react';\n\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { RgbaColorPicker } from 'react-colorful';\nimport { BsPaletteFill } from 'react-icons/bs';\nimport { useClickAway } from 'react-use';\n\nimport { useOptions } from '@/common/recoil/options/options.hooks';\n\nimport { EntryAnimation } from '../animations/Entry.animations';\n\nconst ColorPicker = () => {\n  const [options, setOptions] = useOptions();\n\n  const ref = useRef<HTMLDivElement>(null);\n\n  const [opened, setOpened] = useState(false);\n\n  useClickAway(ref, () => setOpened(false));\n\n  return (\n    <div className=\"relative flex items-center\" ref={ref}>\n      <button\n        className=\"btn-icon\"\n        onClick={() => setOpened(!opened)}\n        disabled={options.mode === 'select'}\n      >\n        <BsPaletteFill />\n      </button>\n      <AnimatePresence>\n        {opened && (\n          <motion.div\n            className=\"absolute left-10 mt-24 sm:left-14\"\n            variants={EntryAnimation}\n            initial=\"from\"\n            animate=\"to\"\n            exit=\"from\"\n          >\n            <h2 className=\"ml-3 font-semibold text-black dark:text-white\">\n              Line color\n            </h2>\n            <RgbaColorPicker\n              color={options.lineColor}\n              onChange={(e) => {\n                setOptions({\n                  ...options,\n                  lineColor: e,\n                });\n              }}\n              className=\"mb-5\"\n            />\n            <h2 className=\"ml-3 font-semibold text-black dark:text-white\">\n              Fill color\n            </h2>\n            <RgbaColorPicker\n              color={options.fillColor}\n              onChange={(e) => {\n                setOptions({\n                  ...options,\n                  fillColor: e,\n                });\n              }}\n            />\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nexport default ColorPicker;\n"
  },
  {
    "path": "modules/room/modules/toolbar/components/HistoryBtns.tsx",
    "content": "import { FaRedo, FaUndo } from 'react-icons/fa';\n\nimport { useMyMoves } from '@/common/recoil/room';\nimport { useSavedMoves } from '@/common/recoil/savedMoves';\n\nimport { useRefs } from '../../../hooks/useRefs';\n\nconst HistoryBtns = () => {\n  const { redoRef, undoRef } = useRefs();\n\n  const { myMoves } = useMyMoves();\n  const savedMoves = useSavedMoves();\n\n  return (\n    <>\n      <button\n        className=\"btn-icon text-xl\"\n        ref={redoRef}\n        disabled={!savedMoves.length}\n      >\n        <FaRedo />\n      </button>\n      <button\n        className=\"btn-icon text-xl\"\n        ref={undoRef}\n        disabled={!myMoves.length}\n      >\n        <FaUndo />\n      </button>\n    </>\n  );\n};\n\nexport default HistoryBtns;\n"
  },
  {
    "path": "modules/room/modules/toolbar/components/ImagePicker.tsx",
    "content": "import { useEffect } from 'react';\n\nimport { BsFillImageFill } from 'react-icons/bs';\n\nimport { optimizeImage } from '@/common/lib/optimizeImage';\n\nimport { useMoveImage } from '../../../hooks/useMoveImage';\n\nconst ImagePicker = () => {\n  const { setMoveImage } = useMoveImage();\n\n  useEffect(() => {\n    const handlePaste = (e: ClipboardEvent) => {\n      const items = e.clipboardData?.items;\n      if (items) {\n        // eslint-disable-next-line no-restricted-syntax\n        for (const item of items) {\n          if (item.type.includes('image')) {\n            const file = item.getAsFile();\n            if (file)\n              optimizeImage(file, (uri) => setMoveImage({ base64: uri }));\n          }\n        }\n      }\n    };\n\n    document.addEventListener('paste', handlePaste);\n\n    return () => {\n      document.removeEventListener('paste', handlePaste);\n    };\n  }, [setMoveImage]);\n\n  const handleImageInput = () => {\n    const fileInput = document.createElement('input');\n    fileInput.type = 'file';\n    fileInput.accept = 'image/*';\n    fileInput.click();\n\n    fileInput.addEventListener('change', () => {\n      if (fileInput && fileInput.files) {\n        const file = fileInput.files[0];\n        optimizeImage(file, (uri) => setMoveImage({ base64: uri }));\n      }\n    });\n  };\n\n  return (\n    <button className=\"btn-icon text-xl\" onClick={handleImageInput}>\n      <BsFillImageFill />\n    </button>\n  );\n};\n\nexport default ImagePicker;\n"
  },
  {
    "path": "modules/room/modules/toolbar/components/LineWidthPicker.tsx",
    "content": "import { useRef, useState } from 'react';\n\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { BsBorderWidth } from 'react-icons/bs';\nimport { useClickAway } from 'react-use';\n\nimport { useOptions } from '@/common/recoil/options';\n\nimport { EntryAnimation } from '../animations/Entry.animations';\n\nconst LineWidthPicker = () => {\n  const [options, setOptions] = useOptions();\n\n  const ref = useRef<HTMLDivElement>(null);\n\n  const [opened, setOpened] = useState(false);\n\n  useClickAway(ref, () => setOpened(false));\n\n  return (\n    <div className=\"relative flex items-center\" ref={ref}>\n      <button\n        className=\"btn-icon text-xl\"\n        onClick={() => setOpened(!opened)}\n        disabled={options.mode === 'select'}\n      >\n        <BsBorderWidth />\n      </button>\n      <AnimatePresence>\n        {opened && (\n          <motion.div\n            className=\"absolute top-[6px] left-14 w-36\"\n            variants={EntryAnimation}\n            initial=\"from\"\n            animate=\"to\"\n            exit=\"from\"\n          >\n            <input\n              type=\"range\"\n              min={1}\n              max={20}\n              value={options.lineWidth}\n              onChange={(e) =>\n                setOptions((prev) => ({\n                  ...prev,\n                  lineWidth: parseInt(e.target.value, 10),\n                }))\n              }\n              className=\"h-4 w-full cursor-pointer appearance-none rounded-lg bg-gray-200\"\n            />\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nexport default LineWidthPicker;\n"
  },
  {
    "path": "modules/room/modules/toolbar/components/ModePicker.tsx",
    "content": "import { useEffect } from 'react';\n\nimport { AiOutlineSelect } from 'react-icons/ai';\nimport { BsPencilFill } from 'react-icons/bs';\nimport { FaEraser } from 'react-icons/fa';\n\nimport { useOptions, useSetSelection } from '@/common/recoil/options';\n\nconst ModePicker = () => {\n  const [options, setOptions] = useOptions();\n  const { clearSelection } = useSetSelection();\n\n  useEffect(() => {\n    clearSelection();\n\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [options.mode]);\n\n  return (\n    <>\n      <button\n        className={`btn-icon text-xl ${\n          options.mode === 'draw' && 'bg-green-400'\n        }`}\n        onClick={() => {\n          setOptions((prev) => ({\n            ...prev,\n            mode: 'draw',\n          }));\n        }}\n      >\n        <BsPencilFill />\n      </button>\n\n      <button\n        className={`btn-icon text-xl ${\n          options.mode === 'eraser' && 'bg-green-400'\n        }`}\n        onClick={() => {\n          setOptions((prev) => ({\n            ...prev,\n            mode: 'eraser',\n          }));\n        }}\n      >\n        <FaEraser />\n      </button>\n\n      <button\n        className={`btn-icon text-2xl ${\n          options.mode === 'select' && 'bg-green-400'\n        }`}\n        onClick={() => {\n          setOptions((prev) => ({\n            ...prev,\n            mode: 'select',\n          }));\n        }}\n      >\n        <AiOutlineSelect />\n      </button>\n    </>\n  );\n};\n\nexport default ModePicker;\n"
  },
  {
    "path": "modules/room/modules/toolbar/components/ShapeSelector.tsx",
    "content": "import { useRef, useState } from 'react';\n\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { BiRectangle } from 'react-icons/bi';\nimport { BsCircle } from 'react-icons/bs';\nimport { CgShapeZigzag } from 'react-icons/cg';\nimport { useClickAway } from 'react-use';\n\nimport { useOptions } from '@/common/recoil/options';\nimport { Shape } from '@/common/types/global';\n\nimport { EntryAnimation } from '../animations/Entry.animations';\n\nconst ShapeSelector = () => {\n  const [options, setOptions] = useOptions();\n\n  const ref = useRef<HTMLDivElement>(null);\n\n  const [opened, setOpened] = useState(false);\n\n  useClickAway(ref, () => setOpened(false));\n\n  const handleShapeChange = (shape: Shape) => {\n    setOptions((prev) => ({\n      ...prev,\n      shape,\n    }));\n\n    setOpened(false);\n  };\n\n  return (\n    <div className=\"relative flex items-center\" ref={ref}>\n      <button\n        className=\"btn-icon text-2xl\"\n        disabled={options.mode === 'select'}\n        onClick={() => setOpened((prev) => !prev)}\n      >\n        {options.shape === 'circle' && <BsCircle />}\n        {options.shape === 'rect' && <BiRectangle />}\n        {options.shape === 'line' && <CgShapeZigzag />}\n      </button>\n\n      <AnimatePresence>\n        {opened && (\n          <motion.div\n            className=\"absolute left-14 z-10 flex gap-1 rounded-lg border bg-zinc-900 p-2 md:border-0\"\n            variants={EntryAnimation}\n            initial=\"from\"\n            animate=\"to\"\n            exit=\"from\"\n          >\n            <button\n              className=\"btn-icon text-2xl\"\n              onClick={() => handleShapeChange('line')}\n            >\n              <CgShapeZigzag />\n            </button>\n\n            <button\n              className=\"btn-icon text-2xl\"\n              onClick={() => handleShapeChange('rect')}\n            >\n              <BiRectangle />\n            </button>\n\n            <button\n              className=\"btn-icon text-2xl\"\n              onClick={() => handleShapeChange('circle')}\n            >\n              <BsCircle />\n            </button>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nexport default ShapeSelector;\n"
  },
  {
    "path": "modules/room/modules/toolbar/components/ToolBar.tsx",
    "content": "import { useEffect, useState } from 'react';\n\nimport { motion } from 'framer-motion';\nimport { useRouter } from 'next/router';\nimport { FiChevronRight } from 'react-icons/fi';\nimport { HiOutlineDownload } from 'react-icons/hi';\nimport { ImExit } from 'react-icons/im';\nimport { IoIosShareAlt } from 'react-icons/io';\n\nimport { CANVAS_SIZE } from '@/common/constants/canvasSize';\nimport { useViewportSize } from '@/common/hooks/useViewportSize';\nimport { useModal } from '@/modules/modal';\n\nimport { useRefs } from '../../../hooks/useRefs';\nimport ShareModal from '../modals/ShareModal';\nimport BackgroundPicker from './BackgoundPicker';\nimport ColorPicker from './ColorPicker';\nimport HistoryBtns from './HistoryBtns';\nimport ImagePicker from './ImagePicker';\nimport LineWidthPicker from './LineWidthPicker';\nimport ModePicker from './ModePicker';\nimport ShapeSelector from './ShapeSelector';\n\nconst ToolBar = () => {\n  const { canvasRef, bgRef } = useRefs();\n  const { openModal } = useModal();\n  const { width } = useViewportSize();\n\n  const [opened, setOpened] = useState(false);\n\n  const router = useRouter();\n\n  useEffect(() => {\n    if (width >= 1024) setOpened(true);\n    else setOpened(false);\n  }, [width]);\n\n  const handleExit = () => router.push('/');\n\n  const handleDownload = () => {\n    const canvas = document.createElement('canvas');\n    canvas.width = CANVAS_SIZE.width;\n    canvas.height = CANVAS_SIZE.height;\n\n    const tempCtx = canvas.getContext('2d');\n\n    if (tempCtx && canvasRef.current && bgRef.current) {\n      tempCtx.drawImage(bgRef.current, 0, 0);\n      tempCtx.drawImage(canvasRef.current, 0, 0);\n    }\n\n    const link = document.createElement('a');\n    link.href = canvas.toDataURL('image/png');\n    link.download = 'canvas.png';\n    link.click();\n  };\n\n  const handleShare = () => openModal(<ShareModal />);\n\n  return (\n    <>\n      <motion.button\n        className=\"btn-icon absolute bottom-1/2 -left-2 z-50 h-10 w-10 rounded-full bg-black text-2xl transition-none lg:hidden\"\n        animate={{ rotate: opened ? 0 : 180 }}\n        transition={{ duration: 0.2 }}\n        onClick={() => setOpened(!opened)}\n      >\n        <FiChevronRight />\n      </motion.button>\n      <motion.div\n        className=\"absolute left-10 top-[50%] z-50 grid grid-cols-2 items-center gap-5 rounded-lg bg-zinc-900 p-5 text-white 2xl:grid-cols-1\"\n        animate={{\n          x: opened ? 0 : -160,\n          y: '-50%',\n        }}\n        transition={{\n          duration: 0.2,\n        }}\n      >\n        <HistoryBtns />\n\n        <div className=\"h-px w-full bg-white 2xl:hidden\" />\n        <div className=\"h-px w-full bg-white\" />\n\n        <ShapeSelector />\n        <ColorPicker />\n        <LineWidthPicker />\n        <ModePicker />\n        <ImagePicker />\n\n        <div className=\"2xl:hidden\"></div>\n        <div className=\"h-px w-full bg-white 2xl:hidden\" />\n        <div className=\"h-px w-full bg-white\" />\n\n        <BackgroundPicker />\n        <button className=\"btn-icon text-2xl\" onClick={handleShare}>\n          <IoIosShareAlt />\n        </button>\n        <button className=\"btn-icon text-2xl\" onClick={handleDownload}>\n          <HiOutlineDownload />\n        </button>\n        <button className=\"btn-icon text-xl\" onClick={handleExit}>\n          <ImExit />\n        </button>\n      </motion.div>\n    </>\n  );\n};\n\nexport default ToolBar;\n"
  },
  {
    "path": "modules/room/modules/toolbar/index.ts",
    "content": "import ToolBar from './components/ToolBar';\n\nexport default ToolBar;\n"
  },
  {
    "path": "modules/room/modules/toolbar/modals/BackgroundModal.tsx",
    "content": "import { useEffect } from 'react';\n\nimport { AiOutlineClose } from 'react-icons/ai';\n\nimport { useBackground, useSetBackground } from '@/common/recoil/background';\nimport { useModal } from '@/modules/modal';\n\nconst BackgroundModal = () => {\n  const { closeModal } = useModal();\n  const setBackground = useSetBackground();\n  const bg = useBackground();\n\n  useEffect(() => closeModal, [bg, closeModal]);\n\n  const renderBg = (\n    ref: HTMLCanvasElement | null,\n    mode: 'dark' | 'light',\n    lines: boolean\n  ) => {\n    const ctx = ref?.getContext('2d');\n    if (ctx) {\n      ctx.fillStyle = mode === 'dark' ? '#222' : '#fff';\n      ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);\n\n      if (lines) {\n        ctx.lineWidth = 1;\n        ctx.strokeStyle = mode === 'dark' ? '#444' : '#ddd';\n        for (let i = 0; i < ctx.canvas.height; i += 10) {\n          ctx.beginPath();\n          ctx.moveTo(0, i);\n          ctx.lineTo(ctx.canvas.width, i);\n          ctx.stroke();\n        }\n\n        for (let i = 0; i < ctx.canvas.width; i += 10) {\n          ctx.beginPath();\n          ctx.moveTo(i, 0);\n          ctx.lineTo(i, ctx.canvas.height);\n          ctx.stroke();\n        }\n      }\n    }\n  };\n\n  return (\n    <div className=\"relative flex flex-col items-center rounded-md bg-white p-10\">\n      <button onClick={closeModal} className=\"absolute top-5 right-5\">\n        <AiOutlineClose />\n      </button>\n      <h2 className=\"mb-4 text-2xl font-bold\">Choose background</h2>\n      <div className=\"grid gap-5 sm:grid-cols-2\">\n        <canvas\n          className=\"h-48 w-64 cursor-pointer rounded-md border-2\"\n          tabIndex={0}\n          width={256}\n          height={192}\n          onClick={() => setBackground('dark', true)}\n          ref={(ref) => renderBg(ref, 'dark', true)}\n        />\n        <canvas\n          className=\"h-48 w-64 cursor-pointer rounded-md border-2\"\n          tabIndex={0}\n          width={256}\n          height={192}\n          onClick={() => setBackground('light', true)}\n          ref={(ref) => renderBg(ref, 'light', true)}\n        />\n        <canvas\n          className=\"h-48 w-64 cursor-pointer rounded-md border-2\"\n          tabIndex={0}\n          width={256}\n          height={192}\n          onClick={() => setBackground('dark', false)}\n          ref={(ref) => renderBg(ref, 'dark', false)}\n        />\n        <canvas\n          className=\"h-48 w-64 cursor-pointer rounded-md border-2\"\n          tabIndex={0}\n          width={256}\n          height={192}\n          onClick={() => setBackground('light', false)}\n          ref={(ref) => renderBg(ref, 'light', false)}\n        />\n      </div>\n    </div>\n  );\n};\n\nexport default BackgroundModal;\n"
  },
  {
    "path": "modules/room/modules/toolbar/modals/ShareModal.tsx",
    "content": "import { useEffect, useState } from 'react';\n\nimport { AiOutlineClose } from 'react-icons/ai';\n\nimport { useRoom } from '@/common/recoil/room';\nimport { useModal } from '@/modules/modal';\n\nconst ShareModal = () => {\n  const { id } = useRoom();\n  const { closeModal } = useModal();\n\n  const [url, setUrl] = useState('');\n\n  useEffect(() => setUrl(window.location.href), []);\n\n  const handleCopy = () => navigator.clipboard.writeText(url);\n\n  return (\n    <div className=\"relative flex flex-col items-center rounded-md bg-white p-10 pt-5\">\n      <button onClick={closeModal} className=\"absolute top-5 right-5\">\n        <AiOutlineClose />\n      </button>\n      <h2 className=\"text-2xl font-bold\">Invite</h2>\n      <h3>\n        Room id: <p className=\"inline font-bold\">{id}</p>\n      </h3>\n      <div className=\"relative mt-2\">\n        <input type=\"text\" value={url} readOnly className=\"input sm:w-96\" />\n        <button className=\"btn absolute right-0 h-full\" onClick={handleCopy}>\n          Copy\n        </button>\n      </div>\n    </div>\n  );\n};\n\nexport default ShareModal;\n"
  },
  {
    "path": "next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nmodule.exports = {\n  reactStrictMode: true,\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"collabio\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"nodemon server/index.ts\",\n    \"dev:client\": \"ts-node server/index.ts\",\n    \"build:server\": \"tsc --project tsconfig.server.json\",\n    \"build:next\": \"next build\",\n    \"build\": \"npm-run-all build:*\",\n    \"start\": \"NODE_ENV=production node build/server/index.js\",\n    \"lint\": \"next lint\"\n  },\n  \"dependencies\": {\n    \"express\": \"^4.17.3\",\n    \"framer-motion\": \"^6.3.3\",\n    \"next\": \"latest\",\n    \"react\": \"^17.0.2\",\n    \"react-colorful\": \"^5.5.1\",\n    \"react-dom\": \"^17.0.2\",\n    \"react-icons\": \"^4.3.1\",\n    \"react-image-file-resizer\": \"^0.4.8\",\n    \"react-toastify\": \"^9.0.3\",\n    \"react-use\": \"^17.3.2\",\n    \"recoil\": \"^0.7.3-alpha.2\",\n    \"socket.io\": \"^4.5.0\",\n    \"socket.io-client\": \"^4.5.0\",\n    \"uuid\": \"^8.3.2\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^4.17.13\",\n    \"@types/node\": \"17.0.4\",\n    \"@types/react\": \"17.0.38\",\n    \"@types/react-dom\": \"^18.0.4\",\n    \"@types/uuid\": \"^8.3.4\",\n    \"autoprefixer\": \"^10.4.0\",\n    \"eslint\": \"8.2.0\",\n    \"eslint-config-airbnb\": \"^19.0.4\",\n    \"eslint-config-airbnb-typescript\": \"^16.1.4\",\n    \"eslint-config-next\": \"^12.1.2\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-plugin-import\": \"^2.25.4\",\n    \"eslint-plugin-jsx-a11y\": \"^6.5.1\",\n    \"eslint-plugin-prettier\": \"^4.0.0\",\n    \"eslint-plugin-react\": \"^7.29.4\",\n    \"eslint-plugin-react-hooks\": \"^4.3.0\",\n    \"eslint-plugin-tailwindcss\": \"^3.5.0\",\n    \"eslint-plugin-unused-imports\": \"^2.0.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"postcss\": \"^8.4.5\",\n    \"prettier\": \"^2.6.1\",\n    \"prettier-plugin-tailwindcss\": \"^0.1.8\",\n    \"tailwindcss\": \"^3.0.7\",\n    \"typescript\": \"4.5.4\"\n  }\n}\n"
  },
  {
    "path": "pages/[roomId].tsx",
    "content": "import type { NextPage } from 'next';\n\nimport Room from '@/modules/room';\n\nconst RoomPage: NextPage = () => {\n  return <Room />;\n};\n\nexport default RoomPage;\n"
  },
  {
    "path": "pages/_app.tsx",
    "content": "import '../common/styles/global.css';\nimport { MotionConfig } from 'framer-motion';\nimport type { AppProps } from 'next/app';\nimport Head from 'next/head';\nimport { ToastContainer } from 'react-toastify';\nimport { RecoilRoot } from 'recoil';\n\nimport { DEFAULT_EASE } from '@/common/constants/easings';\nimport { ModalManager } from '@/modules/modal';\n\nimport 'react-toastify/dist/ReactToastify.min.css';\n\nconst App = ({ Component, pageProps }: AppProps) => {\n  return (\n    <>\n      <Head>\n        <title>Collabio | Online Whiteboard</title>\n        <link rel=\"icon\" href=\"/favicon.ico\" />\n      </Head>\n      <RecoilRoot>\n        <ToastContainer />\n        <MotionConfig transition={{ ease: DEFAULT_EASE }}>\n          <ModalManager />\n          <Component {...pageProps} />\n        </MotionConfig>\n      </RecoilRoot>\n    </>\n  );\n};\n\nexport default App;\n"
  },
  {
    "path": "pages/_document.tsx",
    "content": "import { Html, Main, NextScript, Head } from 'next/document';\n\nconst document = () => (\n  <Html>\n    <Head>\n      <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n      <link\n        rel=\"preconnect\"\n        href=\"https://fonts.gstatic.com\"\n        crossOrigin=\"allow\"\n      />\n      <link\n        href=\"https://fonts.googleapis.com/css2?family=Montserrat:wght@100;300;400;500;600;700;800;900&display=swap\"\n        rel=\"stylesheet\"\n      />\n    </Head>\n    <body>\n      <div id=\"portal\"></div>\n      <Main />\n      <NextScript />\n    </body>\n  </Html>\n);\n\nexport default document;\n"
  },
  {
    "path": "pages/index.tsx",
    "content": "import type { NextPage } from 'next';\n\nimport Home from '@/modules/home';\n\nconst HomePage: NextPage = () => {\n  return <Home />;\n};\n\nexport default HomePage;\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "server/index.ts",
    "content": "import { createServer } from 'http';\n\nimport express from 'express';\nimport next, { NextApiHandler } from 'next';\nimport { Server } from 'socket.io';\nimport { v4 } from 'uuid';\n\nimport {\n  ClientToServerEvents,\n  Move,\n  Room,\n  ServerToClientEvents,\n} from '@/common/types/global';\n\nconst port = parseInt(process.env.PORT || '3000', 10);\nconst dev = process.env.NODE_ENV !== 'production';\nconst nextApp = next({ dev });\nconst nextHandler: NextApiHandler = nextApp.getRequestHandler();\n\nnextApp.prepare().then(async () => {\n  const app = express();\n  const server = createServer(app);\n\n  const io = new Server<ClientToServerEvents, ServerToClientEvents>(server);\n\n  app.get('/hello', async (_, res) => {\n    res.send('Hello World');\n  });\n\n  const rooms = new Map<string, Room>();\n\n  const addMove = (roomId: string, socketId: string, move: Move) => {\n    const room = rooms.get(roomId)!;\n\n    if (!room.users.has(socketId)) {\n      room.usersMoves.set(socketId, [move]);\n    }\n\n    room.usersMoves.get(socketId)!.push(move);\n  };\n\n  const undoMove = (roomId: string, socketId: string) => {\n    const room = rooms.get(roomId)!;\n\n    room.usersMoves.get(socketId)!.pop();\n  };\n\n  io.on('connection', (socket) => {\n    const getRoomId = () => {\n      const joinedRoom = [...socket.rooms].find((room) => room !== socket.id);\n\n      if (!joinedRoom) return socket.id;\n\n      return joinedRoom;\n    };\n\n    const leaveRoom = (roomId: string, socketId: string) => {\n      const room = rooms.get(roomId);\n      if (!room) return;\n\n      const userMoves = room.usersMoves.get(socketId);\n\n      if (userMoves) room.drawed.push(...userMoves);\n      room.users.delete(socketId);\n\n      socket.leave(roomId);\n    };\n\n    socket.on('create_room', (username) => {\n      let roomId: string;\n      do {\n        roomId = Math.random().toString(36).substring(2, 6);\n      } while (rooms.has(roomId));\n\n      socket.join(roomId);\n\n      rooms.set(roomId, {\n        usersMoves: new Map([[socket.id, []]]),\n        drawed: [],\n        users: new Map([[socket.id, username]]),\n      });\n\n      io.to(socket.id).emit('created', roomId);\n    });\n\n    socket.on('check_room', (roomId) => {\n      if (rooms.has(roomId)) socket.emit('room_exists', true);\n      else socket.emit('room_exists', false);\n    });\n\n    socket.on('join_room', (roomId, username) => {\n      const room = rooms.get(roomId);\n\n      if (room && room.users.size < 12) {\n        socket.join(roomId);\n\n        room.users.set(socket.id, username);\n        room.usersMoves.set(socket.id, []);\n\n        io.to(socket.id).emit('joined', roomId);\n      } else io.to(socket.id).emit('joined', '', true);\n    });\n\n    socket.on('joined_room', () => {\n      const roomId = getRoomId();\n\n      const room = rooms.get(roomId);\n      if (!room) return;\n\n      io.to(socket.id).emit(\n        'room',\n        room,\n        JSON.stringify([...room.usersMoves]),\n        JSON.stringify([...room.users])\n      );\n\n      socket.broadcast\n        .to(roomId)\n        .emit('new_user', socket.id, room.users.get(socket.id) || 'Anonymous');\n    });\n\n    socket.on('leave_room', () => {\n      const roomId = getRoomId();\n      leaveRoom(roomId, socket.id);\n\n      io.to(roomId).emit('user_disconnected', socket.id);\n    });\n\n    socket.on('draw', (move) => {\n      const roomId = getRoomId();\n\n      const timestamp = Date.now();\n\n      // eslint-disable-next-line no-param-reassign\n      move.id = v4();\n\n      addMove(roomId, socket.id, { ...move, timestamp });\n\n      io.to(socket.id).emit('your_move', { ...move, timestamp });\n\n      socket.broadcast\n        .to(roomId)\n        .emit('user_draw', { ...move, timestamp }, socket.id);\n    });\n\n    socket.on('undo', () => {\n      const roomId = getRoomId();\n\n      undoMove(roomId, socket.id);\n\n      socket.broadcast.to(roomId).emit('user_undo', socket.id);\n    });\n\n    socket.on('mouse_move', (x, y) => {\n      socket.broadcast.to(getRoomId()).emit('mouse_moved', x, y, socket.id);\n    });\n\n    socket.on('send_msg', (msg) => {\n      io.to(getRoomId()).emit('new_msg', socket.id, msg);\n    });\n\n    socket.on('disconnecting', () => {\n      const roomId = getRoomId();\n      leaveRoom(roomId, socket.id);\n\n      io.to(roomId).emit('user_disconnected', socket.id);\n    });\n  });\n\n  app.all('*', (req: any, res: any) => nextHandler(req, res));\n\n  server.listen(port, () => {\n    // eslint-disable-next-line no-console\n    console.log(`> Ready on http://localhost:${port}`);\n  });\n});\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "module.exports = {\n  content: [\n    './pages/**/*.{js,ts,jsx,tsx}',\n    './common/**/*.{js,ts,jsx,tsx}',\n    './modules/**/*.{js,ts,jsx,tsx}',\n  ],\n  darkMode: 'class',\n  theme: {\n    fontSize: {\n      xs: '0.75rem',\n      sm: '0.875rem',\n      base: '1rem',\n      lg: '1.125rem',\n      xl: '1.25rem',\n      '2xl': '1.5rem',\n      '3xl': '1.875rem',\n      '4xl': '2.25rem',\n      '5xl': '3rem',\n      '6xl': '4rem',\n      extra: '6rem',\n    },\n    extend: {\n      colors: {},\n      width: {\n        160: '40rem',\n      },\n    },\n    fontFamily: {\n      montserrat: ['Montserrat', 'sans-serif'],\n    },\n  },\n  plugins: [],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"CommonJS\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"downlevelIteration\": true,\n\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "tsconfig.server.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"outDir\": \"build\",\n    \"target\": \"es2017\",\n    \"isolatedModules\": false,\n    \"noEmit\": false\n  },\n  \"include\": [\"server\"]\n}\n"
  }
]