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" />
|
<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>
|
||||||
|
|
|
@ -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}`;
|
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);
|
||||||
|
|
|
@ -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',
|
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() : '',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue