Add name editing. Add icons for all buttons.

This commit is contained in:
sepia 2025-07-23 16:50:35 -05:00
parent 02d777c364
commit bc45f3a604
13 changed files with 253 additions and 12 deletions

View File

@ -17,15 +17,40 @@
<link rel="stylesheet" href="style.css" /> <link rel="stylesheet" href="style.css" />
</head> </head>
<body> <body>
<div id="game-container" hx-ext="ws"> <div id="ws-container" hx-ext="ws">
<div id="player-profile-box" class="player-profile-box">
<span id="display-name"></span>
<img
id="edit-display-name-icon"
src="/icons/edit.svg"
alt="Edit Display Name"
class="icon edit-icon"
/>
<div id="display-name-edit-controls" style="display: none">
<input
type="text"
id="display-name-input"
maxlength="20"
placeholder="Enter display name"
/>
<button id="save-display-name-ws-button" ws-send="click">Save</button>
<button id="cancel-display-name-button">Cancel</button>
</div>
</div>
<div id="game-container">
<div class="status-bar">
<div id="title-box"></div> <div id="title-box"></div>
</div>
<div id="game-board"></div> <div id="game-board"></div>
<div id="button-box"></div> <div id="button-box"></div>
</div> </div>
<script src="scripts/display-ws-connection.js"></script> <script src="scripts/display-ws-connection.js"></script>
<script src="scripts/send-ws-messages.js"></script> <script src="scripts/send-ws-messages.js"></script>
<script src="scripts/profile-editor.js"></script>
<script src="scripts/copy-game-link.js"></script> <script src="scripts/copy-game-link.js"></script>
<script src="scripts/handle-redirects.js"></script> <script src="scripts/handle-redirects.js"></script>
</div>
</body> </body>
</html> </html>

4
public/icons/accept.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>

After

Width:  |  Height:  |  Size: 221 B

4
public/icons/decline.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>

After

Width:  |  Height:  |  Size: 220 B

4
public/icons/draw.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v17.25m0 0c-1.472 0-2.882.265-4.185.75M12 20.25c1.472 0 2.882.265 4.185.75M18.75 4.97A48.416 48.416 0 0 0 12 4.5c-2.291 0-4.545.16-6.75.47m13.5 0c1.01.143 2.01.317 3 .52m-3-.52 2.62 10.726c.122.499-.106 1.028-.589 1.202a5.988 5.988 0 0 1-2.031.352 5.988 5.988 0 0 1-2.031-.352c-.483-.174-.711-.703-.59-1.202L18.75 4.971Zm-16.5.52c.99-.203 1.99-.377 3-.52m0 0 2.62 10.726c.122.499-.106 1.028-.589 1.202a5.989 5.989 0 0 1-2.031.352 5.989 5.989 0 0 1-2.031-.352c-.483-.174-.711-.703-.59-1.202L5.25 4.971Z" />
</svg>

After

Width:  |  Height:  |  Size: 706 B

4
public/icons/resign.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 3v1.5M3 21v-6m0 0 2.77-.693a9 9 0 0 1 6.208.682l.108.054a9 9 0 0 0 6.086.71l3.114-.732a48.524 48.524 0 0 1-.005-10.499l-3.11.732a9 9 0 0 1-6.085-.711l-.108-.054a9 9 0 0 0-6.208-.682L3 4.5M3 15V4.5" />
</svg>

After

Width:  |  Height:  |  Size: 399 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m15 15 6-6m0 0-6-6m6 6H9a6 6 0 0 0 0 12h3" />
</svg>

After

Width:  |  Height:  |  Size: 241 B

4
public/icons/undo.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
</svg>

After

Width:  |  Height:  |  Size: 242 B

View File

@ -11,7 +11,7 @@ const playerId = playerIdMeta.content;
const wsUrl = `ws://${window.location.host}/ws?gameId=${gameId}&playerId=${playerId}`; const wsUrl = `ws://${window.location.host}/ws?gameId=${gameId}&playerId=${playerId}`;
// Get the game container element // Get the game container element
const gameContainer = document.getElementById('game-container'); const gameContainer = document.getElementById('ws-container');
// Set the ws-connect attribute // Set the ws-connect attribute
gameContainer.setAttribute('ws-connect', wsUrl); gameContainer.setAttribute('ws-connect', wsUrl);

View File

