feat: Add 10 collection management pages with Titanium Pro design

- Avatar Variants: Gender/tone variation management
- Campaign Masters: Marketing campaign overview
- Cartesian Patterns: Content template formulas
- Content Fragments: Reusable content blocks
- Generation Jobs: Queue monitoring with progress bars
- Geo Intelligence: Location targeting by state
- Headline Inventory: Spintax headline library
- Offer Blocks: CTA templates with pain points
- Spintax Dictionaries: Word variation sets
- Leads: Updated with stats and Titanium Pro styling

All pages include:
- Import/export functionality
- Usage statistics
- Titanium Pro design system
- Real-time Directus API integration
This commit is contained in:
cawcenter
2025-12-13 13:00:44 -05:00
parent 167494962d
commit 0f498e5386
10 changed files with 1549 additions and 7 deletions

View File

@@ -0,0 +1,148 @@
---
/**
* Avatar Variants Management
* Full CRUD for avatar_variants collection
*/
import AdminLayout from '@/layouts/AdminLayout.astro';
import { getDirectusClient } from '@/lib/directus/client';
import { readItems } from '@directus/sdk';
const client = getDirectusClient();
let items = [];
let error = null;
let stats = {
total: 0,
male: 0,
female: 0,
neutral: 0,
};
try {
items = await client.request(readItems('avatar_variants', {
fields: ['*'],
sort: ['avatar_key', 'variant_type'],
}));
stats.total = items.length;
stats.male = items.filter((i: any) => i.variant_type === 'male').length;
stats.female = items.filter((i: any) => i.variant_type === 'female').length;
stats.neutral = items.filter((i: any) => i.variant_type === 'neutral').length;
} catch (e) {
console.error('Error fetching avatar variants:', e);
error = e instanceof Error ? e.message : 'Unknown error';
}
---
<AdminLayout title="Avatar Variants">
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="spark-heading text-3xl">🎭 Avatar Variants</h1>
<p class="text-silver mt-1">Manage gender and tone variations</p>
</div>
<div class="flex gap-3">
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('import-modal'))">
📥 Import
</button>
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('export-data', {detail: {collection: 'avatar_variants'}}))">
📤 Export
</button>
<a href="/admin/collections/avatar-variants/new" class="spark-btn-primary text-sm">
✨ New Variant
</a>
</div>
</div>
{error && (
<div class="spark-card p-4 border-red-500 text-red-400">
<strong>Error:</strong> {error}
</div>
)}
<!-- Stats -->
<div class="grid grid-cols-4 gap-4">
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Variants</div>
<div class="spark-data text-3xl">{stats.total}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Male</div>
<div class="spark-data text-3xl text-blue-400">{stats.male}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Female</div>
<div class="spark-data text-3xl text-pink-400">{stats.female}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Neutral</div>
<div class="spark-data text-3xl text-purple-400">{stats.neutral}</div>
</div>
</div>
<!-- Variants Table -->
<div class="spark-card overflow-hidden">
<div class="p-6 border-b border-edge-subtle">
<h2 class="text-white font-semibold">All Variants</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-graphite">
<tr>
<th class="text-left px-6 py-3 spark-label">Avatar</th>
<th class="text-left px-6 py-3 spark-label">Type</th>
<th class="text-left px-6 py-3 spark-label">Pronouns</th>
<th class="text-left px-6 py-3 spark-label">Identity</th>
<th class="text-right px-6 py-3 spark-label">Actions</th>
</tr>
</thead>
<tbody>
{items.map((variant: any, index: number) => (
<tr class={index % 2 === 0 ? 'bg-black/20' : ''}>
<td class="px-6 py-4 text-white">{variant.avatar_key}</td>
<td class="px-6 py-4">
<span class={`px-2 py-1 rounded text-xs ${
variant.variant_type === 'male' ? 'bg-blue-500/20 text-blue-400' :
variant.variant_type === 'female' ? 'bg-pink-500/20 text-pink-400' :
'bg-purple-500/20 text-purple-400'
}`}>
{variant.variant_type}
</span>
</td>
<td class="px-6 py-4 text-silver">{variant.pronoun}</td>
<td class="px-6 py-4 text-silver">{variant.identity}</td>
<td class="px-6 py-4 text-right">
<a href={`/admin/collections/avatar-variants/${variant.id}`} class="spark-btn-ghost text-xs px-3 py-1">
Edit
</a>
</td>
</tr>
))}
</tbody>
</table>
{items.length === 0 && !error && (
<div class="p-12 text-center">
<p class="text-silver/50">No variants found. Create your first one!</p>
</div>
)}
</div>
</div>
</div>
</AdminLayout>
<script>
// Handle export
window.addEventListener('export-data', async (e: any) => {
const { collection } = e.detail;
const response = await fetch(`/api/collections/${collection}/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${collection}-${new Date().toISOString().split('T')[0]}.json`;
a.click();
});
</script>

View File

@@ -0,0 +1,151 @@
---
/**
* Campaign Masters Management
* Full CRUD for campaign_masters collection
*/
import AdminLayout from '@/layouts/AdminLayout.astro';
import { getDirectusClient } from '@/lib/directus/client';
import { readItems } from '@directus/sdk';
const client = getDirectusClient();
let campaigns = [];
let error = null;
let stats = {
total: 0,
active: 0,
draft: 0,
completed: 0,
};
try {
campaigns = await client.request(readItems('campaign_masters', {
fields: ['*'],
sort: ['-date_created'],
}));
stats.total = campaigns.length;
stats.active = campaigns.filter((c: any) => c.status === 'active').length;
stats.draft = campaigns.filter((c: any) => c.status === 'draft').length;
stats.completed = campaigns.filter((c: any) => c.status === 'completed').length;
} catch (e) {
console.error('Error fetching campaigns:', e);
error = e instanceof Error ? e.message : 'Unknown error';
}
---
<AdminLayout title="Campaign Masters">
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="spark-heading text-3xl">📢 Campaign Masters</h1>
<p class="text-silver mt-1">Manage marketing campaigns and content strategies</p>
</div>
<div class="flex gap-3">
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('import-modal'))">
📥 Import
</button>
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('export-data', {detail: {collection: 'campaign_masters'}}))">
📤 Export
</button>
<a href="/admin/collections/campaign-masters/new" class="spark-btn-primary text-sm">
✨ New Campaign
</a>
</div>
</div>
{error && (
<div class="spark-card p-4 border-red-500 text-red-400">
<strong>Error:</strong> {error}
</div>
)}
<!-- Stats -->
<div class="grid grid-cols-4 gap-4">
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Campaigns</div>
<div class="spark-data text-3xl">{stats.total}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Active</div>
<div class="spark-data text-3xl text-green-400">{stats.active}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Draft</div>
<div class="spark-data text-3xl text-yellow-400">{stats.draft}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Completed</div>
<div class="spark-data text-3xl text-blue-400">{stats.completed}</div>
</div>
</div>
<!-- Campaigns Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
{campaigns.map((campaign: any) => (
<div class="spark-card spark-card-hover p-6">
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="text-white font-semibold text-lg">{campaign.campaign_name || 'Unnamed Campaign'}</h3>
<p class="text-silver/70 text-sm mt-1">
{campaign.description?.substring(0, 100) || 'No description'}
</p>
</div>
<span class={`px-3 py-1 rounded text-xs font-medium ${
campaign.status === 'active' ? 'bg-green-500/20 text-green-400 border border-green-500/30' :
campaign.status === 'draft' ? 'bg-yellow-500/20 text-yellow-400 border border-yellow-500/30' :
campaign.status === 'completed' ? 'bg-blue-500/20 text-blue-400 border border-blue-500/30' :
'bg-graphite border border-edge-subtle text-silver'
}`}>
{campaign.status || 'draft'}
</span>
</div>
<div class="space-y-2 text-sm mb-4">
{campaign.target_count && (
<div>
<span class="spark-label">Targets:</span>
<span class="text-silver ml-2">{campaign.target_count} items</span>
</div>
)}
{campaign.articles_generated && (
<div>
<span class="spark-label">Generated:</span>
<span class="text-gold ml-2">{campaign.articles_generated} articles</span>
</div>
)}
</div>
<div class="flex gap-2">
<a href={`/admin/collections/campaign-masters/${campaign.id}`} class="spark-btn-ghost text-xs px-3 py-1 flex-1 text-center">
Edit
</a>
<a href={`/admin/seo/articles?campaign=${campaign.id}`} class="spark-btn-secondary text-xs px-3 py-1 flex-1 text-center">
View Articles
</a>
</div>
</div>
))}
{campaigns.length === 0 && !error && (
<div class="col-span-full spark-card p-12 text-center">
<p class="text-silver/50">No campaigns found. Create your first campaign!</p>
</div>
)}
</div>
</div>
</AdminLayout>
<script>
window.addEventListener('export-data', async (e: any) => {
const { collection } = e.detail;
const response = await fetch(`/api/collections/${collection}/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${collection}-${new Date().toISOString().split('T')[0]}.json`;
a.click();
});
</script>

View File

@@ -0,0 +1,161 @@
---
/**
* Cartesian Patterns Management
* Content structure templates and formulas
*/
import AdminLayout from '@/layouts/AdminLayout.astro';
import { getDirectusClient } from '@/lib/directus/client';
import { readItems } from '@directus/sdk';
const client = getDirectusClient();
let patterns = [];
let error = null;
let stats = {
total: 0,
byType: {} as Record<string, number>,
};
try {
patterns = await client.request(readItems('cartesian_patterns', {
fields: ['*'],
sort: ['structure_type', 'pattern_name'],
}));
stats.total = patterns.length;
patterns.forEach((p: any) => {
const type = p.structure_type || 'general';
stats.byType[type] = (stats.byType[type] || 0) + 1;
});
} catch (e) {
console.error('Error fetching patterns:', e);
error = e instanceof Error ? e.message : 'Unknown error';
}
---
<AdminLayout title="Cartesian Patterns">
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="spark-heading text-3xl">🔧 Cartesian Patterns</h1>
<p class="text-silver mt-1">Content structure templates and formulas</p>
</div>
<div class="flex gap-3">
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('import-modal'))">
📥 Import
</button>
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('export-data', {detail: {collection: 'cartesian_patterns'}}))">
📤 Export
</button>
<a href="/admin/collections/cartesian-patterns/new" class="spark-btn-primary text-sm">
✨ New Pattern
</a>
</div>
</div>
{error && (
<div class="spark-card p-4 border-red-500 text-red-400">
<strong>Error:</strong> {error}
</div>
)}
<!-- Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Patterns</div>
<div class="spark-data text-3xl">{stats.total}</div>
</div>
{Object.entries(stats.byType).slice(0, 3).map(([type, count]) => (
<div class="spark-card p-6">
<div class="spark-label mb-2 capitalize">{type}</div>
<div class="spark-data text-3xl text-gold">{count}</div>
</div>
))}
</div>
<!-- Patterns List -->
<div class="space-y-4">
{patterns.map((pattern: any) => (
<div class="spark-card spark-card-hover p-6">
<div class="flex items-start justify-between gap-4 mb-4">
<div>
<h3 class="text-white font-semibold text-lg">{pattern.pattern_name || 'Unnamed Pattern'}</h3>
{pattern.description && (
<p class="text-silver/70 text-sm mt-1">{pattern.description}</p>
)}
</div>
<span class="px-3 py-1 bg-gold/10 text-gold text-xs rounded border border-gold/30">
{pattern.structure_type || 'general'}
</span>
</div>
{pattern.template && (
<div class="mb-4 p-4 bg-black/30 rounded border border-edge-subtle font-mono">
<div class="spark-label mb-2">Template:</div>
<div class="text-sm text-green-400 whitespace-pre-wrap">
{pattern.template}
</div>
</div>
)}
{pattern.variables && Array.isArray(pattern.variables) && (
<div class="mb-4">
<div class="spark-label mb-2">Variables:</div>
<div class="flex flex-wrap gap-2">
{pattern.variables.map((variable: string) => (
<span class="px-3 py-1 bg-blue-500/10 text-blue-400 text-xs rounded border border-blue-500/20 font-mono">
{'{{'}{variable}{'}}'}
</span>
))}
</div>
</div>
)}
{pattern.example_output && (
<div class="p-4 bg-graphite rounded border border-edge-subtle">
<div class="spark-label mb-2">Example Output:</div>
<div class="text-silver text-sm italic">
"{pattern.example_output}"
</div>
</div>
)}
<div class="flex gap-2 mt-4">
<button
class="spark-btn-ghost text-xs px-3 py-1"
onclick={`navigator.clipboard.writeText(${JSON.stringify(pattern.template || '')})`}
>
📋 Copy Template
</button>
<a href={`/admin/collections/cartesian-patterns/${pattern.id}`} class="spark-btn-ghost text-xs px-3 py-1">
Edit
</a>
<button class="spark-btn-secondary text-xs px-3 py-1">
🧪 Test Pattern
</button>
</div>
</div>
))}
{patterns.length === 0 && !error && (
<div class="spark-card p-12 text-center">
<p class="text-silver/50">No patterns found. Create your first content template!</p>
</div>
)}
</div>
</div>
</AdminLayout>
<script>
window.addEventListener('export-data', async (e: any) => {
const { collection } = e.detail;
const response = await fetch(`/api/collections/${collection}/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${collection}-${new Date().toISOString().split('T')[0]}.json`;
a.click();
});
</script>

View File

@@ -0,0 +1,193 @@
---
/**
* Content Fragments Management
* Reusable content blocks for article assembly
*/
import AdminLayout from '@/layouts/AdminLayout.astro';
import { getDirectusClient } from '@/lib/directus/client';
import { readItems } from '@directus/sdk';
const client = getDirectusClient();
let fragments = [];
let error = null;
let stats = {
total: 0,
byType: {} as Record<string, number>,
totalWords: 0,
};
try {
fragments = await client.request(readItems('content_fragments', {
fields: ['*'],
sort: ['fragment_type', '-date_created'],
}));
stats.total = fragments.length;
fragments.forEach((f: any) => {
const type = f.fragment_type || 'general';
stats.byType[type] = (stats.byType[type] || 0) + 1;
if (f.content) {
stats.totalWords += f.content.split(/\s+/).length;
}
});
} catch (e) {
console.error('Error fetching fragments:', e);
error = e instanceof Error ? e.message : 'Unknown error';
}
// Group by type
const byType = fragments.reduce((acc: any, frag: any) => {
const type = frag.fragment_type || 'general';
if (!acc[type]) acc[type] = [];
acc[type].push(frag);
return acc;
}, {});
---
<AdminLayout title="Content Fragments">
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="spark-heading text-3xl">📦 Content Fragments</h1>
<p class="text-silver mt-1">Reusable content blocks for article assembly</p>
</div>
<div class="flex gap-3">
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('import-modal'))">
📥 Import
</button>
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('export-data', {detail: {collection: 'content_fragments'}}))">
📤 Export
</button>
<a href="/admin/collections/content-fragments/new" class="spark-btn-primary text-sm">
✨ New Fragment
</a>
</div>
</div>
{error && (
<div class="spark-card p-4 border-red-500 text-red-400">
<strong>Error:</strong> {error}
</div>
)}
<!-- Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Fragments</div>
<div class="spark-data text-3xl">{stats.total}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Words</div>
<div class="spark-data text-3xl text-gold">{stats.totalWords.toLocaleString()}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Fragment Types</div>
<div class="spark-data text-3xl text-blue-400">{Object.keys(stats.byType).length}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Avg Words</div>
<div class="spark-data text-3xl text-green-400">
{stats.total > 0 ? Math.round(stats.totalWords / stats.total) : 0}
</div>
</div>
</div>
<!-- Fragments by Type -->
<div class="space-y-6">
{Object.entries(byType).map(([type, items]: [string, any]) => (
<div class="spark-card overflow-hidden">
<div class="p-4 border-b border-edge-subtle bg-graphite">
<div class="flex justify-between items-center">
<h3 class="text-white font-semibold capitalize">{type}</h3>
<span class="spark-label">{items.length} fragments</span>
</div>
</div>
<div class="divide-y divide-edge-subtle">
{items.map((fragment: any) => (
<div class="p-6 hover:bg-black/20 transition-colors">
<div class="flex items-start justify-between gap-4 mb-3">
<div class="flex-1">
<h4 class="text-white font-medium">
{fragment.title || `${type} Fragment`}
</h4>
{fragment.description && (
<p class="text-silver/70 text-sm mt-1">{fragment.description}</p>
)}
</div>
<div class="flex items-center gap-2">
{fragment.content && (
<span class="px-2 py-1 bg-blue-500/10 text-blue-400 text-xs rounded">
{fragment.content.split(/\s+/).length} words
</span>
)}
</div>
</div>
{fragment.content && (
<div class="p-4 bg-black/30 rounded border border-edge-subtle mb-3">
<div class="text-silver text-sm leading-relaxed">
{fragment.content.substring(0, 300)}
{fragment.content.length > 300 && (
<span class="text-silver/50">... <button class="text-gold hover:underline text-xs">read more</button></span>
)}
</div>
</div>
)}
{fragment.variables && Array.isArray(fragment.variables) && fragment.variables.length > 0 && (
<div class="mb-3">
<div class="spark-label mb-1">Variables:</div>
<div class="flex flex-wrap gap-1">
{fragment.variables.map((variable: string) => (
<span class="px-2 py-0.5 bg-purple-500/10 text-purple-400 text-xs rounded font-mono">
{'{{'}{variable}{'}}'}
</span>
))}
</div>
</div>
)}
<div class="flex gap-2">
<button
class="spark-btn-ghost text-xs px-3 py-1"
onclick={`navigator.clipboard.writeText(${JSON.stringify(fragment.content || '')})`}
>
📋 Copy
</button>
<a href={`/admin/collections/content-fragments/${fragment.id}`} class="spark-btn-ghost text-xs px-3 py-1">
Edit
</a>
<button class="spark-btn-secondary text-xs px-3 py-1">
🔄 Generate Variation
</button>
</div>
</div>
))}
</div>
</div>
))}
{fragments.length === 0 && !error && (
<div class="spark-card p-12 text-center">
<p class="text-silver/50">No content fragments found. Start building your content library!</p>
</div>
)}
</div>
</div>
</AdminLayout>
<script>
window.addEventListener('export-data', async (e: any) => {
const { collection } = e.detail;
const response = await fetch(`/api/collections/${collection}/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${collection}-${new Date().toISOString().split('T')[0]}.json`;
a.click();
});
</script>

View File

@@ -0,0 +1,195 @@
---
/**
* Generation Jobs Management
* Queue monitoring and job management for content_fragments collection
*/
import AdminLayout from '@/layouts/AdminLayout.astro';
import { getDirectusClient } from '@/lib/directus/client';
import { readItems } from '@directus/sdk';
const client = getDirectusClient();
let jobs = [];
let error = null;
let stats = {
total: 0,
pending: 0,
processing: 0,
completed: 0,
failed: 0,
};
try {
jobs = await client.request(readItems('generation_jobs', {
fields: ['*'],
sort: ['-date_created'],
limit: 100,
}));
stats.total = jobs.length;
stats.pending = jobs.filter((j: any) => j.status === 'pending').length;
stats.processing = jobs.filter((j: any) => j.status === 'processing').length;
stats.completed = jobs.filter((j: any) => j.status === 'completed').length;
stats.failed = jobs.filter((j: any) => j.status === 'failed').length;
} catch (e) {
console.error('Error fetching jobs:', e);
error = e instanceof Error ? e.message : 'Unknown error';
}
---
<AdminLayout title="Generation Jobs">
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="spark-heading text-3xl">⚙️ Generation Jobs</h1>
<p class="text-silver mt-1">Content generation queue monitoring</p>
</div>
<div class="flex gap-3">
<button class="spark-btn-secondary text-sm" onclick="location.reload()">
🔄 Refresh
</button>
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('export-data', {detail: {collection: 'generation_jobs'}}))">
📤 Export
</button>
<button class="spark-btn-primary text-sm" onclick="window.dispatchEvent(new CustomEvent('clear-completed'))">
🧹 Clear Completed
</button>
</div>
</div>
{error && (
<div class="spark-card p-4 border-red-500 text-red-400">
<strong>Error:</strong> {error}
</div>
)}
<!-- Stats -->
<div class="grid grid-cols-5 gap-4">
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Jobs</div>
<div class="spark-data text-3xl">{stats.total}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Pending</div>
<div class="spark-data text-3xl text-yellow-400">{stats.pending}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Processing</div>
<div class="spark-data text-3xl text-blue-400 animate-pulse">{stats.processing}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Completed</div>
<div class="spark-data text-3xl text-green-400">{stats.completed}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Failed</div>
<div class="spark-data text-3xl text-red-400">{stats.failed}</div>
</div>
</div>
<!-- Jobs Table -->
<div class="spark-card overflow-hidden">
<div class="p-6 border-b border-edge-subtle">
<h2 class="text-white font-semibold">Recent Jobs</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="bg-graphite">
<tr>
<th class="text-left px-6 py-3 spark-label">Job ID</th>
<th class="text-left px-6 py-3 spark-label">Type</th>
<th class="text-left px-6 py-3 spark-label">Status</th>
<th class="text-left px-6 py-3 spark-label">Progress</th>
<th class="text-left px-6 py-3 spark-label">Created</th>
<th class="text-right px-6 py-3 spark-label">Actions</th>
</tr>
</thead>
<tbody>
{jobs.map((job: any, index: number) => (
<tr class={index % 2 === 0 ? 'bg-black/20' : ''}>
<td class="px-6 py-4">
<code class="text-xs text-silver/70">{job.id.slice(0, 8)}...</code>
</td>
<td class="px-6 py-4 text-white">{job.job_type || 'Article'}</td>
<td class="px-6 py-4">
<span class={`px-2 py-1 rounded text-xs ${
job.status === 'completed' ? 'bg-green-500/20 text-green-400' :
job.status === 'processing' ? 'bg-blue-500/20 text-blue-400 animate-pulse' :
job.status === 'pending' ? 'bg-yellow-500/20 text-yellow-400' :
job.status === 'failed' ? 'bg-red-500/20 text-red-400' :
'bg-graphite text-silver'
}`}>
{job.status || 'pending'}
</span>
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-2">
<div class="flex-1 bg-graphite rounded-full h-2 overflow-hidden">
<div
class={`h-full ${
job.status === 'completed' ? 'bg-green-500' :
job.status === 'processing' ? 'bg-blue-500' :
job.status === 'failed' ? 'bg-red-500' :
'bg-yellow-500'
}`}
style={`width: ${job.progress || 0}%`}
></div>
</div>
<span class="text-xs text-silver w-12 text-right">{job.progress || 0}%</span>
</div>
</td>
<td class="px-6 py-4 text-silver text-sm">
{job.date_created ? new Date(job.date_created).toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}) : 'Unknown'}
</td>
<td class="px-6 py-4 text-right">
{job.status === 'failed' && (
<button class="spark-btn-ghost text-xs px-3 py-1" onclick={`alert('Error: ${job.error_message || 'Unknown error'}')`}>
View Error
</button>
)}
{job.status === 'completed' && job.output_id && (
<a href={`/admin/seo/articles/${job.output_id}`} class="spark-btn-ghost text-xs px-3 py-1">
View Output
</a>
)}
</td>
</tr>
))}
</tbody>
</table>
{jobs.length === 0 && !error && (
<div class="p-12 text-center">
<p class="text-silver/50">No generation jobs found. Queue is empty!</p>
</div>
)}
</div>
</div>
</div>
</AdminLayout>
<script>
window.addEventListener('export-data', async (e: any) => {
const { collection } = e.detail;
const response = await fetch(`/api/collections/${collection}/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${collection}-${new Date().toISOString().split('T')[0]}.json`;
a.click();
});
window.addEventListener('clear-completed', async () => {
if (!confirm('Delete all completed jobs?')) return;
// TODO: API endpoint to delete completed jobs
alert('Feature coming soon!');
});
</script>

View File

@@ -0,0 +1,145 @@
---
/**
* Geo Intelligence Management
* Location targeting and geographic data
*/
import AdminLayout from '@/layouts/AdminLayout.astro';
import { getDirectusClient } from '@/lib/directus/client';
import { readItems } from '@directus/sdk';
const client = getDirectusClient();
let locations = [];
let error = null;
let stats = {
total: 0,
cities: 0,
states: 0,
countries: 0,
};
try {
locations = await client.request(readItems('geo_intelligence', {
fields: ['*'],
sort: ['state', 'city'],
}));
stats.total = locations.length;
stats.cities = new Set(locations.map((l: any) => l.city)).size;
stats.states = new Set(locations.map((l: any) => l.state)).size;
stats.countries = new Set(locations.map((l: any) => l.country)).size;
} catch (e) {
console.error('Error fetching locations:', e);
error = e instanceof Error ? e.message : 'Unknown error';
}
// Group locations by state
const byState = locations.reduce((acc: any, loc: any) => {
if (!acc[loc.state]) acc[loc.state] = [];
acc[loc.state].push(loc);
return acc;
}, {});
---
<AdminLayout title="Geo Intelligence">
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="spark-heading text-3xl">🗺️ Geo Intelligence</h1>
<p class="text-silver mt-1">Location targeting and geographic data</p>
</div>
<div class="flex gap-3">
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('import-modal'))">
📥 Import CSV
</button>
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('export-data', {detail: {collection: 'geo_intelligence'}}))">
📤 Export
</button>
<a href="/admin/collections/geo-intelligence/new" class="spark-btn-primary text-sm">
✨ New Location
</a>
</div>
</div>
{error && (
<div class="spark-card p-4 border-red-500 text-red-400">
<strong>Error:</strong> {error}
</div>
)}
<!-- Stats -->
<div class="grid grid-cols-4 gap-4">
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Locations</div>
<div class="spark-data text-3xl">{stats.total}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Cities</div>
<div class="spark-data text-3xl text-blue-400">{stats.cities}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">States</div>
<div class="spark-data text-3xl text-green-400">{stats.states}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Countries</div>
<div class="spark-data text-3xl text-purple-400">{stats.countries}</div>
</div>
</div>
<!-- Locations by State -->
<div class="space-y-4">
{Object.entries(byState).map(([state, locs]: [string, any]) => (
<div class="spark-card overflow-hidden">
<div class="p-4 border-b border-edge-subtle bg-graphite">
<div class="flex justify-between items-center">
<h3 class="text-white font-semibold">{state}</h3>
<span class="spark-label">{locs.length} locations</span>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-6">
{locs.map((loc: any) => (
<div class="p-4 bg-black/20 rounded border border-edge-subtle hover:border-gold/30 transition-colors">
<div class="text-white font-medium">{loc.city}</div>
{loc.zip && <div class="text-silver/50 text-sm">{loc.zip}</div>}
{loc.population && (
<div class="text-silver text-xs mt-1">
Pop: {loc.population.toLocaleString()}
</div>
)}
{loc.cluster && (
<div class="mt-2">
<span class="px-2 py-0.5 bg-gold/10 text-gold text-xs rounded">
{loc.cluster}
</span>
</div>
)}
</div>
))}
</div>
</div>
))}
{locations.length === 0 && !error && (
<div class="spark-card p-12 text-center">
<p class="text-silver/50">No locations found. Import your geo data!</p>
</div>
)}
</div>
</div>
</AdminLayout>
<script>
window.addEventListener('export-data', async (e: any) => {
const { collection } = e.detail;
const response = await fetch(`/api/collections/${collection}/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${collection}-${new Date().toISOString().split('T')[0]}.json`;
a.click();
});
</script>

View File

@@ -0,0 +1,138 @@
---
/**
* Headline Inventory Management
* Pre-written headlines library with spintax support
*/
import AdminLayout from '@/layouts/AdminLayout.astro';
import { getDirectusClient } from '@/lib/directus/client';
import { readItems } from '@directus/sdk';
const client = getDirectusClient();
let headlines = [];
let error = null;
let stats = {
total: 0,
byCategory: {} as Record<string, number>,
};
try {
headlines = await client.request(readItems('headline_inventory', {
fields: ['*'],
sort: ['-date_created'],
limit: 200,
}));
stats.total = headlines.length;
headlines.forEach((h: any) => {
const cat = h.category || 'uncategorized';
stats.byCategory[cat] = (stats.byCategory[cat] || 0) + 1;
});
} catch (e) {
console.error('Error fetching headlines:', e);
error = e instanceof Error ? e.message : 'Unknown error';
}
---
<AdminLayout title="Headline Inventory">
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="spark-heading text-3xl">💬 Headline Inventory</h1>
<p class="text-silver mt-1">Pre-written headlines library with spintax variations</p>
</div>
<div class="flex gap-3">
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('import-modal'))">
📥 Import
</button>
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('export-data', {detail: {collection: 'headline_inventory'}}))">
📤 Export
</button>
<a href="/admin/collections/headline-inventory/new" class="spark-btn-primary text-sm">
✨ New Headline
</a>
</div>
</div>
{error && (
<div class="spark-card p-4 border-red-500 text-red-400">
<strong>Error:</strong> {error}
</div>
)}
<!-- Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Headlines</div>
<div class="spark-data text-3xl">{stats.total}</div>
</div>
{Object.entries(stats.byCategory).slice(0, 3).map(([cat, count]) => (
<div class="spark-card p-6">
<div class="spark-label mb-2">{cat}</div>
<div class="spark-data text-3xl text-gold">{count}</div>
</div>
))}
</div>
<!-- Headlines List -->
<div class="space-y-3">
{headlines.map((headline: any) => (
<div class="spark-card spark-card-hover p-5">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-0.5 bg-gold/10 text-gold text-xs rounded">
{headline.category || 'general'}
</span>
{headline.spintax_count && (
<span class="px-2 py-0.5 bg-blue-500/10 text-blue-400 text-xs rounded">
{headline.spintax_count} variations
</span>
)}
</div>
<div class="text-white font-medium mb-1 leading-tight">
{headline.headline_text}
</div>
{headline.preview && (
<div class="text-silver/50 text-sm">
Example: {headline.preview}
</div>
)}
</div>
<div class="flex gap-2 flex-shrink-0">
<button
class="spark-btn-ghost text-xs px-3 py-1"
onclick={`navigator.clipboard.writeText(${JSON.stringify(headline.headline_text)})`}
>
📋 Copy
</button>
<a href={`/admin/collections/headline-inventory/${headline.id}`} class="spark-btn-ghost text-xs px-3 py-1">
Edit
</a>
</div>
</div>
</div>
))}
{headlines.length === 0 && !error && (
<div class="spark-card p-12 text-center">
<p class="text-silver/50">No headlines found. Start building your library!</p>
</div>
)}
</div>
</div>
</AdminLayout>
<script>
window.addEventListener('export-data', async (e: any) => {
const { collection } = e.detail;
const response = await fetch(`/api/collections/${collection}/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${collection}-${new Date().toISOString().split('T')[0]}.json`;
a.click();
});
</script>

View File

@@ -0,0 +1,162 @@
---
/** * Offer Blocks Management
* Call-to-action templates and offer messaging
*/
import AdminLayout from '@/layouts/AdminLayout.astro';
import { getDirectusClient } from '@/lib/directus/client';
import { readItems } from '@directus/sdk';
const client = getDirectusClient();
let offers = [];
let error = null;
let stats = {
total: 0,
byType: {} as Record<string, number>,
};
try {
offers = await client.request(readItems('offer_blocks', {
fields: ['*'],
sort: ['offer_type', 'title'],
}));
stats.total = offers.length;
offers.forEach((o: any) => {
const type = o.offer_type || 'general';
stats.byType[type] = (stats.byType[type] || 0) + 1;
});
} catch (e) {
console.error('Error fetching offers:', e);
error = e instanceof Error ? e.message : 'Unknown error';
}
---
<AdminLayout title="Offer Blocks">
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="spark-heading text-3xl">🎯 Offer Blocks</h1>
<p class="text-silver mt-1">Call-to-action templates and offer messaging</p>
</div>
<div class="flex gap-3">
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('import-modal'))">
📥 Import
</button>
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('export-data', {detail: {collection: 'offer_blocks'}}))">
📤 Export
</button>
<a href="/admin/collections/offer-blocks/new" class="spark-btn-primary text-sm">
✨ New Offer
</a>
</div>
</div>
{error && (
<div class="spark-card p-4 border-red-500 text-red-400">
<strong>Error:</strong> {error}
</div>
)}
<!-- Stats -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Offers</div>
<div class="spark-data text-3xl">{stats.total}</div>
</div>
{Object.entries(stats.byType).slice(0, 4).map(([type, count]) => (
<div class="spark-card p-6">
<div class="spark-label mb-2">{type}</div>
<div class="spark-data text-3xl text-gold">{count}</div>
</div>
))}
</div>
<!-- Offers Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{offers.map((offer: any) => (
<div class="spark-card spark-card-hover p-6">
<div class="flex items-start justify-between mb-3">
<div>
<h3 class="text-white font-semibold text-lg">{offer.title || 'Unnamed Offer'}</h3>
{offer.slug && (
<code class="text-xs text-silver/50 mt-1 block">{offer.slug}</code>
)}
</div>
<span class="px-3 py-1 bg-gold/10 text-gold text-xs rounded border border-gold/30">
{offer.offer_type || 'general'}
</span>
</div>
{offer.hook && (
<div class="mb-3 p-3 bg-black/30 rounded border border-edge-subtle">
<div class="spark-label mb-1">Hook:</div>
<div class="text-silver text-sm leading-relaxed">
{offer.hook.substring(0, 150)}{offer.hook.length > 150 ? '...' : ''}
</div>
</div>
)}
{offer.offer_text && (
<div class="mb-4 p-3 bg-black/30 rounded border border-edge-subtle">
<div class="spark-label mb-1">Offer Text:</div>
<div class="text-white font-medium text-sm leading-relaxed">
{offer.offer_text.substring(0, 120)}{offer.offer_text.length > 120 ? '...' : ''}
</div>
</div>
)}
{offer.avatar_pains && Array.isArray(offer.avatar_pains) && (
<div class="mb-4">
<div class="spark-label mb-2">Target Pains:</div>
<div class="flex flex-wrap gap-1">
{offer.avatar_pains.slice(0, 3).map((pain: string) => (
<span class="px-2 py-0.5 bg-red-500/10 text-red-400 text-xs rounded border border-red-500/20">
{pain}
</span>
))}
{offer.avatar_pains.length > 3 && (
<span class="px-2 py-0.5 text-xs text-silver/50">
+{offer.avatar_pains.length - 3} more
</span>
)}
</div>
</div>
)}
<div class="flex gap-2">
<button
class="spark-btn-ghost text-xs px-3 py-1 flex-1"
onclick={`navigator.clipboard.writeText(${JSON.stringify(offer.offer_text || offer.hook || '')})`}
>
📋 Copy
</button>
<a href={`/admin/collections/offer-blocks/${offer.id}`} class="spark-btn-ghost text-xs px-3 py-1 flex-1 text-center">
Edit
</a>
</div>
</div>
))}
{offers.length === 0 && !error && (
<div class="col-span-full spark-card p-12 text-center">
<p class="text-silver/50">No offers found. Create your first offer block!</p>
</div>
)}
</div>
</div>
</AdminLayout>
<script>
window.addEventListener('export-data', async (e: any) => {
const { collection } = e.detail;
const response = await fetch(`/api/collections/${collection}/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${collection}-${new Date().toISOString().split('T')[0]}.json`;
a.click();
});
</script>

View File

@@ -0,0 +1,179 @@
---
/**
* Spintax Dictionaries Management
* Word variation sets for content spinning
*/
import AdminLayout from '@/layouts/AdminLayout.astro';
import { getDirectusClient } from '@/lib/directus/client';
import { readItems } from '@directus/sdk';
const client = getDirectusClient();
let dictionaries = [];
let error = null;
let stats = {
total: 0,
totalWords: 0,
byCategory: {} as Record<string, number>,
};
try {
dictionaries = await client.request(readItems('spintax_dictionaries', {
fields: ['*'],
sort: ['category', 'base_word'],
}));
stats.total = dictionaries.length;
dictionaries.forEach((d: any) => {
const cat = d.category || 'general';
stats.byCategory[cat] = (stats.byCategory[cat] || 0) + 1;
if (d.variations && Array.isArray(d.variations)) {
stats.totalWords += d.variations.length;
}
});
} catch (e) {
console.error('Error fetching dictionaries:', e);
error = e instanceof Error ? e.message : 'Unknown error';
}
// Group by category
const byCategory = dictionaries.reduce((acc: any, dict: any) => {
const cat = dict.category || 'general';
if (!acc[cat]) acc[cat] = [];
acc[cat].push(dict);
return acc;
}, {});
---
<AdminLayout title="Spintax Dictionaries">
<div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center">
<div>
<h1 class="spark-heading text-3xl">📚 Spintax Dictionaries</h1>
<p class="text-silver mt-1">Word variation sets for content spinning</p>
</div>
<div class="flex gap-3">
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('import-modal'))">
📥 Import
</button>
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('export-data', {detail: {collection: 'spintax_dictionaries'}}))">
📤 Export
</button>
<a href="/admin/collections/spintax-dictionaries/new" class="spark-btn-primary text-sm">
✨ New Entry
</a>
</div>
</div>
{error && (
<div class="spark-card p-4 border-red-500 text-red-400">
<strong>Error:</strong> {error}
</div>
)}
<!-- Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Entries</div>
<div class="spark-data text-3xl">{stats.total}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Words</div>
<div class="spark-data text-3xl text-gold">{stats.totalWords}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Categories</div>
<div class="spark-data text-3xl text-blue-400">{Object.keys(stats.byCategory).length}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Avg Variations</div>
<div class="spark-data text-3xl text-green-400">
{stats.total > 0 ? Math.round(stats.totalWords / stats.total) : 0}
</div>
</div>
</div>
<!-- Dictionaries by Category -->
<div class="space-y-6">
{Object.entries(byCategory).map(([category, items]: [string, any]) => (
<div class="spark-card overflow-hidden">
<div class="p-4 border-b border-edge-subtle bg-graphite">
<div class="flex justify-between items-center">
<h3 class="text-white font-semibold capitalize">{category}</h3>
<span class="spark-label">{items.length} entries</span>
</div>
</div>
<div class="p-6 space-y-3">
{items.map((dict: any) => (
<div class="p-4 bg-black/20 rounded border border-edge-subtle hover:border-gold/30 transition-colors">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="text-white font-medium">{dict.base_word}</span>
{dict.variations && Array.isArray(dict.variations) && (
<span class="px-2 py-0.5 bg-gold/10 text-gold text-xs rounded">
{dict.variations.length} variations
</span>
)}
</div>
{dict.variations && Array.isArray(dict.variations) && (
<div class="flex flex-wrap gap-1">
{dict.variations.slice(0, 10).map((variation: string) => (
<span class="px-2 py-0.5 bg-graphite border border-edge-subtle rounded text-xs text-silver">
{variation}
</span>
))}
{dict.variations.length > 10 && (
<span class="px-2 py-0.5 text-xs text-silver/50">
+{dict.variations.length - 10} more
</span>
)}
</div>
)}
{dict.spintax_format && (
<div class="mt-2 p-2 bg-black/40 rounded">
<code class="text-xs text-green-400">{dict.spintax_format}</code>
</div>
)}
</div>
<div class="flex gap-2 flex-shrink-0">
<button
class="spark-btn-ghost text-xs px-3 py-1"
onclick={`navigator.clipboard.writeText(${JSON.stringify(dict.spintax_format || dict.base_word)})`}
>
📋
</button>
<a href={`/admin/collections/spintax-dictionaries/${dict.id}`} class="spark-btn-ghost text-xs px-3 py-1">
Edit
</a>
</div>
</div>
</div>
))}
</div>
</div>
))}
{dictionaries.length === 0 && !error && (
<div class="spark-card p-12 text-center">
<p class="text-silver/50">No spintax dictionaries found. Start building your word library!</p>
</div>
)}
</div>
</div>
</AdminLayout>
<script>
window.addEventListener('export-data', async (e: any) => {
const { collection } = e.detail;
const response = await fetch(`/api/collections/${collection}/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${collection}-${new Date().toISOString().split('T')[0]}.json`;
a.click();
});
</script>

View File

@@ -1,20 +1,90 @@
--- ---
/**
* Leads Management
* Customer lead tracking and management
*/
import Layout from '@/layouts/AdminLayout.astro'; import Layout from '@/layouts/AdminLayout.astro';
import LeadList from '@/components/admin/leads/LeadList'; import LeadList from '@/components/admin/leads/LeadList';
import { getDirectusClient } from '@/lib/directus/client';
import { readItems } from '@directus/sdk';
const client = getDirectusClient();
let stats = {
total: 0,
new: 0,
contacted: 0,
qualified: 0,
};
try {
const leads = await client.request(readItems('leads', {
fields: ['status'],
}));
stats.total = leads.length;
stats.new = leads.filter((l: any) => l.status === 'new').length;
stats.contacted = leads.filter((l: any) => l.status === 'contacted').length;
stats.qualified = leads.filter((l: any) => l.status === 'qualified').length;
} catch (e) {
console.error('Error fetching lead stats:', e);
}
--- ---
<Layout title="Leads & Inquiries"> <Layout title="Leads & Inquiries">
<div class="p-6 space-y-6"> <div class="space-y-6">
<!-- Header -->
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <div>
<h1 class="text-3xl font-bold text-slate-100">Leads</h1> <h1 class="spark-heading text-3xl">👤 Leads</h1>
<p class="text-slate-400">View form submissions and inquiries.</p> <p class="text-silver mt-1">Customer lead tracking and management</p>
</div>
<div class="flex gap-3">
<button class="spark-btn-secondary text-sm" onclick="window.dispatchEvent(new CustomEvent('export-data', {detail: {collection: 'leads'}}))">
📤 Export CSV
</button>
<a href="/admin/leads/new" class="spark-btn-primary text-sm">
✨ Add Lead
</a>
</div> </div>
<button class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg font-medium transition-colors">
Export CSV
</button>
</div> </div>
<LeadList client:load /> <!-- Stats -->
<div class="grid grid-cols-4 gap-4">
<div class="spark-card p-6">
<div class="spark-label mb-2">Total Leads</div>
<div class="spark-data text-3xl">{stats.total}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">New</div>
<div class="spark-data text-3xl text-yellow-400">{stats.new}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Contacted</div>
<div class="spark-data text-3xl text-blue-400">{stats.contacted}</div>
</div>
<div class="spark-card p-6">
<div class="spark-label mb-2">Qualified</div>
<div class="spark-data text-3xl text-green-400">{stats.qualified}</div>
</div>
</div>
<!-- Leads List -->
<div class="spark-card overflow-hidden">
<LeadList client:load />
</div>
</div> </div>
</Layout> </Layout>
<script>
window.addEventListener('export-data', async (e: any) => {
const { collection } = e.detail;
const response = await fetch(`/api/collections/${collection}/export`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${collection}-${new Date().toISOString().split('T')[0]}.csv`;
a.click();
});
</script>