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:
parent
fa3529e784
commit
d9d6e4f657
1 changed files with 157 additions and 0 deletions
157
ui/src/components/onboarding/TelegramStep.tsx
Normal file
157
ui/src/components/onboarding/TelegramStep.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue