thread-master/frontend/src/pages/EasterEggsPage.tsx

149 lines
4.5 KiB
TypeScript
Raw Normal View History

2026-06-26 08:37:04 +00:00
import { useState, type ReactNode } from 'react'
import { Badge, Button, Card, CopyableId, Notice, PageTitle } from '../components/ui'
import { RegisterForm } from '../components/RegisterForm'
import { useIslanderUnlock } from '../hooks/useIslanderUnlock'
import { writeIslanderUnlocked } from '../lib/islander/unlock'
import {
easterEggCatalog,
easterEggKindLabel,
type EasterEggEntry,
} from '../lib/easterEggCatalog'
const ADMIN_TOGGLE_IDS = new Set(['hidden-register', 'islander-guide'])
function EasterEggCard({
entry,
action,
}: {
entry: EasterEggEntry
action?: ReactNode
}) {
const kindTone =
entry.kind === 'keyboard' ? 'brand' : entry.kind === 'admin-page' ? 'sky' : 'neutral'
return (
<Card className="flex flex-col gap-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h2 className="text-lg font-bold text-ink">{entry.title}</h2>
<p className="mt-2 max-w-prose text-base leading-relaxed text-ink-secondary">{entry.summary}</p>
</div>
<Badge tone={kindTone}>{easterEggKindLabel(entry.kind)}</Badge>
</div>
{entry.password ? (
<CopyableId label="密碼(連續輸入)" value={entry.password} />
) : ADMIN_TOGGLE_IDS.has(entry.id) ? null : (
<div className="ac-slot px-5 py-5">
<div className="text-sm font-bold text-ink-secondary"></div>
<p className="mt-3 text-base text-muted"> </p>
</div>
)}
<div>
<h3 className="text-sm font-bold text-ink-secondary"></h3>
<ol className="mt-3 list-decimal space-y-2 pl-5 text-base leading-relaxed text-ink">
{entry.howTo.map((step) => (
<li key={step}>{step}</li>
))}
</ol>
</div>
{action ? <div className="flex flex-col gap-4">{action}</div> : null}
{entry.notes ? (
<p className="rounded-[var(--radius-md)] border border-line bg-surface-muted px-4 py-3 text-sm leading-relaxed text-ink-secondary">
<span className="font-bold text-ink"></span>
{entry.notes}
</p>
) : null}
</Card>
)
}
function renderEasterEggAction(
entry: EasterEggEntry,
state: {
registerOpen: boolean
setRegisterOpen: (open: boolean | ((value: boolean) => boolean)) => void
islanderOpen: boolean
},
) {
if (entry.id === 'hidden-register') {
return (
<>
<Button
type="button"
variant={state.registerOpen ? 'soft' : 'primary'}
className="self-start"
onClick={() => state.setRegisterOpen((open) => !open)}
>
{state.registerOpen ? '關閉註冊表單' : '開啟註冊表單'}
</Button>
{state.registerOpen ? (
<div className="rounded-[var(--radius-lg)] border border-line bg-surface-muted p-5">
<RegisterForm />
</div>
) : null}
</>
)
}
if (entry.id === 'islander-guide') {
return (
<>
<Button
type="button"
variant={state.islanderOpen ? 'soft' : 'primary'}
className="self-start"
onClick={() => writeIslanderUnlocked(!state.islanderOpen)}
>
{state.islanderOpen ? '關閉島民嚮導' : '開啟島民嚮導'}
</Button>
{state.islanderOpen ? (
<Notice
tone="success"
title="島民已開啟"
message="請看畫面右下角,會出現島民浮動按鈕(任務觀察站在左下角,島民在右側)。"
/>
) : null}
</>
)
}
return undefined
}
export function EasterEggsPage() {
const [registerOpen, setRegisterOpen] = useState(false)
const islanderOpen = useIslanderUnlock()
return (
<div className="mx-auto max-w-3xl">
<PageTitle
title="彩蛋手冊"
subtitle="管理員專用備忘 — 密碼與功能說明都記在這裡,免得下次忘記。"
/>
<Notice
tone="info"
title="僅管理員可見"
message="此頁不對一般會員開放。新增或修改彩蛋時,請同步更新 lib/easterEggCatalog.ts。"
/>
<div className="mt-6 flex flex-col gap-5">
{easterEggCatalog.map((entry) => (
<EasterEggCard
key={entry.id}
entry={entry}
action={renderEasterEggAction(entry, {
registerOpen,
setRegisterOpen,
islanderOpen,
})}
/>
))}
</div>
</div>
)
}