feat(38-03): create TelegramStep onboarding component

- BotFather numbered instructions (4-step setup guide)
- Token input with live validation via POST /api/telegram/token
- Success state showing connected bot username
- Error state with descriptive message
- Skip/Back/Next navigation; Next enabled only after validation
This commit is contained in:
Nexus Dev 2026-04-04 03:12:31 +00:00
parent 9959d1b77e
commit 713e92be0f

View file

@ -0,0 +1,157 @@
// [nexus] Telegram bridge onboarding step — BotFather guided setup with token validation
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
interface TelegramStepProps {
onNext: () => void;
onBack: () => void;
}
export function TelegramStep({ onNext, onBack }: TelegramStepProps) {
const [token, setToken] = useState("");
const [validating, setValidating] = useState(false);
const [botUsername, setBotUsername] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
async function handleValidate() {
if (!token.trim()) return;
setValidating(true);
setError(null);
setBotUsername(null);
try {
const res = await fetch("/api/telegram/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: token.trim() }),
});
if (res.ok) {
const data = await res.json();
setBotUsername(data.botUsername ?? data.bot_username ?? null);
} else {
let msg = "Invalid token";
try {
const data = await res.json();
if (data?.error) msg = data.error;
} catch {
// ignore parse errors
}
setError(msg);
}
} catch {
setError("Could not reach the server. Check your connection and try again.");
} finally {
setValidating(false);
}
}
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">Connect Telegram</h1>
<p className="text-sm text-muted-foreground">
Get instant notifications and interact with your agents via Telegram.
</p>
</div>
{/* BotFather instructions */}
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">Set up your bot in 4 steps:</p>
<ol className="flex flex-col gap-2 list-none pl-0">
{[
<>Open Telegram and search for <span className="font-mono text-xs bg-muted px-1 py-0.5 rounded">@BotFather</span></>,
<>Send <span className="font-mono text-xs bg-muted px-1 py-0.5 rounded">/newbot</span> and follow the prompts to create a bot</>,
<>Copy the bot token it looks like <span className="font-mono text-xs bg-muted px-1 py-0.5 rounded">123456:ABC-DEF...</span></>,
"Paste the token below and click Validate",
].map((instruction, i) => (
<li key={i} className="flex items-start gap-3 text-sm text-muted-foreground">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-foreground">
{i + 1}
</span>
<span className="mt-0.5">{instruction}</span>
</li>
))}
</ol>
</div>
{/* Token input */}
<div className="flex flex-col gap-2">
<label htmlFor="telegram-token" className="text-sm font-medium leading-none">
Bot token
</label>
<Input
id="telegram-token"
type="text"
placeholder="Paste bot token here"
value={token}
onChange={(e) => {
setToken(e.target.value);
setBotUsername(null);
setError(null);
}}
disabled={validating}
autoComplete="off"
className="font-mono text-sm"
/>
{/* Success state */}
{botUsername && (
<p className={cn("text-sm", "text-green-600 dark:text-green-400")}>
Connected to @{botUsername}
</p>
)}
{/* Error state */}
{error && (
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
{error}
</p>
)}
</div>
{/* Actions */}
<div className="flex flex-col gap-2">
<Button
type="button"
onClick={handleValidate}
disabled={!token.trim() || validating}
variant="outline"
className="w-full"
>
{validating ? "Validating…" : "Validate Token"}
</Button>
<Button
type="button"
onClick={onNext}
disabled={!botUsername}
className="w-full"
>
Continue
</Button>
<Button
type="button"
variant="ghost"
onClick={onNext}
className="w-full"
>
Skip
</Button>
<Button
type="button"
variant="ghost"
onClick={onBack}
className="w-full"
>
Back
</Button>
</div>
</div>
);
}