diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..a440547 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,48 @@ +# AGENT.md — Stacks Wars Frontend + +Canonical docs: https://docs.stackswars.com/ + +Do not invent architecture. Prefer docs + existing patterns in this repo. + +## Scope + +- App root: this `frontend/` package (Bun + Next.js). +- Games UI: `games/{gameId}/` registered via `games/boot.ts` + `games/registry.ts`. +- Keep `PLAYABLE_GAME_IDS` in sync with `boot.ts` imports. + +## Architecture rules + +1. **WebSocket-first** — lobby/room/presence updates flow through the multiplexed `/app` socket. Do not poll for state the socket already pushes. +2. **Server actions / API** — mutations that need secrets, Hiro, vault, or auth stay in `actions/` or Route Handlers; clients stay thin. +3. **No duplicated domain logic** — amounts, vault helpers, and formatters live in `lib/` (e.g. `lib/vault`, `lib/format`). Reuse them. +4. **Game isolation** — game-specific React stays under `games/{id}/`. Shared room chrome stays in `components/room/`. +5. **Registry only on the client** — `registerGame` is UI wiring; catalog metadata comes from the Rust API. + +## Routing + +- App shell: `app/(app)/…` +- Auth: `app/auth/` +- Profiles: `/profile/[username]` +- Prefer existing route groups; do not add parallel page trees for the same feature. + +## UI + +- Reuse `@/components/ui` and existing feature components. +- Match current visual language; do not introduce a second design system. +- Self-only controls (e.g. profile **Get ID**) must gate on session user id vs profile id so other visitors stay uncluttered. + +## Realtime + +- Use existing hooks/stores for connection status and room channels. +- After reconnect, rely on snapshot/resync paths already used by rooms — do not bespoke a second protocol. + +## Games + +- `gameId` must match backend `GameId` exactly. +- Required: `Room`. Optional: `LobbyPanel`, `Page`, `createActions`, `onMatchFinished`. +- Read https://docs.stackswars.com/develop/registration before adding a game. + +## Don’t + +- Use browser wallet extensions for play funds (custodial USDCx only). +- Commit secrets (mnemonics, KMS keys, cookie secrets). diff --git a/app/api/cron/lobby-ttl/route.ts b/app/api/cron/lobby-ttl/route.ts index 4ff10d9..8605278 100644 --- a/app/api/cron/lobby-ttl/route.ts +++ b/app/api/cron/lobby-ttl/route.ts @@ -30,7 +30,7 @@ function apiBase() { } function cronAuthorized(request: Request): boolean { - const secret = process.env.CRON_SECRET?.trim() || process.env.INTERNAL_API_SECRET?.trim() + const secret = process.env.INTERNAL_API_SECRET?.trim() if (!secret) return false const header = request.headers.get("authorization") const bearer = header?.startsWith("Bearer ") ? header.slice(7) : null diff --git a/components/profile/get-developer-id.tsx b/components/profile/get-developer-id.tsx new file mode 100644 index 0000000..6b1119a --- /dev/null +++ b/components/profile/get-developer-id.tsx @@ -0,0 +1,128 @@ +"use client" + +import * as React from "react" +import { RiCheckLine, RiFileCopyLine, RiKey2Line } from "@remixicon/react" + +import { + Button, + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui" +import { useNotificationsStore } from "@/stores/notifications" +import { useSessionStore } from "@/stores/session" + +type GetDeveloperIdProps = { + /** Profile owner's platform user id (Neon `sub` / `UserId`). */ + userId: string +} + +/** + * Self-only control: opens a dialog so game developers can copy their + * platform user id for `GameMetadata.dev_id` registration. + * Hidden for other visitors so the profile stays uncluttered. + */ +export function GetDeveloperId({ userId }: GetDeveloperIdProps) { + const sessionUserId = useSessionStore((s) => s.user?.id) + const toast = useNotificationsStore((s) => s.toast) + const [open, setOpen] = React.useState(false) + const [copied, setCopied] = React.useState(false) + + if (!sessionUserId || sessionUserId !== userId) { + return null + } + + async function copyId() { + try { + await navigator.clipboard.writeText(userId) + setCopied(true) + window.setTimeout(() => setCopied(false), 1600) + toast({ + title: "Developer ID copied", + body: "Paste it into your game metadata as dev_id.", + tone: "success", + }) + } catch { + toast({ + title: "Could not copy", + body: "Select the ID and copy it manually.", + tone: "danger", + }) + } + } + + return ( + <> + + + + + + + + Developer ID + + + Use this ID as{" "} + + dev_id + {" "} + when you register a game. It identifies you as the + developer for fee payouts. See{" "} + + the developer docs + + . + + + +
+ + {userId} + + +
+ + + + +
+
+ + ) +} diff --git a/components/profile/profile-header.tsx b/components/profile/profile-header.tsx index 39c2698..d3b53c0 100644 --- a/components/profile/profile-header.tsx +++ b/components/profile/profile-header.tsx @@ -1,5 +1,6 @@ import { RiCalendarLine } from "@remixicon/react" +import { GetDeveloperId } from "@/components/profile/get-developer-id" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui" import type { UserProfile } from "@/lib/api/types" import { compact, displayNameFor, formatDate, ordinal } from "@/lib/format" @@ -22,7 +23,7 @@ export function ProfileHeader({ profile }: { profile: UserProfile }) {
@@ -41,10 +42,13 @@ export function ProfileHeader({ profile }: { profile: UserProfile }) { @{user.username}

) : null} -

- - Joined {formatDate(user.createdAt)} -

+
+

+ + Joined {formatDate(user.createdAt)} +

+ +
@@ -90,7 +94,12 @@ function Headline({
{label}
-
+
{value}
diff --git a/components/shell/app-footer.tsx b/components/shell/app-footer.tsx index 7d2efba..a086e3d 100644 --- a/components/shell/app-footer.tsx +++ b/components/shell/app-footer.tsx @@ -6,29 +6,43 @@ const LINKS = [ { href: "/games", label: "Games" }, { href: "/lobbies", label: "Lobbies" }, { href: "/leaderboard", label: "Leaderboard" }, + { href: "https://docs.stackswars.com", label: "Docs", external: true }, + { href: "https://t.me/stackswars", label: "Telegram", external: true }, ] export function AppFooter() { return (