Added easy drop-down to add consoles and games to bio
This commit is contained in:
@@ -1,23 +1,46 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { getIntegrationConfig, getProfile } from "@/lib/integrations";
|
||||
import { SOCIAL_NETWORKS } from "@/lib/integrations/social";
|
||||
import type {
|
||||
ConsoleItem,
|
||||
FavoriteGame,
|
||||
SocialLink,
|
||||
} from "@/lib/integrations/types";
|
||||
import type { SocialLink } from "@/lib/integrations/types";
|
||||
import {
|
||||
saveIntegrationsAction,
|
||||
refreshIntegrationsAction,
|
||||
testIntegrationAction,
|
||||
} from "../../actions";
|
||||
import ConsolePicker from "./ConsolePicker";
|
||||
import GamePicker from "./GamePicker";
|
||||
|
||||
// Expandable "how do I get this?" help, shown under credential fields. Uses a
|
||||
// native <details> so it works with zero JS and stays readable on mobile.
|
||||
function CredHelp({ title, children }: { title: string; children: ReactNode }) {
|
||||
return (
|
||||
<details className="rb-cred-help">
|
||||
<summary>ⓘ {title}</summary>
|
||||
<div className="rb-cred-help-body">{children}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
// A "Test connection" submit button. Submits the whole form to the test action
|
||||
// (so it validates the live, unsaved field values) tagged with which platform.
|
||||
function TestButton({ platform }: { platform: string }) {
|
||||
return (
|
||||
<button
|
||||
type="submit"
|
||||
className="rb-btn rb-btn-ghost"
|
||||
formAction={testIntegrationAction.bind(null, platform)}
|
||||
formNoValidate
|
||||
>
|
||||
Test connection
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -29,18 +52,51 @@ function fmtAge(iso: string): string {
|
||||
return hrs < 24 ? `${hrs}h ago` : `${Math.round(hrs / 24)}d ago`;
|
||||
}
|
||||
|
||||
function Banner({ saved, refreshed }: { saved?: string; refreshed?: string }) {
|
||||
if (saved) return <p className="rb-admin-ok">Integrations saved. Caches cleared.</p>;
|
||||
if (refreshed) return <p className="rb-admin-ok">Platform caches cleared — data refetches on next view.</p>;
|
||||
type Banners = {
|
||||
saved?: string;
|
||||
refreshed?: string;
|
||||
steam?: string;
|
||||
steamErr?: string;
|
||||
test?: string;
|
||||
testErr?: string;
|
||||
testGames?: string;
|
||||
testNotice?: string;
|
||||
};
|
||||
|
||||
function Banner(p: Banners) {
|
||||
if (p.saved) return <p className="rb-admin-ok">Integrations saved. Caches cleared.</p>;
|
||||
if (p.refreshed)
|
||||
return <p className="rb-admin-ok">Platform caches cleared — data refetches on next view.</p>;
|
||||
if (p.steam)
|
||||
return (
|
||||
<p className="rb-admin-ok">
|
||||
Steam linked — SteamID <code>{p.steam}</code> saved. Add your API key below to pull
|
||||
game data.
|
||||
</p>
|
||||
);
|
||||
if (p.steamErr) return <p className="rb-admin-error">{p.steamErr}</p>;
|
||||
if (p.test && p.testErr)
|
||||
return (
|
||||
<p className="rb-admin-error">
|
||||
{p.test.toUpperCase()} test failed: {p.testErr}
|
||||
</p>
|
||||
);
|
||||
if (p.test)
|
||||
return (
|
||||
<p className={p.testNotice ? "rb-admin-warn" : "rb-admin-ok"}>
|
||||
{p.test.toUpperCase()} connection OK — fetched {p.testGames ?? 0} game(s).
|
||||
{p.testNotice ? ` ${p.testNotice}` : ""}
|
||||
</p>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
export default async function IntegrationsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ saved?: string; refreshed?: string }>;
|
||||
searchParams: Promise<Banners>;
|
||||
}) {
|
||||
const { saved, refreshed } = await searchParams;
|
||||
const banners = await searchParams;
|
||||
const cfg = getIntegrationConfig();
|
||||
// Loads (cached) platform data so we can show freshness + any fetch errors.
|
||||
const profile = await getProfile();
|
||||
@@ -49,7 +105,7 @@ export default async function IntegrationsPage({
|
||||
return (
|
||||
<div className="rb-admin-page">
|
||||
<h1 className="rb-admin-h1">Integrations</h1>
|
||||
<Banner saved={saved} refreshed={refreshed} />
|
||||
<Banner {...banners} />
|
||||
<p className="rb-admin-muted">
|
||||
Build your gamer bio: a profile, social links, console list, favorite
|
||||
games, and live data pulled from gaming platforms. Everything here powers
|
||||
@@ -136,31 +192,19 @@ export default async function IntegrationsPage({
|
||||
|
||||
{/* ---- consoles + favorites ---- */}
|
||||
<h2 className="rb-admin-h2">Collection</h2>
|
||||
<div className="rb-field-row">
|
||||
<label className="rb-field">
|
||||
<span>
|
||||
Consoles owned <em className="rb-admin-muted">(name|note per line)</em>
|
||||
</span>
|
||||
<textarea
|
||||
name="consoles"
|
||||
rows={5}
|
||||
className="rb-mono"
|
||||
placeholder={"PlayStation 2|Phat, launch unit\nDreamcast|with VMU"}
|
||||
defaultValue={namedToText(cfg.consoles)}
|
||||
/>
|
||||
</label>
|
||||
<label className="rb-field">
|
||||
<span>
|
||||
Favorite games <em className="rb-admin-muted">(name|note per line)</em>
|
||||
</span>
|
||||
<textarea
|
||||
name="favorites"
|
||||
rows={5}
|
||||
className="rb-mono"
|
||||
placeholder={"Shadow of the Colossus|GOAT\nChrono Trigger"}
|
||||
defaultValue={namedToText(cfg.favorites)}
|
||||
/>
|
||||
</label>
|
||||
<div className="rb-field">
|
||||
<span>
|
||||
Consoles owned{" "}
|
||||
<em className="rb-admin-muted">— search and click to add</em>
|
||||
</span>
|
||||
<ConsolePicker name="consoles" initial={cfg.consoles} />
|
||||
</div>
|
||||
<div className="rb-field">
|
||||
<span>
|
||||
Favorite games{" "}
|
||||
<em className="rb-admin-muted">— search (Steam catalog) and click to add</em>
|
||||
</span>
|
||||
<GamePicker name="favorites" initial={cfg.favorites} />
|
||||
</div>
|
||||
|
||||
{/* ---- platforms ---- */}
|
||||
@@ -169,12 +213,20 @@ export default async function IntegrationsPage({
|
||||
<input type="checkbox" name="steamEnabled" defaultChecked={cfg.steam.enabled} />
|
||||
<span>Enable Steam integration</span>
|
||||
</label>
|
||||
<p className="rb-admin-muted">
|
||||
One-click:{" "}
|
||||
<a className="rb-btn rb-btn-ghost rb-btn-inline" href="/admin/integrations/steam/link">
|
||||
Sign in with Steam
|
||||
</a>{" "}
|
||||
to fill your SteamID automatically{cfg.steam.steamId ? ` (current: ${cfg.steam.steamId})` : ""}.
|
||||
You still add an API key below for game data.
|
||||
</p>
|
||||
<div className="rb-field-row">
|
||||
<label className="rb-field">
|
||||
<span>
|
||||
API key{" "}
|
||||
<em className="rb-admin-muted">
|
||||
({cfg.steam.apiKey ? "stored — blank keeps it" : "from steamcommunity.com/dev"})
|
||||
({cfg.steam.apiKey ? "stored — blank keeps it" : "required for game data"})
|
||||
</em>
|
||||
</span>
|
||||
<input
|
||||
@@ -183,12 +235,41 @@ export default async function IntegrationsPage({
|
||||
autoComplete="off"
|
||||
placeholder={cfg.steam.apiKey ? "•••••••• stored" : ""}
|
||||
/>
|
||||
<CredHelp title="How to get a Steam API key">
|
||||
<ol>
|
||||
<li>
|
||||
Go to{" "}
|
||||
<a href="https://steamcommunity.com/dev/apikey" target="_blank" rel="noreferrer">
|
||||
steamcommunity.com/dev/apikey
|
||||
</a>{" "}
|
||||
(sign in if asked).
|
||||
</li>
|
||||
<li>Enter any domain name (e.g. <code>localhost</code>) and agree to the terms.</li>
|
||||
<li>Copy the generated key and paste it here.</li>
|
||||
</ol>
|
||||
<p>Your Steam profile must be set to <strong>Public</strong> for game data to load.</p>
|
||||
</CredHelp>
|
||||
</label>
|
||||
<label className="rb-field">
|
||||
<span>SteamID (64-bit)</span>
|
||||
<input name="steamId" defaultValue={cfg.steam.steamId} placeholder="7656119…" />
|
||||
<CredHelp title="How to find your SteamID">
|
||||
<ol>
|
||||
<li>Use the “Sign in with Steam” button above — easiest, fills it for you.</li>
|
||||
<li>
|
||||
Or open{" "}
|
||||
<a href="https://steamid.io" target="_blank" rel="noreferrer">
|
||||
steamid.io
|
||||
</a>
|
||||
, paste your profile URL, and copy the <code>steamID64</code> value.
|
||||
</li>
|
||||
</ol>
|
||||
</CredHelp>
|
||||
</label>
|
||||
</div>
|
||||
<div className="rb-form-actions rb-form-actions-sub">
|
||||
<TestButton platform="steam" />
|
||||
</div>
|
||||
|
||||
<h2 className="rb-admin-h2">PlayStation (PSN)</h2>
|
||||
<label className="rb-check">
|
||||
@@ -199,7 +280,7 @@ export default async function IntegrationsPage({
|
||||
<span>
|
||||
NPSSO token{" "}
|
||||
<em className="rb-admin-muted">
|
||||
({cfg.psn.npsso ? "stored — blank keeps it" : "from ca.account.sony.com/api/v1/ssocookie"})
|
||||
({cfg.psn.npsso ? "stored — blank keeps it" : "required"})
|
||||
</em>
|
||||
</span>
|
||||
<input
|
||||
@@ -208,7 +289,37 @@ export default async function IntegrationsPage({
|
||||
autoComplete="off"
|
||||
placeholder={cfg.psn.npsso ? "•••••••• stored" : "64-char token"}
|
||||
/>
|
||||
<CredHelp title="How to get your NPSSO token">
|
||||
<ol>
|
||||
<li>
|
||||
Sign in at{" "}
|
||||
<a href="https://www.playstation.com" target="_blank" rel="noreferrer">
|
||||
playstation.com
|
||||
</a>{" "}
|
||||
in your browser.
|
||||
</li>
|
||||
<li>
|
||||
In the same browser, open{" "}
|
||||
<a
|
||||
href="https://ca.account.sony.com/api/v1/ssocookie"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
ca.account.sony.com/api/v1/ssocookie
|
||||
</a>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
Copy the 64-character value of <code>npsso</code> from the JSON shown and paste it
|
||||
here.
|
||||
</li>
|
||||
</ol>
|
||||
<p>The token expires periodically — re-paste a fresh one if PSN starts erroring.</p>
|
||||
</CredHelp>
|
||||
</label>
|
||||
<div className="rb-form-actions rb-form-actions-sub">
|
||||
<TestButton platform="psn" />
|
||||
</div>
|
||||
|
||||
<h2 className="rb-admin-h2">Xbox</h2>
|
||||
<label className="rb-check">
|
||||
@@ -220,7 +331,7 @@ export default async function IntegrationsPage({
|
||||
<span>
|
||||
OpenXBL API key{" "}
|
||||
<em className="rb-admin-muted">
|
||||
({cfg.xbox.apiKey ? "stored — blank keeps it" : "from xbl.io"})
|
||||
({cfg.xbox.apiKey ? "stored — blank keeps it" : "required"})
|
||||
</em>
|
||||
</span>
|
||||
<input
|
||||
@@ -229,14 +340,93 @@ export default async function IntegrationsPage({
|
||||
autoComplete="off"
|
||||
placeholder={cfg.xbox.apiKey ? "•••••••• stored" : ""}
|
||||
/>
|
||||
<CredHelp title="How to get an OpenXBL API key">
|
||||
<ol>
|
||||
<li>
|
||||
Go to{" "}
|
||||
<a href="https://xbl.io" target="_blank" rel="noreferrer">
|
||||
xbl.io
|
||||
</a>{" "}
|
||||
and sign in with your Microsoft / Xbox account.
|
||||
</li>
|
||||
<li>Open the console / API keys page and generate a key.</li>
|
||||
<li>Copy the key and paste it here.</li>
|
||||
</ol>
|
||||
<p>Xbox has no official public API; OpenXBL is a third-party proxy.</p>
|
||||
</CredHelp>
|
||||
</label>
|
||||
<label className="rb-field">
|
||||
<span>
|
||||
XUID <em className="rb-admin-muted">(optional)</em>
|
||||
</span>
|
||||
<input name="xboxXuid" defaultValue={cfg.xbox.xuid} />
|
||||
<CredHelp title="Do I need a XUID?">
|
||||
<p>
|
||||
No — leave blank and OpenXBL uses the account tied to your API key. Only set this to
|
||||
read a <em>different</em> profile.
|
||||
</p>
|
||||
</CredHelp>
|
||||
</label>
|
||||
</div>
|
||||
<div className="rb-form-actions rb-form-actions-sub">
|
||||
<TestButton platform="xbox" />
|
||||
</div>
|
||||
|
||||
<h2 className="rb-admin-h2">RetroAchievements</h2>
|
||||
<label className="rb-check">
|
||||
<input type="checkbox" name="retroEnabled" defaultChecked={cfg.retro.enabled} />
|
||||
<span>Enable RetroAchievements integration</span>
|
||||
</label>
|
||||
<div className="rb-field-row">
|
||||
<label className="rb-field">
|
||||
<span>Username</span>
|
||||
<input
|
||||
name="retroUsername"
|
||||
defaultValue={cfg.retro.username}
|
||||
placeholder="your RA username"
|
||||
/>
|
||||
</label>
|
||||
<label className="rb-field">
|
||||
<span>
|
||||
Web API key{" "}
|
||||
<em className="rb-admin-muted">
|
||||
({cfg.retro.apiKey ? "stored — blank keeps it" : "required"})
|
||||
</em>
|
||||
</span>
|
||||
<input
|
||||
name="retroApiKey"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
placeholder={cfg.retro.apiKey ? "•••••••• stored" : ""}
|
||||
/>
|
||||
<CredHelp title="How to get a RetroAchievements API key">
|
||||
<ol>
|
||||
<li>
|
||||
Sign in at{" "}
|
||||
<a href="https://retroachievements.org" target="_blank" rel="noreferrer">
|
||||
retroachievements.org
|
||||
</a>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
Open{" "}
|
||||
<a
|
||||
href="https://retroachievements.org/settings"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Settings → Keys
|
||||
</a>{" "}
|
||||
and copy your <strong>Web API Key</strong>.
|
||||
</li>
|
||||
<li>Paste it here along with your username above.</li>
|
||||
</ol>
|
||||
</CredHelp>
|
||||
</label>
|
||||
</div>
|
||||
<div className="rb-form-actions rb-form-actions-sub">
|
||||
<TestButton platform="retro" />
|
||||
</div>
|
||||
|
||||
{/* ---- cache ---- */}
|
||||
<h2 className="rb-admin-h2">Caching</h2>
|
||||
|
||||
Reference in New Issue
Block a user