Skip to content

User Interface Module Plan

Overview

The User Interface Module provides a modern, responsive Next.js application for users to request content generation, monitor job progress, manage strategies, and view generated content. Built with React, TypeScript, and Tailwind CSS, it integrates with Supabase for authentication and real-time updates.

Key Features

  • Content Request Dashboard: Intuitive interface for creating generation requests
  • Job Monitoring: Real-time status updates with progress indicators
  • Content Preview/Export: View and download generated content in multiple formats
  • Strategy Management: UI for updating client strategies and templates
  • Multi-Client Support: Switch between different clients/brands
  • Analytics Dashboard: View usage metrics and costs

Technology Stack

  • Framework: Next.js 14+ with App Router
  • UI Library: React 18+
  • Styling: Tailwind CSS + shadcn/ui components
  • State Management: Zustand + React Query
  • Authentication: Supabase Auth
  • Real-time: Supabase Realtime subscriptions
  • Forms: React Hook Form + Zod validation
  • Charts: Recharts for analytics

Application Structure

apps/frontend/
├── src/
│   ├── app/                    # Next.js App Router
│   │   ├── (auth)/            # Auth routes
│   │   │   ├── login/
│   │   │   └── register/
│   │   ├── (dashboard)/       # Protected routes
│   │   │   ├── layout.tsx
│   │   │   ├── page.tsx       # Main dashboard
│   │   │   ├── content/       # Content management
│   │   │   │   ├── new/
│   │   │   │   ├── [jobId]/
│   │   │   │   └── history/
│   │   │   ├── strategies/    # Strategy management
│   │   │   ├── analytics/     # Usage analytics
│   │   │   └── settings/      # User settings
│   │   ├── api/               # API routes
│   │   └── layout.tsx         # Root layout
│   ├── components/
│   │   ├── ui/                # shadcn/ui components
│   │   ├── content/           # Content-specific components
│   │   ├── dashboard/         # Dashboard components
│   │   └── shared/            # Shared components
│   ├── hooks/                 # Custom React hooks
│   ├── lib/                   # Utilities and configs
│   ├── stores/                # Zustand stores
│   └── types/                 # TypeScript types
├── public/                    # Static assets
└── package.json

Core Components

1. Main Dashboard

// app/(dashboard)/page.tsx
import { Suspense } from 'react'
import { ContentRequestForm } from '@/components/content/ContentRequestForm'
import { RecentJobs } from '@/components/dashboard/RecentJobs'
import { QuickStats } from '@/components/dashboard/QuickStats'
import { ClientSelector } from '@/components/shared/ClientSelector'

export default function DashboardPage() {
  return (
    <div className="container mx-auto py-6 space-y-6">
      <div className="flex justify-between items-center">
        <h1 className="text-3xl font-bold">Content Dashboard</h1>
        <ClientSelector />
      </div>

      <div className="grid gap-6 md:grid-cols-3">
        <Suspense fallback={<div>Loading stats...</div>}>
          <QuickStats />
        </Suspense>
      </div>

      <div className="grid gap-6 lg:grid-cols-2">
        <div>
          <h2 className="text-xl font-semibold mb-4">Create New Content</h2>
          <ContentRequestForm />
        </div>

        <div>
          <h2 className="text-xl font-semibold mb-4">Recent Jobs</h2>
          <Suspense fallback={<div>Loading jobs...</div>}>
            <RecentJobs />
          </Suspense>
        </div>
      </div>
    </div>
  )
}

2. Content Request Form

// components/content/ContentRequestForm.tsx
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { useContentStore } from '@/stores/content'
import { useRouter } from 'next/navigation'
import { toast } from 'sonner'

const contentSchema = z.object({
  topic: z.string().min(5, 'Topic must be at least 5 characters'),
  content_type: z.enum(['blog', 'social', 'local']),
  keywords: z.array(z.string()).optional(),
  length: z.enum(['short', 'medium', 'long']).optional(),
  priority: z.enum(['cost', 'quality', 'speed', 'balanced']).default('balanced')
})

type ContentFormData = z.infer<typeof contentSchema>

