diff --git a/admin-compliance/app/sdk/cookie-banner/_components/EmbeddableVendorHTML.tsx b/admin-compliance/app/sdk/cookie-banner/_components/EmbeddableVendorHTML.tsx new file mode 100644 index 0000000..6fcdc5c --- /dev/null +++ b/admin-compliance/app/sdk/cookie-banner/_components/EmbeddableVendorHTML.tsx @@ -0,0 +1,103 @@ +'use client' + +import { useState, useEffect } from 'react' + +interface Vendor { + vendor_name: string + vendor_url: string | null + category_key: string + description_de: string | null + cookie_names: string[] + retention_days: number | null +} + +const CAT_LABELS: Record = { + necessary: 'Notwendig', + functional: 'Funktional', + statistics: 'Statistik', + marketing: 'Marketing', +} + +function generateHTML(vendors: Vendor[]): string { + const grouped = vendors.reduce>((acc, v) => { + const key = v.category_key || 'other' + if (!acc[key]) acc[key] = [] + acc[key].push(v) + return acc + }, {}) + + let html = `
\n` + html += `

Eingesetzte Dienste und Cookies

\n` + + for (const [catKey, catVendors] of Object.entries(grouped)) { + const label = CAT_LABELS[catKey] || catKey + html += `

${label}

\n` + html += `\n` + html += `\n` + + for (const v of catVendors) { + const name = v.vendor_url + ? `${v.vendor_name}` + : v.vendor_name + const cookies = v.cookie_names?.join(', ') || '-' + const retention = v.retention_days ? `${v.retention_days} Tage` : '-' + html += `\n` + } + html += `
AnbieterZweckCookiesSpeicherdauer
${name}${v.description_de || '-'}${cookies}${retention}
\n` + } + html += `
` + return html +} + +export function EmbeddableVendorHTML({ siteId }: { siteId?: string }) { + const [vendors, setVendors] = useState([]) + const [copied, setCopied] = useState(false) + + useEffect(() => { + const sid = siteId || 'preview-test-site' + fetch(`/api/sdk/v1/banner/admin/sites/${sid}/vendors`) + .then(r => r.ok ? r.json() : []) + .then(data => setVendors(Array.isArray(data) ? data : [])) + .catch(() => {}) + }, [siteId]) + + const html = generateHTML(vendors) + + const handleCopy = () => { + navigator.clipboard.writeText(html) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( +
+
+
+

Einbettbarer HTML-Code

+

+ Kopieren Sie diesen Code in Ihre Datenschutzerklaerung oder Cookie-Richtlinie. +

+
+ +
+ + {/* Preview */} +
+
+
+ + {/* Raw HTML */} +
+ + Quellcode anzeigen + +
+          {html}
+        
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/cookie-banner/_components/SiteSelector.tsx b/admin-compliance/app/sdk/cookie-banner/_components/SiteSelector.tsx new file mode 100644 index 0000000..4bae365 --- /dev/null +++ b/admin-compliance/app/sdk/cookie-banner/_components/SiteSelector.tsx @@ -0,0 +1,76 @@ +'use client' + +import { useState } from 'react' + +interface Site { + id: string + site_id: string + site_name: string + site_url: string + is_active: boolean +} + +interface SiteSelectorProps { + sites: Site[] + activeSiteId: string | null + onSiteChange: (siteId: string) => void + onCreateSite: (data: { site_id: string; site_name: string; site_url: string }) => Promise +} + +export function SiteSelector({ sites, activeSiteId, onSiteChange, onCreateSite }: SiteSelectorProps) { + const [showCreate, setShowCreate] = useState(false) + const [newSite, setNewSite] = useState({ site_id: '', site_name: '', site_url: '' }) + const [creating, setCreating] = useState(false) + + const handleCreate = async () => { + if (!newSite.site_id || !newSite.site_name) return + setCreating(true) + try { + await onCreateSite(newSite) + setNewSite({ site_id: '', site_name: '', site_url: '' }) + setShowCreate(false) + } finally { + setCreating(false) + } + } + + return ( +
+
+
+ + +
+ +
+ + {showCreate && ( +
+ setNewSite({ ...newSite, site_id: e.target.value })} + placeholder="Site-ID (z.B. main-website)" className="px-3 py-2 text-sm border border-gray-200 rounded-lg" /> + setNewSite({ ...newSite, site_name: e.target.value })} + placeholder="Name (z.B. Hauptwebsite)" className="px-3 py-2 text-sm border border-gray-200 rounded-lg" /> +
+ setNewSite({ ...newSite, site_url: e.target.value })} + placeholder="URL (z.B. https://example.com)" className="flex-1 px-3 py-2 text-sm border border-gray-200 rounded-lg" /> + +
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/cookie-banner/_components/VendorTable.tsx b/admin-compliance/app/sdk/cookie-banner/_components/VendorTable.tsx new file mode 100644 index 0000000..8fa12cf --- /dev/null +++ b/admin-compliance/app/sdk/cookie-banner/_components/VendorTable.tsx @@ -0,0 +1,138 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useSDK } from '@/lib/sdk' + +interface Vendor { + id: string + vendor_name: string + vendor_url: string | null + category_key: string + description_de: string | null + description_en: string | null + cookie_names: string[] + retention_days: number | null + is_active: boolean +} + +const CATEGORY_LABELS: Record = { + necessary: { label: 'Notwendig', color: 'bg-green-100 text-green-700' }, + functional: { label: 'Funktional', color: 'bg-blue-100 text-blue-700' }, + statistics: { label: 'Statistik', color: 'bg-yellow-100 text-yellow-700' }, + marketing: { label: 'Marketing', color: 'bg-red-100 text-red-700' }, +} + +export function VendorTable({ siteId }: { siteId?: string }) { + const { projectId } = useSDK() + const [vendors, setVendors] = useState([]) + const [loading, setLoading] = useState(true) + const [expandedId, setExpandedId] = useState(null) + + useEffect(() => { + const sid = siteId || 'preview-test-site' + fetch(`/api/sdk/v1/banner/admin/sites/${sid}/vendors`) + .then(r => r.ok ? r.json() : []) + .then(data => setVendors(Array.isArray(data) ? data : [])) + .catch(() => setVendors([])) + .finally(() => setLoading(false)) + }, [siteId]) + + // Group by category + const grouped = vendors.reduce>((acc, v) => { + const key = v.category_key || 'other' + if (!acc[key]) acc[key] = [] + acc[key].push(v) + return acc + }, {}) + + if (loading) { + return
Lade Verarbeiter...
+ } + + if (vendors.length === 0) { + return ( +
+

Keine Verarbeiter konfiguriert.

+

+ Nutzen Sie den Website-Scanner oder fuegen Sie Verarbeiter manuell hinzu. +

+
+ ) + } + + return ( +
+
+
+

Verarbeiter-Uebersicht

+

{vendors.length} Dienste in {Object.keys(grouped).length} Kategorien

+
+
+ + {Object.entries(grouped).map(([catKey, catVendors]) => { + const catInfo = CATEGORY_LABELS[catKey] || { label: catKey, color: 'bg-gray-100 text-gray-700' } + return ( +
+
+ + {catInfo.label} + + {catVendors.length} Dienste +
+ + + + + + + + + + + + {catVendors.map(v => ( + + + + + + + + ))} + +
AnbieterZweckCookiesAufbewahrungDatenschutz
+ + {expandedId === v.id && v.cookie_names?.length > 0 && ( +
+ {v.cookie_names.map(c => ( + + {c} + + ))} +
+ )} +
+ {v.description_de || '-'} + + {v.cookie_names?.length || 0} + + {v.retention_days ? `${v.retention_days} Tage` : '-'} + + {v.vendor_url ? ( + + Link + + ) : ( + - + )} +
+
+ ) + })} +
+ ) +} diff --git a/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts b/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts index 1b7db88..cc4b092 100644 --- a/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts +++ b/admin-compliance/app/sdk/cookie-banner/_hooks/useCookieBanner.ts @@ -96,13 +96,38 @@ const defaultBannerTexts: BannerTexts = { privacyLink: '/datenschutz', } +export interface BannerSite { + id: string + site_id: string + site_name: string + site_url: string + is_active: boolean +} + export function useCookieBanner() { const [categories, setCategories] = useState([]) const [config, setConfig] = useState(defaultConfig) const [bannerTexts, setBannerTexts] = useState(defaultBannerTexts) const [isSaving, setIsSaving] = useState(false) const [exportToast, setExportToast] = useState(null) + const [sites, setSites] = useState([]) + const [activeSiteId, setActiveSiteId] = useState(null) + // Load sites list + React.useEffect(() => { + fetch('/api/sdk/v1/banner/admin/sites') + .then(r => r.ok ? r.json() : []) + .then(data => { + const siteList = Array.isArray(data) ? data : [] + setSites(siteList) + if (siteList.length > 0 && !activeSiteId) { + setActiveSiteId(siteList[0].site_id) + } + }) + .catch(() => {}) + }, []) + + // Load config for active site React.useEffect(() => { const loadConfig = async () => { try { @@ -125,7 +150,20 @@ export function useCookieBanner() { } } loadConfig() - }, []) + }, [activeSiteId]) + + const createSite = async (data: { site_id: string; site_name: string; site_url: string }) => { + const res = await fetch('/api/sdk/v1/banner/admin/sites', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (res.ok) { + const newSite = await res.json() + setSites(prev => [...prev, newSite]) + setActiveSiteId(newSite.site_id || data.site_id) + } + } const handleCategoryToggle = async (categoryId: string, enabled: boolean) => { setCategories(prev => @@ -180,5 +218,6 @@ export function useCookieBanner() { categories, config, bannerTexts, isSaving, exportToast, setConfig, setBannerTexts, handleCategoryToggle, handleExportCode, handleSaveConfig, + sites, activeSiteId, setActiveSiteId, createSite, } } diff --git a/admin-compliance/app/sdk/cookie-banner/page.tsx b/admin-compliance/app/sdk/cookie-banner/page.tsx index 1733ff0..4cae9c5 100644 --- a/admin-compliance/app/sdk/cookie-banner/page.tsx +++ b/admin-compliance/app/sdk/cookie-banner/page.tsx @@ -1,18 +1,25 @@ 'use client' -import React from 'react' +import React, { useState } from 'react' import { useSDK } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' import { useCookieBanner } from './_hooks/useCookieBanner' import { BannerPreview } from './_components/BannerPreview' import { CategoryCard } from './_components/CategoryCard' +import { VendorTable } from './_components/VendorTable' +import { EmbeddableVendorHTML } from './_components/EmbeddableVendorHTML' +import { SiteSelector } from './_components/SiteSelector' + +type BannerTab = 'config' | 'vendors' | 'embed' export default function CookieBannerPage() { const { state } = useSDK() + const [activeTab, setActiveTab] = useState('config') const { categories, config, bannerTexts, isSaving, exportToast, setConfig, setBannerTexts, handleCategoryToggle, handleExportCode, handleSaveConfig, + sites, activeSiteId, setActiveSiteId, createSite, } = useCookieBanner() const totalCookies = categories.reduce((sum, cat) => sum + cat.cookies.length, 0) @@ -57,6 +64,35 @@ export default function CookieBannerPage() {
+ {/* Site Selector */} + {sites.length > 0 && ( + + )} + + {/* Tabs */} +
+ {([ + { id: 'config' as const, label: 'Konfiguration' }, + { id: 'vendors' as const, label: 'Verarbeiter' }, + { id: 'embed' as const, label: 'Einbettung' }, + ]).map(tab => ( + + ))} +
+ + {/* Tab: Verarbeiter */} + {activeTab === 'vendors' && } + + {/* Tab: Einbettung */} + {activeTab === 'embed' && } + + {/* Tab: Konfiguration */} + {activeTab !== 'config' ? null : (<> {/* Stats */}
@@ -207,6 +243,7 @@ export default function CookieBannerPage() { ))}
+ )} ) }