├── .gitignore
├── client
├── .eslintrc.cjs
├── .gitignore
├── index.html
├── package-lock.json
├── package.json
├── public
│ └── favicon.ico
├── src
│ ├── App.jsx
│ ├── api
│ │ └── socket.js
│ ├── assets
│ │ ├── fonts
│ │ │ ├── Roboto-Bold.ttf
│ │ │ └── Roboto-Medium.ttf
│ │ └── icons
│ │ │ └── index.jsx
│ ├── components
│ │ ├── Canvas.jsx
│ │ ├── Collaboration.jsx
│ │ ├── Credits.jsx
│ │ ├── Menu.jsx
│ │ ├── Style.jsx
│ │ ├── ToolBar.jsx
│ │ ├── Ui.jsx
│ │ ├── UndoRedo.jsx
│ │ └── Zoom.jsx
│ ├── global
│ │ └── var.js
│ ├── helper
│ │ ├── canvas.js
│ │ ├── element.js
│ │ ├── keys.js
│ │ └── ui.js
│ ├── hooks
│ │ ├── useCanvas.jsx
│ │ ├── useDimension.jsx
│ │ ├── useHistory.jsx
│ │ └── useKeys.jsx
│ ├── main.jsx
│ ├── provider
│ │ └── AppStates.jsx
│ ├── styles
│ │ └── index.css
│ └── views
│ │ └── WorkSpace.jsx
├── vercel.json
└── vite.config.js
├── package.json
├── readme.md
└── server
├── .gitignore
├── index.js
├── package-lock.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 |
4 | # testing
5 | /coverage
6 |
7 | # production
8 | /build
9 |
10 | # env
11 | .env
12 | .env.local
13 | .env.development.local
14 | .env.test.local
15 | .env.production.local
16 |
17 | npm-debug.log*
18 | yarn-debug.log*
19 | yarn-error.log*
20 |
21 | # Editor directories and files
22 | .idea
23 | .DS_Store
--------------------------------------------------------------------------------
/client/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react-refresh/only-export-components': [
16 | 'warn',
17 | { allowConstantExport: true },
18 | ],
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 | dist
6 | dist-ssr
7 | *.local
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # env
16 | .env
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | # Editor directories and files
27 | .vscode/*
28 | !.vscode/extensions.json
29 | .idea
30 | .DS_Store
31 | *.suo
32 | *.ntvs*
33 | *.njsproj
34 | *.sln
35 | *.sw?
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sketchflow
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sketchflow",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "dotenv": "^16.4.5",
14 | "framer-motion": "^11.0.8",
15 | "perfect-freehand": "^1.2.0",
16 | "react": "^18.2.0",
17 | "react-dom": "^18.2.0",
18 | "react-router-dom": "^6.22.3",
19 | "socket.io-client": "^4.7.4",
20 | "socket.io-msgpack-parser": "^3.0.2",
21 | "uuid": "^9.0.1"
22 | },
23 | "devDependencies": {
24 | "@types/react": "^18.2.43",
25 | "@types/react-dom": "^18.2.17",
26 | "@vitejs/plugin-react": "^4.2.1",
27 | "eslint": "^8.55.0",
28 | "eslint-plugin-react": "^7.33.2",
29 | "eslint-plugin-react-hooks": "^4.6.0",
30 | "eslint-plugin-react-refresh": "^0.4.5",
31 | "vite": "^5.0.8"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toouil/sketchflow/0428ce17dc596850431a8ed34be86328e7ce2a8b/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Route, Routes, Navigate } from "react-router-dom";
2 | import WorkSpace from "./views/WorkSpace";
3 |
4 | function App() {
5 | return (
6 |
7 | } />
8 | } />
9 |
10 | );
11 | }
12 |
13 | export default App;
--------------------------------------------------------------------------------
/client/src/api/socket.js:
--------------------------------------------------------------------------------
1 | import { io } from "socket.io-client";
2 | import parser from "socket.io-msgpack-parser"
3 |
4 | const BACKEND_URL = import.meta.env.VITE_APP_SERVER_URL;
5 | export const socket = io(BACKEND_URL, {
6 | parser
7 | });
--------------------------------------------------------------------------------
/client/src/assets/fonts/Roboto-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toouil/sketchflow/0428ce17dc596850431a8ed34be86328e7ce2a8b/client/src/assets/fonts/Roboto-Bold.ttf
--------------------------------------------------------------------------------
/client/src/assets/fonts/Roboto-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toouil/sketchflow/0428ce17dc596850431a8ed34be86328e7ce2a8b/client/src/assets/fonts/Roboto-Medium.ttf
--------------------------------------------------------------------------------
/client/src/assets/icons/index.jsx:
--------------------------------------------------------------------------------
1 | const demention = 15;
2 |
3 | export const Pencil = () => (
4 |
10 |
11 |
12 | );
13 |
14 | export const Eraser = () => (
15 |
21 |
22 |
23 | );
24 |
25 | export const Circle = () => (
26 |
32 |
33 |
34 | );
35 |
36 | export const Rectangle = () => (
37 |
43 |
44 |
45 | );
46 |
47 | export const Line = () => (
48 |
56 |
57 |
58 | );
59 |
60 | export const Arrow = () => (
61 |
74 |
75 |
76 | );
77 |
78 | export const Selection = () => (
79 |
85 |
86 |
87 | );
88 |
89 | export const Diamond = () => (
90 |
97 |
98 |
99 | );
100 |
101 | export const Hand = () => (
102 |
110 |
111 |
112 | );
113 |
114 | export const Lock = () => (
115 |
124 |
125 |
126 |
127 | );
128 |
129 | export const SolidLine = () => (
130 |
140 |
141 |
142 | );
143 |
144 | export const DashedLine = () => (
145 |
150 |
154 |
155 | );
156 |
157 | export const DottedLine = () => (
158 |
164 |
165 |
166 | );
167 |
168 | export const Duplicate = () => (
169 |
175 |
176 |
177 | );
178 |
179 | export const Delete = () => (
180 |
190 |
191 |
192 | );
193 |
194 | export const Backward = () => (
195 |
202 |
203 |
204 | );
205 |
206 | export const Forward = () => (
207 |
214 |
215 |
216 | );
217 |
218 | export const ToBack = () => (
219 |
226 |
227 |
228 | );
229 |
230 | export const ToFront = () => (
231 |
238 |
239 |
240 | );
241 |
242 | export const Undo = () => (
243 |
249 |
250 |
251 | );
252 |
253 | export const Redo = () => (
254 |
260 |
261 |
262 | );
263 |
264 | export const MenuIcon = () => (
265 |
275 |
276 |
277 |
278 |
279 | );
280 |
281 | export const Xmark = () => (
282 |
292 |
293 |
294 |
295 | );
296 |
297 | export const Folder = () => (
298 |
307 |
308 |
309 | );
310 |
311 | export const Download = () => (
312 |
321 |
322 |
323 |
324 |
325 | );
326 |
327 | export const Image = () => (
328 |
337 |
338 |
339 |
340 |
341 | );
342 | export const Github = () => (
343 |
352 |
353 |
354 |
355 | );
356 |
--------------------------------------------------------------------------------
/client/src/components/Canvas.jsx:
--------------------------------------------------------------------------------
1 | import useCanvas from "../hooks/useCanvas";
2 |
3 | export default function Canvas() {
4 | const {
5 | canvasRef,
6 | dimension,
7 | handleMouseDown,
8 | handleMouseMove,
9 | handleMouseUp,
10 | handleWheel,
11 | } = useCanvas();
12 |
13 | return (
14 | <>
15 |
25 | >
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/components/Collaboration.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { Xmark } from "../assets/icons";
3 | import { useState } from "react";
4 | import { useAppContext } from "../provider/AppStates";
5 | import { v4 as uuid } from "uuid";
6 | import { useSearchParams } from "react-router-dom";
7 | import { socket } from "../api/socket";
8 |
9 | export default function Collaboration() {
10 | const [searchParams, setSearchParams] = useSearchParams();
11 | const { session, setSession } = useAppContext();
12 | const [open, setOpen] = useState(false);
13 | const users = 0;
14 |
15 | const startSession = () => {
16 | const sessionId = uuid();
17 | setSearchParams({ room: sessionId });
18 | setSession(sessionId);
19 | socket.emit("join", sessionId);
20 | };
21 |
22 | const endSession = () => {
23 | searchParams.delete("room");
24 | socket.emit("leave", session);
25 | setSession(null);
26 | setOpen(false);
27 | window.history.replaceState(null, null, "/");
28 | };
29 |
30 | return (
31 |
32 | 99 ? "99+" : users}
34 | type="button"
35 | className={"collaborateButton" + `${session ? " active" : ""}`}
36 | onClick={() => setOpen(true)}
37 | >
38 | Share
39 |
40 |
41 | {open && (
42 |
43 | {session ? (
44 |
45 | ) : (
46 |
47 | )}
48 |
49 | )}
50 |
51 | );
52 | }
53 |
54 | function CreateSession({ startSession }) {
55 | return (
56 |
57 |
Live collaboration
58 |
59 |
Invite people to collaborate on your drawing.
60 |
61 | Don't worry, the session is end-to-end encrypted, and fully private.
62 | Not even our server can see what you draw.
63 |
64 |
65 |
Start session
66 |
67 | );
68 | }
69 |
70 | function SessionInfo({ endSession }) {
71 | const copy = () => {
72 | navigator.clipboard.writeText(window.location.href);
73 | };
74 |
75 | return (
76 |
77 |
Live collaboration
78 |
79 |
80 |
Link
81 |
82 |
88 |
89 | Copy link
90 |
91 |
92 |
93 |
94 |
95 | Stop session
96 |
97 |
98 |
99 | );
100 | }
101 |
102 | function CollabBox({ collabState, children }) {
103 | const [Open, setOpen] = collabState;
104 | const exit = () => setOpen(false);
105 |
106 | return (
107 |
108 |
115 |
121 |
122 |
123 |
124 |
125 | {children}
126 |
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/client/src/components/Credits.jsx:
--------------------------------------------------------------------------------
1 | import { Github } from "../assets/icons";
2 |
3 | export default function Credits() {
4 | return (
5 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/Menu.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Delete, Download, Folder, MenuIcon, Xmark } from "../assets/icons";
3 | import { useAppContext } from "../provider/AppStates";
4 | import { saveElements, uploadElements } from "../helper/element";
5 |
6 | export default function Menu() {
7 | const { elements, setElements } = useAppContext();
8 | const [show, setShow] = useState(false);
9 |
10 | return (
11 |
12 | setShow((prev) => !prev)}
16 | >
17 | {show ? : }
18 |
19 |
20 | {show && }
21 |
22 | );
23 | }
24 |
25 | function MenuBox({ elements, setElements, setShow }) {
26 | const uploadJson = () => uploadElements(setElements);
27 | const downloadJson = () => saveElements(elements);
28 | const reset = () => setElements([]);
29 |
30 | return (
31 | <>
32 | setShow(false)}>
33 |
34 |
35 | Open
36 |
37 |
38 | Save
39 |
40 |
41 | Reset the canvas
42 |
43 |
44 | >
45 | );
46 | }
--------------------------------------------------------------------------------
/client/src/components/Style.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import {
3 | deleteElement,
4 | duplicateElement,
5 | minmax,
6 | moveElementLayer,
7 | updateElement,
8 | } from "../helper/element";
9 | import { useAppContext } from "../provider/AppStates";
10 | import { BACKGROUND_COLORS, STROKE_COLORS, STROKE_STYLES } from "../global/var";
11 | import { Backward, Delete, Duplicate, Forward, ToBack, ToFront } from "../assets/icons";
12 |
13 | export default function Style({ selectedElement }) {
14 | const { elements, setElements, setSelectedElement, setStyle } =
15 | useAppContext();
16 | const [elementStyle, setElementStyle] = useState({
17 | fill: selectedElement?.fill,
18 | strokeWidth: selectedElement?.strokeWidth,
19 | strokeStyle: selectedElement?.strokeStyle,
20 | strokeColor: selectedElement?.strokeColor,
21 | opacity: selectedElement?.opacity,
22 | });
23 |
24 | useEffect(() => {
25 | setElementStyle({
26 | fill: selectedElement?.fill,
27 | strokeWidth: selectedElement?.strokeWidth,
28 | strokeStyle: selectedElement?.strokeStyle,
29 | strokeColor: selectedElement?.strokeColor,
30 | opacity: selectedElement?.opacity,
31 | });
32 | }, [selectedElement]);
33 |
34 | const setStylesStates = (styleObject) => {
35 | setElementStyle((prevState) => ({ ...prevState, ...styleObject }));
36 | setStyle((prevState) => ({ ...prevState, ...styleObject }));
37 | };
38 |
39 | if (!selectedElement) return;
40 | return (
41 |
42 |
43 |
Stroke
44 |
45 | {STROKE_COLORS.map((color, index) => (
46 | {
56 | setStylesStates({ strokeColor: color });
57 | updateElement(
58 | selectedElement.id,
59 | {
60 | strokeColor: color,
61 | },
62 | setElements,
63 | elements
64 | );
65 | }}
66 | >
67 | ))}
68 |
69 |
70 |
71 |
Background
72 |
73 | {BACKGROUND_COLORS.map((fill, index) => (
74 | {
84 | setStylesStates({ fill });
85 | updateElement(
86 | selectedElement.id,
87 | {
88 | fill,
89 | },
90 | setElements,
91 | elements
92 | );
93 | }}
94 | >
95 | ))}
96 |
97 |
98 |
99 |
Stroke width
100 |
101 | {
109 | setStylesStates({ strokeWidth: minmax(+target.value, [0, 20]) });
110 | updateElement(
111 | selectedElement.id,
112 | {
113 | strokeWidth: minmax(+target.value, [0, 20]),
114 | },
115 | setElements,
116 | elements
117 | );
118 | }}
119 | />
120 |
121 |
122 |
123 |
Stroke style
124 |
125 | {STROKE_STYLES.map((style, index) => (
126 | {
135 | setStylesStates({ strokeStyle: style.slug });
136 | updateElement(
137 | selectedElement.id,
138 | {
139 | strokeStyle: style.slug,
140 | },
141 | setElements,
142 | elements
143 | );
144 | }}
145 | >
146 |
147 |
148 | ))}
149 |
150 |
151 |
152 |
Opacity
153 |
154 | {
162 | setStylesStates({
163 | opacity: minmax(+target.value, [0, 100]),
164 | });
165 | updateElement(
166 | selectedElement.id,
167 | {
168 | opacity: minmax(+target.value, [0, 100]),
169 | },
170 | setElements,
171 | elements
172 | );
173 | }}
174 | />
175 |
176 |
177 | {selectedElement?.id && (
178 |
179 |
180 |
Layers
181 |
182 |
187 | moveElementLayer(
188 | selectedElement.id,
189 | 0,
190 | setElements,
191 | elements
192 | )
193 | }
194 | >
195 |
196 |
197 |
202 | moveElementLayer(
203 | selectedElement.id,
204 | -1,
205 | setElements,
206 | elements
207 | )
208 | }
209 | >
210 |
211 |
212 |
217 | moveElementLayer(selectedElement.id, 1, setElements, elements)
218 | }
219 | >
220 |
221 |
222 |
227 | moveElementLayer(selectedElement.id, 2, setElements, elements)
228 | }
229 | >
230 |
231 |
232 |
233 |
234 |
235 |
236 |
Actions
237 |
238 |
241 | deleteElement(
242 | selectedElement,
243 | setElements,
244 | setSelectedElement
245 | )
246 | }
247 | title="Delete"
248 | className="itemButton option"
249 | >
250 |
251 |
252 |
257 | duplicateElement(
258 | selectedElement,
259 | setElements,
260 | setSelectedElement,
261 | 10
262 | )
263 | }
264 | >
265 |
266 |
267 |
268 |
269 |
270 | )}
271 |
272 | );
273 | }
274 |
--------------------------------------------------------------------------------
/client/src/components/ToolBar.jsx:
--------------------------------------------------------------------------------
1 | import { useAppContext } from "../provider/AppStates";
2 |
3 | export default function ToolBar() {
4 | const { tools: toolCols, selectedTool, lockTool } = useAppContext();
5 |
6 | return (
7 |
8 | {toolCols.map((tools, index) => (
9 |
10 | {tools.map((tool, index_) => (
11 | tool.toolAction(tool.slug)}
20 | title={tool.title}
21 | >
22 |
23 |
24 | ))}
25 |
26 | ))}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/client/src/components/Ui.jsx:
--------------------------------------------------------------------------------
1 | import { useAppContext } from "../provider/AppStates";
2 | import Style from "./Style";
3 | import ToolBar from "./ToolBar";
4 | import Zoom from "./Zoom";
5 | import UndoRedo from "./UndoRedo";
6 | import Menu from "./Menu";
7 | import Collaboration from "./Collaboration";
8 | import Credits from "./Credits";
9 |
10 | export default function Ui() {
11 | const { selectedElement, selectedTool, style } = useAppContext();
12 |
13 | return (
14 |
15 |
20 | {(!["selection", "hand"].includes(selectedTool) || selectedElement) && (
21 |
22 | )}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/client/src/components/UndoRedo.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Redo, Undo } from "../assets/icons";
3 | import { useAppContext } from "../provider/AppStates";
4 |
5 | export default function UndoRedo() {
6 | const { undo, redo } = useAppContext();
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/components/Zoom.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useAppContext } from "../provider/AppStates";
3 |
4 | export default function Zoom() {
5 | const { scale, onZoom } = useAppContext();
6 |
7 | return (
8 |
9 | onZoom(-0.1)}>
10 | -
11 |
12 | onZoom("default")}
15 | title="Reset zoom"
16 | >
17 | {new Intl.NumberFormat("fr-CA", { style: "percent" }).format(scale)}
18 |
19 | onZoom(0.1)}>
20 | +
21 |
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/global/var.js:
--------------------------------------------------------------------------------
1 | import { DashedLine, DottedLine, SolidLine } from "../assets/icons";
2 |
3 | export const BACKGROUND_COLORS = [
4 | "transparent",
5 | "rgb(255, 201, 201)",
6 | "rgb(178, 242, 187)",
7 | "rgb(165, 216, 255)",
8 | "rgb(255, 236, 153)",
9 | ];
10 |
11 | export const STROKE_COLORS = [
12 | "rgb(30, 30, 30)",
13 | "rgb(224, 49, 49)",
14 | "rgb(47, 158, 68)",
15 | "rgb(25, 113, 194)",
16 | "rgb(240, 140, 0)",
17 | ];
18 |
19 | export const STROKE_STYLES = [
20 | {
21 | slug: "solid",
22 | icon: SolidLine,
23 | },
24 | {
25 | slug: "dashed",
26 | icon: DashedLine,
27 | },
28 | {
29 | slug: "dotted",
30 | icon: DottedLine,
31 | },
32 | ];
33 |
34 | export const CANVAS_BACKGROUND = [
35 | "rgb(255, 201, 201)",
36 | "rgb(178, 242, 187)",
37 | "rgb(165, 216, 255)",
38 | "rgb(255, 236, 153)",
39 | ];
--------------------------------------------------------------------------------
/client/src/helper/canvas.js:
--------------------------------------------------------------------------------
1 | export const shapes = {
2 | arrow: (x1, y1, x2, y2, ctx) => {
3 | const headlen = 5;
4 | const angle = Math.atan2(y2 - y1, x2 - x1);
5 |
6 | ctx.moveTo(x1, y1);
7 | ctx.lineTo(x2, y2);
8 |
9 | ctx.moveTo(x2, y2);
10 | ctx.lineTo(
11 | x2 - headlen * Math.cos(angle - Math.PI / 7),
12 | y2 - headlen * Math.sin(angle - Math.PI / 7)
13 | );
14 |
15 | ctx.lineTo(
16 | x2 - headlen * Math.cos(angle + Math.PI / 7),
17 | y2 - headlen * Math.sin(angle + Math.PI / 7)
18 | );
19 |
20 | ctx.lineTo(x2, y2);
21 | ctx.lineTo(
22 | x2 - headlen * Math.cos(angle - Math.PI / 7),
23 | y2 - headlen * Math.sin(angle - Math.PI / 7)
24 | );
25 | },
26 | line: (x1, y1, x2, y2, ctx) => {
27 | ctx.moveTo(x1, y1);
28 | ctx.lineTo(x2, y2);
29 | },
30 | rectangle: (x1, y1, x2, y2, ctx) => {
31 | ctx.rect(x1, y1, x2 - x1, y2 - y1);
32 | },
33 | diamond: (x1, y1, x2, y2, ctx) => {
34 | const width = x2 - x1;
35 | const height = y2 - y1;
36 | ctx.moveTo(x1 + width / 2, y1);
37 | ctx.lineTo(x2, y1 + height / 2);
38 | ctx.lineTo(x1 + width / 2, y2);
39 | ctx.lineTo(x1, y1 + height / 2);
40 | },
41 | circle: (x1, y1, x2, y2, ctx) => {
42 | const width = x2 - x1;
43 | const height = y2 - y1;
44 | ctx.ellipse(
45 | x1 + width / 2,
46 | y1 + height / 2,
47 | Math.abs(width) / 2,
48 | Math.abs(height) / 2,
49 | 0,
50 | 0,
51 | 2 * Math.PI
52 | );
53 | },
54 | };
55 |
56 | export function distance(a, b) {
57 | return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
58 | }
59 |
60 | export function getFocuseDemention(element, padding) {
61 | const { x1, y1, x2, y2 } = element;
62 |
63 | if (element.tool == "line" || element.tool == "arrow")
64 | return { fx: x1, fy: y1, fw: x2, fh: y2 };
65 |
66 | const p = { min: padding, max: padding * 2 };
67 | const minX = Math.min(x1, x2);
68 | const maxX = Math.max(x1, x2);
69 | const minY = Math.min(y1, y2);
70 | const maxY = Math.max(y1, y2);
71 |
72 | return {
73 | fx: minX - p.min,
74 | fy: minY - p.min,
75 | fw: maxX - minX + p.max,
76 | fh: maxY - minY + p.max,
77 | };
78 | }
79 |
80 | export function getFocuseCorners(element, padding, position) {
81 | let { fx, fy, fw, fh } = getFocuseDemention(element, padding);
82 |
83 | if (element.tool == "line" || element.tool == "arrow") {
84 | return {
85 | line: { fx, fy, fw, fh },
86 | corners: [
87 | {
88 | slug: "l1",
89 | x: fx - position,
90 | y: fy - position,
91 | },
92 | {
93 | slug: "l2",
94 | x: fw - position,
95 | y: fh - position,
96 | },
97 | ],
98 | };
99 | }
100 | return {
101 | line: { fx, fy, fw, fh },
102 | corners: [
103 | {
104 | slug: "tl",
105 | x: fx - position,
106 | y: fy - position,
107 | },
108 | {
109 | slug: "tr",
110 | x: fx + fw - position,
111 | y: fy - position,
112 | },
113 | {
114 | slug: "bl",
115 | x: fx - position,
116 | y: fy + fh - position,
117 | },
118 | {
119 | slug: "br",
120 | x: fx + fw - position,
121 | y: fy + fh - position,
122 | },
123 | {
124 | slug: "tt",
125 | x: fx + fw / 2 - position,
126 | y: fy - position,
127 | },
128 | {
129 | slug: "rr",
130 | x: fx + fw - position,
131 | y: fy + fh / 2 - position,
132 | },
133 | {
134 | slug: "ll",
135 | x: fx - position,
136 | y: fy + fh / 2 - position,
137 | },
138 | {
139 | slug: "bb",
140 | x: fx + fw / 2 - position,
141 | y: fy + fh - position,
142 | },
143 | ],
144 | };
145 | }
146 |
147 | export function drawFocuse(element, context, padding, scale) {
148 | const lineWidth = 1 / scale;
149 | const square = 10 / scale;
150 | let round = square;
151 | const position = square / 2;
152 |
153 | let demention = getFocuseCorners(element, padding, position);
154 | let { fx, fy, fw, fh } = demention.line;
155 | let corners = demention.corners;
156 |
157 | context.lineWidth = lineWidth;
158 | context.strokeStyle = "#211C6A";
159 | context.fillStyle = "#EEF5FF";
160 |
161 | if (element.tool != "line" && element.tool != "arrow") {
162 | context.beginPath();
163 | context.rect(fx, fy, fw, fh);
164 | context.setLineDash([0, 0]);
165 | context.stroke();
166 | context.closePath();
167 | round = 3 / scale;
168 | }
169 |
170 | context.beginPath();
171 | corners.forEach((corner) => {
172 | context.roundRect(corner.x, corner.y, square, square, round);
173 | });
174 | context.fill();
175 | context.stroke();
176 | context.closePath();
177 | }
178 |
179 | export function draw(element, context) {
180 | context.beginPath();
181 | const {
182 | tool,
183 | x1,
184 | y1,
185 | x2,
186 | y2,
187 | strokeWidth,
188 | strokeColor,
189 | strokeStyle,
190 | fill,
191 | opacity,
192 | } = element;
193 |
194 | context.lineWidth = strokeWidth;
195 | context.strokeStyle = rgba(strokeColor, opacity);
196 | context.fillStyle = rgba(fill, opacity);
197 |
198 | if (strokeStyle == "dashed")
199 | context.setLineDash([strokeWidth * 2, strokeWidth * 2]);
200 | if (strokeStyle == "dotted") context.setLineDash([strokeWidth, strokeWidth]);
201 | if (strokeStyle == "solid") context.setLineDash([0, 0]);
202 |
203 | shapes[tool](x1, y1, x2, y2, context);
204 | context.fill();
205 | context.closePath();
206 | if (strokeWidth > 0) context.stroke();
207 | }
208 |
209 | function rgba(color, opacity) {
210 | if (color == "transparent") return "transparent";
211 |
212 | let matches = color.match(
213 | /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/
214 | );
215 | if (!matches) {
216 | throw new Error(
217 | "Invalid color format. Please provide a color in RGBA format."
218 | );
219 | }
220 | opacity /= 100;
221 | let red = parseInt(matches[1]);
222 | let green = parseInt(matches[2]);
223 | let blue = parseInt(matches[3]);
224 | let alpha = parseFloat(matches[4] * opacity || opacity);
225 |
226 | let newColor = `rgba(${red}, ${green}, ${blue}, ${alpha})`;
227 |
228 | return newColor;
229 | }
230 |
231 | export function inSelectedCorner(element, x, y, padding, scale) {
232 | padding = element.tool == "line" || element.tool == "arrow" ? 0 : padding;
233 |
234 | const square = 10 / scale;
235 | const position = square / 2;
236 |
237 | const corners = getFocuseCorners(element, padding, position).corners;
238 |
239 | const hoveredCorner = corners.find(
240 | (corner) =>
241 | x - corner.x <= square &&
242 | x - corner.x >= 0 &&
243 | y - corner.y <= square &&
244 | y - corner.y >= 0
245 | );
246 |
247 | return hoveredCorner;
248 | }
249 |
250 | export function cornerCursor(corner) {
251 | switch (corner) {
252 | case "tt":
253 | case "bb":
254 | return "s-resize";
255 | case "ll":
256 | case "rr":
257 | return "e-resize";
258 | case "tl":
259 | case "br":
260 | return "se-resize";
261 | case "tr":
262 | case "bl":
263 | return "ne-resize";
264 | case "l1":
265 | case "l2":
266 | return "pointer";
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/client/src/helper/element.js:
--------------------------------------------------------------------------------
1 | import { distance } from "./canvas";
2 | import { v4 as uuid } from "uuid";
3 |
4 | export function isWithinElement(x, y, element) {
5 | let { tool, x1, y1, x2, y2, strokeWidth } = element;
6 |
7 | switch (tool) {
8 | case "arrow":
9 | case "line":
10 | const a = { x: x1, y: y1 };
11 | const b = { x: x2, y: y2 };
12 | const c = { x, y };
13 |
14 | const offset = distance(a, b) - (distance(a, c) + distance(b, c));
15 | return Math.abs(offset) < (0.05 * strokeWidth || 1);
16 | case "circle":
17 | const width = x2 - x1 + strokeWidth;
18 | const height = y2 - y1 + strokeWidth;
19 | x1 -= strokeWidth / 2;
20 | y1 -= strokeWidth / 2;
21 |
22 | const centreX = x1 + width / 2;
23 | const centreY = y1 + height / 2;
24 |
25 | const mouseToCentreX = centreX - x;
26 | const mouseToCentreY = centreY - y;
27 |
28 | const radiusX = Math.abs(width) / 2;
29 | const radiusY = Math.abs(height) / 2;
30 |
31 | return (
32 | (mouseToCentreX * mouseToCentreX) / (radiusX * radiusX) +
33 | (mouseToCentreY * mouseToCentreY) / (radiusY * radiusY) <=
34 | 1
35 | );
36 |
37 | case "diamond":
38 | case "rectangle":
39 | const minX = Math.min(x1, x2) - strokeWidth / 2;
40 | const maxX = Math.max(x1, x2) + strokeWidth / 2;
41 | const minY = Math.min(y1, y2) - strokeWidth / 2;
42 | const maxY = Math.max(y1, y2) + strokeWidth / 2;
43 |
44 | return x >= minX && x <= maxX && y >= minY && y <= maxY;
45 | }
46 | }
47 |
48 | export function getElementPosition(x, y, elements) {
49 | return elements.filter((element) => isWithinElement(x, y, element)).at(-1);
50 | }
51 |
52 | export function createElement(x1, y1, x2, y2, style, tool) {
53 | return { id: uuid(), x1, y1, x2, y2, ...style, tool };
54 | }
55 |
56 | export function updateElement(
57 | id,
58 | stateOption,
59 | setState,
60 | state,
61 | overwrite = false
62 | ) {
63 | const index = state.findIndex((ele) => ele.id == id);
64 |
65 | const stateCopy = [...state];
66 |
67 | stateCopy[index] = {
68 | ...stateCopy[index],
69 | ...stateOption,
70 | };
71 |
72 | setState(stateCopy, overwrite);
73 | }
74 |
75 | export function deleteElement(s_element, setState, setSelectedElement) {
76 | if (!s_element) return;
77 |
78 | const { id } = s_element;
79 | setState((prevState) => prevState.filter((element) => element.id != id));
80 | setSelectedElement(null);
81 | }
82 |
83 | export function duplicateElement(
84 | s_element,
85 | setState,
86 | setSelected,
87 | factor,
88 | offsets = {}
89 | ) {
90 | if (!s_element) return;
91 |
92 | const { id } = s_element;
93 | setState((prevState) =>
94 | prevState
95 | .map((element) => {
96 | if (element.id == id) {
97 | const duplicated = { ...moveElement(element, factor), id: uuid() };
98 | setSelected({ ...duplicated, ...offsets });
99 | return [element, duplicated];
100 | }
101 | return element;
102 | })
103 | .flat()
104 | );
105 | }
106 |
107 | export function moveElement(element, factorX, factorY = null) {
108 | return {
109 | ...element,
110 | x1: element.x1 + factorX,
111 | y1: element.y1 + (factorY ?? factorX),
112 | x2: element.x2 + factorX,
113 | y2: element.y2 + (factorY ?? factorX),
114 | };
115 | }
116 |
117 | export function moveElementLayer(id, to, setState, state) {
118 | const index = state.findIndex((ele) => ele.id == id);
119 | const stateCopy = [...state];
120 | let replace = stateCopy[index];
121 | stateCopy.splice(index, 1);
122 |
123 | let toReplaceIndex = index;
124 | if (to == 1 && index < state.length - 1) {
125 | toReplaceIndex = index + 1;
126 | } else if (to == -1 && index > 0) {
127 | toReplaceIndex = index - 1;
128 | } else if (to == 0) {
129 | toReplaceIndex = 0;
130 | } else if (to == 2) {
131 | toReplaceIndex = state.length - 1;
132 | }
133 |
134 | const firstPart = stateCopy.slice(0, toReplaceIndex);
135 | const lastPart = stateCopy.slice(toReplaceIndex);
136 |
137 | setState([...firstPart, replace, ...lastPart]);
138 | }
139 |
140 | export function arrowMove(s_element, x, y, setState) {
141 | if (!s_element) return;
142 |
143 | const { id } = s_element;
144 | setState((prevState) =>
145 | prevState.map((element) => {
146 | if (element.id == id) {
147 | return moveElement(element, x, y);
148 | }
149 | return element;
150 | })
151 | );
152 | }
153 |
154 | export function minmax(value, interval) {
155 | return Math.max(Math.min(value, interval[1]), interval[0]);
156 | }
157 |
158 | export function getElementById(id, elements) {
159 | return elements.find((element) => element.id == id);
160 | }
161 |
162 | export function adjustCoordinates(element) {
163 | const { id, x1, x2, y1, y2, tool } = element;
164 | if (tool == "line" || tool == "arrow") return { id, x1, x2, y1, y2 };
165 |
166 | const minX = Math.min(x1, x2);
167 | const maxX = Math.max(x1, x2);
168 | const minY = Math.min(y1, y2);
169 | const maxY = Math.max(y1, y2);
170 |
171 | return { id, x1: minX, y1: minY, x2: maxX, y2: maxY };
172 | }
173 |
174 | export function resizeValue(
175 | corner,
176 | type,
177 | x,
178 | y,
179 | padding,
180 | { x1, x2, y1, y2 },
181 | offset,
182 | elementOffset
183 | ) {
184 | const getPadding = (condition) => {
185 | return condition ? padding : padding * -1;
186 | };
187 |
188 | const getType = (y, coordinate, originalCoordinate, eleOffset, te = false) => {
189 | if (type == "default") return originalCoordinate;
190 |
191 | const def = coordinate - y;
192 | if (te) return eleOffset - def;
193 | return eleOffset + def;
194 | };
195 |
196 | switch (corner) {
197 | case "tt":
198 | return {
199 | y1: y + getPadding(y < y2),
200 | y2: getType(y, offset.y, y2, elementOffset.y2),
201 | x1: getType(y, offset.y, x1, elementOffset.x1, true),
202 | x2: getType(y, offset.y, x2, elementOffset.x2),
203 | };
204 | case "bb":
205 | return { y2: y + getPadding(y < y1) };
206 | case "rr":
207 | return { x2: x + getPadding(x < x1) };
208 | case "ll":
209 | return { x1: x + getPadding(x < x2) };
210 | case "tl":
211 | return { x1: x + getPadding(x < x2), y1: y + getPadding(y < y2) };
212 | case "tr":
213 | return { x2: x + getPadding(x < x1), y1: y + getPadding(y < y2) };
214 | case "bl":
215 | return { x1: x + getPadding(x < x2), y2: y + getPadding(y < y1) };
216 | case "br":
217 | return { x2: x + getPadding(x < x1), y2: y + getPadding(y < y1) };
218 | case "l1":
219 | return { x1: x, y1: y };
220 | case "l2":
221 | return { x2: x, y2: y };
222 | }
223 | }
224 |
225 | export function saveElements(elements) {
226 | const jsonString = JSON.stringify(elements);
227 | const blob = new Blob([jsonString], { type: "application/json" });
228 | const url = URL.createObjectURL(blob);
229 |
230 | const link = document.createElement("a");
231 | link.download = "canvas.sketchFlow";
232 | link.href = url;
233 | link.click();
234 | }
235 |
236 | export function uploadElements(setElements) {
237 | function uploadJSON(event) {
238 | const file = event.target.files[0];
239 | const reader = new FileReader();
240 |
241 | reader.onload = (e) => {
242 | try {
243 | const data = JSON.parse(e.target.result);
244 | setElements(data);
245 | } catch (error) {
246 | console.error("Error :", error);
247 | }
248 | };
249 |
250 | reader.readAsText(file);
251 | }
252 |
253 | const fileInput = document.createElement("input");
254 | fileInput.type = "file";
255 | fileInput.accept = ".kyrosDraw";
256 | fileInput.onchange = uploadJSON;
257 | fileInput.click();
258 | }
259 |
--------------------------------------------------------------------------------
/client/src/helper/keys.js:
--------------------------------------------------------------------------------
1 | import { SHORT_CUTS } from "../global/var";
2 |
3 | export function shortKey(pressedKeys) {
4 | return SHORT_CUTS.some((subArray) => {
5 | const subArraySet = new Set(subArray);
6 | return [...subArraySet].every((item) => pressedKeys.has(item));
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/helper/ui.js:
--------------------------------------------------------------------------------
1 | export function lockUI(lock) {
2 | if (lock) return document.body.classList.add("lock-ui");
3 | document.body.classList.remove("lock-ui");
4 | }
5 |
--------------------------------------------------------------------------------
/client/src/hooks/useCanvas.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useLayoutEffect, useRef, useState } from "react";
2 | import { useAppContext } from "../provider/AppStates";
3 | import useDimension from "./useDimension";
4 | import { lockUI } from "../helper/ui";
5 | import {
6 | draw,
7 | drawFocuse,
8 | cornerCursor,
9 | inSelectedCorner,
10 | } from "../helper/canvas";
11 | import {
12 | adjustCoordinates,
13 | arrowMove,
14 | createElement,
15 | deleteElement,
16 | duplicateElement,
17 | getElementById,
18 | getElementPosition,
19 | minmax,
20 | resizeValue,
21 | saveElements,
22 | updateElement,
23 | uploadElements,
24 | } from "../helper/element";
25 | import useKeys from "./useKeys";
26 |
27 | export default function useCanvas() {
28 | const {
29 | selectedTool,
30 | setSelectedTool,
31 | action,
32 | setAction,
33 | elements,
34 | setElements,
35 | scale,
36 | onZoom,
37 | translate,
38 | setTranslate,
39 | scaleOffset,
40 | setScaleOffset,
41 | lockTool,
42 | style,
43 | selectedElement,
44 | setSelectedElement,
45 | undo,
46 | redo,
47 | } = useAppContext();
48 |
49 | const canvasRef = useRef(null);
50 | const keys = useKeys();
51 | const dimension = useDimension();
52 | const [isInElement, setIsInElement] = useState(false);
53 | const [inCorner, setInCorner] = useState(false);
54 | const [padding, setPadding] = useState(minmax(10 / scale, [0.5, 50]));
55 | const [cursor, setCursor] = useState("default");
56 | const [mouseAction, setMouseAction] = useState({ x: 0, y: 0 });
57 | const [resizeOldDementions, setResizeOldDementions] = useState(null)
58 |
59 | const mousePosition = ({ clientX, clientY }) => {
60 | clientX = (clientX - translate.x * scale + scaleOffset.x) / scale;
61 | clientY = (clientY - translate.y * scale + scaleOffset.y) / scale;
62 | return { clientX, clientY };
63 | };
64 |
65 | const handleMouseDown = (event) => {
66 | const { clientX, clientY } = mousePosition(event);
67 | lockUI(true);
68 |
69 | if (inCorner) {
70 | setResizeOldDementions(getElementById(selectedElement.id, elements))
71 | setElements((prevState) => prevState);
72 | setMouseAction({ x: event.clientX, y: event.clientY });
73 | setCursor(cornerCursor(inCorner.slug));
74 | setAction(
75 | "resize-" + inCorner.slug + (event.shiftKey ? "-shiftkey" : "")
76 | );
77 | return;
78 | }
79 |
80 | if (keys.has(" ") || selectedTool == "hand" || event.button == 1) {
81 | setTranslate((prevState) => ({
82 | ...prevState,
83 | sx: clientX,
84 | sy: clientY,
85 | }));
86 | setAction("translate");
87 | return;
88 | }
89 |
90 | if (selectedTool == "selection") {
91 | const element = getElementPosition(clientX, clientY, elements);
92 |
93 | if (element) {
94 | const offsetX = clientX - element.x1;
95 | const offsetY = clientY - element.y1;
96 |
97 | if (event.altKey) {
98 | duplicateElement(element, setElements, setSelectedElement, 0, {
99 | offsetX,
100 | offsetY,
101 | });
102 | } else {
103 | setElements((prevState) => prevState);
104 | setMouseAction({ x: event.clientX, y: event.clientY });
105 | setSelectedElement({ ...element, offsetX, offsetY });
106 | }
107 | setAction("move");
108 | } else {
109 | setSelectedElement(null);
110 | }
111 |
112 | return;
113 | }
114 |
115 | setAction("draw");
116 |
117 | const element = createElement(
118 | clientX,
119 | clientY,
120 | clientX,
121 | clientY,
122 | style,
123 | selectedTool
124 | );
125 | setElements((prevState) => [...prevState, element]);
126 | };
127 |
128 | const handleMouseMove = (event) => {
129 | const { clientX, clientY } = mousePosition(event);
130 |
131 | if (selectedElement) {
132 | setInCorner(
133 | inSelectedCorner(
134 | getElementById(selectedElement.id, elements),
135 | clientX,
136 | clientY,
137 | padding,
138 | scale
139 | )
140 | );
141 | }
142 |
143 | if (getElementPosition(clientX, clientY, elements)) {
144 | setIsInElement(true);
145 | } else {
146 | setIsInElement(false);
147 | }
148 |
149 | if (action == "draw") {
150 | const { id } = elements.at(-1);
151 | updateElement(
152 | id,
153 | { x2: clientX, y2: clientY },
154 | setElements,
155 | elements,
156 | true
157 | );
158 | } else if (action == "move") {
159 | const { id, x1, y1, x2, y2, offsetX, offsetY } = selectedElement;
160 |
161 | const width = x2 - x1;
162 | const height = y2 - y1;
163 |
164 | const nx = clientX - offsetX;
165 | const ny = clientY - offsetY;
166 |
167 | updateElement(
168 | id,
169 | { x1: nx, y1: ny, x2: nx + width, y2: ny + height },
170 | setElements,
171 | elements,
172 | true
173 | );
174 | } else if (action == "translate") {
175 | const x = clientX - translate.sx;
176 | const y = clientY - translate.sy;
177 |
178 | setTranslate((prevState) => ({
179 | ...prevState,
180 | x: prevState.x + x,
181 | y: prevState.y + y,
182 | }));
183 | } else if (action.startsWith("resize")) {
184 | const resizeCorner = action.slice(7, 9);
185 | const resizeType = action.slice(10) || "default";
186 | const s_element = getElementById(selectedElement.id, elements);
187 |
188 | updateElement(
189 | s_element.id,
190 | resizeValue(resizeCorner, resizeType, clientX, clientY, padding, s_element, mouseAction, resizeOldDementions),
191 | setElements,
192 | elements,
193 | true
194 | );
195 | }
196 | };
197 |
198 | const handleMouseUp = (event) => {
199 | setAction("none");
200 | lockUI(false);
201 |
202 | if (event.clientX == mouseAction.x && event.clientY == mouseAction.y) {
203 | setElements("prevState");
204 | return;
205 | }
206 |
207 | if (action == "draw") {
208 | const lastElement = elements.at(-1);
209 | const { id, x1, y1, x2, y2 } = adjustCoordinates(lastElement);
210 | updateElement(id, { x1, x2, y1, y2 }, setElements, elements, true);
211 | if (!lockTool) {
212 | setSelectedTool("selection");
213 | setSelectedElement(lastElement);
214 | }
215 | }
216 |
217 | if (action.startsWith("resize")) {
218 | const { id, x1, y1, x2, y2 } = adjustCoordinates(
219 | getElementById(selectedElement.id, elements)
220 | );
221 | updateElement(id, { x1, x2, y1, y2 }, setElements, elements, true);
222 | }
223 | };
224 |
225 | const handleWheel = (event) => {
226 | if (event.ctrlKey) {
227 | onZoom(event.deltaY * -0.01);
228 | return;
229 | }
230 |
231 | setTranslate((prevState) => ({
232 | ...prevState,
233 | x: prevState.x - event.deltaX,
234 | y: prevState.y - event.deltaY,
235 | }));
236 | };
237 |
238 | useLayoutEffect(() => {
239 | const canvas = canvasRef.current;
240 | const context = canvas.getContext("2d");
241 |
242 | const zoomPositionX = 2;
243 | const zoomPositionY = 2;
244 | // const zoomPositionX = scaleMouse ? canvas.width / scaleMouse.x : 2;
245 | // const zoomPositionY = scaleMouse ? canvas.height / scaleMouse.y : 2;
246 |
247 | const scaledWidth = canvas.width * scale;
248 | const scaledHeight = canvas.height * scale;
249 |
250 | const scaleOffsetX = (scaledWidth - canvas.width) / zoomPositionX;
251 | const scaleOffsetY = (scaledHeight - canvas.height) / zoomPositionY;
252 |
253 | setScaleOffset({ x: scaleOffsetX, y: scaleOffsetY });
254 |
255 | context.clearRect(0, 0, canvas.width, canvas.height);
256 |
257 | context.save();
258 |
259 | context.translate(
260 | translate.x * scale - scaleOffsetX,
261 | translate.y * scale - scaleOffsetY
262 | );
263 | context.scale(scale, scale);
264 |
265 | let focusedElement = null;
266 | elements.forEach((element) => {
267 | draw(element, context);
268 | if (element.id == selectedElement?.id) focusedElement = element;
269 | });
270 |
271 | const pd = minmax(10 / scale, [0.5, 50]);
272 | if (focusedElement != null) {
273 | drawFocuse(focusedElement, context, pd, scale);
274 | }
275 | setPadding(pd);
276 |
277 | context.restore();
278 | }, [elements, selectedElement, scale, translate, dimension]);
279 |
280 | useEffect(() => {
281 | const keyDownFunction = (event) => {
282 | const { key, ctrlKey, metaKey, shiftKey } = event;
283 | const prevent = () => event.preventDefault();
284 | if (selectedElement) {
285 | if (key == "Backspace" || key == "Delete") {
286 | prevent();
287 | deleteElement(selectedElement, setElements, setSelectedElement);
288 | }
289 |
290 | if (ctrlKey && key.toLowerCase() == "d") {
291 | prevent();
292 | duplicateElement(
293 | selectedElement,
294 | setElements,
295 | setSelectedElement,
296 | 10
297 | );
298 | }
299 |
300 | if (key == "ArrowLeft") {
301 | prevent();
302 | arrowMove(selectedElement, -1, 0, setElements);
303 | }
304 | if (key == "ArrowUp") {
305 | prevent();
306 | arrowMove(selectedElement, 0, -1, setElements);
307 | }
308 | if (key == "ArrowRight") {
309 | prevent();
310 | arrowMove(selectedElement, 1, 0, setElements);
311 | }
312 | if (key == "ArrowDown") {
313 | prevent();
314 | arrowMove(selectedElement, 0, 1, setElements);
315 | }
316 | }
317 |
318 | if (ctrlKey || metaKey) {
319 | if (
320 | key.toLowerCase() == "y" ||
321 | (key.toLowerCase() == "z" && shiftKey)
322 | ) {
323 | prevent();
324 | redo();
325 | } else if (key.toLowerCase() == "z") {
326 | prevent();
327 | undo();
328 | } else if (key.toLowerCase() == "s") {
329 | prevent();
330 | saveElements(elements);
331 | } else if (key.toLowerCase() == "o") {
332 | prevent();
333 | uploadElements(setElements);
334 | }
335 | }
336 | };
337 |
338 | window.addEventListener("keydown", keyDownFunction, { passive: false });
339 | return () => {
340 | window.removeEventListener("keydown", keyDownFunction);
341 | };
342 | }, [undo, redo, selectedElement]);
343 |
344 | useEffect(() => {
345 | if (selectedTool != "selection") {
346 | setSelectedElement(null);
347 | }
348 | }, [selectedTool]);
349 |
350 | useEffect(() => {
351 | if (action == "translate") {
352 | document.documentElement.style.setProperty("--canvas-cursor", "grabbing");
353 | } else if (action.startsWith("resize")) {
354 | document.documentElement.style.setProperty("--canvas-cursor", cursor);
355 | } else if (
356 | (keys.has(" ") || selectedTool == "hand") &&
357 | action != "move" &&
358 | action != "resize"
359 | ) {
360 | document.documentElement.style.setProperty("--canvas-cursor", "grab");
361 | } else if (selectedTool !== "selection") {
362 | document.documentElement.style.setProperty(
363 | "--canvas-cursor",
364 | "crosshair"
365 | );
366 | } else if (inCorner) {
367 | document.documentElement.style.setProperty(
368 | "--canvas-cursor",
369 | cornerCursor(inCorner.slug)
370 | );
371 | } else if (isInElement) {
372 | document.documentElement.style.setProperty("--canvas-cursor", "move");
373 | } else {
374 | document.documentElement.style.setProperty("--canvas-cursor", "default");
375 | }
376 | }, [keys, selectedTool, action, isInElement, inCorner]);
377 |
378 | useEffect(() => {
379 | const fakeWheel = (event) => {
380 | if (event.ctrlKey) {
381 | event.preventDefault();
382 | }
383 | };
384 | window.addEventListener("wheel", fakeWheel, {
385 | passive: false,
386 | });
387 |
388 | return () => {
389 | window.removeEventListener("wheel", fakeWheel);
390 | };
391 | }, []);
392 |
393 | return {
394 | canvasRef,
395 | handleMouseDown,
396 | handleMouseMove,
397 | handleMouseUp,
398 | handleWheel,
399 | dimension,
400 | };
401 | }
402 |
--------------------------------------------------------------------------------
/client/src/hooks/useDimension.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export default function useDimension() {
4 | const [dimension, setDimension] = useState({
5 | width: window.innerWidth,
6 | height: window.innerHeight,
7 | });
8 |
9 | useEffect(() => {
10 | const handleResize = () => {
11 | setDimension({
12 | width: window.innerWidth,
13 | height: window.innerHeight,
14 | });
15 | };
16 | window.addEventListener("resize", handleResize);
17 | return () => {
18 | window.removeEventListener("resize", handleResize);
19 | };
20 | }, []);
21 |
22 | return dimension;
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/hooks/useHistory.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { socket } from "../api/socket";
3 |
4 | export default function useHistory(initialState, session) {
5 | const [history, setHistory] = useState([initialState]);
6 | const [index, setIndex] = useState(0);
7 |
8 | const setState = (action, overwrite = false, emit = true) => {
9 | const newState =
10 | typeof action === "function" ? action(history[index]) : action;
11 |
12 | if (session) {
13 | if (action == "prevState") return;
14 | setHistory([newState]);
15 | setIndex(0);
16 |
17 | if (emit) {
18 | socket.emit("getElements", { elements: newState, room: session });
19 | }
20 | return;
21 | }
22 |
23 | if (action == "prevState") {
24 | const updatedState = [...history].slice(0, index + 1);
25 | setHistory([...updatedState, history[index - 1]]);
26 | setIndex((prevState) => prevState - 1);
27 | return;
28 | }
29 |
30 | if (overwrite) {
31 | const historyCopy = [...history];
32 | historyCopy[index] = newState;
33 | setHistory(historyCopy);
34 | } else {
35 | const updatedState = [...history].slice(0, index + 1);
36 | setHistory([...updatedState, newState]);
37 | setIndex((prevState) => prevState + 1);
38 | }
39 | };
40 |
41 | const undo = () =>
42 | setIndex((prevState) => (prevState > 0 ? prevState - 1 : prevState));
43 |
44 | const redo = () =>
45 | setIndex((prevState) =>
46 | prevState < history.length - 1 ? prevState + 1 : prevState
47 | );
48 |
49 | return [history[index], setState, undo, redo];
50 | }
51 |
--------------------------------------------------------------------------------
/client/src/hooks/useKeys.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 |
3 | export default function useKeys() {
4 | const [pressedKeys, setPressedKeys] = useState(new Set());
5 |
6 | useEffect(() => {
7 | const handleKeyDown = (event) => {
8 | setPressedKeys((prevKeys) => {
9 | const newKeys = new Set(prevKeys).add(event.key);
10 | return newKeys;
11 | });
12 | };
13 |
14 | const handleKeyUp = (event) => {
15 | setPressedKeys((prevKeys) => {
16 | const updatedKeys = new Set(prevKeys);
17 | updatedKeys.delete(event.key);
18 | return updatedKeys;
19 | });
20 | };
21 |
22 | const clearKeys = () => {
23 | setPressedKeys(new Set());
24 | };
25 |
26 | window.addEventListener("keydown", handleKeyDown);
27 | window.addEventListener("keyup", handleKeyUp);
28 | window.addEventListener("blur", clearKeys);
29 | return () => {
30 | window.removeEventListener("keydown", handleKeyDown);
31 | window.removeEventListener("keyup", handleKeyUp);
32 | window.removeEventListener("blur", clearKeys);
33 | };
34 | }, []);
35 |
36 | return pressedKeys;
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 | import App from "./App.jsx";
3 | import "./styles/index.css";
4 | import { AppContextProvider } from "./provider/AppStates.jsx";
5 | import { BrowserRouter } from "react-router-dom"
6 |
7 | ReactDOM.createRoot(document.getElementById("root")).render(
8 |
9 |
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/client/src/provider/AppStates.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from "react";
2 | import {
3 | Circle,
4 | Line,
5 | Rectangle,
6 | Selection,
7 | Diamond,
8 | Hand,
9 | Lock,
10 | Arrow,
11 | } from "../assets/icons";
12 | import { BACKGROUND_COLORS, STROKE_COLORS, STROKE_STYLES } from "../global/var";
13 | import { getElementById, minmax } from "../helper/element";
14 | import useHistory from "../hooks/useHistory";
15 | import { socket } from "../api/socket";
16 |
17 | const AppContext = createContext();
18 |
19 | const isElementsInLocal = () => {
20 | try {
21 | JSON.parse(localStorage.getItem("elements")).forEach(() => {});
22 | return JSON.parse(localStorage.getItem("elements"));
23 | } catch (err) {
24 | return [];
25 | }
26 | };
27 |
28 | const initialElements = isElementsInLocal();
29 |
30 | export function AppContextProvider({ children }) {
31 | const [session, setSession] = useState(null);
32 | const [selectedElement, setSelectedElement] = useState(null);
33 | const [elements, setElements, undo, redo] = useHistory(
34 | initialElements,
35 | session
36 | );
37 | const [action, setAction] = useState("none");
38 | const [selectedTool, setSelectedTool] = useState("selection");
39 | const [translate, setTranslate] = useState({
40 | x: 0,
41 | y: 0,
42 | sx: 0,
43 | sy: 0,
44 | });
45 | const [scale, setScale] = useState(1);
46 | const [scaleOffset, setScaleOffset] = useState({ x: 0, y: 0 });
47 | const [lockTool, setLockTool] = useState(false);
48 | const [style, setStyle] = useState({
49 | strokeWidth: 3,
50 | strokeColor: STROKE_COLORS[0],
51 | strokeStyle: STROKE_STYLES[0].slug,
52 | fill: BACKGROUND_COLORS[0],
53 | opacity: 100,
54 | });
55 |
56 | useEffect(() => {
57 | if (session == null) {
58 | localStorage.setItem("elements", JSON.stringify(elements));
59 | }
60 |
61 | if (!getElementById(selectedElement?.id, elements)) {
62 | setSelectedElement(null);
63 | }
64 | }, [elements, session, selectedElement]);
65 |
66 | const onZoom = (delta) => {
67 | if (delta == "default") {
68 | setScale(1);
69 | return;
70 | }
71 | setScale((prevState) => minmax(prevState + delta, [0.1, 20]));
72 | };
73 |
74 | const toolAction = (slug) => {
75 | if (slug == "lock") {
76 | setLockTool((prevState) => !prevState);
77 | return;
78 | }
79 | setSelectedTool(slug);
80 | };
81 |
82 | const tools = [
83 | [
84 | {
85 | slug: "lock",
86 | icon: Lock,
87 | title: "Keep selected tool active after drawing",
88 | toolAction,
89 | },
90 | ],
91 | [
92 | {
93 | slug: "hand",
94 | icon: Hand,
95 | title: "Hand",
96 | toolAction,
97 | },
98 | {
99 | slug: "selection",
100 | icon: Selection,
101 | title: "Selection",
102 | toolAction,
103 | },
104 | {
105 | slug: "rectangle",
106 | icon: Rectangle,
107 | title: "Rectangle",
108 | toolAction,
109 | },
110 | {
111 | slug: "diamond",
112 | icon: Diamond,
113 | title: "Diamond",
114 | toolAction,
115 | },
116 | {
117 | slug: "circle",
118 | icon: Circle,
119 | title: "Circle",
120 | toolAction,
121 | },
122 | {
123 | slug: "arrow",
124 | icon: Arrow,
125 | title: "Arrow",
126 | toolAction,
127 | },
128 | {
129 | slug: "line",
130 | icon: Line,
131 | title: "Line",
132 | toolAction,
133 | },
134 | ],
135 | ];
136 |
137 | useEffect(() => {
138 | if (session) {
139 | socket.on("setElements", (data) => {
140 | setElements(data, true, false);
141 | });
142 | }
143 | }, [session]);
144 |
145 | return (
146 |
174 | {children}
175 |
176 | );
177 | }
178 |
179 | export function useAppContext() {
180 | return useContext(AppContext);
181 | }
182 |
--------------------------------------------------------------------------------
/client/src/styles/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: primaryFont;
3 | src: url(../assets/fonts/Roboto-Bold.ttf);
4 | }
5 |
6 | @font-face {
7 | font-family: secondaryFont;
8 | src: url(../assets/fonts/Roboto-Medium.ttf);
9 | }
10 |
11 | * {
12 | margin: 0;
13 | padding: 0;
14 | box-sizing: border-box;
15 | font-family: primaryFont;
16 |
17 | user-select: none !important;
18 | }
19 |
20 | html {
21 | --background: #ffffff;
22 | --secondary-background: #f0f0f0;
23 | --background-50: #ffffff80;
24 |
25 | --primary-color: #1f1f22;
26 | }
27 |
28 | html {
29 | --bg-size: 14px;
30 |
31 | position: relative;
32 | background: radial-gradient(
33 | var(--secondary-background) 10%,
34 | var(--background) 0
35 | );
36 | background-size: var(--bg-size) var(--bg-size);
37 | background-repeat: repeat;
38 |
39 | overflow: hidden;
40 |
41 | min-height: 100vh;
42 | }
43 |
44 | body.lock-ui main.ui * {
45 | pointer-events: none;
46 | }
47 |
48 | canvas#canvas {
49 | position: fixed;
50 | cursor: var(--canvas-cursor, "default");
51 | }
52 |
53 | main.ui {
54 | position: fixed;
55 | inset: 0;
56 | z-index: 99 !important;
57 | pointer-events: none;
58 | padding: 20px;
59 |
60 | display: flex;
61 | flex-direction: column;
62 | justify-content: space-between;
63 | }
64 |
65 | main.ui header {
66 | display: flex;
67 | align-items: center;
68 | justify-content: space-between;
69 | }
70 | main.ui footer {
71 | display: flex;
72 | align-items: center;
73 | justify-content: space-between;
74 | gap: 10px;
75 | }
76 |
77 | main.ui footer > div {
78 | display: flex;
79 | gap: 10px;
80 | }
81 |
82 | html::before {
83 | content: "";
84 | position: absolute;
85 | top: 50%;
86 | transform: translateY(-50%);
87 | background: #d2d2d2;
88 | height: 1px;
89 | width: 100%;
90 | pointer-events: none;
91 | z-index: -1;
92 | }
93 | html::after {
94 | content: "";
95 | position: absolute;
96 | left: 50%;
97 | transform: translateX(-50%);
98 | background: #d2d2d2;
99 | width: 1px;
100 | height: 100%;
101 | pointer-events: none;
102 | z-index: -1;
103 | }
104 |
105 | section {
106 | pointer-events: all;
107 | width: fit-content;
108 |
109 | padding: 4px;
110 | border: 1px solid var(--secondary-background);
111 | border-radius: 8px;
112 | background: var(--background-50);
113 | backdrop-filter: blur(2px);
114 |
115 | box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
116 | }
117 |
118 | button:active {
119 | border: 1px solid rgb(71, 114, 180) !important;
120 | }
121 |
122 | section.toolbar {
123 | display: flex;
124 | }
125 |
126 | section.toolbar > div {
127 | --gap: 5px;
128 | display: flex;
129 | gap: var(--gap);
130 | }
131 |
132 | section.toolbar > div:not(:last-child)::after {
133 | content: "";
134 | position: relative;
135 | top: 0;
136 | left: 0;
137 | background: #c0c0c0;
138 | width: 1px;
139 | height: 60%;
140 | border-radius: 50%;
141 | margin: auto;
142 | margin-right: var(--gap);
143 | }
144 |
145 | section.toolbar .toolbutton {
146 | display: grid;
147 | place-content: center;
148 | color: var(--primary-color);
149 |
150 | width: 30px;
151 | height: 30px;
152 |
153 | background: none;
154 | border: none;
155 | border-radius: 4px;
156 | cursor: pointer;
157 | }
158 | section.toolbar .toolbutton:hover {
159 | background: rgba(224, 223, 255, 0.5);
160 | }
161 |
162 | section.toolbar .toolbutton.selected,
163 | section.toolbar .toolbutton.lock[data-lock="true"] {
164 | color: #030064;
165 | background: #e0dfffd5;
166 | }
167 |
168 | section.zoomOptions {
169 | display: flex;
170 | gap: 5px;
171 | }
172 |
173 | section.zoomOptions .zoom {
174 | display: grid;
175 | place-content: center;
176 | color: var(--primary-color);
177 |
178 | width: 30px;
179 | height: 30px;
180 |
181 | background: none;
182 | border: none;
183 | border-radius: 4px;
184 | cursor: pointer;
185 | font-size: 1.1em;
186 | font-weight: 900;
187 | }
188 |
189 | section.zoomOptions .zoom.text {
190 | width: fit-content;
191 | width: 4em !important;
192 | font-size: 15px;
193 | }
194 |
195 | section.styleOptions {
196 | position: fixed;
197 | top: 80px;
198 | left: 20px;
199 | padding: 15px;
200 |
201 | overflow: auto;
202 | max-height: calc(100vh - 140px);
203 | display: flex;
204 | flex-direction: column;
205 | gap: 10px;
206 | }
207 |
208 | section.styleOptions::-webkit-scrollbar {
209 | width: 3px;
210 | }
211 | section.styleOptions::-webkit-scrollbar-thumb {
212 | background: #ced4da;
213 | border-radius: 100px;
214 | }
215 |
216 | section.styleOptions .group p {
217 | font-size: 12px;
218 | font-family: secondaryFont;
219 | color: var(--primary-color);
220 | margin-bottom: 5px;
221 | }
222 |
223 | section.styleOptions .group .innerGroup {
224 | display: flex;
225 | align-items: center;
226 | gap: 5px;
227 | }
228 |
229 | section.styleOptions .group .innerGroup .itemButton {
230 | position: relative;
231 | border-radius: 5px;
232 | box-sizing: content-box;
233 | cursor: pointer;
234 | color: var(--primary-color);
235 |
236 | display: grid;
237 | place-content: center;
238 | }
239 |
240 | section.styleOptions .group .innerGroup .itemButton.color {
241 | border: 1px solid #81818179;
242 | background: var(--color);
243 | width: 20px;
244 | height: 20px;
245 | }
246 |
247 | section.styleOptions .group .innerGroup .itemButton.color.selected::before {
248 | content: "";
249 | position: absolute;
250 | inset: 0;
251 | top: 0;
252 | left: 0;
253 | z-index: 55;
254 | border: 1px solid rgb(71, 114, 180);
255 | border-radius: 5px;
256 | scale: 1.3;
257 | }
258 |
259 | section.styleOptions .group .innerGroup .itemButton.option.selected {
260 | color: #0f0c69;
261 | background: #c3c1ebd5;
262 | }
263 |
264 | section.styleOptions .group .innerGroup .itemButton.option {
265 | border: 1px solid #bbc0c779;
266 | background: #e4e8ea;
267 | width: 30px;
268 | height: 30px;
269 | }
270 |
271 | section.undoRedo {
272 | display: flex;
273 | align-items: center;
274 | justify-content: center;
275 | gap: 5px;
276 | }
277 |
278 | section.undoRedo > button {
279 | display: grid;
280 | place-content: center;
281 | color: var(--primary-color);
282 |
283 | width: 30px;
284 | height: 30px;
285 |
286 | background: none;
287 | border: none;
288 | border-radius: 4px;
289 | cursor: pointer;
290 | }
291 |
292 | div.menu {
293 | position: relative;
294 | pointer-events: all;
295 | }
296 |
297 | div.menu .menuBtn {
298 | display: grid;
299 | place-content: center;
300 | background: var(--secondary-background);
301 | border: 1px solid #949494;
302 | color: #808080;
303 | border-radius: 10px;
304 | width: 40px;
305 | height: 40px;
306 | cursor: pointer;
307 | }
308 |
309 | div.menu .menuItems {
310 | position: absolute;
311 | background: var(--background);
312 | margin-top: 10px;
313 | z-index: 9999;
314 | padding: 7px;
315 | width: 250px;
316 | display: flex;
317 | flex-direction: column;
318 | }
319 |
320 | div.menu .menuItems .menuItem * {
321 | pointer-events: none;
322 | }
323 |
324 | div.menu .menuItems .menuItem {
325 | padding: 10px 10px;
326 | display: flex;
327 | gap: 7px;
328 | border: none;
329 | background: none;
330 | cursor: pointer;
331 | border-radius: 5px;
332 | font-family: secondaryFont;
333 |
334 | color: #373737;
335 | }
336 |
337 | div.menu .menuItems .menuItem:hover {
338 | background: var(--secondary-background);
339 | }
340 |
341 | .menuBlur {
342 | position: fixed;
343 | width: 100vw;
344 | height: 100vh;
345 | z-index: 9998;
346 | top: 0;
347 | left: 0;
348 | }
349 |
350 | div.collaboration {
351 | pointer-events: all;
352 | z-index: 999;
353 | }
354 |
355 | .collaborateButton {
356 | position: relative;
357 | padding: 0 20px;
358 | height: 40px;
359 | border-radius: 7px;
360 | border: 1px solid #6965db;
361 | background: #6965db;
362 | color: #ffffff;
363 | font-family: secondaryFont;
364 | cursor: pointer;
365 | }
366 |
367 | .collaborateButton.active {
368 | border: 1px solid #54b435;
369 | background: #54b435;
370 | }
371 |
372 | /* .collaborateButton.active::before {
373 | content: attr(data-users);
374 | position: absolute;
375 | top: 0;
376 | right: 0;
377 | transform: translate(50%, -50%);
378 | width: 19px;
379 | height: 19px;
380 | border-radius: 50%;
381 |
382 | display: grid;
383 | place-content: center;
384 |
385 | font-size: 0.65em;
386 |
387 | background: #73df4f;
388 | box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.303);
389 | } */
390 |
391 | section.collaborationBox {
392 | position: relative;
393 | padding: 20px;
394 | z-index: 99993;
395 |
396 | width: calc(200px + 31vw);
397 | display: flex;
398 | justify-content: center;
399 | align-items: center;
400 | background: white;
401 | }
402 |
403 | div.collaborationBoxBack {
404 | position: fixed;
405 | inset: 0;
406 | background: #00000061;
407 | z-index: 99992;
408 | }
409 |
410 | div.collaborationContainer {
411 | position: fixed;
412 | inset: 0;
413 | display: grid;
414 | place-content: center;
415 | }
416 |
417 | .closeCollbBox {
418 | position: absolute;
419 | top: 10px;
420 | right: 10px;
421 | display: grid;
422 | place-content: center;
423 | padding-bottom: 1px;
424 | border-radius: 5px;
425 | width: 30px;
426 | height: 30px;
427 | background: none;
428 | border: none;
429 | color: #454545;
430 | cursor: pointer;
431 | transition: 0.3s;
432 | }
433 |
434 | .closeCollbBox:hover {
435 | background-color: #8b8b8b5a;
436 | }
437 |
438 | .collabCreate,
439 | .collabInfo {
440 | display: flex;
441 | flex-direction: column;
442 | align-items: center;
443 | justify-content: center;
444 | gap: 10px;
445 | }
446 |
447 | section.collaborationBox h2 {
448 | text-align: center;
449 | font-size: 25px;
450 | color: #6965db;
451 | }
452 |
453 | .collabCreate p {
454 | font-family: secondaryFont;
455 | color: #454545;
456 | font-size: 14px;
457 | text-align: center;
458 | margin-bottom: 5px;
459 | }
460 |
461 | .collabCreate button {
462 | font-family: secondaryFont;
463 | padding: 10px 20px;
464 | background: #6965db;
465 | border: 1px solid #6965db;
466 | border-radius: 7px;
467 | color: #ffffff;
468 | font-size: 14px;
469 | cursor: pointer;
470 | }
471 |
472 | @media (max-width: 600px) {
473 | section.collaborationBox {
474 | width: 100vw;
475 | height: 100vh;
476 | border: none;
477 | border-radius: 0;
478 | }
479 | }
480 |
481 | .collabGroup {
482 | width: 100%;
483 | }
484 |
485 | .collabLink {
486 | display: flex;
487 | justify-content: space-between;
488 | gap: 5px;
489 | width: 100%;
490 | }
491 |
492 | .collabLink button {
493 | font-family: secondaryFont;
494 | padding: 10px 15px;
495 | background: #6965db;
496 | border: 1px solid #6965db;
497 | border-radius: 7px;
498 | color: #ffffff;
499 | font-size: 12px;
500 | cursor: pointer;
501 | }
502 |
503 | .collabLink input {
504 | font-family: secondaryFont;
505 | padding: 10px 15px;
506 | border: 1px solid #5c5c5c;
507 | border-radius: 7px;
508 | color: #494949;
509 | font-size: 12px;
510 | flex-grow: 1;
511 | }
512 |
513 | .collabInfo {
514 | width: 100%;
515 | }
516 |
517 | .collabGroup label {
518 | display: block;
519 | font-size: 15px;
520 | color: var(--primary-color);
521 | margin-bottom: 5px;
522 | }
523 |
524 | .endCollab button {
525 | font-family: secondaryFont;
526 | padding: 10px 20px;
527 | background: none;
528 | border: 1px solid #d72727;
529 | border-radius: 7px;
530 | color: #d72727;
531 | font-size: 14px;
532 | cursor: pointer;
533 | }
534 |
535 | section.credits {
536 | padding: 0;
537 | }
538 |
539 | #credits {
540 | display: flex;
541 | justify-content: center;
542 | align-items: center;
543 | gap: 10px;
544 | padding: 10px 20px;
545 | color: var(--primary-color);
546 | text-decoration: none;
547 | font-size: 13px;
548 | }
549 |
550 | #credits:hover {
551 | text-decoration: underline;
552 | }
553 |
--------------------------------------------------------------------------------
/client/src/views/WorkSpace.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import Canvas from "../components/Canvas";
3 | import Ui from "../components/Ui";
4 | import { useSearchParams } from "react-router-dom";
5 | import { useAppContext } from "../provider/AppStates";
6 | import { socket } from "../api/socket";
7 |
8 | export default function WorkSpace() {
9 | const { setSession } = useAppContext();
10 | const [searchParams] = useSearchParams();
11 |
12 | useEffect(() => {
13 | const room = searchParams.get("room");
14 |
15 | if (room) {
16 | setSession(room);
17 | socket.emit("join", room);
18 | }
19 | }, [searchParams]);
20 |
21 | return (
22 | <>
23 |
24 |
25 | >
26 | );
27 | }
--------------------------------------------------------------------------------
/client/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/(.*)",
5 | "destination": "/"
6 | }
7 | ]
8 | }
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | })
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "client": "npm --prefix client run dev",
4 | "server": "npm --prefix server run dev"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | SketchFlow is a collaborative drawing tool built with React and Vite, Express, and Socket.IO, designed for creating diagrams, sketches, and illustrations in real-time.
9 |
10 |
11 |
12 | Live demo
13 | •
14 | Download
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## Features
22 |
23 | - **Real-Time Collaboration**: SketchFlow enables multiple users to collaborate on the same drawing simultaneously using WebSockets.
24 | - **Intuitive Interface**: The application provides a simple and intuitive interface for drawing and creating diagrams.
25 | - **Basic Shapes**: Draw various shapes including rectangles, lines, circles, arrows, diamonds, and more.
26 | - **Background and Stroke Color**: Customize background color and stroke color for shapes.
27 | - **Export and Import**: Save and load drawings as .sketchFlow files for easy sharing and editing.
28 | - **Undo and Redo**: Easily undo or redo changes to your drawing.
29 | - **Zoom In & Out**: Zoom in and out for precise editing and viewing.
30 | - **Opacity Control**: Adjust opacity for shapes and elements.
31 | - **Layers**: Organize your drawing with layers for better management and control.
32 | - **Delete and Duplicate**: Delete shapes and elements, and duplicate them for quick replication.
33 | - **Stroke Style**: Change stroke style for shapes.
34 |
35 | ## Tech Stack
36 |
37 | - **Client**: React(vite.js).
38 | - **Server**: Node, Express, webSocket.
39 |
40 |
41 | 🚨 **Note** : Not fully responsive in mobile phone screen
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 |
3 | const express = require("express");
4 | const app = express();
5 | const http = require("http");
6 | const { Server } = require("socket.io");
7 | const cors = require("cors");
8 | const parser = require("socket.io-msgpack-parser");
9 |
10 | const CLIENT_URL = process.env.CLIENT_URL;
11 | const PORT = process.env.PORT || 8080;
12 |
13 | app.use(
14 | cors({
15 | origin: [CLIENT_URL],
16 | })
17 | );
18 |
19 | const server = http.createServer(app);
20 |
21 | const io = new Server(server, {
22 | parser,
23 | cors: {
24 | origin: [CLIENT_URL],
25 | },
26 | });
27 |
28 | io.on("connection", (socket) => {
29 | socket.on("join", (room) => {
30 | socket.join(room);
31 | });
32 |
33 | socket.on("leave", (room) => {
34 | socket.leave(room);
35 | });
36 |
37 | socket.on("getElements", ({ elements, room }) => {
38 | socket.to(room).emit("setElements", elements);
39 | });
40 | });
41 |
42 | app.get("/", (req, res) => {
43 | res.send(
44 | `To try the app visite : ${CLIENT_URL} `
45 | );
46 | });
47 |
48 | server.listen(PORT, () => {
49 | console.log("Listen in port : " + PORT);
50 | });
51 |
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "server",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "cors": "^2.8.5",
13 | "dotenv": "^16.4.5",
14 | "express": "^4.18.3",
15 | "socket.io": "^4.7.4",
16 | "socket.io-msgpack-parser": "^3.0.2"
17 | },
18 | "devDependencies": {
19 | "nodemon": "^3.1.0"
20 | }
21 | },
22 | "node_modules/@socket.io/component-emitter": {
23 | "version": "3.1.0",
24 | "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
25 | "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
26 | },
27 | "node_modules/@types/cookie": {
28 | "version": "0.4.1",
29 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
30 | "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
31 | },
32 | "node_modules/@types/cors": {
33 | "version": "2.8.17",
34 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
35 | "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
36 | "dependencies": {
37 | "@types/node": "*"
38 | }
39 | },
40 | "node_modules/@types/node": {
41 | "version": "20.11.25",
42 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz",
43 | "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==",
44 | "dependencies": {
45 | "undici-types": "~5.26.4"
46 | }
47 | },
48 | "node_modules/abbrev": {
49 | "version": "1.1.1",
50 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
51 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
52 | "dev": true
53 | },
54 | "node_modules/accepts": {
55 | "version": "1.3.8",
56 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
57 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
58 | "dependencies": {
59 | "mime-types": "~2.1.34",
60 | "negotiator": "0.6.3"
61 | },
62 | "engines": {
63 | "node": ">= 0.6"
64 | }
65 | },
66 | "node_modules/anymatch": {
67 | "version": "3.1.3",
68 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
69 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
70 | "dev": true,
71 | "dependencies": {
72 | "normalize-path": "^3.0.0",
73 | "picomatch": "^2.0.4"
74 | },
75 | "engines": {
76 | "node": ">= 8"
77 | }
78 | },
79 | "node_modules/array-flatten": {
80 | "version": "1.1.1",
81 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
82 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
83 | },
84 | "node_modules/balanced-match": {
85 | "version": "1.0.2",
86 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
87 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
88 | "dev": true
89 | },
90 | "node_modules/base64id": {
91 | "version": "2.0.0",
92 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
93 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
94 | "engines": {
95 | "node": "^4.5.0 || >= 5.9"
96 | }
97 | },
98 | "node_modules/binary-extensions": {
99 | "version": "2.2.0",
100 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
101 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
102 | "dev": true,
103 | "engines": {
104 | "node": ">=8"
105 | }
106 | },
107 | "node_modules/body-parser": {
108 | "version": "1.20.2",
109 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
110 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
111 | "dependencies": {
112 | "bytes": "3.1.2",
113 | "content-type": "~1.0.5",
114 | "debug": "2.6.9",
115 | "depd": "2.0.0",
116 | "destroy": "1.2.0",
117 | "http-errors": "2.0.0",
118 | "iconv-lite": "0.4.24",
119 | "on-finished": "2.4.1",
120 | "qs": "6.11.0",
121 | "raw-body": "2.5.2",
122 | "type-is": "~1.6.18",
123 | "unpipe": "1.0.0"
124 | },
125 | "engines": {
126 | "node": ">= 0.8",
127 | "npm": "1.2.8000 || >= 1.4.16"
128 | }
129 | },
130 | "node_modules/brace-expansion": {
131 | "version": "1.1.11",
132 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
133 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
134 | "dev": true,
135 | "dependencies": {
136 | "balanced-match": "^1.0.0",
137 | "concat-map": "0.0.1"
138 | }
139 | },
140 | "node_modules/braces": {
141 | "version": "3.0.2",
142 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
143 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
144 | "dev": true,
145 | "dependencies": {
146 | "fill-range": "^7.0.1"
147 | },
148 | "engines": {
149 | "node": ">=8"
150 | }
151 | },
152 | "node_modules/bytes": {
153 | "version": "3.1.2",
154 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
155 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
156 | "engines": {
157 | "node": ">= 0.8"
158 | }
159 | },
160 | "node_modules/call-bind": {
161 | "version": "1.0.7",
162 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
163 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
164 | "dependencies": {
165 | "es-define-property": "^1.0.0",
166 | "es-errors": "^1.3.0",
167 | "function-bind": "^1.1.2",
168 | "get-intrinsic": "^1.2.4",
169 | "set-function-length": "^1.2.1"
170 | },
171 | "engines": {
172 | "node": ">= 0.4"
173 | },
174 | "funding": {
175 | "url": "https://github.com/sponsors/ljharb"
176 | }
177 | },
178 | "node_modules/chokidar": {
179 | "version": "3.6.0",
180 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
181 | "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
182 | "dev": true,
183 | "dependencies": {
184 | "anymatch": "~3.1.2",
185 | "braces": "~3.0.2",
186 | "glob-parent": "~5.1.2",
187 | "is-binary-path": "~2.1.0",
188 | "is-glob": "~4.0.1",
189 | "normalize-path": "~3.0.0",
190 | "readdirp": "~3.6.0"
191 | },
192 | "engines": {
193 | "node": ">= 8.10.0"
194 | },
195 | "funding": {
196 | "url": "https://paulmillr.com/funding/"
197 | },
198 | "optionalDependencies": {
199 | "fsevents": "~2.3.2"
200 | }
201 | },
202 | "node_modules/component-emitter": {
203 | "version": "1.3.1",
204 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
205 | "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
206 | "funding": {
207 | "url": "https://github.com/sponsors/sindresorhus"
208 | }
209 | },
210 | "node_modules/concat-map": {
211 | "version": "0.0.1",
212 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
213 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
214 | "dev": true
215 | },
216 | "node_modules/content-disposition": {
217 | "version": "0.5.4",
218 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
219 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
220 | "dependencies": {
221 | "safe-buffer": "5.2.1"
222 | },
223 | "engines": {
224 | "node": ">= 0.6"
225 | }
226 | },
227 | "node_modules/content-type": {
228 | "version": "1.0.5",
229 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
230 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
231 | "engines": {
232 | "node": ">= 0.6"
233 | }
234 | },
235 | "node_modules/cookie": {
236 | "version": "0.5.0",
237 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
238 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
239 | "engines": {
240 | "node": ">= 0.6"
241 | }
242 | },
243 | "node_modules/cookie-signature": {
244 | "version": "1.0.6",
245 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
246 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
247 | },
248 | "node_modules/cors": {
249 | "version": "2.8.5",
250 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
251 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
252 | "dependencies": {
253 | "object-assign": "^4",
254 | "vary": "^1"
255 | },
256 | "engines": {
257 | "node": ">= 0.10"
258 | }
259 | },
260 | "node_modules/debug": {
261 | "version": "2.6.9",
262 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
263 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
264 | "dependencies": {
265 | "ms": "2.0.0"
266 | }
267 | },
268 | "node_modules/define-data-property": {
269 | "version": "1.1.4",
270 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
271 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
272 | "dependencies": {
273 | "es-define-property": "^1.0.0",
274 | "es-errors": "^1.3.0",
275 | "gopd": "^1.0.1"
276 | },
277 | "engines": {
278 | "node": ">= 0.4"
279 | },
280 | "funding": {
281 | "url": "https://github.com/sponsors/ljharb"
282 | }
283 | },
284 | "node_modules/depd": {
285 | "version": "2.0.0",
286 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
287 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
288 | "engines": {
289 | "node": ">= 0.8"
290 | }
291 | },
292 | "node_modules/destroy": {
293 | "version": "1.2.0",
294 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
295 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
296 | "engines": {
297 | "node": ">= 0.8",
298 | "npm": "1.2.8000 || >= 1.4.16"
299 | }
300 | },
301 | "node_modules/dotenv": {
302 | "version": "16.4.5",
303 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
304 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
305 | "engines": {
306 | "node": ">=12"
307 | },
308 | "funding": {
309 | "url": "https://dotenvx.com"
310 | }
311 | },
312 | "node_modules/ee-first": {
313 | "version": "1.1.1",
314 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
315 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
316 | },
317 | "node_modules/encodeurl": {
318 | "version": "1.0.2",
319 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
320 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
321 | "engines": {
322 | "node": ">= 0.8"
323 | }
324 | },
325 | "node_modules/engine.io": {
326 | "version": "6.5.4",
327 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz",
328 | "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==",
329 | "dependencies": {
330 | "@types/cookie": "^0.4.1",
331 | "@types/cors": "^2.8.12",
332 | "@types/node": ">=10.0.0",
333 | "accepts": "~1.3.4",
334 | "base64id": "2.0.0",
335 | "cookie": "~0.4.1",
336 | "cors": "~2.8.5",
337 | "debug": "~4.3.1",
338 | "engine.io-parser": "~5.2.1",
339 | "ws": "~8.11.0"
340 | },
341 | "engines": {
342 | "node": ">=10.2.0"
343 | }
344 | },
345 | "node_modules/engine.io-parser": {
346 | "version": "5.2.2",
347 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
348 | "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
349 | "engines": {
350 | "node": ">=10.0.0"
351 | }
352 | },
353 | "node_modules/engine.io/node_modules/cookie": {
354 | "version": "0.4.2",
355 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
356 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
357 | "engines": {
358 | "node": ">= 0.6"
359 | }
360 | },
361 | "node_modules/engine.io/node_modules/debug": {
362 | "version": "4.3.4",
363 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
364 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
365 | "dependencies": {
366 | "ms": "2.1.2"
367 | },
368 | "engines": {
369 | "node": ">=6.0"
370 | },
371 | "peerDependenciesMeta": {
372 | "supports-color": {
373 | "optional": true
374 | }
375 | }
376 | },
377 | "node_modules/engine.io/node_modules/ms": {
378 | "version": "2.1.2",
379 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
380 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
381 | },
382 | "node_modules/es-define-property": {
383 | "version": "1.0.0",
384 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
385 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
386 | "dependencies": {
387 | "get-intrinsic": "^1.2.4"
388 | },
389 | "engines": {
390 | "node": ">= 0.4"
391 | }
392 | },
393 | "node_modules/es-errors": {
394 | "version": "1.3.0",
395 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
396 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
397 | "engines": {
398 | "node": ">= 0.4"
399 | }
400 | },
401 | "node_modules/escape-html": {
402 | "version": "1.0.3",
403 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
404 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
405 | },
406 | "node_modules/etag": {
407 | "version": "1.8.1",
408 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
409 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
410 | "engines": {
411 | "node": ">= 0.6"
412 | }
413 | },
414 | "node_modules/express": {
415 | "version": "4.18.3",
416 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz",
417 | "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==",
418 | "dependencies": {
419 | "accepts": "~1.3.8",
420 | "array-flatten": "1.1.1",
421 | "body-parser": "1.20.2",
422 | "content-disposition": "0.5.4",
423 | "content-type": "~1.0.4",
424 | "cookie": "0.5.0",
425 | "cookie-signature": "1.0.6",
426 | "debug": "2.6.9",
427 | "depd": "2.0.0",
428 | "encodeurl": "~1.0.2",
429 | "escape-html": "~1.0.3",
430 | "etag": "~1.8.1",
431 | "finalhandler": "1.2.0",
432 | "fresh": "0.5.2",
433 | "http-errors": "2.0.0",
434 | "merge-descriptors": "1.0.1",
435 | "methods": "~1.1.2",
436 | "on-finished": "2.4.1",
437 | "parseurl": "~1.3.3",
438 | "path-to-regexp": "0.1.7",
439 | "proxy-addr": "~2.0.7",
440 | "qs": "6.11.0",
441 | "range-parser": "~1.2.1",
442 | "safe-buffer": "5.2.1",
443 | "send": "0.18.0",
444 | "serve-static": "1.15.0",
445 | "setprototypeof": "1.2.0",
446 | "statuses": "2.0.1",
447 | "type-is": "~1.6.18",
448 | "utils-merge": "1.0.1",
449 | "vary": "~1.1.2"
450 | },
451 | "engines": {
452 | "node": ">= 0.10.0"
453 | }
454 | },
455 | "node_modules/fill-range": {
456 | "version": "7.0.1",
457 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
458 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
459 | "dev": true,
460 | "dependencies": {
461 | "to-regex-range": "^5.0.1"
462 | },
463 | "engines": {
464 | "node": ">=8"
465 | }
466 | },
467 | "node_modules/finalhandler": {
468 | "version": "1.2.0",
469 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
470 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
471 | "dependencies": {
472 | "debug": "2.6.9",
473 | "encodeurl": "~1.0.2",
474 | "escape-html": "~1.0.3",
475 | "on-finished": "2.4.1",
476 | "parseurl": "~1.3.3",
477 | "statuses": "2.0.1",
478 | "unpipe": "~1.0.0"
479 | },
480 | "engines": {
481 | "node": ">= 0.8"
482 | }
483 | },
484 | "node_modules/forwarded": {
485 | "version": "0.2.0",
486 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
487 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
488 | "engines": {
489 | "node": ">= 0.6"
490 | }
491 | },
492 | "node_modules/fresh": {
493 | "version": "0.5.2",
494 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
495 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
496 | "engines": {
497 | "node": ">= 0.6"
498 | }
499 | },
500 | "node_modules/fsevents": {
501 | "version": "2.3.3",
502 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
503 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
504 | "dev": true,
505 | "hasInstallScript": true,
506 | "optional": true,
507 | "os": [
508 | "darwin"
509 | ],
510 | "engines": {
511 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
512 | }
513 | },
514 | "node_modules/function-bind": {
515 | "version": "1.1.2",
516 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
517 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
518 | "funding": {
519 | "url": "https://github.com/sponsors/ljharb"
520 | }
521 | },
522 | "node_modules/get-intrinsic": {
523 | "version": "1.2.4",
524 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
525 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
526 | "dependencies": {
527 | "es-errors": "^1.3.0",
528 | "function-bind": "^1.1.2",
529 | "has-proto": "^1.0.1",
530 | "has-symbols": "^1.0.3",
531 | "hasown": "^2.0.0"
532 | },
533 | "engines": {
534 | "node": ">= 0.4"
535 | },
536 | "funding": {
537 | "url": "https://github.com/sponsors/ljharb"
538 | }
539 | },
540 | "node_modules/glob-parent": {
541 | "version": "5.1.2",
542 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
543 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
544 | "dev": true,
545 | "dependencies": {
546 | "is-glob": "^4.0.1"
547 | },
548 | "engines": {
549 | "node": ">= 6"
550 | }
551 | },
552 | "node_modules/gopd": {
553 | "version": "1.0.1",
554 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
555 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
556 | "dependencies": {
557 | "get-intrinsic": "^1.1.3"
558 | },
559 | "funding": {
560 | "url": "https://github.com/sponsors/ljharb"
561 | }
562 | },
563 | "node_modules/has-flag": {
564 | "version": "3.0.0",
565 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
566 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
567 | "dev": true,
568 | "engines": {
569 | "node": ">=4"
570 | }
571 | },
572 | "node_modules/has-property-descriptors": {
573 | "version": "1.0.2",
574 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
575 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
576 | "dependencies": {
577 | "es-define-property": "^1.0.0"
578 | },
579 | "funding": {
580 | "url": "https://github.com/sponsors/ljharb"
581 | }
582 | },
583 | "node_modules/has-proto": {
584 | "version": "1.0.3",
585 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
586 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
587 | "engines": {
588 | "node": ">= 0.4"
589 | },
590 | "funding": {
591 | "url": "https://github.com/sponsors/ljharb"
592 | }
593 | },
594 | "node_modules/has-symbols": {
595 | "version": "1.0.3",
596 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
597 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
598 | "engines": {
599 | "node": ">= 0.4"
600 | },
601 | "funding": {
602 | "url": "https://github.com/sponsors/ljharb"
603 | }
604 | },
605 | "node_modules/hasown": {
606 | "version": "2.0.1",
607 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
608 | "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
609 | "dependencies": {
610 | "function-bind": "^1.1.2"
611 | },
612 | "engines": {
613 | "node": ">= 0.4"
614 | }
615 | },
616 | "node_modules/http-errors": {
617 | "version": "2.0.0",
618 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
619 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
620 | "dependencies": {
621 | "depd": "2.0.0",
622 | "inherits": "2.0.4",
623 | "setprototypeof": "1.2.0",
624 | "statuses": "2.0.1",
625 | "toidentifier": "1.0.1"
626 | },
627 | "engines": {
628 | "node": ">= 0.8"
629 | }
630 | },
631 | "node_modules/iconv-lite": {
632 | "version": "0.4.24",
633 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
634 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
635 | "dependencies": {
636 | "safer-buffer": ">= 2.1.2 < 3"
637 | },
638 | "engines": {
639 | "node": ">=0.10.0"
640 | }
641 | },
642 | "node_modules/ignore-by-default": {
643 | "version": "1.0.1",
644 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
645 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
646 | "dev": true
647 | },
648 | "node_modules/inherits": {
649 | "version": "2.0.4",
650 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
651 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
652 | },
653 | "node_modules/ipaddr.js": {
654 | "version": "1.9.1",
655 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
656 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
657 | "engines": {
658 | "node": ">= 0.10"
659 | }
660 | },
661 | "node_modules/is-binary-path": {
662 | "version": "2.1.0",
663 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
664 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
665 | "dev": true,
666 | "dependencies": {
667 | "binary-extensions": "^2.0.0"
668 | },
669 | "engines": {
670 | "node": ">=8"
671 | }
672 | },
673 | "node_modules/is-extglob": {
674 | "version": "2.1.1",
675 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
676 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
677 | "dev": true,
678 | "engines": {
679 | "node": ">=0.10.0"
680 | }
681 | },
682 | "node_modules/is-glob": {
683 | "version": "4.0.3",
684 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
685 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
686 | "dev": true,
687 | "dependencies": {
688 | "is-extglob": "^2.1.1"
689 | },
690 | "engines": {
691 | "node": ">=0.10.0"
692 | }
693 | },
694 | "node_modules/is-number": {
695 | "version": "7.0.0",
696 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
697 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
698 | "dev": true,
699 | "engines": {
700 | "node": ">=0.12.0"
701 | }
702 | },
703 | "node_modules/lru-cache": {
704 | "version": "6.0.0",
705 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
706 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
707 | "dev": true,
708 | "dependencies": {
709 | "yallist": "^4.0.0"
710 | },
711 | "engines": {
712 | "node": ">=10"
713 | }
714 | },
715 | "node_modules/media-typer": {
716 | "version": "0.3.0",
717 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
718 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
719 | "engines": {
720 | "node": ">= 0.6"
721 | }
722 | },
723 | "node_modules/merge-descriptors": {
724 | "version": "1.0.1",
725 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
726 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
727 | },
728 | "node_modules/methods": {
729 | "version": "1.1.2",
730 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
731 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
732 | "engines": {
733 | "node": ">= 0.6"
734 | }
735 | },
736 | "node_modules/mime": {
737 | "version": "1.6.0",
738 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
739 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
740 | "bin": {
741 | "mime": "cli.js"
742 | },
743 | "engines": {
744 | "node": ">=4"
745 | }
746 | },
747 | "node_modules/mime-db": {
748 | "version": "1.52.0",
749 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
750 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
751 | "engines": {
752 | "node": ">= 0.6"
753 | }
754 | },
755 | "node_modules/mime-types": {
756 | "version": "2.1.35",
757 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
758 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
759 | "dependencies": {
760 | "mime-db": "1.52.0"
761 | },
762 | "engines": {
763 | "node": ">= 0.6"
764 | }
765 | },
766 | "node_modules/minimatch": {
767 | "version": "3.1.2",
768 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
769 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
770 | "dev": true,
771 | "dependencies": {
772 | "brace-expansion": "^1.1.7"
773 | },
774 | "engines": {
775 | "node": "*"
776 | }
777 | },
778 | "node_modules/ms": {
779 | "version": "2.0.0",
780 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
781 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
782 | },
783 | "node_modules/negotiator": {
784 | "version": "0.6.3",
785 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
786 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
787 | "engines": {
788 | "node": ">= 0.6"
789 | }
790 | },
791 | "node_modules/nodemon": {
792 | "version": "3.1.0",
793 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz",
794 | "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==",
795 | "dev": true,
796 | "dependencies": {
797 | "chokidar": "^3.5.2",
798 | "debug": "^4",
799 | "ignore-by-default": "^1.0.1",
800 | "minimatch": "^3.1.2",
801 | "pstree.remy": "^1.1.8",
802 | "semver": "^7.5.3",
803 | "simple-update-notifier": "^2.0.0",
804 | "supports-color": "^5.5.0",
805 | "touch": "^3.1.0",
806 | "undefsafe": "^2.0.5"
807 | },
808 | "bin": {
809 | "nodemon": "bin/nodemon.js"
810 | },
811 | "engines": {
812 | "node": ">=10"
813 | },
814 | "funding": {
815 | "type": "opencollective",
816 | "url": "https://opencollective.com/nodemon"
817 | }
818 | },
819 | "node_modules/nodemon/node_modules/debug": {
820 | "version": "4.3.4",
821 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
822 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
823 | "dev": true,
824 | "dependencies": {
825 | "ms": "2.1.2"
826 | },
827 | "engines": {
828 | "node": ">=6.0"
829 | },
830 | "peerDependenciesMeta": {
831 | "supports-color": {
832 | "optional": true
833 | }
834 | }
835 | },
836 | "node_modules/nodemon/node_modules/ms": {
837 | "version": "2.1.2",
838 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
839 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
840 | "dev": true
841 | },
842 | "node_modules/nopt": {
843 | "version": "1.0.10",
844 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
845 | "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
846 | "dev": true,
847 | "dependencies": {
848 | "abbrev": "1"
849 | },
850 | "bin": {
851 | "nopt": "bin/nopt.js"
852 | },
853 | "engines": {
854 | "node": "*"
855 | }
856 | },
857 | "node_modules/normalize-path": {
858 | "version": "3.0.0",
859 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
860 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
861 | "dev": true,
862 | "engines": {
863 | "node": ">=0.10.0"
864 | }
865 | },
866 | "node_modules/notepack.io": {
867 | "version": "2.2.0",
868 | "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-2.2.0.tgz",
869 | "integrity": "sha512-9b5w3t5VSH6ZPosoYnyDONnUTF8o0UkBw7JLA6eBlYJWyGT1Q3vQa8Hmuj1/X6RYvHjjygBDgw6fJhe0JEojfw=="
870 | },
871 | "node_modules/object-assign": {
872 | "version": "4.1.1",
873 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
874 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
875 | "engines": {
876 | "node": ">=0.10.0"
877 | }
878 | },
879 | "node_modules/object-inspect": {
880 | "version": "1.13.1",
881 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
882 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
883 | "funding": {
884 | "url": "https://github.com/sponsors/ljharb"
885 | }
886 | },
887 | "node_modules/on-finished": {
888 | "version": "2.4.1",
889 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
890 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
891 | "dependencies": {
892 | "ee-first": "1.1.1"
893 | },
894 | "engines": {
895 | "node": ">= 0.8"
896 | }
897 | },
898 | "node_modules/parseurl": {
899 | "version": "1.3.3",
900 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
901 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
902 | "engines": {
903 | "node": ">= 0.8"
904 | }
905 | },
906 | "node_modules/path-to-regexp": {
907 | "version": "0.1.7",
908 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
909 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
910 | },
911 | "node_modules/picomatch": {
912 | "version": "2.3.1",
913 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
914 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
915 | "dev": true,
916 | "engines": {
917 | "node": ">=8.6"
918 | },
919 | "funding": {
920 | "url": "https://github.com/sponsors/jonschlinkert"
921 | }
922 | },
923 | "node_modules/proxy-addr": {
924 | "version": "2.0.7",
925 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
926 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
927 | "dependencies": {
928 | "forwarded": "0.2.0",
929 | "ipaddr.js": "1.9.1"
930 | },
931 | "engines": {
932 | "node": ">= 0.10"
933 | }
934 | },
935 | "node_modules/pstree.remy": {
936 | "version": "1.1.8",
937 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
938 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
939 | "dev": true
940 | },
941 | "node_modules/qs": {
942 | "version": "6.11.0",
943 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
944 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
945 | "dependencies": {
946 | "side-channel": "^1.0.4"
947 | },
948 | "engines": {
949 | "node": ">=0.6"
950 | },
951 | "funding": {
952 | "url": "https://github.com/sponsors/ljharb"
953 | }
954 | },
955 | "node_modules/range-parser": {
956 | "version": "1.2.1",
957 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
958 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
959 | "engines": {
960 | "node": ">= 0.6"
961 | }
962 | },
963 | "node_modules/raw-body": {
964 | "version": "2.5.2",
965 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
966 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
967 | "dependencies": {
968 | "bytes": "3.1.2",
969 | "http-errors": "2.0.0",
970 | "iconv-lite": "0.4.24",
971 | "unpipe": "1.0.0"
972 | },
973 | "engines": {
974 | "node": ">= 0.8"
975 | }
976 | },
977 | "node_modules/readdirp": {
978 | "version": "3.6.0",
979 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
980 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
981 | "dev": true,
982 | "dependencies": {
983 | "picomatch": "^2.2.1"
984 | },
985 | "engines": {
986 | "node": ">=8.10.0"
987 | }
988 | },
989 | "node_modules/safe-buffer": {
990 | "version": "5.2.1",
991 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
992 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
993 | "funding": [
994 | {
995 | "type": "github",
996 | "url": "https://github.com/sponsors/feross"
997 | },
998 | {
999 | "type": "patreon",
1000 | "url": "https://www.patreon.com/feross"
1001 | },
1002 | {
1003 | "type": "consulting",
1004 | "url": "https://feross.org/support"
1005 | }
1006 | ]
1007 | },
1008 | "node_modules/safer-buffer": {
1009 | "version": "2.1.2",
1010 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1011 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1012 | },
1013 | "node_modules/semver": {
1014 | "version": "7.6.0",
1015 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
1016 | "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
1017 | "dev": true,
1018 | "dependencies": {
1019 | "lru-cache": "^6.0.0"
1020 | },
1021 | "bin": {
1022 | "semver": "bin/semver.js"
1023 | },
1024 | "engines": {
1025 | "node": ">=10"
1026 | }
1027 | },
1028 | "node_modules/send": {
1029 | "version": "0.18.0",
1030 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
1031 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
1032 | "dependencies": {
1033 | "debug": "2.6.9",
1034 | "depd": "2.0.0",
1035 | "destroy": "1.2.0",
1036 | "encodeurl": "~1.0.2",
1037 | "escape-html": "~1.0.3",
1038 | "etag": "~1.8.1",
1039 | "fresh": "0.5.2",
1040 | "http-errors": "2.0.0",
1041 | "mime": "1.6.0",
1042 | "ms": "2.1.3",
1043 | "on-finished": "2.4.1",
1044 | "range-parser": "~1.2.1",
1045 | "statuses": "2.0.1"
1046 | },
1047 | "engines": {
1048 | "node": ">= 0.8.0"
1049 | }
1050 | },
1051 | "node_modules/send/node_modules/ms": {
1052 | "version": "2.1.3",
1053 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1054 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1055 | },
1056 | "node_modules/serve-static": {
1057 | "version": "1.15.0",
1058 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
1059 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
1060 | "dependencies": {
1061 | "encodeurl": "~1.0.2",
1062 | "escape-html": "~1.0.3",
1063 | "parseurl": "~1.3.3",
1064 | "send": "0.18.0"
1065 | },
1066 | "engines": {
1067 | "node": ">= 0.8.0"
1068 | }
1069 | },
1070 | "node_modules/set-function-length": {
1071 | "version": "1.2.1",
1072 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
1073 | "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
1074 | "dependencies": {
1075 | "define-data-property": "^1.1.2",
1076 | "es-errors": "^1.3.0",
1077 | "function-bind": "^1.1.2",
1078 | "get-intrinsic": "^1.2.3",
1079 | "gopd": "^1.0.1",
1080 | "has-property-descriptors": "^1.0.1"
1081 | },
1082 | "engines": {
1083 | "node": ">= 0.4"
1084 | }
1085 | },
1086 | "node_modules/setprototypeof": {
1087 | "version": "1.2.0",
1088 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1089 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1090 | },
1091 | "node_modules/side-channel": {
1092 | "version": "1.0.6",
1093 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
1094 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
1095 | "dependencies": {
1096 | "call-bind": "^1.0.7",
1097 | "es-errors": "^1.3.0",
1098 | "get-intrinsic": "^1.2.4",
1099 | "object-inspect": "^1.13.1"
1100 | },
1101 | "engines": {
1102 | "node": ">= 0.4"
1103 | },
1104 | "funding": {
1105 | "url": "https://github.com/sponsors/ljharb"
1106 | }
1107 | },
1108 | "node_modules/simple-update-notifier": {
1109 | "version": "2.0.0",
1110 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1111 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1112 | "dev": true,
1113 | "dependencies": {
1114 | "semver": "^7.5.3"
1115 | },
1116 | "engines": {
1117 | "node": ">=10"
1118 | }
1119 | },
1120 | "node_modules/socket.io": {
1121 | "version": "4.7.4",
1122 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.4.tgz",
1123 | "integrity": "sha512-DcotgfP1Zg9iP/dH9zvAQcWrE0TtbMVwXmlV4T4mqsvY+gw+LqUGPfx2AoVyRk0FLME+GQhufDMyacFmw7ksqw==",
1124 | "dependencies": {
1125 | "accepts": "~1.3.4",
1126 | "base64id": "~2.0.0",
1127 | "cors": "~2.8.5",
1128 | "debug": "~4.3.2",
1129 | "engine.io": "~6.5.2",
1130 | "socket.io-adapter": "~2.5.2",
1131 | "socket.io-parser": "~4.2.4"
1132 | },
1133 | "engines": {
1134 | "node": ">=10.2.0"
1135 | }
1136 | },
1137 | "node_modules/socket.io-adapter": {
1138 | "version": "2.5.4",
1139 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz",
1140 | "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==",
1141 | "dependencies": {
1142 | "debug": "~4.3.4",
1143 | "ws": "~8.11.0"
1144 | }
1145 | },
1146 | "node_modules/socket.io-adapter/node_modules/debug": {
1147 | "version": "4.3.4",
1148 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
1149 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
1150 | "dependencies": {
1151 | "ms": "2.1.2"
1152 | },
1153 | "engines": {
1154 | "node": ">=6.0"
1155 | },
1156 | "peerDependenciesMeta": {
1157 | "supports-color": {
1158 | "optional": true
1159 | }
1160 | }
1161 | },
1162 | "node_modules/socket.io-adapter/node_modules/ms": {
1163 | "version": "2.1.2",
1164 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1165 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1166 | },
1167 | "node_modules/socket.io-msgpack-parser": {
1168 | "version": "3.0.2",
1169 | "resolved": "https://registry.npmjs.org/socket.io-msgpack-parser/-/socket.io-msgpack-parser-3.0.2.tgz",
1170 | "integrity": "sha512-1e76bJ1PCKi9H+JiYk+S29PBJvknHjQWM7Mtj0hjF2KxDA6b6rQxv3rTsnwBoz/haZOhlCDIMQvPATbqYeuMxg==",
1171 | "dependencies": {
1172 | "component-emitter": "~1.3.0",
1173 | "notepack.io": "~2.2.0"
1174 | }
1175 | },
1176 | "node_modules/socket.io-parser": {
1177 | "version": "4.2.4",
1178 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
1179 | "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
1180 | "dependencies": {
1181 | "@socket.io/component-emitter": "~3.1.0",
1182 | "debug": "~4.3.1"
1183 | },
1184 | "engines": {
1185 | "node": ">=10.0.0"
1186 | }
1187 | },
1188 | "node_modules/socket.io-parser/node_modules/debug": {
1189 | "version": "4.3.4",
1190 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
1191 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
1192 | "dependencies": {
1193 | "ms": "2.1.2"
1194 | },
1195 | "engines": {
1196 | "node": ">=6.0"
1197 | },
1198 | "peerDependenciesMeta": {
1199 | "supports-color": {
1200 | "optional": true
1201 | }
1202 | }
1203 | },
1204 | "node_modules/socket.io-parser/node_modules/ms": {
1205 | "version": "2.1.2",
1206 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1207 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1208 | },
1209 | "node_modules/socket.io/node_modules/debug": {
1210 | "version": "4.3.4",
1211 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
1212 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
1213 | "dependencies": {
1214 | "ms": "2.1.2"
1215 | },
1216 | "engines": {
1217 | "node": ">=6.0"
1218 | },
1219 | "peerDependenciesMeta": {
1220 | "supports-color": {
1221 | "optional": true
1222 | }
1223 | }
1224 | },
1225 | "node_modules/socket.io/node_modules/ms": {
1226 | "version": "2.1.2",
1227 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1228 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
1229 | },
1230 | "node_modules/statuses": {
1231 | "version": "2.0.1",
1232 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1233 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1234 | "engines": {
1235 | "node": ">= 0.8"
1236 | }
1237 | },
1238 | "node_modules/supports-color": {
1239 | "version": "5.5.0",
1240 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1241 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1242 | "dev": true,
1243 | "dependencies": {
1244 | "has-flag": "^3.0.0"
1245 | },
1246 | "engines": {
1247 | "node": ">=4"
1248 | }
1249 | },
1250 | "node_modules/to-regex-range": {
1251 | "version": "5.0.1",
1252 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1253 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1254 | "dev": true,
1255 | "dependencies": {
1256 | "is-number": "^7.0.0"
1257 | },
1258 | "engines": {
1259 | "node": ">=8.0"
1260 | }
1261 | },
1262 | "node_modules/toidentifier": {
1263 | "version": "1.0.1",
1264 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1265 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1266 | "engines": {
1267 | "node": ">=0.6"
1268 | }
1269 | },
1270 | "node_modules/touch": {
1271 | "version": "3.1.0",
1272 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
1273 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
1274 | "dev": true,
1275 | "dependencies": {
1276 | "nopt": "~1.0.10"
1277 | },
1278 | "bin": {
1279 | "nodetouch": "bin/nodetouch.js"
1280 | }
1281 | },
1282 | "node_modules/type-is": {
1283 | "version": "1.6.18",
1284 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1285 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1286 | "dependencies": {
1287 | "media-typer": "0.3.0",
1288 | "mime-types": "~2.1.24"
1289 | },
1290 | "engines": {
1291 | "node": ">= 0.6"
1292 | }
1293 | },
1294 | "node_modules/undefsafe": {
1295 | "version": "2.0.5",
1296 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1297 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1298 | "dev": true
1299 | },
1300 | "node_modules/undici-types": {
1301 | "version": "5.26.5",
1302 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
1303 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
1304 | },
1305 | "node_modules/unpipe": {
1306 | "version": "1.0.0",
1307 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1308 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1309 | "engines": {
1310 | "node": ">= 0.8"
1311 | }
1312 | },
1313 | "node_modules/utils-merge": {
1314 | "version": "1.0.1",
1315 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1316 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1317 | "engines": {
1318 | "node": ">= 0.4.0"
1319 | }
1320 | },
1321 | "node_modules/vary": {
1322 | "version": "1.1.2",
1323 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1324 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1325 | "engines": {
1326 | "node": ">= 0.8"
1327 | }
1328 | },
1329 | "node_modules/ws": {
1330 | "version": "8.11.0",
1331 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
1332 | "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
1333 | "engines": {
1334 | "node": ">=10.0.0"
1335 | },
1336 | "peerDependencies": {
1337 | "bufferutil": "^4.0.1",
1338 | "utf-8-validate": "^5.0.2"
1339 | },
1340 | "peerDependenciesMeta": {
1341 | "bufferutil": {
1342 | "optional": true
1343 | },
1344 | "utf-8-validate": {
1345 | "optional": true
1346 | }
1347 | }
1348 | },
1349 | "node_modules/yallist": {
1350 | "version": "4.0.0",
1351 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
1352 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
1353 | "dev": true
1354 | }
1355 | }
1356 | }
1357 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js",
8 | "dev": "nodemon index.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "cors": "^2.8.5",
15 | "dotenv": "^16.4.5",
16 | "express": "^4.18.3",
17 | "socket.io": "^4.7.4",
18 | "socket.io-msgpack-parser": "^3.0.2"
19 | },
20 | "devDependencies": {
21 | "nodemon": "^3.1.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------