export function ContentRequestForm() {
  const router = useRouter()
  const { currentClient, createContentRequest } = useContentStore()
  const [keywords, setKeywords] = useState<string[]>([])
  const [keywordInput, setKeywordInput] = useState('')

  const form = useForm<ContentFormData>({
    resolver: zodResolver(contentSchema),
    defaultValues: {
      content_type: 'blog',
      priority: 'balanced',
      keywords: []
    }
  })

  const onSubmit = async (data: ContentFormData) => {
    try {
      const jobId = await createContentRequest({
        ...data,
        keywords,
        client_id: currentClient?.id
      })

      toast.success('Content request created!')
      router.push(`/content/${jobId}`)
    } catch (error) {
      toast.error('Failed to create request')
    }
  }

  const addKeyword = () => {
    if (keywordInput && !keywords.includes(keywordInput)) {
      setKeywords([...keywords, keywordInput])
      setKeywordInput('')
    }
  }

  return (
    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <Label htmlFor="topic">Topic</Label>
        <Textarea
          id="topic"
          placeholder="What should the content be about?"
          {...form.register('topic')}
        />
        {form.formState.errors.topic && (
          <p className="text-sm text-red-500">{form.formState.errors.topic.message}</p>
        )}
      </div>

      <div>
        <Label htmlFor="content_type">Content Type</Label>
        <Select
          value={form.watch('content_type')}
          onValueChange={(value) => form.setValue('content_type', value as any)}
        >
          <SelectTrigger>
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="blog">Blog Post</SelectItem>
            <SelectItem value="social">Social Media</SelectItem>
            <SelectItem value="local">Local Content</SelectItem>
          </SelectContent>
        </Select>
      </div>

      <div>
        <Label>Keywords</Label>
        <div className="flex gap-2">
          <Input
            value={keywordInput}
            onChange={(e) => setKeywordInput(e.target.value)}
            placeholder="Add keywords..."
            onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addKeyword())}
          />
          <Button type="button" onClick={addKeyword} variant="secondary">
            Add
          </Button>
        </div>
        <div className="flex flex-wrap gap-2 mt-2">
          {keywords.map((keyword) => (
            <Badge key={keyword} variant="secondary">
              {keyword}
              <button
                type="button"
                onClick={() => setKeywords(keywords.filter(k => k !== keyword))}
                className="ml-1 text-xs"
              >
                ×
              </button>
            </Badge>
          ))}
        </div>
      </div>

      <div className="grid grid-cols-2 gap-4">
        <div>
          <Label htmlFor="length">Length</Label>
          <Select
            value={form.watch('length')}
            onValueChange={(value) => form.setValue('length', value as any)}
          >
            <SelectTrigger>
              <SelectValue placeholder="Select length" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="short">Short</SelectItem>
              <SelectItem value="medium">Medium</SelectItem>
              <SelectItem value="long">Long</SelectItem>
            </SelectContent>
          </Select>
        </div>

        <div>
          <Label htmlFor="priority">Priority</Label>
          <Select
            value={form.watch('priority')}
            onValueChange={(value) => form.setValue('priority', value as any)}
          >
            <SelectTrigger>
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="cost">Lowest Cost</SelectItem>
              <SelectItem value="quality">Highest Quality</SelectItem>
              <SelectItem value="speed">Fastest</SelectItem>
              <SelectItem value="balanced">Balanced</SelectItem>
            </SelectContent>
          </Select>
        </div>
      </div>

      <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
        {form.formState.isSubmitting ? 'Creating...' : 'Generate Content'}
      </Button>
    </form>
  )
}

3. Job Status Monitor

// components/content/JobStatusMonitor.tsx
import { useEffect, useState } from 'react'
import { Card } from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { CheckCircle, XCircle, Clock, Download } from 'lucide-react'
import { useJob } from '@/hooks/useJob'
import { ContentPreview } from './ContentPreview'

interface JobStatusMonitorProps {
  jobId: string
}

