This commit is contained in:
Sch1nken 2023-12-26 23:33:11 +01:00
commit bddd51dd5d
14 changed files with 8441 additions and 0 deletions

4492
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

26
client/package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "graffiti-client",
"version": "0.1.0",
"main": "./build/index.js",
"scripts": {
"dev": "webpack-dev-server --mode development",
"start": "npm run dev",
"build": "webpack"
},
"license": "MIT",
"devDependencies": {
"@webpack-cli/serve": "^2.0.5",
"html-webpack-plugin": "^5.6.0",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@pixi/utils": "^7.3.2",
"cors": "^2.8.5",
"dat.gui": "^0.7.9",
"pixi.js": "^7.3.2",
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2",
"webpack-dev-server": "^4.15.1"
}
}

215
client/src/App.js Normal file
View file

@ -0,0 +1,215 @@
import * as PIXI from 'pixi.js';
import {io} from 'socket.io-client';
import {DrawingApp, Layer} from './DrawingApp';
const socket = io('ws://192.168.0.155:7777');
var id = "";
var drawing_app = null;
var game_info = document.getElementById('game_state_info');
var pixi_wrapper = document.getElementById('pixi-wrapper');
function setGameInfo(info) {
game_info.innerHTML = info;
}
socket.on('connect', () => {
console.log("\n*** CONNECTED TO SOCKET *** ", socket.id);
id = socket.id;
console.log("\n*** Startup! ***");
});
document.getElementById('join_room_btn').addEventListener("click", (e) => {
var room_code = document.getElementById('room_code').value;
var username = document.getElementById('username').value;
socket.emit('join_room', room_code, username);
});
socket.on('room', (room_id) => {
// join the room
console.log(room_id);
document.getElementById('game_room_code').innerHTML = room_id;
document.getElementById('game_setup').style.display = "none";
document.getElementById('game_info').style.display = "initial";
});
socket.on('room_error', (error_msg) => {
alert(error_msg);
});
document.getElementById('create_room_btn').addEventListener("click", (e) => {
// Tell the server we want a new room
var username = document.getElementById('username').value;
socket.emit("create_room", username);
});
socket.on('player_data', (player_arr) => {
var count = player_arr.length;
document.getElementById('current_player_count').innerHTML = count;
var player_list = document.getElementById('player_list');
player_list.innerHTML = "";
player_arr.forEach((player) => {
player_list.innerHTML += "<li>" + player.player_name + "</li>";
})
document.getElementById('start_game_btn').disabled = (count < 5);
});
document.getElementById('start_game_btn').addEventListener('click', (e) => {
socket.emit('start_game');
});
socket.on('game_started', () => {
// game_data.game_leader =
console.log("yooo");
document.getElementById('pre_game_player_list').style.display = "none";
});
socket.on('leader_selected', (leader) => {
console.log(leader);
console.log(id);
setGameInfo(`Please wait while the leader ${leader.player_name} is selecting a topic to be painted`);
if(leader.id == id) {
setGameInfo("You are the game leader! Please choose a topic and a category that the topic fits in.");
document.getElementById('game_leader_input').style.display = "initial";
document.getElementById('submit_topic').addEventListener('click', (e) => {
var topic = document.getElementById('game_topic').value;
var category = document.getElementById('game_category').value;
socket.emit('topic_selected', topic, category);
})
}
});
socket.on('topic_selected', (topic, category) => {
console.log(topic, category);
document.getElementById('game_leader_input').style.display = "none";
setGameInfo(`Topic: ${topic}<br />Category: ${category}`);
});
socket.on('game_finished', (leader, player_list) => {
var pl = document.getElementById("endgame_player_list");
pl.innerHTML = "";
player_list.forEach((player) => {
if (player.id == leader.id) {
pl.innerHTML += `<li style="font-weight: bold; color: black;">${player.player_name} (Leader)</li>`;
}
else {
pl.innerHTML += `<li style="color: ${player.player_color};">${player.player_name}</li>`;
}
})
pl.style.display = "initial";
var ink_bar = document.getElementById('myBar');
ink_bar.style.display = "none";
document.getElementById('current_user_text').style.display = "none";
});
socket.on('actually_start_game', (player_data) => {
// Get player with our id to get the correct color
drawing_app = new DrawingApp(pixi_wrapper, player_data);
window.drawing_app = drawing_app;
});
socket.on('draw_data', (round_num, data) => {
console.log("Draw Data: ", round_num);
var cur_layer = drawing_app.layers[round_num];
cur_layer.drawPointLine(data[0], data[1], true);
});
socket.on('advance_round', (round_num, current_player) => {
var ink_bar = document.getElementById('myBar');
ink_bar.style.width = "100%";
var current_user_text = `It's&nbsp;<span style="white-space:pre; font-weight: bold; color: ${current_player.player_color}">${current_player.player_name}</span>'s turn to draw!`
document.getElementById('current_user_text').innerHTML = current_user_text;
if(id == current_player.id) {
ink_bar.style.backgroundColor = current_player.player_color;
ink_bar.style.visibility = "visible";
const cur_layer = drawing_app.layers[round_num];
cur_layer.enable();
cur_layer.setLivePaintProgressCallback((old_pos, new_pos) => {
var ink_pct = cur_layer.getPaintPercentage();
ink_bar.style.width = ((1.0 - ink_pct) * 100) + "%";
console.log(ink_pct);
socket.emit("draw_data", [old_pos, new_pos]);
});
cur_layer.setFinishedPaintingCallback(() => {
cur_layer.disable();
socket.emit("round_finished");
});
/*player_data.forEach((p) => {
if(p.id == id) {
ink_bar.style.backgroundColor = p.player_color;
// can probably get the color from current_player
// since we only show this if we are the current player
// yeah...
}
});*/
}
else {
ink_bar.style.visibility = "hidden";
}
// TODO: Set current drawing layer, activate if WE are drawing
// If we are not drawing, still hook drawing data from server into
// drawing app
// TODO: Figure out how to best "unhook" these afterwards
});
/*function display_colors() {
var color_selection = document.getElementById('color_selection');
color_selection.innerHTML = "";
available_colors.forEach((element) => {
let newDiv = document.createElement('div');
newDiv.classList.add('color');
newDiv.style.backgroundColor = element;
color_selection.appendChild(newDiv);
});
};*/
/*const drawing_app = new DrawingApp(pixi_wrapper, {
'players': [
{'player_id': 'A', 'color': '#c35a00'},
{'player_id': 'B', 'color': '#290097'}
]
});*/
/*drawing_app.layers[0].setLivePaintProgressCallback((old_pos, new_pos) => {
console.log(old_pos);
socket.emit("draw_data", [old_pos, new_pos]);
})*/
// Test

