Update VPN proxy client behavior
Build and Deploy Gateway / build-and-push (push) Successful in 18s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-09 11:55:02 +03:00
parent 71ede44be0
commit 90433d7cd8
27 changed files with 770 additions and 254 deletions
@@ -55,22 +55,48 @@ function InstructionBlock({
open: boolean;
onToggle: () => void;
}) {
const [copyFeedback, setCopyFeedback] = useState<{ id: string; failed: boolean } | null>(null);
const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const [copyFeedback, setCopyFeedback] = useState<Record<string, { failed: boolean }>>({});
const [copyAnnouncement, setCopyAnnouncement] = useState<{ id: string; message: string } | null>(null);
const copyTimers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
const copyAttempts = useRef(new Map<string, object>());
useEffect(() => () => {
if (copyTimer.current) clearTimeout(copyTimer.current);
for (const timer of copyTimers.current.values()) clearTimeout(timer);
copyTimers.current.clear();
copyAttempts.current.clear();
}, []);
async function copyInstruction(action: InstructionCopyAction) {
if (copyTimer.current) clearTimeout(copyTimer.current);
const activeTimer = copyTimers.current.get(action.id);
if (activeTimer) clearTimeout(activeTimer);
const attempt = {};
copyAttempts.current.set(action.id, attempt);
let feedback: { failed: boolean };
try {
await copyText(action.text);
setCopyFeedback({ id: action.id, failed: false });
feedback = { failed: false };
} catch {
setCopyFeedback({ id: action.id, failed: true });
feedback = { failed: true };
}
copyTimer.current = setTimeout(() => setCopyFeedback(null), 800);
if (copyAttempts.current.get(action.id) !== attempt) return;
const announcement = {
id: action.id,
message: feedback.failed ? `Не удалось скопировать ${action.label}` : `${action.label} скопировано`,
};
const pendingTimer = copyTimers.current.get(action.id);
if (pendingTimer) clearTimeout(pendingTimer);
setCopyFeedback((current) => ({ ...current, [action.id]: feedback }));
setCopyAnnouncement(announcement);
copyTimers.current.set(action.id, setTimeout(() => {
setCopyFeedback((current) => {
const next = { ...current };
delete next[action.id];
return next;
});
setCopyAnnouncement((current) => current?.id === action.id ? null : current);
copyTimers.current.delete(action.id);
copyAttempts.current.delete(action.id);
}, 800));
}
return (
@@ -106,7 +132,7 @@ function InstructionBlock({
: <code>{block.code}</code>)}
{block.copies && <div className="client-instruction-copies">
{block.copies.map((action) => {
const feedback = copyFeedback?.id === action.id ? copyFeedback : null;
const feedback = copyFeedback[action.id];
return <div className="client-instruction-copy" key={action.id}>
<span>{action.label}</span>
<button
@@ -122,7 +148,7 @@ function InstructionBlock({
</div>;
})}
<span className="client-live-region" role="status" aria-live="polite">
{copyFeedback ? copyFeedback.failed ? 'Не удалось скопировать' : 'Скопировано' : ''}
{copyAnnouncement?.message || ''}
</span>
</div>}
{block.note && <p className="client-instruction-note">{block.note}</p>}