mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 14:50:44 +00:00
- Update the newbox to fetch news from the api
- Fix the comment recoms and the minor issues
This commit is contained in:
@@ -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}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user