From 99dd0f272ea9ee6842b5b45699f0ad643f85a0a6 Mon Sep 17 00:00:00 2001 From: Jandl-hostacny Date: Thu, 24 Sep 2026 11:46:40 +0200 Subject: [PATCH] feat: real Brevo API verifier, auto-reset if campaign missing, and update existing campaign support --- src/app/api/brevo/export/route.ts | 71 ++++++++++++++++--- src/app/api/brevo/verify/route.ts | 64 +++++++++++++++++ src/app/page.tsx | 31 ++++++++- src/components/editor/TopBar.tsx | 111 ++++++++++++++++++++++++++---- 4 files changed, 250 insertions(+), 27 deletions(-) create mode 100644 src/app/api/brevo/verify/route.ts diff --git a/src/app/api/brevo/export/route.ts b/src/app/api/brevo/export/route.ts index 2a8b8d2..76da96e 100644 --- a/src/app/api/brevo/export/route.ts +++ b/src/app/api/brevo/export/route.ts @@ -2,11 +2,18 @@ import { NextRequest, NextResponse } from 'next/server'; export async function POST(req: NextRequest) { try { - const { campaignName, subject, htmlContent, scheduledAt } = await req.json(); + const { + campaignName, + subject, + htmlContent, + scheduledAt, + updateExisting = false, + existingCampaignId + } = await req.json(); - if (!campaignName || !subject || !htmlContent) { + if (!htmlContent) { return NextResponse.json( - { error: 'Chýbajú povinné polia (campaignName, subject, htmlContent).' }, + { error: 'Chýba povinné pole htmlContent.' }, { status: 400 } ); } @@ -22,10 +29,54 @@ export async function POST(req: NextRequest) { ); } - // Volanie Brevo API v3 - const payload: any = { - name: campaignName, - subject: subject, + // 1. Ak je zadané updateExisting a máme existujúce ID, skúsime aktualizovať existujúcu kampaň + if (updateExisting && existingCampaignId) { + const updatePayload: any = { + name: campaignName, + subject: subject, + sender: { + name: senderName, + email: senderEmail + }, + htmlContent: htmlContent + }; + + if (scheduledAt) { + updatePayload.scheduledAt = scheduledAt; + } + + const updateRes = await fetch( + `https://api.brevo.com/v3/emailCampaigns/${encodeURIComponent(existingCampaignId)}`, + { + method: 'PUT', + headers: { + 'accept': 'application/json', + 'content-type': 'application/json', + 'api-key': apiKey + }, + body: JSON.stringify(updatePayload) + } + ); + + if (updateRes.ok || updateRes.status === 204) { + return NextResponse.json({ + success: true, + updated: true, + campaignId: Number(existingCampaignId), + brevoUrl: `https://app.brevo.com/campaigns/email/edit/${existingCampaignId}`, + brevoListingUrl: 'https://app.brevo.com/campaigns/listing', + message: 'Kampaň bola úspešne aktualizovaná v Brevo!' + }); + } + + // Ak aktualizácia zlyhala (napr. kampaň už bola odoslaná alebo neexistuje), vytvoríme novú + console.warn(`Update failed on campaign ${existingCampaignId}, creating a new one instead.`); + } + + // 2. Vytvorenie novej kampane v Brevo + const createPayload: any = { + name: campaignName || 'COOP Jednota Newsletter', + subject: subject || 'COOP Jednota Newsletter', sender: { name: senderName, email: senderEmail @@ -35,7 +86,7 @@ export async function POST(req: NextRequest) { }; if (scheduledAt) { - payload.scheduledAt = scheduledAt; + createPayload.scheduledAt = scheduledAt; } const brevoRes = await fetch('https://api.brevo.com/v3/emailCampaigns', { @@ -45,7 +96,7 @@ export async function POST(req: NextRequest) { 'content-type': 'application/json', 'api-key': apiKey }, - body: JSON.stringify(payload) + body: JSON.stringify(createPayload) }); const data = await brevoRes.json(); @@ -58,9 +109,9 @@ export async function POST(req: NextRequest) { ); } - // Úspech - vrátime campaignId a priame URL na editáciu a prehľad kampaní v novom Brevo dashboarde return NextResponse.json({ success: true, + updated: false, campaignId: data.id, brevoUrl: `https://app.brevo.com/campaigns/email/edit/${data.id}`, brevoListingUrl: 'https://app.brevo.com/campaigns/listing', diff --git a/src/app/api/brevo/verify/route.ts b/src/app/api/brevo/verify/route.ts new file mode 100644 index 0000000..92cf54c --- /dev/null +++ b/src/app/api/brevo/verify/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const campaignId = searchParams.get('campaignId'); + + if (!campaignId) { + return NextResponse.json( + { exists: false, error: 'Chýba parameter campaignId' }, + { status: 400 } + ); + } + + const apiKey = process.env.BREVO_API_KEY; + if (!apiKey) { + return NextResponse.json( + { exists: false, error: 'BREVO_API_KEY nie je nakonfigurovaný' }, + { status: 500 } + ); + } + + const res = await fetch(`https://api.brevo.com/v3/emailCampaigns/${encodeURIComponent(campaignId)}`, { + method: 'GET', + headers: { + 'accept': 'application/json', + 'api-key': apiKey + }, + cache: 'no-store' + }); + + if (res.status === 404) { + return NextResponse.json({ + exists: false, + message: 'Kampaň s týmto ID v Brevo neexistuje alebo bola vymazaná.' + }); + } + + if (!res.ok) { + const errData = await res.json().catch(() => ({})); + return NextResponse.json({ + exists: false, + error: errData.message || `Brevo API vrátilo status ${res.status}` + }); + } + + const data = await res.json(); + return NextResponse.json({ + exists: true, + campaignId: data.id, + name: data.name, + status: data.status, + subject: data.subject, + brevoUrl: `https://app.brevo.com/campaigns/email/edit/${data.id}`, + brevoListingUrl: 'https://app.brevo.com/campaigns/listing' + }); + } catch (err: any) { + console.error('Brevo verify error:', err); + return NextResponse.json( + { exists: false, error: err.message || 'Chyba spojenia s Brevo' }, + { status: 500 } + ); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index b88d2cd..89b815f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -370,7 +370,7 @@ export default function Home() { saveAs(blob, `coop-newsletter-${new Date().toISOString().split('T')[0]}.zip`); }; - const handleExportBrevo = async () => { + const handleExportBrevo = async (updateExisting = false) => { setIsExportingBrevo(true); try { const rawHtml = renderEmailHtml(campaign); @@ -391,14 +391,22 @@ export default function Home() { body: JSON.stringify({ campaignName: `${campaign.title} - ${new Date().toLocaleDateString('sk-SK')}`, subject: campaign.title, - htmlContent: finalHtml + htmlContent: finalHtml, + updateExisting: updateExisting, + existingCampaignId: campaign.brevoCampaignId }) }); const resData = await response.json(); if (resData.success && resData.campaignId) { + setCampaign((prev) => ({ + ...prev, + brevoCampaignId: resData.campaignId, + status: 'exported' + })); setBrevoUrl(resData.brevoUrl); + if (campaign.id) { await supabase .from('templates') @@ -408,7 +416,12 @@ export default function Home() { }) .eq('id', campaign.id); } - alert('Kampaň bola úspešne odoslaná do Brevo!'); + alert( + resData.message || + (updateExisting + ? 'Kampaň bola v Brevo úspešne aktualizovaná!' + : 'Kampaň bola úspešne odoslaná do Brevo!') + ); } else { alert(`Chyba pri exporte do Brevo: ${resData.error || 'Neznáma chyba'}`); } @@ -419,6 +432,17 @@ export default function Home() { } }; + const handleClearBrevoId = async () => { + setCampaign((prev) => ({ ...prev, brevoCampaignId: undefined })); + setBrevoUrl(null); + if (campaign.id) { + await supabase + .from('templates') + .update({ brevo_campaign_id: null }) + .eq('id', campaign.id); + } + }; + // Vybraný produkt pre editor cenoviek let targetProduct: ProductItem | null = null; if (targetProductIdForBadge && selectedModule?.data?.products) { @@ -465,6 +489,7 @@ export default function Home() { isExportingBrevo={isExportingBrevo} brevoCampaignUrl={brevoUrl} brevoListingUrl="https://app.brevo.com/campaigns/listing" + onClearBrevoId={handleClearBrevoId} onAddModule={handleAddModule} /> diff --git a/src/components/editor/TopBar.tsx b/src/components/editor/TopBar.tsx index 9a3cae0..69d0dc8 100644 --- a/src/components/editor/TopBar.tsx +++ b/src/components/editor/TopBar.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Download, Send, @@ -10,7 +10,10 @@ import { FolderOpen, Save, Copy, - Edit2 + Edit2, + RefreshCw, + UploadCloud, + Check } from 'lucide-react'; import { NewsletterCampaign } from '@/types/newsletter'; @@ -22,10 +25,11 @@ interface TopBarProps { onOpenSavedCampaigns: () => void; onDuplicateCampaign: () => void; onExportZip: () => void; - onExportBrevo: () => void; + onExportBrevo: (updateExisting?: boolean) => void; isExportingBrevo: boolean; brevoCampaignUrl?: string | null; brevoListingUrl?: string | null; + onClearBrevoId?: () => void; onAddModule: (type: any) => void; } @@ -41,9 +45,55 @@ export const TopBar: React.FC = ({ isExportingBrevo, brevoCampaignUrl, brevoListingUrl = 'https://app.brevo.com/campaigns/listing', + onClearBrevoId, onAddModule }) => { const [isEditingTitle, setIsEditingTitle] = useState(false); + const [isVerifyingBrevo, setIsVerifyingBrevo] = useState(false); + const [isVerifiedInBrevo, setIsVerifiedInBrevo] = useState(false); + const [brevoCampaignStatus, setBrevoCampaignStatus] = useState(null); + + // Funkcia na reálne overenie existencie kampane priamo cez Brevo API + const verifyCampaignInBrevo = async (campaignId?: number) => { + const idToCheck = campaignId || campaign.brevoCampaignId; + if (!idToCheck) { + setIsVerifiedInBrevo(false); + setBrevoCampaignStatus(null); + return; + } + + setIsVerifyingBrevo(true); + try { + const res = await fetch(`/api/brevo/verify?campaignId=${encodeURIComponent(idToCheck)}`); + const data = await res.json(); + + if (data.exists) { + setIsVerifiedInBrevo(true); + setBrevoCampaignStatus(data.status || 'draft'); + } else { + // Kampaň v Brevo reálne neexistuje (vymazaná alebo prenos zlyhal) + setIsVerifiedInBrevo(false); + setBrevoCampaignStatus(null); + if (onClearBrevoId) { + onClearBrevoId(); + } + } + } catch (err) { + console.warn('Chyba pri overovaní Brevo kampane:', err); + } finally { + setIsVerifyingBrevo(false); + } + }; + + // Pri zmene brevoCampaignId automaticky overíme stav v Brevo + useEffect(() => { + if (campaign.brevoCampaignId) { + verifyCampaignInBrevo(campaign.brevoCampaignId); + } else { + setIsVerifiedInBrevo(false); + setBrevoCampaignStatus(null); + } + }, [campaign.brevoCampaignId]); return (
@@ -85,7 +135,7 @@ export const TopBar: React.FC = ({ className="group flex items-center gap-1.5 cursor-pointer" title="Kliknite pre úpravu názvu" > -

+

{campaign.title || 'COOP Jednota Newsletter'}

@@ -122,7 +172,7 @@ export const TopBar: React.FC = ({ - {/* Pravá časť: Uložiť, Duplikovať, Export do ZIP a Brevo */} + {/* Pravá časť: Uložiť, Duplikovať, Export do ZIP a Inteligentné Brevo akcie */}
{/* Tlačidlo manuálneho uloženia */} - {brevoCampaignUrl ? ( -
+ {/* BREVO SEKČNÉ OVLÁDANIE S REÁLNYM OVERENÍM */} + {isVerifyingBrevo ? ( +
+ + Overujem v Brevo... +
+ ) : isVerifiedInBrevo && campaign.brevoCampaignId ? ( + /* Kampaň je overená priamo v Brevo: Ponúkame úpravu, aktualizáciu alebo odoslanie ako novú */ +
+ {/* Odkaz na úpravu v Brevo */} Upraviť v Brevo + + {/* Tlačidlo na aktualizáciu existujúcej kampane */} + + + {/* Tlačidlo na overenie stavu */} + + + {/* Odkaz na celkový zoznam kampaní */} Zoznam
) : ( + /* Kampaň v Brevo nie je overená / neexistuje: Zobrazujeme tlačidlo na prvotné odoslanie */ )}