@ -0,0 +1,42 @@
document.addEventListener('DOMContentLoaded', () => {
const displayNameSpan = document.getElementById('display-name');
const editIcon = document.getElementById('edit-display-name-icon');
const editControls = document.getElementById('display-name-edit-controls');
const displayNameInput = document.getElementById('display-name-input');
const saveButton = document.getElementById('save-display-name-ws-button');
const cancelButton = document.getElementById('cancel-display-name-button');
// Get playerId from meta tag
const playerIdMeta = document.querySelector('meta[name="playerId"]');
const playerId = playerIdMeta ? playerIdMeta.content : 'UnknownPlayer';
// Initialize display name with player ID
displayNameSpan.textContent = playerId;
function setEditMode(isEditing) {
if (isEditing) {
displayNameSpan.style.display = 'none';
editIcon.style.display = 'none';
editControls.style.display = 'flex';
displayNameInput.value = displayNameSpan.textContent;
displayNameInput.focus();
} else {
displayNameSpan.style.display = 'inline';
editIcon.style.display = 'inline';
editControls.style.display = 'none';
}
}
editIcon.addEventListener('click', () => setEditMode(true));
cancelButton.addEventListener('click', () => setEditMode(false));
saveButton.addEventListener('click', () => {
// The actual sending of the message is handled by hx-trigger and send-ws-messages.js
// We just handle the optimistic UI update here.
const newName = displayNameInput.value.trim();
if (newName && newName !== displayNameSpan.textContent) {
displayNameSpan.textContent = newName; // Optimistically update display
}
setEditMode(false);
});
});

View File

@ -73,5 +73,11 @@ document.addEventListener('htmx:wsConfigSend', function (e) {
type: 'rematch', type: 'rematch',
action: 'cancel', action: 'cancel',
}; };
} else if (e.target.id == 'save-display-name-ws-button') {
const displayNameInput = document.getElementById('display-name-input');
e.detail.parameters = {
type: 'update_display_name',
displayName: displayNameInput ? displayNameInput.value.trim() : '',
};
} }
}); });

View File

@ -273,3 +273,66 @@ button:hover {
#copy-link-button.copied-state { #copy-link-button.copied-state {
background-color: var(--color-success); background-color: var(--color-success);
} }
.player-profile-box {
position: absolute;
top: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 10px;
background-color: var(--color-neutral-100);
color: var(--color-on-light);
padding: 8px 12px;
border-radius: 20px;
box-shadow: 0 2px 4px var(--color-shadow);
}
.player-profile-box .edit-icon {
cursor: pointer;
width: 1.4em;
height: 1.4em;
opacity: 0.6;
transition: opacity 0.3s ease;
}
.player-profile-box .edit-icon:hover {
opacity: 1;
}
.player-profile-box #display-name-edit-controls {
display: flex;
gap: 5px;
align-items: center;
}
.player-profile-box #display-name-input {
border: 1px solid var(--color-neutral-300);
border-radius: 5px;
padding: 5px 8px;
font-size: 1em;
width: 120px;
}
.player-profile-box button {
background-color: var(--color-success);
color: var(--color-on-primary);
border: none;
border-radius: 5px;
padding: 5px 10px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.3s ease;
}
.player-profile-box button:hover {
background-color: var(--color-success-light);
}
.player-profile-box button#cancel-display-name-button {
background-color: var(--color-error);
}
.player-profile-box button#cancel-display-name-button:hover {
background-color: var(--color-error-light);
}

View File

