feat: all project setup
This commit is contained in:
338
app/(dashboard)/external-channels/import/page.tsx
Normal file
338
app/(dashboard)/external-channels/import/page.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
"use client";
|
||||
|
||||
// ============================================================================
|
||||
// External Channels Import Page
|
||||
// ============================================================================
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { DashboardHeader } from "@/components/layout/dashboard-header";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { externalChannelsApi } from "@/lib/api";
|
||||
import {
|
||||
FileUp,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
Download,
|
||||
ArrowLeft,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import type { ExternalChannelImportResponse } from "@/lib/types/api";
|
||||
|
||||
export default function ImportExternalChannelsPage() {
|
||||
const router = useRouter();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [result, setResult] = useState<ExternalChannelImportResponse | null>(
|
||||
null
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
const droppedFile = e.dataTransfer.files[0];
|
||||
if (droppedFile) {
|
||||
handleFileSelect(droppedFile);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = (selectedFile: File) => {
|
||||
// Проверка типа файла
|
||||
const validTypes = [
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"text/csv",
|
||||
];
|
||||
|
||||
if (
|
||||
!validTypes.includes(selectedFile.type) &&
|
||||
!selectedFile.name.match(/\.(xlsx|xls|csv)$/i)
|
||||
) {
|
||||
setError("Пожалуйста, выберите файл Excel (.xlsx, .xls) или CSV");
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = e.target.files?.[0];
|
||||
if (selectedFile) {
|
||||
handleFileSelect(selectedFile);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
setIsUploading(true);
|
||||
setError(null);
|
||||
|
||||
// Для примера используем первый целевой канал (tc1)
|
||||
// В реальном приложении нужно дать пользователю выбрать
|
||||
const importResult = await externalChannelsApi.import(file, "tc1");
|
||||
setResult(importResult);
|
||||
setFile(null);
|
||||
} catch (err: any) {
|
||||
setError(err?.error?.message || "Ошибка импорта файла");
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setFile(null);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleGoToChannels = () => {
|
||||
router.push("/external-channels");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
breadcrumbs={[
|
||||
{ label: "Главная", href: "/" },
|
||||
{ label: "Внешние каналы", href: "/external-channels" },
|
||||
{ label: "Импорт" },
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4 max-w-4xl mx-auto w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
Импорт внешних каналов
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Загрузите файл Excel с данными из Tgstat
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/external-channels">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Назад
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Формат файла</CardTitle>
|
||||
<CardDescription>
|
||||
Требования к структуре Excel файла
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p>Файл должен содержать следующие колонки:</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
||||
<li>
|
||||
<strong>Название</strong> - название канала
|
||||
</li>
|
||||
<li>
|
||||
<strong>Username</strong> - username канала (с @ или без)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Ссылка</strong> - ссылка на канал (t.me/...)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Подписчики</strong> - количество подписчиков (число)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Описание</strong> - краткое описание (опционально)
|
||||
</li>
|
||||
</ul>
|
||||
<div className="pt-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Скачать шаблон
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{!result ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Загрузка файла</CardTitle>
|
||||
<CardDescription>
|
||||
Перетащите файл или выберите его
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`
|
||||
border-2 border-dashed rounded-lg p-12 text-center transition-colors
|
||||
${
|
||||
isDragging
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-muted-foreground/25"
|
||||
}
|
||||
${file ? "bg-muted/50" : ""}
|
||||
`}
|
||||
>
|
||||
{file ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<FileUp className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{file.name}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{(file.size / 1024).toFixed(2)} KB
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleReset}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Выбрать другой файл
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-center">
|
||||
<FileUp className="h-12 w-12 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-medium">
|
||||
Перетащите файл сюда
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
или нажмите кнопку ниже
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="file-upload">
|
||||
<Button asChild variant="outline">
|
||||
<span>
|
||||
<FileUp className="mr-2 h-4 w-4" />
|
||||
Выбрать файл
|
||||
</span>
|
||||
</Button>
|
||||
</label>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
onChange={handleFileInputChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{file && (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button onClick={handleUpload} disabled={isUploading}>
|
||||
{isUploading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Импорт...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileUp className="mr-2 h-4 w-4" />
|
||||
Импортировать
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="h-6 w-6 text-green-500" />
|
||||
<CardTitle>Импорт завершен</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Результаты обработки файла</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-green-50 dark:bg-green-950/20">
|
||||
<span className="text-sm font-medium">
|
||||
Импортировано каналов
|
||||
</span>
|
||||
<Badge variant="default">{result.imported}</Badge>
|
||||
</div>
|
||||
{result.skipped > 0 && (
|
||||
<div className="flex items-center justify-between p-3 rounded-lg bg-yellow-50 dark:bg-yellow-950/20">
|
||||
<span className="text-sm font-medium">Пропущено</span>
|
||||
<Badge variant="secondary">{result.skipped}</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{result.errors.length > 0 && (
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<p className="font-medium mb-2">
|
||||
Обнаружены ошибки при импорте:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-sm">
|
||||
{result.errors.map((error, index) => (
|
||||
<li key={index}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button variant="outline" onClick={handleReset}>
|
||||
Импортировать еще
|
||||
</Button>
|
||||
<Button onClick={handleGoToChannels}>Перейти к каналам</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user