import { useState, useEffect } from 'react'; import type { ChannelAccount } from '../types'; function StatusDot({ enabled }: { enabled: boolean }) { return (
{enabled ? 'Enabled' : 'Disabled'}
); } function PlatformIcon({ platform }: { platform: ChannelAccount['platform'] }) { return {platform === 'telegram' ? '✈️' : '🎮'}; } interface AddChannelFormProps { onSave: (data: { platform: 'telegram' | 'discord'; bot_token: string; allowed_users: string[] }) => Promise; onCancel: () => void; } function AddChannelForm({ onSave, onCancel }: AddChannelFormProps) { const [platform, setPlatform] = useState<'telegram' | 'discord'>('telegram'); const [botToken, setBotToken] = useState(''); const [allowedUsers, setAllowedUsers] = useState(''); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!botToken.trim()) { setError('Bot token is required'); return; } setSaving(true); setError(''); try { await onSave({ platform, bot_token: botToken.trim(), allowed_users: allowedUsers .split(',') .map((u) => u.trim()) .filter(Boolean) }); } catch (err) { setError(`Save failed: ${(err as Error).message}`); } finally { setSaving(false); } }; return (

Add Channel

{(['telegram', 'discord'] as const).map((p) => ( ))}
setBotToken(e.target.value)} placeholder={ platform === 'telegram' ? '1234567890:ABCDef...' : 'Bot token from Discord Developer Portal' } className="input-base w-full font-mono" />

{platform === 'telegram' ? 'Get a token from @BotFather on Telegram' : 'Create an app at discord.com/developers, add a Bot'}

setAllowedUsers(e.target.value)} placeholder="user1, user2" className="input-base w-full" />
{error &&

{error}

}
); } interface ChannelCardProps { account: ChannelAccount; bridgeStatus: boolean; onToggle: (id: string, enabled: boolean) => void; onDelete: (id: string) => void; onStartStop: (id: string, action: 'start' | 'stop') => void; onUpdate: (id: string, allowed_users: string[]) => void; } function ChannelCard({ account, bridgeStatus, onToggle, onDelete, onStartStop, onUpdate }: ChannelCardProps) { const [confirmDelete, setConfirmDelete] = useState(false); const [editing, setEditing] = useState(false); const [editUsers, setEditUsers] = useState(account.allowed_users.join(', ')); const handleSaveEdit = () => { const users = editUsers.split(',').map(s => s.trim()).filter(Boolean); onUpdate(account.id, users); setEditing(false); }; return (

{account.platform}

{account.id.slice(0, 8)} {bridgeStatus ? ( Connected ) : account.enabled ? ( Disconnected ) : null}
{/* Allowed users — view or edit mode */} {editing ? (
setEditUsers(e.target.value)} className="input-base w-full text-xs" placeholder="e.g. 123456789, username, chat_id" />
) : (
{account.allowed_users.length > 0 ? (

Allowed: {account.allowed_users.join(', ')}

) : (

All users allowed (no filter)

)}
)}

Added: {new Date(account.created_at).toLocaleDateString()}

{/* Start/Stop bridge */} {account.enabled && ( bridgeStatus ? ( ) : ( ) )} {/* Enable/disable toggle */}
); } export default function ChannelsPage() { const [accounts, setAccounts] = useState([]); const [showForm, setShowForm] = useState(false); const [loading, setLoading] = useState(true); const [bridgeStatus, setBridgeStatus] = useState>({}); const fetchStatus = () => { fetch('/api/channels/status') .then(r => r.json()) .then((data: Record) => { const s: Record = {}; for (const [k, v] of Object.entries(data)) s[k] = v.running; setBridgeStatus(s); }) .catch(() => {}); }; useEffect(() => { fetch('/api/channels') .then((r) => r.json()) .then((data: ChannelAccount[]) => { if (Array.isArray(data)) setAccounts(data); }) .catch(console.error) .finally(() => setLoading(false)); fetchStatus(); const interval = setInterval(fetchStatus, 5000); return () => clearInterval(interval); }, []); const addAccount = async (data: { platform: 'telegram' | 'discord'; bot_token: string; allowed_users: string[]; }) => { const res = await fetch('/api/channels', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const created: ChannelAccount = await res.json(); setAccounts((prev) => [...prev, created]); setShowForm(false); }; const toggleAccount = async (id: string, enabled: boolean) => { setAccounts((prev) => prev.map((a) => (a.id === id ? { ...a, enabled } : a))); try { // The server doesn't have a PATCH endpoint — delete and re-add isn't viable here. // We optimistically update the UI. The server will need a PATCH endpoint. // For now, just send the update and handle gracefully. await fetch(`/api/channels/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }) }); } catch { // Revert on error setAccounts((prev) => prev.map((a) => (a.id === id ? { ...a, enabled: !enabled } : a))); } }; const handleStartStop = async (id: string, action: 'start' | 'stop') => { try { await fetch(`/api/channels/${id}/${action}`, { method: 'POST' }); fetchStatus(); } catch {} }; const handleUpdateUsers = async (id: string, allowed_users: string[]) => { try { const res = await fetch(`/api/channels/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ allowed_users }), }); if (res.ok) { setAccounts(prev => prev.map(a => a.id === id ? { ...a, allowed_users } : a)); } } catch {} }; const deleteAccount = async (id: string) => { setAccounts((prev) => prev.filter((a) => a.id !== id)); try { await fetch(`/api/channels/${id}`, { method: 'DELETE' }); } catch { // Silently ignore — optimistic removal already applied } }; return (
{/* Header */}

Channels

Connect Telegram and Discord bots to receive messages

{!showForm && ( )}
{/* Add form */} {showForm && ( setShowForm(false)} /> )} {/* Account list */} {loading ? (
Loading channels...
) : accounts.length === 0 ? (
📡

No channels configured yet

Add a Telegram or Discord bot to receive messages

) : ( accounts.map((account) => ( )) )} {/* Help */} {!loading && (

Setup Guide

  • Telegram: Message @BotFather, use /newbot, copy the token
  • Discord: Visit discord.com/developers, create an Application, add a Bot, copy the token
)}
); }