Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 245 additions & 36 deletions src/client/PublicLobby.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ export class PublicLobby extends LitElement {
@state() public isLobbyHighlighted: boolean = false;
@state() private isButtonDebounced: boolean = false;
@state() private mapImages: Map<GameID, string> = new Map();
@state() private currentLobbyIndex: number = 0;
private lobbiesInterval: number | null = null;
private currLobby: GameInfo | null = null;
private debounceDelay: number = 750;
private lobbyIDToStart = new Map<GameID, number>();
private isDragging: boolean = false;
private startX: number = 0;
private currentX: number = 0;
private dragOffset: number = 0;
private hasDragged: boolean = false;

createRenderRoot() {
return this;
Expand All @@ -41,7 +47,14 @@ export class PublicLobby extends LitElement {

private async fetchAndUpdateLobbies(): Promise<void> {
try {
const previousLobbies = [...this.lobbies];
this.lobbies = await this.fetchLobbies();

// If we have a selected lobby, try to follow it in the carousel
if (this.currLobby) {
this.followSelectedLobby(previousLobbies);
}

this.lobbies.forEach((l) => {
// Store the start time on first fetch because endpoint is cached, causing
// the time to appear irregular.
Expand Down Expand Up @@ -91,15 +104,96 @@ export class PublicLobby extends LitElement {
clearInterval(this.lobbiesInterval);
this.lobbiesInterval = null;
}
// Hide the component when stopping
this.style.display = "none";
}

render() {
if (this.lobbies.length === 0) return html``;

const lobby = this.lobbies[0];
const lobbiesToShow = this.lobbies.slice(0, 3);

return html`
<div class="relative flex items-center">
<!-- Left Arrow -->
${lobbiesToShow.length > 1
? html`
<button
@click=${this.previousLobby}
class="absolute -left-12 top-1/2 transform -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full w-10 h-10 flex items-center justify-center transition-colors z-20 disabled:opacity-30 disabled:cursor-not-allowed"
?disabled=${this.currentLobbyIndex === 0}
>
</button>
`
: ""}

<!-- Carousel Container -->
<div class="relative overflow-hidden rounded-xl flex-1">
<!-- Lobby Display -->
<div
class="flex ${this.isDragging
? ""
: "transition-transform duration-300 ease-in-out"}"
style="transform: translateX(calc(-${this.currentLobbyIndex *
100}% + ${this.dragOffset}px))"
@mousedown=${this.handleMouseDown}
@touchstart=${this.handleTouchStart}
>
${lobbiesToShow.map(
(lobby, index) =>
html`<div
class="w-full flex-shrink-0"
@click=${(e: Event) => this.handleLobbyClick(e, lobby)}
>
${this.renderLobby(lobby, index)}
</div>`,
)}
</div>
</div>

<!-- Right Arrow -->
${lobbiesToShow.length > 1
? html`
<button
@click=${this.nextLobby}
class="absolute -right-12 top-1/2 transform -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full w-10 h-10 flex items-center justify-center transition-colors z-20 disabled:opacity-30 disabled:cursor-not-allowed"
?disabled=${this.currentLobbyIndex >= lobbiesToShow.length - 1}
>
</button>
`
: ""}

<!-- Dots Indicator -->
${lobbiesToShow.length > 1
? html`
<div
class="absolute bottom-2 left-1/2 transform -translate-x-1/2 flex gap-2"
>
${lobbiesToShow.map(
(_, index) => html`
<button
@click=${() => this.goToLobby(index)}
class="w-2 h-2 rounded-full transition-colors ${index ===
this.currentLobbyIndex
? "bg-blue-300"
: "bg-white hover:bg-gray-400"}"
></button>
`,
)}
</div>
`
: ""}
</div>
`;
}

private renderLobby(lobby: GameInfo, index: number) {
if (!lobby?.gameConfig) {
return;
return html``;
}

const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0;
const timeRemaining = Math.max(0, Math.floor((start - Date.now()) / 1000));

Expand All @@ -114,18 +208,13 @@ export class PublicLobby extends LitElement {
: null;

const mapImageSrc = this.mapImages.get(lobby.gameID);
const isCurrentLobby = this.currLobby?.gameID === lobby.gameID;

return html`
<button
@click=${() => this.lobbyClicked(lobby)}
?disabled=${this.isButtonDebounced}
class="isolate grid h-40 grid-cols-[100%] grid-rows-[100%] place-content-stretch w-full overflow-hidden ${this
.isLobbyHighlighted
<div
class="isolate grid h-40 grid-cols-[100%] grid-rows-[100%] place-content-stretch w-full overflow-hidden ${isCurrentLobby
? "bg-gradient-to-r from-green-600 to-green-500"
: "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${this
.isButtonDebounced
? "opacity-70 cursor-not-allowed"
: ""}"
: "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 cursor-pointer select-none"
>
${mapImageSrc
? html`<img
Expand All @@ -143,10 +232,13 @@ export class PublicLobby extends LitElement {
<div>
<div class="text-lg md:text-2xl font-semibold">
${translateText("public_lobby.join")}
${index > 0
? html`<span class="text-sm opacity-75"> (#${index + 1})</span>`
: ""}
</div>
<div class="text-md font-medium text-blue-100">
<span
class="text-sm ${this.isLobbyHighlighted
class="text-sm ${isCurrentLobby
? "text-green-600"
: "text-blue-600"} bg-white rounded-sm px-1"
>
Expand All @@ -173,7 +265,7 @@ export class PublicLobby extends LitElement {
<div class="text-md font-medium text-blue-100">${timeDisplay}</div>
</div>
</div>
</button>
</div>
`;
}

Expand All @@ -182,6 +274,134 @@ export class PublicLobby extends LitElement {
this.currLobby = null;
}

private followSelectedLobby(previousLobbies: GameInfo[]) {
if (!this.currLobby) return;

const previousIndex = previousLobbies.findIndex(
(lobby) => lobby.gameID === this.currLobby!.gameID,
);
const newIndex = this.lobbies.findIndex(
(lobby) => lobby.gameID === this.currLobby!.gameID,
);

if (newIndex !== -1) {
const wasShowingSelectedLobby = this.currentLobbyIndex === previousIndex;
const lobbyMovedPosition =
previousIndex !== newIndex && previousIndex !== -1;

if (wasShowingSelectedLobby && lobbyMovedPosition) {
const maxIndex = Math.min(this.lobbies.length - 1, 2); // Max 3 lobbies
this.currentLobbyIndex = Math.min(newIndex, maxIndex);
}
} else {
this.currLobby = null;
this.isLobbyHighlighted = false;
}
}

private nextLobby() {
const maxIndex = Math.min(this.lobbies.length - 1, 2); // Max 3 lobbies
if (this.currentLobbyIndex < maxIndex) {
this.currentLobbyIndex++;
}
}

private previousLobby() {
if (this.currentLobbyIndex > 0) {
this.currentLobbyIndex--;
}
}

private goToLobby(index: number) {
const maxIndex = Math.min(this.lobbies.length - 1, 2);
this.currentLobbyIndex = Math.max(0, Math.min(index, maxIndex));
}

private handleMouseDown(e: MouseEvent) {
this.isDragging = true;
this.hasDragged = false;
this.startX = e.clientX;
this.currentX = e.clientX;
this.dragOffset = 0;

const handleMouseMove = (e: MouseEvent) => {
if (!this.isDragging) return;
this.currentX = e.clientX;
this.dragOffset = this.currentX - this.startX;

if (Math.abs(this.dragOffset) > 5) {
this.hasDragged = true;
}

this.requestUpdate();
};

const handleMouseUp = () => {
if (this.isDragging) {
this.handleDragEnd();
}
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};

document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}

private handleTouchStart(e: TouchEvent) {
this.isDragging = true;
this.startX = e.touches[0].clientX;
this.currentX = e.touches[0].clientX;

const handleTouchMove = (e: TouchEvent) => {
if (!this.isDragging) return;
this.currentX = e.touches[0].clientX;
};

const handleTouchEnd = () => {
if (this.isDragging) {
this.handleDragEnd();
}
document.removeEventListener("touchmove", handleTouchMove);
document.removeEventListener("touchend", handleTouchEnd);
};

document.addEventListener("touchmove", handleTouchMove);
document.addEventListener("touchend", handleTouchEnd);
}
Comment on lines +351 to +371
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing drag offset tracking in touch handler

The touch handler doesn't update dragOffset or set hasDragged like the mouse handler does, which could cause inconsistent behavior on mobile.

   private handleTouchStart(e: TouchEvent) {
     this.isDragging = true;
+    this.hasDragged = false;
     this.startX = e.touches[0].clientX;
     this.currentX = e.touches[0].clientX;
+    this.dragOffset = 0;

     const handleTouchMove = (e: TouchEvent) => {
       if (!this.isDragging) return;
       this.currentX = e.touches[0].clientX;
+      this.dragOffset = this.currentX - this.startX;
+      
+      if (Math.abs(this.dragOffset) > 5) {
+        this.hasDragged = true;
+      }
+      
+      this.requestUpdate();
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private handleTouchStart(e: TouchEvent) {
this.isDragging = true;
this.startX = e.touches[0].clientX;
this.currentX = e.touches[0].clientX;
const handleTouchMove = (e: TouchEvent) => {
if (!this.isDragging) return;
this.currentX = e.touches[0].clientX;
};
const handleTouchEnd = () => {
if (this.isDragging) {
this.handleDragEnd();
}
document.removeEventListener("touchmove", handleTouchMove);
document.removeEventListener("touchend", handleTouchEnd);
};
document.addEventListener("touchmove", handleTouchMove);
document.addEventListener("touchend", handleTouchEnd);
}
private handleTouchStart(e: TouchEvent) {
this.isDragging = true;
this.hasDragged = false;
this.startX = e.touches[0].clientX;
this.currentX = e.touches[0].clientX;
this.dragOffset = 0;
const handleTouchMove = (e: TouchEvent) => {
if (!this.isDragging) return;
this.currentX = e.touches[0].clientX;
this.dragOffset = this.currentX - this.startX;
if (Math.abs(this.dragOffset) > 5) {
this.hasDragged = true;
}
this.requestUpdate();
};
const handleTouchEnd = () => {
if (this.isDragging) {
this.handleDragEnd();
}
document.removeEventListener("touchmove", handleTouchMove);
document.removeEventListener("touchend", handleTouchEnd);
};
document.addEventListener("touchmove", handleTouchMove);
document.addEventListener("touchend", handleTouchEnd);
}
🤖 Prompt for AI Agents
In src/client/PublicLobby.ts around lines 351 to 371, the touch handlers set
startX/currentX but never update dragOffset or hasDragged like the mouse
handlers do; update handleTouchMove to compute this.currentX, set
this.dragOffset = this.currentX - this.startX, and mark this.hasDragged = true
when movement exceeds the same threshold used by mouse dragging (or always set
true if no threshold exists), and ensure handleTouchEnd leaves this.hasDragged
set before calling handleDragEnd and that event listeners are removed as
currently done.


private handleDragEnd() {
const deltaX = this.dragOffset;
const threshold = 50;

if (Math.abs(deltaX) > threshold) {
if (deltaX > 0) {
this.previousLobby();
} else {
this.nextLobby();
}
}

this.isDragging = false;
this.dragOffset = 0;
this.requestUpdate();

// Reset hasDragged after a short delay to prevent accidental clicks
setTimeout(() => {
this.hasDragged = false;
}, 100);
}

private handleLobbyClick(e: Event, lobby: GameInfo) {
if (this.hasDragged) {
e.preventDefault();
e.stopPropagation();
return;
}

this.lobbyClicked(lobby);
}

private lobbyClicked(lobby: GameInfo) {
if (this.isButtonDebounced) {
return;
Expand All @@ -195,28 +415,17 @@ export class PublicLobby extends LitElement {
this.isButtonDebounced = false;
}, this.debounceDelay);

if (this.currLobby === null) {
this.isLobbyHighlighted = true;
this.currLobby = lobby;
this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: {
gameID: lobby.gameID,
clientID: generateID(),
} as JoinLobbyEvent,
bubbles: true,
composed: true,
}),
);
} else {
this.dispatchEvent(
new CustomEvent("leave-lobby", {
detail: { lobby: this.currLobby },
bubbles: true,
composed: true,
}),
);
this.leaveLobby();
}
this.isLobbyHighlighted = true;
this.currLobby = lobby;
this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: {
gameID: lobby.gameID,
clientID: generateID(),
} as JoinLobbyEvent,
bubbles: true,
composed: true,
}),
);
}
}
Loading
Loading