149 lines
4.5 KiB
TypeScript
149 lines
4.5 KiB
TypeScript
|
|
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>
|
|||
|
|
)
|
|||
|
|
}
|