Skip to content

Conversation

@ryanbarlow97
Copy link
Contributor

Description:

Overhauls the Main Menu UI, visit https://menu.openfront.dev to see everything.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

w.o.n

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/AccountModal.ts (1)

29-46: Add cleanup for the event listener to prevent memory leaks.

The event listener added to document in the constructor is never removed. If the component is disconnected and reconnected multiple times, multiple listeners will accumulate, causing memory leaks and potentially stale state updates.

🧹 Proposed fix: Add disconnectedCallback to clean up

Store a reference to the bound handler and remove it when the component is disconnected:

+  private boundUserMeHandler: (event: Event) => void;
+
   constructor() {
     super();

-    document.addEventListener("userMeResponse", (event: Event) => {
+    this.boundUserMeHandler = (event: Event) => {
       const customEvent = event as CustomEvent;
       if (customEvent.detail) {
         this.userMeResponse = customEvent.detail as UserMeResponse;
         if (this.userMeResponse?.player?.publicId === undefined) {
           this.statsTree = null;
           this.recentGames = [];
         }
       } else {
         this.statsTree = null;
         this.recentGames = [];
         this.requestUpdate();
       }
-    });
+    };
+
+    document.addEventListener("userMeResponse", this.boundUserMeHandler);
   }
+
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    document.removeEventListener("userMeResponse", this.boundUserMeHandler);
+  }
🧹 Nitpick comments (2)
src/client/AccountModal.ts (2)

526-546: Remove redundant requestUpdate() call.

The requestUpdate() at line 545 is unnecessary since it's already called in both the .then() block (line 538) and the .catch() block (line 543).

♻️ Simplify by removing the redundant call
       .catch((err) => {
         console.warn("Failed to fetch user info in AccountModal.open():", err);
         this.isLoadingUser = false;
         this.requestUpdate();
       });
-    this.requestUpdate();
   }

238-290: Consider extracting the repeated email-linking UI into a helper method.

The email input form with the link button appears in both renderLinkAccountSection() (lines 270-286) and renderLoggedInAs() (lines 333-349) with only minor styling differences. Extracting this into a reusable helper would improve maintainability.

♻️ Example extraction
private renderEmailLinkForm(compact = false): TemplateResult {
  return html`
    <div class="relative group ${compact ? '' : 'w-full'}">
      <div class="flex gap-2">
        <input
          type="email"
          .value="${this.email}"
          @input="${this.handleEmailInput}"
          class="${compact ? 'w-full' : 'flex-1 min-w-0'} px-4 py-2 bg-black/40 border border-white/10 rounded-lg text-white text-sm placeholder-white/30 focus:outline-none focus:border-blue-500 transition-all font-medium"
          placeholder="${translateText(
            "account_modal.link_via_email_placeholder",
          )}"
        />
        <button
          @click="${this.handleSubmit}"
          class="px-${compact ? '4' : '5'} py-2 text-sm font-bold text-white uppercase bg-blue-600 hover:bg-blue-500 rounded-lg transition-all ${compact ? '' : 'border border-blue-500/50 hover:shadow-[0_0_15px_rgba(37,99,235,0.3)] shrink-0'}"
        >
          ${translateText("account_modal.link_button")}
        </button>
      </div>
    </div>
  `;
}

Then use ${this.renderEmailLinkForm(false)} and ${this.renderEmailLinkForm(true)} in the respective locations.

Also applies to: 292-352

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e683344 and 88b8116.

📒 Files selected for processing (2)
  • src/client/AccountModal.ts
  • src/client/Navigation.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/Navigation.ts
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/AccountModal.ts
🧬 Code graph analysis (1)
src/client/AccountModal.ts (2)
src/client/Utils.ts (2)
  • copyToClipboard (19-36)
  • translateText (147-202)
src/client/LangSelector.ts (1)
  • translateText (248-268)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (5)
src/client/AccountModal.ts (5)

48-57: Clean implementation of copy-to-clipboard with user feedback.

The method correctly uses the copyToClipboard utility and manages the showCopied state for UI feedback. The early return prevents errors when no ID is available.


71-102: Good handling of loading states and rendering modes.

The conditional rendering for loading state provides clear feedback, and the inline vs. modal rendering flexibility is well implemented.


376-520: Login flow implementation looks solid.

The Discord and email login options are clearly presented, input handling is correct, and appropriate validation is in place. The use of alert() for feedback is simple but effective for this use case.


354-363: Data loading and navigation methods are well implemented.

The viewGame, handleLogout, and loadPlayerProfile methods all handle async operations properly with appropriate error handling and state management. The page reload after logout ensures clean state reset.

Also applies to: 554-577


548-552: Clean event dispatching in the close handler.

The onClose hook properly dispatches a custom event with correct flags for DOM event propagation.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/components/baseComponents/setting/SettingKeybind.ts (1)

126-155: Cancel listening state and ensure event details match the keydown pattern.

When the user clicks Reset or Unbind while the button is still listening, the UI stays in the "..." state. Additionally, these methods omit prevValue and set key to incorrect values:

  • resetToDefault sends key: this.defaultKey (a code string), but the keydown handler sends key: e.key (the actual key)
  • Both methods lack prevValue, making the event detail inconsistent with keydown events