View file

@ -0,0 +1,61 @@
import * as PIXI from 'pixi.js';
const fragment = `
uniform float size;
uniform float erase;
uniform vec3 color;
uniform float smoothing;
void main(){
vec2 uv = vec2(gl_FragCoord.xy) / size;
float dst = distance(uv, vec2(0.5, 0.5)) * 2.;
float alpha = max(0., 1. - dst);
alpha = pow(alpha, smoothing);
if(erase == 0.)
gl_FragColor = vec4(color, 1) * alpha;
else
gl_FragColor = vec4(alpha);
}
`;
export default class BrushGenerator {
constructor(renderer) {
this.renderer = renderer;
this.filter = new PIXI.Filter(null, fragment, {
color: [0, 0, 0],
size: 16,
erase: 0,
smoothing: 0.01
});
}
get(size, color, smoothing) {
this.filter.uniforms.size = size;
this.filter.uniforms.color = color;
this.filter.uniforms.smoothing = smoothing;
const texture = PIXI.RenderTexture.create({width: size, height: size});
texture.baseTexture.premultipliedAlpha = true;
const sprite = new PIXI.Sprite();
sprite.width = size;
sprite.height = size;
sprite.filters = [this.filter];
this.renderer.render(sprite, {
renderTexture: texture,
clear: false,
});
return texture;
}
hexToArray(color) {
const r = color >> 16;
const g = (color & 0x00ffff) >> 8;
const b = color & 0x0000ff;
return [r / 255, g / 255, b / 255];
}
}

202
client/src/DrawingApp.js Normal file
View file

