├── src
├── frontend
│ ├── public
│ │ └── robots.txt
│ ├── .prettierrc.json
│ ├── .gitignore
│ ├── src
│ │ ├── config
│ │ │ ├── default.json
│ │ │ └── sample-exercise-data.js
│ │ ├── form-store.js
│ │ ├── main.jsx
│ │ ├── components
│ │ │ ├── Link.jsx
│ │ │ ├── MultiUserSelect.jsx
│ │ │ └── FormBuilder.jsx
│ │ ├── index.css
│ │ ├── store
│ │ │ └── global.js
│ │ ├── App.css
│ │ ├── favicon.svg
│ │ ├── pages
│ │ │ ├── Assignments.jsx
│ │ │ ├── Home.jsx
│ │ │ ├── AssignmentNew.jsx
│ │ │ └── Assignment.jsx
│ │ ├── logo.svg
│ │ └── App.jsx
│ ├── vite.config.js
│ ├── index.html
│ ├── .eslintrc.json
│ ├── package.json
│ └── rough
│ │ └── old-assignment.jsx
└── backend
│ ├── config
│ └── default.json
│ ├── app
│ ├── user
│ │ ├── controller.js
│ │ └── routes.js
│ ├── health-check
│ │ └── routes.js
│ ├── form-schema
│ │ └── controller.js
│ ├── annotation-exercise
│ │ ├── controller.test.js
│ │ ├── controller.js
│ │ └── routes.js
│ └── post
│ │ ├── routes.js
│ │ └── controller.js
│ ├── model
│ ├── fields
│ │ ├── index.js
│ │ ├── date.js
│ │ ├── number.js
│ │ ├── text.js
│ │ ├── multiselect.js
│ │ └── singleselect.js
│ ├── dummy.js
│ ├── seed-database.js
│ ├── form-data.js
│ ├── post.js
│ ├── form-schema.js
│ ├── user.js
│ └── annotation-exercise.js
│ ├── index.js
│ ├── package.json
│ ├── core
│ ├── socketio.js
│ ├── express.js
│ └── redis.js
│ └── mocktest.js
├── .gitignore
├── package.json
├── doc
├── redis-header.png
├── architecture
│ └── setup.md
├── form-schema.md
└── redis.md
├── docker-compose.yml
├── README.md
└── CONTRIBUTING.md
/src/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/frontend/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/src/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .env
3 | .favorites.json
4 |
5 | # Linters
6 | .eslintcache
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "zustand": "^3.5.1"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/backend/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "express": {
3 | "port": 8000
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/frontend/src/config/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "api_url": "http://localhost:8000"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/redis-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redis-developer/collaborative-media-annotator/master/doc/redis-header.png
--------------------------------------------------------------------------------
/src/backend/app/user/controller.js:
--------------------------------------------------------------------------------
1 | const user = require("../../model/user");
2 |
3 | async function create() {
4 | return await user.InstanceFactory();
5 | }
6 |
7 | module.exports = { create };
8 |
--------------------------------------------------------------------------------
/src/frontend/src/form-store.js:
--------------------------------------------------------------------------------
1 | import create from "zustand";
2 |
3 | // const useStore = create(set => ({
4 | // form : {},
5 | // setValue : (key, value) => set(state=>({...state.form, state.form.key: value}))
6 | // }))
7 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | redis:
5 | image: "redislabs/redismod"
6 | ports:
7 | - "6379:6379"
8 | volumes:
9 | - "~/data/annotation/redis:/data"
10 |
--------------------------------------------------------------------------------
/src/frontend/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import "./index.css"
4 | import App from "./App"
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById("root")
11 | )
12 |
--------------------------------------------------------------------------------
/doc/architecture/setup.md:
--------------------------------------------------------------------------------
1 | # Formatting and Linting
2 |
3 | Uses prettier for code formatting and eslint for flagging syntax and code style convetions
4 | the configurations are defined in .prettierrc.json and .eslintrc.json
5 | We use VS Code extensions to ensure these are run during development
6 |
--------------------------------------------------------------------------------
/src/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import reactRefresh from "@vitejs/plugin-react-refresh";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [reactRefresh()],
7 | // build: {
8 | // outDir: "../backend/ui",
9 | // },
10 | });
11 |
--------------------------------------------------------------------------------
/src/backend/app/health-check/routes.js:
--------------------------------------------------------------------------------
1 | const { StatusCodes } = require("http-status-codes");
2 |
3 | const configure = (expressApp) => {
4 | expressApp.get("/health-check", (req, res) => {
5 | res.status(StatusCodes.OK).send("alive");
6 | });
7 |
8 | return expressApp;
9 | };
10 |
11 | module.exports = { configure };
12 |
--------------------------------------------------------------------------------
/src/backend/model/fields/index.js:
--------------------------------------------------------------------------------
1 | const number = require("./number");
2 | const text = require("./text");
3 | const date = require("./date");
4 | const singleselect = require("./singleselect");
5 | const multiselect = require("./multiselect");
6 |
7 | module.exports = {
8 | number,
9 | text,
10 | date,
11 | singleselect,
12 | multiselect,
13 | };
14 |
--------------------------------------------------------------------------------
/src/backend/app/form-schema/controller.js:
--------------------------------------------------------------------------------
1 | const Redis = require("ioredis");
2 | const { RedisUtils } = require("../../core/redis");
3 |
4 | async function create(schemaJson, key) {
5 | // create a schema instance with schemaJson
6 | // validated it
7 | // flatten it
8 | // save it
9 | return RedisUtils.saveJSON(schemaJson, key);
10 | }
11 |
12 | module.exports = { create };
13 |
--------------------------------------------------------------------------------
/src/frontend/src/components/Link.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import styled from "styled-components";
3 |
4 | const SimpleLink = styled(Link)`
5 | text-decoration: none;
6 | &:focus,
7 | &:hover,
8 | &:visited,
9 | &:link,
10 | &:active {
11 | text-decoration: none;
12 | color: inherit;
13 | }
14 | `;
15 |
16 | export { SimpleLink as Link };
17 |
--------------------------------------------------------------------------------
/src/backend/model/dummy.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 |
3 | const Schema = Joi.object({});
4 |
5 | function InstanceFactory({} = {}) {
6 | const obj = {};
7 | try {
8 | const validatedObj = await Schema.validateAsync(obj);
9 | return validatedObj;
10 | } catch (err) {
11 | throw `Error : Could not create Dummy object. Please check schema. ${err.message}`;
12 | }
13 | }
14 |
15 | export { InstanceFactory };
16 |
--------------------------------------------------------------------------------
/src/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Collaborative Realtime Media Annotator
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/backend/model/seed-database.js:
--------------------------------------------------------------------------------
1 | const user = require("../model/user");
2 | const { redis } = require("../core/redis");
3 |
4 | const createUsers = async () => {
5 | users = [];
6 | for (i = 0; i < 10; i++) {
7 | let userInstance = await user.InstanceFactory();
8 | try {
9 | await user.save(userInstance);
10 | } catch (err) {
11 | console.log(`Error : could not create users. ${err}`);
12 | }
13 | users.push(userInstance);
14 | }
15 | console.log(users);
16 | };
17 |
18 | createUsers();
19 |
--------------------------------------------------------------------------------
/src/frontend/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:react/recommended"
9 | ],
10 | "parserOptions": {
11 | "ecmaFeatures": {
12 | "jsx": true
13 | },
14 | "ecmaVersion": 12,
15 | "sourceType": "module"
16 | },
17 | "plugins": [
18 | "react"
19 | ],
20 | "rules": {
21 | "react/display-name": [2]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Roadmap
2 |
3 | # Running Locally
4 |
5 | `docker-compose up`
6 | this will run redis, frontend and backend services
7 | frontend : http://localhost:3000
8 | backend : http://localhost:8000
9 |
10 | Seeding the database
11 | `docker exec -it backend node models/seed-database.js`
12 |
13 | # Developing Locally
14 |
15 | ```
16 | docker run redismodimage
17 | cd src/frontend && npm run dev
18 | cd src/backend && npm run dev
19 | ```
20 |
21 | # TODO
22 |
23 | UI optimizations and improvements
24 | Database based persistant storage
25 |
--------------------------------------------------------------------------------
/src/backend/app/annotation-exercise/controller.test.js:
--------------------------------------------------------------------------------
1 | // const controller = require("./controller");
2 | // await controller.create({ description: "Yeh hain event badhiya wala" });
3 | import {
4 | create,
5 | getAll,
6 | get,
7 | addParticipants,
8 | getParticipants,
9 | } from "./controller";
10 |
11 | test("create a user in redis", async () => {
12 | const exercise = await create({
13 | name: "test name",
14 | description: "test description",
15 | });
16 | console.log(exercise);
17 | expect(exercise.name).toBe("test name");
18 | });
19 |
--------------------------------------------------------------------------------
/src/frontend/src/store/global.js:
--------------------------------------------------------------------------------
1 | import create from "zustand";
2 | import { persist } from "zustand/middleware";
3 |
4 | export const useStore = create(
5 | persist(
6 | (set, get) => ({
7 | users: [],
8 | currentUser: undefined,
9 | setUsers: (users) => set((state) => ({ users })),
10 | setCurrentUser: (user) => set((state) => ({ currentUser: user })),
11 | resetUser: () => set((state) => ({ users: [] })),
12 | }),
13 | {
14 | name: "chippi-storage",
15 | }
16 | )
17 | );
18 |
19 | export const ustStore = create();
20 |
--------------------------------------------------------------------------------
/doc/form-schema.md:
--------------------------------------------------------------------------------
1 | ```
2 | const instance = await schema.InstanceFactory({
3 | first_field: await text.InstanceFactory({ label: "Your Name" }),
4 |
5 | second_field: await number.InstanceFactory({
6 | label: "Your Age",
7 | parameters: { min: 18, max: 50 },
8 | }),
9 |
10 | third_field: await date.InstanceFactory({ label: "date of birth" }),
11 |
12 | fourth_field: await singleselect.InstanceFactory({
13 | label: "choose one ",
14 | parameters: { name: "options", options: ["A", "B", "C"] },
15 | }),
16 |
17 | fifth_field: await multiselect.InstanceFactory({
18 | label: "choose one ",
19 | parameters: { options: ["A", "B", "C"] },
20 | }),
21 |
22 | });
23 | ```
24 |
--------------------------------------------------------------------------------
/src/backend/app/post/routes.js:
--------------------------------------------------------------------------------
1 | const { StatusCodes } = require("http-status-codes");
2 | const { getAll: getAllPosts, get: getPost } = require("./controller");
3 |
4 | const configure = (expressApp) => {
5 | expressApp.get("/exercise/:exercise_id/post/:post_id", async (req, res) => {
6 | try {
7 | const { exercise_id, post_id } = req.params;
8 |
9 | const post = await getPost(exercise_id, post_id);
10 | res.status(StatusCodes.OK).send({ post });
11 | } catch (err) {
12 | console.log(`Error : could not process GET /exercises. ${err}`);
13 | res.status(StatusCodes.INTERNAL_SERVER_ERROR).send();
14 | }
15 | });
16 |
17 | return expressApp;
18 | };
19 |
20 | module.exports = { configure };
21 |
--------------------------------------------------------------------------------
/doc/redis.md:
--------------------------------------------------------------------------------
1 | # Keys Indiex
2 |
3 | App Data Model
4 |
5 | ```
6 | Exercise :
7 | Hash
8 |
9 | Users :
10 | user:*
11 |
12 | Posts :
13 | post::*
14 |
15 | Schema :
16 | Hash
17 | key : schema::field_a:label
18 | returns string or int stored there
19 | JSON
20 | key : schema::*
21 | once added this is hardly accessed or edited. If that changes in the future, we could store a stringified version
22 |
23 | Data :
24 | string
25 | key : data::
26 | value : string representation of value
27 |
28 |
29 | activity::*
30 |
31 |
32 | comment::*
33 | data:::*
34 |
35 | post:*
36 | exercise:*
37 |
38 | JSON
39 | exercise:* - store data dump basically
40 | schema:
41 | ```
42 |
--------------------------------------------------------------------------------
/src/backend/model/fields/date.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const { nanoid } = require("nanoid");
3 |
4 | const Schema = Joi.object({
5 | id: Joi.string().max(10).required(),
6 | label: Joi.string().min(5).max(25).required(),
7 | type: Joi.string().valid("date").required(),
8 | });
9 |
10 | /**
11 | * USAGE :
12 | * const instance = date.InstanceFactory({
13 | * label: "Enter Date",
14 | * });
15 | */
16 | async function InstanceFactory({ id = nanoid(), label, type } = {}) {
17 | const obj = { id, label, type: "date" };
18 | try {
19 | const validatedObj = await Schema.validateAsync(obj);
20 | return validatedObj;
21 | } catch (err) {
22 | throw `Error : Could not create Date object. Please check schema. ${err.message}`;
23 | }
24 | }
25 |
26 | module.exports = { InstanceFactory, Schema };
27 |
--------------------------------------------------------------------------------
/src/backend/index.js:
--------------------------------------------------------------------------------
1 | const { expressApp, io, configure, start } = require("./core/express");
2 | const {
3 | configure: configureHealthCheck,
4 | } = require("./app/health-check/routes");
5 | const { configure: configureUsers } = require("./app/user/routes");
6 | const {
7 | configure: configureExercise,
8 | } = require("./app/annotation-exercise/routes");
9 | const { configure: configurePost } = require("./app/post/routes");
10 | const { configure: configureWebSocket } = require("./core/socketio");
11 | const config = require("config");
12 |
13 | const PORT = config.get("express.port");
14 |
15 | configure(expressApp);
16 | configureWebSocket(io);
17 |
18 | configureHealthCheck(expressApp);
19 | configureUsers(expressApp);
20 | configureExercise(expressApp);
21 | configurePost(expressApp);
22 |
23 | start(PORT);
24 |
--------------------------------------------------------------------------------
/src/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
40 | button {
41 | font-size: calc(10px + 2vmin);
42 | }
43 |
--------------------------------------------------------------------------------
/src/backend/model/form-data.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const Fields = require("./fields");
3 |
4 | /**
5 | * This validates object with any number of keys and values
6 | * provided that the values are string
7 | *
8 | */
9 | const Schema = Joi.object().pattern(Joi.string(), Joi.string());
10 |
11 | /**
12 | * USAGE
13 | * const instance = await formData.InstanceFactory({
14 | date: "25/12/2020",
15 | time: "8 am",
16 | age: 12,
17 | name: "what?",
18 | options: "[a],[b],[c]",
19 | });
20 | */
21 | async function InstanceFactory(obj) {
22 | try {
23 | const validatedObj = await Schema.validateAsync(obj);
24 | return validatedObj;
25 | } catch (err) {
26 | throw `Error : Could not create Form Data object. Please check schema. ${err.message}`;
27 | }
28 | }
29 |
30 | module.exports = { InstanceFactory };
31 |
--------------------------------------------------------------------------------
/src/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "scripts": {
10 | "test": "jest --watch",
11 | "dev": "nodemon index.js"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "body-parser": "^1.19.0",
18 | "config": "^3.3.6",
19 | "cors": "^2.8.5",
20 | "express": "^4.17.1",
21 | "flat": "^5.0.2",
22 | "generate-password": "^1.6.0",
23 | "http-status-codes": "^2.1.4",
24 | "ioredis": "^4.27.2",
25 | "joi": "^17.4.0",
26 | "nanoid": "^3.1.23",
27 | "nodemon": "^2.0.7",
28 | "randomcolor": "^0.6.2",
29 | "socket.io": "^4.1.1",
30 | "unique-names-generator": "^4.5.0"
31 | },
32 | "devDependencies": {
33 | "jest": "^26.6.3"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/backend/model/post.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const { nanoid } = require("nanoid");
3 |
4 | const Schema = Joi.object({
5 | id: Joi.string().min(1).max(50),
6 | type: Joi.any().allow("text", "audio", "image", "video").required(),
7 | url: Joi.string().uri().required(),
8 | });
9 |
10 | /*
11 | * USAGE :
12 | * const post = InstanceFactory({
13 | * type: "image",
14 | * url: "https://www.google.com/search",
15 | * });
16 | */
17 | async function InstanceFactory({ id = `${nanoid()}`, type, url } = {}) {
18 | const obj = {
19 | id,
20 | type,
21 | url,
22 | };
23 |
24 | try {
25 | const validatedObj = await Schema.validateAsync(obj);
26 | return validatedObj;
27 | } catch (err) {
28 | throw `Error : Could not create Post object. Please check schema. ${err.message}`;
29 | }
30 | }
31 |
32 | module.exports = { InstanceFactory };
33 |
--------------------------------------------------------------------------------
/src/backend/model/fields/number.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const { nanoid } = require("nanoid");
3 |
4 | // todo ensure max is greater than min
5 | const Schema = Joi.object({
6 | id: Joi.string().max(10).required(),
7 | label: Joi.string().min(5).max(25).required(),
8 | type: Joi.string().valid("number").required(),
9 | parameters: Joi.object({
10 | min: Joi.number(),
11 | max: Joi.number(),
12 | }),
13 | }).options({ stripUnknown: true });
14 |
15 | async function InstanceFactory({ id = nanoid(), label, parameters = {} } = {}) {
16 | const obj = { id, label, type: "number", parameters };
17 | try {
18 | const validatedObj = await Schema.validateAsync(obj);
19 | return validatedObj;
20 | } catch (err) {
21 | throw `Error : Could not create Number object. Please check schema. ${err.message}`;
22 | }
23 | }
24 |
25 | module.exports = { InstanceFactory, Schema };
26 |
--------------------------------------------------------------------------------
/src/backend/app/user/routes.js:
--------------------------------------------------------------------------------
1 | const { StatusCodes } = require("http-status-codes");
2 | const user = require("../../model/user");
3 | const { redis } = require("../../core/redis");
4 |
5 | const configure = (expressApp) => {
6 | expressApp.get("/users", async (req, res) => {
7 | const userIds = await redis.keys("user:*");
8 |
9 | const pipeline = redis.pipeline();
10 | userIds.map((id) => {
11 | pipeline.hgetall(id);
12 | });
13 | try {
14 | const response = await pipeline.exec();
15 | const result = response.map((r, i) => {
16 | return { id: userIds[i], ...r[1] };
17 | });
18 | console.log(result);
19 | res.status(StatusCodes.OK).send(result);
20 | } catch (err) {
21 | console.log(err);
22 | res.status(StatusCodes.INTERNAL_SERVER_ERROR).send({
23 | error: err.message,
24 | });
25 | }
26 | });
27 |
28 | return expressApp;
29 | };
30 |
31 | module.exports = { configure };
32 |
--------------------------------------------------------------------------------
/src/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "serve": "vite preview"
8 | },
9 | "dependencies": {
10 | "axios": "^0.21.1",
11 | "grommet": "^2.17.2",
12 | "grommet-icons": "^4.5.0",
13 | "konva": "^7.2.5",
14 | "randomcolor": "^0.6.2",
15 | "react": "^17.0.0",
16 | "react-dom": "^17.0.0",
17 | "react-json-editor-ajrm": "^2.5.13",
18 | "react-konva": "^17.0.2-0",
19 | "react-router-dom": "^5.2.0",
20 | "socket.io": "^4.1.1",
21 | "socket.io-client": "^4.1.1",
22 | "styled-components": "^5.3.0",
23 | "use-image": "^1.0.7",
24 | "zustand": "^3.5.1",
25 | "zustand-persist": "^0.3.1"
26 | },
27 | "devDependencies": {
28 | "@vitejs/plugin-react-refresh": "^1.3.1",
29 | "eslint-plugin-react": "^7.23.2",
30 | "prettier": "^2.2.1",
31 | "vite": "^2.2.3"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/backend/model/fields/text.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const { nanoid } = require("nanoid");
3 |
4 | const Schema = Joi.object({
5 | id: Joi.string().max(10).required(),
6 | label: Joi.string().min(5).max(25).required(),
7 | type: Joi.string().valid("string").required(),
8 | parameters: Joi.object({
9 | length: Joi.number(),
10 | lines: Joi.number(),
11 | }),
12 | }).options({ stripUnknown: true });
13 |
14 | /*
15 | * USAGE :
16 | * const instance = InstanceFactory({
17 | * label: "Please enter a number",
18 | * parameters: { length: 250 },
19 | * });
20 | */
21 |
22 | async function InstanceFactory({ id = nanoid(), label, parameters = {} } = {}) {
23 | const obj = { id, label, type: "string", parameters };
24 | try {
25 | const validatedObj = await Schema.validateAsync(obj);
26 | return validatedObj;
27 | } catch (err) {
28 | throw `Error : Could not create text model. Please check schema. ${err.message}`;
29 | }
30 | }
31 |
32 | module.exports = { InstanceFactory, Schema };
33 |
--------------------------------------------------------------------------------
/src/backend/model/fields/multiselect.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const { nanoid } = require("nanoid");
3 |
4 | const Schema = Joi.object({
5 | id: Joi.string().max(10).required(),
6 | label: Joi.string().min(5).max(25).required(),
7 | type: Joi.string().valid("multiselect").required(),
8 | parameters: Joi.object({
9 | options: Joi.array().items(Joi.string()),
10 | }),
11 | });
12 |
13 | /**
14 | *
15 | * USAGE :
16 | * const instance = await multiselect.InstanceFactory({
17 | * label: "hello",
18 | * parameters: {
19 | * options: ["option a", "option b"],
20 | * },
21 | * });
22 | */
23 |
24 | async function InstanceFactory({ id = nanoid(), label, parameters } = {}) {
25 | const obj = { id, label, type: "multiselect", parameters };
26 | try {
27 | const validatedObj = await Schema.validateAsync(obj);
28 | return validatedObj;
29 | } catch (err) {
30 | throw `Error : Could not create Multiselect object. Please check schema. ${err.message}`;
31 | }
32 | }
33 |
34 | module.exports = { InstanceFactory, Schema };
35 |
--------------------------------------------------------------------------------
/src/backend/app/post/controller.js:
--------------------------------------------------------------------------------
1 | const { redis } = require("../../core/redis");
2 | const postModel = require("../../model/post");
3 |
4 | async function createManyForExercise(urls, exerciseId) {
5 | const posts = [];
6 | const pipeline = redis.pipeline();
7 |
8 | urls.map(async (url) => {
9 | let instance = await postModel.InstanceFactory({ type: "image", url });
10 | posts.push(instance.id);
11 |
12 | try {
13 | // const result = await pipeline.exec();
14 | redis.hset(`post:${exerciseId}:${instance.id}`, instance);
15 | // console.log(`Success : created posts in redis`);
16 | // console.log(result);
17 | } catch (err) {
18 | throw `Error : Could not save posts in redis ${err}`;
19 | }
20 | });
21 | return posts;
22 | }
23 |
24 | async function getAll() {}
25 |
26 | async function get(exerciseId, postId) {
27 | try {
28 | const post = await redis.hgetall(`post:${exerciseId}:${postId}`);
29 | return post;
30 | } catch (err) {
31 | throw "Coult not get post from redis";
32 | }
33 | }
34 |
35 | module.exports = { createManyForExercise, getAll, get };
36 |
--------------------------------------------------------------------------------
/src/backend/core/socketio.js:
--------------------------------------------------------------------------------
1 | const { redis } = require("./redis");
2 |
3 | function configure(io) {
4 | io.on("connection", (socket) => {
5 | console.log("a user connected");
6 | socket.on("disconnect", () => {
7 | console.log("user disconnected");
8 | });
9 |
10 | socket.on("data", (msg) => {
11 | console.log("message : ", msg);
12 | socket.broadcast.emit("data", msg);
13 | });
14 |
15 | socket.on("exit", (msg) => {
16 | console.log("user exit");
17 | });
18 |
19 | socket.on("join", async (msg) => {
20 | console.log("user joined ", msg);
21 | try {
22 | await redis.sadd(
23 | `participant-ol:${msg.exerciseId}:${msg.postId}`,
24 | msg.name
25 | );
26 | } catch (err) {
27 | console.log(`Error : could not add participant. ${err}`);
28 | }
29 |
30 | const onlineParticipants = await redis.smembers(
31 | `participant-ol:${msg.exerciseId}:${msg.postId}`
32 | );
33 | console.log({ onlineParticipants });
34 | socket.broadcast.emit("join", onlineParticipants);
35 | });
36 | });
37 | }
38 |
39 | module.exports = { configure };
40 |
--------------------------------------------------------------------------------
/src/backend/model/fields/singleselect.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const { nanoid } = require("nanoid");
3 |
4 | const Schema = Joi.object({
5 | id: Joi.string().max(10).required(),
6 | label: Joi.string().min(5).max(25).required(),
7 | type: Joi.string().valid("singleselect").required(),
8 | parameters: Joi.object({
9 | name: Joi.string().required(),
10 | options: Joi.array().items(Joi.string()).required(),
11 | }),
12 | }).options({ stripUnknown: true });
13 |
14 | /*
15 | * USAGE :
16 | * const instance = await singleselect.InstanceFactory({
17 | * label: "hello",
18 | * parameters: {
19 | * name: "option-name",
20 | * options: ["option a", "option b"],
21 | * },
22 | * });
23 | */
24 | async function InstanceFactory({ id = nanoid(), label, parameters = {} } = {}) {
25 | const obj = { id, label, type: "singleselect", parameters };
26 | try {
27 | const validatedObj = await Schema.validateAsync(obj);
28 | return validatedObj;
29 | } catch (err) {
30 | throw `Error : Could not create Singleselect model. Please check schema. ${err.message}`;
31 | }
32 | }
33 |
34 | module.exports = { InstanceFactory, Schema };
35 |
--------------------------------------------------------------------------------
/src/backend/core/express.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const expressApp = express();
3 | const bodyParser = require("body-parser");
4 | const cors = require("cors");
5 | const http = require("http");
6 |
7 | expressApp.use(
8 | cors({
9 | origin: "*",
10 | })
11 | );
12 |
13 | expressApp.use((req, res, next) => {
14 | res.header("Access-Control-Allow-Origin", "*");
15 | res.header(
16 | "Access-Control-Allow-Headers",
17 | "Origin, X-Requested-With, Content-Type, Accept"
18 | );
19 | next();
20 | });
21 | // expressApp.use(bodyParser.urlencoded());
22 | // expressApp.use(express.json);
23 | // expressApp.use(authenticationMiddleware);
24 |
25 | expressApp.use(bodyParser.json());
26 |
27 | const server = http.createServer(expressApp);
28 | const { Server } = require("socket.io");
29 | const io = require("socket.io")(server, {
30 | cors: {
31 | origin: "*",
32 | },
33 | });
34 |
35 | const configure = (expressApp) => {
36 | return expressApp;
37 | };
38 |
39 | const start = (port) => {
40 | return server.listen(port, () => {
41 | console.log(`Server Listening on ${port}`);
42 | });
43 | };
44 |
45 | module.exports = { expressApp, io, configure, start };
46 |
--------------------------------------------------------------------------------
/src/backend/model/form-schema.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const Fields = require("./fields");
3 |
4 | const Schema = Joi.object().pattern(
5 | Joi.string(),
6 | Joi.alternatives().try(
7 | Fields.number.Schema,
8 | Fields.text.Schema,
9 | Fields.date.Schema,
10 | Fields.singleselect.Schema,
11 | Fields.multiselect.Schema
12 | )
13 | );
14 |
15 | /**
16 | * USAGE :
17 | * const instance = await schema.InstanceFactory({
18 | * field_a: await text.InstanceFactory({ label: "Your Name" }),
19 | * field_b: await number.InstanceFactory({ label: "Your Age", parameters: { min: 18, max: 50 },}),
20 | * field_c: await date.InstanceFactory({ label: "date of birth" }),
21 | * field_d: await singleselect.InstanceFactory({ label: "choose one ",parameters: { name: "options", options: ["A", "B", "C"] },}),
22 | * field_e: await multiselect.InstanceFactory({ label: "choose one ", parameters: { options: ["A", "B", "C"] } }),
23 | * });
24 | */
25 | async function InstanceFactory(obj) {
26 | try {
27 | const validatedObj = await Schema.validateAsync(obj);
28 | return validatedObj;
29 | } catch (err) {
30 | throw `Error : Could not create Form Schema Model. Please check schema. ${err.message}`;
31 | }
32 | }
33 |
34 | module.exports = { InstanceFactory };
35 |
--------------------------------------------------------------------------------
/src/frontend/src/config/sample-exercise-data.js:
--------------------------------------------------------------------------------
1 | export const samplePostUrls = [
2 | "https://c.files.bbci.co.uk/957C/production/_111686283_pic1.png",
3 | "https://c.files.bbci.co.uk/957C/production/_111686283_pic1.png",
4 | "https://c.files.bbci.co.uk/957C/production/_111686283_pic1.png",
5 | ];
6 |
7 | export const sampleSchema = [
8 | {
9 | id: "first",
10 | label: "Please enter a number",
11 | type: "number",
12 | parameters: {
13 | max: 1,
14 | min: 20,
15 | },
16 | },
17 | {
18 | id: "second",
19 | label: "Please enter a string",
20 | type: "string",
21 | parameters: {
22 | length: 300,
23 | lines: 5,
24 | },
25 | },
26 | {
27 | id: "third",
28 | label: "Date of birth",
29 | type: "date",
30 | },
31 | {
32 | id: "fourth",
33 | label: "Choose one or more",
34 | type: "multiselect",
35 | parameters: {
36 | options: ["Option A", "Option B", "Option C"],
37 | },
38 | },
39 | {
40 | id: "fifth",
41 | label: "Choose one of the following",
42 | type: "singleselect",
43 | parameters: {
44 | name: "options",
45 | options: ["a", "b", "c"],
46 | },
47 | },
48 | // human_zones: {
49 | // label: "Mark any humans in the image",
50 | // type: "rectzone",
51 | // },
52 | ];
53 |
--------------------------------------------------------------------------------
/src/backend/model/user.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const {
3 | uniqueNamesGenerator,
4 | names,
5 | colors,
6 | } = require("unique-names-generator");
7 | const randomColor = require("randomcolor");
8 | const { redis } = require("../core/redis");
9 | const { nanoid } = require("nanoid");
10 |
11 | const Schema = Joi.object({
12 | id: Joi.string().min(1).max(50),
13 | name: Joi.string().min(3).max(25),
14 | avatar_color: Joi.string().regex(/^#[A-Fa-f0-9]{6}$/),
15 | });
16 |
17 | /**
18 | *
19 | * name and avatar_color are optional parameters.
20 | * USAGE :
21 | * user.InstanceFactory();
22 | * user.InstanceFactory({avatar_color: "#6483f4"});
23 | */
24 | async function InstanceFactory({
25 | id = `${nanoid()}`,
26 | name = uniqueNamesGenerator({
27 | dictionaries: [colors, names],
28 | separator: " ",
29 | style: "lowerCase",
30 | }),
31 | avatar_color = randomColor({ hue: "blue" }),
32 | } = {}) {
33 | const obj = { id, name, avatar_color };
34 | try {
35 | const validatedObj = await Schema.validateAsync(obj);
36 | return validatedObj;
37 | } catch (err) {
38 | throw `Error : Could not create User. Please check the schema in docs. ${err.message}`;
39 | }
40 | }
41 |
42 | async function save(user) {
43 | try {
44 | const result = await redis.hset(`user:${user.id}`, user);
45 | } catch (err) {
46 | throw `Could not save user in Redis ${user}. ${err}`;
47 | }
48 | }
49 |
50 | module.exports = { InstanceFactory, save };
51 |
--------------------------------------------------------------------------------
/src/frontend/src/favicon.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/src/backend/model/annotation-exercise.js:
--------------------------------------------------------------------------------
1 | const Joi = require("joi");
2 | const generator = require("generate-password");
3 | const {
4 | uniqueNamesGenerator,
5 | names,
6 | colors,
7 | } = require("unique-names-generator");
8 | const { nanoid } = require("nanoid");
9 |
10 | const Schema = Joi.object({
11 | id: Joi.string().min(1).max(50),
12 | name: Joi.string().required(),
13 | description: Joi.string().min(0).max(140),
14 | password: Joi.string().required(),
15 | schema: Joi.number(),
16 | owner: Joi.number(),
17 | editors: Joi.array().items(Joi.number()),
18 | participants: Joi.array().items(Joi.string()),
19 | posts: Joi.array().items(Joi.number()),
20 | });
21 |
22 | /* Returns an instance or throws an error
23 | *
24 | * USAGE :
25 | * instance = InstanceFactory();
26 | * instance = InstanceFactory({name = "blue rohan"})
27 | * instance = InstanceFactory({name = "blue rohan", participants = 12}) // throws error "participants" must be an array
28 | */
29 | async function InstanceFactory({
30 | id = `${nanoid()}`,
31 | name = uniqueNamesGenerator({
32 | dictionaries: [colors, names],
33 | separator: " ",
34 | style: "lowerCase",
35 | }),
36 | description = "write something here",
37 | password = generator.generate({ length: 16, numbers: true }),
38 | schema = -1,
39 | owner = -1,
40 | editors = [],
41 | participants = [],
42 | posts = [],
43 | } = {}) {
44 | const obj = {
45 | id,
46 | name,
47 | description,
48 | password,
49 | owner,
50 | schema,
51 | editors,
52 | participants,
53 | posts,
54 | };
55 |
56 | try {
57 | const validatedObj = await Schema.validateAsync(obj);
58 | return validatedObj;
59 | } catch (err) {
60 | throw `Error : Could not create Exercise object. Please check Schema. ${err.message}`;
61 | }
62 | }
63 |
64 | module.exports = { InstanceFactory };
65 |
--------------------------------------------------------------------------------
/src/backend/core/redis.js:
--------------------------------------------------------------------------------
1 | const Redis = require("ioredis");
2 | const redis = new Redis();
3 |
4 | Redis.Command.setArgumentTransformer("hset", (args) => {
5 | console.log(args);
6 | let arg = args[1];
7 | let argumentArray = [];
8 | argumentArray.push(args[0]);
9 | Object.keys(arg).map((item) => {
10 | argumentArray.push(item);
11 | argumentArray.push(arg[item] + "");
12 | });
13 |
14 | return argumentArray;
15 | });
16 |
17 | async function saveJSON(schemaJson, key) {
18 | try {
19 | const command = await redis.sendCommand(
20 | new Redis.Command(
21 | "JSON.SET",
22 | [key, ".", JSON.stringify(schemaJson)],
23 | "utf-8",
24 | function (err, value) {
25 | if (err) throw err;
26 | console.log({ SET: value.toJSON() });
27 | return value;
28 | }
29 | )
30 | );
31 | return command;
32 | } catch (err) {
33 | throw `Could not save schema in store. ${err}`;
34 | }
35 | }
36 |
37 | async function getJSON(key) {
38 | try {
39 | const command = await redis.sendCommand(
40 | new Redis.Command("JSON.GET", [key], "utf-8", function (
41 | err,
42 | value
43 | ) {
44 | if (err) throw err;
45 | return value;
46 | })
47 | );
48 | return command;
49 | } catch (err) {
50 | throw `Could not get schema in store. ${err}`;
51 | }
52 | }
53 |
54 | const RedisUtils = {
55 | saveJSON,
56 | getJSON,
57 | };
58 |
59 | module.exports = { redis, RedisUtils };
60 |
61 | // const test = async () => {
62 | // const annotationObject = {
63 | // name: "denny",
64 | // age: 20,
65 | // };
66 | // await redis.hset("annotation", annotationObject);
67 | // const val = await redis.hgetall("annotation");
68 | // const name = await redis.hget("annotation", "name");
69 | // await redis.hset("annotation", { age: 22 });
70 | // // console.log(name);
71 | // };
72 | // test();
73 |
--------------------------------------------------------------------------------
/src/frontend/src/pages/Assignments.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Box, Heading, Text, Button } from "grommet";
3 | import { Switch, Route } from "react-router-dom";
4 | import AssignmentNew from "./AssignmentNew";
5 | import axios from "axios";
6 | import { api_url } from "../config/default.json";
7 | import { View } from "grommet-icons";
8 | import { Link } from "../components/Link";
9 | import { useStore } from "../store/global";
10 |
11 | const Assignments = () => {
12 | const [exercises, setExercises] = useState([]);
13 | useEffect(async () => {
14 | try {
15 | let exercisesResponse = (await axios.get(`${api_url}/exercises`)).data
16 | .exercises;
17 | console.log(exercisesResponse);
18 | setExercises(exercisesResponse);
19 | } catch (err) {
20 | console.log(err);
21 | }
22 | }, []);
23 | return (
24 |
25 | Exercises
26 |
33 | Create New Exercise
34 |
35 |
36 |
37 | {exercises.map((exercise, ix) => (
38 |
39 |
40 |
41 | {exercise.name}
42 |
43 |
44 |
45 | {exercise.description}
46 |
47 |
48 |
49 | ))}
50 |
51 |
52 | );
53 | };
54 |
55 | export default Assignments;
56 |
--------------------------------------------------------------------------------
/src/backend/app/annotation-exercise/controller.js:
--------------------------------------------------------------------------------
1 | const { redis } = require("../../core/redis");
2 | const annotationExercise = require("../../model/annotation-exercise");
3 | const { nanoid } = require("nanoid");
4 |
5 | async function create({ name, description, password, participants } = {}) {
6 | try {
7 | const exercise = await annotationExercise.InstanceFactory({
8 | name,
9 | description,
10 | password,
11 | participants,
12 | });
13 | await redis.hset(`exercise:${exercise.id}`, exercise);
14 | return exercise;
15 | } catch (err) {
16 | throw `Error Creating Exercise. ${err}`;
17 | }
18 | }
19 |
20 | async function getAll() {
21 | try {
22 | let response;
23 | const exercises = await redis.keys("exercise:*");
24 |
25 | const hgetallPromises = [];
26 | exercises.map(async (exercise) => {
27 | hgetallPromises.push(redis.hgetall(exercise));
28 | });
29 |
30 | response = await Promise.all(hgetallPromises);
31 | return response;
32 | } catch (err) {
33 | console.log(err);
34 | throw "Error : Could not fetch exercises from store";
35 | }
36 | }
37 |
38 | async function get(id) {
39 | try {
40 | const exercise = await redis.hgetall(`exercise:${id}`);
41 | return exercise;
42 | } catch (err) {
43 | throw `Could not fetch exercise ${{ id }}. ${err}`;
44 | }
45 | }
46 |
47 | async function addParticipants(participants, annotationId) {
48 | try {
49 | participants.map(async (participant) => {
50 | await redis.sadd(`participant:${annotationId}`, participant);
51 | });
52 | } catch (err) {
53 | throw `Error : Could not add participants`;
54 | }
55 | }
56 |
57 | async function getParticipants(exerciseId) {
58 | try {
59 | const participants = await redis.smembers(`participant:${exerciseId}`);
60 | return participants;
61 | } catch (err) {
62 | throw `Error : Could not add participants`;
63 | }
64 | }
65 |
66 | // async function
67 |
68 | module.exports = { create, getAll, get, addParticipants, getParticipants };
69 |
--------------------------------------------------------------------------------
/src/backend/mocktest.js:
--------------------------------------------------------------------------------
1 | const user = require("./model/user");
2 | const flat = require("flat");
3 | const schema = require("./model/form-schema");
4 | const {
5 | text,
6 | number,
7 | date,
8 | singleselect,
9 | multiselect,
10 | } = require("./model/fields");
11 | const flatten = require("flat");
12 |
13 | const test = async () => {
14 | try {
15 | const schemaInstance = await schema.InstanceFactory({
16 | field_a: await text.InstanceFactory({ label: "Your Name" }),
17 | field_b: await number.InstanceFactory({
18 | label: "Your Age",
19 | parameters: { min: 18, max: 50 },
20 | }),
21 | field_c: await date.InstanceFactory({ label: "date of birth" }),
22 | field_d: await singleselect.InstanceFactory({
23 | label: "choose one ",
24 | parameters: { name: "options", options: ["A", "B", "C"] },
25 | }),
26 | field_e: await multiselect.InstanceFactory({
27 | label: "choose one ",
28 | parameters: { options: ["A", "B", "C"] },
29 | }),
30 | });
31 | console.log(schemaInstance);
32 | console.log(flatten({ schema: schemaInstance }, { delimiter: ":" }));
33 | } catch (err) {
34 | console.log(err);
35 | }
36 | };
37 | test();
38 |
39 | // const {MongoClient} = require('mongodb');
40 |
41 | // describe('insert', () => {
42 | // let connection;
43 | // let db;
44 |
45 | // beforeAll(async () => {
46 | // connection = await MongoClient.connect(global.__MONGO_URI__, {
47 | // useNewUrlParser: true,
48 | // });
49 | // db = await connection.db(global.__MONGO_DB_NAME__);
50 | // });
51 |
52 | // afterAll(async () => {
53 | // await connection.close();
54 | // await db.close();
55 | // });
56 |
57 | // it('should insert a doc into collection', async () => {
58 | // const users = db.collection('users');
59 |
60 | // const mockUser = {_id: 'some-user-id', name: 'John'};
61 | // await users.insertOne(mockUser);
62 |
63 | // const insertedUser = await users.findOne({_id: 'some-user-id'});
64 | // expect(insertedUser).toEqual(mockUser);
65 | // });
66 | // });
67 |
--------------------------------------------------------------------------------
/src/frontend/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Box, Heading, Button } from "grommet";
3 | import { useStore } from "../store/global";
4 | import { Select } from "grommet";
5 | import { Link } from "../components/Link";
6 | import { api_url } from "../config/default.json";
7 | import axios from "axios";
8 | import { useHistory } from "react-router-dom";
9 |
10 | const Home = () => {
11 | const history = useHistory();
12 | const setCurrentUser = useStore((state) => state.setCurrentUser);
13 | const currentUser = useStore((state) => state.currentUser);
14 | // const setUsers = useStore((state) => state.setUsers);
15 | const [users, setUsers] = useState(undefined);
16 | const [value, setValue] = React.useState("");
17 | const [originalUsers, setOriginalUsers] = useState(undefined);
18 |
19 | function computeAndSetUser(id) {
20 | console.log({ id, originalUsers });
21 | const user = originalUsers.filter((user) => user.id === id);
22 | console.log({ USER: user[0] });
23 | setCurrentUser(user[0]);
24 | }
25 |
26 | useEffect(async () => {
27 | console.log(`${api_url}/users`);
28 | try {
29 | let users = await axios.get(`${api_url}/users`);
30 |
31 | const x = users.data.map((user) => ({
32 | label: user.name,
33 | value: user.id,
34 | }));
35 | console.log({ USERS: x });
36 | setUsers(x);
37 | setOriginalUsers(users.data);
38 | } catch (err) {
39 | console.log(err);
40 | }
41 | }, []);
42 |
43 | return (
44 |
45 | Login
46 |
47 | {users && (
48 |
61 |
62 |
63 |
65 |
66 | );
67 | };
68 |
69 | export default Home;
70 |
--------------------------------------------------------------------------------
/src/frontend/src/components/MultiUserSelect.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { FormClose } from "grommet-icons";
3 | import { Box, Button, Heading, Text, Select } from "grommet";
4 |
5 | const MultiUserSelect = ({ users, label, onOptionsChange }) => {
6 | const [selected, setSelected] = useState([]);
7 | const allSeasons = users.map((user) => user.name);
8 |
9 | const onRemoveSeason = (season) => {
10 | const seasonIndex = allSeasons.indexOf(season);
11 | setSelected(
12 | selected.filter((selectedSeason) => selectedSeason !== seasonIndex)
13 | );
14 | };
15 |
16 | const renderSeason = (season) => (
17 |
42 | );
43 |
44 | const renderOption = (option, state) => (
45 |
46 | {option}
47 |
48 | );
49 | return (
50 |
77 | );
78 | };
79 |
80 | export default MultiUserSelect;
81 |
--------------------------------------------------------------------------------
/src/frontend/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Grommet, Box, Text, Avatar } from "grommet";
3 | import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
4 | import Assignments from "./pages/Assignments";
5 | import Assignment from "./pages/Assignment";
6 | import AssignmentNew from "./pages/AssignmentNew";
7 | import Home from "./pages/Home";
8 | import { useStore } from "./store/global";
9 |
10 | function App() {
11 | const currentUser = useStore((state) => state.currentUser);
12 | console.log({ CURRENT: currentUser });
13 | return (
14 |
15 |
16 |
25 |
26 |
27 |
28 | {currentUser && (
29 |
30 |
31 | {currentUser?.name
32 | .split(" ")
33 | .map((word) => word[0])
34 | .join("")}
35 |
36 |
37 | )}
38 |
39 |
40 |
41 | {/*
42 |
57 | */}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | export default App;
82 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Firstly we are really grateful to you that you are considering contributing to our Collaborative Realtime Media Annotator. We welcome contributions of all sorts - filing a bug report, suggesting improvements, proposing new feature, adding documentations, writing tests etc.
4 |
5 | By contributing to this project, you are agreeing to our [community guidelines](https://github.com/tattle-made/docs/blob/master/CODEOFCONDUCT.md)
6 |
7 | Contributing to Kosh takes 4 easy steps
8 |
9 | 👋 Say Hi
10 |
11 | 🔨 Do your thing
12 |
13 | 📞 Tell us
14 |
15 | 🎉 Celebrate
16 |
17 | ---
18 |
19 | # 👋 Say Hi
20 |
21 | The very first thing you should do is letting us know that you are interested in contributing to the project by the following means :
22 |
23 | If you are unsure about how to contribute to Kosh, simply join our slack and introduce yourself and mention what interests you about Kosh. We'll reach out and assist you further.
24 |
25 | If there's a particular improvement you want to suggest, or a bug in kosh you want to fix, simply create a Github Issue regarding it and we'll reach out to assist you further.
26 |
27 | # 🔨 Do your thing
28 |
29 | The very first thing you should do is be able to access Kosh and play around with it. There are two ways to do so
30 |
31 | - Launch the project on your own machine (guide)[docs/development.md]
32 |
33 | ## Pair programming
34 |
35 | We offer pair programming sessions with community members to familiarize them with the product and the code base. This will give you an opportunity to clarify any doubts regarding the codebase and the features that interest you.
36 |
37 | Once you have a local instance of the project running, you can make the changes you want to the code. Test the features and add appropriate documentation for it if needed.
38 |
39 | # 📞 Tell us
40 |
41 | All code changes happen via pull request. We use [Github Flow](https://guides.github.com/introduction/flow/). The easiest way to let us know if you want to combine your changes into the core code is to make a Pull Request (PR)
42 |
43 | In your PR, please mention the following :
44 |
45 | - What does this PR do?
46 | - How do we test this PR?
47 |
48 | We don't strictly follow test driven development (TDD) but any contributions that include tests are greatly appreciated.
49 |
50 | # 🎉 Celebrate
51 |
52 | We typically review a PR within 2-3 days. We might offer you some feedback to your PR and merge it! If you reached till this stage, Congratulations and join us afterwards for virtual coffee/tea on slack 🙂
53 |
54 | # Licence
55 |
56 | When you submit code changes, your submissions are understood to be under the same licence that covers the project - GPL-3. Feel free to contact the maintainers if that's a concern.
57 |
58 | # References
59 |
60 | - Gatsby
61 | - Coral Project
62 |
--------------------------------------------------------------------------------
/src/backend/app/annotation-exercise/routes.js:
--------------------------------------------------------------------------------
1 | const { StatusCodes } = require("http-status-codes");
2 | const {
3 | create: createExercise,
4 | getAll: getAllExercises,
5 | get: getExercise,
6 | addParticipants,
7 | getParticipants,
8 | } = require("./controller");
9 | const {
10 | createManyForExercise: createManyPostsForExercise,
11 | } = require("../post/controller");
12 | const { create: createSchema } = require("../form-schema/controller");
13 | const { nanoid } = require("nanoid");
14 | const { RedisUtils, redis } = require("../../core/redis");
15 |
16 | const configure = (expressApp) => {
17 | expressApp.post("/exercise", async (req, res) => {
18 | try {
19 | const { name, description, post_urls, participants, schema } =
20 | req.body;
21 | // console.log(participants);
22 | const response = {};
23 |
24 | // console.log(schema);
25 |
26 | const exerciseInstance = await createExercise({
27 | name,
28 | description,
29 | participants,
30 | });
31 | // console.log({ EXERCISE: exerciseInstance });
32 |
33 | const posts = await createManyPostsForExercise(
34 | post_urls,
35 | exerciseInstance.id
36 | );
37 | exerciseInstance.posts = posts;
38 | const schemaId = nanoid();
39 | exerciseInstance.schema = schemaId;
40 |
41 | // console.log(participants);
42 | await addParticipants(participants, exerciseInstance.id);
43 |
44 | // console.log({ EXERCISEEEEE: exerciseInstance });
45 |
46 | // store schema in ReJSON
47 | const exerciseRes = await createSchema(
48 | exerciseInstance,
49 | `exerciseJSON:${exerciseInstance.id}`
50 | );
51 | const schemaRes = await createSchema(
52 | schema,
53 | `schema:${exerciseInstance.id}:${schemaId}`
54 | );
55 |
56 | // console.log({
57 | // SCHEMA_ID: schemaId,
58 | // SCHEMA: schema,
59 | // POSTS: posts,
60 | // EXERCISE: exerciseInstance,
61 | // });
62 |
63 | // save schema in redis
64 | res.status(StatusCodes.OK).send(response);
65 | } catch (err) {
66 | console.log(`Error : Could not handle POST /exercise. ${err}`);
67 | }
68 | });
69 |
70 | expressApp.get("/exercises", async (req, res) => {
71 | try {
72 | const exercises = await getAllExercises();
73 | res.status(StatusCodes.OK).send({ exercises });
74 | } catch (err) {
75 | console.log(`Error : could not process GET /exercises. ${err}`);
76 | res.status(StatusCodes.INTERNAL_SERVER_ERROR);
77 | }
78 | });
79 |
80 | expressApp.get("/exercise/:exercise_id", async (req, res) => {
81 | // console.log("HERE");
82 | try {
83 | const { exercise_id } = req.params;
84 | const exercise = await RedisUtils.getJSON(
85 | `exerciseJSON:${exercise_id}`
86 | );
87 | const schema = await RedisUtils.getJSON(
88 | `schema:${exercise_id}:${
89 | JSON.parse(exercise.toString()).schema
90 | }`
91 | );
92 |
93 | const posts = JSON.parse(exercise.toString()).posts;
94 |
95 | // console.log({ POSTS: posts });
96 | let postRes = [];
97 | posts.map(async (post) => {
98 | postRes.push(redis.hgetall(`post:${exercise_id}:${post}`));
99 | });
100 |
101 | const allPosts = await Promise.all(postRes);
102 | // console.log({ PPPPP: allPosts });
103 |
104 | // console.log({ exercise_id, exercise });
105 | res.status(StatusCodes.OK).send({
106 | exercise: JSON.parse(exercise.toString()),
107 | schema: JSON.parse(schema.toString()),
108 | posts: allPosts,
109 | exercise_id,
110 | });
111 | } catch (err) {
112 | console.log(`Error : could not process GET /exercise. ${err}`);
113 | res.status(StatusCodes.INTERNAL_SERVER_ERROR).send();
114 | }
115 | });
116 |
117 | expressApp.get("/exercise/:exercise_id/participants", async (req, res) => {
118 | try {
119 | const { exercise_id } = req.params;
120 | const participants = await getParticipants(exercise_id);
121 | res.status(StatusCodes.OK).send({ participants });
122 | } catch (err) {
123 | console.log(`Error : could not fetch participants. ${err}`);
124 | res.status(StatusCodes.INTERNAL_SERVER_ERROR).send();
125 | }
126 | });
127 |
128 | return expressApp;
129 | };
130 |
131 | module.exports = { configure };
132 |
--------------------------------------------------------------------------------
/src/frontend/src/pages/AssignmentNew.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Box, Heading, Button, TextInput, TextArea } from "grommet";
3 | import JSONInput from "react-json-editor-ajrm";
4 | import locale from "react-json-editor-ajrm/locale/en";
5 | import theme from "react-json-editor-ajrm/themes";
6 | import axios from "axios";
7 | import { api_url } from "../config/default.json";
8 | import { sampleSchema, samplePostUrls } from "../config/sample-exercise-data";
9 | import { useStore } from "../store/global";
10 | import MultiUserSelect from "../components/MultiUserSelect";
11 | import { useHistory } from "react-router-dom";
12 |
13 | const AssignmentNew = () => {
14 | const history = useHistory();
15 | const setUsers = useStore((state) => state.setUsers);
16 | const users = useStore((state) => state.users);
17 |
18 | const [exerciseName, setExerciseName] = React.useState("");
19 | const [exerciseDescription, setExerciseDescription] = React.useState("");
20 | const [exerciseSchema, setExerciseSchema] = React.useState(sampleSchema);
21 | const [postUrls, setPostUrls] = React.useState(samplePostUrls);
22 | const [participants, setParticipants] = React.useState([]);
23 |
24 | useEffect(async () => {
25 | console.log(`${api_url}/users`);
26 | try {
27 | let users = await axios.get(`${api_url}/users`);
28 | setUsers(users.data);
29 | console.log({ USERS: users });
30 | } catch (err) {
31 | console.log(err);
32 | }
33 | }, []);
34 |
35 | async function onClickCreate() {
36 | try {
37 | // console.log(exerciseSchema);
38 | const res = await axios.post(`${api_url}/exercise`, {
39 | name: exerciseName,
40 | description: exerciseDescription,
41 | schema: exerciseSchema,
42 | post_urls: postUrls,
43 | participants,
44 | });
45 | if (res.status === 200) {
46 | // navigate to new page
47 | history.push("/exercises");
48 | } else {
49 | console.log({ res });
50 | }
51 |
52 | // console.log({
53 | // exerciseName,
54 | // exerciseDescription,
55 | // exerciseSchema,
56 | // postUrls,
57 | // partcipants,
58 | // });
59 | } catch (err) {
60 | console.log(err);
61 | }
62 | }
63 |
64 | return (
65 |
66 | Create New Exercise
67 |
68 | Name
69 | setExerciseName(event.target.value)}
73 | />
74 | Description
75 |
121 |
122 | );
123 | };
124 |
125 | export default AssignmentNew;
126 |
--------------------------------------------------------------------------------
/src/frontend/src/components/FormBuilder.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import {
3 | Box,
4 | Text,
5 | TextInput,
6 | CheckBoxGroup,
7 | RadioButtonGroup,
8 | Form,
9 | FormField,
10 | } from "grommet";
11 |
12 | function RectZone() {
13 | const [regions, setRegions] = useState([]);
14 | return <>{regions.map((region) => {})}>;
15 | }
16 |
17 | function LabelWrapper({ children, label, socket, type, fieldId }) {
18 | const [value, setValue] = useState(FormComponentIndex[type].defaultValue);
19 |
20 | function sendEvent(fieldValue) {
21 | console.log("Sending : ", { id: fieldId, value: fieldValue });
22 | socket.emit("data", { id: fieldId, value: fieldValue });
23 | }
24 |
25 | useEffect(() => {
26 | socket.on("data", (msg) => {
27 | if (fieldId === msg.id) {
28 | console.log(`rcvd ${fieldId} : ${msg.value}`);
29 | if (type === "multiselect") {
30 | setValue(msg.value.split(","));
31 | } else {
32 | setValue(msg.value);
33 | }
34 | }
35 | });
36 | }, [socket]);
37 |
38 | function set(e) {
39 | console.log("here");
40 | console.log(e);
41 | // send formId too
42 | FormComponentIndex[type].onChange(setValue, e, sendEvent);
43 | }
44 |
45 | const WrappedChildren = React.cloneElement(children, {
46 | value: value,
47 | onChange: (e) => set(e),
48 | });
49 |
50 | return (
51 |
52 | {WrappedChildren}
53 |
54 | );
55 | }
56 |
57 | const FormBuilder = ({ formData, socket }) => {
58 | const [value, setValue] = React.useState({});
59 | return (
60 |
61 |
88 |
89 | );
90 | };
91 |
92 | export default FormBuilder;
93 |
94 | const FormComponentIndex = {
95 | number: {
96 | component: function Text() {
97 | return ;
98 | },
99 | defaultValue: 0,
100 | onChange: (setValue, event, sendEvent) => {
101 | setValue(event.target.value);
102 | sendEvent(event.target.value);
103 | },
104 | },
105 | string: {
106 | component: function Text() {
107 | return ;
108 | },
109 | defaultValue: "hello",
110 | onChange: (setValue, event, sendEvent) => {
111 | setValue(event.target.value);
112 | sendEvent(event.target.value);
113 | },
114 | },
115 | date: {
116 | component: function Text() {
117 | return ;
118 | },
119 | defaultValue: "02/02/2021",
120 | onChange: (setValue, event, sendEvent) => {
121 | setValue(event.target.value);
122 | sendEvent(event.target.value);
123 | },
124 | },
125 | multiselect: {
126 | component: function CheckBox(parameters) {
127 | return ;
128 | },
129 | defaultValue: [],
130 | onChange: (setValue, event, sendEvent) => {
131 | setValue(event.value);
132 | sendEvent(event.value.join(","));
133 | },
134 | },
135 | singleselect: {
136 | component: function RadioButton(parameters) {
137 | return (
138 |
139 | );
140 | },
141 | defaultValue: "a",
142 | onChange: (setValue, event, sendEvent) => {
143 | setValue(event.target.value);
144 | sendEvent(event.target.value);
145 | },
146 | },
147 | rectzone: {
148 | component: ,
149 | defaultValue: [],
150 | onChange: (setValue, event) => {
151 | //setValue(event.value);
152 | console.log("TODO implemetation");
153 | },
154 | },
155 | };
156 |
--------------------------------------------------------------------------------
/src/frontend/rough/old-assignment.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { Box, Heading } from "grommet";
3 | import { Stage, Layer, Rect, Circle, Image } from "react-konva";
4 | import useImage from "use-image";
5 |
6 | const scaleBy = 1.5;
7 |
8 | const url =
9 | "https://images.unsplash.com/photo-1620416417410-5e467e5dbd25?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib";
10 |
11 | const Assignment = () => {
12 | const [image, setImage] = useImage(url);
13 | const [imageDimensions, setImageDimensions] = useState(undefined);
14 | const stageRef = useRef(null);
15 | const [currentRect, updateCurrentRect] = useState({
16 | track: false,
17 | type: undefined,
18 | id: undefined,
19 | start: undefined,
20 | end: undefined,
21 | });
22 | const [rectangles, setRectangles] = useState([]);
23 |
24 | useEffect(() => {
25 | if (image) {
26 | console.log({ w: image.width, h: image.height });
27 | }
28 | return () => {
29 | //cleanup
30 | };
31 | }, [image]);
32 |
33 | function zoomStage(event) {
34 | event.evt.preventDefault();
35 | if (stageRef.current !== null) {
36 | const stage = stageRef.current;
37 | console.log(stage.scaleX());
38 | // if (stage.scaleX() > 10) {
39 | // return;
40 | // }
41 | const oldScale = stage.scaleX();
42 | const { x: pointerX, y: pointerY } = stage.getPointerPosition();
43 | const mousePointTo = {
44 | x: (pointerX - stage.x()) / oldScale,
45 | y: (pointerY - stage.y()) / oldScale,
46 | };
47 | const newScale =
48 | event.evt.deltaY > 0 ? oldScale * scaleBy : oldScale / scaleBy;
49 | stage.scale({ x: newScale, y: newScale });
50 | const newPos = {
51 | x: pointerX - mousePointTo.x * newScale,
52 | y: pointerY - mousePointTo.y * newScale,
53 | };
54 | stage.position(newPos);
55 | stage.batchDraw();
56 | }
57 | }
58 |
59 | function mouseUp(e) {
60 | console.log("mouse up");
61 |
62 | let n = [];
63 | n = [...rectangles, currentRect];
64 | setRectangles(n);
65 |
66 | console.log({ n });
67 |
68 | updateCurrentRect({
69 | ...currentRect,
70 | track: false,
71 | });
72 |
73 | // console.log({ rectangles: rectangles });
74 | }
75 |
76 | function mouseDown(e) {
77 | const pos = e.target.getStage().getPointerPosition();
78 | console.log("mouse down");
79 | updateCurrentRect({
80 | track: true,
81 | type: undefined,
82 | id: undefined,
83 | start: { x: pos.x, y: pos.y },
84 | end: { x: pos.x, y: pos.y },
85 | });
86 | }
87 |
88 | function mouseMove(e) {
89 | if (currentRect.track) {
90 | const pos = e.target.getStage().getPointerPosition();
91 | console.log("mouse move");
92 | updateCurrentRect({
93 | ...currentRect,
94 | end: { x: pos.x, y: pos.y },
95 | });
96 | }
97 | }
98 |
99 | function drawRect(e) {
100 | // console.log({ x: e.evt.x, y: e.evt.y });
101 | // console.log(e);
102 | // console.log(currentRect);
103 | const pos = e.target.getStage().getPointerPosition();
104 | if (!currentRect.track && e.type === "mousedown") {
105 | console.log("lets begin tracking");
106 | updateCurrentRect({
107 | track: true,
108 | type: undefined,
109 | id: undefined,
110 | start: { x: pos.x, y: pos.y },
111 | end: { x: pos.x, y: pos.y },
112 | });
113 | }
114 | if (currentRect.track && e.type === "mousemove") {
115 | updateCurrentRect({
116 | ...currentRect,
117 | end: { x: pos.x, y: pos.y },
118 | });
119 | console.log(currentRect);
120 | }
121 | if (currentRect.track && e.type === "mouseup") {
122 | updateCurrentRect({
123 | ...currentRect,
124 | track: false,
125 | });
126 | console.log("lets end tracking", currentRect);
127 | // let n = [];
128 | // n = [...rectangles, currentRect];
129 | // setRectangles(n);
130 | // console.log({ rectangles: rectangles });
131 | }
132 | // updateCurrentRect({
133 | // track: false,
134 | // type: undefined,
135 | // id: undefined,
136 | // start: undefined,
137 | // end: undefined,
138 | // });
139 | }
140 |
141 | return (
142 |
143 | Assignment
144 |
145 |
146 |
155 |
156 | {image && (
157 |
162 | )}
163 | {/* */}
164 | {currentRect.track && (
165 |
173 | )}
174 |
175 | {rectangles.map((rectangle, index) => {
176 | console.log(rectangle);
177 | return (
178 |
187 | );
188 | })}
189 |
190 |
191 |
192 |
193 | form
194 |
195 |
196 | );
197 | };
198 |
199 | export default Assignment;
200 |
--------------------------------------------------------------------------------
/src/frontend/src/pages/Assignment.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { Box, Heading, Button, Avatar, Text } from "grommet";
3 | import { Stage, Layer, Image, Circle, Rect } from "react-konva";
4 | import useImage from "use-image";
5 | import FormBuilder from "../components/FormBuilder";
6 | import { useParams } from "react-router-dom";
7 | import axios from "axios";
8 | import { api_url } from "../config/default.json";
9 | import socketIOClient from "socket.io-client";
10 | import { useStore } from "../store/global";
11 | import randomColor from "randomcolor";
12 |
13 | const url =
14 | "https://images.unsplash.com/photo-1620416417410-5e467e5dbd25?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib";
15 |
16 | const AnnotationForm = ({ schema, socket }) => {
17 | return (
18 | <>
19 |
20 | >
21 | );
22 | };
23 |
24 | const Assignment = () => {
25 | const boxRef = useRef(null);
26 | const stageRef = useRef(null);
27 | let { exerciseId } = useParams();
28 | const [image, setImage] = useImage(url);
29 | const [stageDimensions, setStageDimensions] = useState(undefined);
30 | const [scaledImage, setScaledImage] = useState(undefined);
31 | const [currentRect, updateCurrentRect] = useState({
32 | track: false,
33 | type: undefined,
34 | id: undefined,
35 | start: undefined,
36 | end: undefined,
37 | });
38 | const [rectangles, setRectangles] = useState([]);
39 |
40 | const [posts, setPosts] = useState([]);
41 | const [currentPostIndex, setCurrentPostIndex] = useState(0);
42 | const [schema, setSchema] = useState([]);
43 |
44 | const [socket, setSocket] = useState(undefined);
45 | const [participants, setParticipants] = useState([]);
46 | const currentUser = useStore((state) => state.currentUser);
47 |
48 | useEffect(() => {
49 | console.log("CONNECT to socket");
50 | const skt = socketIOClient(api_url);
51 | skt.on("join", (msg) => {
52 | console.log("rcvd", msg);
53 | setParticipants(msg);
54 | });
55 | setSocket(skt);
56 | }, []);
57 |
58 | useEffect(() => {
59 | console.log({ currentUser, posts, currentPostIndex });
60 | // console.log({
61 | // HI: "hi",
62 | // name: currentUser.name,
63 | // exerciseId,
64 | // postId: posts,
65 | // ix: currentPostIndex,
66 | // });
67 | if (currentUser && posts.length != 0) {
68 | console.log("JOIN SEND");
69 | socket.emit("join", {
70 | name: currentUser.name,
71 | exerciseId,
72 | postId: posts[currentPostIndex].id,
73 | });
74 | }
75 | }, [posts]);
76 |
77 | useEffect(async () => {
78 | const exerciseRes = (await axios.get(`${api_url}/exercise/${exerciseId}`))
79 | .data;
80 | // console.log(exerciseRes);
81 | setPosts(exerciseRes.posts);
82 | setSchema(exerciseRes.schema);
83 | }, []);
84 |
85 | useEffect(() => {
86 | if (boxRef.current) {
87 | let height = boxRef.current.offsetHeight;
88 | let width = boxRef.current.offsetWidth;
89 |
90 | setStageDimensions({ width, height });
91 | }
92 | }, [boxRef]);
93 |
94 | useEffect(() => {
95 | // console.log("image loaded");
96 | // console.log({ image, stageDimensions });
97 | if (image && stageDimensions) {
98 | let scaledImage = calculateImagePos(
99 | stageDimensions.width,
100 | stageDimensions.height,
101 | image.width,
102 | image.height
103 | );
104 | // console.log({ scaledImage });
105 | setScaledImage(scaledImage);
106 | }
107 |
108 | return () => {
109 | //cleanup
110 | };
111 | }, [image, stageDimensions]);
112 |
113 | function calculateImagePos(
114 | containerWidth,
115 | containerHeight,
116 | imageWidth,
117 | imageHeight
118 | ) {
119 | let height = containerWidth * (imageHeight / imageWidth);
120 | let width = containerWidth;
121 |
122 | let x = containerWidth / 2 - width / 2;
123 | let y = containerHeight / 2 - height / 2;
124 | return { x, y, height, width };
125 | }
126 |
127 | function mouseUp(e) {
128 | console.log("mouse up");
129 |
130 | let n = [];
131 | n = [...rectangles, currentRect];
132 | setRectangles(n);
133 |
134 | console.log({ n });
135 |
136 | updateCurrentRect({
137 | ...currentRect,
138 | track: false,
139 | });
140 |
141 | // console.log({ rectangles: rectangles });
142 | }
143 |
144 | function mouseDown(e) {
145 | const pos = e.target.getStage().getPointerPosition();
146 | console.log("mouse down");
147 | updateCurrentRect({
148 | track: true,
149 | type: undefined,
150 | id: undefined,
151 | start: { x: pos.x, y: pos.y },
152 | end: { x: pos.x, y: pos.y },
153 | });
154 | }
155 |
156 | function btnClicked() {
157 | // console.log(stageRef.current.toJSON());
158 | console.log({ rectangles });
159 | }
160 |
161 | function mouseMove(e) {
162 | if (currentRect.track) {
163 | const pos = e.target.getStage().getPointerPosition();
164 | console.log("mouse move");
165 | updateCurrentRect({
166 | ...currentRect,
167 | end: { x: pos.x, y: pos.y },
168 | });
169 | }
170 | }
171 |
172 | return (
173 |
174 |
175 | Assignment
176 |
177 |
182 | {participants &&
183 | participants.map((participant, ix) => (
184 |
189 |
190 | {participant
191 | .split(" ")
192 | .map((word) => word[0])
193 | .join("")}
194 |
195 |
196 | ))}
197 |
198 |
199 |
200 |
201 | {stageDimensions && (
202 |
210 |
211 | {image && scaledImage && (
212 |
233 | )}
234 |
235 |
241 |
247 |
253 | {currentRect.track && (
254 |
262 | )}
263 | {rectangles.map((rectangle, index) => {
264 | console.log(rectangle);
265 | return (
266 |
275 | );
276 | })}
277 |
278 |
279 | {/* */}
286 |
287 |
288 | )}
289 |
290 |
291 | {/* */}
292 |
293 |
294 |
295 |
296 | );
297 | };
298 |
299 | export default Assignment;
300 |
--------------------------------------------------------------------------------