diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..87843c2 --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,46 @@ +# 🔱 God Mode - Quick Handoff + +**Status:** ✅ Phases 1-7 Complete | 🚀 Phase 8 Ready +**Commit:** `7348a70` | **Build:** SUCCESS | **Git:** Clean + +## What's Done +- ✅ Content Generation Engine (database, spintax, APIs, worker) +- ✅ 70+ Admin pages (all UI complete) +- ✅ DevStatus component (shows what's missing on each page) +- ✅ 9 documentation files + +## What's Next (30 minutes) +Create 5 API endpoints to connect data to admin pages: + +1. `src/pages/api/collections/sites.ts` → Sites page +2. `src/pages/api/collections/campaign_masters.ts` → Campaigns +3. `src/pages/api/collections/posts.ts` → Posts +4. `src/pages/api/collections/avatars.ts` → Avatars +5. `src/pages/api/queue/status.ts` → Queue monitor + +## Template (Copy & Paste) +```typescript +// src/pages/api/collections/sites.ts +import { pool } from '../../../lib/db.ts'; + +export async function GET() { + const result = await pool.query('SELECT * FROM sites ORDER BY created_at DESC'); + return new Response(JSON.stringify({ data: result.rows }), { + headers: { 'Content-Type': 'application/json' } + }); +} +``` + +## Commands +```bash +npm run dev # Test locally +npm run build # Verify build +git push # Deploy +``` + +## Docs +- `ADMIN_MANUAL.md` - Every page explained +- `TECH_STACK.md` - Architecture +- `ERROR_CHECK_REPORT.md` - Build status + +**Ready to finish in one session!** 🚀 diff --git a/src/components/admin/pages/ContentLibrary.tsx b/src/components/admin/pages/ContentLibrary.tsx new file mode 100644 index 0000000..eabace5 --- /dev/null +++ b/src/components/admin/pages/ContentLibrary.tsx @@ -0,0 +1,190 @@ +import React, { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { getDirectusClient, readItems } from '@/lib/directus/client'; +import { Card } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Search, Blocks, Users, FileText, Layout } from 'lucide-react'; + +interface ContentLibraryProps { + onSelectBlock?: (blockType: string, config?: any) => void; + onSelectAvatar?: (avatar: any) => void; + onSelectFragment?: (fragment: any) => void; +} + +export function ContentLibrary({ onSelectBlock, onSelectAvatar, onSelectFragment }: ContentLibraryProps) { + const [searchTerm, setSearchTerm] = useState(''); + + // Fetch avatars + const { data: avatars, isLoading: loadingAvatars } = useQuery({ + queryKey: ['avatars'], + queryFn: async () => { + const client = getDirectusClient(); + return await client.request(readItems('avatar_intelligence', { + limit: 100, + fields: ['id', 'persona_name', 'pain_points', 'desires', 'demographics'] + })); + } + }); + + // Fetch content fragments + const { data: fragments, isLoading: loadingFragments } = useQuery({ + queryKey: ['content_fragments'], + queryFn: async () => { + const client = getDirectusClient(); + return await client.request(readItems('content_fragments', { + limit: 100, + fields: ['id', 'name', 'type', 'content'] + })); + } + }); + + // Fetch offer blocks + const { data: offers, isLoading: loadingOffers } = useQuery({ + queryKey: ['offer_blocks'], + queryFn: async () => { + const client = getDirectusClient(); + return await client.request(readItems('offer_blocks', { + limit: 100, + fields: ['id', 'title', 'offer_text', 'cta_text'] + })); + } + }); + + // Built-in block types + const blockTypes = [ + { id: 'hero', name: 'Hero Section', icon: '🦸', description: 'Large header with headline and CTA' }, + { id: 'features', name: 'Features Grid', icon: '⚡', description: '3-column feature showcase' }, + { id: 'content', name: 'Content Block', icon: '📝', description: 'Rich text content area' }, + { id: 'cta', name: 'Call to Action', icon: '🎯', description: 'Prominent CTA button' }, + { id: 'form', name: 'Lead Form', icon: '📋', description: 'Contact/signup form' }, + ]; + + const filteredAvatars = avatars?.filter(a => + a.persona_name?.toLowerCase().includes(searchTerm.toLowerCase()) + ) || []; + + const filteredFragments = fragments?.filter(f => + f.name?.toLowerCase().includes(searchTerm.toLowerCase()) + ) || []; + + return ( + +
+

Content Library

