diff --git a/server/src/main/java/lingo/server/WebSocketConfig.java b/server/src/main/java/lingo/server/WebSocketConfig.java index 90ab28c..c956c73 100644 --- a/server/src/main/java/lingo/server/WebSocketConfig.java +++ b/server/src/main/java/lingo/server/WebSocketConfig.java @@ -32,8 +32,7 @@ public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { - // Allow all origins: for JSFiddle, lol - registry.addEndpoint("/stomp").setAllowedOrigins("*").withSockJS(); + registry.addEndpoint("/stomp").withSockJS(); } } diff --git a/server/src/main/resources/static/client.js b/server/src/main/resources/static/client.js index 78d65bc..06ca31c 100644 --- a/server/src/main/resources/static/client.js +++ b/server/src/main/resources/static/client.js @@ -7,22 +7,6 @@ var SIDE = 50; var MARGIN_TOP = 100; var MARGIN_BOTTOM = 75; -var myScore = 0; -var myGuess; -var myGuesses; -var myProgress; -var myResults; -var myUsername; -var opponentScore = 0; -var opponentResults; -var opponentUsername; -var lastWord; - -var appDiv = document.getElementById('appDiv'); -var canvasDiv = document.getElementById('canvasDiv'); -var canvas = document.getElementById('canvas'); -var ctx = canvas.getContext('2d'); - var client; var sessionId = null; @@ -30,14 +14,146 @@ var vm = new Vue({ el: '#vue-app', data: { games: [], - gameId: null + gameId: null, + messages: [], + username: null, + usernameError: '', + myScore: 0, + myGuess: '', + myGuesses: [], + myProgress: [], + myResults: [], + opponentScore: 0, + opponentResults: [], + opponentUsername: null, + lastWord: null }, computed: { inGame: function() { return this.gameId !== null; + }, + inStartedGame: function() { + var game = this.getGame(this.gameId); + return game !== null && game.started === true; } }, methods: { + drawMyBoard: function(ctx) { + var x = 25, y = MARGIN_TOP; + this.drawUsername(ctx, x, y, this.username); + this.drawScore(ctx, x, y, this.myScore); + this.drawInput(ctx, x, y, this.myGuess); + var yStart = this.drawGuesses(ctx, x, y, this.myGuesses, this.myResults); + this.drawHint(ctx, x, yStart, this.myProgress); + this.drawGrid(ctx, x, y); + }, + drawOpponentBoard: function(ctx) { + var x = 325, y = MARGIN_TOP; + this.drawUsername(ctx, x, y, this.opponentUsername); + this.drawScore(ctx, x, y, this.opponentScore); + this.drawResults(ctx, x, y, this.opponentResults); + this.drawGrid(ctx, x, y); + }, + drawLastWord: function(canvas, ctx) { + if (this.lastWord) { + var x = canvas.width / 2; + var y = canvas.height - MARGIN_BOTTOM / 2; + ctx.fillStyle = 'black'; + ctx.fillText('Previous word: ' + this.lastWord.toUpperCase(), x, y); + } + }, + drawUsername: function(ctx, x, y, username) { + var usernameX = x + WIDTH / 2; + var usernameY = y - 60; + ctx.fillStyle = 'black'; + ctx.fillText(username, usernameX, usernameY); + }, + drawScore: function(ctx, x, y, score) { + var scoreX = x + WIDTH / 2; + var scoreY = y - 25; + ctx.fillStyle = 'black'; + ctx.fillText(score, scoreX, scoreY); + }, + drawGrid: function(ctx, xOrigin, yOrigin) { + ctx.beginPath(); + for (var x = 0; x <= WIDTH; x += SIDE) { + ctx.moveTo(xOrigin + x, yOrigin); + ctx.lineTo(xOrigin + x, yOrigin + HEIGHT); + } + for (var y = 0; y <= HEIGHT; y += SIDE) { + ctx.moveTo(xOrigin, yOrigin + y); + ctx.lineTo(xOrigin + WIDTH, yOrigin + y); + } + ctx.strokeStyle = 'black'; + ctx.stroke(); + }, + drawInput: function(ctx, xOrigin, yOrigin, input) { + ctx.fillStyle = 'green'; + var x = xOrigin + SIDE * 0.5; + var y = yOrigin + SIDE * 0.5; + for (var i = 0; i < input.length; i++) { + ctx.fillText(input[i], x, y); + x += SIDE; + } + }, + drawGuesses: function(ctx, xOrigin, yOrigin, guesses, results) { + var y = yOrigin + SIDE * 1.5; + var numGuesses = Math.min(4, guesses.length); + for (var i = 0; i < numGuesses; i++) { + var x = xOrigin + SIDE * 0.5; + var guess = guesses[guesses.length - numGuesses + i]; + var result = results[results.length - numGuesses + i]; + for (var j = 0; j < 5; j++) { + if (result[j] === 1) { + ctx.fillStyle = 'yellow'; + ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE); + } else if (result[j] === 2) { + ctx.fillStyle = 'orange'; + ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE); + } + ctx.fillStyle = 'green'; + ctx.fillText(guess[j], x, y); + x += SIDE; + } + y += SIDE; + } + return y; + }, + drawResults: function(ctx, xOrigin, yOrigin, results) { + var y = yOrigin + SIDE * 1.5; + var numResults = Math.min(4, results.length); + for (var i = 0; i < numResults; i++) { + var x = xOrigin + SIDE * 0.5; + var result = results[results.length - numResults + i]; + for (var j = 0; j < 5; j++) { + if (result[j] === 1) { + ctx.fillStyle = 'yellow'; + ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE); + } else if (result[j] === 2) { + ctx.fillStyle = 'orange'; + ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE); + } + x += SIDE; + } + y += SIDE; + } + return y; + }, + drawHint: function(ctx, xOrigin, yOrigin, progress) { + var x = xOrigin + SIDE * 0.5; + for (var i = 0; i < 5; i++) { + ctx.fillText(progress[i], x, yOrigin); + x += SIDE; + } + }, + getGame: function(gameId) { + for (var i = 0; i < this.games.length; i++) { + if (this.games[i].id === gameId) { + return this.games[i]; + } + } + return null; + }, hostGame: function(event) { client.send('/app/hostGame'); }, @@ -49,6 +165,84 @@ var vm = new Vue({ }, leaveGame: function(event) { client.send('/app/leaveGame'); + }, + removeGame: function(gameId) { + var indexToRemove = null; + for (var i = 0; i < this.games.length; i++) { + if (this.games[i].id === gameId) { + indexToRemove = i; + break; + } + } + this.games.splice(indexToRemove, 1); + }, + onCanvasKeydown: function(event) { + if (event.which === KEYCODE_BACKSPACE) { + event.preventDefault(); + this.myGuess = this.myGuess.substr(0, this.myGuess.length - 1); + this.repaint(); + } + else if (event.which === KEYCODE_RETURN) { + if (this.myGuess.length === 5) { + client.send("/app/guess", {}, this.myGuess); + this.myGuess = ''; + this.repaint(); + } + } + }, + onCanvasKeypress: function(event) { + var charCode = event.charCode; + if (isCharacter(charCode)) { + if (isCharacterLowercase(charCode)) { + charCode = charCode - 32; + } + var char = String.fromCharCode(charCode); + if (this.myGuess.length < 5) { + this.myGuess += char; + this.repaint(); + } + } + }, + onChatKeypress: function(event) { + var messageInput = event.target; + if (event.which === KEYCODE_RETURN) { + // Shift+Enter -> new line + if (!event.shiftKey) { + event.preventDefault(); + var text = messageInput.value.trim(); + if (text.length === 0) { + return; + } + messageInput.value = ''; + client.send('/app/chat', {}, text); + addChatMessage(this.username, text); + } + } + }, + repaint: function() { + var canvas = document.getElementById('canvas'); + var ctx = canvas.getContext('2d'); + ctx.font = '25px Monospace'; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + ctx.clearRect(0, 0, canvas.width, canvas.height); + this.drawMyBoard(ctx); + this.drawOpponentBoard(ctx); + this.drawLastWord(canvas, ctx); + }, + reset: function(firstLetter, clearScore) { + if (!firstLetter) { + firstLetter = ''; + } + this.myGuess = ''; + this.myGuesses = []; + this.myProgress = [firstLetter, '', '', '', '']; + this.myResults = []; + this.opponentResults = []; + if (clearScore) { + this.myScore = 0; + this.opponentScore = 0; + } } } }); @@ -70,8 +264,6 @@ function main() { client = Stomp.over(new SockJS('/stomp')); client.connect({}, afterConnected); - var usernameDiv = document.getElementById('usernameDiv'); - var usernameError = document.getElementById('usernameError'); var usernameInput = document.getElementById('nicknameInput'); var usernameTopic = '/user/topic/sessionUsername'; @@ -80,26 +272,23 @@ function main() { var response = JSON.parse(message.body); if (response.success === true) { console.log('Username: ' + response.username); - myUsername = response.username; + vm.username = response.username; start(); - usernameDiv.classList.add('hidden'); - appDiv.classList.remove('hidden'); } else { - usernameError.innerHTML = response.errorMessage; + vm.usernameError = response.errorMessage; } }; - usernameInput.focus(); usernameInput.addEventListener('keydown', function(e) { - if (e.keyCode === KEYCODE_RETURN) { - e.preventDefault(); + if (event.keyCode === KEYCODE_RETURN) { + event.preventDefault(); if (sessionId === null) { - usernameError.innerHTML = 'Not connected to server'; + vm.usernameError = 'Not connected to server'; return; } var usernameValue = usernameInput.value.trim(); if (usernameValue.length === 0) { - usernameError.innerHTML = 'Name cannot be empty'; + vm.usernameError = 'Name cannot be empty'; return; } if (usernameSubscription === null) { @@ -115,22 +304,12 @@ function main() { } var usernameValue = usernameInput.value.trim(); if (usernameValue.length !== 0) { - usernameError.innerHTML = ''; + vm.usernameError = ''; } }); } function start() { - ctx.font = '25px Monospace'; - ctx.textBaseline = 'middle'; - ctx.textAlign = 'center'; - - addKeydownListener(); - addKeypressListener(); - addChatMessageListener(); - - reset(); - repaint(); // Load initial data doHttpGet('/games', function(games) { @@ -158,89 +337,23 @@ function start() { client.subscribe('/user/topic/playerReports', onPlayerReport); } -// special keys -function addKeydownListener() { - canvasDiv.addEventListener('keydown', function(e) { - if (e.which === KEYCODE_BACKSPACE) { - e.preventDefault(); - myGuess = myGuess.substr(0, myGuess.length - 1); - repaint(); - } - else if (e.which === KEYCODE_RETURN) { - if (myGuess.length === 5) { - client.send("/app/guess", {}, myGuess); - myGuess = ''; - repaint(); - } - } - }); -} - -// characters -function addKeypressListener() { - canvasDiv.addEventListener('keypress', function(e) { - var charCode = e.charCode; - if (isCharacter(charCode)) { - if (isCharacterLowercase(charCode)) { - charCode = charCode - 32; - } - var char = String.fromCharCode(charCode); - if (myGuess.length < 5) { - myGuess += char; - repaint(); - } - } - }); -} - -function addChatMessageListener() { - var messageInput = document.getElementById('messageInput'); - messageInput.addEventListener('keypress', function(e) { - if (e.which === KEYCODE_RETURN) { - // Shift+Enter -> new line - if (!e.shiftKey) { - e.preventDefault(); - var text = messageInput.value.trim(); - if (text.length === 0) { - return; - } - messageInput.value = ''; - client.send('/app/chat', {}, text); - addChatMessage(myUsername, text); - } - } - }); +function addChatAnnouncement(body) { + addMessageItem({ + body: body + }) } function addChatMessage(sender, body) { - var messageList = document.getElementById('messageList'); - var usernameNode = document.createElement('strong'); - var usernameTextNode = document.createTextNode(sender); - usernameNode.appendChild(usernameTextNode); - var messageTextNode = document.createTextNode(' ' + body); - var messageItem = document.createElement('div'); - messageItem.classList.add('message-item'); - messageItem.appendChild(usernameNode); - messageItem.appendChild(messageTextNode); - addMessageItem(messageList, messageItem); -} - -function addChatAnnouncement(body) { - var messageList = document.getElementById('messageList'); - var messageTextNode = document.createTextNode(body); - var messageItem = document.createElement('div'); - messageItem.classList.add('message-item'); - messageItem.classList.add('log'); - messageItem.appendChild(messageTextNode); - addMessageItem(messageList, messageItem); + addMessageItem({ + sender: sender, + body: body + }) } // Auto-scrolls the message list -function addMessageItem(messageList, messageItem) { - if (!messageList.hasChildNodes()) { - messageItem.classList.add('first'); - } - messageList.appendChild(messageItem); +function addMessageItem(messageItem) { + vm.messages.push(messageItem); + var messageList = document.getElementById('messageList'); messageList.scrollTop = messageList.scrollHeight; } @@ -256,124 +369,6 @@ function doHttpGet(url, callback) { xhr.send(); } -function drawMyBoard() { - var x = 25, y = MARGIN_TOP; - drawUsername(x, y, myUsername); - drawScore(x, y, myScore); - drawInput(x, y, myGuess); - var yStart = drawGuesses(x, y, myGuesses, myResults); - drawHint(x, yStart, myProgress); - drawGrid(x, y); -} - -function drawOpponentBoard() { - var x = 325, y = MARGIN_TOP; - drawUsername(x, y, opponentUsername); - drawScore(x, y, opponentScore); - drawResults(x, y, opponentResults); - drawGrid(x, y); -} - -function drawLastWord() { - if (lastWord) { - var x = canvas.width / 2; - var y = canvas.height - MARGIN_BOTTOM / 2; - ctx.fillStyle = 'black'; - ctx.fillText('Previous word: ' + lastWord.toUpperCase(), x, y); - } -} - -function drawUsername(x, y, username) { - var usernameX = x + WIDTH / 2; - var usernameY = y - 60; - ctx.fillStyle = 'black'; - ctx.fillText(username, usernameX, usernameY); -} - -function drawScore(x, y, score) { - var scoreX = x + WIDTH / 2; - var scoreY = y - 25; - ctx.fillStyle = 'black'; - ctx.fillText(score, scoreX, scoreY); -} - -function drawGrid(xOrigin, yOrigin) { - ctx.beginPath(); - for (var x = 0; x <= WIDTH; x += SIDE) { - ctx.moveTo(xOrigin + x, yOrigin); - ctx.lineTo(xOrigin + x, yOrigin + HEIGHT); - } - for (var y = 0; y <= HEIGHT; y += SIDE) { - ctx.moveTo(xOrigin, yOrigin + y); - ctx.lineTo(xOrigin + WIDTH, yOrigin + y); - } - ctx.strokeStyle = 'black'; - ctx.stroke(); -} - -function drawInput(xOrigin, yOrigin, input) { - ctx.fillStyle = 'green'; - var x = xOrigin + SIDE * 0.5; - var y = yOrigin + SIDE * 0.5; - for (var i = 0; i < myGuess.length; i++) { - ctx.fillText(myGuess[i], x, y); - x += SIDE; - } -} - -function drawGuesses(xOrigin, yOrigin, guesses, results) { - var y = yOrigin + SIDE * 1.5; - var numGuesses = Math.min(4, guesses.length); - for (var i = 0; i < numGuesses; i++) { - var x = xOrigin + SIDE * 0.5; - var guess = guesses[guesses.length - numGuesses + i]; - var result = results[results.length - numGuesses + i]; - for (var j = 0; j < 5; j++) { - if (result[j] === 1) { - ctx.fillStyle = 'yellow'; - ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE); - } else if (result[j] === 2) { - ctx.fillStyle = 'orange'; - ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE); - } - ctx.fillStyle = 'green'; - ctx.fillText(guess[j], x, y); - x += SIDE; - } - y += SIDE; - } - return y; -} - -function drawResults(xOrigin, yOrigin, results) { - var y = yOrigin + SIDE * 1.5; - var numResults = Math.min(4, results.length); - for (var i = 0; i < numResults; i++) { - var x = xOrigin + SIDE * 0.5; - var result = results[results.length - numResults + i]; - for (var j = 0; j < 5; j++) { - if (result[j] === 1) { - ctx.fillStyle = 'yellow'; - ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE); - } else if (result[j] === 2) { - ctx.fillStyle = 'orange'; - ctx.fillRect(x - SIDE * 0.5, y - SIDE * 0.5, SIDE, SIDE); - } - x += SIDE; - } - y += SIDE; - } - return y; -} - -function drawHint(xOrigin, yOrigin, progress) { - var x = xOrigin + SIDE * 0.5; - for (var i = 0; i < 5; i++) { - ctx.fillText(progress[i], x, yOrigin); - x += SIDE; - } -} - function isCharacter(charCode) { return isCharacterLowercase(charCode) || isCharacterUppercase(charCode); } @@ -395,64 +390,13 @@ function isValidResult(result) { return true; } -function removeGame(gameId) { - var indexToRemove = null; - for (var i = 0; i < vm.games.length; i++) { - if (vm.games[i].id === gameId) { - indexToRemove = i; - break; - } - } - vm.games.splice(indexToRemove, 1); -} - -function repaint() { - // clear the canvas - ctx.clearRect(0, 0, canvas.width, canvas.height); - - // draw the components - drawMyBoard(); - drawOpponentBoard(); - drawLastWord(); -} - -function reset(firstLetter, clearScore) { - if (!firstLetter) { - firstLetter = ''; - } - myGuess = ''; - myGuesses = []; - myProgress = [firstLetter, '', '', '', '']; - myResults = []; - opponentResults = []; - if (clearScore) { - myScore = 0; - opponentScore = 0; - } -} - -function toggleView() { - var lobbyColumn = document.getElementById('lobbyColumn'); - var gameColumn = document.getElementById('gameColumn') - if (lobbyColumn.classList.contains('primary')) { - lobbyColumn.classList.remove('primary'); - } else { - lobbyColumn.classList.add('primary'); - } - if (gameColumn.classList.contains('primary')) { - gameColumn.classList.remove('primary'); - } else { - gameColumn.classList.add('primary'); - } -} - function onChat(message) { var chatMessage = JSON.parse(message.body); var messageSender = chatMessage.username; var messageBody = chatMessage.message; if (messageSender === null) { addChatAnnouncement(messageBody); - } else if (messageSender === myUsername) { + } else if (messageSender === vm.username) { // Ignore messages sent by yourself } else { console.log('Message from ' + messageSender + ": " + messageBody); @@ -464,22 +408,26 @@ function onGameClosed(message) { var game = JSON.parse(message.body); var gameId = game.id; var playerOne = game.playerOne.username; - if (playerOne === myUsername) { + console.log(playerOne + ' closed Game ' + gameId); + if (playerOne === vm.username) { vm.gameId = null; } - console.log(playerOne + ' closed Game ' + gameId); - removeGame(gameId); + vm.removeGame(gameId); } function onGameHosted(message) { var game = JSON.parse(message.body); var gameId = game.id; var playerOne = game.playerOne.username; - if (playerOne === myUsername) { + console.log(playerOne + ' hosted Game ' + gameId); + vm.games.push({ + id: gameId, + playerOne: playerOne, + started: false + }); + if (playerOne === vm.username) { vm.gameId = gameId; } - console.log(playerOne + ' hosted Game ' + gameId); - vm.games.push({ id: gameId, playerOne: playerOne, started: false }); } function onGameJoined(message) { @@ -487,9 +435,6 @@ function onGameJoined(message) { var gameId = game.id; var playerOne = game.playerOne.username; var playerTwo = game.playerTwo.username; - if (playerTwo === myUsername) { - vm.gameId = gameId; - } console.log(playerTwo + ' joined ' + playerOne + "'s game"); for (var i = 0; i < vm.games.length; i++) { if (vm.games[i].id === gameId) { @@ -498,8 +443,8 @@ function onGameJoined(message) { break; } } - if (playerOne === myUsername || playerTwo === myUsername) { - toggleView(); + if (playerTwo === vm.username) { + vm.gameId = gameId; } } @@ -509,6 +454,7 @@ function onGameLeft(message) { var gameId = game.id; var playerOne = game.playerOne.username; var gameLeaver = report.gameLeaver.username; + console.log(gameLeaver + ' left ' + playerOne + "'s game"); var previousPlayers = []; for (var i = 0; i < vm.games.length; i++) { if (vm.games[i].id === gameId) { @@ -520,13 +466,11 @@ function onGameLeft(message) { break; } } - console.log(gameLeaver + ' left ' + playerOne + "'s game"); - if (gameLeaver === myUsername) { + if (gameLeaver === vm.username) { vm.gameId = null; } - if (previousPlayers.indexOf(myUsername) != -1) { + if (previousPlayers.indexOf(vm.username) != -1) { onOpponentLeft(); - toggleView(); } } @@ -534,9 +478,9 @@ function onGameStarted(message) { var report = JSON.parse(message.body); var playerOne = report[0]; var playerTwo = report[1]; - if (playerOne === myUsername) { + if (playerOne === vm.username) { addChatAnnouncement('You are playing with ' + playerTwo); - } else if (playerTwo === myUsername) { + } else if (playerTwo === vm.username) { addChatAnnouncement('You are playing with ' + playerOne); } else { addChatAnnouncement(playerOne + ' is playing with ' + playerTwo); @@ -546,18 +490,16 @@ function onGameStarted(message) { function onOpponentJoined(message) { var report = JSON.parse(message.body); var firstLetter = report[0]; - opponentUsername = report[1]; - console.log('Opponent username: ' + opponentUsername); - reset(firstLetter, true); - canvasDiv.classList.remove('hidden'); - repaint(); + vm.opponentUsername = report[1]; + console.log('Opponent username: ' + vm.opponentUsername); + vm.reset(firstLetter, true); + vm.repaint(); } function onOpponentLeft(message) { - opponentUsername = null; - lastWord = null; - canvasDiv.classList.add('hidden'); - repaint(); + vm.opponentUsername = null; + vm.lastWord = null; + vm.repaint(); } function onOpponentReport(message) { @@ -566,15 +508,15 @@ function onOpponentReport(message) { var guess = report.guess; var firstLetter = report.firstLetter; console.log('Opponent guessed correctly! ' + guess); - opponentScore = opponentScore + 100; - lastWord = guess; - reset(firstLetter, false); - repaint(); + vm.opponentScore = vm.opponentScore + 100; + vm.lastWord = guess; + vm.reset(firstLetter, false); + vm.repaint(); } else { var result = report.result; console.log('Opponent result: ' + result); - opponentResults.push(result); - repaint(); + vm.opponentResults.push(result); + vm.repaint(); } } @@ -585,27 +527,27 @@ function onPlayerReport(message) { var guess = report.guess; var firstLetter = report.firstLetter; console.log('I guessed correctly!'); - myScore = myScore + 100; - lastWord = guess; - reset(firstLetter, false); - repaint(); + vm.myScore = vm.myScore + 100; + vm.lastWord = guess; + vm.reset(firstLetter, false); + vm.repaint(); } else { var guess = report.guess; var result = report.result; console.log('My result: ' + result); // TODO: use isValidResult function if (result[0] === 9) { - myGuesses.push('-----'); + vm.myGuesses.push('-----'); } else { for (var i = 0; i < 5; i++) { if (result[i] === 2) { - myProgress[i] = guess[i]; + vm.myProgress[i] = guess[i]; } } - myGuesses.push(guess); + vm.myGuesses.push(guess); } - myResults.push(result); - repaint(); + vm.myResults.push(result); + vm.repaint(); } } @@ -613,7 +555,7 @@ function onUserJoined(message) { var report = JSON.parse(message.body); var username = report[0]; var numUsers = report[1]; - if (username === myUsername) { + if (username === vm.username) { addChatAnnouncement('Welcome to Lingo!'); if (numUsers === 1) { addChatAnnouncement('You are the only player online'); diff --git a/server/src/main/resources/static/index.html b/server/src/main/resources/static/index.html index 496bff6..293e3dc 100644 --- a/server/src/main/resources/static/index.html +++ b/server/src/main/resources/static/index.html @@ -8,19 +8,19 @@ -
+
Lingo
-
+

What is your name?

- -

+ +

{{ usernameError }}

-