- Update the newbox to fetch news from the api

- Fix the comment recoms and the minor issues
This commit is contained in:
AlexBesios
2026-04-07 14:29:06 +03:00
parent b58319b238
commit dbaa61318e
6 changed files with 96 additions and 32 deletions
+7
View File
@@ -1043,5 +1043,12 @@
"description": "(ALPHA)",
"login_required": "Login to play ranked!",
"must_login": "You must be logged in to play ranked matchmaking."
},
"news_box": {
"tournament": "TOURNAMENT",
"tutorial": "TUTORIAL",
"news": "NEWS",
"dismiss": "Dismiss",
"go_to_item": "Go to item {num}"
}
}
+25
View File
@@ -1,7 +1,10 @@
import newsItemsFallback from "resources/news.json";
import { z } from "zod";
import type { NewsItem } from "../core/ApiSchemas";
import {
ClanLeaderboardResponse,
ClanLeaderboardResponseSchema,
NewsItemSchema,
PlayerProfile,
PlayerProfileSchema,
RankedLeaderboardResponse,
@@ -263,3 +266,25 @@ export async function fetchPlayerLeaderboard(
return false;
}
}
export async function getNews(): Promise<NewsItem[]> {
try {
const res = await fetch(`${getApiBase()}/news`, {
headers: { Accept: "application/json" },
});
if (res.status !== 200) {
console.warn("getNews: unexpected status", res.status);
return newsItemsFallback as NewsItem[];
}
const json = await res.json();
const parsed = z.array(NewsItemSchema).safeParse(json);
if (!parsed.success) {
console.warn("getNews: Zod validation failed", parsed.error);
return newsItemsFallback as NewsItem[];
}
return parsed.data;
} catch (err) {
console.warn("getNews: request failed, using fallback", err);
return newsItemsFallback as NewsItem[];
}
}
+42 -20
View File
@@ -1,14 +1,10 @@
import { LitElement, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import newsItems from "resources/news.json";
import type { NewsItem } from "../../core/ApiSchemas";
import { getNews } from "../Api";
import { translateText } from "../Utils";
export interface NewsItem {
id: string;
title: string;
description: string;
url?: string;
type: "tournament" | "tutorial" | "announcement";
}
export type { NewsItem };
const DISMISSED_NEWS_KEY = "dismissedNewsItems";
const CYCLE_INTERVAL_MS = 5000;
@@ -24,18 +20,22 @@ function getDismissedIds(): Set<string> {
}
function saveDismissedIds(ids: Set<string>): void {
localStorage.setItem(DISMISSED_NEWS_KEY, JSON.stringify([...ids]));
try {
localStorage.setItem(DISMISSED_NEWS_KEY, JSON.stringify([...ids]));
} catch {
// Ignore storage errors — dismiss still works for this session
}
}
export function getVisibleNewsItems(): NewsItem[] {
export function getVisibleNewsItems(items: NewsItem[]): NewsItem[] {
const dismissed = getDismissedIds();
return (newsItems as NewsItem[]).filter((item) => !dismissed.has(item.id));
return items.filter((item) => !dismissed.has(item.id));
}
const typeLabels: Record<NewsItem["type"], string> = {
tournament: "TOURNAMENT",
tutorial: "TUTORIAL",
announcement: "NEWS",
const typeLabelKeys: Record<NewsItem["type"], string> = {
tournament: "news_box.tournament",
tutorial: "news_box.tutorial",
announcement: "news_box.news",
};
const typeLabelColors: Record<NewsItem["type"], string> = {
@@ -56,8 +56,28 @@ export class NewsBox extends LitElement {
connectedCallback() {
super.connectedCallback();
this.items = getVisibleNewsItems();
this.startCycle();
this.loadNews();
}
private async loadNews() {
try {
const allItems = await getNews();
// Reset stale dismissed list when all items would be hidden
const visible = getVisibleNewsItems(allItems);
if (visible.length === 0 && allItems.length > 0) {
try {
localStorage.removeItem(DISMISSED_NEWS_KEY);
} catch {
// ignore
}
this.items = allItems;
} else {
this.items = visible;
}
this.startCycle();
} catch {
// Silently fail — component simply won't render
}
}
disconnectedCallback() {
@@ -111,7 +131,7 @@ export class NewsBox extends LitElement {
class="shrink-0 text-[10px] font-bold tracking-wider px-2 py-0.5 rounded ${typeLabelColors[
item.type
]}"
>${typeLabels[item.type]}</span
>${translateText(typeLabelKeys[item.type])}</span
>
<div class="flex-1 min-w-0">
${item.url
@@ -140,7 +160,9 @@ export class NewsBox extends LitElement {
this.activeIndex
? "bg-white/60"
: "bg-white/20 hover:bg-white/40"}"
aria-label="Go to item ${i + 1}"
aria-label="${translateText("news_box.go_to_item", {
num: i + 1,
})}"
></button>
`,
)}
@@ -150,7 +172,7 @@ export class NewsBox extends LitElement {
<button
@click=${() => this.dismiss(item.id)}
class="shrink-0 p-0.5 text-white/30 hover:text-white/70 transition-colors"
aria-label="Dismiss"
aria-label="${translateText("news_box.dismiss")}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
+2 -2
View File
@@ -1,6 +1,6 @@
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import "./NewsBox.js";
import "./NewsBox";
@customElement("play-page")
export class PlayPage extends LitElement {
@@ -12,7 +12,7 @@ export class PlayPage extends LitElement {
return html`
<div
id="page-play"
class="flex flex-col gap-2 w-full px-0 lg:px-4 lg:my-auto min-h-0"
class="flex flex-col gap-2 w-full px-0 lg:px-4 min-h-0"
>
<token-login class="absolute"></token-login>
+9
View File
@@ -191,3 +191,12 @@ export const RankedLeaderboardResponseSchema = z.object({
export type RankedLeaderboardResponse = z.infer<
typeof RankedLeaderboardResponseSchema
>;
export const NewsItemSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string(),
url: z.string().optional(),
type: z.enum(["tournament", "tutorial", "announcement"]),
});
export type NewsItem = z.infer<typeof NewsItemSchema>;
+11 -10
View File
@@ -5,6 +5,7 @@ import {
} from "../../../src/client/components/NewsBox";
const DISMISSED_NEWS_KEY = "dismissedNewsItems";
const allItems = newsItems as NewsItem[];
function createMockLocalStorage(): Storage {
let store: Record<string, string> = {};
@@ -37,37 +38,37 @@ describe("NewsBox", () => {
describe("getVisibleNewsItems", () => {
it("returns all items when none are dismissed", () => {
const items = getVisibleNewsItems();
const items = getVisibleNewsItems(allItems);
expect(items.length).toBe(newsItems.length);
});
it("filters out dismissed items", () => {
const items = getVisibleNewsItems();
const items = getVisibleNewsItems(allItems);
const firstId = items[0].id;
localStorage.setItem(DISMISSED_NEWS_KEY, JSON.stringify([firstId]));
const filtered = getVisibleNewsItems();
const filtered = getVisibleNewsItems(allItems);
expect(filtered.find((i) => i.id === firstId)).toBeUndefined();
expect(filtered.length).toBe(items.length - 1);
});
it("handles corrupted localStorage gracefully", () => {
localStorage.setItem(DISMISSED_NEWS_KEY, "not-valid-json");
expect(() => getVisibleNewsItems()).not.toThrow();
const items = getVisibleNewsItems();
expect(() => getVisibleNewsItems(allItems)).not.toThrow();
const items = getVisibleNewsItems(allItems);
expect(items.length).toBe(newsItems.length);
});
it("returns empty when all items are dismissed", () => {
const allIds = (newsItems as NewsItem[]).map((i) => i.id);
const allIds = allItems.map((i) => i.id);
localStorage.setItem(DISMISSED_NEWS_KEY, JSON.stringify(allIds));
const items = getVisibleNewsItems();
const items = getVisibleNewsItems(allItems);
expect(items.length).toBe(0);
});
});
describe("news items structure", () => {
it("each item has required fields", () => {
const items = getVisibleNewsItems();
const items = getVisibleNewsItems(allItems);
for (const item of items) {
expect(item.id).toBeDefined();
expect(typeof item.id).toBe("string");
@@ -81,13 +82,13 @@ describe("NewsBox", () => {
});
it("each item has a unique id", () => {
const items = getVisibleNewsItems();
const items = getVisibleNewsItems(allItems);
const ids = items.map((i) => i.id);
expect(new Set(ids).size).toBe(ids.length);
});
it("contains a tournament entry", () => {
const items = getVisibleNewsItems();
const items = getVisibleNewsItems(allItems);
expect(items.some((i) => i.type === "tournament")).toBe(true);
});
});