export function JobStatusMonitor({ jobId }: JobStatusMonitorProps) {
  const { job, loading, error } = useJob(jobId)
  const [showPreview, setShowPreview] = useState(false)

  if (loading) return <div>Loading job details...</div>
  if (error) return <div>Error loading job: {error.message}</div>
  if (!job) return <div>Job not found</div>

  const getStatusIcon = () => {
    switch (job.status) {
      case 'completed':
        return <CheckCircle className="text-green-500" />
      case 'failed':
        return <XCircle className="text-red-500" />
      default:
        return <Clock className="text-yellow-500 animate-pulse" />
    }
  }

  const getProgress = () => {
    switch (job.status) {
      case 'pending':
        return 25
      case 'in_progress':
        return 50
      case 'completed':
      case 'failed':
        return 100
      default:
        return 0
    }
  }

  const handleExport = (format: 'txt' | 'md' | 'html') => {
    if (!job.result) return

    let content = job.result.content
    let filename = `${job.result.title || 'content'}.${format}`
    let mimeType = 'text/plain'

    if (format === 'html') {
      content = `<!DOCTYPE html>
<html>
<head><title>${job.result.title}</title></head>
<body>${content.replace(/\n/g, '<br>')}</body>
</html>`
      mimeType = 'text/html'
    } else if (format === 'md') {
      mimeType = 'text/markdown'
    }

    const blob = new Blob([content], { type: mimeType })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = filename
    a.click()
    URL.revokeObjectURL(url)
  }

  return (
    <div className="space-y-6">
      <Card className="p-6">
        <div className="flex items-center justify-between mb-4">
          <h2 className="text-xl font-semibold flex items-center gap-2">
            Job Status
            {getStatusIcon()}
          </h2>
          <Badge>{job.status}</Badge>
        </div>

        <Progress value={getProgress()} className="mb-4" />

        <div className="grid grid-cols-2 gap-4 text-sm">
          <div>
            <p className="text-gray-500">Job ID</p>
            <p className="font-mono">{job.id}</p>
          </div>
          <div>
            <p className="text-gray-500">Created</p>
            <p>{new Date(job.created_at).toLocaleString()}</p>
          </div>
          <div>
            <p className="text-gray-500">Client</p>
            <p>{job.params.client_id}</p>
          </div>
          <div>
            <p className="text-gray-500">Content Type</p>
            <p className="capitalize">{job.params.content_type}</p>
          </div>
        </div>

        {job.error && (
          <div className="mt-4 p-3 bg-red-50 text-red-700 rounded">
            Error: {job.error}
          </div>
        )}
      </Card>

      {job.status === 'completed' && job.result && (
        <>
          <Card className="p-6">
            <div className="flex items-center justify-between mb-4">
              <h3 className="text-lg font-semibold">Generated Content</h3>
              <div className="flex gap-2">
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => setShowPreview(!showPreview)}
                >
                  {showPreview ? 'Hide' : 'Show'} Preview
                </Button>
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => handleExport('txt')}
                >
                  <Download className="w-4 h-4 mr-1" />
                  TXT
                </Button>
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => handleExport('md')}
                >
                  <Download className="w-4 h-4 mr-1" />
                  MD
                </Button>
                <Button
                  size="sm"
                  variant="outline"
                  onClick={() => handleExport('html')}
                >
                  <Download className="w-4 h-4 mr-1" />
                  HTML
                </Button>
              </div>
            </div>

            <div className="grid grid-cols-3 gap-4 text-sm mb-4">
              <div>
                <p className="text-gray-500">Word Count</p>
                <p className="font-semibold">{job.result.metadata.word_count}</p>
              </div>
              <div>
                <p className="text-gray-500">Model Used</p>
                <p className="font-semibold">{job.result.metadata.model}</p>
              </div>
              <div>
                <p className="text-gray-500">Generation Cost</p>
                <p className="font-semibold">${job.result.metadata.generation_cost.toFixed(4)}</p>
              </div>
            </div>

            {showPreview && (
              <ContentPreview content={job.result.content} />
            )}
          </Card>

          <Card className="p-6">
            <h3 className="text-lg font-semibold mb-4">SEO Analysis</h3>
            <div className="space-y-2">
              {job.result.metadata.keywords_used.map((keyword: string) => (
                <div key={keyword} className="flex items-center justify-between">
                  <span>{keyword}</span>
                  <Badge variant="outline" className="text-xs">
                    Used
                  </Badge>
                </div>
              ))}
            </div>
            {job.result.metadata.seo_notes && (
              <p className="text-sm text-gray-600 mt-4">
                {job.result.metadata.seo_notes}
              </p>
            )}
          </Card>
        </>
      )}
    </div>
  )
}

4. Analytics Dashboard

// components/dashboard/AnalyticsDashboard.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
import { useAnalytics } from '@/hooks/useAnalytics'

