From 1826483750bddf70434d5c48acccc3af6ddc4e40 Mon Sep 17 00:00:00 2001 From: ivannoskov Date: Mon, 9 Feb 2026 13:10:08 +0300 Subject: [PATCH] feat: platform update --- app/dashboard/[workspaceId]/channels/page.tsx | 108 ++- .../[workspaceId]/creatives/new/page.tsx | 255 ------ .../[workspaceId]/creatives/page.tsx | 101 +-- .../placements/[placementId]/page.tsx | 408 --------- .../[workspaceId]/placements/new/page.tsx | 414 --------- .../[workspaceId]/placements/page.tsx | 784 ------------------ .../purchase-plans/[projectId]/page.tsx | 75 +- components/channel-input.tsx | 317 +++++++ components/dialog-add-channel.tsx | 217 ----- components/dialog-create-placements.tsx | 591 ------------- components/placement-wizard.tsx | 412 +++++++++ lib/api/channels.ts | 7 +- lib/utils/channel-parser.ts | 10 +- lib/utils/constants.ts | 3 - public/icons/telegram.svg | 1 + 15 files changed, 907 insertions(+), 2796 deletions(-) delete mode 100644 app/dashboard/[workspaceId]/creatives/new/page.tsx delete mode 100644 app/dashboard/[workspaceId]/placements/[placementId]/page.tsx delete mode 100644 app/dashboard/[workspaceId]/placements/new/page.tsx delete mode 100644 app/dashboard/[workspaceId]/placements/page.tsx create mode 100644 components/channel-input.tsx delete mode 100644 components/dialog-add-channel.tsx delete mode 100644 components/dialog-create-placements.tsx create mode 100644 components/placement-wizard.tsx create mode 100644 public/icons/telegram.svg diff --git a/app/dashboard/[workspaceId]/channels/page.tsx b/app/dashboard/[workspaceId]/channels/page.tsx index 92ba86c..fe5863f 100644 --- a/app/dashboard/[workspaceId]/channels/page.tsx +++ b/app/dashboard/[workspaceId]/channels/page.tsx @@ -45,7 +45,22 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import type { Channel } from "@/lib/types/api"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useWorkspace } from "@/components/providers/workspace-provider"; +import type { Channel, Project } from "@/lib/types/api"; // ============================================================================ // Helpers @@ -70,6 +85,7 @@ type SortOrder = "asc" | "desc" | null; export default function ChannelsCatalogPage() { const params = useParams(); const workspaceId = params?.workspaceId as string; + const { currentProject, projects } = useWorkspace(); const [channels, setChannels] = useState([]); const [loading, setLoading] = useState(true); @@ -78,6 +94,11 @@ export default function ChannelsCatalogPage() { const [sortField, setSortField] = useState(null); const [sortOrder, setSortOrder] = useState(null); + // Placement dialog + const [selectedChannel, setSelectedChannel] = useState(null); + const [showPlacementDialog, setShowPlacementDialog] = useState(false); + const [placementProjectId, setPlacementProjectId] = useState(""); + useEffect(() => { const loadChannels = async () => { try { @@ -126,6 +147,23 @@ export default function ChannelsCatalogPage() { return ; }; + const handleOpenPlacement = (channel: Channel) => { + setSelectedChannel(channel); + if (currentProject) { + setPlacementProjectId(currentProject.id); + } + setShowPlacementDialog(true); + }; + + const handleConfirmPlacement = () => { + if (!placementProjectId || !selectedChannel) return; + window.location.href = `/dashboard/${workspaceId}/purchase-plans/${placementProjectId}?openPlacement=true&channel_id=${selectedChannel.id}`; + }; + + const activeProjects = useMemo(() => { + return projects.filter(p => p.status === "active"); + }, [projects]); + // Filter and sort channels const filteredChannels = useMemo(() => { let result = [...channels]; @@ -317,10 +355,8 @@ export default function ChannelsCatalogPage() { - @@ -361,23 +397,65 @@ export default function ChannelsCatalogPage() { {channel.description}

)} - + + ))} )} + + + + + Создание размещения + + Выберите проект для размещения на канале {selectedChannel?.title} + + +
+
+ + + {activeProjects.length === 0 && ( +

+ Нет активных проектов. Создайте проект в настройках. +

+ )} +
+
+ + +
+
+
+
); } diff --git a/app/dashboard/[workspaceId]/creatives/new/page.tsx b/app/dashboard/[workspaceId]/creatives/new/page.tsx deleted file mode 100644 index ecf4631..0000000 --- a/app/dashboard/[workspaceId]/creatives/new/page.tsx +++ /dev/null @@ -1,255 +0,0 @@ -"use client"; - -// ============================================================================ -// Create Creative Page -// ============================================================================ - -import { useEffect, useState } from "react"; -import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { Loader2, ArrowLeft, Info } 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 { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Alert, AlertDescription } from "@/components/ui/alert"; - -// ============================================================================ -// Component -// ============================================================================ - -export default function NewCreativePage() { - const params = useParams(); - const router = useRouter(); - const searchParams = useSearchParams(); - const workspaceId = params?.workspaceId as string; - const { currentProject, setSelectedProjects, projects } = useWorkspace(); - - const prefilledProjectId = searchParams.get("project_id"); - - const [name, setName] = useState(""); - const [text, setText] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(null); - - // Auto-select project if prefilled from URL - useEffect(() => { - if (prefilledProjectId && projects.length > 0) { - const project = projects.find(p => p.id === prefilledProjectId); - if (project) { - setSelectedProjects([project]); - } - } - }, [prefilledProjectId, projects, setSelectedProjects]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!name.trim() || !text.trim()) { - setError("Заполните все обязательные поля"); - return; - } - - if (!text.includes("{invite_link}")) { - setError("Текст должен содержать {invite_link} для вставки ссылки"); - return; - } - - if (!currentProject) { - setError("Выберите проект в верхнем меню"); - return; - } - - try { - setIsSubmitting(true); - setError(null); - - const creative = await creativesApi.create( - workspaceId, - currentProject.id, - { - name: name.trim(), - text: text.trim(), - } - ); - - // Redirect to creatives list with project filter - router.push(`/dashboard/${workspaceId}/creatives?project_id=${currentProject.id}`); - } catch (err: any) { - setError(err?.detail || "Не удалось создать креатив"); - } finally { - setIsSubmitting(false); - } - }; - - const insertInviteLink = () => { - if (!text.includes("{invite_link}")) { - setText((prev) => prev + "{invite_link}"); - } - }; - - return ( - <> - - -
-
- -
-

Новый креатив

-

- Создайте рекламный материал для размещений -

-
-
- - {!currentProject && ( - - - - Выберите проект в верхнем меню для создания креатива - - - )} - -
- - - Информация о креативе - - Креатив — это текст рекламного сообщения с пригласительной ссылкой - - - -
- - setName(e.target.value)} - disabled={isSubmitting} - /> -

- Внутреннее название для удобства поиска -

-
- -
-
- - -
-