Files
tgex-frontend/components/target-channel-selector.tsx

280 lines
9.5 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";
import React, { useState, useEffect, useRef } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { useTargetChannel } from "@/components/providers/target-channel-provider";
import {
Loader2,
AlertCircle,
Plus,
ExternalLink,
CheckCircle2,
ArrowUpRightIcon,
} from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { BOT_USERNAME } from "@/lib/utils/constants";
import { channelsApi } from "@/lib/api";
export const TargetChannelSelector: React.FC = () => {
const { channels, selectedChannel, setSelectedChannel, isLoading, error } =
useTargetChannel();
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [initialChannelCount, setInitialChannelCount] = useState(0);
const pollingIntervalRef = useRef<NodeJS.Timeout | null>(null);
// Очистка интервала при размонтировании
useEffect(() => {
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
};
}, []);
// Автоматический запуск polling при открытии диалога
useEffect(() => {
if (isAddDialogOpen && !isSuccess && initialChannelCount > 0) {
// Polling каждую секунду
pollingIntervalRef.current = setInterval(() => {
checkForNewChannels();
}, 1000);
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
};
}
}, [isAddDialogOpen, isSuccess, initialChannelCount]);
const checkForNewChannels = async () => {
try {
const response = await channelsApi.list();
const activeChannels = response.target_channels.filter(
(c) => c.is_active
);
if (activeChannels.length > initialChannelCount) {
// Новый канал добавлен!
setIsSuccess(true);
// Останавливаем polling
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
// Закрываем диалог через 2 секунды
setTimeout(() => {
setIsAddDialogOpen(false);
setIsSuccess(false);
// Обновляем список каналов через провайдер
window.location.reload();
}, 2000);
}
} catch (err) {
console.error("Error polling channels:", err);
}
};
const handleAddChannel = () => {
setInitialChannelCount(channels.length);
setIsSuccess(false);
setIsAddDialogOpen(true);
};
const handleDialogClose = () => {
if (!isSuccess) {
// Не даем закрыть диалог во время ожидания
return;
}
setIsAddDialogOpen(false);
setIsSuccess(false);
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
pollingIntervalRef.current = null;
}
};
if (isLoading) {
return (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
Загрузка каналов...
</div>
);
}
if (error) {
return (
<Alert variant="destructive" className="p-2 h-auto">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-xs">Ошибка: {error}</AlertDescription>
</Alert>
);
}
if (channels.length === 0) {
return (
<Button variant="outline" size="sm" onClick={handleAddChannel}>
<Plus className="h-4 w-4 mr-2" />
Добавить канал
</Button>
);
}
return (
<>
<Select
value={selectedChannel?.id || "add-new"}
onValueChange={(value) => {
if (value === "add-new") {
handleAddChannel();
} else {
const channel = channels.find((c) => c.id === value);
if (channel) {
setSelectedChannel(channel);
}
}
}}
>
<SelectTrigger className="w-[260px] h-auto min-h-[52px] py-2 rounded-lg">
<SelectValue placeholder="Выберите канал">
{selectedChannel && (
<div className="flex flex-col items-start gap-0.5">
<span className="font-semibold text-base">
{selectedChannel.title}
</span>
{selectedChannel.username && (
<span className="text-xs text-muted-foreground -mt-1">
@{selectedChannel.username}
</span>
)}
</div>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{channels.map((channel) => (
<SelectItem
key={channel.id}
value={channel.id}
className="cursor-pointer py-3"
>
<div className="flex items-center justify-between w-full gap-2">
<div className="flex flex-col items-start flex-1 min-w-0 gap-0.5">
<span className="font-semibold text-base truncate w-full">
{channel.title}
</span>
{channel.username && (
<span className="text-xs text-muted-foreground flex items-center gap-1">
@{channel.username}
</span>
)}
</div>
</div>
</SelectItem>
))}
<SelectItem
value="add-new"
className="cursor-pointer border-t mt-1 py-3"
>
<div className="flex items-center gap-2 text-primary">
<Plus className="h-4 w-4" />
<span className="font-medium">Добавить канал</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<Dialog open={isAddDialogOpen} onOpenChange={handleDialogClose}>
<DialogContent
className="sm:max-w-md"
onInteractOutside={(e) => !isSuccess && e.preventDefault()}
>
<DialogHeader>
<DialogTitle>Добавить целевой канал</DialogTitle>
<DialogDescription>
Следуйте инструкциям ниже, чтобы подключить новый канал
</DialogDescription>
</DialogHeader>
{isSuccess ? (
<div className="flex flex-col items-center justify-center py-8 space-y-4">
<div className="rounded-full bg-green-100 dark:bg-green-900 p-3">
<CheckCircle2 className="h-8 w-8 text-green-600 dark:text-green-400" />
</div>
<div className="text-center">
<h3 className="text-lg font-semibold">
Канал успешно добавлен!
</h3>
<p className="text-sm text-muted-foreground mt-1">
Страница обновится автоматически...
</p>
</div>
</div>
) : (
<div className="space-y-4">
<Alert>
<AlertDescription className="space-y-2">
<p className="font-medium">Шаги для добавления канала:</p>
<ol className="list-decimal list-inside space-y-1.5 text-sm">
<li>Откройте свой Telegram канал</li>
<li>Перейдите в настройки канала Администраторы</li>
<li>
Добавьте бота{" "}
<a
href={`https://t.me/${BOT_USERNAME}`}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-primary hover:underline inline-flex items-center gap-1"
>
@{BOT_USERNAME}
<ExternalLink className="h-3 w-3" />
</a>{" "}
как администратора
</li>
<li>
Предоставьте боту права:{" "}
<span className="font-medium">
Публикация сообщений, Удаление сообщений
</span>
</li>
</ol>
</AlertDescription>
</Alert>
<div className="flex flex-col items-center justify-center py-6 space-y-3">
<Loader2 className="h-10 w-10 animate-spin text-primary" />
<p className="text-sm text-muted-foreground text-center">
Ждем добавления бота...
<br />
<span className="text-xs">
Как только вы добавите бота, канал автоматически появится
</span>
</p>
</div>
</div>
)}
</DialogContent>
</Dialog>
</>
);
};