export function AnalyticsDashboard() {
  const { data, loading } = useAnalytics()

  if (loading) return <div>Loading analytics...</div>

  const costByProvider = data?.costByProvider || []
  const dailyCosts = data?.dailyCosts || []
  const contentTypeDistribution = data?.contentTypeDistribution || []

  const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']

  return (
    <div className="space-y-6">
      <div className="grid gap-4 md:grid-cols-4">
        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">Total Spent</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">${data?.totalSpent.toFixed(2)}</div>
            <p className="text-xs text-muted-foreground">This month</p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">Content Generated</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{data?.totalContent}</div>
            <p className="text-xs text-muted-foreground">Total pieces</p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">Avg Cost</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">${data?.avgCostPerContent.toFixed(3)}</div>
            <p className="text-xs text-muted-foreground">Per content</p>
          </CardContent>
        </Card>

        <Card>
          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
            <CardTitle className="text-sm font-medium">Success Rate</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="text-2xl font-bold">{data?.successRate.toFixed(1)}%</div>
            <p className="text-xs text-muted-foreground">Completed jobs</p>
          </CardContent>
        </Card>
      </div>

      <Tabs defaultValue="costs" className="space-y-4">
        <TabsList>
          <TabsTrigger value="costs">Costs</TabsTrigger>
          <TabsTrigger value="usage">Usage</TabsTrigger>
          <TabsTrigger value="performance">Performance</TabsTrigger>
        </TabsList>

        <TabsContent value="costs" className="space-y-4">
          <Card>
            <CardHeader>
              <CardTitle>Daily Costs</CardTitle>
            </CardHeader>
            <CardContent>
              <ResponsiveContainer width="100%" height={300}>
                <LineChart data={dailyCosts}>
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis dataKey="date" />
                  <YAxis />
                  <Tooltip formatter={(value) => `$${value.toFixed(2)}`} />
                  <Line type="monotone" dataKey="cost" stroke="#8884d8" />
                </LineChart>
              </ResponsiveContainer>
            </CardContent>
          </Card>

          <Card>
            <CardHeader>
              <CardTitle>Cost by Provider</CardTitle>
            </CardHeader>
            <CardContent>
              <ResponsiveContainer width="100%" height={300}>
                <BarChart data={costByProvider}>
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis dataKey="provider" />
                  <YAxis />
                  <Tooltip formatter={(value) => `$${value.toFixed(2)}`} />
                  <Bar dataKey="cost" fill="#8884d8" />
                </BarChart>
              </ResponsiveContainer>
            </CardContent>
          </Card>
        </TabsContent>

        <TabsContent value="usage" className="space-y-4">
          <Card>
            <CardHeader>
              <CardTitle>Content Type Distribution</CardTitle>
            </CardHeader>
            <CardContent>
              <ResponsiveContainer width="100%" height={300}>
                <PieChart>
                  <Pie
                    data={contentTypeDistribution}
                    cx="50%"
                    cy="50%"
                    labelLine={false}
                    label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
                    outerRadius={80}
                    fill="#8884d8"
                    dataKey="value"
                  >
                    {contentTypeDistribution.map((entry, index) => (
                      <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
                    ))}
                  </Pie>
                  <Tooltip />
                </PieChart>
              </ResponsiveContainer>
            </CardContent>
          </Card>
        </TabsContent>
      </Tabs>
    </div>
  )
}

State Management

Content Store (Zustand)

// stores/content.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { createClient } from '@supabase/supabase-js'

interface Client {
  id: string
  name: string
  type: string
}

interface ContentStore {
  currentClient: Client | null
  setCurrentClient: (client: Client) => void

  createContentRequest: (params: any) => Promise<string>
  getJob: (jobId: string) => Promise<any>
  getRecentJobs: (limit?: number) => Promise<any[]>
}

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

export const useContentStore = create<ContentStore>()(
  persist(
    (set, get) => ({
      currentClient: null,

      setCurrentClient: (client) => set({ currentClient: client }),

      createContentRequest: async (params) => {
        const { currentClient } = get()
        if (!currentClient) throw new Error('No client selected')

        const response = await fetch('/api/content/generate', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            ...params,
            client_id: currentClient.id
          })
        })

        if (!response.ok) throw new Error('Failed to create request')

        const { job_id } = await response.json()
        return job_id
      },

      getJob: async (jobId) => {
        const { data } = await supabase
          .from('jobs')
          .select('*')
          .eq('id', jobId)
          .single()

        return data
      },

      getRecentJobs: async (limit = 10) => {
        const { currentClient } = get()
        if (!currentClient) return []

        const { data } = await supabase
          .from('jobs')
          .select('*')
          .eq('client_id', currentClient.id)
          .order('created_at', { ascending: false })
          .limit(limit)

        return data || []
      }
    }),
    {
      name: 'content-storage',
      partialize: (state) => ({ currentClient: state.currentClient })
    }
  )
)

Hooks

useJob Hook with Real-time Updates

// hooks/useJob.ts
import { useEffect, useState } from 'react'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

