|
|
|
@@ -42,6 +42,26 @@ interface Snapshot {
|
|
|
|
|
|
|
|
|
|
// name_lower → tatsächliche Kategorie laut Library (aus /cookie-check).
|
|
|
|
|
export type LibCategories = Record<string, string>
|
|
|
|
|
// name_lower → Speichertyp (cookie | local_storage | framework_storage | …).
|
|
|
|
|
export type StorageTypes = Record<string, string>
|
|
|
|
|
|
|
|
|
|
const STORAGE_LABEL: Record<string, string> = {
|
|
|
|
|
cookie: 'Cookie', local_storage: 'Local Storage',
|
|
|
|
|
session_storage: 'Session Storage', indexeddb: 'IndexedDB',
|
|
|
|
|
framework_storage: 'Framework',
|
|
|
|
|
}
|
|
|
|
|
const STORAGE_COLOR: Record<string, string> = {
|
|
|
|
|
cookie: 'bg-gray-100 text-gray-500',
|
|
|
|
|
local_storage: 'bg-purple-100 text-purple-700',
|
|
|
|
|
session_storage: 'bg-indigo-100 text-indigo-700',
|
|
|
|
|
indexeddb: 'bg-cyan-100 text-cyan-700',
|
|
|
|
|
framework_storage: 'bg-orange-100 text-orange-700',
|
|
|
|
|
}
|
|
|
|
|
const STORAGE_ORDER = ['cookie', 'local_storage', 'session_storage', 'indexeddb', 'framework_storage']
|
|
|
|
|
|
|
|
|
|
function storageOf(name: string, st?: StorageTypes): string {
|
|
|
|
|
return st?.[(name || '').toLowerCase()] || 'cookie'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ROLE_LABEL: Record<string, string> = {
|
|
|
|
|
unknown: 'Unbekannt', ad_pixel: 'Werbe-Pixel', auth_token: 'Auth-Token',
|
|
|
|
@@ -116,9 +136,14 @@ function Tile({ label, value, tone }: { label: string; value: React.ReactNode; t
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function VendorRow({ v, lib }: { v: SnapshotVendor; lib?: LibCategories }) {
|
|
|
|
|
function VendorRow(
|
|
|
|
|
{ v, lib, st, sf }:
|
|
|
|
|
{ v: SnapshotVendor; lib?: LibCategories; st?: StorageTypes; sf: string },
|
|
|
|
|
) {
|
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
|
const cookies = v.cookies || []
|
|
|
|
|
const cookies = sf
|
|
|
|
|
? (v.cookies || []).filter(c => storageOf(c.name, st) === sf)
|
|
|
|
|
: (v.cookies || [])
|
|
|
|
|
const cat = (v.category || '').toLowerCase()
|
|
|
|
|
const declaredCanon = canonCat(v.category)
|
|
|
|
|
const drittland = !!v.country && !EEA.has((v.country || '').toUpperCase())
|
|
|
|
@@ -151,6 +176,7 @@ function VendorRow({ v, lib }: { v: SnapshotVendor; lib?: LibCategories }) {
|
|
|
|
|
<thead className="text-gray-400">
|
|
|
|
|
<tr>
|
|
|
|
|
<th className="px-2 py-1 text-left font-normal">Cookie</th>
|
|
|
|
|
<th className="px-2 py-1 text-left font-normal">Speicher</th>
|
|
|
|
|
<th className="px-2 py-1 text-left font-normal">Rolle</th>
|
|
|
|
|
<th className="px-2 py-1 text-left font-normal">Zweck</th>
|
|
|
|
|
<th className="px-2 py-1 text-left font-normal">Laufzeit</th>
|
|
|
|
@@ -172,6 +198,16 @@ function VendorRow({ v, lib }: { v: SnapshotVendor; lib?: LibCategories }) {
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-2 py-1 w-24">
|
|
|
|
|
{(() => {
|
|
|
|
|
const t = storageOf(c.name, st)
|
|
|
|
|
return t !== 'cookie' ? (
|
|
|
|
|
<span className={`px-1 py-0.5 rounded text-[9px] ${STORAGE_COLOR[t]}`}>
|
|
|
|
|
{STORAGE_LABEL[t] || t}
|
|
|
|
|
</span>
|
|
|
|
|
) : <span className="text-gray-300 text-[10px]">Cookie</span>
|
|
|
|
|
})()}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-2 py-1 text-gray-500 w-24">
|
|
|
|
|
{c.functional_role && c.functional_role !== 'unknown'
|
|
|
|
|
? (ROLE_LABEL[c.functional_role] || c.functional_role)
|
|
|
|
@@ -195,11 +231,26 @@ function VendorRow({ v, lib }: { v: SnapshotVendor; lib?: LibCategories }) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function CookieResultView(
|
|
|
|
|
{ snapshot, cookieCategories }:
|
|
|
|
|
{ snapshot: Snapshot; cookieCategories?: LibCategories },
|
|
|
|
|
{ snapshot, cookieCategories, storageTypes }:
|
|
|
|
|
{ snapshot: Snapshot; cookieCategories?: LibCategories; storageTypes?: StorageTypes },
|
|
|
|
|
) {
|
|
|
|
|
const vendors = snapshot.cmp_vendors || []
|
|
|
|
|
const [viewMode, setViewMode] = useState<'role' | 'category'>('role')
|
|
|
|
|
const [storageFilter, setStorageFilter] = useState('')
|
|
|
|
|
|
|
|
|
|
// Speichertyp-Verteilung über alle Cookies (für die Filter-Chips + Zähler).
|
|
|
|
|
const storagePresent = useMemo(() => {
|
|
|
|
|
const counts: Record<string, number> = {}
|
|
|
|
|
for (const v of vendors)
|
|
|
|
|
for (const c of v.cookies || []) {
|
|
|
|
|
const t = storageOf(c.name, storageTypes)
|
|
|
|
|
counts[t] = (counts[t] || 0) + 1
|
|
|
|
|
}
|
|
|
|
|
return counts
|
|
|
|
|
}, [vendors, storageTypes])
|
|
|
|
|
|
|
|
|
|
const matchesSF = (v: SnapshotVendor) =>
|
|
|
|
|
!storageFilter || (v.cookies || []).some(c => storageOf(c.name, storageTypes) === storageFilter)
|
|
|
|
|
|
|
|
|
|
const stats = useMemo(() => {
|
|
|
|
|
const cookies = vendors.reduce((n, v) => n + (v.cookies?.length || 0), 0)
|
|
|
|
@@ -220,7 +271,7 @@ export function CookieResultView(
|
|
|
|
|
(a.compliance_score ?? 100) - (b.compliance_score ?? 100)
|
|
|
|
|
if (viewMode === 'category') {
|
|
|
|
|
return CATEGORY_GROUPS
|
|
|
|
|
.map(g => ({ ...g, vendors: vendors.filter(v => canonCat(v.category) === g.key).sort(sortByScore) }))
|
|
|
|
|
.map(g => ({ ...g, vendors: vendors.filter(v => canonCat(v.category) === g.key).filter(matchesSF).sort(sortByScore) }))
|
|
|
|
|
.filter(g => g.vendors.length > 0)
|
|
|
|
|
}
|
|
|
|
|
return GROUPS
|
|
|
|
@@ -228,10 +279,11 @@ export function CookieResultView(
|
|
|
|
|
...g,
|
|
|
|
|
vendors: vendors
|
|
|
|
|
.filter(v => GROUPS.find(gg => gg.test((v.recipient_type || '').toUpperCase()))?.key === g.key)
|
|
|
|
|
.filter(matchesSF)
|
|
|
|
|
.sort(sortByScore),
|
|
|
|
|
}))
|
|
|
|
|
.filter(g => g.vendors.length > 0)
|
|
|
|
|
}, [vendors, viewMode])
|
|
|
|
|
}, [vendors, viewMode, storageFilter, storageTypes])
|
|
|
|
|
|
|
|
|
|
const toggleBtn = (mode: 'role' | 'category', label: string) => (
|
|
|
|
|
<button
|
|
|
|
@@ -263,12 +315,37 @@ export function CookieResultView(
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
|
|
|
|
<Tile label="Anbieter" value={vendors.length} tone="text-gray-800" />
|
|
|
|
|
<Tile label="Cookies gesamt" value={stats.cookies} tone="text-gray-800" />
|
|
|
|
|
<Tile
|
|
|
|
|
label={storageFilter ? `${STORAGE_LABEL[storageFilter] || storageFilter} (gefiltert)` : 'Cookies gesamt'}
|
|
|
|
|
value={storageFilter ? (storagePresent[storageFilter] || 0) : stats.cookies}
|
|
|
|
|
tone="text-gray-800"
|
|
|
|
|
/>
|
|
|
|
|
<Tile label="Marketing-Anbieter" value={stats.marketing} tone={stats.marketing > 0 ? 'text-red-700' : 'text-gray-800'} />
|
|
|
|
|
<Tile label="Drittland (außerhalb EWR)" value={stats.drittland} tone={stats.drittland > 0 ? 'text-amber-700' : 'text-gray-800'} />
|
|
|
|
|
<Tile label="Falsch einsortiert (lt. Library)" value={stats.misplaced} tone={stats.misplaced > 0 ? 'text-red-700' : 'text-gray-800'} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{Object.keys(storagePresent).filter(t => t !== 'cookie').length > 0 && (
|
|
|
|
|
<div className="flex items-center gap-1 flex-wrap">
|
|
|
|
|
<span className="text-[11px] text-gray-500 mr-1">Speichertyp:</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setStorageFilter('')}
|
|
|
|
|
className={`px-2 py-0.5 rounded text-[11px] ${!storageFilter ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
|
|
|
|
>
|
|
|
|
|
Alle ({stats.cookies})
|
|
|
|
|
</button>
|
|
|
|
|
{STORAGE_ORDER.filter(t => storagePresent[t]).map(t => (
|
|
|
|
|
<button
|
|
|
|
|
key={t}
|
|
|
|
|
onClick={() => setStorageFilter(f => f === t ? '' : t)}
|
|
|
|
|
className={`px-2 py-0.5 rounded text-[11px] ${storageFilter === t ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
|
|
|
|
>
|
|
|
|
|
{STORAGE_LABEL[t] || t} ({storagePresent[t]})
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{viewMode === 'category' && (
|
|
|
|
|
<p className="text-[11px] text-gray-500 -mt-1">
|
|
|
|
|
Banner-Kategorie wie im Consent-Tool deklariert. Badge{' '}
|
|
|
|
@@ -283,7 +360,7 @@ export function CookieResultView(
|
|
|
|
|
{g.label} <span className="text-gray-400 font-normal">({g.vendors.length})</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="divide-y divide-gray-100">
|
|
|
|
|
{g.vendors.map((v, i) => <VendorRow key={i} v={v} lib={cookieCategories} />)}
|
|
|
|
|
{g.vendors.map((v, i) => <VendorRow key={i} v={v} lib={cookieCategories} st={storageTypes} sf={storageFilter} />)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|