Update both methods to:

  1. Capture prevValue before changing the value
  2. Set this.listening = false to clear the listening state
  3. Use key: "" to indicate a programmatic action (not a user keypress)
Suggested fix
   private resetToDefault() {
+    const prevValue = this.value;
     this.value = this.defaultKey;
+    this.listening = false;
     this.dispatchEvent(
       new CustomEvent("change", {
         detail: {
           action: this.action,
           value: this.defaultKey,
-          key: this.defaultKey,
+          key: "",
+          prevValue,
         },
         bubbles: true,
         composed: true,
       }),
     );
   }
 
   private unbindKey() {
+    const prevValue = this.value;
     this.value = "Null";
+    this.listening = false;
     this.dispatchEvent(
       new CustomEvent("change", {
         detail: {
           action: this.action,
           value: "Null",
           key: "",
+          prevValue,
         },
         bubbles: true,
         composed: true,
       }),
     );
     this.requestUpdate();
   }
🤖 Fix all issues with AI agents
In @src/client/components/baseComponents/setting/SettingKeybind.ts:
- Around line 21-55: Replace the interactive <div role="button"> in the render()
of SettingKeybind with a real <button type="button"> element (the element that
currently has the big class string and event handlers), remove the role and
manual tabindex attributes, keep the same class list, aria-label,
@click=${this.startListening}, @blur=${this.handleBlur} and the visual state
logic, and retain @keydown=${this.handleKeydown} only if it’s required for
capturing keys while listening; using a real button ensures Enter/Space
activation and prevents accidental form submission (type="button").
🧹 Nitpick comments (2)
src/client/components/baseComponents/setting/SettingKeybind.ts (2)

86-119: Avoid multiple “change” events from key-repeat, and consider a clearer payload contract.

Holding a key can fire repeated keydown events and spam change. Also, detail.key here is e.key (label-ish), while other paths set it to code-ish values; that’s easy for parent code to mis-handle.

Proposed fix (ignore repeats + normalize event detail)
   private handleKeydown(e: KeyboardEvent) {
     if (!this.listening) return;
 
+    if (e.repeat) return;
+
     // Allow Tab and Escape to work normally (don't trap focus)
     if (e.key === "Tab" || e.key === "Escape") {
       if (e.key === "Escape") {
         // Cancel listening on Escape
         this.listening = false;
         this.requestUpdate();
       }
       return;
     }
 
     // Prevent default only for keys we're actually capturing
     e.preventDefault();
 
     const code = e.code;
     const prevValue = this.value;
 
     // Temporarily set the value to the new code for validation in parent
     this.value = code;
 
     const event = new CustomEvent("change", {
-      detail: { action: this.action, value: code, key: e.key, prevValue },
+      detail: { action: this.action, value: code, code, key: e.key, prevValue },
       bubbles: true,
       composed: true,
     });
     this.dispatchEvent(event);

21-27: Please confirm "Null" is the intended “unbound” sentinel everywhere.

Right now unbind sets this.value = "Null", and displayKey() will render "Null" (via formatKeyForDisplay fallback). If the intended UX is “Press a key” (or blank) when unbound, consider using "" for unbound and keep "Null" only as a serialization detail (if needed).

Also applies to: 141-155

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88b8116 and 4260442.

📒 Files selected for processing (2)
  • src/client/KeybindsModal.ts
  • src/client/components/baseComponents/setting/SettingKeybind.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/KeybindsModal.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
🧬 Code graph analysis (1)
src/client/components/baseComponents/setting/SettingKeybind.ts (2)
src/client/LangSelector.ts (1)
  • translateText (248-268)
src/client/Utils.ts (2)
  • translateText (147-202)
  • formatKeyForDisplay (79-98)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/components/baseComponents/setting/SettingKeybind.ts (1)

130-158: Fix event detail consistency: all change events must include the same properties.

The event detail structures are inconsistent across methods and violate the parent component's type contract:

  • handleKeydown: sends { action, value, key, prevValue }
  • resetToDefault: sends { action, value } ✗ Missing key (required by type)
  • unbindKey: sends { action, value, key } ✓ but missing prevValue (optional)

KeybindsModal.ts expects key: string in its type definition (line 100), and the handler destructures it at line 104. Although line 125 has a fallback (key || value), this doesn't fix the type violation.

Make all three methods include the same detail structure: { action, value, key, prevValue }. For resetToDefault, add key: this.defaultKey (or empty string) and prevValue: this.value to match the signature.

Regarding the "Null" string: this is intentional. It's used throughout the codebase (KeybindsModal, HelpModal, InputHandler) as a sentinel value for unbound keys, not as a string representation of null.

🧹 Nitpick comments (2)
src/client/components/baseComponents/setting/SettingKeybind.ts (2)

22-24: Consider simplifying the currentValue logic for clarity.

The expression this.value === "" ? "" : this.value || this.defaultKey mixes strict equality with falsy checks. This works but could be clearer.

♻️ Optional refactor for explicit intent
-    const currentValue = this.value === "" ? "" : this.value || this.defaultKey;
+    const currentValue = this.value === "" ? "" : (this.value ?? this.defaultKey);

Using nullish coalescing (??) makes it explicit that you're checking for null/undefined rather than all falsy values.


25-27: Extract the rainbow gradient string to improve readability.

The 220+ character gradient class string makes the code hard to read and maintain.

♻️ Refactor to extract rainbow class constant

Define the class at the top of the file or in a constant:

+const RAINBOW_GRADIENT_CLASS = 
+  "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] " +
+  "bg-[length:1400%_1400%] animate-rainbow-bg text-white " +
+  "hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]";
+

Then in render:

-    const rainbowClass = this.easter
-      ? "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] bg-[length:1400%_1400%] animate-rainbow-bg text-white hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]"
-      : "";
+    const rainbowClass = this.easter ? RAINBOW_GRADIENT_CLASS : "";
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4260442 and d94d618.

📒 Files selected for processing (1)
  • src/client/components/baseComponents/setting/SettingKeybind.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
🧬 Code graph analysis (1)
src/client/components/baseComponents/setting/SettingKeybind.ts (2)
src/client/LangSelector.ts (1)
  • translateText (248-268)
src/client/Utils.ts (2)
  • translateText (147-202)
  • formatKeyForDisplay (79-98)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (5)
src/client/components/baseComponents/setting/SettingKeybind.ts (5)

3-3: Good use of the utility function.

Using formatKeyForDisplay ensures consistent key formatting across the application.


29-77: Excellent accessibility improvements.

The refactored render includes:

  • Proper role="button" for the keybind control
  • aria-label with translated text
  • tabindex="0" for keyboard navigation
  • Visual feedback for listening state

These changes make the component much more accessible.


80-83: Clean implementation of key display logic.

Using the utility function and translation system keeps the display logic simple and maintainable.


125-128: Good UX addition.

Stopping the listening state when focus leaves prevents confusion if the user clicks away.


90-123: Tab and Escape handling for accessibility is good. Allowing Tab to traverse focus normally and Escape to cancel listening prevents focus traps, which matters for keyboard navigation.

Parent component (KeybindsModal.ts) correctly handles the updated event detail structure with prevValue and key fields—both are properly destructured and used (key for display formatting, prevValue for value restoration on rejection).

@VariableVince
Copy link
Contributor

VariableVince commented Jan 9, 2026

Resolves #2652 #2291 (apparently, see #2840 (comment))

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @src/client/TerritoryPatternsModal.ts:
- Around line 305-318: In the open method, the current guard uses typeof options
=== "object" which also matches arrays; tighten it by ensuring options is a
non-null plain object (e.g., options !== null && !Array.isArray(options) &&
typeof options === "object") or check for the expected property (e.g.,
'affiliateCode' in options) before reading options. Update the conditional that
assigns this.affiliateCode and this.showOnlyOwned so it only runs when options
is a non-array object or when the expected keys exist, leaving the other
branches unchanged.
- Around line 52-56: The event listener for "pattern-selected" is added inline
in connectedCallback and never removed, causing a leak; refactor by creating a
class property handler (e.g., this._onPatternSelected) that wraps the current
arrow function (calling this.updateFromSettings() and this.refresh()), use
window.addEventListener("pattern-selected", this._onPatternSelected) in
connectedCallback, and implement disconnectedCallback to call
window.removeEventListener("pattern-selected", this._onPatternSelected) to
ensure proper cleanup.
🧹 Nitpick comments (5)
src/client/TerritoryPatternsModal.ts (3)

352-375: Consider extracting the name formatting logic.

The manual string formatting (splitting on underscore, capitalizing) at lines 356-364 could be fragile if pattern name formats change. Extract this to a utility function or use translation keys for pattern display names if available.

Example utility function
// In Utils.ts or similar
export function formatPatternDisplayName(pattern: PlayerPattern | null): string {
  if (!pattern || !pattern.name) {
    return translateText("territory_patterns.pattern.default");
  }
  
  let name = pattern.name
    .split("_")
    .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
    .join(" ");
    
  if (pattern.colorPalette?.name) {
    name += ` (${pattern.colorPalette.name})`;
  }
  
  return name;
}

// Then in TerritoryPatternsModal.ts
private showSkinSelectedPopup() {
  const skinName = formatPatternDisplayName(this.selectedPattern);
  window.dispatchEvent(
    new CustomEvent("show-message", {
      detail: {
        message: `${skinName} ${translateText("territory_patterns.selected")}`,
        duration: 2000,
      },
    }),
  );
}

399-429: Remove redundant operations in refresh.

Two minor inefficiencies:

  1. Line 400 calls requestUpdate() before any state changes or external rendering, which seems unnecessary.
  2. Lines 408 and 418 both check if previewButton is in the DOM, duplicating the check.
♻️ Streamlined version
   public async refresh() {
-    this.requestUpdate();
-
     const preview = this.selectedColor
       ? this.renderColorPreview(this.selectedColor, 48, 48)
       : renderPatternPreview(this.selectedPattern ?? null, 48, 48);

-    if (
-      this.previewButton === null ||
-      !document.body.contains(this.previewButton)
-    ) {
+    if (this.previewButton === null) {
       this.previewButton = document.getElementById(
         "territory-patterns-input-preview-button",
       );
     }
 
-    if (this.previewButton === null) return;
-
     // Check if the element is still in the DOM to avoid lit-html errors
-    if (!document.body.contains(this.previewButton)) {
+    if (this.previewButton === null || !document.body.contains(this.previewButton)) {
       console.warn(
         "TerritoryPatternsModal: previewButton is disconnected from DOM, skipping render",
       );
       return;
     }

     // Clear and re-render using Lit
     render(preview, this.previewButton);
     this.previewButton.style.padding = "4px";
     this.requestUpdate();
   }

142-194: Consider extracting helper methods for pattern grid logic.

The pattern grid rendering at lines 148-194 mixes several concerns: relationship determination, ownership filtering, and selection state calculation. Extract these into helper methods to improve readability.

Example refactor
private isPatternSelected(
  pattern: Pattern | null,
  colorPalette: ColorPalette | null
): boolean {
  const isDefaultPattern = pattern === null;
  return (
    (isDefaultPattern && this.selectedPattern === null) ||
    (!isDefaultPattern &&
      this.selectedPattern &&
      this.selectedPattern.name === pattern?.name &&
      (this.selectedPattern.colorPalette?.name ?? null) ===
        (colorPalette?.name ?? null))
  );
}

private shouldShowPatternInGrid(
  relationship: "owned" | "purchasable" | "blocked"
): boolean {
  if (relationship === "blocked") return false;
  
  if (this.showOnlyOwned) {
    return relationship === "owned";
  } else {
    // Store mode: hide owned items
    return relationship !== "owned";
  }
}

private renderPatternGrid(): TemplateResult {
  const buttons: TemplateResult[] = [];
  const patterns: (Pattern | null)[] = [
    null,
    ...Object.values(this.cosmetics?.patterns ?? {}),
  ];
  
  for (const pattern of patterns) {
    const colorPalettes = pattern
      ? [...(pattern.colorPalettes ?? []), null]
      : [null];
      
    for (const colorPalette of colorPalettes) {
      let rel = "owned";
      if (pattern) {
        rel = patternRelationship(
          pattern,
          colorPalette,
          this.userMeResponse,
          this.affiliateCode,
        );
      }
      
      if (!this.shouldShowPatternInGrid(rel)) continue;
      
      const isSelected = this.isPatternSelected(pattern, colorPalette);
      
      buttons.push(html`
        <pattern-button
          .pattern=${pattern}
          .colorPalette=${this.cosmetics?.colorPalettes?.[
            colorPalette?.name ?? ""
          ] ?? null}
          .requiresPurchase=${rel === "purchasable"}
          .selected=${isSelected}
          .onSelect=${(p: PlayerPattern | null) => this.selectPattern(p)}
          .onPurchase=${(p: Pattern, colorPalette: ColorPalette | null) =>
            handlePurchase(p, colorPalette)}
        ></pattern-button>
      `);
    }
  }

  return html`
    <div class="flex flex-col gap-4">
      <div class="flex justify-center">
        ${hasLinkedAccount(this.userMeResponse)
          ? this.renderMySkinsButton()
          : html``}
      </div>
      ${!this.showOnlyOwned && buttons.length === 0
        ? html`<div
            class="text-white/40 text-sm font-bold uppercase tracking-wider text-center py-8"
          >
            ${translateText("territory_patterns.all_owned")}
          </div>`
        : html`
            <div
              class="flex flex-wrap gap-4 p-2 justify-center items-stretch content-start"
            >
              ${buttons}
            </div>
          `}
    </div>
  `;
}
src/client/Main.ts (2)

236-255: Timing hack: 100ms delay is fragile.

The setTimeout with 100ms delay after customElements.whenDefined() is a timing-based workaround that could fail if the component renders slowly. The selector also tightly couples to the component's internal structure.

Consider having the flag-input component dispatch a ready event or expose a public method to register this click handler, which would be more reliable than timing-based rendering assumptions.

Alternative approach

In the FlagInput component, dispatch a ready event:

// In flag-input component after render
this.dispatchEvent(new CustomEvent('ready', { bubbles: true }));

Then in Main.ts:

-    // Wait for the flag-input component to be fully ready
-    customElements.whenDefined("flag-input").then(() => {
-      // Use a small delay to ensure the component has rendered
-      setTimeout(() => {
-        const flagButton = document.querySelector(
-          "#flag-input-component #flag-input_",
-        );
-        if (!flagButton) {
-          console.warn("Flag button not found inside component");
-          return;
-        }
-        flagButton.addEventListener("click", (e) => {
-          e.preventDefault();
-          e.stopPropagation();
-          if (flagInputModal && flagInputModal instanceof FlagInputModal) {
-            flagInputModal.open();
-          }
-        });
-      }, 100);
+    const flagInputComponent = document.querySelector("flag-input");
+    flagInputComponent?.addEventListener("ready", () => {
+      const flagButton = document.querySelector(
+        "#flag-input-component #flag-input_",
+      );
+      if (!flagButton) {
+        console.warn("Flag button not found inside component");
+        return;
+      }
+      flagButton.addEventListener("click", (e) => {
+        e.preventDefault();
+        e.stopPropagation();
+        if (flagInputModal && flagInputModal instanceof FlagInputModal) {
+          flagInputModal.open();
+        }
+      });
     });

273-309: Refactor: Move responsive logic to CSS.

The responsive button repositioning logic manipulates long Tailwind class strings directly in JavaScript, which is fragile and hard to maintain. The escaped selector .lg\\:col-span-9.flex is particularly brittle.

Consider using CSS media queries or data attributes to control styling instead of JavaScript class manipulation. This would be more maintainable and performant.

CSS-based alternative

Instead of moving the element and changing classes in JS, use CSS to control visibility and positioning:

-    // Move button to desktop wrapper on large screens
-    const desktopWrapper = document.getElementById(
-      "territory-patterns-preview-desktop-wrapper",
-    );
-    if (desktopWrapper && patternButton) {
-      const moveButtonBasedOnScreenSize = () => {
-        if (window.innerWidth >= 1024) {
-          // Desktop: move to wrapper
-          if (
-            patternButton.parentElement?.id !==
-            "territory-patterns-preview-desktop-wrapper"
-          ) {
-            patternButton.className =
-              "w-full h-[60px] border border-white/20 bg-white/5 hover:bg-white/10 active:bg-white/20 rounded-lg cursor-pointer focus:outline-none transition-all duration-200 hover:scale-105 overflow-hidden";
-            patternButton.style.backgroundSize = "auto 100%";
-            patternButton.style.backgroundRepeat = "repeat-x";
-            desktopWrapper.appendChild(patternButton);
-          }
-        } else {
-          // Mobile: move back to bar
-          const mobileParent = document.querySelector(".lg\\:col-span-9.flex");
-          if (
-            mobileParent &&
-            patternButton.parentElement?.id ===
-              "territory-patterns-preview-desktop-wrapper"
-          ) {
-            patternButton.className =
-              "aspect-square h-[40px] sm:h-[50px] lg:hidden border border-white/20 bg-white/5 hover:bg-white/10 active:bg-white/20 rounded-lg cursor-pointer focus:outline-none transition-all duration-200 hover:scale-105 overflow-hidden shrink-0";
-            patternButton.style.backgroundSize = "";
-            patternButton.style.backgroundRepeat = "";
-            mobileParent.appendChild(patternButton);
-          }
-        }
-      };
-      moveButtonBasedOnScreenSize();
-      window.addEventListener("resize", moveButtonBasedOnScreenSize);
-    }
+    // Apply base responsive classes in HTML instead
+    if (patternButton) {
+      patternButton.className = 
+        "territory-pattern-button aspect-square h-[40px] sm:h-[50px] lg:w-full lg:h-[60px] border border-white/20 bg-white/5 hover:bg-white/10 active:bg-white/20 rounded-lg cursor-pointer focus:outline-none transition-all duration-200 hover:scale-105 overflow-hidden shrink-0 lg:shrink";
+    }

Then in CSS:

@media (min-width: 1024px) {
  .territory-pattern-button {
    background-size: auto 100%;
    background-repeat: repeat-x;
  }
}

This eliminates the resize listener, DOM manipulation, and hard-coded class strings.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d94d618 and 73b6d00.

📒 Files selected for processing (3)
  • src/client/Main.ts
  • src/client/Navigation.ts
  • src/client/TerritoryPatternsModal.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/Navigation.ts
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/Main.ts
  • src/client/TerritoryPatternsModal.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/Main.ts
  • src/client/TerritoryPatternsModal.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/Main.ts
  • src/client/TerritoryPatternsModal.ts
📚 Learning: 2025-06-20T20:11:00.965Z
Learnt from: devalnor
Repo: openfrontio/OpenFrontIO PR: 1195
File: src/client/graphics/layers/AlertFrame.ts:18-18
Timestamp: 2025-06-20T20:11:00.965Z
Learning: In the OpenFrontIO codebase, UserSettings instances are created directly with `new UserSettings()` in each component that needs them. This pattern is used consistently across at least 12+ files including OptionsMenu, EventsDisplay, DarkModeButton, Main, UserSettingModal, UsernameInput, NameLayer, AlertFrame, UILayer, InputHandler, ClientGameRunner, and GameView. This is the established architectural pattern and should be followed for consistency.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-06-22T05:48:19.241Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 786
File: src/client/TerritoryPatternsModal.ts:337-338
Timestamp: 2025-06-22T05:48:19.241Z
Learning: In src/client/TerritoryPatternsModal.ts, the bit shifting operators (<<) used in coordinate calculations with decoder.getScale() are intentional and should not be changed to multiplication. The user scottanderson confirmed this is functioning as intended.

Applied to files:

  • src/client/TerritoryPatternsModal.ts
🧬 Code graph analysis (2)
src/client/Main.ts (4)
src/client/TerritoryPatternsModal.ts (1)
  • onUserMe (66-77)
src/core/ApiSchemas.ts (1)
  • UserMeResponse (78-78)
src/client/Layout.ts (1)
  • initLayout (1-81)
src/client/Navigation.ts (1)
  • initNavigation (1-77)
src/client/TerritoryPatternsModal.ts (4)
src/client/Utils.ts (1)
  • translateText (147-202)
src/client/Api.ts (1)
  • hasLinkedAccount (137-145)
src/core/CosmeticSchemas.ts (1)
  • Pattern (7-7)
src/client/components/PatternButton.ts (2)
  • renderPatternPreview (161-174)
  • render (70-158)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (13)
src/client/Main.ts (13)

17-18: LGTM: Clean integration of new modules.

The imports for FlagInputModal, KeybindsModal, Layout, and Navigation are properly structured. The showPage extension to the Window interface is correctly typed as optional.

Also applies to: 26-32, 83-83


132-143: LGTM: Robust version display handling.

The refactor to support multiple version display elements using querySelectorAll is well-implemented. The defensive check and version normalization with "v" prefix are appropriate.


196-210: Good UX improvement: User feedback for validation errors.

The integration with window.showPage and the addition of validation error feedback improves the user experience. The error message properly uses this.usernameInput?.validationError.


220-227: LGTM: Proper defensive handling.

Making the help button optional with appropriate null checks follows good defensive programming practices.


321-332: Minor timing dependency in refresh logic.

The 50ms setTimeout in the showPage listener is a timing-based workaround, likely needed for DOM updates to settle. While functional, consider whether the refresh could be triggered by a more deterministic event if available.


366-398: LGTM: Clean authentication-aware UI state.

The matchmaking button logic properly handles logged-in vs logged-out states, validates username input, and provides clear user feedback. The separation of concerns is well-executed.


401-409: LGTM: Improved authentication state detection.

The refined check for "logged in" now correctly distinguishes between users with just a publicId vs. those with actual authentication (Discord or email). This is more accurate for features requiring real accounts.


460-472: LGTM: Consistent validation and navigation pattern.

Both host and join lobby buttons follow the same pattern: validate username, provide feedback, and use window.showPage for navigation. The logic is consistent and well-structured.

Also applies to: 488-498


511-534: LGTM: Correct implementation of navigation rollback handling.

The preventHashUpdate flag correctly prevents the hashchange listener from running after popstate during navigation rollback scenarios. This matches the pattern documented in the learnings and prevents subtle UI/game state inconsistencies.

Based on learnings, this addresses the browser back/forward navigation ordering issue correctly.


562-563: LGTM: Consistent navigation through showPage.

The URL handling correctly uses window.showPage for navigation while maintaining the subsequent modal operations. The optional chaining provides appropriate safety.

Also applies to: 641-642, 650-650


664-664: LGTM: Clean game state class management.

The in-game class is properly added when the game starts and removed when stopping or leaving. The lifecycle management is symmetric and clear.

Also applies to: 756-756, 775-775


859-869: LGTM: Robust initialization sequence.

The bootstrap function properly sequences initialization: Layout → Client → Navigation. The readyState check ensures the code runs correctly whether the script loads early or late.


334-345: Remove redundant classList.remove("hidden") call.

The showPage function in Navigation.ts already removes the "hidden" class from the target element. The manual skinStoreModal.classList.remove("hidden") is unnecessary duplication that contradicts the pattern used elsewhere in the codebase (see other showPage calls at lines 562 and 641).

Delete line 340 to rely on showPage for visibility management:

Diff
    patternButton.addEventListener("click", () => {
      window.showPage?.("page-item-store");
      const skinStoreModal = document.getElementById(
        "page-item-store",
      ) as HTMLElement & { open?: (opts: any) => void };
      if (skinStoreModal) {
-       skinStoreModal.classList.remove("hidden");
        if (typeof skinStoreModal.open === "function") {
          skinStoreModal.open({ showOnlyOwned: true });
        }
      }
    });
⛔ Skipped due to learnings
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

@ryanbarlow97
Copy link
Contributor Author

Resolves #2652 #2291 (apparently, see #2840 (comment))

It may fix some issues, I fixed a few but there are still localisation issues, e.g. french keyboard W -> Z and Z -> Q (an issue on main)

@VariableVince
Copy link
Contributor

Resolves #2652 #2291 (apparently, see #2840 (comment))

It may fix some issues, I fixed a few but there are still localisation issues, e.g. french keyboard W -> Z and Z -> Q (an issue on main)

Ok, that may be the thing in this other Issue #1071 (not 100% sure) in which this comment from 2 days ago contains outcome of someone's investigation: #1071 (comment)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/client/TerritoryPatternsModal.ts (4)

312-333: Consider simplifying the options parsing.

The type guards on lines 320-322 include redundant checks. Since TypeScript narrows the type at line 316, the remaining logic can be cleaner.

♻️ Simpler options parsing
  public async open(
    options?: string | { affiliateCode?: string; showOnlyOwned?: boolean },
  ) {
    this.isActive = true;
-   if (typeof options === "string") {
-     this.affiliateCode = options;
-     this.showOnlyOwned = false;
-   } else if (
-     options !== null &&
-     typeof options === "object" &&
-     !Array.isArray(options)
-   ) {
-     this.affiliateCode = options.affiliateCode ?? null;
-     this.showOnlyOwned = options.showOnlyOwned ?? false;
-   } else {
-     this.affiliateCode = null;
-     this.showOnlyOwned = false;
-   }
+   if (typeof options === "string") {
+     this.affiliateCode = options;
+     this.showOnlyOwned = false;
+   } else if (options) {
+     this.affiliateCode = options.affiliateCode ?? null;
+     this.showOnlyOwned = options.showOnlyOwned ?? false;
+   } else {
+     this.affiliateCode = null;
+     this.showOnlyOwned = false;
+   }

    await this.refresh();
    super.open();
  }

363-386: Nitpick: Display name formatting could be a utility.

The pattern name formatting (lines 367-376) transforms snake_case to Title Case. If this formatting is needed elsewhere, consider extracting it to Utils.ts.

Example extraction:

// In Utils.ts
export function formatPatternDisplayName(patternName: string): string {
  return patternName
    .split("_")
    .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
    .join(" ");
}

417-434: Simplify redundant DOM checks.

Line 426 checks for null, but lines 429-434 perform the same check with document.body.contains. The first null check is redundant.

♻️ Remove redundant null check
    if (
      this.previewButton === null ||
      !document.body.contains(this.previewButton)
    ) {
      this.previewButton = document.getElementById(
        "territory-patterns-input-preview-button",
      );
    }

-   if (this.previewButton === null) return;
-
    // Check if the element is still in the DOM to avoid lit-html errors
    if (!document.body.contains(this.previewButton)) {
      console.warn(
        "TerritoryPatternsModal: previewButton is disconnected from DOM, skipping render",
      );
      return;
    }

Note: document.body.contains(null) returns false, so the second check handles both cases.


179-186: Consider extracting selection matching logic.

The isSelected computation (lines 180-186) has nested conditions that could be clearer as a helper method. This would improve readability and make the logic testable.

Example:

private isPatternSelected(
  pattern: Pattern | null,
  colorPalette: ColorPalette | null,
): boolean {
  if (pattern === null) {
    return this.selectedPattern === null;
  }
  
  if (!this.selectedPattern) {
    return false;
  }
  
  return (
    this.selectedPattern.name === pattern.name &&
    (this.selectedPattern.colorPalette?.name ?? null) ===
      (colorPalette?.name ?? null)
  );
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73b6d00 and 71dd0ef.

📒 Files selected for processing (1)
  • src/client/TerritoryPatternsModal.ts
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/TerritoryPatternsModal.ts
📚 Learning: 2025-06-22T05:48:19.241Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 786
File: src/client/TerritoryPatternsModal.ts:337-338
Timestamp: 2025-06-22T05:48:19.241Z
Learning: In src/client/TerritoryPatternsModal.ts, the bit shifting operators (<<) used in coordinate calculations with decoder.getScale() are intentional and should not be changed to multiplication. The user scottanderson confirmed this is functioning as intended.

Applied to files:

  • src/client/TerritoryPatternsModal.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/client/TerritoryPatternsModal.ts
🧬 Code graph analysis (1)
src/client/TerritoryPatternsModal.ts (4)
src/client/Utils.ts (1)
  • translateText (147-202)
src/client/Api.ts (1)
  • hasLinkedAccount (137-145)
src/core/CosmeticSchemas.ts (1)
  • Pattern (7-7)
src/client/Cosmetics.ts (1)
  • patternRelationship (51-94)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (2)
src/client/TerritoryPatternsModal.ts (2)

40-63: LGTM! Clean event listener lifecycle.

The arrow function property pattern ensures proper cleanup in disconnectedCallback, preventing memory leaks.


277-310: LGTM! Clean inline rendering support.

The content template is correctly reused for both inline and modal modes, with appropriate flags to hide default modal chrome.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/client/components/PatternButton.ts:
- Around line 74-79: The root div in PatternButton.ts currently only uses visual
cues via this.selected; add an accessible selection state by giving the element
an appropriate role (e.g., role="option") and an aria-selected attribute bound
to the component state (use aria-selected={this.selected} or stringified
'true'/'false') so screen readers announce selection; update the root div that
contains this.selected styling to include role and aria-selected attributes and
ensure the binding reflects changes to this.selected.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 71dd0ef and 151b90d.

📒 Files selected for processing (2)
  • resources/lang/en.json
  • src/client/components/PatternButton.ts
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-16T10:52:08.292Z
Learnt from: TheGiraffe3
Repo: openfrontio/OpenFrontIO PR: 884
File: resources/lang/en.json:456-461
Timestamp: 2025-08-16T10:52:08.292Z
Learning: In OpenFrontIO, translation files in resources/lang/*.json (except en.json) should not be updated in regular PRs. Only dedicated translation PRs titled "mls" and made by Aotumori should update non-English locale files. Regular PRs should only update en.json when adding or modifying translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-07-12T08:41:35.101Z
Learnt from: Aotumuri
Repo: openfrontio/OpenFrontIO PR: 1357
File: resources/lang/de.json:523-540
Timestamp: 2025-07-12T08:41:35.101Z
Learning: In OpenFrontIO project localization files, always check the en.json source file before flagging potential spelling errors in other language files, as some keys may intentionally use non-standard spellings that need to be consistent across all translations.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-27T08:12:19.610Z
Learnt from: mokizzz
Repo: openfrontio/OpenFrontIO PR: 1940
File: resources/lang/en.json:763-766
Timestamp: 2025-08-27T08:12:19.610Z
Learning: In OpenFrontIO, some country entries in src/client/data/countries.json may have similar names but different codes (e.g., "Empire of Japan" vs "Empire of Japan1"). Each unique code requires its own translation key in resources/lang/en.json after normalization. Always verify against countries.json before suggesting removal of translation keys.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
Repo: openfrontio/OpenFrontIO PR: 2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • resources/lang/en.json
🧬 Code graph analysis (1)
src/client/components/PatternButton.ts (1)
src/client/LangSelector.ts (1)
  • translateText (248-268)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (12)
src/client/components/PatternButton.ts (5)

18-19: LGTM! Clean property declaration.

The selected property follows standard Lit patterns with proper decorator and type annotation.


123-138: LGTM! Clean preview rendering.

The aspect-square container with hover effects looks good. Type safety with satisfies PlayerPattern is a nice touch.


140-155: LGTM! Purchase section with layout stability.

The dedicated purchase area with a spacer fallback prevents layout shifts. The fixed height h-[34px] ensures consistent spacing across all pattern buttons.


176-199: LGTM! Responsive blank preview design.

The responsive approach shows a visual grid on mobile and a helpful text hint on desktop. Good UX differentiation based on screen size.


86-121: No action needed. The code gracefully handles missing translation keys via fallback formatting. The translateCosmetic function attempts to fetch ${prefix}.${name} from translations, but if the key is not found, it formats the name by splitting on underscores, capitalizing each word, and joining with spaces (e.g., red_pattern becomes Red Pattern). This prevents runtime warnings and is intentional design. The required key territory_patterns.pattern.default is confirmed to exist in en.json.

Likely an incorrect or invalid review comment.

resources/lang/en.json (7)

10-23: LGTM!

New common keys (back, none, copied, click_to_copy) follow the existing snake_case naming pattern and provide clear, reusable UI strings.


32-51: LGTM!

Key renames (create, join, solo) are concise and appropriate for button labels. New navigation keys align with the sidebar menu overhaul.


180-192: LGTM!

Account modal keys are well-organized and support the new linking flow. Naming follows project conventions.


342-345: LGTM!

Clan tag validation keys (tag, tag_too_short, tag_invalid_chars) are clear and provide actionable error messages for users.


928-933: LGTM!

New matchmaking_button section provides clear UI strings. Having both login_required (short prompt) and must_login (detailed message) is appropriate for different UI contexts.


918-927: LGTM!

Consistent use of "Solo" (matching single_modal.title) and addition of no_stats for empty state handling. Based on learnings, only en.json should be updated in regular PRs, which this PR correctly follows.


382-383: Remove unused translation key teams_Humans Vs Nations.

The code in HostLobbyModal.ts branches to public_lobby.teams_hvn for HumansVsNations mode, not host_modal.teams_Humans Vs Nations. This key is never used and should be deleted.

🔧 Remove unused key
    "teams_Quads": "Quads (teams of 4)",
-    "teams_Humans Vs Nations": "Humans vs Nations"
⛔ Skipped due to learnings
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.
Learnt from: FloPinguin
Repo: openfrontio/OpenFrontIO PR: 2689
File: src/client/PublicLobby.ts:245-245
Timestamp: 2025-12-26T22:21:21.904Z
Learning: In public lobbies with HumansVsNations mode in src/client/PublicLobby.ts, maxPlayers represents only human player slots (already halved in DefaultConfig.ts). The nation NPCs are added automatically server-side and don't count toward maxPlayers. Therefore, getTeamSize correctly returns maxPlayers directly for HumansVsNations to display the proper team size (e.g., maxPlayers=5 yields "5 Humans vs 5 Nations").
Learnt from: mokizzz
Repo: openfrontio/OpenFrontIO PR: 1940
File: resources/lang/en.json:763-766
Timestamp: 2025-08-27T08:12:19.610Z
Learning: In OpenFrontIO, some country entries in src/client/data/countries.json may have similar names but different codes (e.g., "Empire of Japan" vs "Empire of Japan1"). Each unique code requires its own translation key in resources/lang/en.json after normalization. Always verify against countries.json before suggesting removal of translation keys.
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

UI/UX UI/UX changes including assets, menus, QoL, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keybinde Overlapping and rebinding issue Keybinds - Unbind key button doesn't work

3 participants