{title}
++ š Content Factory + Beta +
++ Drag and drop articles to move them through the production pipeline. +
+diff --git a/backend/scripts/check_status.ts b/backend/scripts/check_status.ts new file mode 100644 index 0000000..5462186 --- /dev/null +++ b/backend/scripts/check_status.ts @@ -0,0 +1,24 @@ +import { createDirectus, rest, authentication, readField } 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 checkStatusField() { + await client.login(EMAIL!, PASSWORD!); + + try { + const field = await client.request(readField('generated_articles', 'status')); + console.log('Status Field Choices:', JSON.stringify(field.meta?.options?.choices, null, 2)); + } catch (e: any) { + console.error('Error reading status field:', e.message); + } +} + +checkStatusField(); diff --git a/backend/scripts/setup_factory_schema.ts b/backend/scripts/setup_factory_schema.ts new file mode 100644 index 0000000..287a419 --- /dev/null +++ b/backend/scripts/setup_factory_schema.ts @@ -0,0 +1,118 @@ +import { createDirectus, rest, authentication, createField, updateCollection, readCollections } 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 setupFactorySchema() { + console.log(`š Connecting to Directus at ${DIRECTUS_URL}...`); + + try { + console.log(`š Authenticating as ${EMAIL}...`); + await client.login(EMAIL!, PASSWORD!); + console.log('ā Authentication successful.'); + + // 1. Setup Kanban Status Field + console.log('\n--- Setting up Kanban Status ---'); + try { + await client.request(createField('generated_articles', { + field: 'status', + type: 'string', + meta: { + interface: 'select-dropdown', + options: { + choices: [ + { text: 'Queued', value: 'queued', color: '#6366f1' }, // Indigo + { text: 'Processing', value: 'processing', color: '#eab308' }, // Yellow + { text: 'QC Review', value: 'qc', color: '#a855f7' }, // Purple + { text: 'Approved', value: 'approved', color: '#22c55e' }, // Green + { text: 'Published', value: 'published', color: '#10b981' } // Emerald + ] + }, + display: 'labels', + display_options: { + show_as_dot: true + }, + note: 'Current stage in the content factory' + }, + schema: { + default_value: 'queued' + } + })); + console.log(' ā Field created: generated_articles.status'); + } catch (e: any) { + if (e.errors?.[0]?.extensions?.code === 'FIELD_DUPLICATE') { + console.log(' āļø Field exists: generated_articles.status'); + } else { + console.log(' ā Error creating status field:', e.message); + } + } + + // 2. Add CRM Fields + console.log('\n--- Adding CRM Fields ---'); + const crmFields = [ + { field: 'priority', type: 'string', note: 'Article priority', choices: [{ text: 'High', value: 'high' }, { text: 'Medium', value: 'medium' }, { text: 'Low', value: 'low' }] }, + { field: 'due_date', type: 'date', note: 'Target publication date' }, + { field: 'assignee', type: 'string', note: 'Team member responsible' }, + { field: 'seo_score', type: 'integer', note: 'Rankmath/Yoast score' }, + { field: 'notes', type: 'text', note: 'Internal notes' } + ]; + + for (const f of crmFields) { + try { + const meta: any = { note: f.note }; + if (f.choices) { + meta.interface = 'select-dropdown'; + meta.options = { choices: f.choices }; + } + await client.request(createField('generated_articles', { + field: f.field, + type: f.type, + meta, + schema: {} + })); + console.log(` ā Field created: generated_articles.${f.field}`); + } catch (e: any) { + if (e.errors?.[0]?.extensions?.code === 'FIELD_DUPLICATE') { + console.log(` āļø Field exists: generated_articles.${f.field}`); + } + } + } + + // 3. Fix Visual Preview URL + console.log('\n--- Fixing Visual Preview Link ---'); + // We need to update the collection metadata to point to our Astro frontend + // Note: The URL must be absolute or relative to the Directus root if served together. + // Since frontend is separate, we use the absolute URL of the deployed frontend. + // Assuming user acts as the frontend base safely. + + // We'll set it to the Vercel/Coolify URL + const FRONTEND_URL = 'https://launch.jumpstartscaling.com'; + + try { + await client.request(updateCollection('generated_articles', { + meta: { + preview_url: `${FRONTEND_URL}/preview/article/{id}` + } + })); + console.log(` ā Updated preview_url to: ${FRONTEND_URL}/preview/article/{id}`); + } catch (e: any) { + console.log(' ā Error updating collection metadata:', e.message); + } + + console.log('\nā Factory Setup Complete!'); + console.log('Your Directus "generated_articles" collection now supports Kanban & CRM features.'); + + } catch (error) { + console.error('ā Failed:', error); + process.exit(1); + } +} + +setupFactorySchema(); diff --git a/backend/scripts/update_status_choices.ts b/backend/scripts/update_status_choices.ts new file mode 100644 index 0000000..eacf0f6 --- /dev/null +++ b/backend/scripts/update_status_choices.ts @@ -0,0 +1,39 @@ +import { createDirectus, rest, authentication, updateField } 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 updateStatusChoices() { + await client.login(EMAIL!, PASSWORD!); + + try { + console.log('Updating status field choices for Kanban...'); + await client.request(updateField('generated_articles', 'status', { + meta: { + options: { + choices: [ + { text: 'Queued', value: 'queued', color: '#6366f1' }, // Indigo + { text: 'Processing', value: 'processing', color: '#eab308' }, // Yellow + { text: 'QC Review', value: 'qc', color: '#a855f7' }, // Purple + { text: 'Approved', value: 'approved', color: '#22c55e' }, // Green + { text: 'Published', value: 'published', color: '#10b981' }, // Emerald + { text: 'Draft', value: 'draft', color: '#94a3b8' }, // Legacy/Grey + { text: 'Archived', value: 'archived', color: '#475569' } // Legacy/Slate + ] + } + } + })); + console.log('ā Status choices updated successfully!'); + } catch (e: any) { + console.error('ā Error updating status field:', e.message); + } +} + +updateStatusChoices(); diff --git a/frontend/src/components/admin/factory/ArticleCard.tsx b/frontend/src/components/admin/factory/ArticleCard.tsx index e69de29..0a4d9b6 100644 --- a/frontend/src/components/admin/factory/ArticleCard.tsx +++ b/frontend/src/components/admin/factory/ArticleCard.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { formatDistanceToNow } from 'date-fns'; +import { FileText, Calendar, User, Eye, ArrowRight, MoreHorizontal } from 'lucide-react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { cn } from '@/lib/utils'; + +export interface Article { + id: string; + title: string; + slug: string; + status: string; + priority: 'high' | 'medium' | 'low'; + due_date?: string; + assignee?: string; + date_created: string; +} + +interface ArticleCardProps { + article: Article; + onPreview: (id: string) => void; +} + +export const ArticleCard = ({ article, onPreview }: ArticleCardProps) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ id: article.id, data: { status: article.status } }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + const getPriorityColor = (p: string) => { + switch (p) { + case 'high': return 'bg-red-500/10 text-red-500 border-red-500/20'; + case 'medium': return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20'; + case 'low': return 'bg-blue-500/10 text-blue-500 border-blue-500/20'; + default: return 'bg-zinc-500/10 text-zinc-500 border-zinc-500/20'; + } + }; + + return ( +
+ Drag and drop articles to move them through the production pipeline. +
+