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. 215
      backend/src/Room.ts
  7. 115
      backend/src/Service.ts
  8. 109
      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. 44
      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. 288
      frontend/src/components/Lobby.tsx
  34. 50
      frontend/src/components/NextShot.tsx
  35. 189
      frontend/src/components/Player.tsx
  36. 43
      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. 236
      frontend/src/lib/Connection.ts
  47. 1
      frontend/src/react-app-env.d.ts
  48. 133
      frontend/src/types/types.ts
  49. 35
      frontend/src/util/hooks.ts
  50. 9
      frontend/src/util/socket.ts
  51. 27
      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
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
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
COPY package.json yarn.lock ./
RUN yarn install
# Install TS manually since it's only included in the parent's package.json
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 src src/
COPY data data/
CMD ["npm", "run", "app"]
RUN npm run build
CMD ["npm", "run", "start-compiled"]

File diff suppressed because it is too large Load Diff

@ -5,18 +5,21 @@
"main": "./src/index.js",
"scripts": {
"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": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"socket.io": "^2.3.0"
"socket.io": "^4.4.0"
},
"devDependencies": {
"@types/express": "^4.17.6",
"@types/socket.io": "^2.1.4",
"ts-node": "^8.8.2",
"typescript": "^3.8.3"
"@types/express": "^4.17.13",
"@types/node": "^16.11.12",
"ts-node": "^10.4.0"
}
}

