update to newer version

master
Thomas 2 years ago
parent 5d839e157b
commit 5c7a30c7f8
  1. 15
      README.md
  2. 7
      backend/.eslintrc.js
  3. 14
      backend/Dockerfile
  4. 1455
      backend/package-lock.json
  5. 15
      backend/package.json
  6. 213
      backend/src/Room.ts
  7. 113
      backend/src/Service.ts
  8. 105
      backend/src/User.ts
  9. 3384
      backend/src/data/timelines.ts
  10. 144
      backend/src/index.ts
  11. 57
      backend/src/timeline.ts
  12. 2
      backend/src/util.ts
  13. 6
      backend/tsconfig.json
  14. 739
      backend/yarn.lock
  15. 23
      frontend/.eslintrc.js
  16. 12
      frontend/Dockerfile
  17. 25
      frontend/config-overrides.js
  18. 21
      frontend/index.html
  19. 5282
      frontend/package-lock.json
  20. 64
      frontend/package.json
  21. 6
      frontend/postcss.config.js
  22. BIN
      frontend/public/favicon.ico
  23. 39
      frontend/public/index.html
  24. BIN
      frontend/public/logo192.png
  25. BIN
      frontend/public/logo512.png
  26. 8
      frontend/public/manifest.json
  27. 1
      frontend/public/robots.txt
  28. 11
      frontend/src/components/App.tsx
  29. 42
      frontend/src/components/Centurion.tsx
  30. 216
      frontend/src/components/Feed.tsx
  31. 43
      frontend/src/components/FeedItem.tsx
  32. 95
      frontend/src/components/FeedTicker.tsx
  33. 272
      frontend/src/components/Lobby.tsx
  34. 48
      frontend/src/components/NextShot.tsx
  35. 187
      frontend/src/components/Player.tsx
  36. 41
      frontend/src/components/ShotsTaken.tsx
  37. 133
      frontend/src/css/feed.css
  38. 49
      frontend/src/css/feed.sass
  39. 59
      frontend/src/css/index.css
  40. 57
      frontend/src/css/index.sass
  41. 76
      frontend/src/css/lobby.css
  42. 62
      frontend/src/css/lobby.sass
  43. 16
      frontend/src/css/player.css
  44. 4
      frontend/src/css/player.sass
  45. 9
      frontend/src/index.tsx
  46. 232
      frontend/src/lib/Connection.ts
  47. 1
      frontend/src/react-app-env.d.ts
  48. 133
      frontend/src/types/types.ts
  49. 33
      frontend/src/util/hooks.ts
  50. 9
      frontend/src/util/socket.ts
  51. 25
      frontend/src/util/sub.ts
  52. 18
      frontend/tsconfig.json
  53. 46
      frontend/vite.config.ts
  54. 12344
      frontend/yarn.lock
  55. 11772
      package-lock.json
  56. 44
      package.json

@ -1,13 +1,20 @@
CENTVRION CENTURION
========= =========
## Introduction ## Introduction
The projects consists of two projects, a frontend written in React, and a backend written in javascript. Both communicate with a websocket. The projects consists of two projects, a frontend written in React, and a backend written in Typescript (Node). Both communicate with a websocket.
## Setup ## Setup
Download centurion.m4a and place it in frontend/public/centurion.m4a 1. Download the various song files and place them in `frontend/public/songs/`. Easiest way to do this is to get them from production (E.g. `https://centurion.svia.nl/songs/centurion.m4a`)
2. Run `npm i` in the root directory, frontend, and backend.
3. Prepare the pre-commit hooks by running `npm run prepare`.
Start the backend with `npm run app` and the frontend with `npm run start`. ESLint is used for linting, Prettier for formatting. Currently, there are no tests. Tread carefully.
## Starting
* Start the frontend by running `npm run start` in the frontend directory.
* Idem for the backend.

@ -0,0 +1,7 @@
module.exports = {
"ignorePatterns": [".eslintrc.js"],
"parserOptions": {
"tsconfigRootDir": __dirname,
"project": ["./tsconfig.json"]
}
};

@ -1,11 +1,13 @@
FROM node:13-alpine FROM node:16-alpine
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock ./ # Install TS manually since it's only included in the parent's package.json
RUN yarn install RUN npm install --global typescript@^4.5.2
COPY package.json package-lock.json ./
RUN npm ci --no-progress --no-optional
COPY tsconfig.json ./ COPY tsconfig.json ./
COPY src src/ COPY src src/
COPY data data/ RUN npm run build
CMD ["npm", "run", "app"]
CMD ["npm", "run", "start-compiled"]

File diff suppressed because it is too large Load Diff

@ -5,18 +5,21 @@
"main": "./src/index.js", "main": "./src/index.js",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"app": "ts-node src/index.ts" "start-compiled": "node build/index.js",
"start": "ts-node src/index.ts",
"check": "tsc --noEmit",
"lint": "eslint 'src/**/*.{ts,tsx}'",
"fix": "eslint --fix 'src/**/*.{ts,tsx}'"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.17.1", "express": "^4.17.1",
"socket.io": "^2.3.0" "socket.io": "^4.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.6", "@types/express": "^4.17.13",
"@types/socket.io": "^2.1.4", "@types/node": "^16.11.12",
"ts-node": "^8.8.2", "ts-node": "^10.4.0"
"typescript": "^3.8.3"
} }
} }

