Merge branch 'main' into svg-logo

This commit is contained in:
evanpelle
2025-02-12 08:18:31 -08:00
committed by GitHub
6 changed files with 239 additions and 80 deletions
+76 -24
View File
@@ -198,7 +198,35 @@ export class HostLobbyModal extends LitElement {
align-items: center;
justify-content: center;
}
.option-card input[type="checkbox"] {
display: none;
}
label.option-card:hover {
transform: none;
}
.checkbox-icon {
width: 16px;
height: 16px;
border: 2px solid #aaa;
border-radius: 6px;
margin: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease-in-out;
}
.option-card.selected .checkbox-icon {
border-color: #4a9eff;
background: #4a9eff;
}
.option-card.selected .checkbox-icon::after {
content: "✓";
color: white;
}
/* HostLobbyModal css */
.clipboard-icon {
display: flex;
@@ -358,6 +386,54 @@ export class HostLobbyModal extends LitElement {
</div>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">Options</div>
<div class="option-cards">
<label
for="disable-bots"
class="option-card ${this.disableBots ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
.checked=${this.disableBots}
/>
<div class="option-card-title">Disable Bots</div>
</label>
<label
for="disable-npcs"
class="option-card ${this.disableNPCs ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
/>
<div class="option-card-title">Disable NPCs</div>
</label>
<label
for="creative-mode"
class="option-card ${this.creativeMode ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
.checked=${this.creativeMode}
/>
<div class="option-card-title">Creative Mode</div>
</label>
</div>
</div>
<!-- Lobby Selection -->
<div class="options-section">
<div class="option-title">
@@ -371,30 +447,6 @@ export class HostLobbyModal extends LitElement {
)}
</div>
</div>
<div>
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
/>
<label for="disable-bots">Disable Bots</label>
</div>
<div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
/>
<label for="disable-npcs">Disable NPCs</label>
</div>
<div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
/>
<label for="creative-mode">Creative mode</label>
</div>
</div>
<button
@click=${this.startGame}
+77
View File
@@ -9,6 +9,9 @@ export class JoinPrivateLobbyModal extends LitElement {
@state() private message: string = "";
@query("#lobbyIdInput") private lobbyIdInput!: HTMLInputElement;
@state() private hasJoined = false;
@state() private players: string[] = [];
private playersInterval = null;
static styles = css`
.modal-overlay {
@@ -180,6 +183,39 @@ export class JoinPrivateLobbyModal extends LitElement {
justify-content: center;
color: #fff;
}
.options-section {
background: rgba(0, 0, 0, 0.2);
padding: 12px 24px 24px 24px;
border-radius: 12px;
}
.option-title {
margin: 0 0 16px 0;
font-size: 20px;
color: #fff;
text-align: center;
}
.players-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
padding: 0 16px;
}
.player-tag {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
padding: 4px 16px;
border-radius: 16px;
font-size: 14px;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
`;
render() {
@@ -216,6 +252,24 @@ export class JoinPrivateLobbyModal extends LitElement {
<div class="message-area ${this.message ? "show" : ""}">
${this.message}
</div>
<div class="options-layout">
<!-- Lobby Selection -->
${this.hasJoined && this.players.length > 0
? html`<div class="options-section">
<div class="option-title">
${this.players.length}
${this.players.length === 1 ? "Player" : "Players"}
</div>
<div class="players-list">
${this.players.map(
(player) =>
html`<span class="player-tag">${player}</span>`
)}
</div>
</div>`
: ""}
</div>
${!this.hasJoined
? html`<button class="start-game-button" @click=${this.joinLobby}>
Join Lobby
@@ -233,6 +287,10 @@ export class JoinPrivateLobbyModal extends LitElement {
public close() {
this.isModalOpen = false;
this.lobbyIdInput.value = null;
if (this.playersInterval) {
clearInterval(this.playersInterval);
this.playersInterval = null;
}
}
public closeAndLeave() {
@@ -284,6 +342,7 @@ export class JoinPrivateLobbyModal extends LitElement {
composed: true,
})
);
this.playersInterval = setInterval(() => this.pollPlayers(), 1000);
} else {
this.message = "Lobby not found. Please check the ID and try again.";
}
@@ -293,4 +352,22 @@ export class JoinPrivateLobbyModal extends LitElement {
this.message = "An error occurred. Please try again.";
});
}
private async pollPlayers() {
if (!this.lobbyIdInput?.value) return;
fetch(`/lobby/${this.lobbyIdInput.value}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
this.players = data.players.map((p) => p.username);
})
.catch((error) => {
consolex.error("Error polling players:", error);
});
}
}
+76 -48
View File
@@ -193,6 +193,36 @@ export class SinglePlayerModal extends LitElement {
align-items: center;
justify-content: center;
}
.option-card input[type="checkbox"] {
display: none;
}
label.option-card:hover {
transform: none;
}
.checkbox-icon {
width: 16px;
height: 16px;
border: 2px solid #aaa;
border-radius: 6px;
margin: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease-in-out;
}
.option-card.selected .checkbox-icon {
border-color: #4a9eff;
background: #4a9eff;
}
.option-card.selected .checkbox-icon::after {
content: "✓";
color: white;
}
`;
render() {
@@ -251,56 +281,54 @@ export class SinglePlayerModal extends LitElement {
)}
</div>
</div>
</div>
<div>
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
/>
<label for="disable-bots">Disable Bots</label>
</div>
<div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
/>
<label for="disable-npcs">Disable NPCs</label>
</div>
<div>
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
/>
<label for="disable-bots">Disable Bots</label>
</div>
<div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
/>
<label for="disable-npcs">Disable NPCs</label>
</div>
<div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
/>
<label for="creative-mode">Creative mode</label>
</div>
<!-- Game Options -->
<div class="options-section">
<div class="option-title">Options</div>
<div class="option-cards">
<label
for="disable-bots"
class="option-card ${this.disableBots ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-bots"
@change=${this.handleDisableBotsChange}
.checked=${this.disableBots}
/>
<div class="option-card-title">Disable Bots</div>
</label>
<div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
/>
<label for="creative-mode">Creative mode</label>
<label
for="disable-npcs"
class="option-card ${this.disableNPCs ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="disable-npcs"
@change=${this.handleDisableNPCsChange}
.checked=${this.disableNPCs}
/>
<div class="option-card-title">Disable NPCs</div>
</label>
<label
for="creative-mode"
class="option-card ${this.creativeMode ? "selected" : ""}"
>
<div class="checkbox-icon"></div>
<input
type="checkbox"
id="creative-mode"
@change=${this.handleCreativeModeChange}
.checked=${this.creativeMode}
/>
<div class="option-card-title">Creative Mode</div>
</label>
</div>
</div>
</div>
<button @click=${this.startGame} class="start-game-button">
+4 -5
View File
@@ -201,11 +201,10 @@
<div class="bottom-0 w-full flex-col-reverse sm:flex-row z-50" style="position: fixed; pointer-events: none">
<div class="w-full sm:w-2/3 sm:fixed sm:right-0 sm:bottom-0 sm:flex justify-end" style="pointer-events: auto">
<events-display></events-display>
</div>
<div class="w-full sm:w-1/3" style="pointer-events: auto">
<control-panel></control-panel>
</div>
<events-display></events-display>
</div>
<div class="w-full sm:w-1/3 md:max-w-72" style="pointer-events: auto">
<control-panel></control-panel>
</div>
<!-- Footer section -->
+4 -1
View File
@@ -100,7 +100,8 @@ const GameConfigSchema = z.object({
const SafeString = z
.string()
// Remove common dangerous characters and patterns
.regex(/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=\[\]{}|;:"'\/]+$/)
// The weird \u stuff is to allow emojis
.regex(/^[a-zA-Z0-9\s.,!?@#$%&*()-_+=\[\]{}|;:"'\/\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]]+$/)
// Reasonable max length to prevent DOS
.max(1000);
@@ -323,3 +324,5 @@ export const GameRecordSchema = z.object({
turns: z.array(TurnSchema),
winner: ID.nullable(),
});
+2 -2
View File
@@ -13,7 +13,7 @@ const matcher = new RegExpMatcher({
export const MIN_USERNAME_LENGTH = 3;
export const MAX_USERNAME_LENGTH = 27;
const validPattern = /^[a-zA-Z0-9_\[\] 🐈🍀]+$/;
const validPattern = /^[a-zA-Z0-9_\[\] 🐈🍀]+$/u;
const shadowNames = [
"NicePeopleOnly",
@@ -71,7 +71,7 @@ export function validateUsername(username: string): {
export function sanitizeUsername(str: string): string {
const sanitized = str
.replace(/[^a-zA-Z0-9_\[\] 🐈🍀]/g, "")
.replace(/[^a-zA-Z0-9_\[\] 🐈🍀]/gu, "")
.slice(0, MAX_USERNAME_LENGTH);
return sanitized.padEnd(MIN_USERNAME_LENGTH, "x");
}