@ -0,0 +1,202 @@
import * as PIXI from 'pixi.js';
import BrushGenerator from './BrushGenerator';
import SpritePool from './SpritePool';
const max_ink_per_layer = 1000;
const layer_width = 512;
const layer_height = 512;
const brush_size = 16;
const brush_smoothing = 0.01;
export class Layer {
constructor(app, render_texture, brushGenerator, player_id, color) {
this.app = app;
this.render_texture = render_texture;
this.spritePool = new SpritePool();
this.ink_used = 0.0;
this.lifted = false;
this.player_id = player_id;
this.color = this.hexToRgb(color);
this.draw_buffer = new PIXI.Container();
this.sprite = new PIXI.Sprite(this.render_texture);
this.sprite.width = layer_width;
this.sprite.height = layer_height;
this.sprite.eventMode = 'none';
this.app.stage.addChildAt(this.sprite, 0);
this.brushTexture =
brushGenerator.get(brush_size, this.color, brush_smoothing);
this.drawingStarted = false;
this.lastPosition = null;
const onDown = (e) => {
const position = this.sprite.toLocal(e.data.global);
// position.x += 512;
// position.y += 512;
this.lastPosition = position;
this.drawingStarted = true;
};
const onMove = (e) => {
const position = this.sprite.toLocal(e.data.global);
// position.x += 512;
// position.y += 512;
// console.log(position);
if (this.drawingStarted) {
this.drawPointLine(this.lastPosition, position);
}
this.lastPosition = position;
};
const onUp = (e) => {
this.lifted = true;
this.drawingStarted = false;
this.finished_painting_cb();
};
this.sprite.on('mousedown', onDown);
this.sprite.on('touchstart', onDown);
this.sprite.on('mousemove', onMove);
this.sprite.on('touchmove', onMove);
this.sprite.on('mouseup', onUp);
this.sprite.on('touchend', onUp);
this.app.ticker.add(() => {
this.renderPoints();
});
}
getPaintPercentage() {
return Math.min(this.ink_used / max_ink_per_layer, 1.0);
}
hexToRgb(hex) {
var res = hex.match(/[a-f0-9]{2}/gi);
return res && res.length === 3 ? res.map(function(v) {
return parseInt(v, 16) / 255
}) :
[0, 0, 0];
}
setFinishedPaintingCallback(callback) {
this.finished_painting_cb = callback;
}
setLivePaintProgressCallback(callback) {
this.live_paint_progress_cb = callback;
}
drawPoint(x, y) {
const sprite = this.spritePool.get();
sprite.x = x;
sprite.y = y;
sprite.texture = this.brushTexture;
sprite.blendMode = PIXI.BLEND_MODES.NORMAL;
this.draw_buffer.addChild(sprite);
}
drawPointLine(oldPos, newPos, force = false) {
if (this.lifted) {
return;
}
const delta = {
x: oldPos.x - newPos.x,
y: oldPos.y - newPos.y,
};
const deltaLength = Math.sqrt(delta.x ** 2 + delta.y ** 2);
if (!force) {
this.ink_used += deltaLength;
this.live_paint_progress_cb(oldPos, newPos);
}
if (this.ink_used >= max_ink_per_layer) {
this.lifted = true;
this.drawingStarted = false;
this.finished_painting_cb();
return;
}
// TODO: Pass socket.io to tell server?
// Or save to array and just pass to server on round-end. Should be easier?
this.drawPoint(newPos.x, newPos.y);
if (deltaLength >= brush_size / 8) {
const additionalPoints = Math.ceil(deltaLength / (brush_size / 8));
for (let i = 1; i < additionalPoints; i++) {
const pos = {
x: newPos.x + delta.x * (i / additionalPoints),
y: newPos.y + delta.y * (i / additionalPoints),
};
this.drawPoint(pos.x, pos.y);
}
}
}
renderPoints() {
this.app.renderer.render(this.draw_buffer, {
renderTexture: this.render_texture,
clear: false,
});
this.draw_buffer.children = [];
this.spritePool.reset();
}
enable() {
this.sprite.eventMode = 'static';
}
disable() {
this.sprite.eventMode = 'none';
}
}
export class DrawingApp {
constructor(dom_elem, player_data) {
var self = this;
this.app = new PIXI.Application({
width: layer_width,
height: layer_height,
backgroundColor: 0xffffff,
});
this.brushGenerator = new BrushGenerator(this.app.renderer);
this.brushTexture = null;
this.layers = [];
for (var i = 0; i < player_data.length * 2; i++) {
const render_texture =
PIXI.RenderTexture.create({width: 1024, height: 1024});
var cur_player = player_data[i % player_data.length];
this.layers.push(new Layer(
this.app, render_texture, this.brushGenerator, cur_player.id,
cur_player.player_color));
}
dom_elem.appendChild(this.app.view);
}
get() {
return this.app;
}
lock_all_layers() {}
}

