Merge everything to main branch #1
37 changed files with 9881 additions and 1 deletions
106
.gitignore
vendored
Normal file
106
.gitignore
vendored
Normal file
|
@ -0,0 +1,106 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
.vercel
|
|
@ -1,2 +1,2 @@
|
|||
# BSX
|
||||
The card game B.S., but better!
|
||||
The card game B.S., but better! Currently under development.
|
||||
|
|
5
back/nodemon.json
Normal file
5
back/nodemon.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"ext": "js, ts, json",
|
||||
"exec": "npm run dev2 || exit 1",
|
||||
"ignore": ["dist/*"]
|
||||
}
|
2731
back/package-lock.json
generated
Normal file
2731
back/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
back/package.json
Normal file
26
back/package.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "bsx-back",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"build": "tsc",
|
||||
"dev": "nodemon",
|
||||
"dev2": "npm run build && npm start",
|
||||
"preinstall": "cd ../core && npm i && npm run build"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bsx-core": "file:../core",
|
||||
"dotenv": "^8.2.0",
|
||||
"socket.io": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.14.28",
|
||||
"@types/socket.io": "^2.1.13",
|
||||
"nodemon": "^2.0.7",
|
||||
"typescript": "^4.1.5"
|
||||
}
|
||||
}
|
56
back/src/Client.ts
Normal file
56
back/src/Client.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import {Socket} from 'socket.io';
|
||||
|
||||
import Room from './Room';
|
||||
|
||||
export const clients = new Map();
|
||||
export const usernameToId = new Map();
|
||||
|
||||
let newClientId = 0;
|
||||
|
||||
export default class Client {
|
||||
id: number;
|
||||
username: string;
|
||||
socket: Socket;
|
||||
disconnectTimeout: NodeJS.Timeout | null = null;
|
||||
room: Room | null = null;
|
||||
onListeners = new Map();
|
||||
onceListeners = new Map();
|
||||
constructor(username: string, socket: Socket) {
|
||||
this.id = newClientId++;
|
||||
this.username = username;
|
||||
this.socket = socket;
|
||||
}
|
||||
on(e: string, f: (...args: any[]) => void) {
|
||||
if (!this.onListeners.has(e))
|
||||
this.onListeners.set(e, []);
|
||||
this.onListeners.get(e).push(f);
|
||||
this.socket.on(e, f);
|
||||
}
|
||||
once(e: string, f: (...args: any[]) => void) {
|
||||
if (!this.onceListeners.has(e))
|
||||
this.onceListeners.set(e, []);
|
||||
this.onceListeners.get(e).push(f);
|
||||
this.socket.once(e, (...args) => {
|
||||
this.onceListeners.get(e).splice(this.onceListeners.get(e).indexOf(f), 1);
|
||||
f(...args);
|
||||
});
|
||||
}
|
||||
removeAllListeners(e: string) {
|
||||
this.socket.removeAllListeners(e);
|
||||
this.onListeners.delete(e);
|
||||
this.onceListeners.delete(e);
|
||||
}
|
||||
updateSocket(socket: Socket) {
|
||||
this.socket = socket;
|
||||
// Add listeners
|
||||
this.onListeners.forEach((fs, e) => fs.forEach((f: (...args: any[]) => void) => socket.on(e, f)));
|
||||
this.onceListeners.forEach((fs, e) => fs.forEach((f: (...args: any[]) => void) => {
|
||||
socket.once(e, (...args) => {
|
||||
this.onceListeners.get(e).splice(this.onceListeners.get(e).indexOf(f), 1);
|
||||
f(...args);
|
||||
});
|
||||
}));
|
||||
if (this.room)
|
||||
this.room.updateSocket(this);
|
||||
}
|
||||
}
|
169
back/src/Game.ts
Normal file
169
back/src/Game.ts
Normal file
|
@ -0,0 +1,169 @@
|
|||
import {canPlay, Card, cmpCard, Suit} from 'bsx-core';
|
||||
|
||||
import Client from './Client';
|
||||
import logSocket from './logSocket';
|
||||
import Room from './Room';
|
||||
import server from './server';
|
||||
|
||||
class Player {
|
||||
game: Game;
|
||||
client: Client;
|
||||
cards: Card[] = [];
|
||||
disconnected = false;
|
||||
disconnectListener?: () => void;
|
||||
rank = 0;
|
||||
passed = false;
|
||||
constructor(game: Game, client: Client) {
|
||||
this.game = game;
|
||||
this.client = client;
|
||||
}
|
||||
sendGameState() {
|
||||
this.cards.sort(cmpCard);
|
||||
const i = this.game.players.indexOf(this);
|
||||
const otherPlayers = [];
|
||||
for (let j = 1; j < this.game.players.length; ++j)
|
||||
otherPlayers.push(this.game.players[(i + j) % this.game.players.length]);
|
||||
this.client.socket.emit('gameState', {
|
||||
cards: this.cards,
|
||||
rank: this.rank,
|
||||
passed: this.passed,
|
||||
players: otherPlayers.map((p: Player) => ({
|
||||
username: p.client.username,
|
||||
numCards: p.cards.length,
|
||||
rank: p.rank,
|
||||
passed: p.passed
|
||||
})),
|
||||
lastPlayed: this.game.lastPlayed,
|
||||
lastPlayedPlayer: this.game.lastPlayedPlayer < 0 ? null : this.game.players[this.game.lastPlayedPlayer].client.username,
|
||||
playerTurn: this.game.players[this.game.playerTurn].client.username
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default class Game {
|
||||
room: Room;
|
||||
players: Player[] = [];
|
||||
lastPlayed: Card[] | null = null;
|
||||
lastPlayedPlayer = -1;
|
||||
playerTurn = 0;
|
||||
playersFinished = 0;
|
||||
constructor(room: Room) {
|
||||
this.room = room;
|
||||
this.start();
|
||||
}
|
||||
async start() {
|
||||
const cards = [];
|
||||
for (let i = 1; i <= 13; ++i)
|
||||
for (let j = 0; j < 4; ++j)
|
||||
cards.push({rank: i, suit: j});
|
||||
for (let i = 0; i < 52; ++i) {
|
||||
const j = Math.floor(Math.random() * (i+1));
|
||||
[cards[i], cards[j]] = [cards[j], cards[i]];
|
||||
}
|
||||
const handSize = Math.floor(52 / this.room.clients.length);
|
||||
for (let i = 0; i < this.room.clients.length; ++i) {
|
||||
this.players.push(new Player(this, this.room.clients[i]));
|
||||
this.players[i].cards = cards.slice(i * handSize, (i + 1) * handSize);
|
||||
}
|
||||
const startingPlayer = this.players.find((p: Player) => p.cards.some(
|
||||
(card: Card) => card.rank === 3 && card.suit === Suit.Clubs
|
||||
)) || this.players[0];
|
||||
if (this.room.clients.length === 3)
|
||||
startingPlayer.cards.push(cards[51]);
|
||||
this.playerTurn = this.players.indexOf(startingPlayer);
|
||||
while (true) {
|
||||
// Check if game ended
|
||||
const playersLeft: Player[] = [];
|
||||
this.players.forEach((p: Player) => {
|
||||
if (!p.rank && !p.disconnected)
|
||||
playersLeft.push(p);
|
||||
});
|
||||
if (playersLeft.length < 2) {
|
||||
if (playersLeft.length === 1)
|
||||
playersLeft[0].rank = ++this.playersFinished;
|
||||
break;
|
||||
}
|
||||
await this.round();
|
||||
}
|
||||
this.broadcastGameState();
|
||||
setTimeout(() => {
|
||||
server.to(this.room.name).emit('endGame');
|
||||
this.room.host.once('startGame', () => this.room.startGame());
|
||||
this.room.game = null;
|
||||
}, 5000);
|
||||
}
|
||||
broadcastGameState() {
|
||||
this.players.forEach((p: Player) => p.sendGameState());
|
||||
}
|
||||
async round() {
|
||||
while (true) {
|
||||
// Everyone passes
|
||||
if (this.playerTurn === this.lastPlayedPlayer) break;
|
||||
const p = this.players[this.playerTurn];
|
||||
// Guy passes
|
||||
if (p.rank || p.disconnected || p.passed) {
|
||||
this.playerTurn = (this.playerTurn + 1) % this.players.length;
|
||||
continue;
|
||||
}
|
||||
await this.turn();
|
||||
this.playerTurn = (this.playerTurn + 1) % this.players.length;
|
||||
// Check if person ends
|
||||
if (p.rank)
|
||||
break;
|
||||
}
|
||||
this.lastPlayed = null;
|
||||
this.lastPlayedPlayer = -1;
|
||||
this.players.forEach((p: Player) => p.passed = false);
|
||||
}
|
||||
async turn() {
|
||||
const p = this.players[this.playerTurn];
|
||||
if (p.passed) return;
|
||||
this.broadcastGameState();
|
||||
await new Promise<void>(resolve => {
|
||||
p.client.once('turn', cards => {
|
||||
delete p.disconnectListener;
|
||||
(() => {
|
||||
// Pass
|
||||
if (cards === null) {
|
||||
p.passed = true;
|
||||
return;
|
||||
}
|
||||
// Play
|
||||
if (cards && Array.isArray(cards) && cards.every((a: Card) => a && p.cards.some((b: Card) => !cmpCard(a, b))) && canPlay(this.lastPlayed, cards)) {
|
||||
// Cards have to be ascending
|
||||
let ok = true;
|
||||
for (let i = 0; i + 1 < cards.length; ++i)
|
||||
ok = ok && cmpCard(cards[i], cards[i + 1]) < 0;
|
||||
if (ok) {
|
||||
// Remove cards
|
||||
p.cards = p.cards.filter((a: Card) => !cards.some((b: Card) => !cmpCard(a, b)));
|
||||
// Check if won
|
||||
if (!p.cards.length)
|
||||
p.rank = ++this.playersFinished;
|
||||
this.lastPlayed = cards;
|
||||
this.lastPlayedPlayer = this.playerTurn;
|
||||
return;
|
||||
}
|
||||
}
|
||||
p.client.socket.disconnect();
|
||||
logSocket(p.client.socket, 'Bad cards argument on turn');
|
||||
})();
|
||||
resolve();
|
||||
});
|
||||
p.disconnectListener = () => {
|
||||
delete p.disconnectListener;
|
||||
p.client.removeAllListeners('turn');
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
updateSocket(client: Client) {
|
||||
this.players.find((p: Player) => p.client === client)!.sendGameState();
|
||||
}
|
||||
remove(client: Client) {
|
||||
const p = this.players.find((p: Player) => p.client === client)!;
|
||||
p.disconnected = true;
|
||||
if (p.disconnectListener)
|
||||
p.disconnectListener();
|
||||
}
|
||||
};
|
74
back/src/Room.ts
Normal file
74
back/src/Room.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import Client from './Client';
|
||||
import Game from './Game';
|
||||
import logSocket from './logSocket';
|
||||
import server from './server';
|
||||
|
||||
export default class Room {
|
||||
static rooms = new Map();
|
||||
static get(name: string) {
|
||||
return this.rooms.get(name);
|
||||
}
|
||||
name: string;
|
||||
password: string;
|
||||
clients: Client[] = [];
|
||||
host: Client;
|
||||
game: Game | null = null;
|
||||
constructor(name: string, password: string, host: Client) {
|
||||
this.name = name;
|
||||
this.password = password;
|
||||
this.host = host;
|
||||
this.add(host);
|
||||
host.once('startGame', () => this.startGame());
|
||||
Room.rooms.set(name, this);
|
||||
}
|
||||
add(client: Client) {
|
||||
this.clients.push(client);
|
||||
client.room = this;
|
||||
server.to(this.name).emit('roomUpdate', {
|
||||
users: this.clients.map((client: Client) => client.username),
|
||||
host: this.host.username
|
||||
});
|
||||
client.once('leaveRoom', () => this.remove(client));
|
||||
this.updateSocket(client);
|
||||
}
|
||||
updateSocket(client: Client) {
|
||||
client.socket.join(this.name);
|
||||
client.socket.emit('joinRoom', {name: this.name});
|
||||
client.socket.emit('roomUpdate', {
|
||||
users: this.clients.map((client: Client) => client.username),
|
||||
host: this.host.username
|
||||
});
|
||||
if (this.game)
|
||||
this.game.updateSocket(client);
|
||||
}
|
||||
remove(client: Client) {
|
||||
this.clients.splice(this.clients.indexOf(client), 1);
|
||||
client.room = null;
|
||||
client.socket.leave(this.name);
|
||||
client.socket.emit('leaveRoom');
|
||||
if (!this.clients.length) {
|
||||
Room.rooms.delete(this.name);
|
||||
return;
|
||||
}
|
||||
if (this.host === client) {
|
||||
this.host.removeAllListeners('startGame');
|
||||
this.host = this.clients[0];
|
||||
if (!this.game)
|
||||
this.host.once('startGame', () => this.startGame());
|
||||
}
|
||||
server.to(this.name).emit('roomUpdate', {
|
||||
users: this.clients.map((client: Client) => client.username),
|
||||
host: this.host.username
|
||||
});
|
||||
if (this.game)
|
||||
this.game.remove(client);
|
||||
}
|
||||
startGame() {
|
||||
if (this.clients.length < 3) {
|
||||
this.host.socket.disconnect();
|
||||
logSocket(this.host.socket, 'Not enough users for startGame');
|
||||
return;
|
||||
}
|
||||
this.game = new Game(this);
|
||||
}
|
||||
};
|
155
back/src/index.ts
Normal file
155
back/src/index.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
import dotenv from 'dotenv';
|
||||
import {Socket} from 'socket.io';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
import Client, {clients, usernameToId} from './Client';
|
||||
import logSocket from './logSocket';
|
||||
import Room from './Room';
|
||||
import server from './server';
|
||||
|
||||
server.on('connection', (socket: Socket) => {
|
||||
logSocket(socket, 'Connect');
|
||||
let client: Client | null = null;
|
||||
|
||||
socket.on('login', username => {
|
||||
logSocket(socket, `Login as ${username}`);
|
||||
// Shouldn't login again???
|
||||
if (client) {
|
||||
socket.disconnect();
|
||||
logSocket(socket, 'Already logged in');
|
||||
return;
|
||||
}
|
||||
// Bad username argument
|
||||
if (typeof username !== 'string' || username.length < 1 || username.length > 15) {
|
||||
socket.disconnect();
|
||||
logSocket(socket, 'Bad username argument on login');
|
||||
return;
|
||||
}
|
||||
// Character filter
|
||||
if (!username.match(/^[0-9a-zA-Z]+$/)) {
|
||||
socket.emit('loginRes', {success: false, error: 'Username can only consist of alphanumeric characters'});
|
||||
return;
|
||||
}
|
||||
if (usernameToId.has(username)) {
|
||||
const client2 = clients.get(usernameToId.get(username)) as Client;
|
||||
if(client2.socket.connected || client2.socket.handshake.address !== socket.handshake.address) {
|
||||
socket.emit('loginRes', {success: false, error: 'Username taken'});
|
||||
logSocket(socket, 'Username taken');
|
||||
return;
|
||||
}
|
||||
client = client2;
|
||||
clearTimeout(client.disconnectTimeout!);
|
||||
// client.socket = socket;
|
||||
client.updateSocket(socket);
|
||||
socket.emit('loginRes', {success: true});
|
||||
// if (client.room)
|
||||
// client.room.updateSocket(client);
|
||||
logSocket(socket, 'Reconnect login successful');
|
||||
} else {
|
||||
client = new Client(username, socket);
|
||||
clients.set(client.id, client);
|
||||
usernameToId.set(username, client.id);
|
||||
socket.emit('loginRes', {success: true});
|
||||
logSocket(socket, 'Login successful');
|
||||
|
||||
client.on('joinRoom', (name, password) => {
|
||||
client = client as Client;
|
||||
// Bad name argument
|
||||
if (typeof name !== 'string' || name.length < 1 || name.length > 15) {
|
||||
socket.disconnect();
|
||||
logSocket(socket, 'Bad name argument on joinRoom');
|
||||
return;
|
||||
}
|
||||
// Bad password argument
|
||||
if (typeof password !== 'string' || password.length > 15) {
|
||||
socket.disconnect();
|
||||
logSocket(socket, 'Bad password argument on joinRoom');
|
||||
return;
|
||||
}
|
||||
// Character filter
|
||||
if (!name.match(/^[0-9a-zA-Z ]+$/)) {
|
||||
socket.emit('joinRoomRes', {success: false, error: 'Room name can only consist of alphanumeric characters and spaces'});
|
||||
return;
|
||||
}
|
||||
// Make sure not in room
|
||||
if (client.room) {
|
||||
socket.disconnect();
|
||||
logSocket(socket, 'Already in room on joinRoom');
|
||||
return;
|
||||
}
|
||||
// Make sure room exists
|
||||
if (!Room.get(name)) {
|
||||
socket.emit('joinRoomRes', {success: false, error: `Room "${name}" does not exist`});
|
||||
return;
|
||||
}
|
||||
const room = Room.get(name);
|
||||
// Make sure password is correct
|
||||
if (password !== room.password) {
|
||||
socket.emit('joinRoomRes', {success: false, error: 'Password is incorrect'});
|
||||
return;
|
||||
}
|
||||
// Make sure game has not yet started
|
||||
if (room.game) {
|
||||
socket.emit('joinRoomRes', {success: false, error: 'The game has already started'});
|
||||
return;
|
||||
}
|
||||
// Make sure room has space
|
||||
if (room.clients.length > 3) {
|
||||
socket.emit('joinRoomRes', {success: false, error: 'Room does not have enough space'});
|
||||
return;
|
||||
}
|
||||
// Success
|
||||
socket.emit('joinRoomRes', {success: true});
|
||||
room.add(client);
|
||||
});
|
||||
|
||||
client.on('createRoom', (name, password) => {
|
||||
client = client as Client;
|
||||
// Bad name argument
|
||||
if (typeof name !== 'string' || name.length < 1 || name.length > 15) {
|
||||
socket.disconnect();
|
||||
logSocket(socket, 'Bad name argument on createRoom');
|
||||
return;
|
||||
}
|
||||
// Bad password argument
|
||||
if (typeof password !== 'string' || password.length > 15) {
|
||||
socket.disconnect();
|
||||
logSocket(socket, 'Bad password argument on createRoom');
|
||||
return;
|
||||
}
|
||||
// Character filter
|
||||
if (!name.match(/^[0-9a-zA-Z ]+$/)) {
|
||||
socket.emit('createRoomRes', {success: false, error: 'Room name can only consist of alphanumeric characters and spaces'});
|
||||
return;
|
||||
}
|
||||
// Make sure not in room
|
||||
if (client.room) {
|
||||
socket.disconnect();
|
||||
logSocket(socket, 'Already in room on createRoom');
|
||||
return;
|
||||
}
|
||||
// Make sure room does not exist
|
||||
if (Room.get(name)) {
|
||||
socket.emit('createRoomRes', {success: false, error: `Room "${name}" already exists`});
|
||||
return;
|
||||
}
|
||||
// Success
|
||||
socket.emit('createRoomRes', {success: true});
|
||||
new Room(name, password, client);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
logSocket(socket, 'Disconnect');
|
||||
if (!client) return;
|
||||
client.disconnectTimeout = setTimeout(() => {
|
||||
client = client as Client;
|
||||
if (client.room) client.room.remove(client);
|
||||
logSocket(socket, 'Full disconnect');
|
||||
clients.delete(client.id);
|
||||
usernameToId.delete(client.username);
|
||||
}, 60000);
|
||||
});
|
||||
});
|
5
back/src/logSocket.ts
Normal file
5
back/src/logSocket.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {Socket} from 'socket.io';
|
||||
|
||||
export default (socket: Socket, s: string) => {
|
||||
console.log(`[${socket.id}, ${socket.handshake.address}] ${s}`);
|
||||
};
|
17
back/src/server.ts
Normal file
17
back/src/server.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import fs from 'fs';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import {Server} from 'socket.io';
|
||||
|
||||
//export default new Server({cors: {origin: process.env.ORIGIN, methods: ['GET', 'POST']}});
|
||||
|
||||
const base = process.env.SSL_KEY && process.env.SSL_CERT && process.env.SSL_CA ? https.createServer({
|
||||
"key": fs.readFileSync(process.env.SSL_KEY),
|
||||
"cert": fs.readFileSync(process.env.SSL_CERT),
|
||||
"ca": fs.readFileSync(process.env.SSL_CA)
|
||||
}) : http.createServer();
|
||||
base.listen(+process.env.PORT!, () => {
|
||||
console.log(`Listening on port ${process.env.PORT}`);
|
||||
});
|
||||
|
||||
export default new Server(base, {cors: {origin: process.env.ORIGIN, methods: ['GET', 'POST']}});
|
11
back/tsconfig.json
Normal file
11
back/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
}
|
37
core/package-lock.json
generated
Normal file
37
core/package-lock.json
generated
Normal file
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "bsx-core",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bsx-core",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"typescript": "^4.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz",
|
||||
"integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz",
|
||||
"integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
14
core/package.json
Normal file
14
core/package.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "bsx-core",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"typescript": "^4.1.5"
|
||||
}
|
||||
}
|
8
core/src/Card.ts
Normal file
8
core/src/Card.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import Suit from './Suit';
|
||||
|
||||
interface Card {
|
||||
rank: number;
|
||||
suit: Suit;
|
||||
}
|
||||
|
||||
export default Card;
|
8
core/src/Suit.ts
Normal file
8
core/src/Suit.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
enum Suit {
|
||||
Clubs,
|
||||
Diamonds,
|
||||
Hearts,
|
||||
Spades
|
||||
}
|
||||
|
||||
export default Suit;
|
72
core/src/canPlay.ts
Normal file
72
core/src/canPlay.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import Card from './Card';
|
||||
import cmpCard from './cmpCard';
|
||||
import realRank from './realRank';
|
||||
|
||||
class Hand {
|
||||
size: number;
|
||||
overrideSize: boolean;
|
||||
matchFunc: (cards: Card[]) => Card | null;
|
||||
constructor(size: number, overrideSize: boolean, matchFunc: (cards: Card[]) => Card | null) {
|
||||
this.size = size;
|
||||
this.overrideSize = overrideSize;
|
||||
this.matchFunc = matchFunc;
|
||||
}
|
||||
match(cards: Card[]) {
|
||||
return cards.length === this.size && this.matchFunc(cards);
|
||||
}
|
||||
}
|
||||
|
||||
const chkFlush = (cards: Card[]) => cards.every((card: Card) => card.suit === cards[0].suit);
|
||||
const chkStraight = (cards: Card[]) => {
|
||||
let ok = true;
|
||||
for (let i = 1; i < 5; ++i)
|
||||
ok = ok && realRank(cards[0]) + i === realRank(cards[i]);
|
||||
if (ok) return true;
|
||||
// 3 4 5 6 2
|
||||
ok = cards[4].rank === 2;
|
||||
for (let i = 0; i < 4; ++i)
|
||||
ok = ok && cards[0].rank === 3 + i;
|
||||
if (ok) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const hands = [
|
||||
// Straight Flush
|
||||
new Hand(5, true, (cards: Card[]) => chkFlush(cards) && chkStraight(cards) ? cards[4] : null),
|
||||
// Four of a Kind
|
||||
new Hand(5, true, (cards: Card[]) =>
|
||||
(cards[0].rank === cards[3].rank ? cards[3] : null) ||
|
||||
(cards[1].rank === cards[4].rank ? cards[4] : null)
|
||||
),
|
||||
// Full House
|
||||
new Hand(5, false, (cards: Card[]) =>
|
||||
(cards[0].rank === cards[2].rank && cards[3].rank === cards[4].rank ? cards[2] : null) ||
|
||||
(cards[0].rank === cards[1].rank && cards[2].rank === cards[4].rank ? cards[4] : null)
|
||||
),
|
||||
// Straight
|
||||
new Hand(5, false, (cards: Card[]) => chkStraight(cards) ? cards[4] : null),
|
||||
// Flush
|
||||
new Hand(5, false, (cards: Card[]) => chkFlush(cards) ? cards[4] : null),
|
||||
// Pair
|
||||
new Hand(2, false, (cards: Card[]) => cards[0].rank === cards[1].rank ? cards[1] : null),
|
||||
// Single
|
||||
new Hand(1, false, (cards: Card[]) => cards[0])
|
||||
];
|
||||
|
||||
const parseHand = (cards: Card[]) => {
|
||||
for (let i = 0; i < hands.length; ++i) {
|
||||
const card = hands[i].match(cards);
|
||||
if (card) return {id: i, card};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default function canPlay(prev: Card[] | null, cur: Card[]) {
|
||||
const curHand = parseHand(cur);
|
||||
if (!curHand) return false;
|
||||
if (!prev) return true;
|
||||
const prevHand = parseHand(prev)!;
|
||||
if (!hands[curHand.id].overrideSize && cur.length !== prev.length) return false;
|
||||
if (curHand.id !== prevHand.id) return curHand.id < prevHand.id;
|
||||
return cmpCard(prevHand.card, curHand.card) < 0;
|
||||
};
|
8
core/src/cmpCard.ts
Normal file
8
core/src/cmpCard.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import Card from './Card';
|
||||
import realRank from './realRank';
|
||||
|
||||
export default function cmpCard(a: Card, b: Card) {
|
||||
if (a.rank !== b.rank)
|
||||
return realRank(a) - realRank(b);
|
||||
return a.suit - b.suit;
|
||||
};
|
5
core/src/index.ts
Normal file
5
core/src/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export {default as canPlay} from './canPlay';
|
||||
export {default as Card} from './Card';
|
||||
export {default as cmpCard} from './cmpCard';
|
||||
export {default as realRank} from './realRank';
|
||||
export {default as Suit} from './Suit';
|
5
core/src/realRank.ts
Normal file
5
core/src/realRank.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Card from './Card';
|
||||
|
||||
export default function realRank(card: Card) {
|
||||
return (card.rank + 10) % 13;
|
||||
}
|
12
core/tsconfig.json
Normal file
12
core/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true
|
||||
}
|
||||
}
|
36
front.md
Normal file
36
front.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Front
|
||||
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
46
front/components/CreateRoomForm.tsx
Normal file
46
front/components/CreateRoomForm.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import {useState} from 'react';
|
||||
|
||||
export default function CreateRoomForm({socket} : {socket: SocketIOClient.Socket}) {
|
||||
const [name, setName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Create Room</p>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={15}
|
||||
placeholder="Room name..."
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
disabled={waiting}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={15}
|
||||
placeholder="Password..."
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={waiting}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setWaiting(true);
|
||||
socket.emit('createRoom', name, password);
|
||||
socket.once('createRoomRes', (data: {success: boolean; error?: string}) => {
|
||||
setWaiting(false);
|
||||
if (!data.success)
|
||||
setError(data.error!);
|
||||
});
|
||||
}}
|
||||
disabled={!name || waiting}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
{waiting && 'Creating room...'}
|
||||
<p style={{color: 'red'}}>{error}</p>
|
||||
</>
|
||||
);
|
||||
};
|
46
front/components/JoinRoomForm.tsx
Normal file
46
front/components/JoinRoomForm.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import {useState} from 'react';
|
||||
|
||||
export default function JoinRoomForm({socket} : {socket: SocketIOClient.Socket}) {
|
||||
const [name, setName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Join Room</p>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={15}
|
||||
placeholder="Room name..."
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
disabled={waiting}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={15}
|
||||
placeholder="Password..."
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={waiting}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
setWaiting(true);
|
||||
socket.emit('joinRoom', name, password);
|
||||
socket.once('joinRoomRes', (data: {success: boolean; error?: string}) => {
|
||||
setWaiting(false);
|
||||
if (!data.success)
|
||||
setError(data.error!);
|
||||
});
|
||||
}}
|
||||
disabled={!name || waiting}
|
||||
>
|
||||
Join
|
||||
</button>
|
||||
{waiting && 'Joining room...'}
|
||||
<p style={{color: 'red'}}>{error}</p>
|
||||
</>
|
||||
);
|
||||
};
|
41
front/components/LoginForm.tsx
Normal file
41
front/components/LoginForm.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import {useEffect, useState} from 'react';
|
||||
|
||||
export default function LoginForm({finish, socket, username}: {finish: (s: string) => void, socket: SocketIOClient.Socket, username: string | null}) {
|
||||
const [value, setValue] = useState(username || '');
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const login = () => {
|
||||
setWaiting(true);
|
||||
socket.emit('login', value);
|
||||
socket.once('loginRes', (data: {success: boolean; error?: string}) => {
|
||||
setWaiting(false);
|
||||
if (data.success)
|
||||
finish(value);
|
||||
else
|
||||
setError(data.error!);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (username) login();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={15}
|
||||
placeholder="Your username..."
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
disabled={waiting}
|
||||
/>
|
||||
<button onClick={login} disabled={!value || waiting}>
|
||||
Ok
|
||||
</button>
|
||||
{waiting && 'Logging in...'}
|
||||
<p style={{color: 'red'}}>{error}</p>
|
||||
</>
|
||||
);
|
||||
};
|
2
front/next-env.d.ts
vendored
Normal file
2
front/next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
5763
front/package-lock.json
generated
Normal file
5763
front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
25
front/package.json
Normal file
25
front/package.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "bsx-front",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"tsc": "tsc",
|
||||
"preinstall": "cd ../core && npm i && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"bsx-core": "file:../core",
|
||||
"next": "10.0.6",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"socket.io-client": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.14.27",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/socket.io-client": "^1.4.35",
|
||||
"typescript": "^4.1.5"
|
||||
}
|
||||
}
|
7
front/pages/_app.js
Normal file
7
front/pages/_app.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import '../styles/globals.css'
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
|
||||
export default MyApp
|
5
front/pages/api/hello.js
Normal file
5
front/pages/api/hello.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
|
||||
export default (req, res) => {
|
||||
res.status(200).json({ name: 'John Doe' })
|
||||
}
|
159
front/pages/game.tsx
Normal file
159
front/pages/game.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
import {canPlay, Card} from 'bsx-core';
|
||||
import {useEffect, useState} from 'react';
|
||||
import io from 'socket.io-client';
|
||||
|
||||
import CreateRoomForm from '../components/CreateRoomForm';
|
||||
import JoinRoomForm from '../components/JoinRoomForm';
|
||||
import LoginForm from '../components/LoginForm';
|
||||
|
||||
interface GameState {
|
||||
cards: Card[],
|
||||
players: {username: string, numCards: number, rank: number}[],
|
||||
lastPlayed: Card[] | null,
|
||||
lastPlayedPlayer: string | null,
|
||||
playerTurn: string
|
||||
}
|
||||
|
||||
const rankStrs = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
|
||||
const suitChars = ['♣', '♦', '♥', '♠'];
|
||||
|
||||
export default function Game() {
|
||||
const [socket, setSocket] = useState<SocketIOClient.Socket | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [loggedIn, setLoggedIn] = useState(false);
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
|
||||
const [room, setRoom] = useState<string | null>(null);
|
||||
const [roomUsers, setRoomUsers] = useState<string[]>([]);
|
||||
const [roomHost, setRoomHost] = useState('');
|
||||
|
||||
const [gameState, setGameState] = useState<GameState | null>(null);
|
||||
const [cardSelected, setCardSelected] = useState<boolean[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const socket = io(process.env.NEXT_PUBLIC_BACK_HOST!);
|
||||
setSocket(socket);
|
||||
socket.on('connect', () => setConnected(true));
|
||||
socket.on('disconnect', () => {
|
||||
setConnected(false);
|
||||
setLoggedIn(false);
|
||||
setRoom(null);
|
||||
setGameState(null);
|
||||
});
|
||||
|
||||
socket.on('joinRoom', (data: {name: string}) => setRoom(data.name));
|
||||
socket.on('leaveRoom', () => {
|
||||
setRoom(null);
|
||||
setGameState(null);
|
||||
});
|
||||
socket.on('roomUpdate', (data: {users: string[], host: string}) => {
|
||||
setRoomUsers(data.users);
|
||||
setRoomHost(data.host);
|
||||
});
|
||||
|
||||
socket.on('gameState', (data: GameState) => {
|
||||
setGameState(data);
|
||||
if (data.cards.length !== cardSelected.length)
|
||||
setCardSelected(new Array(data.cards.length).fill(false));
|
||||
});
|
||||
socket.on('endGame', () => setGameState(null));
|
||||
|
||||
return () => {socket.close()};
|
||||
}, []);
|
||||
|
||||
if (!socket) return null;
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<LoginForm
|
||||
socket={socket}
|
||||
finish={(s) => {
|
||||
setLoggedIn(true);
|
||||
setUsername(s);
|
||||
}}
|
||||
username={username}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!room) {
|
||||
return (
|
||||
<>
|
||||
<p>Logged in as {username}</p>
|
||||
<hr />
|
||||
<JoinRoomForm socket={socket} />
|
||||
<hr />
|
||||
<CreateRoomForm socket={socket} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (!gameState) {
|
||||
return (
|
||||
<>
|
||||
<p>Room {room}</p>
|
||||
<p>Users:</p>
|
||||
<ul>
|
||||
{roomUsers.map(user => <li key={user}>{user + (user === roomHost ? ' (Host)':'')}</li>)}
|
||||
</ul>
|
||||
<button onClick={() => socket.emit('leaveRoom')}>Leave</button>
|
||||
{username === roomHost &&
|
||||
<button
|
||||
onClick={() => socket.emit('startGame')}
|
||||
disabled={roomUsers.length < 3}
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
const selectedCards = gameState.cards.filter((_, i) => cardSelected[i]);
|
||||
return (
|
||||
<>
|
||||
<ul>
|
||||
{gameState.players.map(p => (
|
||||
<li key={p.username}>
|
||||
{p.username + (p.rank ? ` (Rank ${p.rank})` : '') + (p.numCards ? ` (${p.numCards} cards)` : '')}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<p>Last played:</p>
|
||||
{gameState.lastPlayed ? (
|
||||
<>
|
||||
{gameState.lastPlayed.map((card, i) => <span key={i}>{rankStrs[card.rank]+' '+suitChars[card.suit]+'|'}</span>)}
|
||||
{` by ${gameState.lastPlayedPlayer}`}
|
||||
</>
|
||||
) : '(Nothing)'}
|
||||
</div>
|
||||
{`It's ${gameState.playerTurn}'s turn!`}
|
||||
<div>
|
||||
<p>Your cards:</p>
|
||||
{gameState.cards.map((card, i) => (
|
||||
<label key={card.rank+' '+card.suit}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={cardSelected[i] || false}
|
||||
onChange={() => {
|
||||
const cardSelected2 = [...cardSelected];
|
||||
cardSelected2[i] = !cardSelected[i];
|
||||
setCardSelected(cardSelected2);
|
||||
}}
|
||||
/>
|
||||
{rankStrs[card.rank]+' '+suitChars[card.suit]}
|
||||
</label>
|
||||
))}
|
||||
<button
|
||||
onClick={() => socket.emit('turn', selectedCards)}
|
||||
disabled={username !== gameState.playerTurn || !canPlay(gameState.lastPlayed, selectedCards)}
|
||||
>
|
||||
Play
|
||||
</button>
|
||||
<button
|
||||
onClick={() => socket.emit('turn', null)}
|
||||
disabled={username !== gameState.playerTurn}
|
||||
>
|
||||
Pass
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
65
front/pages/index.js
Normal file
65
front/pages/index.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
import Head from 'next/head'
|
||||
import styles from '../styles/Home.module.css'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<main className={styles.main}>
|
||||
<h1 className={styles.title}>
|
||||
Welcome to <a href="https://nextjs.org">Next.js!</a>
|
||||
</h1>
|
||||
|
||||
<p className={styles.description}>
|
||||
Get started by editing{' '}
|
||||
<code className={styles.code}>pages/index.js</code>
|
||||
</p>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a href="https://nextjs.org/docs" className={styles.card}>
|
||||
<h3>Documentation →</h3>
|
||||
<p>Find in-depth information about Next.js features and API.</p>
|
||||
</a>
|
||||
|
||||
<a href="https://nextjs.org/learn" className={styles.card}>
|
||||
<h3>Learn →</h3>
|
||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/vercel/next.js/tree/master/examples"
|
||||
className={styles.card}
|
||||
>
|
||||
<h3>Examples →</h3>
|
||||
<p>Discover and deploy boilerplate example Next.js projects.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
>
|
||||
<h3>Deploy →</h3>
|
||||
<p>
|
||||
Instantly deploy your Next.js site to a public URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className={styles.footer}>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Powered by{' '}
|
||||
<img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} />
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
BIN
front/public/favicon.ico
Normal file
BIN
front/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
4
front/public/vercel.svg
Normal file
4
front/public/vercel.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
122
front/styles/Home.module.css
Normal file
122
front/styles/Home.module.css
Normal file
|
@ -0,0 +1,122 @@
|
|||
.container {
|
||||
min-height: 100vh;
|
||||
padding: 0 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 5rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
border-top: 1px solid #eaeaea;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title a {
|
||||
color: #0070f3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title a:hover,
|
||||
.title a:focus,
|
||||
.title a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 1.5;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
background: #fafafa;
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
max-width: 800px;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 1rem;
|
||||
flex-basis: 45%;
|
||||
padding: 1.5rem;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 10px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.card:focus,
|
||||
.card:active {
|
||||
color: #0070f3;
|
||||
border-color: #0070f3;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
16
front/styles/globals.css
Normal file
16
front/styles/globals.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
19
front/tsconfig.json
Normal file
19
front/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
Loading…
Reference in a new issue