Files
tgex-frontend/app/(dashboard)/channels/page.tsx
2025-11-19 12:56:04 +03:00

250 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
// ============================================================================
// Target Channels List Page
// ============================================================================
import React, { useEffect, useState } from "react";
import Link from "next/link";
import { DashboardHeader } from "@/components/layout/dashboard-header";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { channelsApi } from "@/lib/api";
import { formatNumber, formatMetric, formatUsername } from "@/lib/utils/format";
import {
Target,
Users,
TrendingUp,
ExternalLink as ExternalLinkIcon,
Loader2,
AlertCircle,
Plus,
Eye,
} from "lucide-react";
import type { TargetChannel } from "@/lib/types/api";
import { Alert, AlertDescription } from "@/components/ui/alert";
export default function ChannelsPage() {
const [channels, setChannels] = useState<TargetChannel[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<"all" | "active" | "inactive">(
"all"
);
useEffect(() => {
const loadChannels = async () => {
try {
setLoading(true);
const response = await channelsApi.list();
setChannels(response.target_channels);
} catch (err: any) {
setError(
err?.error?.message || err?.detail || "Ошибка загрузки каналов"
);
} finally {
setLoading(false);
}
};
loadChannels();
}, []);
const filteredChannels = channels.filter((channel) => {
if (activeTab === "active") return channel.is_active;
if (activeTab === "inactive") return !channel.is_active;
return true;
});
const handleDisconnect = async (id: string, title: string) => {
if (!confirm(`Вы уверены, что хотите отключить канал "${title}"?`)) {
return;
}
try {
await channelsApi.delete(id);
setChannels((prev) => prev.filter((ch) => ch.id !== id));
} catch (err: any) {
alert(err?.error?.message || err?.detail || "Ошибка отключения канала");
}
};
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: "/" },
{ label: "Целевые каналы" },
]}
/>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
Целевые каналы
</h1>
<p className="text-muted-foreground mt-1">
Каналы, в которые вы приводите подписчиков
</p>
</div>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Подключение нового канала</CardTitle>
<CardDescription>
Добавьте бота в свой Telegram-канал с правами администратора
</CardDescription>
</div>
<Button asChild>
<a
href="https://t.me/tgex_bot?startgroup=admin"
target="_blank"
rel="noopener noreferrer"
>
<Plus className="mr-2 h-4 w-4" />
Подключить канал
</a>
</Button>
</div>
</CardHeader>
<CardContent>
<div className="rounded-lg border bg-muted/50 p-4">
<ol className="list-decimal list-inside space-y-2 text-sm">
<li>Откройте свой Telegram-канал</li>
<li>Добавьте @tgex_bot в администраторы канала</li>
<li>
Предоставьте боту права:{" "}
<strong>создавать инвайт-ссылки</strong> и{" "}
<strong>видеть вступления</strong>
</li>
<li>Канал автоматически появится в списке ниже</li>
</ol>
</div>
</CardContent>
</Card>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as any)}
className="w-full"
>
<TabsList>
<TabsTrigger value="all">Все ({channels.length})</TabsTrigger>
<TabsTrigger value="active">
Активные ({channels.filter((c) => c.is_active).length})
</TabsTrigger>
<TabsTrigger value="inactive">
Отключенные ({channels.filter((c) => !c.is_active).length})
</TabsTrigger>
</TabsList>
<TabsContent value={activeTab} className="mt-4">
{loading ? (
<div className="flex items-center justify-center p-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : filteredChannels.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center p-12">
<Target className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-lg font-medium text-muted-foreground">
{activeTab === "all" && "Нет подключенных каналов"}
{activeTab === "active" && "Нет активных каналов"}
{activeTab === "inactive" && "Нет отключенных каналов"}
</p>
<p className="text-sm text-muted-foreground mt-1">
Добавьте бота в свой канал, чтобы начать работу
</p>
</CardContent>
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredChannels.map((channel) => (
<Card
key={channel.id}
className="hover:shadow-md transition-shadow"
>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<CardTitle className="truncate flex items-center gap-2">
<Target className="h-4 w-4 shrink-0" />
{channel.title}
</CardTitle>
<CardDescription className="truncate mt-1">
{channel.username ? (
<a
href={`https://t.me/${channel.username.replace(
"@",
""
)}`}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{formatUsername(channel.username)}
</a>
) : (
<span className="text-muted-foreground">
Приватный канал
</span>
)}
</CardDescription>
</div>
<Badge
variant={channel.is_active ? "default" : "secondary"}
>
{channel.is_active ? "Активен" : "Отключен"}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="text-sm text-muted-foreground">
Telegram ID:{" "}
<code className="text-xs bg-muted px-1 rounded">
{channel.telegram_id}
</code>
</div>
<div className="flex gap-2 pt-2">
<Button
variant="outline"
size="sm"
onClick={() =>
handleDisconnect(channel.id, channel.title)
}
className="flex-1"
>
Отключить канал
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
</Tabs>
</div>
</>
);
}