222 lines
7.7 KiB
TypeScript
222 lines
7.7 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
||
import { useNavigate } from 'react-router-dom'
|
||
import { useOnboarding } from '../onboarding/OnboardingContext'
|
||
import { useThreadsAccount } from '../threads/ThreadsAccountContext'
|
||
import {
|
||
threadsAccountConnected,
|
||
threadsAccountLabel,
|
||
threadsAccountStatusLabel,
|
||
} from '../lib/threadsAccount'
|
||
import { ApiError } from '../api/client'
|
||
import { Button, ErrorText, Field, Input } from './ui'
|
||
import { AcIcon } from './AcIcon'
|
||
|
||
function AccountAvatar({
|
||
connected,
|
||
size = 'md',
|
||
}: {
|
||
connected: boolean
|
||
size?: 'sm' | 'md'
|
||
}) {
|
||
const iconWrap = size === 'sm' ? '!h-7 !w-7' : '!h-8 !w-8'
|
||
return (
|
||
<span
|
||
className={`ac-account-avatar ac-account-avatar--${size} ${connected ? 'ac-account-avatar--online' : ''}`}
|
||
>
|
||
<AcIcon app="threads" size="sm" className={`${iconWrap} !border-0 !bg-transparent`} />
|
||
</span>
|
||
)
|
||
}
|
||
|
||
export function AccountSwitcher() {
|
||
const navigate = useNavigate()
|
||
const { accounts, activeAccountId, activeAccount, loading, switchAccount, createAccount } =
|
||
useThreadsAccount()
|
||
const { nextStep, refresh: refreshOnboarding } = useOnboarding()
|
||
const accountGuideActive = !loading && nextStep === 'account'
|
||
const [open, setOpen] = useState(false)
|
||
const [panel, setPanel] = useState<'list' | 'create'>('list')
|
||
const [creating, setCreating] = useState(false)
|
||
const [displayName, setDisplayName] = useState('')
|
||
const [error, setError] = useState('')
|
||
const rootRef = useRef<HTMLDivElement>(null)
|
||
|
||
useEffect(() => {
|
||
if (!open) return
|
||
const onPointer = (event: MouseEvent) => {
|
||
if (!rootRef.current?.contains(event.target as Node)) {
|
||
setOpen(false)
|
||
setPanel('list')
|
||
}
|
||
}
|
||
const onKey = (event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') {
|
||
if (panel === 'create') {
|
||
setPanel('list')
|
||
setError('')
|
||
return
|
||
}
|
||
setOpen(false)
|
||
}
|
||
}
|
||
document.addEventListener('mousedown', onPointer)
|
||
document.addEventListener('keydown', onKey)
|
||
return () => {
|
||
document.removeEventListener('mousedown', onPointer)
|
||
document.removeEventListener('keydown', onKey)
|
||
}
|
||
}, [open, panel])
|
||
|
||
const connected = threadsAccountConnected(activeAccount)
|
||
const label = loading ? '載入中' : threadsAccountLabel(activeAccount)
|
||
|
||
function openCreatePanel() {
|
||
setPanel('create')
|
||
setDisplayName('')
|
||
setError('')
|
||
}
|
||
|
||
async function pickAccount(id: string) {
|
||
setOpen(false)
|
||
setPanel('list')
|
||
await switchAccount(id)
|
||
const snapshot = await refreshOnboarding()
|
||
navigate(snapshot.isComplete ? `/threads/${id}/publish` : '/settings')
|
||
}
|
||
|
||
async function handleCreate() {
|
||
setCreating(true)
|
||
setError('')
|
||
try {
|
||
const created = await createAccount({
|
||
displayName: displayName.trim() || undefined,
|
||
})
|
||
setOpen(false)
|
||
setPanel('list')
|
||
const snapshot = await refreshOnboarding()
|
||
navigate(snapshot.isComplete ? `/threads/${created.id}/publish` : '/settings')
|
||
} catch (e) {
|
||
setError(e instanceof ApiError ? e.message : '建立帳號失敗')
|
||
} finally {
|
||
setCreating(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div ref={rootRef} className="ac-account-switcher relative">
|
||
{accountGuideActive && !open ? (
|
||
<span className="hx-guide-badge" aria-hidden>
|
||
先建立帳號
|
||
</span>
|
||
) : null}
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setOpen((v) => !v)
|
||
if (open) setPanel('list')
|
||
}}
|
||
className={`ac-account-trigger ${accountGuideActive ? 'hx-guide-glow ac-account-trigger--prompt' : ''}`}
|
||
aria-expanded={open}
|
||
aria-haspopup="listbox"
|
||
aria-label={`經營帳號:${label}${accountGuideActive ? '(入門下一步:建立經營帳號)' : ''}`}
|
||
>
|
||
<AccountAvatar connected={connected} />
|
||
<span className="ac-account-trigger-text hidden min-w-0 max-w-[7.5rem] truncate sm:inline">
|
||
{label}
|
||
</span>
|
||
<span className="ac-account-trigger-chevron" aria-hidden>
|
||
{open ? '▴' : '▾'}
|
||
</span>
|
||
</button>
|
||
|
||
{open ? (
|
||
<div role="listbox" className="ac-account-menu">
|
||
{panel === 'list' ? (
|
||
<>
|
||
<p className="ac-account-menu-title">經營帳號</p>
|
||
<div className="ac-account-menu-list">
|
||
{accounts.length ? (
|
||
accounts.map((account) => {
|
||
const isActive = account.id === activeAccountId
|
||
const itemConnected = threadsAccountConnected(account)
|
||
return (
|
||
<button
|
||
key={account.id}
|
||
type="button"
|
||
role="option"
|
||
aria-selected={isActive}
|
||
onClick={() => pickAccount(account.id)}
|
||
className={`ac-account-menu-item ${isActive ? 'ac-account-menu-item--active' : ''}`}
|
||
>
|
||
<AccountAvatar connected={itemConnected} size="sm" />
|
||
<span className="min-w-0 flex-1 text-left">
|
||
<span className="block truncate text-sm font-semibold">
|
||
{threadsAccountLabel(account)}
|
||
</span>
|
||
<span className="block truncate text-xs text-muted">
|
||
{threadsAccountStatusLabel(account)}
|
||
</span>
|
||
</span>
|
||
{isActive ? (
|
||
<span className="ac-account-menu-pill">使用中</span>
|
||
) : null}
|
||
</button>
|
||
)
|
||
})
|
||
) : (
|
||
<p className="px-3 py-5 text-center text-sm text-muted">尚未建立 Threads 帳號</p>
|
||
)}
|
||
</div>
|
||
<div className="ac-account-menu-footer">
|
||
<Button
|
||
type="button"
|
||
variant="soft"
|
||
className="w-full rounded-[var(--radius-pill)]"
|
||
onClick={openCreatePanel}
|
||
>
|
||
新增經營帳號
|
||
</Button>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="ac-account-create-panel">
|
||
<div className="ac-account-create-head">
|
||
<button
|
||
type="button"
|
||
className="ac-account-create-back"
|
||
onClick={() => {
|
||
setPanel('list')
|
||
setError('')
|
||
}}
|
||
>
|
||
← 返回
|
||
</button>
|
||
<p className="ac-account-menu-title !px-0 !pt-0">新增經營帳號</p>
|
||
</div>
|
||
<p className="ac-account-create-hint">建立後進入連線設定;人設請到側欄「人設庫」建立並綁定。</p>
|
||
<div className="ac-account-create-form">
|
||
<Field label="顯示名稱(選填)">
|
||
<Input
|
||
value={displayName}
|
||
onChange={(e) => setDisplayName(e.target.value)}
|
||
placeholder="側欄顯示用,不填則自動命名"
|
||
autoFocus
|
||
/>
|
||
</Field>
|
||
<ErrorText message={error} />
|
||
<Button
|
||
type="button"
|
||
className="w-full rounded-[var(--radius-pill)]"
|
||
disabled={creating}
|
||
onClick={handleCreate}
|
||
>
|
||
{creating ? '建立中…' : '建立並設定連線'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
)
|
||
} |