29
client/src/SpritePool.js Normal file
View file

@ -0,0 +1,29 @@
import * as PIXI from 'pixi.js';
export default class SpritePool {
constructor() {
this.sprites = [];
this.index = 0;
}
get() {
if(this.index < this.sprites.length) {
return this.sprites[this.index++];
}
const sprite = new PIXI.Sprite(PIXI.Texture.EMPTY);
sprite.anchor.set(0.5);
this.sprites.push(sprite);
return sprite;
}
reset() {
this.index = 0;
}
destroy() {
for(let i = 0; i < this.sprites.length; i++)
this.sprites[i].destroy();
}
}

162
client/src/index.html Normal file
View file

@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fake Artist</title>
<style>
body {
margin: 0 auto;
background-color: black;
font-size: 1.4vmax;
}
input {
font-size: 1.0vmax;
}
#color_selection {
display: grid;
grid-template-columns: 25px 25px 25px 25px 25px;
display: none;
}
.gameinfo {
color: #FFFFFF;
}
li {
color: #FFFFFF;
}
.color {
width: 25px;
height: 25px;
}
.center {
height: 100%;
display: flex;
justify-content: center;
}
#game_leader_input {
display: none;
}
#pixi-wrapper {
height: 100%;
display: flex;
justify-content: center;
display: initial;
}
#game_info {
color: #FFFFFF;
display: none;
}
#myProgress {
width: 100%;
background-color: black;
}
#myBar {
width: 100%;
height: 30px;
}
#pixi-wrapper {
height: 100%;
display: flex;
justify-content: center;
/*display: initial;*/
}
#current_user_text {
color: black;
background-color: #FFFFFF;
}
#endgame_player_list {
display: none;
color: black;
background-color: white;
list-style-type: none;
}
#pl_container {
background-color: white;
margin-bottom: 20px;
}
label {
color: white;
}
</style>
</head>
<body>
<div class="center">
<div id="game_setup">
<form>
<label>
Username:
<input id="username" type="text" placeholder="Username" maxlength="16" />
</label>
</form>
<form>
<label>
Room-Code: <input id="room_code" maxlength="4" type="text" placeholder="Room-Code" />
</label>
<input id="join_room_btn" type="button" value="Join Room" />
</form>
<form>
<input id="create_room_btn" type="button" value="Create Room" />
</form>
</div>
</div>
<div class="center">
<div id="game_info">
<span id="game_room_code"></span>
<div id="pre_game_player_list" class="gameinfo">
<span id="current_player_count">X</span>/<span id="max_player_count">10</span>
<ul id="player_list">
</ul>
<form>
<input id="start_game_btn" type="button" value="Start" />
</form>
</div>
<div>
<span id="game_state_info"></span>
</div>
<div id="game_leader_input">
<form>
<label>
Topic: <input type="text" placeholder="Topic" id="game_topic" />
</label>
<label>
Category: <input type="text" placeholder="Category" id="game_category" />
</label>
<input type="button" id="submit_topic" value="Start" />
</form>
</div>
</div>
</div>
<div id="game">
<div class="center" id="pl_container">
<ul id="endgame_player_list"></ul>
</div>
<div id="user_turn_info">
<div class="center" id="current_user_text"></div>
<div>
<div class="center" id="myProgress">
<div id="myBar"></div>
</div>
</div>
</div>
<div id="pixi-wrapper"></div>
</div>
</body>
</html>

372
client/src/server/server.js Normal file
View file

