diff --git a/frontend/src/pages/admin/collections/avatar-variants.astro b/frontend/src/pages/admin/collections/avatar-variants.astro new file mode 100644 index 0000000..81bee6a --- /dev/null +++ b/frontend/src/pages/admin/collections/avatar-variants.astro @@ -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'; +} +--- + + +
+ +
+
+

🎭 Avatar Variants

+

Manage gender and tone variations

+
+
+ + + + ✨ New Variant + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + +
+
+
Total Variants
+
{stats.total}
+
+
+
Male
+
{stats.male}
+
+
+
Female
+
{stats.female}
+
+
+
Neutral
+
{stats.neutral}
+
+
+ + +
+
+

All Variants

+
+ +
+ + + + + + + + + + + + {items.map((variant: any, index: number) => ( + + + + + + + + ))} + +
AvatarTypePronounsIdentityActions
{variant.avatar_key} + + {variant.variant_type} + + {variant.pronoun}{variant.identity} + + Edit + +
+ + {items.length === 0 && !error && ( +
+

No variants found. Create your first one!

+
+ )} +
+
+
+
+ + diff --git a/frontend/src/pages/admin/collections/campaign-masters.astro b/frontend/src/pages/admin/collections/campaign-masters.astro new file mode 100644 index 0000000..8da6b68 --- /dev/null +++ b/frontend/src/pages/admin/collections/campaign-masters.astro @@ -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'; +} +--- + + +
+ +
+
+

πŸ“’ Campaign Masters

+

Manage marketing campaigns and content strategies

+
+
+ + + + ✨ New Campaign + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + +
+
+
Total Campaigns
+
{stats.total}
+
+
+
Active
+
{stats.active}
+
+
+
Draft
+
{stats.draft}
+
+
+
Completed
+
{stats.completed}
+
+
+ + +
+ {campaigns.map((campaign: any) => ( +
+
+
+

{campaign.campaign_name || 'Unnamed Campaign'}

+

+ {campaign.description?.substring(0, 100) || 'No description'} +

+
+ + {campaign.status || 'draft'} + +
+ +
+ {campaign.target_count && ( +
+ Targets: + {campaign.target_count} items +
+ )} + {campaign.articles_generated && ( +
+ Generated: + {campaign.articles_generated} articles +
+ )} +
+ + +
+ ))} + + {campaigns.length === 0 && !error && ( +
+

No campaigns found. Create your first campaign!

+
+ )} +
+
+
+ + diff --git a/frontend/src/pages/admin/collections/cartesian-patterns.astro b/frontend/src/pages/admin/collections/cartesian-patterns.astro new file mode 100644 index 0000000..c8dc53a --- /dev/null +++ b/frontend/src/pages/admin/collections/cartesian-patterns.astro @@ -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, +}; + +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'; +} +--- + + +
+ +
+
+

πŸ”§ Cartesian Patterns

+

Content structure templates and formulas

+
+
+ + + + ✨ New Pattern + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + +
+
+
Total Patterns
+
{stats.total}
+
+ {Object.entries(stats.byType).slice(0, 3).map(([type, count]) => ( +
+
{type}
+
{count}
+
+ ))} +
+ + +
+ {patterns.map((pattern: any) => ( +
+
+
+

{pattern.pattern_name || 'Unnamed Pattern'}

+ {pattern.description && ( +

{pattern.description}

+ )} +
+ + {pattern.structure_type || 'general'} + +
+ + {pattern.template && ( +
+
Template:
+
+ {pattern.template} +
+
+ )} + + {pattern.variables && Array.isArray(pattern.variables) && ( +
+
Variables:
+
+ {pattern.variables.map((variable: string) => ( + + {'{{'}{variable}{'}}'} + + ))} +
+
+ )} + + {pattern.example_output && ( +
+
Example Output:
+
+ "{pattern.example_output}" +
+
+ )} + +
+ + + Edit + + +
+
+ ))} + + {patterns.length === 0 && !error && ( +
+

No patterns found. Create your first content template!

+
+ )} +
+
+
+ + diff --git a/frontend/src/pages/admin/collections/content-fragments.astro b/frontend/src/pages/admin/collections/content-fragments.astro new file mode 100644 index 0000000..8455101 --- /dev/null +++ b/frontend/src/pages/admin/collections/content-fragments.astro @@ -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, + 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; +}, {}); +--- + + +
+ +
+
+

πŸ“¦ Content Fragments

+

Reusable content blocks for article assembly

