Add name editing. Add icons for all buttons.
This commit is contained in:
parent
02d777c364
commit
bc45f3a604
27
index.html
27
index.html
|
@ -17,15 +17,40 @@
|
|||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<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>
|
||||
<div id="game-board"></div>
|
||||
<div id="button-box"></div>
|
||||
</div>
|
||||
|
||||
<script src="scripts/display-ws-connection.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/handle-redirects.js"></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -11,7 +11,7 @@ const playerId = playerIdMeta.content;
|
|||
const wsUrl = `ws://${window.location.host}/ws?gameId=${gameId}&playerId=${playerId}`;
|
||||
|
||||
// Get the game container element
|
||||
const gameContainer = document.getElementById('game-container');
|
||||
const gameContainer = document.getElementById('ws-container');
|
||||
|
||||
// Set the ws-connect attribute
|
||||
gameContainer.setAttribute('ws-connect', wsUrl);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -73,5 +73,11 @@ document.addEventListener('htmx:wsConfigSend', function (e) {
|
|||
type: 'rematch',
|
||||
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() : '',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -273,3 +273,66 @@ button:hover {
|
|||
#copy-link-button.copied-state {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,12 @@ export interface Message {
|
|||
| 'takeback'
|
||||
| 'draw'
|
||||
| 'rematch'
|
||||
| 'redirect_to_game';
|
||||
| 'redirect_to_game'
|
||||
| 'update_display_name';
|
||||
}
|
||||
|
||||
export interface UpdateDisplayNameMessage extends Message {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface MakeMoveMessage extends Message {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
DrawMessage,
|
||||
RematchMessage,
|
||||
RedirectToGameMessage,
|
||||
UpdateDisplayNameMessage,
|
||||
} from './messages';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
@ -27,6 +28,7 @@ class PlayerConnection {
|
|||
}
|
||||
|
||||
public sendMessage(severity: 'info' | 'error', message: string) {
|
||||
console.log(`Sending message ${message} to player ${this.id}`);
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
@ -146,17 +148,26 @@ class GameServer {
|
|||
if (this.takebackRequesterId === conn.id) {
|
||||
buttons.push(
|
||||
<button id="cancel-takeback-request-button" ws-send="click">
|
||||
<svg class="icon" alt="Cancel">
|
||||
<use href="/icons/decline.svg"></use>
|
||||
</svg>
|
||||
Cancel Takeback Request
|
||||
</button>,
|
||||
);
|
||||
} else {
|
||||
buttons.push(
|
||||
<button id="accept-takeback-button" ws-send="click">
|
||||
<svg class="icon" alt="Accept">
|
||||
<use href="/icons/accept.svg"></use>
|
||||
</svg>
|
||||
Accept Takeback
|
||||
</button>,
|
||||
);
|
||||
buttons.push(
|
||||
<button id="decline-takeback-button" ws-send="click">
|
||||
<svg class="icon" alt="Decline">
|
||||
<use href="/icons/decline.svg"></use>
|
||||
</svg>
|
||||
Decline Takeback
|
||||
</button>,
|
||||
);
|
||||
|
@ -165,17 +176,26 @@ class GameServer {
|
|||
if (this.drawRequesterId === conn.id) {
|
||||
buttons.push(
|
||||
<button id="cancel-draw-request-button" ws-send="click">
|
||||
<svg class="icon" alt="Cancel">
|
||||
<use href="/icons/decline.svg"></use>
|
||||
</svg>
|
||||
Cancel Draw Request
|
||||
</button>,
|
||||
);
|
||||
} else {
|
||||
buttons.push(
|
||||
<button id="accept-draw-button" ws-send="click">
|
||||
<svg class="icon" alt="Accept">
|
||||
<use href="/icons/accept.svg"></use>
|
||||
</svg>
|
||||
Accept Draw
|
||||
</button>,
|
||||
);
|
||||
buttons.push(
|
||||
<button id="decline-draw-button" ws-send="click">
|
||||
<svg class="icon" alt="Decline">
|
||||
<use href="/icons/decline.svg"></use>
|
||||
</svg>
|
||||
Decline Draw
|
||||
</button>,
|
||||
);
|
||||
|
@ -183,16 +203,25 @@ class GameServer {
|
|||
} else {
|
||||
buttons.push(
|
||||
<button id="resign-button" ws-send="click">
|
||||
<svg class="icon" alt="Resign">
|
||||
<use href="/icons/resign.svg"></use>
|
||||
</svg>
|
||||
Resign
|
||||
</button>,
|
||||
);
|
||||
buttons.push(
|
||||
<button id="takeback-button" ws-send="click">
|
||||
<svg class="icon" alt="Takeback">
|
||||
<use href="/icons/undo.svg"></use>
|
||||
</svg>
|
||||
Takeback
|
||||
</button>,
|
||||
);
|
||||
buttons.push(
|
||||
<button id="draw-button" ws-send="click">
|
||||
<svg class="icon" alt="Draw">
|
||||
<use href="/icons/draw.svg"></use>
|
||||
</svg>
|
||||
Draw
|
||||
</button>,
|
||||
);
|
||||
|
@ -202,17 +231,26 @@ class GameServer {
|
|||
if (this.rematchRequesterId === conn.id) {
|
||||
buttons.push(
|
||||
<button id="cancel-rematch-request-button" ws-send="click">
|
||||
<svg class="icon" alt="Cancel">
|
||||
<use href="/icons/decline.svg"></use>
|
||||
</svg>
|
||||
Cancel Rematch Request
|
||||
</button>,
|
||||
);
|
||||
} else {
|
||||
buttons.push(
|
||||
<button id="accept-rematch-button" ws-send="click">
|
||||
<svg class="icon" alt="Accept">
|
||||
<use href="/icons/accept.svg"></use>
|
||||
</svg>
|
||||
Accept Rematch
|
||||
</button>,
|
||||
);
|
||||
buttons.push(
|
||||
<button id="decline-rematch-button" ws-send="click">
|
||||
<svg class="icon" alt="Decline">
|
||||
<use href="/icons/decline.svg"></use>
|
||||
</svg>
|
||||
Decline Rematch
|
||||
</button>,
|
||||
);
|
||||
|
@ -220,6 +258,9 @@ class GameServer {
|
|||
} else {
|
||||
buttons.push(
|
||||
<button id="rematch-button" ws-send="click">
|
||||
<svg class="icon" alt="Rematch">
|
||||
<use href="/icons/rotate-right.svg"></use>
|
||||
</svg>
|
||||
Rematch
|
||||
</button>,
|
||||
);
|
||||
|
@ -248,7 +289,7 @@ class GameServer {
|
|||
const classes = `player-name ${'player-' + color} ${turnClass}`.trim();
|
||||
return (
|
||||
<span class={classes}>
|
||||
{playerId}
|
||||
{this.connections.get(playerId)?.name}
|
||||
{connectionIcon}
|
||||
</span>
|
||||
);
|
||||
|
@ -287,6 +328,10 @@ class GameServer {
|
|||
this.handleRematchMessage(conn, message as RematchMessage);
|
||||
break;
|
||||
}
|
||||
case 'update_display_name': {
|
||||
this.handleUpdateDisplayName(conn, message as UpdateDisplayNameMessage);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -588,6 +633,37 @@ class GameServer {
|
|||
this.rematchRequesterId = null;
|
||||
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 {
|
||||
|
|
Loading…
Reference in New Issue