feat: global platform v2 update

This commit is contained in:
ivannoskov
2025-12-29 10:12:13 +03:00
parent 8e6a1fa83f
commit f64cf02100
84 changed files with 11023 additions and 11486 deletions

View File

@@ -0,0 +1,366 @@
"use client";
// ============================================================================
// Creative Detail Page
// ============================================================================
import { useEffect, useState } from "react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import {
Loader2,
ArrowLeft,
Pencil,
Archive,
Trash2,
Copy,
Check,
ExternalLink,
} from "lucide-react";
import { DashboardHeader } from "@/components/layout/dashboard-header";
import { useWorkspace } from "@/components/providers/workspace-provider";
import { creativesApi } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import type { Creative } from "@/lib/types/api";
// ============================================================================
// Component
// ============================================================================
export default function CreativeDetailPage() {
const params = useParams();
const router = useRouter();
const searchParams = useSearchParams();
const workspaceId = params?.workspaceId as string;
const creativeId = params?.creativeId as string;
const { hasPermission } = useWorkspace();
const [creative, setCreative] = useState<Creative | null>(null);
const [loading, setLoading] = useState(true);
const [isEditing, setIsEditing] = useState(searchParams.get("edit") === "true");
const [isSaving, setIsSaving] = useState(false);
const [copied, setCopied] = useState(false);
// Edit form state
const [name, setName] = useState("");
const [text, setText] = useState("");
const [error, setError] = useState<string | null>(null);
const canWrite = hasPermission("placements_write");
useEffect(() => {
loadCreative();
}, [workspaceId, creativeId]);
const loadCreative = async () => {
try {
setLoading(true);
const data = await creativesApi.get(workspaceId, creativeId);
setCreative(data);
setName(data.name);
setText(data.text);
} catch (err) {
console.error("Failed to load creative:", err);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
if (!name.trim() || !text.trim()) {
setError("Заполните все поля");
return;
}
if (!text.includes("{invite_link}")) {
setError("Текст должен содержать {invite_link}");
return;
}
try {
setIsSaving(true);
setError(null);
const updated = await creativesApi.update(workspaceId, creativeId, {
name: name.trim(),
text: text.trim(),
});
setCreative(updated);
setIsEditing(false);
} catch (err: any) {
setError(err?.detail || "Не удалось сохранить");
} finally {
setIsSaving(false);
}
};
const handleArchive = async () => {
if (!creative) return;
try {
// Toggle archive status by updating via delete endpoint (soft delete)
await creativesApi.delete(workspaceId, creativeId);
router.push(`/dashboard/${workspaceId}/creatives`);
} catch (err) {
console.error("Failed to archive:", err);
}
};
const handleCopy = () => {
if (creative) {
navigator.clipboard.writeText(creative.text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleCancel = () => {
if (creative) {
setName(creative.name);
setText(creative.text);
}
setIsEditing(false);
setError(null);
};
if (loading) {
return (
<>
<DashboardHeader breadcrumbs={[{ label: "Загрузка..." }]} />
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</>
);
}
if (!creative) {
return (
<>
<DashboardHeader breadcrumbs={[{ label: "Ошибка" }]} />
<div className="flex flex-col items-center justify-center py-12">
<h2 className="text-xl font-semibold mb-2">Креатив не найден</h2>
<Button asChild>
<Link href={`/dashboard/${workspaceId}/creatives`}>
Вернуться к списку
</Link>
</Button>
</div>
</>
);
}
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: `/dashboard/${workspaceId}` },
{ label: "Креативы", href: `/dashboard/${workspaceId}/creatives` },
{ label: creative.name },
]}
/>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4 max-w-3xl">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={() => router.push(`/dashboard/${workspaceId}/creatives`)}
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold tracking-tight">
{creative.name}
</h1>
<Badge
variant={
creative.status === "active" ? "default" : "secondary"
}
>
{creative.status === "active" ? "Активен" : "Архив"}
</Badge>
</div>
<p className="text-muted-foreground">
{creative.placements_count} размещений
</p>
</div>
</div>
{canWrite && !isEditing && (
<div className="flex gap-2">
<Button variant="outline" onClick={() => setIsEditing(true)}>
<Pencil className="h-4 w-4 mr-2" />
Редактировать
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">
<Archive className="h-4 w-4 mr-2" />
В архив
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Архивировать креатив?</AlertDialogTitle>
<AlertDialogDescription>
Креатив будет перемещён в архив. Существующие размещения
останутся без изменений.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Отмена</AlertDialogCancel>
<AlertDialogAction onClick={handleArchive}>
Архивировать
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)}
</div>
{isEditing ? (
<Card>
<CardHeader>
<CardTitle>Редактирование</CardTitle>
<CardDescription>
Изменения не повлияют на существующие размещения
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Название</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
disabled={isSaving}
/>
</div>
<div className="space-y-2">
<Label htmlFor="text">Текст креатива</Label>
<Textarea
id="text"
value={text}
onChange={(e) => setText(e.target.value)}
disabled={isSaving}
rows={10}
className="font-mono text-sm"
/>
<p className="text-xs text-muted-foreground">
Не забудьте <code className="bg-muted px-1 rounded">{"{invite_link}"}</code>
</p>
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
<div className="flex gap-2 pt-4">
<Button
variant="outline"
onClick={handleCancel}
disabled={isSaving}
>
Отмена
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Сохранение...
</>
) : (
"Сохранить"
)}
</Button>
</div>
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Текст креатива</CardTitle>
<Button variant="outline" size="sm" onClick={handleCopy}>
{copied ? (
<>
<Check className="h-4 w-4 mr-2" />
Скопировано
</>
) : (
<>
<Copy className="h-4 w-4 mr-2" />
Копировать
</>
)}
</Button>
</div>
</CardHeader>
<CardContent>
<pre className="whitespace-pre-wrap text-sm bg-muted p-4 rounded-lg">
{creative.text.replace(
"{invite_link}",
"https://t.me/+AbCdEfGhIjK"
)}
</pre>
<p className="text-xs text-muted-foreground mt-2">
<code className="bg-muted px-1 rounded">{"{invite_link}"}</code>{" "}
заменяется уникальной ссылкой при создании размещения
</p>
</CardContent>
</Card>
)}
{/* Quick Actions */}
{canWrite && creative.status === "active" && (
<Card>
<CardHeader>
<CardTitle>Действия</CardTitle>
</CardHeader>
<CardContent>
<Button asChild>
<Link
href={`/dashboard/${workspaceId}/placements/new?creative_id=${creative.id}`}
>
<ExternalLink className="h-4 w-4 mr-2" />
Создать размещение с этим креативом
</Link>
</Button>
</CardContent>
</Card>
)}
</div>
</>
);
}