+
+
+ + + + ✨ New Fragment + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + +
+
+
Total Fragments
+
{stats.total}
+
+
+
Total Words
+
{stats.totalWords.toLocaleString()}
+
+
+
Fragment Types
+
{Object.keys(stats.byType).length}
+
+
+
Avg Words
+
+ {stats.total > 0 ? Math.round(stats.totalWords / stats.total) : 0} +
+
+
+ + +
+ {Object.entries(byType).map(([type, items]: [string, any]) => ( +
+
+
+

{type}

+ {items.length} fragments +
+
+ +
+ {items.map((fragment: any) => ( +
+
+
+

+ {fragment.title || `${type} Fragment`} +

+ {fragment.description && ( +

{fragment.description}

+ )} +
+
+ {fragment.content && ( + + {fragment.content.split(/\s+/).length} words + + )} +
+
+ + {fragment.content && ( +
+
+ {fragment.content.substring(0, 300)} + {fragment.content.length > 300 && ( + ... + )} +
+
+ )} + + {fragment.variables && Array.isArray(fragment.variables) && fragment.variables.length > 0 && ( +
+
Variables:
+
+ {fragment.variables.map((variable: string) => ( + + {'{{'}{variable}{'}}'} + + ))} +
+
+ )} + +
+ + + Edit + + +
+
+ ))} +
+
+ ))} + + {fragments.length === 0 && !error && ( +
+

No content fragments found. Start building your content library!

+
+ )} +
+
+
+ + diff --git a/frontend/src/pages/admin/collections/generation-jobs.astro b/frontend/src/pages/admin/collections/generation-jobs.astro new file mode 100644 index 0000000..d2d427a --- /dev/null +++ b/frontend/src/pages/admin/collections/generation-jobs.astro @@ -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'; +} +--- + + +
+ +
+
+

βš™οΈ Generation Jobs

+

Content generation queue monitoring

+
+
+ + + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + +
+
+
Total Jobs
+
{stats.total}
+
+
+
Pending
+
{stats.pending}
+
+
+
Processing
+
{stats.processing}
+
+
+
Completed
+
{stats.completed}
+
+
+
Failed
+
{stats.failed}
+
+
+ + +
+
+

Recent Jobs

+
+ +
+ + + + + + + + + + + + + {jobs.map((job: any, index: number) => ( + + + + + + + + + ))} + +
Job IDTypeStatusProgressCreatedActions
+ {job.id.slice(0, 8)}... + {job.job_type || 'Article'} + + {job.status || 'pending'} + + +
+
+
+
+ {job.progress || 0}% +
+
+ {job.date_created ? new Date(job.date_created).toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) : 'Unknown'} + + {job.status === 'failed' && ( + + )} + {job.status === 'completed' && job.output_id && ( + + View Output + + )} +
+ + {jobs.length === 0 && !error && ( +
+

No generation jobs found. Queue is empty!

+
+ )} +
+
+
+
+ + diff --git a/frontend/src/pages/admin/collections/geo-intelligence.astro b/frontend/src/pages/admin/collections/geo-intelligence.astro new file mode 100644 index 0000000..0a7200d --- /dev/null +++ b/frontend/src/pages/admin/collections/geo-intelligence.astro @@ -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; +}, {}); +--- + + +
+ +
+
+

πŸ—ΊοΈ Geo Intelligence

+

Location targeting and geographic data

+
+
+ + + + ✨ New Location + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + +
+
+
Total Locations
+
{stats.total}
+
+
+
Cities
+
{stats.cities}
+
+
+
States
+
{stats.states}
+
+
+
Countries
+
{stats.countries}
+
+
+ + +
+ {Object.entries(byState).map(([state, locs]: [string, any]) => ( +
+
+
+

{state}

+ {locs.length} locations +
+
+ +
+ {locs.map((loc: any) => ( +
+
{loc.city}
+ {loc.zip &&
{loc.zip}
} + {loc.population && ( +
+ Pop: {loc.population.toLocaleString()} +
+ )} + {loc.cluster && ( +
+ + {loc.cluster} + +
+ )} +
+ ))} +
+
+ ))} + + {locations.length === 0 && !error && ( +
+

No locations found. Import your geo data!

+
+ )} +
+
+
+ + diff --git a/frontend/src/pages/admin/collections/headline-inventory.astro b/frontend/src/pages/admin/collections/headline-inventory.astro new file mode 100644 index 0000000..c30a32c --- /dev/null +++ b/frontend/src/pages/admin/collections/headline-inventory.astro @@ -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, +}; + +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'; +} +--- + + +
+ +
+
+

πŸ’¬ Headline Inventory

+

Pre-written headlines library with spintax variations

+
+
+ + + + ✨ New Headline + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + +
+
+
Total Headlines
+
{stats.total}
+
+ {Object.entries(stats.byCategory).slice(0, 3).map(([cat, count]) => ( +
+
{cat}
+
{count}
+
+ ))} +
+ + +
+ {headlines.map((headline: any) => ( +
+
+
+
+ + {headline.category || 'general'} + + {headline.spintax_count && ( + + {headline.spintax_count} variations + + )} +
+
+ {headline.headline_text} +
+ {headline.preview && ( +
+ Example: {headline.preview} +
+ )} +
+
+ + + Edit + +
+
+
+ ))} + + {headlines.length === 0 && !error && ( +
+

No headlines found. Start building your library!

+
+ )} +
+
+
+ + diff --git a/frontend/src/pages/admin/collections/offer-blocks.astro b/frontend/src/pages/admin/collections/offer-blocks.astro new file mode 100644 index 0000000..c21dbcf --- /dev/null +++ b/frontend/src/pages/admin/collections/offer-blocks.astro @@ -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, +}; + +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'; +} +--- + + +
+ +
+
+

🎯 Offer Blocks

+

Call-to-action templates and offer messaging

+
+
+ + + + ✨ New Offer + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + +
+
+
Total Offers
+
{stats.total}
+
+ {Object.entries(stats.byType).slice(0, 4).map(([type, count]) => ( +
+
{type}
+
{count}
+
+ ))} +
+ + +
+ {offers.map((offer: any) => ( +
+
+
+

{offer.title || 'Unnamed Offer'}

+ {offer.slug && ( + {offer.slug} + )} +
+ + {offer.offer_type || 'general'} + +
+ + {offer.hook && ( +
+
Hook:
+
+ {offer.hook.substring(0, 150)}{offer.hook.length > 150 ? '...' : ''} +
+
+ )} + + {offer.offer_text && ( +
+
Offer Text:
+
+ {offer.offer_text.substring(0, 120)}{offer.offer_text.length > 120 ? '...' : ''} +
+
+ )} + + {offer.avatar_pains && Array.isArray(offer.avatar_pains) && ( +
+
Target Pains:
+
+ {offer.avatar_pains.slice(0, 3).map((pain: string) => ( + + {pain} + + ))} + {offer.avatar_pains.length > 3 && ( + + +{offer.avatar_pains.length - 3} more + + )} +
+
+ )} + +
+ + + Edit + +
+
+ ))} + + {offers.length === 0 && !error && ( +
+

No offers found. Create your first offer block!

+
+ )} +
+
+
+ + diff --git a/frontend/src/pages/admin/collections/spintax-dictionaries.astro b/frontend/src/pages/admin/collections/spintax-dictionaries.astro new file mode 100644 index 0000000..b927b46 --- /dev/null +++ b/frontend/src/pages/admin/collections/spintax-dictionaries.astro @@ -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, +}; + +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; +}, {}); +--- + + +
+ +
+
+

πŸ“š Spintax Dictionaries

+

Word variation sets for content spinning

+
+
+ + + + ✨ New Entry + +
+
+ + {error && ( +
+ Error: {error} +
+ )} + + +
+
+
Total Entries
+
{stats.total}
+
+
+
Total Words
+
{stats.totalWords}
+
+
+
Categories
+
{Object.keys(stats.byCategory).length}
+
+
+
Avg Variations
+
+ {stats.total > 0 ? Math.round(stats.totalWords / stats.total) : 0} +
+
+
+ + +
+ {Object.entries(byCategory).map(([category, items]: [string, any]) => ( +
+
+
+

{category}

+ {items.length} entries +
+
+ +
+ {items.map((dict: any) => ( +
+
+
+
+ {dict.base_word} + {dict.variations && Array.isArray(dict.variations) && ( + + {dict.variations.length} variations + + )} +
+ {dict.variations && Array.isArray(dict.variations) && ( +
+ {dict.variations.slice(0, 10).map((variation: string) => ( + + {variation} + + ))} + {dict.variations.length > 10 && ( + + +{dict.variations.length - 10} more + + )} +
+ )} + {dict.spintax_format && ( +
+ {dict.spintax_format} +
+ )} +
+
+ + + Edit + +
+
+
+ ))} +
+
+ ))} + + {dictionaries.length === 0 && !error && ( +
+

No spintax dictionaries found. Start building your word library!

+
+ )} +
+
+
+ + diff --git a/frontend/src/pages/admin/leads/index.astro b/frontend/src/pages/admin/leads/index.astro index 3ac3bd9..392c954 100644 --- a/frontend/src/pages/admin/leads/index.astro +++ b/frontend/src/pages/admin/leads/index.astro @@ -1,20 +1,90 @@ --- +/** + * Leads Management + * Customer lead tracking and management + */ import Layout from '@/layouts/AdminLayout.astro'; 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); +} --- -
+
+
-

Leads

-

View form submissions and inquiries.

+

πŸ‘€ Leads

+

Customer lead tracking and management

+
+
+ + + ✨ Add Lead +
-
- + +
+
+
Total Leads
+
{stats.total}
+
+
+
New
+
{stats.new}
+
+
+
Contacted
+
{stats.contacted}
+
+
+
Qualified
+
{stats.qualified}
+
+
+ + +
+ +
+ +