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. 299
      backend/src/Room.ts
  7. 223
      backend/src/Service.ts
  8. 193
      backend/src/User.ts
  9. 4896
      backend/src/data/timelines.ts
  10. 230
      backend/src/index.ts
  11. 61
      backend/src/timeline.ts
  12. 10
      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. 56
      frontend/src/components/Centurion.tsx
  30. 232
      frontend/src/components/Feed.tsx
  31. 47
      frontend/src/components/FeedItem.tsx
  32. 95
      frontend/src/components/FeedTicker.tsx
  33. 502
      frontend/src/components/Lobby.tsx
  34. 80
      frontend/src/components/NextShot.tsx
  35. 261
      frontend/src/components/Player.tsx
  36. 57
      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. 518
      frontend/src/lib/Connection.ts
  47. 1
      frontend/src/react-app-env.d.ts
  48. 153
      frontend/src/types/types.ts
  49. 39
      frontend/src/util/hooks.ts
  50. 33
      frontend/src/util/socket.ts
  51. 67
      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,210 +1,167 @@
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;
users: User[] = [];
leader: User | null = null;
id = 0;
users: User[] = [];
leader: User | null = null;
running = false;
startTime = 0;
currentSeconds = 0;
timelineIndex: number = 0;
ticker: TickerMessage[] = [];
seekTime: number = 0;
timelineName: string = 'Centurion';
running = false;
startTime: number | undefined = undefined;
// For debugging purposes
speedFactor = 1;
autoStart = false;
seekTime = 0;
timelineName = "Centurion";
constructor(name: number) {
this.id = name;
}
// For debugging purposes
speedFactor = 1;
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
}
}
constructor(name: number) {
this.id = name;
}
serializeTimeline(user: User) {
return getTimeline(this.timelineName);
}
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),
};
sync() {
this.users.forEach(u => u.sync());
if (typeof user === "undefined" || this.leader === user) {
obj["users"] = this.users.map((u) => u.serialize());
}
join(user: User) {
this.users.push(user);
user.setRoom(this);
return obj;
}
if (!this.hasLeader()) {
this.setLeader(user);
}
serializeTimeline() {
return getTimeline(this.timelineName);
}
if (this.autoStart) {
this.seekTime = 2500000;
this.running = true;
this.start();
}
sync() {
this.users.forEach((u) => u.sync());
}
this.sync();
async join(user: User) {
this.users.push(user);
await user.setRoom(this);
if (!this.hasLeader()) {
this.setLeader(user);
}
leave(user: User) {
this.users.splice(this.users.indexOf(user), 1);
user.setRoom(null);
this.sync();
}
if (this.leader == user) {
this.setRandomLeader();
}
async leave(user: User) {
this.removeTickerMessageForUser(user);
this.users.splice(this.users.indexOf(user), 1);
await user.setRoom(null);
this.sync();
if (this.leader == user) {
this.setRandomLeader();
}
onBeforeDelete() {
}
this.sync();
}
setOptions(options: any) {
this.seekTime = Math.max(0, Math.min(options.seekTime, 250 * 60 * 1000))
if (getTimelineNames().indexOf(options.timelineName) >= 0) {
this.timelineName = options.timelineName;
}
this.sync()
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();
}
start() {
this.running = true;
this.startTime = getCurrentTime() - this.seekTime
start() {
this.running = true;
this.startTime = getCurrentTime() - this.seekTime;
this.sync();
}
this.sync();
}
/**
*
* @returns {boolean}
*/
hasUsers() {
return this.users.length !== 0;
}
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();
setRandomLeader() {
if (this.hasUsers()) {
this.leader = this.users[0];
}
}
/**
*
* @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);
}
hasLeader(): boolean {
return this.leader != null;
}
/**
*
* @returns {boolean}
*/
hasUsers() {
return this.users.length !== 0;
}
setLeader(user: User) {
this.leader = user;
}
setRandomLeader() {
if (this.hasUsers()) {
this.leader = this.users[0];
}
}
getLeader(): User | null {
return this.leader;
}
/**
*
* @param id
* @returns {User|undefined}
*/
getUser(id: string) {
return this.users.find(u => u.id === id);
}
submitTickerMessage(user: User, message: string) {
message = message.replace("\n", "");
/**
*
* @param {string} id
*/
removeUser(id: string) {
this.users = this.users.filter(u => u.id !== id);
}
this.removeTickerMessageForUser(user);
hasLeader(): boolean {
return this.leader != null;
}
this.ticker.push({
user: user,
message: message,
});
setLeader(user: User) {
this.leader = user;
}
this.sync();
}
getLeader(): User | null {
return this.leader;
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,140 +1,163 @@
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>();
private roomIdToRooms = new Map<number, Room>();
private socketsToUsers = new Map<string, User>();
onSocketConnect(socket: Socket) {
let user = new User(socket);
this.socketsToUsers.set(socket.id, user);
user.sync();
get rooms(): Room[] {
const rooms = [];
for (const [, room] of this.roomIdToRooms) {
rooms.push(room);
}
onSocketDisconnect(socket: Socket) {
let user = this.getUser(socket);
return rooms;
}
if (user.room != null) {
user.room.leave(user);
}
onSocketConnect(socket: Socket) {
const user = new User(socket);
this.socketsToUsers.set(socket.id, user);
user.sync();
}
this.deleteEmptyRooms();
async onSocketDisconnect(socket: Socket) {
const user = this.getUser(socket);
user.onDisconnect();
if (user.room != null) {
await user.room.leave(user);
}
onTimeSync(socket: Socket, requestId: number, clientTime: number) {
let user = this.getUser(socket);
this.deleteEmptyRooms();
}
let now = getCurrentTime();
user.emit('time_sync', {
'requestId': requestId,
'clientDiff': now - clientTime,
'serverTime': now
})
}
onSetRoomOptions(socket: Socket, options: RoomOptions) {
let user = this.getUser(socket);
onTimeSync(socket: Socket, requestId: number, clientTime: number) {
const user = this.getUser(socket);
if (user.room?.getLeader() == user) {
user.room!!.setOptions(options)
}
}
const now = getCurrentTime();
user.emit("time_sync", {
requestId: requestId,
clientDiff: now - clientTime,
serverTime: now,
});
}
onRequestStart(socket: Socket) {
let user = this.getUser(socket);
onSetRoomOptions(socket: Socket, options: RoomOptions) {
const user = this.getUser(socket);
if (user.room?.getLeader() == user) {
user.room!!.start();
}
if (user.room?.getLeader() == user) {
user.room.setOptions(options);
}
}
onRequestJoin(socket: Socket, roomId: number): boolean {
let user = this.getUser(socket);
if (user.room && user.room.id == roomId) return false;
onRequestStart(socket: Socket) {
const user = this.getUser(socket);
if (user.room) {
user.room.leave(user);
this.deleteEmptyRooms();
}
if (user.room?.getLeader() === user) {
user.room.start();
user.room.sync();
}
}
if (!this.roomIdToRooms.has(roomId)) {
this.createRoomWithId(roomId);
}
async onRequestJoin(socket: Socket, roomId: number) {
const user = this.getUser(socket);
if (user.room && user.room.id == roomId) return false;
let room = this.roomIdToRooms.get(roomId)!!;
room.join(user);
if (user.room) {
await user.room.leave(user);
this.deleteEmptyRooms();
}
return true;
if (!this.roomIdToRooms.has(roomId)) {
this.createRoomWithId(roomId);
}
onRequestReady(socket: Socket) {
let user = this.getUser(socket);
if (!user.room || user.readyToParticipate) return;
user.readyToParticipate = true;
user.sync();
const room = this.roomIdToRooms.get(roomId);
if (!room) {
return false;
}
onRequestJoinRandom(socket: Socket) {
let user = this.getUser(socket);
await room.join(user);
if (user.room) {
user.room.leave(user);
this.deleteEmptyRooms();
}
return true;
}
const room = this.createRandomRoom();
if (!room) throw Error('Too many rooms active');
room.join(user);
}
onRequestSetReady(socket: Socket) {
const user = this.getUser(socket);
if (!user.room || user.readyToParticipate) return;
user.readyToParticipate = true;
user.room.sync();
}
hasRoomId(roomId: number): boolean {
return this.roomIdToRooms.has(roomId);
}
async onRequestJoinRandom(socket: Socket) {
const user = this.getUser(socket);
private getUser(socket: Socket): User {
let user = this.socketsToUsers.get(socket.id);
if (!user) {
throw new Error('User not found');
}
return user;
if (user.room) {
await user.room.leave(user);
this.deleteEmptyRooms();
}
private deleteEmptyRooms() {
for (let room of this.roomIdToRooms.values()) {
if (room.users.length == 0) {
this.deleteRoom(room);
}
}
}
const room = this.createRandomRoom();
if (!room) throw Error("Too many rooms active");
await room.join(user);
}
hasRoomId(roomId: number): boolean {
return this.roomIdToRooms.has(roomId);
}
private createRandomRoom(): Room | null {
let tries = 0;
while (tries++ < 1000) {
const randomId = randomInt(100, Math.max(1000, this.roomIdToRooms.size * 2));
if (this.roomIdToRooms.has(randomId)) continue;
submitTickerMessage(socket: Socket, message: string) {
const user = this.getUser(socket);
return this.createRoomWithId(randomId);
}
return null;
if (!user.room) {
throw new Error("User has no room");
}
private createRoomWithId(roomId: number): Room {
if (this.roomIdToRooms.has(roomId)) {
throw new Error('A room with the given id already exists');
}
user.room.submitTickerMessage(user, message);
}
let room = new Room(roomId);
this.roomIdToRooms.set(roomId, room);
return room;
private getUser(socket: Socket): User {
const user = this.socketsToUsers.get(socket.id);
if (!user) {
throw new Error("User not found");
}
return user;
}
private deleteEmptyRooms() {
for (const room of this.roomIdToRooms.values()) {
if (room.users.length == 0) {
this.deleteRoom(room);
}
}
}
private createRandomRoom(): Room | null {
let tries = 0;
let i = 1;
while (tries++ < 1000) {
if (this.roomIdToRooms.has(i)) {
i++;
} else {
return this.createRoomWithId(i);
}
}
return null;
}
private deleteRoom(room: Room) {
this.roomIdToRooms.get(room.id)!!.onBeforeDelete();
this.roomIdToRooms.delete(room.id)
private createRoomWithId(roomId: number): Room {
if (this.roomIdToRooms.has(roomId)) {
throw new Error("A room with the given id already exists");
}
const room = new Room(roomId);
this.roomIdToRooms.set(roomId, room);
return room;
}
private deleteRoom(room: Room) {
this.roomIdToRooms.delete(room.id);
}
}

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

File diff suppressed because it is too large Load Diff

@ -1,105 +1,133 @@
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);
});
socket.on('ping', () => {
socket.emit('pong');
})
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) => {
if (!options) return;
if (!options.timelineName || typeof (options.timelineName) !== 'string') return;
if (!Number.isSafeInteger(options.seekTime)) return;
service.onSetRoomOptions(socket, options);
});
socket.on('request_start', (options) => {
service.onRequestStart(socket);
});
socket.on('request_join', (roomId: number) => {
if (!Number.isSafeInteger(roomId)) return;
service.onRequestJoin(socket, roomId);
});
socket.on('request_ready', () => {
service.onRequestReady(socket);
})
socket.on('request_join_random', () => {
service.onRequestJoinRandom(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;
let call = new Call(socket, id, name, params);
if (name == 'room_exists') {
let roomId = params && params['roomId'];
if (!Number.isSafeInteger(roomId)) {
call.error('Invalid room id');
return;
}
app.get("/state", (req, res) => {
return res.json(service.rooms.map((r) => r.serialize()));
});
call.respond(service.hasRoomId(roomId));
return;
}
//
// if (name == 'request_join') {
// let roomId = params && params['roomId'];
// if (!Number.isSafeInteger(roomId)) {
// call.error('Invalid room id');
// return;
// }
// if (!service.hasRoomId(roomId)) {
// call.respond(false);
// return;
// }
// if (service.onRequestJoin(socket, roomId)) {
// call.respond(true);
// } else {
// call.respond(false);
// }
// }
})
service.onSocketConnect(socket);
/*socket.on('join_room', (roomId, callback) => {
io.on("connection", (socket) => {
socket.on("disconnect", async () => {
await service.onSocketDisconnect(socket);
});
socket.on("ping", () => {
socket.emit("pong");
});
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: RoomOptions) => {
if (!options) return;
if (!options.timelineName || typeof options.timelineName !== "string")
return;
if (!Number.isSafeInteger(options.seekTime)) return;
service.onSetRoomOptions(socket, options);
});
socket.on("request_start", () => {
service.onRequestStart(socket);
});
socket.on(
"request_join",
async (roomId: number, callback: (err?: string, res?: boolean) => void) => {
if (!Number.isSafeInteger(roomId)) {
return callback("Invalid roomId.");
}
if (!service.hasRoomId(roomId)) {
// cannot join a room that does not exist.
return callback(undefined, false);
}
try {
const didJoinRoom = await service.onRequestJoin(socket, roomId);
callback(undefined, didJoinRoom);
} catch (e) {
callback(e instanceof Error ? e.message : "Unknown error.");
}
}
);
socket.on("request_set_ready", () => {
service.onRequestSetReady(socket);
});
socket.on("request_join_random", async () => {
await service.onRequestJoinRandom(socket);
});
socket.on(
"submit_ticker_message",
(message?: unknown, callback?: (res?: null, err?: string) => void) => {
if (typeof message !== "string") {
return callback && callback(null, "Invalid message.");
}
if (message.length > 192) {
// perfect voor het Wilhelmus
return callback && callback(null, "Message too long.");
}
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)) {
// call.error('Invalid room id');
// return;
// }
// if (!service.hasRoomId(roomId)) {
// call.respond(false);
// return;
// }
// if (service.onRequestJoin(socket, roomId)) {
// call.respond(true);
// } else {
// call.respond(false);
// }
// }
/*socket.on('join_room', (roomId, callback) => {
if (!callback || typeof callback !== 'function') {
console.error("Join: Callback not a function.");
return
@ -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);
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;
const t = timeline.timelines.find((t) => t.name == name);
if (!t) return null;
return t;
}

@ -5,16 +5,14 @@
* @returns {number}
*/
export function randomInt(min: number, max: number): number {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
}
let _randomTimeOffsetForDebug = randomInt(-10000, 10000);
_randomTimeOffsetForDebug = 0;
console.log('random time offset', _randomTimeOffsetForDebug);
export function getCurrentTime() {
return Date.now() + _randomTimeOffsetForDebug;
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,46 +1,26 @@
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 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 [currentUserReady, setCurrentUserReady] = useState(false);
const room = useRoomRunningAndReadyChanged();
const showFeed = (room?.readyToParticipate && currentUserReady) || false;
return (
<>
<section className="content">
{showFeed ? feedContent : lobbyContent}
</section>
<footer>
<img src={haramlogo} className="haram-logo" alt="haramlogo"/>
<img src={logo} className="via-logo" alt="logo"/>
</footer>
</>
);
return (
<section className="content">
{showFeed ? (
<Feed />
) : (
<Lobby
currentUserReady={currentUserReady}
onCurrentUserReadyChange={(b: boolean) => setCurrentUserReady(b)}
/>
)}
</section>
);
};
export default Centurion;
export default Centurion;

@ -1,40 +1,220 @@
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) => {
const timeline = useTimeline();
import "../css/feed.css";
import FeedTicker from "./FeedTicker";
useUpdateAfterDelay(500)
declare global {
interface Window {
__feedShakeDebug?: boolean;
}
}
let liveFeed: TimelineItem[] = [];
function getNextItemDelay(timeline: Timeline | null, defaultDelay = 500) {
if (!timeline) {
return defaultDelay;
}
if (timeline != null) {
liveFeed = timeline.feed.filter(item => {
return item.timestamp * 1000 <= roomTime()
});
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();
return (
<Col className="time-feed" span={24} md={16}>
<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`}/>
</CSSTransition>
)
)}
</TransitionGroup>
const feedElement = useRef<HTMLDivElement>(null);
const [lastShotId, setLastShotId] = useState<string | null>(null);
useUpdateAfterDelay(getNextItemDelay(timeline));
if (!timeline) {
throw new TypeError("Feed without timeline.");
}
const time = calculateRoomTime();
const liveFeed = timeline.feed.filter((item) => {
return item.timestamp * 1000 <= time;
});
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 (
<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) =>
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>
);
};
export default Feed;

@ -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() {
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>
);
}
}
const FeedItem = ({ item }: FeedItemProps) => {
return (
<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,219 +1,313 @@
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) => {
// 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);
// 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);
}
function handleJoin(e: MouseEvent) {
connection.requestReady();
}
function applyRoomId(v: number) {
connection.requestJoin(v).then(v => {
setJoiningLobby(false);
setJoinLobbyError(!v);
})
setJoiningLobby(true)
}
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();
const isLeader = room?.isLeader || false;
const userCount = room?.userCount || 0;
async function handleJoin() {
await preloadAudio();
connection.requestSetReady();
props.onCurrentUserReadyChange?.(true);
}
async function applyRoomId(v: number) {
setJoiningLobby(true);
await connection.requestJoin(v);
setJoiningLobby(false);
setJoinLobbyError(!v);
}
function handleJoinRandomLobby() {
connection.requestJoinRandom();
setJoinLobbyError(false);
}
function handleTimelineNameSet(timelineName: any) {
setTimelineName(timelineName);
connection.setRoomOptions(
new RoomOptions(
seekTime * 1000 || 0,
timelineName || room?.timelineName || ""
)
);
}
function handleSetSeekTime(seekTime: number) {
setSeekTime(seekTime);
connection.setRoomOptions(
new RoomOptions(
seekTime * 1000 || 0,
timelineName || room?.timelineName || ""
)
);
}
function handleJoinRandomLobby() {
connection.requestJoinRandom()
setJoinLobbyError(false);
}
function preloadAudio(): Promise<boolean> {
setIsPreloading(true);
const songFile = timeline?.songFile;
function handleTimelineNameSet(timelineName: any) {
setTimelineName(timelineName);
connection.setRoomOptions(new RoomOptions(
seekTime || 0,
timelineName || room?.timelineName || ''))
if (!songFile) {
return Promise.resolve(false);
}
function handleSetSeekTime(seekTime: number) {
setSeekTime(seekTime);
connection.setRoomOptions(new RoomOptions(
seekTime * 1000 || 0,
timelineName || room?.timelineName || ''))
}
let leaderConfig = (
return new Promise<boolean>((resolve) => {
const audioElement = new Audio();
audioElement.addEventListener("canplaythrough", () => {
// 'canplaythrough' means the browser thinks it has buffered enough to play
// until the end.
setIsPreloading(false);
resolve(true);
});
audioElement.src = songFile;
});
}
const leaderConfig = (
<Row justify="center">
<Col>
<Form
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)}
/>
</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>
</Form.Item>
</Form>
<Button
block
type="primary"
loading={isPreloading}
onClick={handleJoin}
>
Start
</Button>
</Col>
</Row>
);
const nonLeaderConfig = (
<Row justify="center">
<Col>
<p>
{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>
)}
</p>
<Button
block
type="primary"
disabled={!room || props.currentUserReady}
loading={isPreloading}
onClick={handleJoin}
>
{room && props.currentUserReady
? "Wachten op het startsein"
: "Kom erbij"}
</Button>
</Col>
</Row>
);
return (
<div className="lobby">
<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>
<Col className="hints" span={24}>
<div>Honderd minuten...</div>
<div>Honderd shots...</div>
<div>Kun jij het aan?</div>
</Col>
</Row>
<br />
{!isConnected && (
<Row justify="center">
<Col>
<Form
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)}/>
</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>
</Form.Item>
</Form>
<Button
block
type="primary"
onClick={handleRequestStartClicked}>Start</Button>
</Col>
<Col className="lobby-connecting">
<h2>Verbinden...</h2>
</Col>
</Row>
)
)}
let nonLeaderConfig = (
{isConnected && (
<Row justify="center">
<Col>
<p>
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>}
</p>
<Button
block
<Col xs={24} sm={16} md={12} xl={10}>
<Card>
<h3>
Huidige lobby: <b>{room ? `#${room.id}` : "Geen lobby"}</b>
</h3>
{room && (
<Row>
{userCount === 1 ? (
<span>Er is één gebruiker aanwezig.</span>
) : (
<span>Er zijn {userCount} gebruikers aanwezig.</span>
)}
</Row>
)}
<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 />
<Row justify="center">
<Col>
<InputNumber
style={{ width: "calc(100% - 150px)" }}
min={1}
max={100000}
value={selectedRoomId || room?.id || 0}
onChange={(v) => setSelectedRoomId(v || 0)}
/>
<Button
style={{ width: "150px" }}
type="primary"
disabled={!room || room.readyToParticipate}
onClick={handleJoin}>{room && room.readyToParticipate ? '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"/>
loading={joiningLobby}
onClick={async () => {
await applyRoomId(selectedRoomId);
}}
>
Ga naar die lobby
</Button>
{joinLobbyError && (
<span style={{ color: red[4] }}>
Die lobby bestaat niet
</span>
)}
</Col>
</Row>
<Row>
<Col className="hints" span={24}>
<div>Honderd minuten...</div>
<div>Honderd shots...</div>
<div>Kun jij het aan?</div>
</Col>
</Row>
<br/>
</Row>
{!isConnected &&
<Row justify="center">
<Col className="lobby-connecting">
<h2>Verbinden...</h2>
</Col>
</Row>
}
{isConnected &&
<Row justify="center">
<Col xs={24} sm={16} md={12} xl={10} className="lobby-info">
<Card>
<h3>Huidige lobby: <b>{room?.id || 'Geen lobby'}</b></h3>
{/*<span>Verbonden met {connectionType}</span>*/}
{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/>
{room && (isLeader ? leaderConfig : nonLeaderConfig)}
<Divider/>
<Row justify="center">
<Col>
<InputNumber
style={{'width': 'calc(100% - 150px)'}}
min={1}
max={100000}
value={selectedRoomId || room?.id || 0}
onChange={(v) => setSelectedRoomId(v || 0)}/>
<Button
style={{'width': '150px'}}
type="primary"
loading={joiningLobby}
onClick={() => {
applyRoomId(selectedRoomId)
}}>Verander van lobby</Button>
{joinLobbyError &&
<span style={{color: red[4]}}>Die lobby bestaat niet</span>
}
</Col>
</Row>
<Row justify="center">
<span className={'lobby-options-or'}>of</span>
</Row>
<Row justify="center">
<Col>
<Button type="primary"
onClick={() => {
handleJoinRandomLobby()
}}>Join een willekeurige lobby</Button>
</Col>
</Row>
</Card>
<Row justify="center">
<span className={"lobby-options-or"}>of</span>
</Row>
<Row justify="center">
<Col>
<Button
type="primary"
onClick={() => {
handleJoinRandomLobby();
}}
>
Join een nieuwe lobby
</Button>
</Col>
</Row>
}
</div>
);
</Row>
</Card>
</Col>
</Row>
)}
<img src={haramlogo} className="haram-logo" alt="haramlogo"/>
<img src={logo} className="via-logo" alt="logo" />
</div>
);
};
export default Lobby;

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

@ -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();
const room = useRoomRunningAndReadyChanged();
const timeline = useTimelineSongFileChanged();
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.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;
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) {
throw new Error("No player after mount.");
}
let player = useRef<HTMLAudioElement>(null)
player.current.src = timeline.songFile;
}, [timeline]);
const [timesSeeked, setTimesSeeked] = useState(0);
const [hadError, setHadError] = useState(false);
function handlePlayerOnPlay(e: SyntheticEvent) {
e.preventDefault();
// For when the user manually started the player for when autoplay is off.
setHadError(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;
async function handlePlayerPause(e: SyntheticEvent) {
if (!shouldPlay()) {
// We should not be playing, pausing is fine.
console.log("should not play, paused");
return;
}
// 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;
e.preventDefault();
const query = parseQueryString(window.location.search);
if (query.nosound) {
return null;
if (room) {
setPlayerTime(room, true);
}
await player.current?.play();
}
if (player.current && player.current.dataset.src != timeline!!.songFile) {
player.current.dataset.src = timeline!!.songFile;
player.current.src = timeline!!.songFile;
function handlePlayerCanPlayThrough() {
if (!finishedLoading) {
setFinishedLoading(true);
connection.requestStart();
}
}
function shouldPlay() {
return (
player.current &&
timeline &&
room &&
room.running &&
room.readyToParticipate &&
!player.current.ended
);
}
async function startPlaying(manual: boolean) {
if (!player.current) {
return;
}
function handlePlayerOnPlay(e: SyntheticEvent) {
e.preventDefault();
if (player.current.paused && !hadError) {
setPlayerVolume(volume);
// For when the user manually started the player for when autoplay is off.
try {
await player.current.play();
setHadError(false);
if (shouldPlay()) {
startPlaying(true)
}
} catch (e) {
console.error("Error playing", e);
setHadError(true);
}
}
function shouldPlay() {
return player.current && timeline && room && room.running && room.readyToParticipate
if (!hadError && room) {
setPlayerTime(room, manual);
}
}
function startPlaying(manual: boolean) {
if (!player.current) return;
if (player.current.paused && !hadError) {
player.current.play().then(() => {
setHadError(false);
}).catch(e => {
console.error('Error playing', e);
setHadError(true);
})
}
if (!hadError) {
setPlayerTime(room!!, manual);
}
function setPlayerTime(room: Room, manualAdjustment: boolean) {
if (!player.current) {
return;
}
function setPlayerTime(room: Room, manualAdjustment: boolean) {
if (!player.current) return;
let targetTime = roomTime() / 1000;
let diff = player.current.currentTime - targetTime;
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);
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.');
}
}
// 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;
}
if (shouldPlay()) {
startPlaying(false)
} else {
if (player.current) {
player.current.pause();
}
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;
}
function render() {
return (
<audio ref={player} className='player' hidden={!hadError} controls={true} onPlay={handlePlayerOnPlay}/>
)
player.current.currentTime = targetTime;
player.current.playbackRate = Math.min(room.speedFactor, 5);
if (!manualAdjustment) {
setTimesSeeked(timesSeeked + 1);
}
return render();
}
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();
}
}
return (
<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,34 +1,41 @@
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();
useUpdateAfterDelay(1000);
let taken = 0;
let taken = 0;
const time = calculateRoomTime();
const prevShot = timeline.eventBeforeTime(time, "shot");
if (timeline) {
let [current, _] = timeline.eventAtTime(roomTime(), 'shot');
if (current) {
taken = current.shotCount!!;
}
}
if (prevShot) {
taken = prevShot.shotCount;
} else {
const nextShot = timeline.eventAfterTime(time, "shot");
taken = nextShot ? nextShot.shotCount - 1 : taken;
}
return (
<Col className="sider" span={24} md={4}>
<h1>Shots genomen:</h1>
<Progress type="circle"
percent={taken}
format={_ => taken + ' / 100'}
status="normal"
strokeColor={"#304ba3"}
strokeWidth={10}/>
</Col>
);
return (
<>
<h1>Shots genomen:</h1>
<Progress
type="circle"
percent={(taken / totalShots) * 100}
format={() => `${taken} / ${totalShots}`}
status="normal"
strokeColor={"#304ba3"}
strokeWidth={10}
/>
</>
);
};
export default ShotsTaken;

@ -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,335 +1,311 @@
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 = '/';
socket: SocketIOClient.Socket;
isConnected = new Sub<boolean>();
config = new Sub<Config | null>();
room = new Sub<Room | null>();
timeline = new Sub<Timeline | null>();
timeSyncIntervals = [500, 1000, 3000, 5000, 10000, 30000];
timeSyncs: { [requestId: number]: TimeSyncRequest } = {};
timeSyncTimeoutIds: number[] = [];
timeSyncTooOld = 120000;
roomTime = new Sub<number>();
calls: { [id: number]: Call } = {};
constructor() {
this.isConnected.set(false);
this.socket = io(this.url, {
autoConnect: false,
transports: ['websocket']
});
url = "/";
socket: Socket;
isConnected = new Sub<boolean>();
config = new Sub<Config | null>();
room = new Sub<Room | null>();
timeline = new Sub<Timeline | null>();
timeSyncIntervals = [500, 1000, 3000, 5000, 10000, 30000];
timeSyncs: { [requestId: number]: TimeSyncRequest } = {};
timeSyncTimeoutIds: number[] = [];
timeSyncTooOld = 120000;
roomTime = new Sub<number>();
constructor() {
this.isConnected.set(false);
this.socket = io(this.url, {
autoConnect: false,
transports: ["websocket"],
});
this.setupSocketListeners();
this.connect();
this.roomTime.set(0);
}
connect() {
this.socket.connect();
}
setupSocketListeners() {
this.socket.on("connect", async () => {
this.isConnected.set(true);
await this.onConnect();
});
this.socket.on("disconnect", () => {
this.isConnected.set(false);
this.onDisconnect();
});
this.socket.on("config", (data: any) => {
this.config.set(data.config);
});
this.socket.on("time_sync", (data: any) => {
this.timeSyncResponse(data.requestId, data.clientDiff, data.serverTime);
});
this.socket.on("room", (data: any) => {
if (data.room) {
this.setQueryLobbyId(data.room.id);
}
this.room.set(data.room);
});
this.socket.on("timeline", (data: any) => {
if (data.timeline) {
this.timeline.set(new Timeline(data.timeline));
} else {
this.timeline.set(null);
}
});
}
async onConnect() {
this.startTimeSync();
const lobbyId = this.getQueryLobbyId();
if (lobbyId) {
const exists = await this.requestJoin(lobbyId);
if (!exists) {
this.setQueryLobbyId();
this.requestJoinRandom();
}
} else {
this.requestJoinRandom();
}
}
this.setupSocketListeners();
onDisconnect() {
this.stopTimeSync();
}
this.connect();
private getQueryLobbyId(): number | null {
const query = new URLSearchParams(window.location.search);
const lobby = query.get("lobby");
this.roomTime.set(0);
if (!lobby) {
return null;
}
connect() {
this.socket.connect();
}
const lobbyId = Number.parseInt(query.get("lobby")!.toString());
setupSocketListeners() {
this.socket.on('connect', () => {
this.isConnected.set(true);
this.onConnect();
})
this.socket.on('disconnect', () => {
this.isConnected.set(false);
this.onDisconnect();
})
this.socket.on('config', (data: any) => {
this.config.set(data.config);
})
this.socket.on('time_sync', (data: any) => {
this.timeSyncResponse(data.requestId, data.clientDiff, data.serverTime);
})
this.socket.on('room', (data: any) => {
if (data.room) {
this.setQueryLobbyId(data.room.id);
}
this.room.set(data.room);
});
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];
});
if (!Number.isSafeInteger(lobbyId) || lobbyId < 1) {
return null;
}
onConnect() {
this.startTimeSync();
let lobbyId = this.getQueryLobbyId();
if (lobbyId) {
this.requestJoin(lobbyId).then(v => {
if (!v) {
this.setQueryLobbyId(null);
this.requestJoinRandom();
}
})
} else {
this.requestJoinRandom();
}
}
return lobbyId;
}
onDisconnect() {
this.stopTimeSync();
}
private setQueryLobbyId(lobbyId?: number) {
const newUrl = new URL(window.location.href);
autoStart() : boolean {
let query = parseQueryString(window.location.search);
return !!query.autostart;
if (lobbyId) {
newUrl.searchParams.set("lobby", String(lobbyId));
} else {
newUrl.searchParams.delete("lobby");
}
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
}
window.history.pushState({}, "", newUrl.toString());
}
setRoomOptions(roomOptions: RoomOptions) {
this.socket.emit("room_options", roomOptions);
}
requestStart() {
this.socket.emit("request_start");
}
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();
}
}
return null;
}
private setQueryLobbyId(lobbyId: number | null) {
let query = parseQueryString(window.location.search);
if (lobbyId) {
query.lobby = lobbyId.toString();
} else {
delete query.lobby;
);
});
}
async requestJoin(roomId: number): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
this.socket.emit(
"request_join",
roomId,
(err?: string, didJoinRoom?: boolean) => {
if (err) {
return reject(err);
}
return resolve(!!didJoinRoom);
}
let newUrl = window.location.protocol + "//" + window.location.host +
window.location.pathname + (Object.keys(query).length ? ('?' + stringifyQueryString(query)) : '');
window.history.pushState({}, '', newUrl);
}
async call(name: string, params: any) {
return new Promise<any>((resolve, reject) => {
let callback = (err: any, res: any) => {
if (err) {
return reject(err);
}
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)
}
requestStart(seekTime: number) {
this.socket.emit('request_start', {
seekTime: seekTime
});
}
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 true;
} else {
return false;
}
})
);
});
}
requestSetReady() {
this.socket.emit("request_set_ready");
}
requestJoinRandom() {
this.socket.emit("request_join_random");
}
startTimeSync() {
for (let i = 0; i < this.timeSyncIntervals.length; i++) {
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]);
this.timeSyncTimeoutIds.push(timeoutId);
}
}
requestReady() {
this.socket.emit('request_ready');
stopTimeSync() {
for (let i = 0; i < this.timeSyncTimeoutIds.length; i++) {
clearTimeout(this.timeSyncTimeoutIds[i]);
}
}
requestJoinRandom() {
this.socket.emit('request_join_random');
}
sendTimeSync(alsoSchedule: boolean) {
const sync = new TimeSyncRequest();
this.socket.emit("time_sync", sync.requestId, Date.now());
this.timeSyncs[sync.requestId] = sync;
startTimeSync() {
for (let i = 0; i < this.timeSyncIntervals.length; i++) {
let timeoutId = setTimeout(() => {
this.sendTimeSync(i === this.timeSyncIntervals.length - 1);
}, this.timeSyncIntervals[i]);
// @ts-ignore
this.timeSyncTimeoutIds.push(timeoutId);
}
if (alsoSchedule) {
setTimeout(() => {
this.sendTimeSync(true);
}, this.timeSyncIntervals[this.timeSyncIntervals.length - 1]);
}
}
stopTimeSync() {
for (let i = 0; i < this.timeSyncTimeoutIds.length; i++) {
clearTimeout(this.timeSyncTimeoutIds[i]);
}
timeSyncResponse(requestId: number, clientDiff: number, serverTime: number) {
const syncReq = this.timeSyncs[requestId];
if (!syncReq) {
return;
}
sendTimeSync(alsoSchedule: boolean) {
let sync = new TimeSyncRequest();
this.socket.emit('time_sync', sync.requestId, Date.now());
this.timeSyncs[sync.requestId] = sync;
syncReq.response(clientDiff, serverTime);
if (alsoSchedule) {
setTimeout(() => {
this.sendTimeSync(true);
}, this.timeSyncIntervals[this.timeSyncIntervals.length - 1]);
}
for (const i in this.timeSyncs) {
if (this.timeSyncs[i].start < Date.now() - this.timeSyncTooOld) {
delete this.timeSyncs[i];
break;
}
}
timeSyncResponse(requestId: number, clientDiff: number, serverTime: number) {
let syncReq = this.timeSyncs[requestId];
if (!syncReq) return
delete this.timeSyncs[requestId];
syncReq.response(clientDiff, serverTime);
for (let 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 {
return Date.now() + this.serverTimeOffset();
}
serverTimeOffset(): number {
let num = 0;
let sum = 0;
for (const i in this.timeSyncs) {
const sync = this.timeSyncs[i];
if (!sync.ready) continue;
sum += sync.offset;
num += 1;
}
serverTime(): number {
return Date.now() + this.serverTimeOffset();
if (num === 0) {
return 0;
}
serverTimeOffset(): number {
let num = 0;
let sum = 0;
for (let i in this.timeSyncs) {
let sync = this.timeSyncs[i];
if (!sync.ready) continue;
sum += sync.offset;
num += 1;
}
if (num === 0) {
return 0;
}
return Math.round(sum / num);
}
}
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;
}
return Math.round(sum / num);
}
}
let _timeSyncId = 0;
class TimeSyncRequest {
requestId: number;
start: number;
offset: number = 0;
ready = false;
constructor() {
this.requestId = _timeSyncId++;
this.start = Date.now();
}
response(clientDiff: number, serverTime: number) {
this.ready = true;
let now = Date.now();
let lag = now - this.start;
this.offset = serverTime - now + lag / 2;
// console.log('TIME SYNC', 'cdiff:', clientDiff, 'lag:',
// lag, 'diff:', serverTime - now, 'offset:', this.offset);
}
requestId: number;
start: number;
offset = 0;
ready = false;
constructor() {
this.requestId = _timeSyncId++;
this.start = Date.now();
}
response(clientDiff: number, serverTime: number) {
this.ready = true;
const now = Date.now();
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 {
return useSub(connection.room);
return useSub(connection.room);
}
export function useConfig(): Config | null {
return useSub(connection.config);
return useSub(connection.config);
}
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 {
return useSub(connection.timeline);
return useSub(connection.timeline);
}
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;
return (connection.serverTime() - room.startTime) * room.speedFactor;
/**
* 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,78 +1,125 @@
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;
this.timelineName = timelineName;
}
constructor(seekTime: number, timelineName: string) {
this.seekTime = seekTime;
this.timelineName = timelineName;
}
}
export class Timeline {
name: string
songFile: string
feed: TimelineItem[]
constructor(obj: any) {
this.name = obj.name;
this.songFile = obj.songFile;
this.feed = obj.feed;
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}`;
}
}
}
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;
getTotalShotCount(): number {
let maxShot = 0;
for (let i = 1; i < feedToSearch.length; i++) {
if (feedToSearch[i].timestamp * 1000 >= time) {
return [feedToSearch[i - 1], feedToSearch[i]];
}
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
);
}
}
}
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);
}
return [feedToSearch[feedToSearch.length - 1], null];
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);
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);
}
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;
}
return item.events.find((ev) => ev.type === type);
}
}
export interface TimelineItem {
timestamp: number,
events: TimestampEvent[]
id: string;
timestamp: number;
events: TimestampEvent[];
}
export interface TimestampEvent {
type: 'talk' | 'shot' | 'song' | 'time',
text: string[],
shotCount?: number
export const EVENT_PRIORITY: EventType[] = ["shot", "talk", "time", "song"];
export type EventType = "talk" | "shot" | "song" | "time";
interface TimestampEventBase {
id: string;
type: EventType;
text: string[];
}
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);
useEffect(() => {
let timeoutId = setTimeout(() => {
timedUpdateSet(v => v + 1);
}, delay);
return () => clearTimeout(timeoutId)
})
const [, timedUpdateSet] = useState(0);
useEffect(() => {
const timeoutId = setTimeout(() => {
timedUpdateSet((v) => v + 1);
}, delay);
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,23 +1,24 @@
import { Socket } from "socket.io-client";
/**
* Promisify emit.
* @param event
* @param arg
*/
export function emit(socket: SocketIOClient.Socket, event: string, arg: any = null) {
return new Promise((resolve, reject) => {
const cb = (err: any, res: any) => {
if (err) {
return reject(err);
}
resolve(res);
};
export function emit(socket: Socket, event: string, arg: any = null) {
return new Promise((resolve, reject) => {
const cb = (err: any, res: any) => {
if (err) {
return reject(err);
}
if (arg === null || typeof arg === 'undefined') {
socket.emit(event, cb);
} else {
socket.emit(event, arg, cb);
}
resolve(res);
};
})
}
if (arg === null || typeof arg === "undefined") {
socket.emit(event, cb);
} else {
socket.emit(event, arg, cb);
}
});
}

@ -1,40 +1,49 @@
import {useEffect, useState} from "react";
import { useEffect, useState } from "react";
export class Sub<T> {
_listeners: ((obj: T) => void)[] = [];
_current: any = null;
_listeners: ((obj: T) => void)[] = [];
_current: T | null = null;
subscribe(listener: any) {
if (this._listeners.indexOf(listener) < 0) {
this._listeners.push(listener);
}
subscribe(listener: any) {
if (this._listeners.indexOf(listener) < 0) {
this._listeners.push(listener);
}
}
unsubscribe(listener: any) {
let index = this._listeners.indexOf(listener);
if (index >= 0) {
this._listeners.splice(index, 1);
}
unsubscribe(listener: any) {
const index = this._listeners.indexOf(listener);
if (index >= 0) {
this._listeners.splice(index, 1);
}
}
get() {
return this._current;
}
get(): T | null {
return this._current;
}
set(obj: T) {
this._current = obj;
this._listeners.forEach(cb => cb(obj));
}
set(obj: T) {
this._current = obj;
this._listeners.forEach((cb) => cb(obj));
}
}
export function useSub<T>(sub: Sub<T>, effectChanges: ((v: T) => any[]) | null = null): T {
const [currentState, stateSetter] = useState(sub.get());
useEffect(() => {
let listener = (obj: T) => stateSetter(obj);
sub.subscribe(listener);
return () => sub.unsubscribe(listener);
}, effectChanges ? effectChanges(currentState) : []);
return currentState;
export function useSub<T>(
sub: Sub<T>,
effectChanges: ((v: T | null) => any[]) | null = null
): T | null {
const [currentState, stateSetter] = useState(sub.get());
useEffect(
() => {
const listener = (obj: T) => stateSetter(obj);
sub.subscribe(listener);
return () => sub.unsubscribe(listener);
// 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