Pulse/frontend-modern/src/components/TokenRevealDialog.tsx
2025-10-15 22:45:14 +00:00

164 lines
6.5 KiB
TypeScript

import { Show, createSignal, createEffect, onCleanup } from 'solid-js';
import type { Component } from 'solid-js';
import { Card } from '@/components/shared/Card';
import { copyToClipboard } from '@/utils/clipboard';
import { showError, showSuccess } from '@/utils/toast';
import { useTokenRevealState, dismissTokenReveal } from '@/stores/tokenReveal';
const formatSourceLabel = (value?: string) => {
if (!value) return '';
return value
.split(/[-_\s]+/)
.filter(Boolean)
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
.join(' ');
};
export const TokenRevealDialog: Component = () => {
const state = useTokenRevealState();
const [copied, setCopied] = createSignal(false);
const handleDismiss = () => {
dismissTokenReveal();
setCopied(false);
};
const handleCopy = async (token: string) => {
const success = await copyToClipboard(token);
if (success) {
setCopied(true);
showSuccess('Token copied to clipboard');
setTimeout(() => setCopied(false), 2000);
} else {
showError('Failed to copy token');
}
};
createEffect(() => {
const current = state();
setCopied(false);
if (!current) {
return;
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
handleDismiss();
}
};
window.addEventListener('keydown', onKeyDown);
onCleanup(() => window.removeEventListener('keydown', onKeyDown));
});
return (
<Show when={state()}>
{(tokenInfo) => {
const info = tokenInfo();
const sourceLabel = formatSourceLabel(info.source);
const recordName = info.record?.name?.trim() || 'Untitled token';
const hint =
info.note ||
'Copy this token now; Pulse will not display it again after you close this dialog.';
const tokenHint =
info.record?.prefix && info.record?.suffix
? `${info.record.prefix}${info.record.suffix}`
: info.record?.prefix
? `${info.record.prefix}`
: info.record?.suffix
? `${info.record.suffix}`
: null;
return (
<div class="fixed inset-0 z-[60] flex items-center justify-center px-4 py-6">
<div
class="absolute inset-0 bg-gray-900/70 backdrop-blur-sm"
role="presentation"
onClick={handleDismiss}
/>
<div class="relative z-[61] w-full max-w-xl">
<Card padding="lg" class="shadow-2xl relative">
<button
type="button"
class="absolute top-3 right-3 rounded-md px-2 py-1 text-xs font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
onClick={handleDismiss}
aria-label="Close token dialog"
>
Close
</button>
<div class="flex items-start gap-3">
<div class="flex-shrink-0 rounded-full bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300 p-2">
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div class="space-y-2 flex-1">
<div class="flex flex-wrap items-center gap-2">
<h2 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
API token ready
</h2>
<Show when={sourceLabel}>
<span class="inline-flex items-center rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide">
{sourceLabel}
</span>
</Show>
</div>
<p class="text-sm text-gray-700 dark:text-gray-300 leading-snug">{hint}</p>
</div>
</div>
<div class="mt-5 space-y-3">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Token value
</div>
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
<code class="flex-1 rounded-lg border border-green-300 dark:border-green-700 bg-white dark:bg-gray-900 px-4 py-3 font-mono text-base font-semibold text-gray-900 dark:text-gray-100 break-all">
{info.token}
</code>
<button
type="button"
onClick={() => handleCopy(info.token)}
class="inline-flex items-center justify-center rounded-lg bg-green-600 hover:bg-green-700 text-white text-sm font-semibold px-4 py-2 transition-colors shadow-sm"
>
{copied() ? 'Copied!' : 'Copy token'}
</button>
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
Label: <span class="font-semibold text-gray-800 dark:text-gray-200">{recordName}</span>
<Show when={tokenHint}>
<span>
{' '}
· Hint:{' '}
<code class="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 font-mono text-[11px] text-gray-600 dark:text-gray-400">
{tokenHint as string}
</code>
</span>
</Show>
</div>
</div>
<div class="mt-6 flex justify-end">
<button
type="button"
onClick={handleDismiss}
class="rounded-md border border-gray-300 dark:border-gray-600 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Dismiss
</button>
</div>
</Card>
</div>
</div>
);
}}
</Show>
);
};
export default TokenRevealDialog;