+
+ + setSearchTerm(e.target.value)} + className="pl-9 bg-zinc-800 border-zinc-700 text-white" + /> +
+
+ + + + + + Blocks + + + + Avatars + + + + Fragments + + + + Templates + + + +
+ + {blockTypes.map(block => ( + + ))} + + + + {loadingAvatars ? ( +
Loading avatars...
+ ) : filteredAvatars.length === 0 ? ( +
No avatars found
+ ) : ( + filteredAvatars.map((avatar: any) => ( + + )) + )} +
+ + + {loadingFragments ? ( +
Loading fragments...
+ ) : filteredFragments.length === 0 ? ( +
No fragments found
+ ) : ( + filteredFragments.map((fragment: any) => ( + + )) + )} +
+ + +
Pre-built page templates
+ {['Landing Page', 'Squeeze Page', 'Sales Page', 'About Page'].map(template => ( + + ))} +
+
+
+
+ ); +} diff --git a/src/components/admin/pages/EnhancedPageBuilder.tsx b/src/components/admin/pages/EnhancedPageBuilder.tsx new file mode 100644 index 0000000..a5f8d55 --- /dev/null +++ b/src/components/admin/pages/EnhancedPageBuilder.tsx @@ -0,0 +1,157 @@ +import React, { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { getDirectusClient, readItems } from '@/lib/directus/client'; +import VisualBlockEditor from '@/components/blocks/VisualBlockEditor'; +import { ContentLibrary } from './ContentLibrary'; +import { Button } from '@/components/ui/button'; +import { Save, Eye, Settings } from 'lucide-react'; + +interface EnhancedPageBuilderProps { + pageId?: string; + siteId: string; + onSave?: (blocks: any[]) => void; +} + +export function EnhancedPageBuilder({ pageId, siteId, onSave }: EnhancedPageBuilderProps) { + const [blocks, setBlocks] = useState([]); + const [selectedAvatar, setSelectedAvatar] = useState(null); + const [showPreview, setShowPreview] = useState(false); + + // Load existing page if editing + const { data: existingPage } = useQuery({ + queryKey: ['page', pageId], + queryFn: async () => { + if (!pageId) return null; + const client = getDirectusClient(); + return await client.request(readItems('pages', { + filter: { id: { _eq: pageId } }, + fields: ['*'] + })); + }, + enabled: !!pageId + }); + + const handleSave = async () => { + const client = getDirectusClient(); + + const pageData = { + site_id: siteId, + blocks: JSON.stringify(blocks), + status: 'draft' + }; + + if (pageId) { + // Update existing + await client.request({ + type: 'updateItem', + collection: 'pages', + id: pageId, + data: pageData + }); + } else { + // Create new + await client.request({ + type: 'createItem', + collection: 'pages', + data: pageData + }); + } + + onSave?.(blocks); + }; + + const handleSelectBlock = (blockType: string, config?: any) => { + const newBlock = { + id: `block-${Date.now()}`, + type: blockType, + config: config || {} + }; + setBlocks([...blocks, newBlock]); + }; + + const handleSelectAvatar = (avatar: any) => { + setSelectedAvatar(avatar); + }; + + return ( +
+ {/* Top toolbar */} +
+

Visual Page Builder

+ +
+ + + +
+
+ + {/* Main builder area */} +
+ {/* Left: Content Library */} +
+ +
+ + {/* Center: Visual Editor */} +
+ {showPreview ? ( +
+
+ {/* Preview of blocks */} +
+

Page Preview

+

Blocks: {blocks.length}

+ {blocks.map(block => ( +
+ {block.type} +
+ ))} +
+
+
+ ) : ( + + )} +
+ + {/* Right: Settings */} +
+
+ +

Settings

+
+ + {selectedAvatar && ( +
+
Active Avatar
+
{selectedAvatar.persona_name}
+
+ )} + +
+
Blocks: {blocks.length}
+
Site ID: {siteId}
+
+
+
+
+ ); +} diff --git a/src/lib/templates/funnels.ts b/src/lib/templates/funnels.ts new file mode 100644 index 0000000..bb721b1 --- /dev/null +++ b/src/lib/templates/funnels.ts @@ -0,0 +1,70 @@ +// Funnel page templates with pre-configured blocks + +export interface BlockConfig { + type: string; + config: Record; +} + +export interface PageTemplate { + id: string; + name: string; + description: string; + category: 'funnel' | 'general' | 'ecommerce'; + blocks: BlockConfig[]; +} + +export const FUNNEL_TEMPLATES: Record = { + landing_page: { + id: 'landing_page', + name: 'Landing Page', + description: 'Classic landing page with hero, features, and CTA', + category: 'funnel', + blocks: [ + { + type: 'hero', + config: { + headline: 'Transform Your Business Today', + subheadline: 'Join thousands of successful entrepreneurs', + ctaText: 'Get Started Free', + ctaUrl: '#signup', + } + }, + { + type: 'features', + config: { + title: 'Why Choose Us', + features: [ + { title: 'Fast Results', description: 'See results in days, not months', icon: '⚡' }, + { title: 'Expert Support', description: '24/7 dedicated support team', icon: '🎯' }, + { title: 'Proven System', description: 'Tested with 10,000+ users', icon: '✅' }, + ] + } + }, + ] + }, + + squeeze_page: { + id: 'squeeze_page', + name: 'Squeeze Page', + description: 'High-converting lead capture page', + category: 'funnel', + blocks: [ + { + type: 'hero', + config: { + headline: 'Get Our Free Guide', + subheadline: 'Download the complete blueprint to success', + ctaText: 'Download Now', + } + }, + ] + }, +}; + +export function getTemplateById(id: string): PageTemplate | undefined { + return FUNNEL_TEMPLATES[id]; +} + +export function getAllTemplates(): PageTemplate[] { + return Object.values(FUNNEL_TEMPLATES); +} diff --git a/src/lib/utils/avatar-injector.ts b/src/lib/utils/avatar-injector.ts new file mode 100644 index 0000000..dac8e53 --- /dev/null +++ b/src/lib/utils/avatar-injector.ts @@ -0,0 +1,16 @@ +// Avatar variable injection system + +interface Avatar { + id: string; + persona_name: string; + pain_points?: string; + desires?: string; + [key: string]: any; +} + +export function injectAvatarVariables(content: string, avatar: Avatar | null): string { + if (!avatar || !content) return content; + return content.replace(/\{\{avatar\.(\w+)\}\}/g, (match, fieldName) => { + return avatar[fieldName] || match; + }); +} diff --git a/src/pages/admin/pages/builder/[id].astro b/src/pages/admin/pages/builder/[id].astro new file mode 100644 index 0000000..24bf62c --- /dev/null +++ b/src/pages/admin/pages/builder/[id].astro @@ -0,0 +1,15 @@ +--- +import AdminLayout from '@/layouts/AdminLayout.astro'; +import { EnhancedPageBuilder } from '@/components/admin/pages/EnhancedPageBuilder'; + +const { id } = Astro.params; +const siteId = Astro.url.searchParams.get('site') || ''; +--- + + + +