159 lines
4.4 KiB
TypeScript
159 lines
4.4 KiB
TypeScript
import {Save} from 'lucide-react';
|
|
import {FormEvent, useState} from 'react';
|
|
|
|
import {upsertProfileInviteCode} from '../api/adminApiClient';
|
|
import type {ProfileInviteCodeAdminResponse} from '../api/adminApiTypes';
|
|
import {handlePageError} from './pageUtils';
|
|
|
|
interface AdminInviteCodePageProps {
|
|
token: string;
|
|
result: ProfileInviteCodeAdminResponse | null;
|
|
onUnauthorized: (message?: string) => void;
|
|
onResultChange: (result: ProfileInviteCodeAdminResponse) => void;
|
|
}
|
|
|
|
export function AdminInviteCodePage({
|
|
token,
|
|
result,
|
|
onUnauthorized,
|
|
onResultChange,
|
|
}: AdminInviteCodePageProps) {
|
|
const [inviteCode, setInviteCode] = useState('');
|
|
const [metadataText, setMetadataText] = useState('{}');
|
|
const [errorMessage, setErrorMessage] = useState('');
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
async function handleSave(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
if (isSaving) {
|
|
return;
|
|
}
|
|
|
|
setErrorMessage('');
|
|
setIsSaving(true);
|
|
try {
|
|
const response = await upsertProfileInviteCode(token, {
|
|
inviteCode: inviteCode.trim(),
|
|
metadata: parseMetadata(metadataText),
|
|
});
|
|
onResultChange(response);
|
|
} catch (error: unknown) {
|
|
handlePageError(error, onUnauthorized, setErrorMessage);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section className="admin-page">
|
|
<div className="admin-page-heading">
|
|
<div>
|
|
<h2>邀请码</h2>
|
|
<p>注册链路预置码</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-two-column admin-two-column-wide">
|
|
<form className="admin-panel admin-form" onSubmit={handleSave}>
|
|
<label className="admin-field">
|
|
<span>Invite Code</span>
|
|
<input
|
|
autoComplete="off"
|
|
value={inviteCode}
|
|
onChange={(event) => setInviteCode(event.target.value)}
|
|
/>
|
|
</label>
|
|
|
|
<label className="admin-field">
|
|
<span>Metadata JSON</span>
|
|
<textarea
|
|
rows={10}
|
|
spellCheck={false}
|
|
value={metadataText}
|
|
onChange={(event) => setMetadataText(event.target.value)}
|
|
/>
|
|
</label>
|
|
|
|
{errorMessage ? (
|
|
<div className="admin-alert" role="status">
|
|
{errorMessage}
|
|
</div>
|
|
) : null}
|
|
|
|
<button
|
|
className="admin-primary-button"
|
|
disabled={isSaving || !inviteCode.trim() || !isMetadataReady(metadataText)}
|
|
type="submit"
|
|
>
|
|
<Save size={17} aria-hidden="true" />
|
|
<span>{isSaving ? '保存中' : '保存'}</span>
|
|
</button>
|
|
</form>
|
|
|
|
<section className="admin-panel admin-result-panel">
|
|
<div className="admin-panel-heading">
|
|
<h3>记录</h3>
|
|
<span>{result?.inviteCode ?? '-'}</span>
|
|
</div>
|
|
{result ? (
|
|
<dl className="admin-info-list">
|
|
<div>
|
|
<dt>User ID</dt>
|
|
<dd>{result.userId}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>邀请码</dt>
|
|
<dd>{result.inviteCode}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>创建</dt>
|
|
<dd>{result.createdAt}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>更新</dt>
|
|
<dd>{result.updatedAt}</dd>
|
|
</div>
|
|
<div>
|
|
<dt>Metadata</dt>
|
|
<dd>
|
|
<pre className="admin-code-block">
|
|
{JSON.stringify(result.metadata, null, 2)}
|
|
</pre>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
) : (
|
|
<div className="admin-empty-state">暂无记录</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function parseMetadata(value: string): Record<string, unknown> {
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
return {};
|
|
}
|
|
|
|
const parsed = JSON.parse(trimmed) as unknown;
|
|
if (!isRecord(parsed)) {
|
|
throw new Error('Metadata 必须是 JSON 对象');
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function isMetadataReady(value: string) {
|
|
try {
|
|
parseMetadata(value);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
}
|