From 1f195a16de4d394967607900e8001c5e208f87e7 Mon Sep 17 00:00:00 2001 From: kawa Date: Sun, 7 Jun 2026 04:26:51 +0200 Subject: [PATCH] feat: add platform integrations + gamer bio system Blog can now pull live gaming data and surface a curated gamer profile. - DB: integrations config table (single JSON row, holds secrets, excluded from export), integration_cache table (lazy TTL cache), posts.links column - Platform fetchers: Steam (official Web API), PSN (NPSSO->token->trophies), Xbox (OpenXBL). All degrade gracefully; cache keeps last-good payload on fetch failure so pages never blank - Admin /integrations: profile/bio, social links, consoles, favorites, per-platform creds (blank keeps stored secret), cache TTL, live status table with refresh - Public /bio page: avatar, bio, recent games, achievements, favorites, consoles. Gated behind hasIntegrations() so a vanilla blog stays clean - About page bio teaser, Shell "now playing" tray widget + Bio nav link - Per-post "shared on" social links (editable in PostForm, shown on post page) Co-Authored-By: Claude Opus 4.8 --- src/app/about/page.tsx | 18 ++ src/app/admin/(panel)/integrations/page.tsx | 263 ++++++++++++++++++++ src/app/admin/(panel)/layout.tsx | 1 + src/app/admin/(panel)/posts/PostForm.tsx | 19 ++ src/app/admin/actions.ts | 80 ++++++ src/app/admin/admin.css | 20 ++ src/app/bio/page.tsx | 162 ++++++++++++ src/app/globals.css | 258 +++++++++++++++++++ src/app/posts/[slug]/page.tsx | 7 + src/components/Shell.tsx | 19 ++ src/components/SocialLinks.tsx | 36 +++ src/lib/db.ts | 25 ++ src/lib/integrations/cache.ts | 89 +++++++ src/lib/integrations/config.ts | 125 ++++++++++ src/lib/integrations/index.ts | 127 ++++++++++ src/lib/integrations/psn.ts | 124 +++++++++ src/lib/integrations/social.ts | 57 +++++ src/lib/integrations/steam.ts | 120 +++++++++ src/lib/integrations/types.ts | 118 +++++++++ src/lib/integrations/xbox.ts | 93 +++++++ src/lib/posts.ts | 64 ++++- 21 files changed, 1822 insertions(+), 3 deletions(-) create mode 100644 src/app/admin/(panel)/integrations/page.tsx create mode 100644 src/app/bio/page.tsx create mode 100644 src/components/SocialLinks.tsx create mode 100644 src/lib/integrations/cache.ts create mode 100644 src/lib/integrations/config.ts create mode 100644 src/lib/integrations/index.ts create mode 100644 src/lib/integrations/psn.ts create mode 100644 src/lib/integrations/social.ts create mode 100644 src/lib/integrations/steam.ts create mode 100644 src/lib/integrations/types.ts create mode 100644 src/lib/integrations/xbox.ts diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index ed4ab67..5b86a46 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,9 +1,14 @@ +import Link from "next/link"; import Shell from "@/components/Shell"; +import SocialLinks from "@/components/SocialLinks"; import { getTheme } from "@/themes/server"; import { THEMES } from "@/themes/registry"; +import { getIntegrationConfig, hasIntegrations } from "@/lib/integrations"; export default async function AboutPage() { const theme = await getTheme(); + const showBio = hasIntegrations(); + const cfg = showBio ? getIntegrationConfig() : null; return (
@@ -30,6 +35,19 @@ export default async function AboutPage() { file and registering it — no markup changes required.

+ + {showBio && cfg && ( +
+

About the webmaster

+

+ {cfg.displayName ? {cfg.displayName} : "The webmaster"}{" "} + keeps a living gamer profile here — recently played titles, + achievements, the console shelf, and all-time favorites.{" "} + See the full bio → +

+ +
+ )}
); diff --git a/src/app/admin/(panel)/integrations/page.tsx b/src/app/admin/(panel)/integrations/page.tsx new file mode 100644 index 0000000..b63c7b1 --- /dev/null +++ b/src/app/admin/(panel)/integrations/page.tsx @@ -0,0 +1,263 @@ +import { getIntegrationConfig, getProfile } from "@/lib/integrations"; +import { SOCIAL_NETWORKS } from "@/lib/integrations/social"; +import type { + ConsoleItem, + FavoriteGame, + SocialLink, +} from "@/lib/integrations/types"; +import { + saveIntegrationsAction, + refreshIntegrationsAction, +} from "../../actions"; + +function socialToText(links: SocialLink[]): string { + return links + .map((l) => [l.network, l.url, l.label].filter(Boolean).join(" | ")) + .join("\n"); +} +function namedToText(items: (ConsoleItem | FavoriteGame)[]): string { + return items.map((i) => [i.name, i.note].filter(Boolean).join(" | ")).join("\n"); +} + +function fmtAge(iso: string): string { + const t = new Date(iso).getTime(); + if (Number.isNaN(t)) return "—"; + const mins = Math.round((Date.now() - t) / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins} min ago`; + const hrs = Math.round(mins / 60); + return hrs < 24 ? `${hrs}h ago` : `${Math.round(hrs / 24)}d ago`; +} + +function Banner({ saved, refreshed }: { saved?: string; refreshed?: string }) { + if (saved) return

Integrations saved. Caches cleared.

; + if (refreshed) return

Platform caches cleared — data refetches on next view.

; + return null; +} + +export default async function IntegrationsPage({ + searchParams, +}: { + searchParams: Promise<{ saved?: string; refreshed?: string }>; +}) { + const { saved, refreshed } = await searchParams; + const cfg = getIntegrationConfig(); + // Loads (cached) platform data so we can show freshness + any fetch errors. + const profile = await getProfile(); + const networkIds = SOCIAL_NETWORKS.map((n) => n.id).join(", "); + + return ( +
+

Integrations

+ +

+ Build your gamer bio: a profile, social links, console list, favorite + games, and live data pulled from gaming platforms. Everything here powers + the public bio page. +

+ + {/* ---- platform status ---- */} + {profile.platforms.length > 0 && ( +
+

Platform status

+ + + + + + + + + + + + {profile.platforms.map((p) => ( + + + + + + + + ))} + +
PlatformGamesAchievementsFetchedStatus
{p.platform.toUpperCase()}{p.games.length}{p.achievements.length}{fmtAge(p.fetchedAt)} + {p.error ? ( + + {p.error} + + ) : ( + "OK" + )} +
+
+ +
+
+ )} + +
+ {/* ---- profile ---- */} +

Profile

+
+ + +
+