@ -1,77 +1,96 @@
import {Socket} from "socket.io";
import User from "./User"; import User from "./User";
import {getIndex, getNextShot, getTimeline, getTimelineNames, indexForTime} from "./timeline"; import { getTimeline, getTimelineNames } from "./timeline";
import { getCurrentTime } from "./util"; import { getCurrentTime } from "./util";
export interface RoomOptions { export interface RoomOptions {
seekTime: number seekTime: number;
timelineName: string timelineName: string;
}
export interface TickerMessage {
user: User;
message: string;
}
// FIXME: dedupe with frontend
export interface SerializedRoom {
id: number;
userCount: number;
isLeader: boolean;
running: boolean;
startTime?: number;
seekTime: number;
timelineName: string;
readyToParticipate: boolean;
speedFactor: number;
ticker: string[];
users?: { id: string; readyToParticipate: boolean }[];
} }
export default class Room { export default class Room {
id: number = 0; id = 0;
users: User[] = []; users: User[] = [];
leader: User | null = null; leader: User | null = null;
ticker: TickerMessage[] = [];
running = false; running = false;
startTime = 0; startTime: number | undefined = undefined;
currentSeconds = 0;
timelineIndex: number = 0;
seekTime: number = 0; seekTime = 0;
timelineName: string = 'Centurion'; timelineName = "Centurion";
// For debugging purposes // For debugging purposes
speedFactor = 1; speedFactor = 1;
autoStart = false;
constructor(name: number) { constructor(name: number) {
this.id = name; this.id = name;
} }
serialize(user: User) { serialize(user?: User) {
return { const obj: SerializedRoom = {
'id': this.id, id: this.id,
'userCount': this.users.length, userCount: this.users.length,
'isLeader': this.leader == user, isLeader: this.leader === user,
'running': this.running, running: this.running,
'startTime': this.startTime, startTime: this.startTime,
'timelineName': this.timelineName, timelineName: this.timelineName,
'seekTime': this.seekTime, seekTime: this.seekTime,
'readyToParticipate': user.readyToParticipate || this.leader == user, readyToParticipate: this.getLeader()?.readyToParticipate || false,
'speedFactor': this.speedFactor speedFactor: this.speedFactor,
ticker: this.ticker.map((i) => i.message),
};
if (typeof user === "undefined" || this.leader === user) {
obj["users"] = this.users.map((u) => u.serialize());
} }
return obj;
} }
serializeTimeline(user: User) { serializeTimeline() {
return getTimeline(this.timelineName); return getTimeline(this.timelineName);
} }
sync() { sync() {
this.users.forEach(u => u.sync()); this.users.forEach((u) => u.sync());
} }
join(user: User) { async join(user: User) {
this.users.push(user); this.users.push(user);
user.setRoom(this); await user.setRoom(this);
if (!this.hasLeader()) { if (!this.hasLeader()) {
this.setLeader(user); this.setLeader(user);
} }
if (this.autoStart) {
this.seekTime = 2500000;
this.running = true;
this.start();
}
this.sync(); this.sync();
} }
leave(user: User) { async leave(user: User) {
this.removeTickerMessageForUser(user);
this.users.splice(this.users.indexOf(user), 1); this.users.splice(this.users.indexOf(user), 1);
user.setRoom(null); await user.setRoom(null);
if (this.leader == user) { if (this.leader == user) {
this.setRandomLeader(); this.setRandomLeader();
@ -80,91 +99,20 @@ export default class Room {
this.sync(); this.sync();
} }
onBeforeDelete() { setOptions(options: { seekTime: number; timelineName: string }) {
} this.seekTime = Math.max(0, Math.min(options.seekTime, 250 * 60 * 1000));
setOptions(options: any) {
this.seekTime = Math.max(0, Math.min(options.seekTime, 250 * 60 * 1000))
if (getTimelineNames().indexOf(options.timelineName) >= 0) { if (getTimelineNames().indexOf(options.timelineName) >= 0) {
this.timelineName = options.timelineName; this.timelineName = options.timelineName;
} }
this.sync() this.sync();
} }
start() { start() {
this.running = true; this.running = true;
this.startTime = getCurrentTime() - this.seekTime this.startTime = getCurrentTime() - this.seekTime;
this.sync(); this.sync();
} }
run(io: Socket) {
this.running = true;
this.startTime = Date.now();
// io.to(this.id.toString()).emit('timeline', {
// 'timeline': {
// }
// });
const doTick = () => {
if (this.users.length === 0) {
// this room is over.
return;
}
const timestamp = getIndex(this.timelineIndex);
const nextShot = getNextShot(this.timelineIndex);
if (!timestamp) {
// We are done.
io.to(this.id.toString()).emit('tick_event', {
tick: {
current: this.currentSeconds
}
});
console.log("Done");
this.running = false;
return;
}
console.log("ticking", this.currentSeconds);
io.to(this.id.toString()).emit('tick_event', {
tick: {
current: this.currentSeconds,
next: timestamp,
nextShot: nextShot
}
});
if (this.currentSeconds >= timestamp.timestamp) {
this.timelineIndex += 1;
}
this.currentSeconds += 1;
// We spend some time processing, wait a bit less than 1000ms
const nextTickTime = this.startTime + (1000 * this.currentSeconds / this.speedFactor);
const waitTime = nextTickTime - Date.now();
console.log("waiting", waitTime);
setTimeout(doTick, Math.floor(waitTime / this.speedFactor));
};
doTick();
}
/**
*
* @param io
* @param {number} time
*/
seek(io: Socket, time: number) {
this.currentSeconds = time;
this.startTime = Date.now() - time * 1000;
this.timelineIndex = indexForTime(this.currentSeconds);
io.to(this.id.toString()).emit('seek', time);
}
/** /**
* *
* @returns {boolean} * @returns {boolean}
@ -179,23 +127,6 @@ export default class Room {
} }
} }
/**
*
* @param id
* @returns {User|undefined}
*/
getUser(id: string) {
return this.users.find(u => u.id === id);
}
/**
*
* @param {string} id
*/
removeUser(id: string) {
this.users = this.users.filter(u => u.id !== id);
}
hasLeader(): boolean { hasLeader(): boolean {
return this.leader != null; return this.leader != null;
} }
@ -207,4 +138,30 @@ export default class Room {
getLeader(): User | null { getLeader(): User | null {
return this.leader; return this.leader;
} }
};
submitTickerMessage(user: User, message: string) {
message = message.replace("\n", "");
this.removeTickerMessageForUser(user);
this.ticker.push({
user: user,
message: message,
});
this.sync();
}
removeTickerMessageForUser(user: User) {
let existing = -1;
for (let i = 0; i < this.ticker.length; i++) {
if (this.ticker[i].user === user) {
existing = i;
break;
}
}
if (existing >= 0) {
this.ticker.splice(existing, 1);
}
}
}

@ -1,64 +1,72 @@
import { Socket } from "socket.io"; import { Socket } from "socket.io";
import User from './User' import User from "./User";
import Room, {RoomOptions} from './Room' import Room, { RoomOptions } from "./Room";
import {getCurrentTime, randomInt} from "./util"; import { getCurrentTime } from "./util";
export default class Service { export default class Service {
private roomIdToRooms = new Map<number, Room>(); private roomIdToRooms = new Map<number, Room>();
private socketsToUsers = new Map<string, User>(); private socketsToUsers = new Map<string, User>();
get rooms(): Room[] {
const rooms = [];
for (const [, room] of this.roomIdToRooms) {
rooms.push(room);
}
return rooms;
}
onSocketConnect(socket: Socket) { onSocketConnect(socket: Socket) {
let user = new User(socket); const user = new User(socket);
this.socketsToUsers.set(socket.id, user); this.socketsToUsers.set(socket.id, user);
user.sync(); user.sync();
} }
onSocketDisconnect(socket: Socket) { async onSocketDisconnect(socket: Socket) {
let user = this.getUser(socket); const user = this.getUser(socket);
if (user.room != null) { if (user.room != null) {
user.room.leave(user); await user.room.leave(user);
} }
this.deleteEmptyRooms(); this.deleteEmptyRooms();
user.onDisconnect();
} }
onTimeSync(socket: Socket, requestId: number, clientTime: number) { onTimeSync(socket: Socket, requestId: number, clientTime: number) {
let user = this.getUser(socket); const user = this.getUser(socket);
let now = getCurrentTime(); const now = getCurrentTime();
user.emit('time_sync', { user.emit("time_sync", {
'requestId': requestId, requestId: requestId,
'clientDiff': now - clientTime, clientDiff: now - clientTime,
'serverTime': now serverTime: now,
}) });
} }
onSetRoomOptions(socket: Socket, options: RoomOptions) { onSetRoomOptions(socket: Socket, options: RoomOptions) {
let user = this.getUser(socket); const user = this.getUser(socket);
if (user.room?.getLeader() == user) { if (user.room?.getLeader() == user) {
user.room!!.setOptions(options) user.room.setOptions(options);
} }
} }
onRequestStart(socket: Socket) { onRequestStart(socket: Socket) {
let user = this.getUser(socket); const user = this.getUser(socket);
if (user.room?.getLeader() == user) { if (user.room?.getLeader() === user) {
user.room!!.start(); user.room.start();
user.room.sync();
} }
} }
onRequestJoin(socket: Socket, roomId: number): boolean { async onRequestJoin(socket: Socket, roomId: number) {
let user = this.getUser(socket); const user = this.getUser(socket);
if (user.room && user.room.id == roomId) return false; if (user.room && user.room.id == roomId) return false;
if (user.room) { if (user.room) {
user.room.leave(user); await user.room.leave(user);
this.deleteEmptyRooms(); this.deleteEmptyRooms();
} }
@ -66,46 +74,60 @@ export default class Service {
this.createRoomWithId(roomId); this.createRoomWithId(roomId);
} }
let room = this.roomIdToRooms.get(roomId)!!; const room = this.roomIdToRooms.get(roomId);
room.join(user); if (!room) {
return false;
}
await room.join(user);
return true; return true;
} }
onRequestReady(socket: Socket) { onRequestSetReady(socket: Socket) {
let user = this.getUser(socket); const user = this.getUser(socket);
if (!user.room || user.readyToParticipate) return; if (!user.room || user.readyToParticipate) return;
user.readyToParticipate = true; user.readyToParticipate = true;
user.sync(); user.room.sync();
} }
onRequestJoinRandom(socket: Socket) { async onRequestJoinRandom(socket: Socket) {
let user = this.getUser(socket); const user = this.getUser(socket);
if (user.room) { if (user.room) {
user.room.leave(user); await user.room.leave(user);
this.deleteEmptyRooms(); this.deleteEmptyRooms();
} }
const room = this.createRandomRoom(); const room = this.createRandomRoom();
if (!room) throw Error('Too many rooms active'); if (!room) throw Error("Too many rooms active");
room.join(user); await room.join(user);
} }
hasRoomId(roomId: number): boolean { hasRoomId(roomId: number): boolean {
return this.roomIdToRooms.has(roomId); return this.roomIdToRooms.has(roomId);
} }
submitTickerMessage(socket: Socket, message: string) {
const user = this.getUser(socket);
if (!user.room) {
throw new Error("User has no room");
}
user.room.submitTickerMessage(user, message);
}
private getUser(socket: Socket): User { private getUser(socket: Socket): User {
let user = this.socketsToUsers.get(socket.id); const user = this.socketsToUsers.get(socket.id);
if (!user) { if (!user) {
throw new Error('User not found'); throw new Error("User not found");
} }
return user; return user;
} }
private deleteEmptyRooms() { private deleteEmptyRooms() {
for (let room of this.roomIdToRooms.values()) { for (const room of this.roomIdToRooms.values()) {
if (room.users.length == 0) { if (room.users.length == 0) {
this.deleteRoom(room); this.deleteRoom(room);
} }
@ -114,27 +136,28 @@ export default class Service {
private createRandomRoom(): Room | null { private createRandomRoom(): Room | null {
let tries = 0; let tries = 0;
let i = 1;
while (tries++ < 1000) { while (tries++ < 1000) {
const randomId = randomInt(100, Math.max(1000, this.roomIdToRooms.size * 2)); if (this.roomIdToRooms.has(i)) {
if (this.roomIdToRooms.has(randomId)) continue; i++;
} else {
return this.createRoomWithId(randomId); return this.createRoomWithId(i);
}
} }
return null; return null;
} }
private createRoomWithId(roomId: number): Room { private createRoomWithId(roomId: number): Room {
if (this.roomIdToRooms.has(roomId)) { if (this.roomIdToRooms.has(roomId)) {
throw new Error('A room with the given id already exists'); throw new Error("A room with the given id already exists");
} }
let room = new Room(roomId); const room = new Room(roomId);
this.roomIdToRooms.set(roomId, room); this.roomIdToRooms.set(roomId, room);
return room; return room;
} }
private deleteRoom(room: Room) { private deleteRoom(room: Room) {
this.roomIdToRooms.get(room.id)!!.onBeforeDelete(); this.roomIdToRooms.delete(room.id);
this.roomIdToRooms.delete(room.id)
} }
} }

@ -1,38 +1,43 @@
import { Socket } from "socket.io"; import { Socket } from "socket.io";
import Room from "./Room"; import Room, { SerializedRoom } from "./Room";
import { getTimelineNames } from "./timeline"; import { getTimelineNames } from "./timeline";
export interface Config {
availableTimelines: string[];
}
export default class User { export default class User {
socket: Socket; socket: Socket;
id: string; id: string;
room: Room | null = null; room: Room | null = null;
readyToParticipate: boolean = false; readyToParticipate = false;
constructor(socket: Socket) { constructor(socket: Socket) {
this.socket = socket; this.socket = socket;
this.id = socket.id; this.id = socket.id;
} }
onDisconnect() { serialize() {
if (this.room != null) { return {
id: this.id,
} readyToParticipate: this.readyToParticipate,
};
} }
setRoom(room: Room | null) { async setRoom(room: Room | null) {
if (this.room === room) return; if (this.room === room) return;
if (this.room != null) { if (this.room !== null) {
this.socket.leave(this.room.id.toString()); await this.socket.leave(this.room.id.toString());
this.readyToParticipate = false; this.readyToParticipate = false;
} }
this.room = room; this.room = room;
if (this.room != null) { if (this.room !== null) {
this.socket.join(this.room.id.toString()); await this.socket.join(this.room.id.toString());
} }
this.sync(); this.sync();
@ -40,69 +45,87 @@ export default class User {
getConfig() { getConfig() {
return { return {
'availableTimelines': getTimelineNames() availableTimelines: getTimelineNames(),
} };
} }
sentConfig: any = null; sentConfig: Config | null = null;
sentRoom: any = null; sentRoom: SerializedRoom | null = null;
sentTimelineName: string | null = null; sentTimelineName: string | null = null;
sync() { sync() {
// Config // Config
let config = this.getConfig(); const config = this.getConfig();
if (!this.syncEquals(this.sentConfig, config)) { if (!this.syncEquals(this.sentConfig, config)) {
this.sentConfig = config; this.sentConfig = Object.assign({}, config);
this.emit('config', { this.emit("config", {
'config': this.sentConfig config: this.sentConfig,
}); });
} }
// Room // Room
if (!this.syncEquals(this.sentRoom, this.room?.serialize(this))) { if (!this.syncEquals(this.sentRoom, this.room?.serialize(this))) {
this.sentRoom = this.room?.serialize(this); this.sentRoom = this.room
this.emit('room', { ? (JSON.parse(
'room': this.sentRoom JSON.stringify(this.room.serialize(this))
}) ) as SerializedRoom)
: null;
this.emit("room", {
room: this.sentRoom,
});
} }
// Timeline // Timeline
if (!this.syncEquals(this.sentTimelineName, this.room?.timelineName)) { if (!this.syncEquals(this.sentTimelineName, this.room?.timelineName)) {
this.sentTimelineName = this.room?.timelineName || null; this.sentTimelineName = this.room?.timelineName || null;
this.emit('timeline', { this.emit("timeline", {
'timeline': this.sentTimelineName == null ? null : this.room!!.serializeTimeline(this) timeline:
}) this.sentTimelineName == null ? null : this.room?.serializeTimeline(),
});
} }
} }
emit(eventName: string, obj: any) { emit(eventName: string, obj: unknown) {
this.socket.emit(eventName, obj); this.socket.emit(eventName, obj);
} }
syncEquals(obj1: any, obj2: any): boolean { syncEquals(obj1: unknown, obj2: unknown): boolean {
if (obj1 === undefined && obj2 === undefined) if (typeof obj1 !== typeof obj2) {
return true;
if ((obj1 === undefined && obj2 !== undefined) || (obj1 !== undefined && obj2 === undefined))
return false; return false;
}
if (obj1 === null && obj2 === null) if (typeof obj1 !== "object") {
// Both are not 'object'
return Object.is(obj1, obj2);
}
if (obj1 === null && obj2 === null) {
return true; return true;
}
if ((obj1 === null && obj2 !== null) || (obj1 !== null && obj2 === null)) if (obj1 === null || obj2 === null) {
return false; return false;
}
if (typeof (obj1) !== typeof (obj2)) if (typeof obj2 !== "object") {
return false; // This can not happen ;)
throw new TypeError("Obj2 is not object while obj1 is.");
}
if (typeof (obj1) === 'string' || typeof (obj1) === 'number' || typeof (obj1) === 'boolean') { if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return obj1 === obj2; return false;
} }
if (Object.keys(obj1).length !== Object.keys(obj2).length) return false return Object.keys(obj1).every((key: string) => {
if (!(key in obj1) || !(key in obj2)) {
return false;
}
return Object.keys(obj1).every(key => return this.syncEquals(
obj2.hasOwnProperty(key) && this.syncEquals(obj1[key], obj2[key]) obj1[key as keyof object],
obj2[key as keyof object]
); );
});
} }
} }

File diff suppressed because it is too large Load Diff

@ -1,84 +1,115 @@
import express from "express"; import express from "express";
import SocketIO, {Socket} from "socket.io"; import { Server } from "socket.io";
import path from "path"; import path from "path";
import Service from './Service' import Service from "./Service";
import { RoomOptions } from "./Room";
// process.on('SIGINT', () => process.exit()); // process.on('SIGINT', () => process.exit());
// process.on('SIGTERM', () => process.exit()); // process.on('SIGTERM', () => process.exit());
const HOST = '0.0.0.0'; const HOST = "0.0.0.0";
const PORT = 3001; const PORT = 3001;
const app = express(); const app = express();
const server = app.listen(PORT, HOST, () => console.log(`Centurion listening on port ${PORT}!`)); const httpServer = app.listen(PORT, HOST, () =>
app.use(express.static(path.join(__dirname, '../public'))); console.log(`Centurion listening on port ${PORT}!`)
);
app.use(express.static(path.join(__dirname, "../public")));
const io = SocketIO(server); const io = new Server(httpServer);
const service = new Service(); const service = new Service();
io.on('connection', socket => { app.get("/state", (req, res) => {
socket.on('disconnect', (reason) => { return res.json(service.rooms.map((r) => r.serialize()));
service.onSocketDisconnect(socket);
}); });
socket.on('ping', () => { io.on("connection", (socket) => {
socket.emit('pong'); socket.on("disconnect", async () => {
}) await service.onSocketDisconnect(socket);
});
socket.on("ping", () => {
socket.emit("pong");
});
socket.on('time_sync', (requestId: number, clientTime: number) => { socket.on("time_sync", (requestId: number, clientTime: number) => {
if (!Number.isSafeInteger(requestId)) return; if (!Number.isSafeInteger(requestId)) return;
if (!Number.isSafeInteger(clientTime)) return; if (!Number.isSafeInteger(clientTime)) return;
service.onTimeSync(socket, requestId, clientTime); service.onTimeSync(socket, requestId, clientTime);
}) });
socket.on('room_options', (options) => { socket.on("room_options", (options: RoomOptions) => {
if (!options) return; if (!options) return;
if (!options.timelineName || typeof (options.timelineName) !== 'string') return; if (!options.timelineName || typeof options.timelineName !== "string")
return;
if (!Number.isSafeInteger(options.seekTime)) return; if (!Number.isSafeInteger(options.seekTime)) return;
service.onSetRoomOptions(socket, options); service.onSetRoomOptions(socket, options);
}); });
socket.on('request_start', (options) => { socket.on("request_start", () => {
service.onRequestStart(socket); service.onRequestStart(socket);
}); });
socket.on('request_join', (roomId: number) => { socket.on(
if (!Number.isSafeInteger(roomId)) return; "request_join",
async (roomId: number, callback: (err?: string, res?: boolean) => void) => {
if (!Number.isSafeInteger(roomId)) {
return callback("Invalid roomId.");
}
service.onRequestJoin(socket, roomId); if (!service.hasRoomId(roomId)) {
}); // cannot join a room that does not exist.
return callback(undefined, false);
}
socket.on('request_ready', () => { try {
service.onRequestReady(socket); const didJoinRoom = await service.onRequestJoin(socket, roomId);
}) callback(undefined, didJoinRoom);
} catch (e) {
callback(e instanceof Error ? e.message : "Unknown error.");
}
}
);
socket.on('request_join_random', () => { socket.on("request_set_ready", () => {
service.onRequestJoinRandom(socket); service.onRequestSetReady(socket);
}) });
socket.on('call', (id: number, name: string, params: any) => { socket.on("request_join_random", async () => {
if (!Number.isSafeInteger(id)) return; await service.onRequestJoinRandom(socket);
// noinspection SuspiciousTypeOfGuard });
if (!name || typeof (name) !== 'string') return;
// if (!params) return;
let call = new Call(socket, id, name, params); socket.on(
"submit_ticker_message",
(message?: unknown, callback?: (res?: null, err?: string) => void) => {
if (typeof message !== "string") {
return callback && callback(null, "Invalid message.");
}
if (name == 'room_exists') { if (message.length > 192) {
let roomId = params && params['roomId']; // perfect voor het Wilhelmus
if (!Number.isSafeInteger(roomId)) { return callback && callback(null, "Message too long.");
call.error('Invalid room id');
return;
} }
call.respond(service.hasRoomId(roomId)); try {
return; service.submitTickerMessage(socket, message);
return callback && callback();
} catch (e) {
console.error(e);
return (
callback &&
callback(null, e instanceof Error ? e.message : "Unknown error")
);
}
} }
// );
service.onSocketConnect(socket);
// if (name == 'request_join') { // if (name == 'request_join') {
// let roomId = params && params['roomId']; // let roomId = params && params['roomId'];
// if (!Number.isSafeInteger(roomId)) { // if (!Number.isSafeInteger(roomId)) {
@ -95,9 +126,6 @@ io.on('connection', socket => {
// call.respond(false); // call.respond(false);
// } // }
// } // }
})
service.onSocketConnect(socket);
/*socket.on('join_room', (roomId, callback) => { /*socket.on('join_room', (roomId, callback) => {
if (!callback || typeof callback !== 'function') { if (!callback || typeof callback !== 'function') {
@ -157,31 +185,3 @@ io.on('connection', socket => {
} }
});*/ });*/
}); });
class Call {
private socket: Socket;
private id: number;
private name: string;
private params: any;
constructor(socket: Socket, id: number, name: string, params: any) {
this.socket = socket;
this.id = id;
this.name = name;
this.params = params;
}
error(reason: string) {
this.socket.emit('call_response', {
'id': this.id,
'error': reason
})
}
respond(data: any) {
this.socket.emit('call_response', {
'id': this.id,
'response': data
});
}
}

@ -1,62 +1,11 @@
// @ts-ignore import timeline from "./data/timelines";
import timeline from '../data/timelines.js';
export function getTimelineNames(): string[] { export function getTimelineNames(): string[] {
return timeline.timelines.map((i: any) => i.name) return timeline.timelines.map((timeline) => timeline.name);
} }
export function getTimeline(name: string) { export function getTimeline(name: string) {
let t = timeline.timelines.find((i: any) => i.name == name); const t = timeline.timelines.find((t) => t.name == name);
if (!t) return null; if (!t) return null;
return t; return t;
} }
/**
*
* @param i
* @returns {*}
*/
export function getIndex(i: number): any {
if (i >= timeline.length) {
return;
}
return timeline[i];
}
/**
* @param {number} i - the index.
* @returns {{count: number, timestamp: number}|undefined}
*/
export function getNextShot(i: number) {
for (; i < timeline.length; i++) {
const time = getIndex(i);
for (let event of time.events) {
if (event.type === 'shot') {
return {
timestamp: time.timestamp,
count: event.shotCount
}
}
}
}
return undefined;
}
export function indexForTime(seconds: number): number {
let lastIndex = 0;
for (let i = 0; i < timeline.length; i++) {
const time = timeline[i];
if (time.timestamp >= seconds) {
return lastIndex;
}
lastIndex = i;
}
return -1;
}

@ -13,8 +13,6 @@ export function randomInt(min: number, max: number): number {
let _randomTimeOffsetForDebug = randomInt(-10000, 10000); let _randomTimeOffsetForDebug = randomInt(-10000, 10000);
_randomTimeOffsetForDebug = 0; _randomTimeOffsetForDebug = 0;
console.log('random time offset', _randomTimeOffsetForDebug);
export function getCurrentTime() { export function getCurrentTime() {
return Date.now() + _randomTimeOffsetForDebug; return Date.now() + _randomTimeOffsetForDebug;
} }

@ -1,8 +1,12 @@
{ {
"compilerOptions": { "compilerOptions": {
"outDir": "build",
"target": "es6", "target": "es6",
"module": "commonjs", "module": "commonjs",
"strict": true, "strict": true,
"esModuleInterop": true "esModuleInterop": true
} },
"include": [
"src"
]
} }

@ -1,739 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/body-parser@*":
version "1.19.0"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
dependencies:
"@types/connect" "*"
"@types/node" "*"
"@types/connect@*":
version "3.4.33"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546"
integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==
dependencies:
"@types/node" "*"
"@types/express-serve-static-core@*":
version "4.17.5"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.5.tgz#a00ac7dadd746ae82477443e4d480a6a93ea083c"
integrity sha512-578YH5Lt88AKoADy0b2jQGwJtrBxezXtVe/MBqWXKZpqx91SnC0pVkVCcxcytz3lWW+cHBYDi3Ysh0WXc+rAYw==
dependencies:
"@types/node" "*"
"@types/range-parser" "*"
"@types/express@^4.17.6":
version "4.17.6"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.6.tgz#6bce49e49570507b86ea1b07b806f04697fac45e"
integrity sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "*"
"@types/qs" "*"
"@types/serve-static" "*"
"@types/mime@*":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
"@types/node@*":
version "13.9.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.2.tgz#ace1880c03594cc3e80206d96847157d8e7fa349"
integrity sha512-bnoqK579sAYrQbp73wwglccjJ4sfRdKU7WNEZ5FW4K2U6Kc0/eZ5kvXG0JKsEKFB50zrFmfFt52/cvBbZa7eXg==
"@types/qs@*":
version "6.9.1"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7"
integrity sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==
"@types/range-parser@*":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
"@types/serve-static@*":
version "1.13.3"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1"
integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==
dependencies:
"@types/express-serve-static-core" "*"
"@types/mime" "*"
"@types/socket.io@^2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-2.1.4.tgz#674e7bc193c5ccdadd4433f79f3660d31759e9ac"
integrity sha512-cI98INy7tYnweTsUlp8ocveVdAxENUThO0JsLSCs51cjOP2yV5Mqo5QszMDPckyRRA+PO6+wBgKvGvHUCc23TQ==
dependencies:
"@types/node" "*"
accepts@~1.3.4, accepts@~1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
dependencies:
mime-types "~2.1.24"
negotiator "0.6.2"
after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
arraybuffer.slice@~0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
async-limiter@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
base64-arraybuffer@0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
base64id@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
better-assert@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=
dependencies:
callsite "1.0.0"
blob@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
body-parser@1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
dependencies:
bytes "3.1.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "1.7.2"
iconv-lite "0.4.24"
on-finished "~2.3.0"
qs "6.7.0"
raw-body "2.4.0"
type-is "~1.6.17"
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
bytes@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
callsite@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
component-bind@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=
component-emitter@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
component-inherit@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
dependencies:
safe-buffer "5.1.2"
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
cookie@0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
debug@~3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@~4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
dependencies:
ms "^2.1.1"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
engine.io-client@~3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700"
integrity sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==
dependencies:
component-emitter "1.2.1"
component-inherit "0.0.3"
debug "~4.1.0"
engine.io-parser "~2.2.0"
has-cors "1.1.0"
indexof "0.0.1"
parseqs "0.0.5"
parseuri "0.0.5"
ws "~6.1.0"
xmlhttprequest-ssl "~1.5.4"
yeast "0.1.2"
engine.io-parser@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed"
integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==
dependencies:
after "0.8.2"
arraybuffer.slice "~0.0.7"
base64-arraybuffer "0.1.5"
blob "0.0.5"
has-binary2 "~1.0.2"
engine.io@~3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3"
integrity sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w==
dependencies:
accepts "~1.3.4"
base64id "2.0.0"
cookie "0.3.1"
debug "~4.1.0"
engine.io-parser "~2.2.0"
ws "^7.1.2"
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express@^4.17.1:
version "4.17.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
dependencies:
accepts "~1.3.7"
array-flatten "1.1.1"
body-parser "1.19.0"
content-disposition "0.5.3"
content-type "~1.0.4"
cookie "0.4.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "~1.1.2"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.3"
path-to-regexp "0.1.7"
proxy-addr "~2.0.5"
qs "6.7.0"
range-parser "~1.2.1"
safe-buffer "5.1.2"
send "0.17.1"
serve-static "1.14.1"
setprototypeof "1.1.1"
statuses "~1.5.0"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.3"
statuses "~1.5.0"
unpipe "~1.0.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
has-binary2@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d"
integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==
dependencies:
isarray "2.0.1"
has-cors@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
http-errors@1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
http-errors@~1.7.2:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
dependencies:
depd "~1.1.2"
inherits "2.0.4"
setprototypeof "1.1.1"
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
indexof@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
isarray@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@1.43.0:
version "1.43.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
mime-types@~2.1.24:
version "2.1.26"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
dependencies:
mime-db "1.43.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
ms@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
object-component@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseqs@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=
dependencies:
better-assert "~1.0.0"
parseuri@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=
dependencies:
better-assert "~1.0.0"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
dependencies:
forwarded "~0.1.2"
ipaddr.js "1.9.1"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
dependencies:
bytes "3.1.0"
http-errors "1.7.2"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.7.2"
mime "1.6.0"
ms "2.1.1"
on-finished "~2.3.0"
range-parser "~1.2.1"
statuses "~1.5.0"
serve-static@1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.17.1"
setprototypeof@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
socket.io-adapter@~1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9"
integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==
socket.io-client@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4"
integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==
dependencies:
backo2 "1.0.2"
base64-arraybuffer "0.1.5"
component-bind "1.0.0"
component-emitter "1.2.1"
debug "~4.1.0"
engine.io-client "~3.4.0"
has-binary2 "~1.0.2"
has-cors "1.1.0"
indexof "0.0.1"
object-component "0.0.3"
parseqs "0.0.5"
parseuri "0.0.5"
socket.io-parser "~3.3.0"
to-array "0.1.4"
socket.io-parser@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f"
integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==
dependencies:
component-emitter "1.2.1"
debug "~3.1.0"
isarray "2.0.1"
socket.io-parser@~3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.0.tgz#370bb4a151df2f77ce3345ff55a7072cc6e9565a"
integrity sha512-/G/VOI+3DBp0+DJKW4KesGnQkQPFmUCbA/oO2QGT6CWxU7hLGWqU3tyuzeSK/dqcyeHsQg1vTe9jiZI8GU9SCQ==
dependencies:
component-emitter "1.2.1"
debug "~4.1.0"
isarray "2.0.1"
socket.io@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb"
integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==
dependencies:
debug "~4.1.0"
engine.io "~3.4.0"
has-binary2 "~1.0.2"
socket.io-adapter "~1.1.0"
socket.io-client "2.3.0"
socket.io-parser "~3.4.0"
source-map-support@^0.5.17:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
to-array@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
ts-node@^8.8.2:
version "8.9.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.9.0.tgz#d7bf7272dcbecd3a2aa18bd0b96c7d2f270c15d4"
integrity sha512-rwkXfOs9zmoHrV8xE++dmNd6ZIS+nmHHCxcV53ekGJrxFLMbp+pizpPS07ARvhwneCIECPppOwbZHvw9sQtU4w==
dependencies:
arg "^4.1.0"
diff "^4.0.1"
make-error "^1.1.1"
source-map-support "^0.5.17"
yn "3.1.1"
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
typescript@^3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
ws@^7.1.2:
version "7.2.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46"
integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==
ws@~6.1.0:
version "6.1.4"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
dependencies:
async-limiter "~1.0.0"
xmlhttprequest-ssl@~1.5.4:
version "1.5.5"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==

@ -0,0 +1,23 @@
module.exports = {
"ignorePatterns": [".eslintrc.js", "vite.config.ts", "postcss.config.js"],
"settings": {
"react": {
"version": "detect"
}
},
"parserOptions": {
"tsconfigRootDir": __dirname,
"project": ["./tsconfig.json"]
},
"extends": [
"plugin:react/recommended"
],
"rules": {
// suppress errors for missing 'import React' in files
"react/react-in-jsx-scope": "off",
// At some point these must be set to "error"
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-argument": "warn"
}
};

@ -1,14 +1,16 @@
FROM node:13-alpine AS build FROM node:16-alpine AS build
WORKDIR /app WORKDIR /app
COPY package.json yarn.lock config-overrides.js ./ # Install TS manually since it's only included in the parent's package.json
RUN yarn install RUN npm install --global typescript@^4.5.2
COPY package.json package-lock.json vite.config.ts ./
RUN npm ci --no-progress --no-optional
COPY tsconfig.json ./ COPY tsconfig.json postcss.config.js index.html ./
COPY public public/ COPY public public/
COPY src src/ COPY src src/
RUN yarn build RUN npm run build
FROM nginx:alpine FROM nginx:alpine
WORKDIR /app WORKDIR /app

@ -1,25 +0,0 @@
module.exports = {
webpack: function (config, env) {
return config;
},
devServer: function (configFunction) {
// Return the replacement function for create-react-app to use to generate the Webpack
// Development Server config. "configFunction" is the function that would normally have
// been used to generate the Webpack Development server config - you can use it to create
// a starting configuration to then modify instead of having to create a config from scratch.
return function (proxy, allowedHost) {
// Create the default config by calling configFunction with the proxy/allowedHost parameters
const config = configFunction(proxy, allowedHost);
config.proxy = {
"/socket.io": {
target: "http://localhost:3001",
ws: true
}
}
// Return your customised Webpack Development Server config.
return config;
};
}
}

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/harambee.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#304ba3" />
<meta
name="description"
content="Centurion: Honderd minuten... Honderd shots... Kan jij het aan?"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>Centurion</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -3,35 +3,38 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^4.2.4", "antd": "^4.17.2",
"@testing-library/react": "^9.4.0", "react": "^17.0.2",
"@testing-library/user-event": "^7.2.1", "react-dom": "^17.0.2",
"@types/jest": "^24.9.1", "react-ticker": "^1.3.0",
"@types/node": "^12.12.27", "react-transition-group": "^4.4.2",
"@types/react": "^16.9.19", "socket.io-client": "^4.4.0"
"@types/react-dom": "^16.9.5",
"@types/react-transition-group": "^4.2.3",
"antd": "^4.1.4",
"node-sass": "^4.13.1",
"query-string": "^6.12.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "^3.4.0",
"react-transition-group": "^4.3.0",
"socket.io-client": "^2.3.0",
"sscaffold-css": "^0.1.0",
"typescript": "^3.7.5",
"use-query-params": "^0.6.0",
"use-socketio": "^2.0.0"
}, },
"scripts": { "devDependencies": {
"start": "react-app-rewired start", "@testing-library/jest-dom": "^5.15.1",
"build": "react-app-rewired build", "@testing-library/react": "^12.1.2",
"test": "react-app-rewired test", "@testing-library/user-event": "^13.5.0",
"eject": "react-scripts eject" "@types/jest": "^27.0.3",
"@types/react": "^17.0.37",
"@types/react-dom": "^17.0.11",
"@types/react-transition-group": "^4.4.4",
"@types/socket.io-client": "^1.4.36",
"@vitejs/plugin-react": "^1.1.1",
"autoprefixer": "^10.4.0",
"less": "^4.1.2",
"postcss": "^8.4.4",
"postcss-nested": "^5.0.6",
"vite": "^2.7.1",
"vite-plugin-imp": "^2.0.10",
"vite-plugin-svgr": "^0.6.0"
}, },
"eslintConfig": { "scripts": {
"extends": "react-app" "start": "vite",
"build": "tsc && vite build",
"serve": "vite preview",
"check": "tsc --noEmit",
"lint": "eslint 'src/**/*.{ts,tsx}'",
"fix": "eslint --fix 'src/**/*.{ts,tsx}'"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -44,12 +47,5 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@types/socket.io-client": "^1.4.32",
"customize-cra": "^0.9.1",
"eslint-plugin-react-hooks": "^2.3.0",
"react-app-rewired": "^2.1.5",
"workerize-loader": "^1.1.0"
} }
} }

@ -0,0 +1,6 @@
module.exports = {
plugins: [
require('autoprefixer'),
require('postcss-nested'),
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@ -1,39 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/harambee.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Centurion</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 229 KiB

@ -1,9 +1,9 @@
{ {
"short_name": "React App", "short_name": "Centurion",
"name": "Create React App Sample", "name": "Centurion: honderd minuten...",
"icons": [ "icons": [
{ {
"src": "harambee.ico", "src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
}, },
@ -20,6 +20,6 @@
], ],
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"theme_color": "#000000", "theme_color": "#304ba3",
"background_color": "#ffffff" "background_color": "#ffffff"
} }

@ -1,2 +1,3 @@
# https://www.robotstxt.org/robotstxt.html # https://www.robotstxt.org/robotstxt.html
User-agent: * User-agent: *
Disallow: /

@ -1,11 +0,0 @@
import React from 'react';
import Centurion from "./Centurion";
const App = () => {
return (
<Centurion/>
);
};
export default App;

@ -1,45 +1,25 @@
import React from "react"; import React, { useState } from "react";
import {Row} from "antd";
import { useRoomRunningAndReadyChanged } from "../lib/Connection"; import { useRoomRunningAndReadyChanged } from "../lib/Connection";
import NextShot from "./NextShot";
import Feed from "./Feed"; import Feed from "./Feed";
import ShotsTaken from "./ShotsTaken";
import Lobby from "./Lobby"; import Lobby from "./Lobby";
import logo from "../img/via-logo.svg";
import haramlogo from "../img/harambee_logo.png";
import Player from "./Player";
const Centurion = () => { const Centurion = () => {
const [currentUserReady, setCurrentUserReady] = useState(false);
const room = useRoomRunningAndReadyChanged(); const room = useRoomRunningAndReadyChanged();
const showFeed = (room?.running && room.readyToParticipate) || false; const showFeed = (room?.readyToParticipate && currentUserReady) || false;
const feedContent = (
<React.Fragment>
<Row>
<NextShot/>
<Feed/>
<ShotsTaken/>
</Row>
<Player/>
</React.Fragment>
);
const lobbyContent = (
<Lobby/>
);
return ( return (
<>
<section className="content"> <section className="content">
{showFeed ? feedContent : lobbyContent} {showFeed ? (
<Feed />
) : (
<Lobby
currentUserReady={currentUserReady}
onCurrentUserReadyChange={(b: boolean) => setCurrentUserReady(b)}
/>
)}
</section> </section>
<footer>
<img src={haramlogo} className="haram-logo" alt="haramlogo"/>
<img src={logo} className="via-logo" alt="logo"/>
</footer>
</>
); );
}; };

@ -1,39 +1,219 @@
import React from 'react'; import React, { useRef, useState } from "react";
import {Col} from "antd" import { Col, Row } from "antd";
import {TimelineItem} from "../types/types"; import {
EVENT_PRIORITY,
Timeline,
TimelineItem,
TimestampEvent,
} from "../types/types";
import FeedItem from "./FeedItem" import FeedItem from "./FeedItem";
import {roomTime, useTimeline} from "../lib/Connection"; import connection, {
import {useUpdateAfterDelay} from "../util/hooks"; calculateRoomTime,
useRoomRunningAndReadyChanged,
useRoomTime,
useTimeline,
} from "../lib/Connection";
import { useResize, useUpdateAfterDelay } from "../util/hooks";
import CSSTransition from "react-transition-group/CSSTransition"; import CSSTransition from "react-transition-group/CSSTransition";
import TransitionGroup from "react-transition-group/TransitionGroup"; import TransitionGroup from "react-transition-group/TransitionGroup";
import NextShot from "./NextShot";
import ShotsTaken from "./ShotsTaken";
import Player from "./Player";
const Feed = (props: any) => { import "../css/feed.css";
import FeedTicker from "./FeedTicker";
declare global {
interface Window {
__feedShakeDebug?: boolean;
}
}
function getNextItemDelay(timeline: Timeline | null, defaultDelay = 500) {
if (!timeline) {
return defaultDelay;
}
const time = calculateRoomTime();
const nextItem = timeline.itemAfterTime(time);
if (!nextItem) {
return defaultDelay;
}
const speedFactor = connection.room.get()?.speedFactor || 1;
return (nextItem.timestamp * 1000 - time) / speedFactor;
}
/**
* This map relates back an event to the index of the item displayed from
* top to bottom. We can use this to calculate opacity.
*/
function calculateKeyToIndexMap(feed: TimelineItem[]) {
const keyToIndex: Map<string, number> = new Map();
let totalEvents = 0;
for (const item of feed) {
totalEvents += item.events.length;
}
let totalIdx = 0;
for (const item of feed) {
for (let eventIdx = 0; eventIdx < item.events.length; eventIdx++) {
keyToIndex.set(`${item.timestamp}.${eventIdx}`, totalEvents - ++totalIdx);
}
}
return keyToIndex;
}
/**
* Makes sure 'shot' is always top-most within an timeline item
*/
function sortEvents(events: TimestampEvent[]) {
return events.sort((a, b) => {
return EVENT_PRIORITY.indexOf(b.type) - EVENT_PRIORITY.indexOf(a.type);
});
}
const Feed = () => {
const timeline = useTimeline(); const timeline = useTimeline();
useRoomRunningAndReadyChanged();
useResize();
useRoomTime();
const feedElement = useRef<HTMLDivElement>(null);
const [lastShotId, setLastShotId] = useState<string | null>(null);
useUpdateAfterDelay(getNextItemDelay(timeline));
if (!timeline) {
throw new TypeError("Feed without timeline.");
}
const time = calculateRoomTime();
const liveFeed = timeline.feed.filter((item) => {
return item.timestamp * 1000 <= time;
});
useUpdateAfterDelay(500) const keyToIndex: Map<string, number> = calculateKeyToIndexMap(liveFeed);
let liveFeed: TimelineItem[] = []; const lastEvent = timeline.eventBeforeTime(time, "shot");
if (lastEvent && lastShotId !== lastEvent?.id) {
setLastShotId(lastEvent.id);
if (feedElement.current) {
// Let the browser first do the heavy dom stuff to avoid lagging our
// animation.
setTimeout(doSingleShake, 100);
}
}
if (timeline != null) { function doSingleShake() {
liveFeed = timeline.feed.filter(item => { if (!feedElement.current) return;
return item.timestamp * 1000 <= roomTime()
if (feedElement.current.getAnimations().length > 0) return;
if (feedElement.current.classList.contains("feed-shot-shake")) {
feedElement.current.getAnimations().forEach((i) => i.finish());
feedElement.current.classList.remove("feed-shot-shake");
setTimeout(doSingleShake, 0);
return;
}
feedElement.current.classList.add("feed-shot-shake");
const el = feedElement.current;
const listener = function () {
el.classList.remove("feed-shot-shake");
el.removeEventListener("animationend", listener);
};
el.addEventListener("animationend", listener);
}
function itemOpacity(itemKey: string) {
// This is a bit of a hack but works better than trying to do this in css.
// This fades out the elements the lower the element is on the screen.
// Ticker is only visible on large screens, the ticker obstructs some of
// the items. If it is not visible we simply take body height, otherwise
// we use the height where the ticker starts.
const tickerHeight = document
.querySelector(".ticker-container")
?.getBoundingClientRect().y;
const bodyHeight = document.body.clientHeight;
const totalHeight = tickerHeight ? tickerHeight : bodyHeight;
// Start at top most item, figure out which index is the first
// that is NOT visible.
const items = document.querySelectorAll(".feed-item-container");
let i = items.length - 1;
for (; i > 0; i--) {
const rect = items[i].getBoundingClientRect();
// If the bottom of this element is below the screen, we declare it
// not-visible.
if (rect.y + rect.height >= totalHeight) {
break;
}
}
const totalItemsOnScreen = items.length - i;
const index = keyToIndex.get(itemKey) ?? 0;
let x = index / (totalItemsOnScreen - 1.8);
x = Math.min(0.9, Math.max(0, x));
return 1 - Math.pow(x, 4);
}
const debug = true;
if (debug) {
if (!window["__feedShakeDebug"]) {
window["__feedShakeDebug"] = true;
window.document.documentElement.addEventListener("keydown", (e) => {
if (e.keyCode === 67) {
// c
doSingleShake();
}
}); });
} }
}
return ( return (
<Col className="time-feed" span={24} md={16}> <div className="feed" ref={feedElement}>
<Row className="feed-items">
<Col span={12} md={4} className="sider">
<NextShot />
<Player />
</Col>
<Col span={12} md={{ span: 4, push: 16 }} className="sider">
<ShotsTaken />
</Col>
<Col span={24} md={{ span: 16, pull: 4 }}>
<TransitionGroup className="feed-reverse"> <TransitionGroup className="feed-reverse">
{liveFeed.map((item, i) => {liveFeed.map((item) =>
item.events.map((event, j) => sortEvents(item.events).map((event, j) => (
<CSSTransition timeout={500} classNames="fade" key={`${item.timestamp}.${j}`}> <CSSTransition
<FeedItem item={event} key={`${item.timestamp}.${j}f`}/> timeout={300}
classNames="fade"
key={`${item.timestamp}.${j}`}
>
<div
className="feed-item-container"
style={{ opacity: itemOpacity(`${item.timestamp}.${j}`) }}
>
<FeedItem item={event} />
</div>
</CSSTransition> </CSSTransition>
) ))
)} )}
</TransitionGroup> </TransitionGroup>
</Col> </Col>
</Row>
<Row className="ticker">
<FeedTicker />
</Row>
</div>
); );
}; };

@ -1,33 +1,38 @@
import React, {PureComponent} from 'react'; import { Col, Row } from "antd";
import {TimestampEvent} from "../types/types"; import type { TimestampEvent } from "../types/types";
import '../css/feed.sass' import "../css/feed.css";
import shot from "../img/shot.png"; import shot from "../img/shot.png";
import song from "../img/song.png"; import song from "../img/song.png";
import talk from "../img/talk.png"; import talk from "../img/talk.png";
import time from "../img/time.png"; import time from "../img/time.png";
export interface FeedItemProps {
item: TimestampEvent;
}
const images = { const images = {
shot, song, talk, time shot,
song,
talk,
time,
}; };
class FeedItem extends PureComponent<{item: TimestampEvent}> { const FeedItem = ({ item }: FeedItemProps) => {
render() {
return ( return (
<div className="feed-item"> <Row align="middle" className="feed-item">
<div className="feed-item__title"> <Col span={11} className="feed-item__title">
{this.props.item.text[0]} {item.text[0]}
</div> </Col>
<div className="feed-item__emoji"> <Col span={2} className="feed-item__emoji">
<img src={images[this.props.item.type]}/> <img alt={item.type} src={images[item.type]} />
</div> </Col>
<div className="feed-item__desc"> <Col span={11} className="feed-item__desc">
{this.props.item.text[1]} {item.text[1]}
</div> </Col>
</div> </Row>
); );
} };
}
export default FeedItem; export default FeedItem;

@ -0,0 +1,95 @@
import React, { MouseEvent, useRef, useState } from "react";
import "../css/feed.css";
import connection, { useRoom } from "../lib/Connection";
import { Button, Input, Modal } from "antd";
import Ticker from "react-ticker";
const FeedTicker = (props: any) => {
const room = useRoom();
const [showTickerMessageModal, setShowTickerMessageModal] = useState(false);
const [tickerMessage, setTickerMessage] = useState("");
const [blockMessageInput, setBlockMessageInput] = useState(false);
const messageInput = useRef<Input>(null);
function handleTickerMessageButton(e: MouseEvent) {
if (blockMessageInput) return;
setShowTickerMessageModal(true);
setTimeout(function () {
messageInput.current?.focus();
}, 100);
}
function cancelTickerMessageModal() {
if (blockMessageInput) return;
setShowTickerMessageModal(false);
setTickerMessage("");
}
async function okTickerMessageModal() {
if (blockMessageInput) return;
if (tickerMessage) {
setBlockMessageInput(true);
try {
await connection.submitTickerMessage(tickerMessage);
setBlockMessageInput(false);
setShowTickerMessageModal(false);
setTickerMessage("");
if (messageInput.current) {
messageInput.current.input.value = "";
}
} catch {
setBlockMessageInput(false);
}
}
}
function getForIndex(index: number) {
return room?.ticker[index % room.ticker.length];
}
return (
<div className="ticker-container">
<Modal
title="Stuur berichtje naar de ticker"
onCancel={cancelTickerMessageModal}
onOk={okTickerMessageModal}
visible={showTickerMessageModal}
>
<Input
value={tickerMessage}
ref={messageInput}
placeholder="Bericht"
onChange={(e) => setTickerMessage(e.target.value)}
onKeyPress={(e) => {
e.key === "Enter" && okTickerMessageModal();
}}
/>
</Modal>
<div className="ticker-outer">
<Ticker>
{({ index }) => (
<>
{room?.ticker && (
<span className="ticker-item">{getForIndex(index)}</span>
)}
</>
)}
</Ticker>
<Button
className="ticker-message-button"
type="ghost"
onClick={handleTickerMessageButton}
>
+
</Button>
</div>
</div>
);
};
export default FeedTicker;

@ -1,74 +1,119 @@
import React, {MouseEvent, useState} from 'react'; import { useState } from "react";
import {Button, Card, Col, Divider, Form, Input, InputNumber, Row, Select} from "antd" import {
import {red} from '@ant-design/colors'; Button,
Card,
import connection, {useConfig, useIsConnected, useRoom} from "../lib/Connection"; Col,
Divider,
import "../css/lobby.sass"; Form,
import beer from "../img/beer.png" Input,
InputNumber,
Row,
Select,
Badge,
} from "antd";
import { red } from "@ant-design/colors";
import connection, {
useConfig,
useIsConnected,
useRoom,
useTimelineSongFileChanged,
} from "../lib/Connection";
import "../css/lobby.css";
import logo from "../img/via-logo.svg";
import haramlogo from "../img/harambee_logo.png";
import beer from "../img/beer.png";
import { RoomOptions } from "../types/types"; import { RoomOptions } from "../types/types";
const { Option } = Select; const { Option } = Select;
const Lobby = (props: any) => { export interface PropType {
currentUserReady: boolean;
onCurrentUserReadyChange?: (ready: boolean) => void;
}
const Lobby = (props: PropType) => {
// Form/control states. // Form/control states.
const [selectedRoomId, setSelectedRoomId] = useState(1); const [selectedRoomId, setSelectedRoomId] = useState(1);
const [seekTime, setSeekTime] = useState(0); const [seekTime, setSeekTime] = useState(0);
const [timelineName, setTimelineName] = useState(null); const [timelineName, setTimelineName] = useState(null);
const [joiningLobby, setJoiningLobby] = useState(false); const [joiningLobby, setJoiningLobby] = useState(false);
const [joinLobbyError, setJoinLobbyError] = useState(false); const [joinLobbyError, setJoinLobbyError] = useState(false);
const [isPreloading, setIsPreloading] = useState(false);
const timeline = useTimelineSongFileChanged();
// Room and logic states. // Room and logic states.
const isConnected = useIsConnected(); const isConnected = useIsConnected();
const room = useRoom(); const room = useRoom();
const config = useConfig(); const config = useConfig();
// @ts-ignore const isLeader = room?.isLeader || false;
const connectionType = connection.socket.io.engine.transport.name; const userCount = room?.userCount || 0;
let isLeader = room?.isLeader || false; async function handleJoin() {
let userCount = room?.userCount || 0; await preloadAudio();
connection.requestSetReady();
function handleRequestStartClicked(e: MouseEvent) { props.onCurrentUserReadyChange?.(true);
connection.requestStart(seekTime * 1000);
}
function handleJoin(e: MouseEvent) {
connection.requestReady();
} }
function applyRoomId(v: number) { async function applyRoomId(v: number) {
connection.requestJoin(v).then(v => { setJoiningLobby(true);
await connection.requestJoin(v);
setJoiningLobby(false); setJoiningLobby(false);
setJoinLobbyError(!v); setJoinLobbyError(!v);
})
setJoiningLobby(true)
} }
function handleJoinRandomLobby() { function handleJoinRandomLobby() {
connection.requestJoinRandom() connection.requestJoinRandom();
setJoinLobbyError(false); setJoinLobbyError(false);
} }
function handleTimelineNameSet(timelineName: any) { function handleTimelineNameSet(timelineName: any) {
setTimelineName(timelineName); setTimelineName(timelineName);
connection.setRoomOptions(new RoomOptions( connection.setRoomOptions(
seekTime || 0, new RoomOptions(
timelineName || room?.timelineName || '')) seekTime * 1000 || 0,
timelineName || room?.timelineName || ""
)
);
} }
function handleSetSeekTime(seekTime: number) { function handleSetSeekTime(seekTime: number) {
setSeekTime(seekTime); setSeekTime(seekTime);
connection.setRoomOptions(new RoomOptions( connection.setRoomOptions(
new RoomOptions(
seekTime * 1000 || 0, seekTime * 1000 || 0,
timelineName || room?.timelineName || '')) timelineName || room?.timelineName || ""
)
);
}
function preloadAudio(): Promise<boolean> {
setIsPreloading(true);
const songFile = timeline?.songFile;
if (!songFile) {
return Promise.resolve(false);
} }
let leaderConfig = ( return new Promise<boolean>((resolve) => {
const audioElement = new Audio();
audioElement.addEventListener("canplaythrough", () => {
// 'canplaythrough' means the browser thinks it has buffered enough to play
// until the end.
setIsPreloading(false);
resolve(true);
});
audioElement.src = songFile;
});
}
const leaderConfig = (
<Row justify="center"> <Row justify="center">
<Col> <Col>
<Form <Form
layout='horizontal' layout="horizontal"
labelCol={{ span: 8 }} labelCol={{ span: 8 }}
wrapperCol={{ span: 24 }} wrapperCol={{ span: 24 }}
> >
@ -77,56 +122,85 @@ const Lobby = (props: any) => {
type="number" type="number"
suffix="sec" suffix="sec"
value={seekTime} value={seekTime}
onChange={v => handleSetSeekTime(parseInt(v.target.value) || 0)}/> onChange={(v) => handleSetSeekTime(parseInt(v.target.value) || 0)}
/>
</Form.Item> </Form.Item>
<Form.Item label="Nummer"> <Form.Item label="Nummer">
<Select defaultValue={(room && room.timelineName) || ''} <Select
onChange={e => handleTimelineNameSet(e)}> defaultValue={(room && room.timelineName) || ""}
{config && config.availableTimelines.map((item, i) => onChange={(e) => handleTimelineNameSet(e)}
<Option key={item} value={item}>{item}</Option> >
)} {config &&
config.availableTimelines.map((item, i) => (
<Option key={item} value={item}>
{item}
</Option>
))}
</Select> </Select>
</Form.Item> </Form.Item>
</Form> </Form>
<Button <Button
block block
type="primary" type="primary"
onClick={handleRequestStartClicked}>Start</Button> loading={isPreloading}
onClick={handleJoin}
>
Start
</Button>
</Col> </Col>
</Row> </Row>
) );
let nonLeaderConfig = ( const nonLeaderConfig = (
<Row justify="center"> <Row justify="center">
<Col> <Col>
<p> <p>
We gaan luisteren naar <b>{room && room.timelineName}</b> en {room?.running ? "We luisteren naar" : "We gaan luisteren naar"}{" "}
<b>{room && room.timelineName}</b> en
{room?.running && <span> zijn al gestart!</span>} {room?.running && <span> zijn al gestart!</span>}
{!room?.running && <span> starten op {(room?.seekTime || 0) / 1000} seconden</span>} {!room?.running && (
<span> starten op {(room?.seekTime || 0) / 1000} seconden</span>
)}
</p> </p>
<Button <Button
block block
type="primary" type="primary"
disabled={!room || room.readyToParticipate} disabled={!room || props.currentUserReady}
onClick={handleJoin}>{room && room.readyToParticipate ? 'Wachten op het startsein' : 'Kom erbij'}</Button> loading={isPreloading}
onClick={handleJoin}
>
{room && props.currentUserReady
? "Wachten op het startsein"
: "Kom erbij"}
</Button>
</Col> </Col>
</Row> </Row>
) );
// @ts-ignore
return ( return (
<div className="lobby"> <div className="lobby">
<Row> <Row className="centurion-title" justify="center">
<Col className="centurion-title" span={24}> <Col span={4} md={4}>
<div className="beer-flipped"> <img
<img src={beer} className={`beer ${isConnected ? 'connected' : 'connecting'}`} alt="beer"/> src={beer}
</div> className={`beer beer-flipped ${
<span className="text">Centurion!</span> isConnected ? "connected" : "connecting"
<img src={beer} className={`beer ${isConnected ? 'connected' : 'connecting'}`} alt="beer"/> }`}
alt="beer"
/>
</Col>
<Col span={12} md={6}>
Centurion!
</Col>
<Col span={4} md={4}>
<img
src={beer}
className={`beer ${isConnected ? "connected" : "connecting"}`}
alt="beer"
/>
</Col> </Col>
</Row> </Row>
<Row> <Row>
@ -138,36 +212,44 @@ const Lobby = (props: any) => {
</Row> </Row>
<br /> <br />
{!isConnected && {!isConnected && (
<Row justify="center"> <Row justify="center">
<Col className="lobby-connecting"> <Col className="lobby-connecting">
<h2>Verbinden...</h2> <h2>Verbinden...</h2>
</Col> </Col>
</Row> </Row>
} )}
{isConnected && {isConnected && (
<Row justify="center"> <Row justify="center">
<Col xs={24} sm={16} md={12} xl={10} className="lobby-info"> <Col xs={24} sm={16} md={12} xl={10}>
<Card> <Card>
<h3>Huidige lobby: <b>{room?.id || 'Geen lobby'}</b></h3> <h3>
Huidige lobby: <b>{room ? `#${room.id}` : "Geen lobby"}</b>
</h3>
{/*<span>Verbonden met {connectionType}</span>*/} {room && (
<Row>
{userCount === 1 ? (
<span>Er is één gebruiker aanwezig.</span>
) : (
<span>Er zijn {userCount} gebruikers aanwezig.</span>
)}
</Row>
)}
{room && <Row justify="center">
<span> {room?.users?.map((u) => (
{userCount === 1 ? <Badge
<p>Er is één gebruiker aanwezig.</p> key={u.id}
: status={u.readyToParticipate ? "success" : "error"}
<p>Er zijn {userCount} gebruikers aanwezig.</p> />
} ))}
</span> </Row>
}
{room && {room && <Row>Deel de link met je vrienden om mee te doen!</Row>}
<span>Deel de link met je vrienden om mee te doen!</span>
} <Divider />
<br/>
<br/>
{room && (isLeader ? leaderConfig : nonLeaderConfig)} {room && (isLeader ? leaderConfig : nonLeaderConfig)}
@ -176,42 +258,54 @@ const Lobby = (props: any) => {
<Row justify="center"> <Row justify="center">
<Col> <Col>
<InputNumber <InputNumber
style={{'width': 'calc(100% - 150px)'}} style={{ width: "calc(100% - 150px)" }}
min={1} min={1}
max={100000} max={100000}
value={selectedRoomId || room?.id || 0} value={selectedRoomId || room?.id || 0}
onChange={(v) => setSelectedRoomId(v || 0)}/> onChange={(v) => setSelectedRoomId(v || 0)}
/>
<Button <Button
style={{'width': '150px'}} style={{ width: "150px" }}
type="primary" type="primary"
loading={joiningLobby} loading={joiningLobby}
onClick={() => { onClick={async () => {
applyRoomId(selectedRoomId) await applyRoomId(selectedRoomId);
}}>Verander van lobby</Button> }}
>
Ga naar die lobby
</Button>
{joinLobbyError && {joinLobbyError && (
<span style={{color: red[4]}}>Die lobby bestaat niet</span> <span style={{ color: red[4] }}>
} Die lobby bestaat niet
</span>
)}
</Col> </Col>
</Row> </Row>
<Row justify="center"> <Row justify="center">
<span className={'lobby-options-or'}>of</span> <span className={"lobby-options-or"}>of</span>
</Row> </Row>
<Row justify="center"> <Row justify="center">
<Col> <Col>
<Button type="primary" <Button
type="primary"
onClick={() => { onClick={() => {
handleJoinRandomLobby() handleJoinRandomLobby();
}}>Join een willekeurige lobby</Button> }}
>
Join een nieuwe lobby
</Button>
</Col> </Col>
</Row> </Row>
</Card> </Card>
</Col> </Col>
</Row> </Row>
} )}
<img src={haramlogo} className="haram-logo" alt="haramlogo"/>
<img src={logo} className="via-logo" alt="logo" />
</div> </div>
); );
}; };

@ -1,41 +1,47 @@
import React from 'react'; import { Progress } from "antd";
import {Col, Progress} from "antd" import { calculateRoomTime, useTimeline } from "../lib/Connection";
import {roomTime, useTimeline} from "../lib/Connection";
import { useUpdateAfterDelay } from "../util/hooks"; import { useUpdateAfterDelay } from "../util/hooks";
const NextShot = () => { const NextShot = () => {
const timeline = useTimeline() const timeline = useTimeline();
useUpdateAfterDelay(1000);
useUpdateAfterDelay(1000) if (!timeline) {
throw new TypeError("NextShot without timeline");
}
let remainingTime = 0; let remainingTime = 0;
let remainingPercentage = 0; let remainingPercentage = 0;
if (timeline) { const currentRoomTime = calculateRoomTime();
const time = roomTime();
const [current, next] = timeline.itemAtTime(time, 'shot');
if (current && next) { const nextItem = timeline.itemAfterTime(currentRoomTime, "shot");
let currentTime = time - current.timestamp * 1000
let nextTime = next.timestamp * 1000 - current.timestamp * 1000;
remainingTime = Math.round((nextTime - currentTime) / 1000) if (nextItem) {
remainingPercentage = 100 - (currentTime / (nextTime || 1)) * 100; const prevShotRoomTime =
} (timeline.itemBeforeTime(currentRoomTime, "shot")?.timestamp || 0) * 1000;
const nextShotRoomTime = nextItem?.timestamp * 1000;
const totalRoomTimeBetweenShots = nextShotRoomTime - prevShotRoomTime;
const roomTimeSinceLastShot = currentRoomTime - prevShotRoomTime;
remainingTime = Math.round((nextShotRoomTime - currentRoomTime) / 1000);
remainingPercentage =
100 - (roomTimeSinceLastShot / totalRoomTimeBetweenShots) * 100;
} }
return ( return (
<Col className="sider" span={24} md={4}> <>
<h1>Tijd tot volgende shot:</h1> <h1>Tijd tot volgende shot:</h1>
<Progress type="circle" <Progress
type="circle"
percent={remainingPercentage} percent={remainingPercentage}
format={_ => remainingTime + ' sec.'} format={() => `${remainingTime} sec.`}
strokeColor={"#304ba3"} strokeColor={"#304ba3"}
strokeWidth={10} strokeWidth={10}
status="normal"/> status="normal"
</Col> />
</>
); );
}; };

@ -1,106 +1,211 @@
import {roomTime, useRoomRunningAndReadyChanged, useRoomTime, useTimelineSongFileChanged} from "../lib/Connection"; import connection, {
import React, {createRef, SyntheticEvent, useRef, useState} from "react"; calculateRoomTime,
useRoomRunningAndReadyChanged,
useTimelineSongFileChanged,
} from "../lib/Connection";
import { SyntheticEvent, useEffect, useRef, useState } from "react";
import '../css/player.sass' import { Button, Slider } from "antd";
import { SoundFilled, SoundOutlined } from "@ant-design/icons";
import "../css/player.css";
import { Room } from "../types/types"; import { Room } from "../types/types";
import {parse as parseQueryString} from "query-string";
const Player = () => { const Player = () => {
const room = useRoomRunningAndReadyChanged(); const room = useRoomRunningAndReadyChanged();
const _ = useRoomTime()
const timeline = useTimelineSongFileChanged(); const timeline = useTimelineSongFileChanged();
let player = useRef<HTMLAudioElement>(null) const player = useRef<HTMLAudioElement>(null);
const defaultVolume = parseInt(localStorage.getItem("volume") ?? "100");
const [volume, setVolume] = useState(defaultVolume);
const [muted, setMuted] = useState(false);
const [finishedLoading, setFinishedLoading] = useState(false);
const [timesSeeked, setTimesSeeked] = useState(0); const [timesSeeked, setTimesSeeked] = useState(0);
const [hadError, setHadError] = useState(false); const [hadError, setHadError] = useState(false);
// If our time synchronisation algorithm thing thinks the time is off by more // If our time synchronisation algorithm thing thinks the time is off by more
// than this value, we seek the running player to correct it. // than this value, we seek the running player to correct it.
const diffSecondsRequiredToSeekRunningPlayer = 0.20; const diffSecondsRequiredToSeekRunningPlayer = 0.2;
// Hard cap we are allowed to seek this player. Some browsers are slow or inaccurate // Hard cap we are allowed to seek this player. Some browsers are slow or inaccurate
// and will always be off. To avoid endless skipping of the song this cap stops seeking the // and will always be off. To avoid endless skipping of the song this cap stops seeking the
// player. // player.
const maxTimesSeekAllow = 25; const maxTimesSeekAllow = 25;
const query = parseQueryString(window.location.search); useEffect(() => {
if (query.nosound) { // Need to use an effect since 'player' will only contain a reference after first render.
return null;
if (!timeline) {
throw new Error("Player without active timeline.");
} }
if (player.current && player.current.dataset.src != timeline!!.songFile) { if (!player.current) {
player.current.dataset.src = timeline!!.songFile; throw new Error("No player after mount.");
player.current.src = timeline!!.songFile;
} }
player.current.src = timeline.songFile;
}, [timeline]);
function handlePlayerOnPlay(e: SyntheticEvent) { function handlePlayerOnPlay(e: SyntheticEvent) {
e.preventDefault(); e.preventDefault();
// For when the user manually started the player for when autoplay is off. // For when the user manually started the player for when autoplay is off.
setHadError(false); setHadError(false);
if (shouldPlay()) { }
startPlaying(true)
async function handlePlayerPause(e: SyntheticEvent) {
if (!shouldPlay()) {
// We should not be playing, pausing is fine.
console.log("should not play, paused");
return;
}
e.preventDefault();
if (room) {
setPlayerTime(room, true);
}
await player.current?.play();
}
function handlePlayerCanPlayThrough() {
if (!finishedLoading) {
setFinishedLoading(true);
connection.requestStart();
} }
} }
function shouldPlay() { function shouldPlay() {
return player.current && timeline && room && room.running && room.readyToParticipate return (
player.current &&
timeline &&
room &&
room.running &&
room.readyToParticipate &&
!player.current.ended
);
} }
function startPlaying(manual: boolean) { async function startPlaying(manual: boolean) {
if (!player.current) return; if (!player.current) {
return;
}
if (player.current.paused && !hadError) { if (player.current.paused && !hadError) {
player.current.play().then(() => { setPlayerVolume(volume);
try {
await player.current.play();
setHadError(false); setHadError(false);
}).catch(e => { } catch (e) {
console.error('Error playing', e); console.error("Error playing", e);
setHadError(true); setHadError(true);
}) }
} }
if (!hadError) { if (!hadError && room) {
setPlayerTime(room!!, manual); setPlayerTime(room, manual);
} }
} }
function setPlayerTime(room: Room, manualAdjustment: boolean) { function setPlayerTime(room: Room, manualAdjustment: boolean) {
if (!player.current) return; if (!player.current) {
return;
}
// Player's currentTime is in seconds, not ms.
const targetTime = calculateRoomTime() / 1000;
const diff = Math.abs(player.current.currentTime - targetTime);
// console.log('PLAYER DIFF', diff,
// 'min req to seek: ', diffSecondsRequiredToSeekRunningPlayer,
// `(${timesSeeked} / ${maxTimesSeekAllow})`);
if (diff <= diffSecondsRequiredToSeekRunningPlayer) {
return;
}
let targetTime = roomTime() / 1000; if (timesSeeked >= maxTimesSeekAllow && !manualAdjustment) {
let diff = player.current.currentTime - targetTime; // If we are adjusting manually we always allow a seek.
console.warn(
"The running player is off, but we've changed the time " +
"too often, skipping synchronizing the player."
);
return;
}
if (player.current && Math.abs(diff) > diffSecondsRequiredToSeekRunningPlayer) {
if (room.speedFactor != 1 || manualAdjustment || timesSeeked < maxTimesSeekAllow) {
player.current.currentTime = targetTime; player.current.currentTime = targetTime;
player.current.playbackRate = Math.max(Math.min(4.0, room.speedFactor), 0.25); player.current.playbackRate = Math.min(room.speedFactor, 5);
if (!manualAdjustment) { if (!manualAdjustment) {
setTimesSeeked(timesSeeked + 1); setTimesSeeked(timesSeeked + 1);
} }
} else {
console.warn('The running player is off, but we\'ve changed the time ' + console.log(
'too often, skipping synchronizing the player.'); `Player seeked: diff: ${diff}, target: ${targetTime}, (${timesSeeked} / ${maxTimesSeekAllow})`
);
} }
function toggleMute() {
if (player.current != null) {
player.current.muted = !player.current.muted;
setMuted(!muted);
}
}
function changeVolume(sliderValue: number) {
setVolume(sliderValue);
localStorage["volume"] = sliderValue;
setPlayerVolume(sliderValue);
}
function setPlayerVolume(value: number) {
if (player.current) {
player.current.volume =
value === 0.0 ? 0.0 : Math.pow(10, (value / 100 - 1) * 2);
} }
} }
if (shouldPlay()) { if (shouldPlay()) {
startPlaying(false) startPlaying(false)
.then(() => {
//
})
.catch((e) => {
console.error(e);
});
} else { } else {
if (player.current) { if (player.current) {
player.current.pause(); player.current.pause();
} }
} }
function render() {
return ( return (
<audio ref={player} className='player' hidden={!hadError} controls={true} onPlay={handlePlayerOnPlay}/> <div id="audio">
) <audio
} ref={player}
className="player"
return render(); controls={true}
} loop={false}
hidden={!hadError}
onPause={handlePlayerPause}
onPlay={handlePlayerOnPlay}
onCanPlayThrough={handlePlayerCanPlayThrough}
/>
<div id="volume-control">
<Button onClick={toggleMute} shape="circle">
{muted ? <SoundOutlined /> : <SoundFilled />}
</Button>
<Slider
id="volume-slider"
defaultValue={volume}
onChange={changeVolume}
trackStyle={{ backgroundColor: "var(--secondary-color)" }}
/>
</div>
</div>
);
};
export default Player; export default Player;

@ -1,33 +1,40 @@
import React from 'react'; import { Progress } from "antd";
import {Col, Progress} from "antd" import { calculateRoomTime, useTimeline } from "../lib/Connection";
import {roomTime, useTimeline} from "../lib/Connection";
import { useUpdateAfterDelay } from "../util/hooks"; import { useUpdateAfterDelay } from "../util/hooks";
const ShotsTaken = () => { const ShotsTaken = () => {
let timeline = useTimeline(); const timeline = useTimeline();
useUpdateAfterDelay(1000); useUpdateAfterDelay(1000);
if (!timeline) {
throw new TypeError("ShotsTaken without timeline");
}
const totalShots = timeline.getTotalShotCount();
let taken = 0; let taken = 0;
if (timeline) { const time = calculateRoomTime();
let [current, _] = timeline.eventAtTime(roomTime(), 'shot'); const prevShot = timeline.eventBeforeTime(time, "shot");
if (current) {
taken = current.shotCount!!; if (prevShot) {
} taken = prevShot.shotCount;
} else {
const nextShot = timeline.eventAfterTime(time, "shot");
taken = nextShot ? nextShot.shotCount - 1 : taken;
} }
return ( return (
<Col className="sider" span={24} md={4}> <>
<h1>Shots genomen:</h1> <h1>Shots genomen:</h1>
<Progress type="circle" <Progress
percent={taken} type="circle"
format={_ => taken + ' / 100'} percent={(taken / totalShots) * 100}
format={() => `${taken} / ${totalShots}`}
status="normal" status="normal"
strokeColor={"#304ba3"} strokeColor={"#304ba3"}
strokeWidth={10}/> strokeWidth={10}
</Col> />
</>
); );
}; };

@ -0,0 +1,133 @@
.feed {
#audio {
padding-top: 15px;
padding-bottom: 15px;
padding: 15px auto;
}
&.feed-shot-shake {
animation: shake-out 0.5s ease-out;
}
@keyframes shake-out {
0%,
20% {
transform: translate3d(-4px, 0, 0);
}
10%,
30% {
transform: translate3d(4px, 0, 0);
}
40% {
transform: translate3d(-3px, 0, 0);
}
50% {
transform: translate3d(3px, 0, 0);
}
60% {
transform: translate3d(-2px, 0, 0);
}
70% {
transform: translate3d(2px, 0, 0);
}
80% {
transform: translate3d(-1px, 0, 0);
}
90% {
transform: translate3d(1px, 0, 0);
}
100% {
transform: translate3d(0px, 0, 0);
}
}
.fade-enter {
opacity: 1;
transform: translateY(-30%);
}
.fade-enter-active {
transition: opacity 0.3s ease-out, transform 0.3s ease-out;
opacity: 1;
transform: translateY(0);
}
.fade-leave {
opacity: 1;
}
.fade-leave.fade-leave-active {
transition: opacity 0.3s ease-out;
opacity: 0;
}
}
.feed-reverse {
display: flex;
flex-direction: column-reverse;
}
.feed-item {
border: 1px solid #efefef;
border-radius: 2px;
padding: 0.35em;
margin: 0.25em;
text-align: center;
font-size: 1.1rem;
font-weight: bold;
color: rgba(0, 0, 0, 0.85);
box-shadow: 0 0 10px -4px rgba(48, 75, 163, 0.2);
@media (min-width: 992px) {
font-size: 1.5rem;
}
}
.feed-item__emoji > img {
max-width: 100%;
max-height: 40px;
}
:root {
--ticker-height: 3em;
--feed-height: calc(100% - var(--ticker-height));
}
.feed-items {
overflow: hidden;
@media (min-width: 992px) {
height: var(--feed-height);
padding-bottom: var(--ticker-height);
}
}
.ticker-container {
display: none;
width: 100%;
height: var(--ticker-height);
overflow: hidden;
@media (min-width: 992px) {
display: block;
}
.ticker-outer {
width: 100%;
height: 100%;
position: relative;
}
.ticker-message-button {
position: absolute;
left: 0;
bottom: 0;
background: white;
}
.ticker-item {
font-size: 2em;
margin-right: 2em;
}
}

@ -1,49 +0,0 @@
.feed-item
display: flex
border: 1px solid #efefef
border-radius: 4px
padding: 10px
margin: 10px
text-align: center
font-size: 16pt
font-weight: bold
color: rgba(0, 0, 0, 0.85)
box-shadow: 0 0 10px -4px rgba(48, 75, 163, 0.2)
.feed-item__title
width: 45%
.feed-item__desc
width: 45%
.feed-item__emoji
width: 10%
img
width: 60%
max-width: 100%
max-height: 100%
.feed-reverse
display: flex
flex-direction: column-reverse
// Animations
.fade-enter
opacity: 0.01
max-height: 0
.fade-enter.fade-enter-active
opacity: 1
max-height: 1000px
transition: opacity 500ms ease-in, max-height 500ms ease-in
.fade-leave
opacity: 1
max-height: 1000px
.fade-leave.fade-leave-active
opacity: 0.01
max-height: 0
transition: opacity 300ms ease-in, max-height 300ms ease-in

@ -0,0 +1,59 @@
@import 'antd/es/style/index';
@font-face {
font-family: 'SourceSansPro';
src: local('SourceSansPro'), url('./SourceSansPro.otf') format('opentype');
}
:root {
--primary-color: #304ba3;
--secondary-color: #ae2573;
}
body {
font-family: "SourceSansPro", sans-serif !important;
background-color: white;
}
#root {
height: 100%;
}
.feed {
height: 100%;
overflow: hidden;
padding: 0.5em;
}
.content {
padding: 1rem;
height: 100%;
}
.via-logo {
z-index: -10000;
position: fixed;
right: 0;
bottom: 0;
width: auto;
height: 5em;
padding: 10px;
}
.haram-logo {
z-index: -10000
position: fixed;
left: 0;
bottom: 0;
width: auto;
height: 10em;
padding: 10px;
}
.sider {
text-align: center;
}
h1 {
min-height: 3em;
}

@ -1,57 +0,0 @@
@import '~antd/dist/antd.css'
@font-face
font-family: 'SourceSansPro'
src: local('SourceSansPro'), url('./SourceSansPro.otf') format('opentype')
$footer-height: 4.5em
$content-height: calc(100vh - #{$footer-height})
body
font-family: 'SourceSansPro', sans-serif
background-color: white
.feed
max-height: $content-height
overflow: hidden
padding: 0.5em
.content
padding: 1rem
height: $content-height
.via-logo
z-index: -10000
position: fixed
right: 0
bottom: 0
width: auto
height: 5em
padding: 10px
.haram-logo
z-index: -10000
position: fixed
left: 0
bottom: 0
width: auto
height: 10em
padding: 10px
.sider
text-align: center
h1
min-height: 3em
footer
padding: 0.25em 0.5em
height: 4.5em

@ -0,0 +1,76 @@
.lobby {
.centurion-title {
text-align: center;
font-size: 2.5rem;
min-height: inherit;
.text {
padding: 0 2rem;
}
.beer {
animation-fill-mode: forwards;
max-width: 120%;
max-height: 120%;
&.connecting {
animation: spin 0.4s ease-in-out 25;
}
@keyframes spin {
100% {
transform: rotate(-360deg);
}
}
}
}
.beer-flipped {
transform: scaleX(-1);
}
.lobby-connecting {
margin: 2em 0 0 0;
text-align: center;
}
.hints {
margin: 1rem 0 0 0;
font-size: 1.5rem;
text-align: center;
}
.control {
font-size: 1.3rem;
margin: 2em 1em 2em 1em;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
}
.lobby-options-or {
display: inline-block;
position: relative;
margin: 16px 0;
color: #888;
&:before,
&:after {
content: "";
width: 1px;
height: 6px;
position: absolute;
left: 50%;
margin-left: 0.1px;
background: #bbb;
}
&:before {
bottom: 100%;
}
&:after {
top: 100%;
}
}
}

@ -1,62 +0,0 @@
.lobby
.centurion-title
text-align: center
font-size: 2.5rem
min-height: inherit
.text
padding: 0 2rem
.beer
animation-fill-mode: forwards
&.connecting
animation: spin 0.4s ease-in-out 25
@keyframes spin
100%
transform: rotate(-360deg)
.beer-flipped
transform: scaleX(-1)
display: inline-block
.lobby-connecting
margin: 2em 0 0 0
text-align: center
.hints
margin: 1rem 0 0 0
font-size: 1.5rem
text-align: center
.lobby-info
text-align: center
.control
font-size: 1.3rem
margin: 2em 1em 2em 1em
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)
.lobby-options-or
display: inline-block
position: relative
margin: 16px 0
color: #888
&:before, &:after
content: ''
width: 1px
height: 6px
position: absolute
left: 50%
margin-left: 0.1px
background: #bbb
&:before
bottom: 100%
&:after
top: 100%

@ -0,0 +1,16 @@
#volume-control {
display: flex;
flex-direction: row;
justify-content: center;
}
#volume-control .ant-slider {
width: 65%;
padding-left: 10px;
}
.player {
position: fixed;
left: 0;
bottom: 0;
}

@ -1,4 +0,0 @@
.player
position: fixed
left: 0
bottom: 0

@ -1,7 +1,6 @@
import React from 'react'; import ReactDOM from "react-dom";
import ReactDOM from 'react-dom'; import "./css/index.css";
import './css/index.sass';
import App from './components/App'; import Centurion from "./components/Centurion";
ReactDOM.render(<App/>, document.getElementById('root')); ReactDOM.render(<Centurion />, document.getElementById("root"));

@ -1,14 +1,12 @@
import io from "socket.io-client"; import io, { Socket } from "socket.io-client";
import {useEffect, useState} from "react";
import {parse as parseQueryString, stringify as stringifyQueryString} from 'query-string';
import { Config, Room, RoomOptions, Timeline } from "../types/types"; import { Config, Room, RoomOptions, Timeline } from "../types/types";
import { Sub, useSub } from "../util/sub"; import { Sub, useSub } from "../util/sub";
class Connection { class Connection {
url = '/'; url = "/";
socket: SocketIOClient.Socket; socket: Socket;
isConnected = new Sub<boolean>(); isConnected = new Sub<boolean>();
@ -22,14 +20,12 @@ class Connection {
timeSyncTooOld = 120000; timeSyncTooOld = 120000;
roomTime = new Sub<number>(); roomTime = new Sub<number>();
calls: { [id: number]: Call } = {};
constructor() { constructor() {
this.isConnected.set(false); this.isConnected.set(false);
this.socket = io(this.url, { this.socket = io(this.url, {
autoConnect: false, autoConnect: false,
transports: ['websocket'] transports: ["websocket"],
}); });
this.setupSocketListeners(); this.setupSocketListeners();
@ -44,25 +40,25 @@ class Connection {
} }
setupSocketListeners() { setupSocketListeners() {
this.socket.on('connect', () => { this.socket.on("connect", async () => {
this.isConnected.set(true); this.isConnected.set(true);
this.onConnect(); await this.onConnect();
}) });
this.socket.on('disconnect', () => { this.socket.on("disconnect", () => {
this.isConnected.set(false); this.isConnected.set(false);
this.onDisconnect(); this.onDisconnect();
}) });
this.socket.on('config', (data: any) => { this.socket.on("config", (data: any) => {
this.config.set(data.config); this.config.set(data.config);
}) });
this.socket.on('time_sync', (data: any) => { this.socket.on("time_sync", (data: any) => {
this.timeSyncResponse(data.requestId, data.clientDiff, data.serverTime); this.timeSyncResponse(data.requestId, data.clientDiff, data.serverTime);
}) });
this.socket.on('room', (data: any) => { this.socket.on("room", (data: any) => {
if (data.room) { if (data.room) {
this.setQueryLobbyId(data.room.id); this.setQueryLobbyId(data.room.id);
} }
@ -70,38 +66,25 @@ class Connection {
this.room.set(data.room); this.room.set(data.room);
}); });
this.socket.on('timeline', (data: any) => { this.socket.on("timeline", (data: any) => {
if (data.timeline) { if (data.timeline) {
this.timeline.set(new Timeline(data.timeline)); this.timeline.set(new Timeline(data.timeline));
} else { } else {
this.timeline.set(null); this.timeline.set(null);
} }
}); });
this.socket.on('call_response', (data: any) => {
let call = this.calls[data.id];
if (!call) return;
if (data.error) {
call.callback(data.error, null);
} else {
call.callback(null, data.response);
}
delete this.calls[data.id];
});
} }
onConnect() { async onConnect() {
this.startTimeSync(); this.startTimeSync();
let lobbyId = this.getQueryLobbyId(); const lobbyId = this.getQueryLobbyId();
if (lobbyId) { if (lobbyId) {
this.requestJoin(lobbyId).then(v => { const exists = await this.requestJoin(lobbyId);
if (!v) { if (!exists) {
this.setQueryLobbyId(null); this.setQueryLobbyId();
this.requestJoinRandom(); this.requestJoinRandom();
} }
})
} else { } else {
this.requestJoinRandom(); this.requestJoinRandom();
} }
@ -111,88 +94,91 @@ class Connection {
this.stopTimeSync(); this.stopTimeSync();
} }
autoStart() : boolean {
let query = parseQueryString(window.location.search);
return !!query.autostart;
}
private getQueryLobbyId(): number | null { private getQueryLobbyId(): number | null {
let query = parseQueryString(window.location.search); const query = new URLSearchParams(window.location.search);
if (query.lobby) { const lobby = query.get("lobby");
let lobbyId = Number.parseInt(query.lobby.toString());
if (Number.isSafeInteger(lobbyId) && lobbyId > 0) { if (!lobby) {
return lobbyId return null;
}
} }
const lobbyId = Number.parseInt(query.get("lobby")!.toString());
if (!Number.isSafeInteger(lobbyId) || lobbyId < 1) {
return null; return null;
} }
private setQueryLobbyId(lobbyId: number | null) { return lobbyId;
let query = parseQueryString(window.location.search); }
private setQueryLobbyId(lobbyId?: number) {
const newUrl = new URL(window.location.href);
if (lobbyId) { if (lobbyId) {
query.lobby = lobbyId.toString(); newUrl.searchParams.set("lobby", String(lobbyId));
} else { } else {
delete query.lobby; newUrl.searchParams.delete("lobby");
}
let newUrl = window.location.protocol + "//" + window.location.host +
window.location.pathname + (Object.keys(query).length ? ('?' + stringifyQueryString(query)) : '');
window.history.pushState({}, '', newUrl);
} }
async call(name: string, params: any) { window.history.pushState({}, "", newUrl.toString());
return new Promise<any>((resolve, reject) => {
let callback = (err: any, res: any) => {
if (err) {
return reject(err);
} }
resolve(res); setRoomOptions(roomOptions: RoomOptions) {
}; this.socket.emit("room_options", roomOptions);
let call = new Call(name, params, callback);
this.calls[call.id] = call;
this.socket.emit('call', call.id, name, params);
});
} }
setRoomOptions(roomOptions: RoomOptions) { requestStart() {
this.socket.emit('room_options', roomOptions) this.socket.emit("request_start");
} }
requestStart(seekTime: number) { submitTickerMessage(message: string) {
this.socket.emit('request_start', { return new Promise<void>((resolve, reject) => {
seekTime: seekTime this.socket.emit(
"submit_ticker_message",
message,
(_: null, err?: string) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
}); });
} }
async requestJoin(roomId: number): Promise<boolean> { async requestJoin(roomId: number): Promise<boolean> {
return this.call('room_exists', {roomId: roomId}).then(v => { return new Promise<boolean>((resolve, reject) => {
if (v) { this.socket.emit(
this.socket.emit('request_join', roomId, this.autoStart()); "request_join",
if (this.autoStart()) { roomId,
this.requestReady(); (err?: string, didJoinRoom?: boolean) => {
if (err) {
return reject(err);
} }
return true;
} else { return resolve(!!didJoinRoom);
return false;
} }
}) );
});
} }
requestReady() { requestSetReady() {
this.socket.emit('request_ready'); this.socket.emit("request_set_ready");
} }
requestJoinRandom() { requestJoinRandom() {
this.socket.emit('request_join_random'); this.socket.emit("request_join_random");
} }
startTimeSync() { startTimeSync() {
for (let i = 0; i < this.timeSyncIntervals.length; i++) { for (let i = 0; i < this.timeSyncIntervals.length; i++) {
let timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
this.sendTimeSync(i === this.timeSyncIntervals.length - 1); // Only reschedule the last sync interval (i.e. every 30 seconds)
const shouldReschedule = i === this.timeSyncIntervals.length - 1;
this.sendTimeSync(shouldReschedule);
}, this.timeSyncIntervals[i]); }, this.timeSyncIntervals[i]);
// @ts-ignore
this.timeSyncTimeoutIds.push(timeoutId); this.timeSyncTimeoutIds.push(timeoutId);
} }
} }
@ -204,8 +190,8 @@ class Connection {
} }
sendTimeSync(alsoSchedule: boolean) { sendTimeSync(alsoSchedule: boolean) {
let sync = new TimeSyncRequest(); const sync = new TimeSyncRequest();
this.socket.emit('time_sync', sync.requestId, Date.now()); this.socket.emit("time_sync", sync.requestId, Date.now());
this.timeSyncs[sync.requestId] = sync; this.timeSyncs[sync.requestId] = sync;
if (alsoSchedule) { if (alsoSchedule) {
@ -216,21 +202,21 @@ class Connection {
} }
timeSyncResponse(requestId: number, clientDiff: number, serverTime: number) { timeSyncResponse(requestId: number, clientDiff: number, serverTime: number) {
let syncReq = this.timeSyncs[requestId]; const syncReq = this.timeSyncs[requestId];
if (!syncReq) return if (!syncReq) {
delete this.timeSyncs[requestId]; return;
}
syncReq.response(clientDiff, serverTime); syncReq.response(clientDiff, serverTime);
for (let i in this.timeSyncs) { for (const i in this.timeSyncs) {
if (this.timeSyncs[i].start < Date.now() - this.timeSyncTooOld) { if (this.timeSyncs[i].start < Date.now() - this.timeSyncTooOld) {
delete this.timeSyncs[i]; delete this.timeSyncs[i];
break; break;
} }
} }
// console.log(this.timeSyncs);
// console.log('SERVER TIME', this.serverTimeOffset());
this.roomTime.set(roomTime()); this.roomTime.set(calculateRoomTime());
} }
serverTime(): number { serverTime(): number {
@ -240,8 +226,8 @@ class Connection {
serverTimeOffset(): number { serverTimeOffset(): number {
let num = 0; let num = 0;
let sum = 0; let sum = 0;
for (let i in this.timeSyncs) { for (const i in this.timeSyncs) {
let sync = this.timeSyncs[i]; const sync = this.timeSyncs[i];
if (!sync.ready) continue; if (!sync.ready) continue;
sum += sync.offset; sum += sync.offset;
num += 1; num += 1;
@ -255,28 +241,12 @@ class Connection {
} }
} }
let _callId = 0;
class Call {
id: number;
name: string;
params: any;
callback: (err: any, res: any) => any;
constructor(name: string, params: any, callback: (err: any, res: any) => void) {
this.name = name;
this.params = params;
this.id = _callId++;
this.callback = callback;
}
}
let _timeSyncId = 0; let _timeSyncId = 0;
class TimeSyncRequest { class TimeSyncRequest {
requestId: number; requestId: number;
start: number; start: number;
offset: number = 0; offset = 0;
ready = false; ready = false;
constructor() { constructor() {
@ -286,18 +256,16 @@ class TimeSyncRequest {
response(clientDiff: number, serverTime: number) { response(clientDiff: number, serverTime: number) {
this.ready = true; this.ready = true;
let now = Date.now(); const now = Date.now();
let lag = now - this.start; const lag = now - this.start;
this.offset = serverTime - now + lag / 2; this.offset = serverTime - now + lag / 2;
// console.log('TIME SYNC', 'cdiff:', clientDiff, 'lag:', // console.log('TIME SYNC', 'cdiff:', clientDiff, 'lag:',
// lag, 'diff:', serverTime - now, 'offset:', this.offset); // lag, 'diff:', serverTime - now, 'offset:', this.offset);
} }
} }
let connection: Connection = new Connection(); const connection: Connection = new Connection();
// @ts-ignore
window['connection'] = connection;
export default connection; export default connection;
export function useRoom(): Room | null { export function useRoom(): Room | null {
@ -309,7 +277,7 @@ export function useConfig(): Config | null {
} }
export function useRoomRunningAndReadyChanged(): Room | null { export function useRoomRunningAndReadyChanged(): Room | null {
return useSub(connection.room, (v) => [v && v.running && v.readyToParticipate]); return useSub(connection.room, (v) => [v, v?.running, v?.readyToParticipate]);
} }
export function useTimeline(): Timeline | null { export function useTimeline(): Timeline | null {
@ -317,19 +285,27 @@ export function useTimeline(): Timeline | null {
} }
export function useTimelineSongFileChanged(): Timeline | null { export function useTimelineSongFileChanged(): Timeline | null {
return useSub(connection.timeline, (v) => [v && v.songFile]); return useSub(connection.timeline, (v) => [v?.songFile]);
} }
export function useRoomTime(): number { export function useRoomTime(): number {
return useSub(connection.roomTime); return useSub(connection.roomTime) || 0;
}
/**
* Calculates the current room time, adjusted for any possible server time
* offset and lag.
*/
export function calculateRoomTime(): number {
const room = connection.room.get();
if (!room || typeof room.startTime === "undefined") {
return 0;
} }
export function roomTime(): number {
let room = connection.room.get();
if (!room) return 0;
return (connection.serverTime() - room.startTime) * room.speedFactor; return (connection.serverTime() - room.startTime) * room.speedFactor;
} }
export function useIsConnected(): boolean { export function useIsConnected(): boolean {
return useSub(connection.isConnected); return useSub(connection.isConnected) || false;
} }

@ -1 +0,0 @@
/// <reference types="react-scripts" />

@ -1,34 +1,24 @@
export interface Tick {
current: number,
next?: {
timestamp: number,
events: TimestampEvent[]
},
nextShot?: {
timestamp: number,
count: number
}
}
export interface Config { export interface Config {
availableTimelines: string[] availableTimelines: string[];
} }
export interface Room { export interface Room {
id: number, id: number;
userCount: number, userCount: number;
isLeader: boolean, isLeader: boolean;
running: boolean, running: boolean;
startTime: number, startTime?: number;
seekTime: number, seekTime: number;
timelineName: string, timelineName: string;
readyToParticipate: boolean, readyToParticipate: boolean;
speedFactor: number speedFactor: number;
ticker: string[];
users?: { id: string; readyToParticipate: boolean }[];
} }
export class RoomOptions { export class RoomOptions {
seekTime: number seekTime: number;
timelineName: string timelineName: string;
constructor(seekTime: number, timelineName: string) { constructor(seekTime: number, timelineName: string) {
this.seekTime = seekTime; this.seekTime = seekTime;
@ -37,42 +27,99 @@ export class RoomOptions {
} }
export class Timeline { export class Timeline {
name: string name: string;
songFile: string songFile: string;
feed: TimelineItem[] feed: TimelineItem[];
constructor(obj: any) { constructor(obj: any) {
this.name = obj.name; this.name = obj.name;
this.songFile = obj.songFile; this.songFile = obj.songFile;
this.feed = obj.feed; this.feed = obj.feed;
this.feed = this.feed.sort((a, b) => a.timestamp - b.timestamp);
// Add string ids to the feed to uniquely identify them in the various
// reactive components.
for (let i = 0; i < this.feed.length; i++) {
this.feed[i].id = i.toString();
for (let j = 0; j < this.feed[i].events.length; j++) {
this.feed[i].events[j].id = `${i}:${j}`;
}
}
}
getTotalShotCount(): number {
let maxShot = 0;
for (const item of this.feed) {
for (const event of item.events) {
if (event.type === "shot") {
maxShot = Math.max(
maxShot,
(event as TimestampEventShot).shotCount || 0
);
}
}
} }
itemAtTime(time: number, type: string = ''): [TimelineItem | null, TimelineItem | null] { return maxShot;
let feedToSearch = type ? this.feed.filter(i => i.events.some(j => j.type == type)) : this.feed; }
itemAfterTime(time: number, type?: EventType): TimelineItem | undefined {
const feedToSearch = type
? this.feed.filter((i) => i.events.some((j) => j.type === type))
: this.feed;
return feedToSearch.find((item) => item.timestamp * 1000 > time);
}
itemBeforeTime(time: number, type?: EventType): TimelineItem | undefined {
const feedToSearch = type
? this.feed.filter((i) => i.events.some((j) => j.type === type))
: this.feed;
return feedToSearch.reverse().find((item) => item.timestamp * 1000 < time);
}
eventAfterTime(time: number, type: "shot"): TimestampEventShot | undefined;
eventAfterTime(time: number, type: EventType): TimestampEvent | undefined {
const item = this.itemAfterTime(time, type);
for (let i = 1; i < feedToSearch.length; i++) { if (!item || !item.events.length) {
if (feedToSearch[i].timestamp * 1000 >= time) { return undefined;
return [feedToSearch[i - 1], feedToSearch[i]];
} }
return item.events.find((ev) => ev.type === type);
} }
return [feedToSearch[feedToSearch.length - 1], null]; eventBeforeTime(time: number, type: "shot"): TimestampEventShot | undefined;
eventBeforeTime(time: number, type: EventType): TimestampEvent | undefined {
const item = this.itemBeforeTime(time, type);
if (!item || !item.events.length) {
return undefined;
} }
eventAtTime(time: number, type: string = ''): [TimestampEvent | null, TimestampEvent | null] { return item.events.find((ev) => ev.type === type);
let [current, next] = this.itemAtTime(time, type)
return [current ? (current.events.find(i => i.type == type) || null) : null,
next ? (next.events.find(i => i.type == type) || null) : null]
} }
} }
export interface TimelineItem { export interface TimelineItem {
timestamp: number, id: string;
events: TimestampEvent[] timestamp: number;
events: TimestampEvent[];
}
export const EVENT_PRIORITY: EventType[] = ["shot", "talk", "time", "song"];
export type EventType = "talk" | "shot" | "song" | "time";
interface TimestampEventBase {
id: string;
type: EventType;
text: string[];
} }
export interface TimestampEvent { interface TimestampEventShot extends TimestampEventBase {
type: 'talk' | 'shot' | 'song' | 'time', type: "shot";
text: string[], shotCount: number;
shotCount?: number
} }
export type TimestampEvent = TimestampEventBase | TimestampEventShot;

@ -1,11 +1,34 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export function useUpdateAfterDelay(delay: number) { export function useUpdateAfterDelay(delay: number) {
const [_, timedUpdateSet] = useState(0); const [, timedUpdateSet] = useState(0);
useEffect(() => { useEffect(() => {
let timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
timedUpdateSet(v => v + 1); timedUpdateSet((v) => v + 1);
}, delay); }, delay);
return () => clearTimeout(timeoutId) return () => clearTimeout(timeoutId);
}) });
}
export function useResize() {
const [dimensions, setDimensions] = useState({
height: window.innerHeight,
width: window.innerWidth,
});
useEffect(() => {
const listener = (ev: UIEvent) => {
setDimensions({
height: window.innerHeight,
width: window.innerWidth,
});
};
window.addEventListener("resize", listener);
return () => {
window.removeEventListener("resize", listener);
};
});
return dimensions;
} }

@ -1,9 +1,11 @@
import { Socket } from "socket.io-client";
/** /**
* Promisify emit. * Promisify emit.
* @param event * @param event
* @param arg * @param arg
*/ */
export function emit(socket: SocketIOClient.Socket, event: string, arg: any = null) { export function emit(socket: Socket, event: string, arg: any = null) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const cb = (err: any, res: any) => { const cb = (err: any, res: any) => {
if (err) { if (err) {
@ -13,11 +15,10 @@ export function emit(socket: SocketIOClient.Socket, event: string, arg: any = nu
resolve(res); resolve(res);
}; };
if (arg === null || typeof arg === 'undefined') { if (arg === null || typeof arg === "undefined") {
socket.emit(event, cb); socket.emit(event, cb);
} else { } else {
socket.emit(event, arg, cb); socket.emit(event, arg, cb);
} }
});
})
} }

@ -2,7 +2,7 @@ import {useEffect, useState} from "react";
export class Sub<T> { export class Sub<T> {
_listeners: ((obj: T) => void)[] = []; _listeners: ((obj: T) => void)[] = [];
_current: any = null; _current: T | null = null;
subscribe(listener: any) { subscribe(listener: any) {
if (this._listeners.indexOf(listener) < 0) { if (this._listeners.indexOf(listener) < 0) {
@ -11,30 +11,39 @@ export class Sub<T> {
} }
unsubscribe(listener: any) { unsubscribe(listener: any) {
let index = this._listeners.indexOf(listener); const index = this._listeners.indexOf(listener);
if (index >= 0) { if (index >= 0) {
this._listeners.splice(index, 1); this._listeners.splice(index, 1);
} }
} }
get() { get(): T | null {
return this._current; return this._current;
} }
set(obj: T) { set(obj: T) {
this._current = obj; this._current = obj;
this._listeners.forEach(cb => cb(obj)); this._listeners.forEach((cb) => cb(obj));
} }
} }
export function useSub<T>(sub: Sub<T>, effectChanges: ((v: T) => any[]) | null = null): T { export function useSub<T>(
sub: Sub<T>,
effectChanges: ((v: T | null) => any[]) | null = null
): T | null {
const [currentState, stateSetter] = useState(sub.get()); const [currentState, stateSetter] = useState(sub.get());
useEffect(() => { useEffect(
let listener = (obj: T) => stateSetter(obj); () => {
const listener = (obj: T) => stateSetter(obj);
sub.subscribe(listener); sub.subscribe(listener);
return () => sub.unsubscribe(listener); return () => sub.unsubscribe(listener);
}, effectChanges ? effectChanges(currentState) : []); // This effect uses "sub" from outside of its scope, should it be a dependency?
// Ignore the lint warning for now.
// eslint-disable-next-line
},
effectChanges ? effectChanges(currentState) : []
);
return currentState; return currentState;
} }

@ -1,23 +1,21 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "ESNext",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom", "types": ["vite/client", "vite-plugin-svgr/client"],
"dom.iterable", "allowJs": false,
"esnext" "skipLibCheck": false,
], "esModuleInterop": false,
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react" "jsx": "react-jsx"
}, },
"include": [ "include": [
"src" "src"

@ -0,0 +1,46 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
import vitePluginImp from "vite-plugin-imp";
// https://vitejs.dev/config/
export default defineConfig({
// This changes the out put dir from dist to build
// comment this out if that isn't relevant for your project
build: {
outDir: "build",
},
plugins: [
react(),
svgr(),
vitePluginImp({
libList: [
{
libName: "antd",
style: (name) => `antd/es/${name}/style`,
},
],
}),
],
server: {
proxy: {
"/socket.io": {
target: "http://localhost:3001",
ws: true,
},
},
},
resolve: {
alias: [{ find: /^~/, replacement: "" }],
},
css: {
preprocessorOptions: {
less: {
modifyVars: {
"primary-color": "#304ba3",
},
javascriptEnabled: true,
},
},
},
});

File diff suppressed because it is too large Load Diff

11772
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,44 @@
{
"name": "centurion",
"version": "1.0.0",
"description": "Centurion: Honderd minuten... Honderd shots... Kan jij het aan?",
"engines": {
"node": ">=16",
"npm": ">=7"
},
"workspaces": [
"frontend",
"backend"
],
"scripts": {
"fmt": "prettier -w '**/*.{ts,tsx}'",
"check-fmt": "prettier --check '**/*.{ts,tsx}'",
"prepare": "husky install"
},
"repository": {
"type": "git",
"url": "git@gitlab.com:studieverenigingvia/ict/centurion.git"
},
"author": "Vereniging Informatiewetenschappen Amsterdam (V.I.A.)",
"license": "MIT",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"eslint": "^8.4.1",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-react": "^7.27.1",
"eslint-plugin-react-hooks": "^4.3.0",
"husky": "^7.0.4",
"lint-staged": "^12.1.2",
"prettier": "^2.5.1",
"typescript": "^4.5.2"
},
"lint-staged": {
"backend/**/*.{ts,tsx}": "eslint --fix",
"frontend/**/*.{ts,tsx}": "eslint --fix",
"*.{ts,tsx}": [
"prettier -w"
]
}
}
Loading…
Cancel
Save