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