Add glowing red dot on store when skins change (#3066)

## Description:

Hash the pattern + is for sale and store it in local storage. then check
if the new cosmetics hash is different from the one we last saw. If it's
different, add a glowing red dot on the store. After user clicks it,
then update the hash and remove the dot.
<img width="1030" height="143" alt="Screenshot 2026-01-29 at 3 54 46 PM"
src="https://github.com/user-attachments/assets/e5727764-40e4-45e1-b651-65816e657067"
/>

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] 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:

evan
This commit is contained in:
Evan
2026-01-30 14:24:55 -08:00
committed by GitHub
parent 683ba4c6c1
commit 02dc5fc153
2 changed files with 57 additions and 4 deletions
+22
View File
@@ -30,6 +30,18 @@ export async function handlePurchase(
}
let __cosmetics: Promise<Cosmetics | null> | null = null;
let __cosmeticsHash: string | null = null;
function simpleHash(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return hash.toString(36);
}
export async function fetchCosmetics(): Promise<Cosmetics | null> {
if (__cosmetics !== null) {
return __cosmetics;
@@ -46,6 +58,11 @@ export async function fetchCosmetics(): Promise<Cosmetics | null> {
console.error(`Invalid cosmetics: ${result.error.message}`);
return null;
}
const patternKeys = Object.keys(result.data.patterns).sort();
const hashInput = patternKeys
.map((k) => k + (result.data.patterns[k].product ? "sale" : ""))
.join(",");
__cosmeticsHash = simpleHash(hashInput);
return result.data;
} catch (error) {
console.error("Error getting cosmetics:", error);
@@ -55,6 +72,11 @@ export async function fetchCosmetics(): Promise<Cosmetics | null> {
return __cosmetics;
}
export async function getCosmeticsHash(): Promise<string | null> {
await fetchCosmetics();
return __cosmeticsHash;
}
export function patternRelationship(
pattern: Pattern,
colorPalette: { name: string; isArchived?: boolean } | null,
+35 -4
View File
@@ -1,12 +1,15 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { getCosmeticsHash } from "../Cosmetics";
import { getGamesPlayed } from "../Utils";
const HELP_SEEN_KEY = "helpSeen";
const STORE_SEEN_HASH_KEY = "storeSeenHash";
@customElement("desktop-nav-bar")
export class DesktopNavBar extends LitElement {
@state() private _helpSeen = localStorage.getItem(HELP_SEEN_KEY) === "true";
@state() private _hasNewCosmetics = false;
createRenderRoot() {
return this;
@@ -23,6 +26,12 @@ export class DesktopNavBar extends LitElement {
this._updateActiveState(current);
});
}
// Check if cosmetics have changed
getCosmeticsHash().then((hash: string | null) => {
const seenHash = localStorage.getItem(STORE_SEEN_HASH_KEY);
this._hasNewCosmetics = hash !== null && hash !== seenHash;
});
}
disconnectedCallback() {
@@ -46,14 +55,29 @@ export class DesktopNavBar extends LitElement {
}
private showHelpDot(): boolean {
// Only show one dot at a time to prevent
// overwhelming users.
return getGamesPlayed() < 10 && !this._helpSeen;
}
private showStoreDot(): boolean {
return this._hasNewCosmetics && !this.showHelpDot();
}
private onHelpClick = () => {
localStorage.setItem(HELP_SEEN_KEY, "true");
this._helpSeen = true;
};
private onStoreClick = () => {
this._hasNewCosmetics = false;
getCosmeticsHash().then((hash: string | null) => {
if (hash !== null) {
localStorage.setItem(STORE_SEEN_HASH_KEY, hash);
}
});
};
render() {
return html`
<nav
@@ -123,11 +147,18 @@ export class DesktopNavBar extends LitElement {
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
data-page="page-item-store"
data-i18n="main.store"
@click=${this.onStoreClick}
></button>
<span
class="absolute -top-3 -right-2 bg-gradient-to-br from-red-600 to-red-700 text-white text-[9px] font-black tracking-wider px-2 py-0.5 rounded rotate-12 shadow-lg shadow-red-600/50 animate-pulse pointer-events-none"
data-i18n="main.store_new_badge"
></span>
${this.showStoreDot()
? html`
<span
class="absolute -top-1 -right-1 w-2 h-2 bg-red-500 rounded-full animate-ping"
></span>
<span
class="absolute -top-1 -right-1 w-2 h-2 bg-red-500 rounded-full"
></span>
`
: ""}
</div>
<button
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"