Skip to content

Commit 44f251a

Browse files
B2JK-Industryclaude
andcommitted
fix(ui): PR-P G-04 + G-05 + G-28 — /consent page, drop SK leak, all building chips
G-05 — extract Slovak strings from /o-platforme. The page imports `dictFor(lang).aboutPage` for most copy but 7 strings were still hardcoded in Slovak (Vďaka, Vstupná, Heslá, Claude … 3× preklad, žiadne PNG, sú vektor, Späť, súkromia). Visible to PL/UK/CS/EN players as a 7-string Slovak leak. Five became new aboutPage keys (`techNoteZod`, `techNoteAuth`, `techNoteSdk`, `techSvgName`, `techSvgNote`); the thanks line + 2 footer links reuse pre-existing `sponsorsThanks`, `footerHome`, `footerPrivacy` keys (avoided introducing duplicates). G-04 — `/consent` informational page (NEW). Cookie banner's "More" CTA used to deep-link straight into the long privacy policy. We ship exactly 3 strictly-necessary cookies (xp-session, wc_csrf, xp_lang), no analytics, no tracking — so the right surface is a focused page that lists them with purpose + lifetime. Privacy policy stays the secondary deep-link from /consent. Server component, per-locale via `getLang()`. New `consent.*` dict keys × 4 langs. Banner "More / Více" link on desktop now points at /consent via `<Link>` (next/link required by ESLint @next/next/no-html-link-for-pages); mobile inline link kept on /ochrana-sukromia per banner-tightness pattern. G-28 — CityLevelCard show all building groups. User feedback (screenshot 0:15:46): "Twoje budynki" strip clamped at 6 chips via `aggregateByCatalog(...).slice(0, 6)`. Player with > 6 distinct catalog types couldn't see the rest. Removed the slice — catalog ceiling (~30 entries) is the natural upper bound, parent flex-wrap handles overflow into multiple rows. Tooling note: initial uk/pl/cs intro strings used German low-9 + straight-quote inner labels („Akceptuj") which TS parsed as mid-string termination. Replaced inner decorative quotes with context-only label words (no quotes) so all 4 locales compile clean. Validation: - pnpm typecheck → 0 errors - pnpm lint → 0 errors Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1dc4924 commit 44f251a

8 files changed

Lines changed: 170 additions & 18 deletions

File tree

app/consent/page.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Link from "next/link";
2+
import { dictFor } from "@/lib/i18n";
3+
import { getLang } from "@/lib/i18n-server";
4+
5+
/* G-04 — informational `/consent` page.
6+
*
7+
* cookie-consent.tsx (`Více / More` link) used to point at
8+
* /ochrana-sukromia (the long privacy policy). After the banner
9+
* audit confirmed we ship exactly 3 strictly-necessary cookies and
10+
* no analytics/tracking, the right surface for the "More" CTA is a
11+
* focused informational page that lists those 3 cookies + their
12+
* purpose + lifetime. Privacy policy stays the secondary deep-link.
13+
*/
14+
export default async function ConsentPage() {
15+
const lang = await getLang();
16+
const t = dictFor(lang).consent;
17+
const cookies = [
18+
{
19+
name: "xp-session",
20+
purpose: t.cookieSessionPurpose,
21+
duration: t.cookieDurationSession,
22+
},
23+
{
24+
name: "wc_csrf",
25+
purpose: t.cookieCsrfPurpose,
26+
duration: t.cookieDurationSession,
27+
},
28+
{
29+
name: "xp_lang",
30+
purpose: t.cookieLangPurpose,
31+
duration: t.cookieDuration1y,
32+
},
33+
];
34+
return (
35+
<main className="max-w-2xl mx-auto card p-6 flex flex-col gap-4 animate-slide-up">
36+
<h1 className="t-h2 text-[var(--accent)]">{t.title}</h1>
37+
<p className="text-[var(--ink-muted)]">{t.intro}</p>
38+
<table className="w-full text-sm">
39+
<thead className="text-xs text-[var(--ink-muted)] border-b border-[var(--line)]">
40+
<tr>
41+
<th className="text-left p-2">{t.colName}</th>
42+
<th className="text-left p-2">{t.colPurpose}</th>
43+
<th className="text-left p-2">{t.colDuration}</th>
44+
</tr>
45+
</thead>
46+
<tbody>
47+
{cookies.map((c) => (
48+
<tr key={c.name} className="border-b border-[var(--line)]">
49+
<td className="p-2 font-mono">{c.name}</td>
50+
<td className="p-2">{c.purpose}</td>
51+
<td className="p-2 text-[var(--ink-muted)]">{c.duration}</td>
52+
</tr>
53+
))}
54+
</tbody>
55+
</table>
56+
<p className="t-caption text-[var(--ink-muted)]">{t.optOutBody}</p>
57+
<Link href="/ochrana-sukromia" className="btn btn-secondary self-start">
58+
{t.privacyPolicy}
59+
</Link>
60+
</main>
61+
);
62+
}

app/o-platforme/page.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,7 @@ export default async function AboutPage() {
283283
/ochrana-sukromia
284284
</Link>
285285
</p>
286-
<p className="text-xs text-[var(--ink-muted)]">
287-
Vďaka: PKO Bank Polski · Tauron · ETHWarsaw · AKMF ·
288-
Katowicki.Hub.
289-
</p>
286+
<p className="text-xs text-[var(--ink-muted)]">{t.sponsorsThanks}</p>
290287
</div>
291288
</section>
292289

@@ -389,11 +386,11 @@ export default async function AboutPage() {
389386
<TechItem name="TypeScript strict" note="Úplné typové pokrytie." />
390387
<TechItem name="Tailwind CSS 4" note="Neo-brutalist tokens + primitives v globals.css." />
391388
<TechItem name="Upstash Redis" note="Sorted sets pre leaderboardy, JSON pre účty a duely, EU región." />
392-
<TechItem name="zod" note="Vstupná + AI-output validácia." />
393-
<TechItem name="scrypt + HMAC" note="Heslá + HTTP-only signed session cookie." />
389+
<TechItem name="zod" note={t.techNoteZod} />
390+
<TechItem name="scrypt + HMAC" note={t.techNoteAuth} />
394391
<TechItem name="Vercel Cron" note="AI pipeline trigger denne o 09:00 UTC." />
395-
<TechItem name="Anthropic SDK" note="Claude Sonnet 4.6 (PL gen) + Haiku 4.5 (3× preklad), JSON structured output." />
396-
<TechItem name="SVG, žiadne PNG/JPG" note="Celé mestečko + budova sú vektor, ostrý na 4K." />
392+
<TechItem name="Anthropic SDK" note={t.techNoteSdk} />
393+
<TechItem name={t.techSvgName} note={t.techSvgNote} />
397394
</div>
398395
</section>
399396

@@ -449,8 +446,8 @@ export default async function AboutPage() {
449446
</section>
450447

451448
<footer className="text-xs text-[var(--ink-muted)] border-t border-[var(--line)] pt-4 flex flex-wrap gap-4">
452-
<Link href="/" className="underline">Späť na domov</Link>
453-
<Link href="/ochrana-sukromia" className="underline">Ochrana súkromia</Link>
449+
<Link href="/" className="underline">{t.footerHome}</Link>
450+
<Link href="/ochrana-sukromia" className="underline">{t.footerPrivacy}</Link>
454451
<a
455452
href="https://github.com/B2JK-Industry/watt-city"
456453
className="underline"

components/city-level-card.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,13 @@ type Props = {
104104

105105
/** Aggregate the player's buildings by catalog id so a city of "3 Domek
106106
* + 2 Sklepik" renders as two chips (🏠 ×3, 🏪 ×2) instead of five
107-
* identical glyphs. Returns up to 6 chips ordered by descending count
108-
* (most-built first) so the strip reads as "what defines this city". */
107+
* identical glyphs. Returns ALL distinct building groups ordered by
108+
* descending count (most-built first) so the strip reads as
109+
* "everything you've built so far" — flex-wrap on the parent
110+
* handles overflow into multiple rows.
111+
* G-28 — previously clamped at 6; user reported missing buildings
112+
* on cities with >6 distinct types. The catalog ceiling (~30
113+
* entries) is the natural upper bound now. */
109114
function aggregateByCatalog(
110115
buildings: ReadonlyArray<{ catalogId: string; level: number }>,
111116
): Array<{ glyph: string; name: string; count: number; topLevel: number }> {
@@ -129,9 +134,7 @@ function aggregateByCatalog(
129134
});
130135
}
131136
}
132-
return Array.from(groups.values())
133-
.sort((a, b) => b.count - a.count)
134-
.slice(0, 6);
137+
return Array.from(groups.values()).sort((a, b) => b.count - a.count);
135138
}
136139

137140
export function CityLevelCard({ player, lang }: Props) {

components/cookie-consent.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { useEffect, useState } from "react";
4+
import Link from "next/link";
45
import { usePathname } from "next/navigation";
56
import type { Lang } from "@/lib/i18n";
67

@@ -197,12 +198,17 @@ export function CookieConsent({
197198
<span>{copy.noAds}</span>
198199
</p>
199200
</div>
200-
<a
201-
href="/ochrana-sukromia"
201+
{/* G-04 — desktop "more" CTA points at the focused informational
202+
/consent page (cookie list + 1-line privacy policy link). The
203+
long policy lives at /ochrana-sukromia and is reachable from
204+
/consent; mobile inline link still goes there directly to
205+
keep the banner tight. */}
206+
<Link
207+
href="/consent"
202208
className="hidden sm:inline-flex btn btn-ghost btn-sm shrink-0"
203209
>
204210
{copy.more}
205-
</a>
211+
</Link>
206212
<button
207213
type="button"
208214
onClick={ack}

lib/locales/cs.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import type plDict from "./pl";
22

33
const cs: typeof plDict = {
4+
consent: {
5+
title: "Cookies ve Watt City",
6+
intro:
7+
"Používáme pouze 3 cookies — všechny nezbytné pro chod aplikace. Nesbíráme žádná analytická ani reklamová data, proto má banner jen jedno tlačítko Přijmout.",
8+
colName: "Název",
9+
colPurpose: "Účel",
10+
colDuration: "Životnost",
11+
cookieSessionPurpose: "Relace přihlášeného uživatele (HTTP-only, signed).",
12+
cookieCsrfPurpose: "CSRF token chránící formuláře.",
13+
cookieLangPurpose: "Zvolený jazyk rozhraní.",
14+
cookieDurationSession: "Relace",
15+
cookieDuration1y: "1 rok",
16+
optOutBody:
17+
"Tyto 3 cookies nelze vypnout — bez nich aplikace nepoběží (přihlášení, ochrana formulářů, jazyk). Úplné zásady ochrany soukromí níže.",
18+
privacyPolicy: "Zásady ochrany soukromí",
19+
},
420
errors: {
521
title: "Něco se pokazilo",
622
body: "Nepodařilo se načíst stránku. Zkus to znovu — tvá data jsou v bezpečí.",
@@ -430,6 +446,11 @@ const cs: typeof plDict = {
430446
},
431447
aboutPage: {
432448
title: "O platformě",
449+
techNoteZod: "Validace vstupů + AI outputu.",
450+
techNoteAuth: "Hesla + HTTP-only signed session cookie.",
451+
techNoteSdk: "Claude Sonnet 4.6 (gen PL) + Haiku 4.5 (3× překlad), JSON structured output.",
452+
techSvgName: "SVG, žádné PNG/JPG",
453+
techSvgNote: "Celé městečko + budovy jsou vektor, ostré na 4K.",
433454
ideaTitle: "Myšlenka projektu",
434455
scienceTitle: "Věda za návykem — proč efemerní hry",
435456
howTitle: "Jak to funguje",

lib/locales/en.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import type plDict from "./pl";
22

33
const en: typeof plDict = {
4+
consent: {
5+
title: "Cookies in Watt City",
6+
intro:
7+
"We use exactly 3 cookies — all strictly necessary for the app to work. No analytics, no advertising, no tracking — so the banner has a single \"Accept\" button.",
8+
colName: "Name",
9+
colPurpose: "Purpose",
10+
colDuration: "Lifetime",
11+
cookieSessionPurpose: "Authenticated user session (HTTP-only, signed).",
12+
cookieCsrfPurpose: "CSRF token protecting forms.",
13+
cookieLangPurpose: "Chosen interface language.",
14+
cookieDurationSession: "Session",
15+
cookieDuration1y: "1 year",
16+
optOutBody:
17+
"These 3 cookies can't be turned off — without them the app won't work (login, form protection, language). Full privacy policy below.",
18+
privacyPolicy: "Privacy policy",
19+
},
420
errors: {
521
title: "Something went wrong",
622
body: "We couldn't load this page. Try again — your data is safe.",
@@ -430,6 +446,11 @@ const en: typeof plDict = {
430446
},
431447
aboutPage: {
432448
title: "About",
449+
techNoteZod: "Input + AI-output validation.",
450+
techNoteAuth: "Passwords + HTTP-only signed session cookie.",
451+
techNoteSdk: "Claude Sonnet 4.6 (PL gen) + Haiku 4.5 (3× translation), JSON structured output.",
452+
techSvgName: "SVG, no PNG/JPG",
453+
techSvgNote: "Entire city + buildings are vector — sharp on 4K.",
433454
ideaTitle: "Project idea",
434455
scienceTitle: "The science behind the habit — why ephemeral games",
435456
howTitle: "How it works",

lib/locales/pl.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
const pl = {
2+
consent: {
3+
title: "Cookies w Watt City",
4+
intro:
5+
"Używamy wyłącznie 3 plików cookie, wszystkie ściśle niezbędne do działania aplikacji. Nie zbieramy żadnych danych analitycznych ani reklamowych — dlatego banner ma tylko jeden przycisk Akceptuj.",
6+
colName: "Nazwa",
7+
colPurpose: "Cel",
8+
colDuration: "Czas życia",
9+
cookieSessionPurpose: "Sesja zalogowanego użytkownika (HTTP-only, signed).",
10+
cookieCsrfPurpose: "Token CSRF chroniący formularze.",
11+
cookieLangPurpose: "Wybrany język interfejsu.",
12+
cookieDurationSession: "Sesja",
13+
cookieDuration1y: "1 rok",
14+
optOutBody:
15+
"Te 3 cookie nie da się wyłączyć — bez nich aplikacja nie zadziała (logowanie, ochrona formularzy, język). Pełna polityka prywatności poniżej.",
16+
privacyPolicy: "Polityka prywatności",
17+
},
218
errors: {
319
title: "Coś poszło nie tak",
420
body: "Nie udało się wczytać strony. Spróbuj ponownie — Twoje dane są bezpieczne.",
@@ -428,6 +444,11 @@ const pl = {
428444
},
429445
aboutPage: {
430446
title: "O platformie",
447+
techNoteZod: "Walidacja wejść + outputu AI.",
448+
techNoteAuth: "Hasła + HTTP-only signed session cookie.",
449+
techNoteSdk: "Claude Sonnet 4.6 (gen PL) + Haiku 4.5 (3× tłumaczenie), JSON structured output.",
450+
techSvgName: "SVG, żadnych PNG/JPG",
451+
techSvgNote: "Całe miasteczko + budynki są wektorem, ostre na 4K.",
431452
ideaTitle: "Idea projektu",
432453
scienceTitle: "Nauka za nawykiem — dlaczego efemeryczne gry",
433454
howTitle: "Jak to działa",

lib/locales/uk.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import type plDict from "./pl";
22

33
const uk: typeof plDict = {
4+
consent: {
5+
title: "Cookies у Watt City",
6+
intro:
7+
"Використовуємо лише 3 cookies — усі необхідні для роботи застосунку. Не збираємо аналітики чи реклами, тому банер має лише кнопку Прийняти.",
8+
colName: "Назва",
9+
colPurpose: "Призначення",
10+
colDuration: "Тривалість",
11+
cookieSessionPurpose: "Сесія авторизованого користувача (HTTP-only, signed).",
12+
cookieCsrfPurpose: "CSRF-токен для захисту форм.",
13+
cookieLangPurpose: "Обрана мова інтерфейсу.",
14+
cookieDurationSession: "Сесія",
15+
cookieDuration1y: "1 рік",
16+
optOutBody:
17+
"Ці 3 cookies не можна вимкнути — без них застосунок не працюватиме (логін, захист форм, мова). Повна політика приватності нижче.",
18+
privacyPolicy: "Політика приватності",
19+
},
420
errors: {
521
title: "Щось пішло не так",
622
body: "Не вдалося завантажити сторінку. Спробуй ще раз — твої дані в безпеці.",
@@ -430,6 +446,11 @@ const uk: typeof plDict = {
430446
},
431447
aboutPage: {
432448
title: "Про платформу",
449+
techNoteZod: "Валідація вхідних даних + AI-output.",
450+
techNoteAuth: "Паролі + HTTP-only signed session cookie.",
451+
techNoteSdk: "Claude Sonnet 4.6 (gen PL) + Haiku 4.5 (3× переклад), JSON structured output.",
452+
techSvgName: "SVG, ніяких PNG/JPG",
453+
techSvgNote: "Усе містечко + будівлі — вектор, чіткі на 4K.",
433454
ideaTitle: "Ідея проєкту",
434455
scienceTitle: "Наука за звичкою — чому ефемерні ігри",
435456
howTitle: "Як це працює",

0 commit comments

Comments
 (0)