@ -7,7 +7,12 @@ export interface Message {
| 'takeback' | 'takeback'
| 'draw' | 'draw'
| 'rematch' | 'rematch'
| 'redirect_to_game'; | 'redirect_to_game'
| 'update_display_name';
}
export interface UpdateDisplayNameMessage extends Message {
displayName: string;
} }
export interface MakeMoveMessage extends Message { export interface MakeMoveMessage extends Message {

View File

@ -10,6 +10,7 @@ import {
DrawMessage, DrawMessage,
RematchMessage, RematchMessage,
RedirectToGameMessage, RedirectToGameMessage,
UpdateDisplayNameMessage,
} from './messages'; } from './messages';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -27,6 +28,7 @@ class PlayerConnection {
} }
public sendMessage(severity: 'info' | 'error', message: string) { public sendMessage(severity: 'info' | 'error', message: string) {
console.log(`Sending message ${message} to player ${this.id}`);
// TODO // TODO
} }
} }
@ -146,17 +148,26 @@ class GameServer {
if (this.takebackRequesterId === conn.id) { if (this.takebackRequesterId === conn.id) {
buttons.push( buttons.push(
<button id="cancel-takeback-request-button" ws-send="click"> <button id="cancel-takeback-request-button" ws-send="click">
<svg class="icon" alt="Cancel">
<use href="/icons/decline.svg"></use>
</svg>
Cancel Takeback Request Cancel Takeback Request
</button>, </button>,
); );
} else { } else {
buttons.push( buttons.push(
<button id="accept-takeback-button" ws-send="click"> <button id="accept-takeback-button" ws-send="click">
<svg class="icon" alt="Accept">
<use href="/icons/accept.svg"></use>
</svg>
Accept Takeback Accept Takeback
</button>, </button>,
); );
buttons.push( buttons.push(
<button id="decline-takeback-button" ws-send="click"> <button id="decline-takeback-button" ws-send="click">
<svg class="icon" alt="Decline">
<use href="/icons/decline.svg"></use>
</svg>
Decline Takeback Decline Takeback
</button>, </button>,
); );
@ -165,17 +176,26 @@ class GameServer {
if (this.drawRequesterId === conn.id) { if (this.drawRequesterId === conn.id) {
buttons.push( buttons.push(
<button id="cancel-draw-request-button" ws-send="click"> <button id="cancel-draw-request-button" ws-send="click">
<svg class="icon" alt="Cancel">
<use href="/icons/decline.svg"></use>
</svg>
Cancel Draw Request Cancel Draw Request
</button>, </button>,
); );
} else { } else {
buttons.push( buttons.push(
<button id="accept-draw-button" ws-send="click"> <button id="accept-draw-button" ws-send="click">
<svg class="icon" alt="Accept">
<use href="/icons/accept.svg"></use>
</svg>
Accept Draw Accept Draw
</button>, </button>,
); );
buttons.push( buttons.push(
<button id="decline-draw-button" ws-send="click"> <button id="decline-draw-button" ws-send="click">
<svg class="icon" alt="Decline">
<use href="/icons/decline.svg"></use>
</svg>
Decline Draw Decline Draw
</button>, </button>,
); );
@ -183,16 +203,25 @@ class GameServer {
} else { } else {
buttons.push( buttons.push(
<button id="resign-button" ws-send="click"> <button id="resign-button" ws-send="click">
<svg class="icon" alt="Resign">
<use href="/icons/resign.svg"></use>
</svg>
Resign Resign
</button>, </button>,
); );
buttons.push( buttons.push(
<button id="takeback-button" ws-send="click"> <button id="takeback-button" ws-send="click">
<svg class="icon" alt="Takeback">
<use href="/icons/undo.svg"></use>
</svg>
Takeback Takeback
</button>, </button>,
); );
buttons.push( buttons.push(
<button id="draw-button" ws-send="click"> <button id="draw-button" ws-send="click">
<svg class="icon" alt="Draw">
<use href="/icons/draw.svg"></use>
</svg>
Draw Draw
</button>, </button>,
); );
@ -202,17 +231,26 @@ class GameServer {
if (this.rematchRequesterId === conn.id) { if (this.rematchRequesterId === conn.id) {
buttons.push( buttons.push(
<button id="cancel-rematch-request-button" ws-send="click"> <button id="cancel-rematch-request-button" ws-send="click">
<svg class="icon" alt="Cancel">
<use href="/icons/decline.svg"></use>
</svg>
Cancel Rematch Request Cancel Rematch Request
</button>, </button>,
); );
} else { } else {
buttons.push( buttons.push(
<button id="accept-rematch-button" ws-send="click"> <button id="accept-rematch-button" ws-send="click">
<svg class="icon" alt="Accept">
<use href="/icons/accept.svg"></use>
</svg>
Accept Rematch Accept Rematch
</button>, </button>,
); );
buttons.push( buttons.push(
<button id="decline-rematch-button" ws-send="click"> <button id="decline-rematch-button" ws-send="click">
<svg class="icon" alt="Decline">
<use href="/icons/decline.svg"></use>
</svg>
Decline Rematch Decline Rematch
</button>, </button>,
); );
@ -220,6 +258,9 @@ class GameServer {
} else { } else {
buttons.push( buttons.push(
<button id="rematch-button" ws-send="click"> <button id="rematch-button" ws-send="click">
<svg class="icon" alt="Rematch">
<use href="/icons/rotate-right.svg"></use>
</svg>
Rematch Rematch
</button>, </button>,
); );
@ -248,7 +289,7 @@ class GameServer {
const classes = `player-name ${'player-' + color} ${turnClass}`.trim(); const classes = `player-name ${'player-' + color} ${turnClass}`.trim();
return ( return (
<span class={classes}> <span class={classes}>
{playerId} {this.connections.get(playerId)?.name}
{connectionIcon} {connectionIcon}
</span> </span>
); );
@ -287,6 +328,10 @@ class GameServer {
this.handleRematchMessage(conn, message as RematchMessage); this.handleRematchMessage(conn, message as RematchMessage);
break; break;
} }
case 'update_display_name': {
this.handleUpdateDisplayName(conn, message as UpdateDisplayNameMessage);
break;
}
} }
} }
@ -588,6 +633,37 @@ class GameServer {
this.rematchRequesterId = null; this.rematchRequesterId = null;
this.broadcastButtons(); this.broadcastButtons();
} }
private handleUpdateDisplayName(
conn: PlayerConnection,
message: UpdateDisplayNameMessage,
): void {
const newDisplayName = message.displayName.trim();
if (!newDisplayName) {
conn.sendMessage('error', 'Display name cannot be empty.');
return;
}
if (newDisplayName.length > 20) {
conn.sendMessage(
'error',
'Display name cannot be longer than 20 characters.',
);
return;
}
if (newDisplayName === conn.name) {
return; // No change, do nothing
}
conn.name = newDisplayName;
this.broadcastTitle();
conn.sendMessage(
'info',
`Your display name has been updated to "${newDisplayName}".`,
);
}
} }
export class WebSocketHandler { export class WebSocketHandler {