export function useJob(jobId: string) {
  const [job, setJob] = useState<any>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    // Initial load
    loadJob()

    // Subscribe to updates
    const subscription = supabase
      .channel(`job:${jobId}`)
      .on('postgres_changes', 
        { 
          event: 'UPDATE', 
          schema: 'public', 
          table: 'jobs',
          filter: `id=eq.${jobId}`
        }, 
        (payload) => {
          setJob(payload.new)
        }
      )
      .subscribe()

    return () => {
      subscription.unsubscribe()
    }
  }, [jobId])

  const loadJob = async () => {
    try {
      const { data, error } = await supabase
        .from('jobs')
        .select('*')
        .eq('id', jobId)
        .single()

      if (error) throw error

      setJob(data)
    } catch (err) {
      setError(err as Error)
    } finally {
      setLoading(false)
    }
  }

  return { job, loading, error, refetch: loadJob }
}

API Routes

Content Generation API

// app/api/content/generate/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
)

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()

    // Call CPM service
    const response = await fetch(`${process.env.CPM_SERVICE_URL}/generate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body)
    })

    if (!response.ok) {
      throw new Error('CPM service error')
    }

    const data = await response.json()
    return NextResponse.json(data)

  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to generate content' },
      { status: 500 }
    )
  }
}

Authentication Flow

// app/(auth)/login/page.tsx
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

export default function LoginPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="w-full max-w-md">
        <h1 className="text-2xl font-bold text-center mb-6">
          Content Generation System
        </h1>
        <Auth
          supabaseClient={supabase}
          appearance={{ theme: ThemeSupa }}
          providers={['google', 'github']}
          redirectTo={`${window.location.origin}/auth/callback`}
        />
      </div>
    </div>
  )
}

Environment Variables

# .env.local
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_KEY=your_supabase_service_key

# Service URLs
CPM_SERVICE_URL=http://localhost:8000
IM_SERVICE_URL=http://localhost:8001

# Production URLs (set in Vercel)
# CPM_SERVICE_URL=https://cpm.railway.app
# IM_SERVICE_URL=https://im.railway.app

Deployment Configuration

Vercel Configuration

// vercel.json
{
  "buildCommand": "cd ../.. && pnpm build --filter=frontend",
  "installCommand": "pnpm install",
  "framework": "nextjs",
  "outputDirectory": ".next",
  "rewrites": [
    {
      "source": "/api/cpm/:path*",
      "destination": "https://cpm.railway.app/:path*"
    },
    {
      "source": "/api/im/:path*",
      "destination": "https://im.railway.app/:path*"
    }
  ]
}

V2 Enhancements

1. Advanced Content Editor

// components/content/AdvancedEditor.tsx
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Highlight from '@tiptap/extension-highlight'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'

export function AdvancedEditor({ content, onChange }) {
  const editor = useEditor({
    extensions: [
      StarterKit,
      Highlight,
      TaskList,
      TaskItem,
    ],
    content,
    onUpdate: ({ editor }) => {
      onChange(editor.getHTML())
    },
  })

  return (
    <div className="prose prose-sm max-w-none">
      <EditorContent editor={editor} />
    </div>
  )
}

2. Collaborative Features

// Real-time collaboration using Supabase Presence
const presence = supabase.channel('content-collaboration')
  .on('presence', { event: 'sync' }, () => {
    const state = presence.presenceState()
    // Update UI with active users
  })
  .on('broadcast', { event: 'cursor' }, ({ payload }) => {
    // Update cursor positions
  })
  .subscribe()

3. Mobile App (React Native)

// Basic structure for mobile companion app
import { NavigationContainer } from '@react-navigation/native'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'

const Tab = createBottomTabNavigator()

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen name="Dashboard" component={DashboardScreen} />
        <Tab.Screen name="Create" component={CreateScreen} />
        <Tab.Screen name="History" component={HistoryScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  )
}

Performance Optimization

1. Code Splitting

// Dynamic imports for heavy components
const AnalyticsDashboard = dynamic(
  () => import('@/components/dashboard/AnalyticsDashboard'),
  { ssr: false }
)

2. Data Fetching Optimization

// Use React Query for caching and prefetching
import { useQuery } from '@tanstack/react-query'

export function useRecentJobs() {
  return useQuery({
    queryKey: ['jobs', 'recent'],
    queryFn: async () => {
      const { data } = await supabase
        .from('jobs')
        .select('*')
        .order('created_at', { ascending: false })
        .limit(10)
      return data
    },
    staleTime: 30000, // 30 seconds
    refetchInterval: 60000, // 1 minute
  })
}

Success Metrics

MVP Goals

  • Page load time: <2s
  • Time to interactive: <3s
  • Lighthouse score: >90
  • User satisfaction: >80%

V2 Goals

  • Real-time updates: <100ms latency
  • Mobile app adoption: 30% of users
  • Collaborative features usage: 50% of teams
  • Advanced editor adoption: 60% of content creators