@ -1,77 +1,96 @@
import {Socket} from "socket.io";
import User from "./User";
import {getIndex, getNextShot, getTimeline, getTimelineNames, indexForTime} from "./timeline";
import {getCurrentTime} from "./util";
import { getTimeline, getTimelineNames } from "./timeline";
import { getCurrentTime } from "./util";
export interface RoomOptions {
seekTime: number
timelineName: string
seekTime: number;
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 {
id: number = 0;
id = 0;
users: User[] = [];
leader: User | null = null;
ticker: TickerMessage[] = [];
running = false;
startTime = 0;
currentSeconds = 0;
timelineIndex: number = 0;
startTime: number | undefined = undefined;
seekTime: number = 0;
timelineName: string = 'Centurion';
seekTime = 0;
timelineName = "Centurion";
// For debugging purposes
speedFactor = 1;
autoStart = false;
constructor(name: number) {
this.id = name;
}
serialize(user: User) {
return {
'id': this.id,
'userCount': this.users.length,
'isLeader': this.leader == user,
'running': this.running,
'startTime': this.startTime,
'timelineName': this.timelineName,
'seekTime': this.seekTime,
'readyToParticipate': user.readyToParticipate || this.leader == user,
'speedFactor': this.speedFactor
serialize(user?: User) {
const obj: SerializedRoom = {
id: this.id,
userCount: this.users.length,
isLeader: this.leader === user,
running: this.running,
startTime: this.startTime,
timelineName: this.timelineName,
seekTime: this.seekTime,
readyToParticipate: this.getLeader()?.readyToParticipate || false,
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);
}
sync() {
this.users.forEach(u => u.sync());
this.users.forEach((u) => u.sync());
}
join(user: User) {
async join(user: User) {
this.users.push(user);
user.setRoom(this);
await user.setRoom(this);
if (!this.hasLeader()) {
this.setLeader(user);
}
if (this.autoStart) {
this.seekTime = 2500000;
this.running = true;
this.start();
}
this.sync();
}
leave(user: User) {
async leave(user: User) {
this.removeTickerMessageForUser(user);
this.users.splice(this.users.indexOf(user), 1);
user.setRoom(null);
await user.setRoom(null);
if (this.leader == user) {
this.setRandomLeader();
@ -80,91 +99,20 @@ export default class Room {
this.sync();
}
onBeforeDelete() {
}
setOptions(options: any) {
this.seekTime = Math.max(0, Math.min(options.seekTime, 250 * 60 * 1000))
setOptions(options: { seekTime: number; timelineName: string }) {
this.seekTime = Math.max(0, Math.min(options.seekTime, 250 * 60 * 1000));
if (getTimelineNames().indexOf(options.timelineName) >= 0) {
this.timelineName = options.timelineName;
}
this.sync()
this.sync();
}
start() {
this.running = true;
this.startTime = getCurrentTime() - this.seekTime
this.startTime = getCurrentTime() - this.seekTime;
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}
@ -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 {
return this.leader != null;
}
@ -207,4 +138,30 @@ export default class Room {
getLeader(): User | null {
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 Room, {RoomOptions} from './Room'
import {getCurrentTime, randomInt} from "./util";
import User from "./User";
import Room, { RoomOptions } from "./Room";
import { getCurrentTime } from "./util";
export default class Service {
private roomIdToRooms = new Map<number, Room>();
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) {
let user = new User(socket);
const user = new User(socket);
this.socketsToUsers.set(socket.id, user);
user.sync();
}
onSocketDisconnect(socket: Socket) {
let user = this.getUser(socket);
async onSocketDisconnect(socket: Socket) {
const user = this.getUser(socket);
if (user.room != null) {
user.room.leave(user);
await user.room.leave(user);
}
this.deleteEmptyRooms();
user.onDisconnect();
}
onTimeSync(socket: Socket, requestId: number, clientTime: number) {
let user = this.getUser(socket);
const user = this.getUser(socket);
let now = getCurrentTime();
user.emit('time_sync', {
'requestId': requestId,
'clientDiff': now - clientTime,
'serverTime': now
})
const now = getCurrentTime();
user.emit("time_sync", {
requestId: requestId,
clientDiff: now - clientTime,
serverTime: now,
});
}
onSetRoomOptions(socket: Socket, options: RoomOptions) {
let user = this.getUser(socket);
const user = this.getUser(socket);
if (user.room?.getLeader() == user) {
user.room!!.setOptions(options)
user.room.setOptions(options);
}
}
onRequestStart(socket: Socket) {
let user = this.getUser(socket);
const user = this.getUser(socket);
if (user.room?.getLeader() == user) {
user.room!!.start();
if (user.room?.getLeader() === user) {
user.room.start();
user.room.sync();
}
}
onRequestJoin(socket: Socket, roomId: number): boolean {
let user = this.getUser(socket);
async onRequestJoin(socket: Socket, roomId: number) {
const user = this.getUser(socket);
if (user.room && user.room.id == roomId) return false;
if (user.room) {
user.room.leave(user);
await user.room.leave(user);
this.deleteEmptyRooms();
}
@ -66,46 +74,60 @@ export default class Service {
this.createRoomWithId(roomId);
}
let room = this.roomIdToRooms.get(roomId)!!;
room.join(user);
const room = this.roomIdToRooms.get(roomId);
if (!room) {
return false;
}
await room.join(user);
return true;
}
onRequestReady(socket: Socket) {
let user = this.getUser(socket);
onRequestSetReady(socket: Socket) {
const user = this.getUser(socket);
if (!user.room || user.readyToParticipate) return;
user.readyToParticipate = true;
user.sync();
user.room.sync();
}
onRequestJoinRandom(socket: Socket) {
let user = this.getUser(socket);
async onRequestJoinRandom(socket: Socket) {
const user = this.getUser(socket);
if (user.room) {
user.room.leave(user);
await user.room.leave(user);
this.deleteEmptyRooms();
}
const room = this.createRandomRoom();
if (!room) throw Error('Too many rooms active');
room.join(user);
if (!room) throw Error("Too many rooms active");
await room.join(user);
}
hasRoomId(roomId: number): boolean {
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 {
let user = this.socketsToUsers.get(socket.id);
const user = this.socketsToUsers.get(socket.id);
if (!user) {
throw new Error('User not found');
throw new Error("User not found");
}
return user;
}
private deleteEmptyRooms() {
for (let room of this.roomIdToRooms.values()) {
for (const room of this.roomIdToRooms.values()) {
if (room.users.length == 0) {
this.deleteRoom(room);
}
@ -114,27 +136,28 @@ export default class Service {
private createRandomRoom(): Room | null {
let tries = 0;
let i = 1;
while (tries++ < 1000) {
const randomId = randomInt(100, Math.max(1000, this.roomIdToRooms.size * 2));
if (this.roomIdToRooms.has(randomId)) continue;
return this.createRoomWithId(randomId);
if (this.roomIdToRooms.has(i)) {
i++;
} else {
return this.createRoomWithId(i);
}
}
return null;
}
private createRoomWithId(roomId: number): Room {
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);
return 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 {getTimelineNames} from "./timeline";
import Room, { SerializedRoom } from "./Room";
import { getTimelineNames } from "./timeline";
export interface Config {
availableTimelines: string[];
}
export default class User {
socket: Socket;
id: string;
room: Room | null = null;
readyToParticipate: boolean = false;
readyToParticipate = false;
constructor(socket: Socket) {
this.socket = socket;
this.id = socket.id;
}
onDisconnect() {
if (this.room != null) {
}
serialize() {
return {
id: this.id,
readyToParticipate: this.readyToParticipate,
};
}
setRoom(room: Room | null) {
async setRoom(room: Room | null) {
if (this.room === room) return;
if (this.room != null) {
this.socket.leave(this.room.id.toString());
if (this.room !== null) {
await this.socket.leave(this.room.id.toString());
this.readyToParticipate = false;
}
this.room = room;
if (this.room != null) {
this.socket.join(this.room.id.toString());
if (this.room !== null) {
await this.socket.join(this.room.id.toString());
}
this.sync();
@ -40,69 +45,87 @@ export default class User {
getConfig() {
return {
'availableTimelines': getTimelineNames()
}
availableTimelines: getTimelineNames(),
};
}
sentConfig: any = null;
sentRoom: any = null;
sentConfig: Config | null = null;
sentRoom: SerializedRoom | null = null;
sentTimelineName: string | null = null;
sync() {
// Config
let config = this.getConfig();
const config = this.getConfig();
if (!this.syncEquals(this.sentConfig, config)) {
this.sentConfig = config;
this.emit('config', {
'config': this.sentConfig
this.sentConfig = Object.assign({}, config);
this.emit("config", {
config: this.sentConfig,
});
}
// Room
if (!this.syncEquals(this.sentRoom, this.room?.serialize(this))) {
this.sentRoom = this.room?.serialize(this);
this.emit('room', {
'room': this.sentRoom
})
this.sentRoom = this.room
? (JSON.parse(
JSON.stringify(this.room.serialize(this))
) as SerializedRoom)
: null;
this.emit("room", {
room: this.sentRoom,
});
}
// Timeline
if (!this.syncEquals(this.sentTimelineName, this.room?.timelineName)) {
this.sentTimelineName = this.room?.timelineName || null;
this.emit('timeline', {
'timeline': this.sentTimelineName == null ? null : this.room!!.serializeTimeline(this)
})
this.emit("timeline", {
timeline:
this.sentTimelineName == null ? null : this.room?.serializeTimeline(),
});
}
}
emit(eventName: string, obj: any) {
emit(eventName: string, obj: unknown) {
this.socket.emit(eventName, obj);
}
syncEquals(obj1: any, obj2: any): boolean {
if (obj1 === undefined && obj2 === undefined)
return true;
if ((obj1 === undefined && obj2 !== undefined) || (obj1 !== undefined && obj2 === undefined))
syncEquals(obj1: unknown, obj2: unknown): boolean {
if (typeof obj1 !== typeof obj2) {
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;
}
if ((obj1 === null && obj2 !== null) || (obj1 !== null && obj2 === null))
if (obj1 === null || obj2 === null) {
return false;
}
if (typeof (obj1) !== typeof (obj2))
return false;
if (typeof obj2 !== "object") {
// This can not happen ;)
throw new TypeError("Obj2 is not object while obj1 is.");
}
if (typeof (obj1) === 'string' || typeof (obj1) === 'number' || typeof (obj1) === 'boolean') {
return obj1 === obj2;
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
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 =>
obj2.hasOwnProperty(key) && this.syncEquals(obj1[key], obj2[key])
return this.syncEquals(
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 SocketIO, {Socket} from "socket.io";
import { Server } from "socket.io";
import path from "path";
import Service from './Service'
import Service from "./Service";
import { RoomOptions } from "./Room";
// process.on('SIGINT', () => process.exit());
// process.on('SIGTERM', () => process.exit());
const HOST = '0.0.0.0';
const HOST = "0.0.0.0";
const PORT = 3001;
const app = express();
const server = app.listen(PORT, HOST, () => console.log(`Centurion listening on port ${PORT}!`));
app.use(express.static(path.join(__dirname, '../public')));
const httpServer = app.listen(PORT, HOST, () =>
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();
io.on('connection', socket => {
socket.on('disconnect', (reason) => {
service.onSocketDisconnect(socket);
app.get("/state", (req, res) => {
return res.json(service.rooms.map((r) => r.serialize()));
});
io.on("connection", (socket) => {
socket.on("disconnect", async () => {
await service.onSocketDisconnect(socket);
});
socket.on('ping', () => {
socket.emit('pong');
})
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(clientTime)) return;
service.onTimeSync(socket, requestId, clientTime);
})
});
socket.on('room_options', (options) => {
socket.on("room_options", (options: RoomOptions) => {
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;
service.onSetRoomOptions(socket, options);
});
socket.on('request_start', (options) => {
socket.on("request_start", () => {
service.onRequestStart(socket);
});
socket.on('request_join', (roomId: number) => {
if (!Number.isSafeInteger(roomId)) return;
socket.on(
"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', () => {
service.onRequestReady(socket);
})
try {
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', () => {
service.onRequestJoinRandom(socket);
})
socket.on("request_set_ready", () => {
service.onRequestSetReady(socket);
});
socket.on('call', (id: number, name: string, params: any) => {
if (!Number.isSafeInteger(id)) return;
// noinspection SuspiciousTypeOfGuard
if (!name || typeof (name) !== 'string') return;
// if (!params) return;
socket.on("request_join_random", async () => {
await service.onRequestJoinRandom(socket);
});
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') {
let roomId = params && params['roomId'];
if (!Number.isSafeInteger(roomId)) {
call.error('Invalid room id');
return;
if (message.length > 192) {
// perfect voor het Wilhelmus
return callback && callback(null, "Message too long.");
}
call.respond(service.hasRoomId(roomId));
return;
try {
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') {
// let roomId = params && params['roomId'];
// if (!Number.isSafeInteger(roomId)) {
@ -95,9 +126,6 @@ io.on('connection', socket => {
// call.respond(false);
// }
// }
})
service.onSocketConnect(socket);
/*socket.on('join_room', (roomId, callback) => {
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.js';
import timeline from "./data/timelines";
export function getTimelineNames(): string[] {
return timeline.timelines.map((i: any) => i.name)
return timeline.timelines.map((timeline) => timeline.name);
}
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;
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);
_randomTimeOffsetForDebug = 0;
console.log('random time offset', _randomTimeOffsetForDebug);
export function getCurrentTime() {
return Date.now() + _randomTimeOffsetForDebug;
}

@ -1,8 +1,12 @@
{
"compilerOptions": {
"outDir": "build",
"target": "es6",
"module": "commonjs",
"strict": 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
COPY package.json yarn.lock config-overrides.js ./
RUN yarn install
# Install TS manually since it's only included in the parent's package.json
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 src src/
RUN yarn build
RUN npm run build
FROM nginx:alpine
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",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1",
"@types/jest": "^24.9.1",
"@types/node": "^12.12.27",
"@types/react": "^16.9.19",
"@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"
"antd": "^4.17.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-ticker": "^1.3.0",
"react-transition-group": "^4.4.2",
"socket.io-client": "^4.4.0"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
"devDependencies": {
"@testing-library/jest-dom": "^5.15.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@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": {
"extends": "react-app"
"scripts": {
"start": "vite",
"build": "tsc && vite build",
"serve": "vite preview",
"check": "tsc --noEmit",
"lint": "eslint 'src/**/*.{ts,tsx}'",
"fix": "eslint --fix 'src/**/*.{ts,tsx}'"
},
"browserslist": {
"production": [
@ -44,12 +47,5 @@
"last 1 firefox 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",
"name": "Create React App Sample",
"short_name": "Centurion",
"name": "Centurion: honderd minuten...",
"icons": [
{
"src": "harambee.ico",
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
@ -20,6 +20,6 @@
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"theme_color": "#304ba3",
"background_color": "#ffffff"
}

@ -1,2 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
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 {Row} from "antd";
import React, { useState } from "react";
import {useRoomRunningAndReadyChanged} from "../lib/Connection";
import NextShot from "./NextShot";
import { useRoomRunningAndReadyChanged } from "../lib/Connection";
import Feed from "./Feed";
import ShotsTaken from "./ShotsTaken";
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 [currentUserReady, setCurrentUserReady] = useState(false);
const room = useRoomRunningAndReadyChanged();
const showFeed = (room?.running && room.readyToParticipate) || false;
const feedContent = (
<React.Fragment>
<Row>
<NextShot/>
<Feed/>
<ShotsTaken/>
</Row>
<Player/>
</React.Fragment>
);
const lobbyContent = (
<Lobby/>
);
const showFeed = (room?.readyToParticipate && currentUserReady) || false;
return (
<>
<section className="content">
{showFeed ? feedContent : lobbyContent}
{showFeed ? (
<Feed />
) : (
<Lobby
currentUserReady={currentUserReady}
onCurrentUserReadyChange={(b: boolean) => setCurrentUserReady(b)}
/>
)}
</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 {Col} from "antd"
import React, { useRef, useState } from "react";
import { Col, Row } from "antd";
import {TimelineItem} from "../types/types";
import {
EVENT_PRIORITY,
Timeline,
TimelineItem,
TimestampEvent,
} from "../types/types";
import FeedItem from "./FeedItem"
import {roomTime, useTimeline} from "../lib/Connection";
import {useUpdateAfterDelay} from "../util/hooks";
import FeedItem from "./FeedItem";
import connection, {
calculateRoomTime,
useRoomRunningAndReadyChanged,
useRoomTime,
useTimeline,
} from "../lib/Connection";
import { useResize, useUpdateAfterDelay } from "../util/hooks";
import CSSTransition from "react-transition-group/CSSTransition";
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();
useRoomRunningAndReadyChanged();
useResize();
useRoomTime();
useUpdateAfterDelay(500)
const feedElement = useRef<HTMLDivElement>(null);
const [lastShotId, setLastShotId] = useState<string | null>(null);
let liveFeed: TimelineItem[] = [];
useUpdateAfterDelay(getNextItemDelay(timeline));
if (timeline != null) {
liveFeed = timeline.feed.filter(item => {
return item.timestamp * 1000 <= roomTime()
if (!timeline) {
throw new TypeError("Feed without timeline.");
}
const time = calculateRoomTime();
const liveFeed = timeline.feed.filter((item) => {
return item.timestamp * 1000 <= time;
});
const keyToIndex: Map<string, number> = calculateKeyToIndexMap(liveFeed);
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);
}
}
function doSingleShake() {
if (!feedElement.current) return;
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 (
<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">
{liveFeed.map((item, i) =>
item.events.map((event, j) =>
<CSSTransition timeout={500} classNames="fade" key={`${item.timestamp}.${j}`}>
<FeedItem item={event} key={`${item.timestamp}.${j}f`}/>
{liveFeed.map((item) =>
sortEvents(item.events).map((event, j) => (
<CSSTransition
timeout={300}
classNames="fade"
key={`${item.timestamp}.${j}`}
>
<div
className="feed-item-container"
style={{ opacity: itemOpacity(`${item.timestamp}.${j}`) }}
>
<FeedItem item={event} />
</div>
</CSSTransition>
)
))
)}
</TransitionGroup>
</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 song from "../img/song.png";
import talk from "../img/talk.png";
import time from "../img/time.png";
export interface FeedItemProps {
item: TimestampEvent;
}
const images = {
shot, song, talk, time
shot,
song,
talk,
time,
};
class FeedItem extends PureComponent<{item: TimestampEvent}> {
render() {
const FeedItem = ({ item }: FeedItemProps) => {
return (
<div className="feed-item">
<div className="feed-item__title">
{this.props.item.text[0]}
</div>
<div className="feed-item__emoji">
<img src={images[this.props.item.type]}/>
</div>
<div className="feed-item__desc">
{this.props.item.text[1]}
</div>
</div>
<Row align="middle" className="feed-item">
<Col span={11} className="feed-item__title">
{item.text[0]}
</Col>
<Col span={2} className="feed-item__emoji">
<img alt={item.type} src={images[item.type]} />
</Col>
<Col span={11} className="feed-item__desc">
{item.text[1]}
</Col>
</Row>
);
}
}
};
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,132 +1,206 @@
import React, {MouseEvent, useState} from 'react';
import {Button, Card, Col, Divider, Form, Input, InputNumber, Row, Select} from "antd"
import {red} from '@ant-design/colors';
import connection, {useConfig, useIsConnected, useRoom} from "../lib/Connection";
import "../css/lobby.sass";
import beer from "../img/beer.png"
import {RoomOptions} from "../types/types";
const {Option} = Select;
const Lobby = (props: any) => {
import { useState } from "react";
import {
Button,
Card,
Col,
Divider,
Form,
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";
const { Option } = Select;
export interface PropType {
currentUserReady: boolean;
onCurrentUserReadyChange?: (ready: boolean) => void;
}
const Lobby = (props: PropType) => {
// Form/control states.
const [selectedRoomId, setSelectedRoomId] = useState(1);
const [seekTime, setSeekTime] = useState(0);
const [timelineName, setTimelineName] = useState(null);
const [joiningLobby, setJoiningLobby] = useState(false);
const [joinLobbyError, setJoinLobbyError] = useState(false);
const [isPreloading, setIsPreloading] = useState(false);
const timeline = useTimelineSongFileChanged();
// Room and logic states.
const isConnected = useIsConnected();
const room = useRoom();
const config = useConfig();
// @ts-ignore
const connectionType = connection.socket.io.engine.transport.name;
let isLeader = room?.isLeader || false;
let userCount = room?.userCount || 0;
function handleRequestStartClicked(e: MouseEvent) {
connection.requestStart(seekTime * 1000);
}
const isLeader = room?.isLeader || false;
const userCount = room?.userCount || 0;
function handleJoin(e: MouseEvent) {
connection.requestReady();
async function handleJoin() {
await preloadAudio();
connection.requestSetReady();
props.onCurrentUserReadyChange?.(true);
}
function applyRoomId(v: number) {
connection.requestJoin(v).then(v => {
async function applyRoomId(v: number) {
setJoiningLobby(true);
await connection.requestJoin(v);
setJoiningLobby(false);
setJoinLobbyError(!v);
})
setJoiningLobby(true)
}
function handleJoinRandomLobby() {
connection.requestJoinRandom()
connection.requestJoinRandom();
setJoinLobbyError(false);
}
function handleTimelineNameSet(timelineName: any) {
setTimelineName(timelineName);
connection.setRoomOptions(new RoomOptions(
seekTime || 0,
timelineName || room?.timelineName || ''))
connection.setRoomOptions(
new RoomOptions(
seekTime * 1000 || 0,
timelineName || room?.timelineName || ""
)
);
}
function handleSetSeekTime(seekTime: number) {
setSeekTime(seekTime);
connection.setRoomOptions(new RoomOptions(
connection.setRoomOptions(
new RoomOptions(
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);
}
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;
});
}
let leaderConfig = (
const leaderConfig = (
<Row justify="center">
<Col>
<Form
layout='horizontal'
labelCol={{span: 8}}
wrapperCol={{span: 24}}
layout="horizontal"
labelCol={{ span: 8 }}
wrapperCol={{ span: 24 }}
>
<Form.Item label="Starttijd">
<Input
type="number"
suffix="sec"
value={seekTime}
onChange={v => handleSetSeekTime(parseInt(v.target.value) || 0)}/>
onChange={(v) => handleSetSeekTime(parseInt(v.target.value) || 0)}
/>
</Form.Item>
<Form.Item label="Nummer">
<Select defaultValue={(room && room.timelineName) || ''}
onChange={e => handleTimelineNameSet(e)}>
{config && config.availableTimelines.map((item, i) =>
<Option key={item} value={item}>{item}</Option>
)}
<Select
defaultValue={(room && room.timelineName) || ""}
onChange={(e) => handleTimelineNameSet(e)}
>
{config &&
config.availableTimelines.map((item, i) => (
<Option key={item} value={item}>
{item}
</Option>
))}
</Select>
</Form.Item>
</Form>
<Button
block
type="primary"
onClick={handleRequestStartClicked}>Start</Button>
loading={isPreloading}
onClick={handleJoin}
>
Start
</Button>
</Col>
</Row>
)
);
let nonLeaderConfig = (
const nonLeaderConfig = (
<Row justify="center">
<Col>
<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> starten op {(room?.seekTime || 0) / 1000} seconden</span>}
{!room?.running && (
<span> starten op {(room?.seekTime || 0) / 1000} seconden</span>
)}
</p>
<Button
block
type="primary"
disabled={!room || room.readyToParticipate}
onClick={handleJoin}>{room && room.readyToParticipate ? 'Wachten op het startsein' : 'Kom erbij'}</Button>
disabled={!room || props.currentUserReady}
loading={isPreloading}
onClick={handleJoin}
>
{room && props.currentUserReady
? "Wachten op het startsein"
: "Kom erbij"}
</Button>
</Col>
</Row>
)
);
// @ts-ignore
return (
<div className="lobby">
<Row>
<Col className="centurion-title" span={24}>
<div className="beer-flipped">
<img src={beer} className={`beer ${isConnected ? 'connected' : 'connecting'}`} alt="beer"/>
</div>
<span className="text">Centurion!</span>
<img src={beer} className={`beer ${isConnected ? 'connected' : 'connecting'}`} alt="beer"/>
<Row className="centurion-title" justify="center">
<Col span={4} md={4}>
<img
src={beer}
className={`beer beer-flipped ${
isConnected ? "connected" : "connecting"
}`}
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>
</Row>
<Row>
@ -136,82 +210,102 @@ const Lobby = (props: any) => {
<div>Kun jij het aan?</div>
</Col>
</Row>
<br/>
<br />
{!isConnected &&
{!isConnected && (
<Row justify="center">
<Col className="lobby-connecting">
<h2>Verbinden...</h2>
</Col>
</Row>
}
)}
{isConnected &&
{isConnected && (
<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>
<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 &&
<span>
{userCount === 1 ?
<p>Er is één gebruiker aanwezig.</p>
:
<p>Er zijn {userCount} gebruikers aanwezig.</p>
}
</span>
}
{room &&
<span>Deel de link met je vrienden om mee te doen!</span>
}
<br/>
<br/>
<Row justify="center">
{room?.users?.map((u) => (
<Badge
key={u.id}
status={u.readyToParticipate ? "success" : "error"}
/>
))}
</Row>
{room && <Row>Deel de link met je vrienden om mee te doen!</Row>}
<Divider />
{room && (isLeader ? leaderConfig : nonLeaderConfig)}
<Divider/>
<Divider />
<Row justify="center">
<Col>
<InputNumber
style={{'width': 'calc(100% - 150px)'}}
style={{ width: "calc(100% - 150px)" }}
min={1}
max={100000}
value={selectedRoomId || room?.id || 0}
onChange={(v) => setSelectedRoomId(v || 0)}/>
onChange={(v) => setSelectedRoomId(v || 0)}
/>
<Button
style={{'width': '150px'}}
style={{ width: "150px" }}
type="primary"
loading={joiningLobby}
onClick={() => {
applyRoomId(selectedRoomId)
}}>Verander van lobby</Button>
onClick={async () => {
await applyRoomId(selectedRoomId);
}}
>
Ga naar die lobby
</Button>
{joinLobbyError &&
<span style={{color: red[4]}}>Die lobby bestaat niet</span>
}
{joinLobbyError && (
<span style={{ color: red[4] }}>
Die lobby bestaat niet
</span>
)}
</Col>
</Row>
<Row justify="center">
<span className={'lobby-options-or'}>of</span>
<span className={"lobby-options-or"}>of</span>
</Row>
<Row justify="center">
<Col>
<Button type="primary"
<Button
type="primary"
onClick={() => {
handleJoinRandomLobby()
}}>Join een willekeurige lobby</Button>
handleJoinRandomLobby();
}}
>
Join een nieuwe lobby
</Button>
</Col>
</Row>
</Card>
</Col>
</Row>
}
)}
<img src={haramlogo} className="haram-logo" alt="haramlogo"/>
<img src={logo} className="via-logo" alt="logo" />
</div>
);
};

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

@ -1,106 +1,211 @@
import {roomTime, useRoomRunningAndReadyChanged, useRoomTime, useTimelineSongFileChanged} from "../lib/Connection";
import React, {createRef, SyntheticEvent, useRef, useState} from "react";
import connection, {
calculateRoomTime,
useRoomRunningAndReadyChanged,
useTimelineSongFileChanged,
} from "../lib/Connection";
import { SyntheticEvent, useEffect, useRef, useState } from "react";
import '../css/player.sass'
import {Room} from "../types/types";
import {parse as parseQueryString} from "query-string";
import { Button, Slider } from "antd";
import { SoundFilled, SoundOutlined } from "@ant-design/icons";
import "../css/player.css";
import { Room } from "../types/types";
const Player = () => {
const room = useRoomRunningAndReadyChanged();
const _ = useRoomTime()
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 [hadError, setHadError] = useState(false);
// If our time synchronisation algorithm thing thinks the time is off by more
// 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
// and will always be off. To avoid endless skipping of the song this cap stops seeking the
// player.
const maxTimesSeekAllow = 25;
const query = parseQueryString(window.location.search);
if (query.nosound) {
return null;
useEffect(() => {
// Need to use an effect since 'player' will only contain a reference after first render.
if (!timeline) {
throw new Error("Player without active timeline.");
}
if (player.current && player.current.dataset.src != timeline!!.songFile) {
player.current.dataset.src = timeline!!.songFile;
player.current.src = timeline!!.songFile;
if (!player.current) {
throw new Error("No player after mount.");
}
player.current.src = timeline.songFile;
}, [timeline]);
function handlePlayerOnPlay(e: SyntheticEvent) {
e.preventDefault();
// For when the user manually started the player for when autoplay is off.
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() {
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) {
if (!player.current) return;
async function startPlaying(manual: boolean) {
if (!player.current) {
return;
}
if (player.current.paused && !hadError) {
player.current.play().then(() => {
setPlayerVolume(volume);
try {
await player.current.play();
setHadError(false);
}).catch(e => {
console.error('Error playing', e);
} catch (e) {
console.error("Error playing", e);
setHadError(true);
})
}
}
if (!hadError) {
setPlayerTime(room!!, manual);
if (!hadError && room) {
setPlayerTime(room, manual);
}
}
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;
let diff = player.current.currentTime - targetTime;
if (timesSeeked >= maxTimesSeekAllow && !manualAdjustment) {
// 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.playbackRate = Math.max(Math.min(4.0, room.speedFactor), 0.25);
player.current.playbackRate = Math.min(room.speedFactor, 5);
if (!manualAdjustment) {
setTimesSeeked(timesSeeked + 1);
}
} else {
console.warn('The running player is off, but we\'ve changed the time ' +
'too often, skipping synchronizing the player.');
console.log(
`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()) {
startPlaying(false)
.then(() => {
//
})
.catch((e) => {
console.error(e);
});
} else {
if (player.current) {
player.current.pause();
}
}
function render() {
return (
<audio ref={player} className='player' hidden={!hadError} controls={true} onPlay={handlePlayerOnPlay}/>
)
}
return render();
}
<div id="audio">
<audio
ref={player}
className="player"
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;

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

@ -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 './css/index.sass';
import ReactDOM from "react-dom";
import "./css/index.css";
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 {useEffect, useState} from "react";
import {parse as parseQueryString, stringify as stringifyQueryString} from 'query-string';
import io, { Socket } from "socket.io-client";
import {Config, Room, RoomOptions, Timeline} from "../types/types";
import {Sub, useSub} from "../util/sub";
import { Config, Room, RoomOptions, Timeline } from "../types/types";
import { Sub, useSub } from "../util/sub";
class Connection {
url = '/';
url = "/";
socket: SocketIOClient.Socket;
socket: Socket;
isConnected = new Sub<boolean>();
@ -22,14 +20,12 @@ class Connection {
timeSyncTooOld = 120000;
roomTime = new Sub<number>();
calls: { [id: number]: Call } = {};
constructor() {
this.isConnected.set(false);
this.socket = io(this.url, {
autoConnect: false,
transports: ['websocket']
transports: ["websocket"],
});
this.setupSocketListeners();
@ -44,25 +40,25 @@ class Connection {
}
setupSocketListeners() {
this.socket.on('connect', () => {
this.socket.on("connect", async () => {
this.isConnected.set(true);
this.onConnect();
})
await this.onConnect();
});
this.socket.on('disconnect', () => {
this.socket.on("disconnect", () => {
this.isConnected.set(false);
this.onDisconnect();
})
});
this.socket.on('config', (data: any) => {
this.socket.on("config", (data: any) => {
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.socket.on('room', (data: any) => {
this.socket.on("room", (data: any) => {
if (data.room) {
this.setQueryLobbyId(data.room.id);
}
@ -70,38 +66,25 @@ class Connection {
this.room.set(data.room);
});
this.socket.on('timeline', (data: any) => {
this.socket.on("timeline", (data: any) => {
if (data.timeline) {
this.timeline.set(new Timeline(data.timeline));
} else {
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();
let lobbyId = this.getQueryLobbyId();
const lobbyId = this.getQueryLobbyId();
if (lobbyId) {
this.requestJoin(lobbyId).then(v => {
if (!v) {
this.setQueryLobbyId(null);
const exists = await this.requestJoin(lobbyId);
if (!exists) {
this.setQueryLobbyId();
this.requestJoinRandom();
}
})
} else {
this.requestJoinRandom();
}
@ -111,88 +94,91 @@ class Connection {
this.stopTimeSync();
}
autoStart() : boolean {
let query = parseQueryString(window.location.search);
return !!query.autostart;
}
private getQueryLobbyId(): number | null {
let query = parseQueryString(window.location.search);
if (query.lobby) {
let lobbyId = Number.parseInt(query.lobby.toString());
if (Number.isSafeInteger(lobbyId) && lobbyId > 0) {
return lobbyId
}
const query = new URLSearchParams(window.location.search);
const lobby = query.get("lobby");
if (!lobby) {
return null;
}
const lobbyId = Number.parseInt(query.get("lobby")!.toString());
if (!Number.isSafeInteger(lobbyId) || lobbyId < 1) {
return null;
}
private setQueryLobbyId(lobbyId: number | null) {
let query = parseQueryString(window.location.search);
return lobbyId;
}
private setQueryLobbyId(lobbyId?: number) {
const newUrl = new URL(window.location.href);
if (lobbyId) {
query.lobby = lobbyId.toString();
newUrl.searchParams.set("lobby", String(lobbyId));
} else {
delete query.lobby;
}
let newUrl = window.location.protocol + "//" + window.location.host +
window.location.pathname + (Object.keys(query).length ? ('?' + stringifyQueryString(query)) : '');
window.history.pushState({}, '', newUrl);
newUrl.searchParams.delete("lobby");
}
async call(name: string, params: any) {
return new Promise<any>((resolve, reject) => {
let callback = (err: any, res: any) => {
if (err) {
return reject(err);
window.history.pushState({}, "", newUrl.toString());
}
resolve(res);
};
let call = new Call(name, params, callback);
this.calls[call.id] = call;
this.socket.emit('call', call.id, name, params);
});
setRoomOptions(roomOptions: RoomOptions) {
this.socket.emit("room_options", roomOptions);
}
setRoomOptions(roomOptions: RoomOptions) {
this.socket.emit('room_options', roomOptions)
requestStart() {
this.socket.emit("request_start");
}
requestStart(seekTime: number) {
this.socket.emit('request_start', {
seekTime: seekTime
submitTickerMessage(message: string) {
return new Promise<void>((resolve, reject) => {
this.socket.emit(
"submit_ticker_message",
message,
(_: null, err?: string) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
});
}
async requestJoin(roomId: number): Promise<boolean> {
return this.call('room_exists', {roomId: roomId}).then(v => {
if (v) {
this.socket.emit('request_join', roomId, this.autoStart());
if (this.autoStart()) {
this.requestReady();
return new Promise<boolean>((resolve, reject) => {
this.socket.emit(
"request_join",
roomId,
(err?: string, didJoinRoom?: boolean) => {
if (err) {
return reject(err);
}
return true;
} else {
return false;
return resolve(!!didJoinRoom);
}
})
);
});
}
requestReady() {
this.socket.emit('request_ready');
requestSetReady() {
this.socket.emit("request_set_ready");
}
requestJoinRandom() {
this.socket.emit('request_join_random');
this.socket.emit("request_join_random");
}
startTimeSync() {
for (let i = 0; i < this.timeSyncIntervals.length; i++) {
let timeoutId = setTimeout(() => {
this.sendTimeSync(i === this.timeSyncIntervals.length - 1);
const timeoutId = setTimeout(() => {
// Only reschedule the last sync interval (i.e. every 30 seconds)
const shouldReschedule = i === this.timeSyncIntervals.length - 1;
this.sendTimeSync(shouldReschedule);
}, this.timeSyncIntervals[i]);
// @ts-ignore
this.timeSyncTimeoutIds.push(timeoutId);
}
}
@ -204,8 +190,8 @@ class Connection {
}
sendTimeSync(alsoSchedule: boolean) {
let sync = new TimeSyncRequest();
this.socket.emit('time_sync', sync.requestId, Date.now());
const sync = new TimeSyncRequest();
this.socket.emit("time_sync", sync.requestId, Date.now());
this.timeSyncs[sync.requestId] = sync;
if (alsoSchedule) {
@ -216,21 +202,21 @@ class Connection {
}
timeSyncResponse(requestId: number, clientDiff: number, serverTime: number) {
let syncReq = this.timeSyncs[requestId];
if (!syncReq) return
delete this.timeSyncs[requestId];
const syncReq = this.timeSyncs[requestId];
if (!syncReq) {
return;
}
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) {
delete this.timeSyncs[i];
break;
}
}
// console.log(this.timeSyncs);
// console.log('SERVER TIME', this.serverTimeOffset());
this.roomTime.set(roomTime());
this.roomTime.set(calculateRoomTime());
}
serverTime(): number {
@ -240,8 +226,8 @@ class Connection {
serverTimeOffset(): number {
let num = 0;
let sum = 0;
for (let i in this.timeSyncs) {
let sync = this.timeSyncs[i];
for (const i in this.timeSyncs) {
const sync = this.timeSyncs[i];
if (!sync.ready) continue;
sum += sync.offset;
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;
class TimeSyncRequest {
requestId: number;
start: number;
offset: number = 0;
offset = 0;
ready = false;
constructor() {
@ -286,18 +256,16 @@ class TimeSyncRequest {
response(clientDiff: number, serverTime: number) {
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;
// console.log('TIME SYNC', 'cdiff:', clientDiff, 'lag:',
// lag, 'diff:', serverTime - now, 'offset:', this.offset);
}
}
let connection: Connection = new Connection();
// @ts-ignore
window['connection'] = connection;
const connection: Connection = new Connection();
export default connection;
export function useRoom(): Room | null {
@ -309,7 +277,7 @@ export function useConfig(): Config | 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 {
@ -317,19 +285,27 @@ export function useTimeline(): 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 {
return useSub(connection.roomTime);
return useSub(connection.roomTime) || 0;
}
export function roomTime(): number {
let room = connection.room.get();
if (!room) return 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;
}
return (connection.serverTime() - room.startTime) * room.speedFactor;
}
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 {
availableTimelines: string[]
availableTimelines: string[];
}
export interface Room {
id: number,
userCount: number,
isLeader: boolean,
running: boolean,
startTime: number,
seekTime: number,
timelineName: string,
readyToParticipate: boolean,
speedFactor: number
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 class RoomOptions {
seekTime: number
timelineName: string
seekTime: number;
timelineName: string;
constructor(seekTime: number, timelineName: string) {
this.seekTime = seekTime;
@ -37,42 +27,99 @@ export class RoomOptions {
}
export class Timeline {
name: string
songFile: string
feed: TimelineItem[]
name: string;
songFile: string;
feed: TimelineItem[];
constructor(obj: any) {
this.name = obj.name;
this.songFile = obj.songFile;
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] {
let feedToSearch = type ? this.feed.filter(i => i.events.some(j => j.type == type)) : this.feed;
return maxShot;
}
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 (feedToSearch[i].timestamp * 1000 >= time) {
return [feedToSearch[i - 1], feedToSearch[i]];
if (!item || !item.events.length) {
return undefined;
}
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] {
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]
return item.events.find((ev) => ev.type === type);
}
}
export interface TimelineItem {
timestamp: number,
events: TimestampEvent[]
id: string;
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 {
type: 'talk' | 'shot' | 'song' | 'time',
text: string[],
shotCount?: number
interface TimestampEventShot extends TimestampEventBase {
type: "shot";
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) {
const [_, timedUpdateSet] = useState(0);
const [, timedUpdateSet] = useState(0);
useEffect(() => {
let timeoutId = setTimeout(() => {
timedUpdateSet(v => v + 1);
const timeoutId = setTimeout(() => {
timedUpdateSet((v) => v + 1);
}, 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.
* @param event
* @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) => {
const cb = (err: any, res: any) => {
if (err) {
@ -13,11 +15,10 @@ export function emit(socket: SocketIOClient.Socket, event: string, arg: any = nu
resolve(res);
};
if (arg === null || typeof arg === 'undefined') {
if (arg === null || typeof arg === "undefined") {
socket.emit(event, cb);
} else {
socket.emit(event, arg, cb);
}
})
});
}

@ -1,8 +1,8 @@
import {useEffect, useState} from "react";
import { useEffect, useState } from "react";
export class Sub<T> {
_listeners: ((obj: T) => void)[] = [];
_current: any = null;
_current: T | null = null;
subscribe(listener: any) {
if (this._listeners.indexOf(listener) < 0) {
@ -11,30 +11,39 @@ export class Sub<T> {
}
unsubscribe(listener: any) {
let index = this._listeners.indexOf(listener);
const index = this._listeners.indexOf(listener);
if (index >= 0) {
this._listeners.splice(index, 1);
}
}
get() {
get(): T | null {
return this._current;
}
set(obj: T) {
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());
useEffect(() => {
let listener = (obj: T) => stateSetter(obj);
useEffect(
() => {
const listener = (obj: T) => stateSetter(obj);
sub.subscribe(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;
}

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