Files
tgex-frontend/components/dialog-add-channel.tsx
2026-01-21 23:52:21 +03:00

220 lines
6.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 { useState } from "react";
import { Loader2, Plus, AlertCircle, CheckCircle2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { parseChannelInput } from "@/lib/utils/channel-parser";
import { channelsApi, placementsApi } from "@/lib/api";
import type { Placement } from "@/lib/types/api";
interface AddChannelDialogProps {
workspaceId: string;
projectId: string;
existingPlacements: Placement[];
onSuccess: () => void;
children?: React.ReactNode;
}
export function AddChannelDialog({
workspaceId,
projectId,
existingPlacements,
onSuccess,
children,
}: AddChannelDialogProps) {
const [open, setOpen] = useState(false);
const [inputValue, setInputValue] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const existingUsernames = new Set(
existingPlacements
.map((p) => p.channel.username?.toLowerCase())
.filter(Boolean)
);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSuccess(false);
const username = parseChannelInput(inputValue);
if (!username) {
setError("Не удалось распознать username из ссылки");
return;
}
if (existingUsernames.has(username.toLowerCase())) {
setError("Этот канал уже добавлен в проект");
return;
}
try {
setLoading(true);
const username = parseChannelInput(inputValue);
if (!username) {
setError("Не удалось распознать username из ссылки");
return;
}
if (existingUsernames.has(username.toLowerCase())) {
setError("Этот канал уже добавлен в проект");
return;
}
// Step 1: Find channel by username to get channel_id
const channelsResponse = await channelsApi.list({ username });
const channel = channelsResponse.items.find(
(c) => c.username?.toLowerCase() === username.toLowerCase()
);
if (!channel) {
setError("Канал не найден в каталоге");
return;
}
// Step 2: Create empty placement with channel_id
await placementsApi.createBulk(workspaceId, projectId, {
channels: [
{
channel_id: channel.id,
status: "Без статуса",
},
],
});
setSuccess(true);
setInputValue("");
// Close dialog after short delay
setTimeout(() => {
setOpen(false);
setSuccess(false);
onSuccess();
}, 1500);
} catch (err: any) {
console.error("Failed to add channel:", err);
setError(err.detail || "Не удалось добавить канал");
} finally {
setLoading(false);
}
};
const handleOpenChange = (isOpen: boolean) => {
if (!isOpen) {
setInputValue("");
setError(null);
setSuccess(false);
}
setOpen(isOpen);
};
const examples = [
"t.me/rian_ru",
"@rian_ru",
"telemetr.me/content/rian_ru",
"tgstat.ru/channel/@rian_ru",
];
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Добавить канал</DialogTitle>
<DialogDescription>
Введите ссылку или username канала для проверки и добавления в проект
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="channel-input">Ссылка или username канала</Label>
<Input
id="channel-input"
placeholder="https://t.me/rian_ru или @rian_ru"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
disabled={loading}
autoComplete="off"
/>
<p className="text-xs text-muted-foreground">
Поддерживаются: t.me, telemetr.me, tgstat.ru, а также username с @
</p>
</div>
{error && (
<div className="flex items-center gap-2 p-3 rounded-md bg-destructive/10 text-destructive text-sm">
<AlertCircle className="h-4 w-4 shrink-0" />
{error}
</div>
)}
{success && (
<div className="flex items-center gap-2 p-3 rounded-md bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400 text-sm">
<CheckCircle2 className="h-4 w-4 shrink-0" />
Канал успешно добавлен!
</div>
)}
<div className="space-y-2">
<p className="text-xs text-muted-foreground">Примеры:</p>
<div className="flex flex-wrap gap-2">
{examples.map((example) => (
<button
key={example}
type="button"
onClick={() => setInputValue(example)}
className="text-xs px-2 py-1 rounded-md bg-muted hover:bg-muted/80 transition-colors"
>
{example}
</button>
))}
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setOpen(false)}
disabled={loading}
>
Отмена
</Button>
<Button type="submit" disabled={loading || !inputValue.trim()}>
{loading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Проверка...
</>
) : (
<>
<Plus className="h-4 w-4 mr-2" />
Проверить и добавить
</>
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}