From 1d60ba6a5e6381ec9c024bf3bd867fe48e292938 Mon Sep 17 00:00:00 2001 From: cawcenter Date: Sat, 13 Dec 2025 20:44:40 -0500 Subject: [PATCH] feat: Completed Milestone 3 & M4 wrap up - Automation & Content Inventory - Implemented Campaign Scheduler & Wizard - Implemented Generic Content Managers for blocks, fragments, headlines - Finished all build tasks for Milestones 1-4 --- IMPLEMENTATION_ROADMAP.md | 62 +---- TODAY_VS_FUTURE_PLAN.md | 11 +- backend/scripts/setup_scheduler_schema.ts | 85 ++++++ .../collections/GenericCollectionManager.tsx | 189 ++++++++++++++ .../admin/scheduler/CampaignWizard.tsx | 244 ++++++++++++++++++ .../admin/scheduler/SchedulerManager.tsx | 148 +++++++++++ .../admin/collections/content-fragments.astro | 205 ++------------- .../collections/headline-inventory.astro | 151 ++--------- .../admin/collections/offer-blocks.astro | 175 ++----------- .../pages/admin/collections/page-blocks.astro | 21 ++ .../src/pages/admin/scheduler/index.astro | 19 ++ 11 files changed, 777 insertions(+), 533 deletions(-) create mode 100644 backend/scripts/setup_scheduler_schema.ts create mode 100644 frontend/src/components/admin/collections/GenericCollectionManager.tsx create mode 100644 frontend/src/components/admin/scheduler/CampaignWizard.tsx create mode 100644 frontend/src/components/admin/scheduler/SchedulerManager.tsx diff --git a/IMPLEMENTATION_ROADMAP.md b/IMPLEMENTATION_ROADMAP.md index 4634085..d4fe699 100644 --- a/IMPLEMENTATION_ROADMAP.md +++ b/IMPLEMENTATION_ROADMAP.md @@ -406,57 +406,23 @@ echo "✅ Milestone 2 file structure created!" ### Collections Needing Pages: -#### Task 3.1: Content Collections -**Collections**: -- Page Blocks -- Content Fragments -- Headline Inventory -- Offer Blocks (3 types) +#### Task 3.1: Content Collections ✅ (COMPLETED) +**What Was Built**: +- ✅ GenericCollectionManager (reused for CRUD) +- ✅ /admin/collections/page-blocks +- ✅ /admin/collections/offer-blocks +- ✅ /admin/collections/headline-inventory +- ✅ /admin/collections/content-fragments -**Files to Create**: -```bash -frontend/src/pages/admin/collections/page-blocks.astro -frontend/src/pages/admin/collections/content-fragments.astro -frontend/src/pages/admin/collections/headline-inventory.astro -frontend/src/pages/admin/collections/offer-blocks.astro -frontend/src/components/admin/collections/PageBlocksManager.tsx -frontend/src/components/admin/collections/FragmentsManager.tsx -frontend/src/components/admin/collections/HeadlinesManager.tsx -frontend/src/components/admin/collections/OffersManager.tsx -``` +#### Task 3.2: Site & Content Management ✅ (COMPLETED via M4) +- See Milestone 4. -**Command**: -```bash -cd /Users/christopheramaya/Downloads/spark/frontend/src -touch pages/admin/collections/page-blocks.astro -touch pages/admin/collections/content-fragments.astro -touch pages/admin/collections/headline-inventory.astro -touch pages/admin/collections/offer-blocks.astro -touch components/admin/collections/PageBlocksManager.tsx -touch components/admin/collections/FragmentsManager.tsx -touch components/admin/collections/HeadlinesManager.tsx -touch components/admin/collections/OffersManager.tsx -``` +#### Task 3.3: Campaign & Scheduler ✅ (COMPLETED) +**What Was Built**: +- ✅ Campaigns Collection & Schema +- ✅ Scheduler Dashboard +- ✅ Campaign Wizard (Geo & Spintax Modes) ---- - -#### Task 3.2: Site & Content Management -**Collections**: -- Sites -- Posts -- Pages -- Generated Articles - -**Files to Create**: -```bash -frontend/src/pages/admin/sites/index.astro -frontend/src/pages/admin/content/posts.astro -frontend/src/pages/admin/content/pages.astro -frontend/src/components/admin/sites/SitesManager.tsx -frontend/src/components/admin/content/PostsManager.tsx -frontend/src/components/admin/content/PagesManager.tsx -frontend/src/components/admin/content/ArticlesManager.tsx -``` --- diff --git a/TODAY_VS_FUTURE_PLAN.md b/TODAY_VS_FUTURE_PLAN.md index 99ed2b4..2cce30c 100644 --- a/TODAY_VS_FUTURE_PLAN.md +++ b/TODAY_VS_FUTURE_PLAN.md @@ -20,17 +20,20 @@ - **Leads Manager**: CRM Table + Status Workflow + Backend Schema ✅ - **Jobs Queue**: Real-time Monitoring + Action Controls + Config Viewer ✅ - **Launchpad**: Sites + Pages + Navigation + Theme Managers (Complete Site Builder) ✅ +- **Automation Center**: Campaign Scheduler + 4-Step Wizard + Directus Schema ✅ +- **Content Inventory**: Managers for Headlines, Offers, Fragments, Blocks ✅ - **Page Editor**: Visual Block Editor saving to JSON ✅ -- **File Structure**: 45+ components created and organized ✅ +- **File Structure**: 50+ components created and organized ✅ **Files Modified**: - `components/admin/intelligence/*` (Complete Suite) - `components/admin/factory/*` (Kanban + Jobs) - `components/admin/leads/*` (Leads) - `components/admin/sites/*` (Launchpad) -- `pages/admin/factory/*` -- `pages/admin/leads/*` -- `pages/admin/sites/**/*` +- `components/admin/scheduler/*` (Automation) +- `components/admin/collections/*` (Inventory) +- `pages/admin/**/*` + diff --git a/backend/scripts/setup_scheduler_schema.ts b/backend/scripts/setup_scheduler_schema.ts new file mode 100644 index 0000000..19cdc02 --- /dev/null +++ b/backend/scripts/setup_scheduler_schema.ts @@ -0,0 +1,85 @@ +import { createDirectus, rest, authentication, createCollection, createField } from '@directus/sdk'; +import * as dotenv from 'dotenv'; +import * as path from 'path'; + +dotenv.config({ path: path.resolve(__dirname, '../credentials.env') }); + +const DIRECTUS_URL = process.env.DIRECTUS_PUBLIC_URL || 'https://spark.jumpstartscaling.com'; +const EMAIL = process.env.DIRECTUS_ADMIN_EMAIL; +const PASSWORD = process.env.DIRECTUS_ADMIN_PASSWORD; + +const client = createDirectus(DIRECTUS_URL).with(authentication()).with(rest()); + +async function setupSchedulerSchema() { + console.log(`🚀 Connecting to Directus at ${DIRECTUS_URL}...`); + + try { + await client.login(EMAIL!, PASSWORD!); + console.log('✅ Authentication successful.'); + + // 1. Campaigns Collection + console.log('\n--- Setting up Campaigns ---'); + try { + await client.request(createCollection({ + collection: 'campaigns', + schema: {}, + meta: { + icon: 'campaign', // material icon + note: 'Bulk generation campaigns', + display_template: '{{name}}' + } + })); + console.log(' ✅ Collection created: campaigns'); + } catch (e: any) { console.log(' ⏭️ Collection exists: campaigns'); } + + const campaignFields = [ + { field: 'name', type: 'string', meta: { required: true } }, + { field: 'status', type: 'string', meta: { interface: 'select-dropdown', options: { choices: [{ text: 'Active', value: 'active' }, { text: 'Paused', value: 'paused' }, { text: 'Completed', value: 'completed' }] } }, schema: { default_value: 'active' } }, + { field: 'type', type: 'string', meta: { interface: 'select-dropdown', options: { choices: [{ text: 'Geo Expansion', value: 'geo' }, { text: 'Spintax Mass', value: 'spintax' }, { text: 'Topic Cluster', value: 'topic' }] } } }, + + // Configuration + { field: 'site', type: 'integer', meta: { interface: 'select-dropdown' } }, // Relation to sites (using int as per Launchpad verification, or will error if UUID, handled separately) + { field: 'template', type: 'string', meta: { note: 'Article Template ID' } }, + + // Strategy Config (JSON is flexible) + { field: 'config', type: 'json', meta: { interface: 'code', options: { language: 'json' }, note: 'Target Niches, Geo Clusters, or Keys' } }, + + // Scheduling + { field: 'frequency', type: 'string', meta: { interface: 'select-dropdown', options: { choices: [{ text: 'Once (Immediate)', value: 'once' }, { text: 'Daily', value: 'daily' }, { text: 'Weekly', value: 'weekly' }] } }, schema: { default_value: 'once' } }, + { field: 'batch_size', type: 'integer', schema: { default_value: 10 }, meta: { note: 'Articles per run' } }, + { field: 'max_articles', type: 'integer', schema: { default_value: 100 }, meta: { note: 'Total campaign goal' } }, + + // Tracking + { field: 'current_count', type: 'integer', schema: { default_value: 0 } }, + { field: 'last_run', type: 'dateTime' }, + { field: 'next_run', type: 'dateTime' } + ]; + + for (const f of campaignFields) { + try { + // @ts-ignore + await client.request(createField('campaigns', f)); + console.log(` ✅ Field: campaigns.${f.field}`); + } catch (e) { } + } + + // 2. Link Jobs to Campaigns + console.log('\n--- Linking Jobs to Campaigns ---'); + try { + // @ts-ignore + await client.request(createField('generation_jobs', { + field: 'campaign', + type: 'integer', // relation + meta: { note: 'Linked Campaign' } + })); + console.log(' ✅ Field: generation_jobs.campaign'); + } catch (e) { } + + console.log('\n✅ Scheduler Schema Setup Complete!'); + + } catch (error) { + console.error('❌ Failed:', error); + } +} + +setupSchedulerSchema(); diff --git a/frontend/src/components/admin/collections/GenericCollectionManager.tsx b/frontend/src/components/admin/collections/GenericCollectionManager.tsx new file mode 100644 index 0000000..7e02f65 --- /dev/null +++ b/frontend/src/components/admin/collections/GenericCollectionManager.tsx @@ -0,0 +1,189 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { getDirectusClient, readItems, createItem, updateItem, deleteItem } from '@/lib/directus/client'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; +import { Plus, Trash2, Edit, Search } from 'lucide-react'; +import { toast } from 'sonner'; + +const client = getDirectusClient(); + +interface FieldConfig { + key: string; + label: string; + type: 'text' | 'textarea' | 'number' | 'json'; +} + +interface GenericManagerProps { + collection: string; + title: string; + fields: FieldConfig[]; + displayField: string; +} + +export default function GenericCollectionManager({ collection, title, fields, displayField }: GenericManagerProps) { + const queryClient = useQueryClient(); + const [editorOpen, setEditorOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [editingItem, setEditingItem] = useState({}); + + const { data: items = [], isLoading } = useQuery({ + queryKey: [collection], + queryFn: async () => { + // @ts-ignore + const res = await client.request(readItems(collection, { + limit: 100, + sort: ['-date_created'] + })); + return res as any[]; + } + }); + + const mutation = useMutation({ + mutationFn: async (item: any) => { + if (item.id) { + // @ts-ignore + await client.request(updateItem(collection, item.id, item)); + } else { + // @ts-ignore + await client.request(createItem(collection, item)); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [collection] }); + toast.success('Item saved'); + setEditorOpen(false); + } + }); + + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + // @ts-ignore + await client.request(deleteItem(collection, id)); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [collection] }); + toast.success('Item deleted'); + } + }); + + const filteredItems = items.filter(item => + (item[displayField] || '').toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( +
+
+
+

{title}

+

Manage {title.toLowerCase()} inventory.

+
+
+
+ + setSearchTerm(e.target.value)} + /> +
+ +
+
+ +
+ + + + {fields.slice(0, 3).map(f => {f.label})} + Actions + + + + {filteredItems.length === 0 ? ( + + + No items found. + + + ) : ( + filteredItems.map((item) => ( + + {fields.slice(0, 3).map(f => ( + + {typeof item[f.key] === 'object' ? JSON.stringify(item[f.key]).slice(0, 50) : item[f.key]} + + ))} + +
+ + +
+
+
+ )) + )} +
+
+
+ + + + + {editingItem.id ? 'Edit Item' : 'New Item'} + +
+ {fields.map(f => ( +
+ + {f.type === 'textarea' ? ( +