218 lines
6.4 KiB
TypeScript
218 lines
6.4 KiB
TypeScript
"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: Create channel via API to get channel_id
|
||
const createResponse = await channelsApi.create([{ username }]);
|
||
const channelId = createResponse.results[0]?.channel.id;
|
||
|
||
if (!channelId) {
|
||
setError("Не удалось получить ID канала");
|
||
return;
|
||
}
|
||
|
||
// Step 2: Create empty placement with channel_id
|
||
await placementsApi.createBulk(workspaceId, projectId, {
|
||
channels: [
|
||
{
|
||
channel_id: channelId,
|
||
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>
|
||
);
|
||
}
|