feat: add invite code validity controls
- Add invite code starts/expires fields across contracts, API, Spacetime bindings, and admin UI - Enforce pending/expired invite code redemption behavior and expose admin status - Add admin write-operation confirmation guard and documentation - Add invite code contract/runtime tests
This commit is contained in:
105
apps/admin-web/src/components/useAdminWriteConfirm.tsx
Normal file
105
apps/admin-web/src/components/useAdminWriteConfirm.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
|
||||
interface AdminWriteConfirmOptions {
|
||||
action: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
interface PendingConfirm extends AdminWriteConfirmOptions {
|
||||
resolve: (confirmed: boolean) => void;
|
||||
}
|
||||
|
||||
export function useAdminWriteConfirm() {
|
||||
const [pendingConfirm, setPendingConfirm] = useState<PendingConfirm | null>(null);
|
||||
const cancelButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const confirmWrite = useCallback((options: AdminWriteConfirmOptions) => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
setPendingConfirm((current) => {
|
||||
if (current) {
|
||||
current.resolve(false);
|
||||
}
|
||||
return {...options, resolve};
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
const closeConfirm = useCallback(
|
||||
(confirmed: boolean) => {
|
||||
const current = pendingConfirm;
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
setPendingConfirm(null);
|
||||
current.resolve(confirmed);
|
||||
},
|
||||
[pendingConfirm],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingConfirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
cancelButtonRef.current?.focus();
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
closeConfirm(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [closeConfirm, pendingConfirm]);
|
||||
|
||||
const confirmDialog = pendingConfirm ? (
|
||||
<div
|
||||
aria-modal="true"
|
||||
aria-labelledby="admin-write-confirm-title"
|
||||
className="admin-confirm-backdrop"
|
||||
role="dialog"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
closeConfirm(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<section className="admin-confirm-panel">
|
||||
<div className="admin-panel-heading">
|
||||
<h3 id="admin-write-confirm-title">确认操作</h3>
|
||||
<span>{pendingConfirm.action}</span>
|
||||
</div>
|
||||
<dl className="admin-info-list">
|
||||
<div>
|
||||
<dt>操作</dt>
|
||||
<dd>{pendingConfirm.action}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>对象</dt>
|
||||
<dd>{pendingConfirm.target}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="admin-confirm-warning">该操作会立即影响线上数据</div>
|
||||
<div className="admin-confirm-actions">
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
ref={cancelButtonRef}
|
||||
type="button"
|
||||
onClick={() => closeConfirm(false)}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="admin-danger-button"
|
||||
type="button"
|
||||
onClick={() => closeConfirm(true)}
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return {confirmWrite, confirmDialog};
|
||||
}
|
||||
Reference in New Issue
Block a user