├── .gitignore
├── README.md
├── firebase.json
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.js
├── App.test.js
├── chess
│ ├── assets
│ │ ├── chessBoard.png
│ │ └── moveSoundEffect.mp3
│ ├── model
│ │ ├── chess.js
│ │ ├── chesspiece.js
│ │ └── square.js
│ └── ui
│ │ ├── chessgame.js
│ │ ├── piece.js
│ │ └── piecemap.js
├── connection
│ ├── socket.js
│ └── videochat.js
├── context
│ └── colorcontext.js
├── index.js
├── logo.svg
├── onboard
│ ├── joingame.js
│ ├── joinroom.js
│ └── onboard.js
├── serviceWorker.js
└── setupTests.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Multiplayer Chess Game built with React and Node.js + Express.
2 |
3 | - Users can play their friends anonymously via link.
4 | - Users are also able to chat with each other during the game via camera + microphone.
5 | - Tech stack: React, webRTC, Node.js, Express, Socket.io
6 |
7 | Link to the backend: [Backend](https://github.com/ProjectsByJackHe/multiplayer-chess-game-backend)
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "rewrites": [
10 | {
11 | "source": "**",
12 | "destination": "/index.html"
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chess-game",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^4.2.4",
7 | "@testing-library/react": "^9.3.2",
8 | "@testing-library/user-event": "^7.1.2",
9 | "chess.js": "^0.10.3",
10 | "konva": "6.0.0",
11 | "react": "^16.13.1",
12 | "react-dom": "^16.13.1",
13 | "react-konva": "^16.13.0-3",
14 | "react-router-dom": "^5.2.0",
15 | "react-scripts": "3.4.1",
16 | "simple-peer": "^9.7.2",
17 | "socket.io-client": "2.3.0",
18 | "styled-components": "^5.1.1",
19 | "use-image": "^1.0.5",
20 | "use-sound": "^1.0.2",
21 | "uuid": "^7.0.3"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": "react-app"
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackHeTech/multiplayer-chess-game/859918336b7fa7e04b1855ec5d0cd4bfa21e52db/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 | You need to enable JavaScript to run this app.
32 |
33 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackHeTech/multiplayer-chess-game/859918336b7fa7e04b1855ec5d0cd4bfa21e52db/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackHeTech/multiplayer-chess-game/859918336b7fa7e04b1855ec5d0cd4bfa21e52db/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom';
3 | import JoinRoom from './onboard/joinroom'
4 | import { ColorContext } from './context/colorcontext'
5 | import Onboard from './onboard/onboard'
6 | import JoinGame from './onboard/joingame'
7 | import ChessGame from './chess/ui/chessgame'
8 | /*
9 | * Frontend flow:
10 | *
11 | * 1. user first opens this app in the browser.
12 | * 2. a screen appears asking the user to send their friend their game URL to start the game.
13 | * 3. the user sends their friend their game URL
14 | * 4. the user clicks the 'start' button and waits for the other player to join.
15 | * 5. As soon as the other player joins, the game starts.
16 | *
17 | *
18 | * Other player flow:
19 | * 1. user gets the link sent by their friend
20 | * 2. user clicks on the link and it redirects to their game. If the 'host' has not yet
21 | * clicked the 'start' button yet, the user will wait for when the host clicks the start button.
22 | * If the host decides to leave before they click on the "start" button, the user will be notified
23 | * that the host has ended the session.
24 | * 3. Once the host clicks the start button or the start button was already clicked on
25 | * before, that's when the game starts.
26 | * Onboarding screen =====> Game start.
27 | *
28 | * Every time a user opens our site from the '/' path, a new game instance is automatically created
29 | * on the back-end. We should generate the uuid on the frontend, send the request with the uuid
30 | * as a part of the body of the request. If any player leaves, then the other player wins automatically.
31 | *
32 | */
33 |
34 |
35 | function App() {
36 |
37 | const [didRedirect, setDidRedirect] = React.useState(false)
38 |
39 | const playerDidRedirect = React.useCallback(() => {
40 | setDidRedirect(true)
41 | }, [])
42 |
43 | const playerDidNotRedirect = React.useCallback(() => {
44 | setDidRedirect(false)
45 | }, [])
46 |
47 | const [userName, setUserName] = React.useState('')
48 |
49 | return (
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {didRedirect ?
58 |
59 |
60 |
61 |
62 | :
63 | }
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | export default App;
72 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render( );
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/chess/assets/chessBoard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackHeTech/multiplayer-chess-game/859918336b7fa7e04b1855ec5d0cd4bfa21e52db/src/chess/assets/chessBoard.png
--------------------------------------------------------------------------------
/src/chess/assets/moveSoundEffect.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JackHeTech/multiplayer-chess-game/859918336b7fa7e04b1855ec5d0cd4bfa21e52db/src/chess/assets/moveSoundEffect.mp3
--------------------------------------------------------------------------------
/src/chess/model/chess.js:
--------------------------------------------------------------------------------
1 | import Chess from 'chess.js'
2 | import ChessPiece from './chesspiece'
3 | import Square from './square'
4 | // when indexing, remember: [y][x].
5 | /**
6 | * If the player color is black, make sure to invert the board.
7 | */
8 |
9 |
10 |
11 |
12 | class Game {
13 | constructor(thisPlayersColorIsWhite) {
14 | this.thisPlayersColorIsWhite = thisPlayersColorIsWhite // once initialized, this value should never change.
15 | // console.log("this player's color is white: " + this.thisPlayersColorIsWhite)
16 | this.chessBoard = this.makeStartingBoard() // the actual chessBoard
17 | this.chess = new Chess()
18 |
19 | this.toCoord = thisPlayersColorIsWhite ? {
20 | 0:8, 1:7, 2: 6, 3: 5, 4: 4, 5: 3, 6: 2, 7: 1
21 | } : {
22 | 0:1, 1:2, 2: 3, 3: 4, 4: 5, 5: 6, 6: 7, 7: 8
23 | }
24 |
25 | this.toAlphabet = thisPlayersColorIsWhite ? {
26 | 0:"a", 1:"b", 2: "c", 3: "d", 4: "e", 5: "f", 6: "g", 7: "h"
27 | } : {
28 | 0:"h", 1:"g", 2: "f", 3: "e", 4: "d", 5: "c", 6: "b", 7: "a"
29 | }
30 |
31 | this.toCoord2 = thisPlayersColorIsWhite ? {
32 | 8:0, 7:1, 6: 2, 5: 3, 4: 4, 3: 5, 2: 6, 1: 7
33 | } : {
34 | 1:0, 2:1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 7
35 | }
36 |
37 | this.toAlphabet2 = thisPlayersColorIsWhite ? {
38 | "a":0, "b":1, "c":2, "d":3, "e":4, "f":5, "g":6, "h":7
39 | } : {
40 | "h":0, "g":1, "f":2, "e":3, "d":4, "c":5, "b":6, "a":7
41 | }
42 |
43 | this.nQueens = 1
44 | }
45 |
46 | getBoard() {
47 | return this.chessBoard
48 | }
49 |
50 | // nextPlayersTurn() {
51 | // this.isWhitesTurn = !this.isWhitesTurn
52 | // }
53 |
54 | setBoard(newBoard) {
55 | this.chessBoard = newBoard
56 | }
57 |
58 | movePiece(pieceId, to, isMyMove) {
59 |
60 | const to2D = isMyMove ? {
61 | 105:0, 195:1, 285: 2, 375: 3, 465: 4, 555: 5, 645: 6, 735: 7
62 | } : {
63 | 105:7, 195:6, 285: 5, 375: 4, 465: 3, 555: 2, 645: 1, 735: 0
64 | }
65 |
66 |
67 | var currentBoard = this.getBoard()
68 | const pieceCoordinates = this.findPiece(currentBoard, pieceId)
69 |
70 |
71 | // can't find piece coordinates (piece doesn't exist on the board)
72 | if (!pieceCoordinates) {
73 | return
74 | }
75 |
76 | const y = pieceCoordinates[1]
77 | const x = pieceCoordinates[0]
78 |
79 | // new coordinates
80 | const to_y = to2D[to[1]]
81 | const to_x = to2D[to[0]]
82 |
83 | const originalPiece = currentBoard[y][x].getPiece()
84 |
85 | if (y === to_y && x === to_x) {
86 | return "moved in the same position."
87 | }
88 |
89 | /**
90 | * In order for this method to do anything meaningful,
91 | * the 'reassign const' line of code must run. Therefore,
92 | * for it to run, we must check first that the given move is valid.
93 | */
94 |
95 | const isPromotion = this.isPawnPromotion(to, pieceId[1])
96 | const moveAttempt = !isPromotion ? this.chess.move({
97 | from: this.toChessMove([x, y], to2D),
98 | to: this.toChessMove(to, to2D),
99 | piece: pieceId[1]})
100 | :
101 | this.chess.move({
102 | from: this.toChessMove([x, y], to2D),
103 | to: this.toChessMove(to, to2D),
104 | piece: pieceId[1],
105 | promotion: 'q'
106 | })
107 |
108 |
109 | console.log(moveAttempt)
110 | // console.log(isPromotion)
111 |
112 | if (moveAttempt === null) {
113 | return "invalid move"
114 | }
115 |
116 |
117 | if (moveAttempt.flags === 'e') {
118 | const move = moveAttempt.to
119 | const x = this.toAlphabet2[move[0]]
120 | let y
121 | if (moveAttempt.color === 'w') {
122 | y = parseInt(move[1], 10) - 1
123 | } else {
124 | y = parseInt(move[1], 10) + 1
125 | }
126 | currentBoard[this.toCoord2[y]][x].setPiece(null)
127 | }
128 |
129 |
130 |
131 | // Check castling
132 | const castle = this.isCastle(moveAttempt)
133 | if (castle.didCastle) {
134 | /**
135 | * The main thing we are doing here is moving the right rook
136 | * to the right position.
137 | *
138 | * - Get original piece by calling getPiece() on the original [x, y]
139 | * - Set the new [to_x, to_y] to the original piece
140 | * - Set the original [x, y] to null
141 | */
142 |
143 | const originalRook = currentBoard[castle.y][castle.x].getPiece()
144 | currentBoard[castle.to_y][castle.to_x].setPiece(originalRook)
145 | currentBoard[castle.y][castle.x].setPiece(null)
146 | }
147 |
148 |
149 | // ___actually changing the board model___
150 |
151 | const reassign = isPromotion ? currentBoard[to_y][to_x].setPiece(
152 | new ChessPiece(
153 | 'queen',
154 | false,
155 | pieceId[0] === 'w' ? 'white' : 'black',
156 | pieceId[0] === 'w' ? 'wq' + this.nQueens : 'bq' + this.nQueens))
157 | : currentBoard[to_y][to_x].setPiece(originalPiece)
158 |
159 | if (reassign !== "user tried to capture their own piece") {
160 | currentBoard[y][x].setPiece(null)
161 | } else {
162 | return reassign
163 | }
164 |
165 | // ___actually changing the board model___
166 |
167 |
168 | const checkMate = this.chess.in_checkmate() ? " has been checkmated" : " has not been checkmated"
169 | console.log(this.chess.turn() + checkMate)
170 | if (checkMate === " has been checkmated") {
171 | return this.chess.turn() + checkMate
172 | }
173 | // changes the fill color of the opponent's king that is in check
174 | const check = this.chess.in_check() ? " is in check" : " is not in check"
175 | console.log(this.chess.turn() + check)
176 | if (check === " is in check") {
177 | return this.chess.turn() + check
178 | }
179 |
180 | console.log(currentBoard)
181 | // update board
182 | this.setBoard(currentBoard)
183 | }
184 |
185 |
186 |
187 | isCastle(moveAttempt) {
188 | /**
189 | * Assume moveAttempt is legal.
190 | *
191 | * {moveAttempt} -> {boolean x, y to_x, to_y}
192 | *
193 | * returns if a player has castled, the final position of
194 | * the rook (to_x, to_y), and the original position of the rook (x, y)
195 | *
196 | */
197 |
198 |
199 | const piece = moveAttempt.piece
200 | const move = {from: moveAttempt.from, to: moveAttempt.to}
201 |
202 | const isBlackCastle = ((move.from === 'e1' && move.to === 'g1') || (move.from === 'e1' && move.to === 'c1'))
203 | const isWhiteCastle = (move.from === 'e8' && move.to === 'g8') || (move.from === 'e8' && move.to === 'c8')
204 |
205 |
206 | if (!(isWhiteCastle || isBlackCastle) || piece !== 'k') {
207 | return {
208 | didCastle: false
209 | }
210 | }
211 |
212 | let originalPositionOfRook
213 | let newPositionOfRook
214 |
215 | if ((move.from === 'e1' && move.to === 'g1')) {
216 | originalPositionOfRook = 'h1'
217 | newPositionOfRook = 'f1'
218 | } else if ((move.from === 'e1' && move.to === 'c1')) {
219 | originalPositionOfRook = 'a1'
220 | newPositionOfRook = 'd1'
221 | } else if ((move.from === 'e8' && move.to === 'g8')) {
222 | originalPositionOfRook = 'h8'
223 | newPositionOfRook = 'f8'
224 | } else { // e8 to c8
225 | originalPositionOfRook = 'a8'
226 | newPositionOfRook = 'd8'
227 | }
228 |
229 |
230 | return {
231 | didCastle: true,
232 | x: this.toAlphabet2[originalPositionOfRook[0]],
233 | y: this.toCoord2[originalPositionOfRook[1]],
234 | to_x: this.toAlphabet2[newPositionOfRook[0]],
235 | to_y: this.toCoord2[newPositionOfRook[1]]
236 | }
237 | }
238 |
239 |
240 | isPawnPromotion(to, piece) {
241 | const res = piece === 'p' && (to[1] === 105 || to[1] === 735)
242 | if (res) {
243 | this.nQueens += 1
244 | }
245 | return res
246 | }
247 |
248 |
249 | toChessMove(finalPosition, to2D) {
250 |
251 | let move
252 |
253 | if (finalPosition[0] > 100) {
254 | move = this.toAlphabet[to2D[finalPosition[0]]] + this.toCoord[to2D[finalPosition[1]]]
255 | } else {
256 | move = this.toAlphabet[finalPosition[0]] + this.toCoord[finalPosition[1]]
257 | }
258 |
259 | // console.log("proposed move: " + move)
260 | return move
261 | }
262 |
263 | findPiece(board, pieceId) {
264 | // ChessBoard, String -> [Int, Int]
265 | // console.log("piecetofind: " + pieceId)
266 | for (var i = 0; i < 8; i++) {
267 | for (var j = 0; j < 8; j++) {
268 | if (board[i][j].getPieceIdOnThisSquare() === pieceId) {
269 | return [j, i]
270 | }
271 | }
272 | }
273 | }
274 |
275 | makeStartingBoard() {
276 | const backRank = ["rook", "knight", "bishop", "queen", "king", "bishop", "knight", "rook"]
277 | var startingChessBoard = []
278 | for (var i = 0; i < 8; i++) {
279 | startingChessBoard.push([])
280 | for (var j = 0; j < 8; j++) {
281 | // j is horizontal
282 | // i is vertical
283 | const coordinatesOnCanvas = [((j + 1) * 90 + 15), ((i + 1) * 90 + 15)]
284 | const emptySquare = new Square(j, i, null, coordinatesOnCanvas)
285 |
286 | startingChessBoard[i].push(emptySquare)
287 | }
288 | }
289 | const whiteBackRankId = ["wr1", "wn1", "wb1", "wq1", "wk1", "wb2", "wn2", "wr2"]
290 | const blackBackRankId = ["br1", "bn1", "bb1", "bq1", "bk1", "bb2", "bn2", "br2"]
291 | for (var j = 0; j < 8; j += 7) {
292 | for (var i = 0; i < 8; i++) {
293 | if (j == 0) {
294 | // top
295 | // console.log(backRank[i])
296 | startingChessBoard[j][this.thisPlayersColorIsWhite ? i : 7 - i].setPiece(new ChessPiece(backRank[i], false, this.thisPlayersColorIsWhite ? "black" : "white", this.thisPlayersColorIsWhite ? blackBackRankId[i] : whiteBackRankId[i]))
297 | startingChessBoard[j + 1][this.thisPlayersColorIsWhite ? i : 7 - i].setPiece(new ChessPiece("pawn", false, this.thisPlayersColorIsWhite ? "black" : "white", this.thisPlayersColorIsWhite ? "bp" + i : "wp" + i))
298 | } else {
299 | // bottom
300 | startingChessBoard[j - 1][this.thisPlayersColorIsWhite ? i : 7 - i].setPiece(new ChessPiece("pawn", false, this.thisPlayersColorIsWhite ? "white" : "black", this.thisPlayersColorIsWhite ? "wp" + i : "bp" + i))
301 | startingChessBoard[j][this.thisPlayersColorIsWhite ? i : 7 - i].setPiece(new ChessPiece(backRank[i], false, this.thisPlayersColorIsWhite ? "white" : "black", this.thisPlayersColorIsWhite ? whiteBackRankId[i] : blackBackRankId[i]))
302 | }
303 | }
304 | }
305 | return startingChessBoard
306 | }
307 | }
308 |
309 | export default Game
--------------------------------------------------------------------------------
/src/chess/model/chesspiece.js:
--------------------------------------------------------------------------------
1 |
2 | class ChessPiece {
3 | constructor(name, isAttacked, color, id) {
4 | this.name = name // string
5 | this.isAttacked = isAttacked // boolean
6 | this.color = color // string
7 | this.id = id // string
8 | }
9 |
10 | setSquare(newSquare) {
11 | // set the square this piece is sitting top of.
12 | // on any given piece (on the board), there will always be a piece on top of it.
13 | // console.log(newSquare)
14 | if (newSquare === undefined) {
15 | this.squareThisPieceIsOn = newSquare
16 | return
17 | }
18 |
19 | if (this.squareThisPieceIsOn === undefined) {
20 | this.squareThisPieceIsOn = newSquare
21 | newSquare.setPiece(this)
22 | }
23 |
24 | const isNewSquareDifferent = this.squareThisPieceIsOn.x != newSquare.x || this.squareThisPieceIsOn.y != newSquare.y
25 |
26 | if (isNewSquareDifferent) {
27 | // console.log("set")
28 | this.squareThisPieceIsOn = newSquare
29 | newSquare.setPiece(this)
30 | }
31 | }
32 |
33 | getSquare() {
34 | return this.squareThisPieceIsOn
35 | }
36 | }
37 |
38 |
39 | export default ChessPiece
--------------------------------------------------------------------------------
/src/chess/model/square.js:
--------------------------------------------------------------------------------
1 | class Square {
2 | constructor(x, y, pieceOnThisSquare, canvasCoord) {
3 | this.x = x // Int 0 < x < 7
4 | this.y = y // Int 0 < y < 7
5 | this.canvasCoord = canvasCoord
6 | this.pieceOnThisSquare = pieceOnThisSquare // ChessPiece || null
7 | }
8 |
9 | setPiece(newPiece) {
10 | if (newPiece === null && this.pieceOnThisSquare === null) {
11 | return
12 | } else if (newPiece === null) {
13 | // case where the function caller wants to remove the piece that is on this square.
14 | this.pieceOnThisSquare.setSquare(undefined)
15 | this.pieceOnThisSquare = null
16 | } else if (this.pieceOnThisSquare === null) {
17 | // case where the function caller wants assign a new piece on this square
18 | this.pieceOnThisSquare = newPiece
19 | newPiece.setSquare(this)
20 | } else if (this.getPieceIdOnThisSquare() != newPiece.id && this.pieceOnThisSquare.color != newPiece.color) {
21 | // case where the function caller wants to change the piece on this square. (only different color allowed)
22 | console.log("capture!")
23 | this.pieceOnThisSquare = newPiece
24 | newPiece.setSquare(this)
25 | } else {
26 | return "user tried to capture their own piece"
27 | }
28 | }
29 |
30 | removePiece() {
31 | this.pieceOnThisSquare = null
32 | }
33 |
34 | getPiece() {
35 | return this.pieceOnThisSquare
36 | }
37 |
38 | getPieceIdOnThisSquare() {
39 | if (this.pieceOnThisSquare === null) {
40 | return "empty"
41 | }
42 | return this.pieceOnThisSquare.id
43 | }
44 |
45 | isOccupied() {
46 | return this.pieceOnThisSquare != null
47 | }
48 |
49 | getCoord() {
50 | return [this.x, this.y]
51 | }
52 |
53 | getCanvasCoord() {
54 | return this.canvasCoord
55 | }
56 | }
57 |
58 | export default Square
--------------------------------------------------------------------------------
/src/chess/ui/chessgame.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Game from '../model/chess'
3 | import Square from '../model/square'
4 | import { Stage, Layer } from 'react-konva';
5 | import Board from '../assets/chessBoard.png'
6 | import useSound from 'use-sound'
7 | import chessMove from '../assets/moveSoundEffect.mp3'
8 | import Piece from './piece'
9 | import piecemap from './piecemap'
10 | import { useParams } from 'react-router-dom'
11 | import { ColorContext } from '../../context/colorcontext'
12 | import VideoChatApp from '../../connection/videochat'
13 | const socket = require('../../connection/socket').socket
14 |
15 |
16 | class ChessGame extends React.Component {
17 |
18 | state = {
19 | gameState: new Game(this.props.color),
20 | draggedPieceTargetId: "", // empty string means no piece is being dragged
21 | playerTurnToMoveIsWhite: true,
22 | whiteKingInCheck: false,
23 | blackKingInCheck: false
24 | }
25 |
26 |
27 | componentDidMount() {
28 | console.log(this.props.myUserName)
29 | console.log(this.props.opponentUserName)
30 | // register event listeners
31 | socket.on('opponent move', move => {
32 | // move == [pieceId, finalPosition]
33 | // console.log("opponenet's move: " + move.selectedId + ", " + move.finalPosition)
34 | if (move.playerColorThatJustMovedIsWhite !== this.props.color) {
35 | this.movePiece(move.selectedId, move.finalPosition, this.state.gameState, false)
36 | this.setState({
37 | playerTurnToMoveIsWhite: !move.playerColorThatJustMovedIsWhite
38 | })
39 | }
40 | })
41 | }
42 |
43 | startDragging = (e) => {
44 | this.setState({
45 | draggedPieceTargetId: e.target.attrs.id
46 | })
47 | }
48 |
49 |
50 | movePiece = (selectedId, finalPosition, currentGame, isMyMove) => {
51 | /**
52 | * "update" is the connection between the model and the UI.
53 | * This could also be an HTTP request and the "update" could be the server response.
54 | * (model is hosted on the server instead of the browser)
55 | */
56 | var whiteKingInCheck = false
57 | var blackKingInCheck = false
58 | var blackCheckmated = false
59 | var whiteCheckmated = false
60 | const update = currentGame.movePiece(selectedId, finalPosition, isMyMove)
61 |
62 | if (update === "moved in the same position.") {
63 | this.revertToPreviousState(selectedId) // pass in selected ID to identify the piece that messed up
64 | return
65 | } else if (update === "user tried to capture their own piece") {
66 | this.revertToPreviousState(selectedId)
67 | return
68 | } else if (update === "b is in check" || update === "w is in check") {
69 | // change the fill of the enemy king or your king based on which side is in check.
70 | // play a sound or something
71 | if (update[0] === "b") {
72 | blackKingInCheck = true
73 | } else {
74 | whiteKingInCheck = true
75 | }
76 | } else if (update === "b has been checkmated" || update === "w has been checkmated") {
77 | if (update[0] === "b") {
78 | blackCheckmated = true
79 | } else {
80 | whiteCheckmated = true
81 | }
82 | } else if (update === "invalid move") {
83 | this.revertToPreviousState(selectedId)
84 | return
85 | }
86 |
87 | // let the server and the other client know your move
88 | if (isMyMove) {
89 | socket.emit('new move', {
90 | nextPlayerColorToMove: !this.state.gameState.thisPlayersColorIsWhite,
91 | playerColorThatJustMovedIsWhite: this.state.gameState.thisPlayersColorIsWhite,
92 | selectedId: selectedId,
93 | finalPosition: finalPosition,
94 | gameId: this.props.gameId
95 | })
96 | }
97 |
98 |
99 | this.props.playAudio()
100 |
101 | // sets the new game state.
102 | this.setState({
103 | draggedPieceTargetId: "",
104 | gameState: currentGame,
105 | playerTurnToMoveIsWhite: !this.props.color,
106 | whiteKingInCheck: whiteKingInCheck,
107 | blackKingInCheck: blackKingInCheck
108 | })
109 |
110 | if (blackCheckmated) {
111 | alert("WHITE WON BY CHECKMATE!")
112 | } else if (whiteCheckmated) {
113 | alert("BLACK WON BY CHECKMATE!")
114 | }
115 | }
116 |
117 |
118 | endDragging = (e) => {
119 | const currentGame = this.state.gameState
120 | const currentBoard = currentGame.getBoard()
121 | const finalPosition = this.inferCoord(e.target.x() + 90, e.target.y() + 90, currentBoard)
122 | const selectedId = this.state.draggedPieceTargetId
123 | this.movePiece(selectedId, finalPosition, currentGame, true)
124 | }
125 |
126 | revertToPreviousState = (selectedId) => {
127 | /**
128 | * Should update the UI to what the board looked like before.
129 | */
130 | const oldGS = this.state.gameState
131 | const oldBoard = oldGS.getBoard()
132 | const tmpGS = new Game(true)
133 | const tmpBoard = []
134 |
135 | for (var i = 0; i < 8; i++) {
136 | tmpBoard.push([])
137 | for (var j = 0; j < 8; j++) {
138 | if (oldBoard[i][j].getPieceIdOnThisSquare() === selectedId) {
139 | tmpBoard[i].push(new Square(j, i, null, oldBoard[i][j].canvasCoord))
140 | } else {
141 | tmpBoard[i].push(oldBoard[i][j])
142 | }
143 | }
144 | }
145 |
146 | // temporarily remove the piece that was just moved
147 | tmpGS.setBoard(tmpBoard)
148 |
149 | this.setState({
150 | gameState: tmpGS,
151 | draggedPieceTargetId: "",
152 | })
153 |
154 | this.setState({
155 | gameState: oldGS,
156 | })
157 | }
158 |
159 |
160 | inferCoord = (x, y, chessBoard) => {
161 | // console.log("actual mouse coordinates: " + x + ", " + y)
162 | /*
163 | Should give the closest estimate for new position.
164 | */
165 | var hashmap = {}
166 | var shortestDistance = Infinity
167 | for (var i = 0; i < 8; i++) {
168 | for (var j = 0; j < 8; j++) {
169 | const canvasCoord = chessBoard[i][j].getCanvasCoord()
170 | // calculate distance
171 | const delta_x = canvasCoord[0] - x
172 | const delta_y = canvasCoord[1] - y
173 | const newDistance = Math.sqrt(delta_x**2 + delta_y**2)
174 | hashmap[newDistance] = canvasCoord
175 | if (newDistance < shortestDistance) {
176 | shortestDistance = newDistance
177 | }
178 | }
179 | }
180 |
181 | return hashmap[shortestDistance]
182 | }
183 |
184 | render() {
185 | // console.log(this.state.gameState.getBoard())
186 | // console.log("it's white's move this time: " + this.state.playerTurnToMoveIsWhite)
187 | /*
188 | Look at the current game state in the model and populate the UI accordingly
189 | */
190 | // console.log(this.state.gameState.getBoard())
191 |
192 | return (
193 |
194 |
199 |
200 |
201 | {this.state.gameState.getBoard().map((row) => {
202 | return (
203 | {row.map((square) => {
204 | if (square.isOccupied()) {
205 | return (
206 | )
220 | }
221 | return
222 | })}
223 | )
224 | })}
225 |
226 |
227 |
228 | )
229 | }
230 | }
231 |
232 |
233 |
234 | const ChessGameWrapper = (props) => {
235 | /**
236 | * player 1
237 | * - socketId 1
238 | * - socketId 2 ???
239 | * player 2
240 | * - socketId 2
241 | * - socketId 1
242 | */
243 |
244 |
245 |
246 | // get the gameId from the URL here and pass it to the chessGame component as a prop.
247 | const domainName = 'http://localhost:3000'
248 | const color = React.useContext(ColorContext)
249 | const { gameid } = useParams()
250 | const [play] = useSound(chessMove);
251 | const [opponentSocketId, setOpponentSocketId] = React.useState('')
252 | const [opponentDidJoinTheGame, didJoinGame] = React.useState(false)
253 | const [opponentUserName, setUserName] = React.useState('')
254 | const [gameSessionDoesNotExist, doesntExist] = React.useState(false)
255 |
256 | React.useEffect(() => {
257 | socket.on("playerJoinedRoom", statusUpdate => {
258 | console.log("A new player has joined the room! Username: " + statusUpdate.userName + ", Game id: " + statusUpdate.gameId + " Socket id: " + statusUpdate.mySocketId)
259 | if (socket.id !== statusUpdate.mySocketId) {
260 | setOpponentSocketId(statusUpdate.mySocketId)
261 | }
262 | })
263 |
264 | socket.on("status", statusUpdate => {
265 | console.log(statusUpdate)
266 | alert(statusUpdate)
267 | if (statusUpdate === 'This game session does not exist.' || statusUpdate === 'There are already 2 people playing in this room.') {
268 | doesntExist(true)
269 | }
270 | })
271 |
272 |
273 | socket.on('start game', (opponentUserName) => {
274 | console.log("START!")
275 | if (opponentUserName !== props.myUserName) {
276 | setUserName(opponentUserName)
277 | didJoinGame(true)
278 | } else {
279 | // in chessGame, pass opponentUserName as a prop and label it as the enemy.
280 | // in chessGame, use reactContext to get your own userName
281 | // socket.emit('myUserName')
282 | socket.emit('request username', gameid)
283 | }
284 | })
285 |
286 |
287 | socket.on('give userName', (socketId) => {
288 | if (socket.id !== socketId) {
289 | console.log("give userName stage: " + props.myUserName)
290 | socket.emit('recieved userName', {userName: props.myUserName, gameId: gameid})
291 | }
292 | })
293 |
294 | socket.on('get Opponent UserName', (data) => {
295 | if (socket.id !== data.socketId) {
296 | setUserName(data.userName)
297 | console.log('data.socketId: data.socketId')
298 | setOpponentSocketId(data.socketId)
299 | didJoinGame(true)
300 | }
301 | })
302 | }, [])
303 |
304 |
305 | return (
306 |
307 | {opponentDidJoinTheGame ? (
308 |
309 |
Opponent: {opponentUserName}
310 |
311 |
316 |
322 |
323 |
You: {props.myUserName}
324 |
325 | ) : gameSessionDoesNotExist ? (
326 |
327 |
:(
328 |
329 | ) : (
330 |
331 |
337 | Hey {props.myUserName} , copy and paste the URL
338 | below to send to your friend:
339 |
340 |
349 |
350 |
351 |
352 | {" "}
353 | Waiting for other opponent to join the game...{" "}
354 |
355 |
356 | )}
357 |
358 | );
359 | };
360 |
361 | export default ChessGameWrapper
362 |
--------------------------------------------------------------------------------
/src/chess/ui/piece.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Image } from 'react-konva';
3 | import useImage from 'use-image'
4 |
5 | const Piece = (props) => {
6 | const choiceOfColor = props.isWhite ? 0 : 1
7 | const [image] = useImage(props.imgurls[choiceOfColor]);
8 | const isDragged = props.id === props.draggedPieceTargetId
9 |
10 | const canThisPieceEvenBeMovedByThisPlayer = props.isWhite === props.thisPlayersColorIsWhite
11 | const isItThatPlayersTurn = props.playerTurnToMoveIsWhite === props.thisPlayersColorIsWhite
12 |
13 | const thisWhiteKingInCheck = props.id === "wk1" && props.whiteKingInCheck
14 | const thisBlackKingInCheck = props.id === "bk1" && props.blackKingInCheck
15 |
16 |
17 | // console.log("this piece ID:" + props.thisPieceTargetId)
18 | // console.log("dragged piece ID:" + props.draggedPieceTargetId)
19 | return ;
30 | };
31 |
32 | export default Piece
--------------------------------------------------------------------------------
/src/chess/ui/piecemap.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'pawn': ['https://upload.wikimedia.org/wikipedia/commons/0/04/Chess_plt60.png', 'https://upload.wikimedia.org/wikipedia/commons/c/cd/Chess_pdt60.png'],
3 | 'knight':['https://upload.wikimedia.org/wikipedia/commons/2/28/Chess_nlt60.png','https://upload.wikimedia.org/wikipedia/commons/f/f1/Chess_ndt60.png'],
4 | 'bishop':['https://upload.wikimedia.org/wikipedia/commons/9/9b/Chess_blt60.png','https://upload.wikimedia.org/wikipedia/commons/8/81/Chess_bdt60.png'],
5 | 'king':['https://upload.wikimedia.org/wikipedia/commons/3/3b/Chess_klt60.png','https://upload.wikimedia.org/wikipedia/commons/e/e3/Chess_kdt60.png'],
6 | 'queen':['https://upload.wikimedia.org/wikipedia/commons/4/49/Chess_qlt60.png','https://upload.wikimedia.org/wikipedia/commons/a/af/Chess_qdt60.png'],
7 | 'rook':['https://upload.wikimedia.org/wikipedia/commons/5/5c/Chess_rlt60.png','https://upload.wikimedia.org/wikipedia/commons/a/a0/Chess_rdt60.png']
8 | }
--------------------------------------------------------------------------------
/src/connection/socket.js:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client'
2 |
3 | const URL = 'http://localhost:8000'
4 |
5 | const socket = io(URL)
6 |
7 | var mySocketId
8 | // register preliminary event listeners here:
9 |
10 |
11 | socket.on("createNewGame", statusUpdate => {
12 | console.log("A new game has been created! Username: " + statusUpdate.userName + ", Game id: " + statusUpdate.gameId + " Socket id: " + statusUpdate.mySocketId)
13 | mySocketId = statusUpdate.mySocketId
14 | })
15 |
16 | export {
17 | socket,
18 | mySocketId
19 | }
20 |
--------------------------------------------------------------------------------
/src/connection/videochat.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from 'react';
2 | import Peer from "simple-peer";
3 | import styled from "styled-components";
4 | const socket = require('../connection/socket').socket
5 |
6 |
7 | const Container = styled.div`
8 | height: 100vh;
9 | width: 100%;
10 | flex-direction: column;
11 | `;
12 |
13 | const Row = styled.div`
14 | width: 100%;
15 | `;
16 |
17 | const Video = styled.video`
18 | border: 1px solid blue;
19 | `;
20 |
21 | function VideoChatApp(props) {
22 | /**
23 | * initial state: both player is neutral and have the option to call each other
24 | *
25 | * player 1 calls player 2: Player 1 should display: 'Calling {player 2 username},' and the
26 | * 'CallPeer' button should disappear for Player 1.
27 | * Player 2 should display '{player 1 username} is calling you' and
28 | * the 'CallPeer' button for Player 2 should also disappear.
29 | *
30 | * Case 1: player 2 accepts call - the video chat begins and there is no button to end it.
31 | *
32 | * Case 2: player 2 ignores player 1 call - nothing happens. Wait until the connection times out.
33 | *
34 | */
35 |
36 | const [stream, setStream] = useState();
37 | const [receivingCall, setReceivingCall] = useState(false);
38 | const [caller, setCaller] = useState("");
39 | const [callerSignal, setCallerSignal] = useState();
40 | const [callAccepted, setCallAccepted] = useState(false);
41 | const [isCalling, setIsCalling] = useState(false)
42 | const userVideo = useRef();
43 | const partnerVideo = useRef();
44 |
45 | useEffect(() => {
46 | navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {
47 | setStream(stream);
48 | if (userVideo.current) {
49 | userVideo.current.srcObject = stream;
50 | }
51 | })
52 |
53 | socket.on("hey", (data) => {
54 | setReceivingCall(true);
55 | setCaller(data.from);
56 | setCallerSignal(data.signal);
57 | })
58 | }, []);
59 |
60 | function callPeer(id) {
61 | setIsCalling(true)
62 | const peer = new Peer({
63 | initiator: true,
64 | trickle: false,
65 | stream: stream,
66 | });
67 |
68 | peer.on("signal", data => {
69 | socket.emit("callUser", { userToCall: id, signalData: data, from: props.mySocketId})
70 | })
71 |
72 | peer.on("stream", stream => {
73 | if (partnerVideo.current) {
74 | partnerVideo.current.srcObject = stream;
75 | }
76 | });
77 |
78 | socket.on("callAccepted", signal => {
79 | setCallAccepted(true);
80 | peer.signal(signal);
81 | })
82 |
83 | }
84 |
85 | function acceptCall() {
86 | setCallAccepted(true);
87 | setIsCalling(false)
88 | const peer = new Peer({
89 | initiator: false,
90 | trickle: false,
91 | stream: stream,
92 | });
93 | peer.on("signal", data => {
94 | socket.emit("acceptCall", { signal: data, to: caller })
95 | })
96 |
97 | peer.on("stream", stream => {
98 | partnerVideo.current.srcObject = stream;
99 | });
100 |
101 | peer.signal(callerSignal);
102 | }
103 |
104 | let UserVideo;
105 | if (stream) {
106 | UserVideo = (
107 |
108 | );
109 | }
110 |
111 | let mainView;
112 |
113 | if (callAccepted) {
114 | mainView = (
115 |
116 | );
117 | } else if (receivingCall) {
118 | mainView = (
119 |
120 |
{props.opponentUserName} is calling you
121 | Accept
122 |
123 | )
124 | } else if (isCalling) {
125 | mainView = (
126 |
127 |
Currently calling {props.opponentUserName}...
128 |
129 | )
130 | } else {
131 | mainView = (
132 | {
133 | callPeer(props.opponentSocketId)
134 | }}>Chat with your friend while you play!
135 | )
136 | }
137 |
138 |
139 |
140 | return (
141 |
142 | {mainView}
143 | {UserVideo}
144 |
145 | );
146 | }
147 |
148 | export default VideoChatApp;
149 |
--------------------------------------------------------------------------------
/src/context/colorcontext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react'
2 |
3 | export const ColorContext = createContext({
4 | didRedirect: false,
5 | playerDidRedirect: () => {},
6 | playerDidNotRedirect: () => {}
7 | })
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import * as serviceWorker from './serviceWorker';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
12 |
13 | // If you want your app to work offline and load faster, you can change
14 | // unregister() to register() below. Note this comes with some pitfalls.
15 | // Learn more about service workers: https://bit.ly/CRA-PWA
16 | serviceWorker.unregister();
17 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/onboard/joingame.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useParams } from 'react-router-dom'
3 | const socket = require('../connection/socket').socket
4 |
5 | /**
6 | * 'Join game' is where we actually join the game room.
7 | */
8 |
9 |
10 | const JoinGameRoom = (gameid, userName, isCreator) => {
11 | /**
12 | * For this browser instance, we want
13 | * to join it to a gameRoom. For now
14 | * assume that the game room exists
15 | * on the backend.
16 | *
17 | *
18 | * TODO: handle the case when the game room doesn't exist.
19 | */
20 | const idData = {
21 | gameId : gameid,
22 | userName : userName,
23 | isCreator: isCreator
24 | }
25 | socket.emit("playerJoinGame", idData)
26 | }
27 |
28 |
29 | const JoinGame = (props) => {
30 | /**
31 | * Extract the 'gameId' from the URL.
32 | * the 'gameId' is the gameRoom ID.
33 | */
34 | const { gameid } = useParams()
35 | JoinGameRoom(gameid, props.userName, props.isCreator)
36 | return
37 |
Welcome to Chess with Friend!
38 |
39 |
40 | }
41 |
42 | export default JoinGame
43 |
44 |
--------------------------------------------------------------------------------
/src/onboard/joinroom.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import JoinGame from './joingame'
3 | import ChessGame from '../chess/ui/chessgame'
4 |
5 |
6 | /**
7 | * Onboard is where we create the game room.
8 | */
9 |
10 | class JoinRoom extends React.Component {
11 | state = {
12 | didGetUserName: false,
13 | inputText: ""
14 | }
15 |
16 | constructor(props) {
17 | super(props);
18 | this.textArea = React.createRef();
19 | }
20 |
21 | typingUserName = () => {
22 | // grab the input text from the field from the DOM
23 | const typedText = this.textArea.current.value
24 |
25 | // set the state with that text
26 | this.setState({
27 | inputText: typedText
28 | })
29 | }
30 |
31 | render() {
32 |
33 | return (
34 | {
35 | this.state.didGetUserName ?
36 |
37 |
38 |
39 |
40 | :
41 |
42 |
Your Username:
43 |
44 |
47 |
48 | 0)}
51 | onClick = {() => {
52 | // When the 'Submit' button gets pressed from the username screen,
53 | // We should send a request to the server to create a new room with
54 | // the uuid we generate here.
55 | this.setState({
56 | didGetUserName: true
57 | })
58 | }}>Submit
59 |
60 | }
61 | )
62 | }
63 | }
64 |
65 | export default JoinRoom
--------------------------------------------------------------------------------
/src/onboard/onboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Redirect } from 'react-router-dom'
3 | import uuid from 'uuid/v4'
4 | import { ColorContext } from '../context/colorcontext'
5 | const socket = require('../connection/socket').socket
6 |
7 | /**
8 | * Onboard is where we create the game room.
9 | */
10 |
11 | class CreateNewGame extends React.Component {
12 | state = {
13 | didGetUserName: false,
14 | inputText: "",
15 | gameId: ""
16 | }
17 |
18 | constructor(props) {
19 | super(props);
20 | this.textArea = React.createRef();
21 | }
22 |
23 | send = () => {
24 | /**
25 | * This method should create a new room in the '/' namespace
26 | * with a unique identifier.
27 | */
28 | const newGameRoomId = uuid()
29 |
30 | // set the state of this component with the gameId so that we can
31 | // redirect the user to that URL later.
32 | this.setState({
33 | gameId: newGameRoomId
34 | })
35 |
36 | // emit an event to the server to create a new room
37 | socket.emit('createNewGame', newGameRoomId)
38 | }
39 |
40 | typingUserName = () => {
41 | // grab the input text from the field from the DOM
42 | const typedText = this.textArea.current.value
43 |
44 | // set the state with that text
45 | this.setState({
46 | inputText: typedText
47 | })
48 | }
49 |
50 | render() {
51 | // !!! TODO: edit this later once you have bought your own domain.
52 |
53 | return (
54 | {
55 | this.state.didGetUserName ?
56 |
57 | Start Game
58 |
59 | :
60 |
61 |
Your Username:
62 |
63 |
66 |
67 | 0)}
70 | onClick = {() => {
71 | // When the 'Submit' button gets pressed from the username screen,
72 | // We should send a request to the server to create a new room with
73 | // the uuid we generate here.
74 | this.props.didRedirect()
75 | this.props.setUserName(this.state.inputText)
76 | this.setState({
77 | didGetUserName: true
78 | })
79 | this.send()
80 | }}>Submit
81 |
82 | }
83 | )
84 | }
85 | }
86 |
87 | const Onboard = (props) => {
88 | const color = React.useContext(ColorContext)
89 |
90 | return
91 | }
92 |
93 |
94 | export default Onboard
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------