@ -0,0 +1,372 @@
const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const cors = require('cors');
const room_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
Array.prototype.remove = function() {
var what, a = arguments, L = a.length, ax;
while (L && this.length) {
what = a[--L];
while ((ax = this.indexOf(what)) !== -1) {
this.splice(ax, 1);
}
}
return this;
};
function randomInt(low, high) {
return Math.floor(Math.random() * (high - low + 1) + low);
}
function randomId() {
let out = '';
for (let i = 0; i < 4; i++) {
out += room_chars[randomInt(0, room_chars.length - 1)];
}
return out;
}
const available_colors = [
'#c35a00', '#290097', '#b8c400', '#8c8eff', '#47d058', '#cf0036', '#01cefd',
'#500027', '#006241', '#ffc4e4'
];
class Player {
id = null;
player_name = 'Player';
player_color = '';
constructor(id_, name_, player_color_) {
this.id = id_;
this.player_name = name_;
this.player_color = player_color_;
}
}
const GAME_STATE = {
PRE_GAME: 0,
TOPIC_SELECTION: 1,
DRAWING: 2,
VOTING: 3,
RESULTS: 4
}
class Game {
room_id = '';
topic = '';
category = '';
players = []; // Player Class
actual_players = [];
current_game_state = 0;
current_player = null;
leader = null;
fake_artist = null;
current_round = 0;
player_turns = [];
disconnected_during_session = [];
constructor(room_id_) {
this.room_id = room_id_
}
add_player(player) {
this.players.push(player);
}
remove_player(player_id) {
this.players.splice(
this.players.findIndex(item => item.id === player_id), 1)
}
}
var active_games = {};
/*
GameLoop:
- Lobby
- Wait for 5+ Players -> enable "start" button (any player can start the
game)
- Randomly select one Player to be "game leader"
- All other players have to draw
- They take turns, only getting to see the image once its their turn (may be
changed, depending) on how boring it gets to wait (maybe only let game leade
peek)
- Once everybody painted two strokes (two full rounds of the game)
Reveal the image to everybody
*/
initGame =
() => {
console.log('Initializing Game!');
let count = 0;
// Game.map = {};
// console.log("Map Created: ", Game.map);
}
app.use(cors());
app.use(express.static('static'));
app.get('/', (req, res) => {
res.sendFile('index.html');
});
io.on('connection', (socket) => {
console.log('User: ', socket.id, ' connected.');
socket.on('draw_data', (arr) => {
if (socket.room) {
var room_id = socket.room;
var game = active_games[room_id];
socket.broadcast.to(game.room_id)
.emit('draw_data', game.current_round - 1, arr);
}
console.log(arr[0], arr[1]);
});
// socket.on('')
socket.on('create_room', (username) => {
if (username == '') {
socket.emit('room_error', 'Invalid username!');
return;
}
console.log('New room');
var room_id = randomId();
console.log(room_id);
socket.join(room_id);
socket.emit('room', room_id);
console.log(io.sockets.adapter.rooms);
var game = new Game(room_id);
var player_color = available_colors[0];
for (var i = 0; i < available_colors.length; i++) {
var free = true;
for (var k = 0; k < game.players.length; k++) {
if (available_colors[i] == game.players[k].player_color) {
free = false;
break;
}
}
if (free) {
player_color = available_colors[i];
break;
}
}
var player = new Player(socket.id, username, player_color);
game.add_player(player);
socket.room = room_id;
active_games[room_id] = game;
io.to(room_id).emit('player_data', active_games[room_id].players);
console.log(JSON.stringify(active_games[room_id]));
});
socket.on('start_game', () => {
if (socket.room) {
var room_id = socket.room;
var game = active_games[room_id];
game.game_state = GAME_STATE.TOPIC_SELECTION;
io.to(room_id).emit('game_started');
var leader =
game.players[Math.floor(Math.random() * game.players.length)];
game.leader = leader;
var remaining_players = game.players.filter(e => e !== leader);
game.actual_players = remaining_players;
game.player_turns = game.actual_players.concat(game.actual_players);
var fake_artist = remaining_players[Math.floor(
Math.random() * remaining_players.length)];
game.fake_artist = fake_artist;
io.to(room_id).emit('leader_selected', leader);
}
});
socket.on('topic_selected', (topic, category) => {
console.log(topic, category);
if (socket.room) {
var room_id = socket.room;
var game = active_games[room_id];
game.topic = topic;
game.category = category;
game.players.forEach((player) => {
console.log(player.id);
if (player.id != game.fake_artist.id) {
console.log('OK');
io.to(player.id).emit('topic_selected', topic, category);
} else {
console.log('FAKE');
io.to(player.id).emit('topic_selected', '???', category);
}
});
game.game_state = GAME_STATE.DRAWING;
io.to(socket.room).emit('actually_start_game', game.actual_players);
// FIXME: Super slow for some reason
// Maybe just do the following:
// Have a manual advance on the clients (button, ink empty, on mouse up)
// If someone disconnects, just manually advance, no timeout needed
game.current_round = 0;
// game.current_player = game.player_turns[game.current_round];
// socket.to(socket.room).emit('advance_round', game.current_round,
// game.current_player);
play_next_turn(game);
}
});
function play_next_turn(game) {
console.log('Round: ', game.current_round);
if (game.game_state == GAME_STATE.VOTING) {
return;
}
if (game.current_round >= game.player_turns.length) {
game.game_state = GAME_STATE.VOTING;
console.log('GAME FINISHED!');
io.to(game.room_id).emit('game_finished', game.leader, game.players);
game.game_state = GAME_STATE.VOTING;
// TODO: Pass fake artist after voting... implement voting in the first
// place
return;
}
//while(game.current_player)
game.current_player = game.player_turns[game.current_round];
io.to(game.room_id)
.emit('advance_round', game.current_round, game.current_player);
game.current_round++;
}
socket.on('round_finished', () => {
if (socket.room) {
var room_id = socket.room;
var game = active_games[room_id];
if (game.game_state != GAME_STATE.DRAWING) {
return;
}
console.log('round finished!');
play_next_turn(game);
}
});
// socket.
socket.on('join_room', (room_id, username) => {
if (username == '') {
socket.emit('room_error', 'Invalid username!');
return;
}
if (!active_games[room_id]) {
socket.emit('room_error', 'This room does not exist!');
return;
}
var user_count = active_games[room_id].players.length;
if (user_count > 10) {
socket.emit('room_error', 'This room is full!');
return;
}
var game_state = active_games[room_id].current_game_state;
if (game_state != GAME_STATE.PRE_GAME) {
socket.emit('room_error', 'This game is already in progress!');
return;
}
var game = active_games[room_id];
socket.join(room_id);
var player_color = available_colors[0];
for (var i = 0; i < available_colors.length; i++) {
var free = true;
for (var k = 0; k < game.players.length; k++) {
if (available_colors[i] == game.players[k].player_color) {
free = false;
break;
}
}
if (free) {
player_color = available_colors[i];
break;
}
}
var player = new Player(socket.id, username, player_color);
active_games[room_id].add_player(player);
socket.room = room_id;
socket.emit('room', room_id);
io.to(room_id).emit('player_data', active_games[room_id].players);
console.log(JSON.stringify(active_games[room_id]));
});
socket.on('disconnect', () => {
console.log('User: ', socket.id, ' disconnected.');
if (socket.room) {
var room_id = socket.room;
var game = active_games[room_id];
game.remove_player(room_id);
io.to(room_id).emit('player_data', game.players);
console.log(socket.id, game.current_player);
if (game.game_state == GAME_STATE.DRAWING) {
game.disconnected_during_session.push(game.players[socket.id]);
/*console.log(game.player_turns);
game.player_turns = game.player_turns.filter(e => e.id != socket.id);
console.log(game.player_turns);*/
}
if (socket.id == game.current_player.id) {
play_next_turn(game);
}
}
});
});
http.listen(7777, '0.0.0.0', () => {
console.log('Listening on 7777');
})
initGame();

21
client/webpack.config.js Normal file
View file

@ -0,0 +1,21 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/App.js',
output: {
filename: './index.js',
path: path.resolve(__dirname, 'build')
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, './src/index.html'),
}),
],
devServer: {
static: path.join(__dirname, 'build'),
compress: true,
port: 8080
}
};

2801
client/yarn.lock Normal file

File diff suppressed because it is too large Load diff