F017 - Author Management Dashboard
Objective
Create a comprehensive dashboard where authors can manage their manuscripts, view analysis history, track progress, and access all features in one place.
Quick Implementation
Using MyStoryFlow Components
- Dashboard layout from
@mystoryflow/ui - DataGrid for manuscript list from
@mystoryflow/ui - Card components for stats from
@mystoryflow/ui - Navigation sidebar from
@mystoryflow/ui - Chart components for analytics from
@mystoryflow/ui
New Requirements
- Manuscript management logic
- Dashboard statistics
- Quick action buttons
- Activity timeline
MVP Implementation
1. Database Queries
-- Dashboard statistics view (in analyzer schema)
CREATE VIEW analyzer.author_dashboard_stats AS
SELECT
u.id as user_id,
COUNT(DISTINCT m.id) as total_manuscripts,
COUNT(DISTINCT ma.id) as total_analyses,
AVG(ma.overall_score) as average_score,
MAX(m.created_at) as last_upload_date,
COUNT(DISTINCT CASE WHEN m.created_at > NOW() - INTERVAL '30 days'
THEN m.id END) as manuscripts_last_30_days
FROM auth.users u
LEFT JOIN analyzer.manuscripts m ON m.user_id = u.id
LEFT JOIN analyzer.manuscript_analyses ma ON ma.manuscript_id = m.id
GROUP BY u.id;
-- Recent activity
CREATE VIEW analyzer.author_recent_activity AS
SELECT
'upload' as activity_type,
m.id as entity_id,
m.title as entity_name,
m.created_at as activity_date,
m.user_id
FROM analyzer.manuscripts m
UNION ALL
SELECT
'analysis' as activity_type,
ma.manuscript_id as entity_id,
m.title as entity_name,
ma.created_at as activity_date,
m.user_id
FROM analyzer.manuscript_analyses ma
JOIN analyzer.manuscripts m ON m.id = ma.manuscript_id
ORDER BY activity_date DESC;2. Dashboard Layout Component
// apps/analyzer-app/src/app/dashboard/page.tsx
import { Suspense } from 'react'
import {
DashboardShell,
DashboardHeader,
DashboardContent
} from '@mystoryflow/ui/dashboard'
import { getSupabaseBrowserClient } from '@mystoryflow/database'
import {
ManuscriptStats,
RecentManuscripts,
ActivityTimeline,
QuickActions
} from '@/components/dashboard'
export default async function DashboardPage() {
const supabase = getSupabaseBrowserClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return null
// Fetch dashboard data
const [stats, manuscripts, activity] = await Promise.all([
getDashboardStats(user.id),
getRecentManuscripts(user.id),
getRecentActivity(user.id)
])
return (
<DashboardShell>
<DashboardHeader
heading="Author Dashboard"
text="Manage your manuscripts and track your writing progress."
/>
<DashboardContent>
<div className="grid gap-6">
{/* Quick Actions */}
<QuickActions />
{/* Statistics Cards */}
<ManuscriptStats stats={stats} />
{/* Recent Manuscripts */}
<Suspense fallback={<div>Loading manuscripts...</div>}>
<RecentManuscripts manuscripts={manuscripts} />
</Suspense>
{/* Activity Timeline */}
<ActivityTimeline activities={activity} />
</div>
</DashboardContent>
</DashboardShell>
)
}
async function getDashboardStats(userId: string) {
const supabase = getSupabaseBrowserClient()
const { data } = await supabase
.from('analyzer.author_dashboard_stats')
.select('*')
.eq('user_id', userId)
.single()
return data
}
async function getRecentManuscripts(userId: string) {
const supabase = getSupabaseBrowserClient()
const { data } = await supabase
.from('analyzer.manuscripts')
.select(`
*,
manuscript_analyses (
overall_score,
created_at
),
genre_detections (
primary_genre
)
`)
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(5)
return data
}
async function getRecentActivity(userId: string) {
const supabase = getSupabaseBrowserClient()
const { data } = await supabase
.from('analyzer.author_recent_activity')
.select('*')
.eq('user_id', userId)
.limit(10)
return data
}3. Dashboard Components
// apps/analyzer-app/src/components/dashboard/ManuscriptStats.tsx
import { Card } from '@mystoryflow/ui'
import {
FileText,
TrendingUp,
Calendar,
BarChart3
} from 'lucide-react'
interface StatsProps {
stats: {
total_manuscripts: number
total_analyses: number
average_score: number
manuscripts_last_30_days: number
}
}
export function ManuscriptStats({ stats }: StatsProps) {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-primary/10 rounded-lg">
<FileText className="h-6 w-6 text-primary" />
</div>
<div>
<p className="text-sm text-muted-foreground">
Total Manuscripts
</p>
<p className="text-2xl font-bold">
{stats.total_manuscripts}
</p>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-blue-500/10 rounded-lg">
<BarChart3 className="h-6 w-6 text-blue-500" />
</div>
<div>
<p className="text-sm text-muted-foreground">
Analyses Run
</p>
<p className="text-2xl font-bold">
{stats.total_analyses}
</p>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-green-500/10 rounded-lg">
<TrendingUp className="h-6 w-6 text-green-500" />
</div>
<div>
<p className="text-sm text-muted-foreground">
Average Score
</p>
<p className="text-2xl font-bold">
{Math.round(stats.average_score)}%
</p>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-purple-500/10 rounded-lg">
<Calendar className="h-6 w-6 text-purple-500" />
</div>
<div>
<p className="text-sm text-muted-foreground">
This Month
</p>
<p className="text-2xl font-bold">
{stats.manuscripts_last_30_days}
</p>
</div>
</div>
</Card>
</div>
)
}
// apps/analyzer-app/src/components/dashboard/RecentManuscripts.tsx
import { Card, DataGrid, Badge, Button } from '@mystoryflow/ui'
import Link from 'next/link'
import { Eye, Download, RefreshCw } from 'lucide-react'
export function RecentManuscripts({ manuscripts }: { manuscripts: any[] }) {
const columns = [
{
header: 'Title',
accessor: 'title',
cell: (row: any) => (
<Link
href={`/manuscripts/${row.id}`}
className="font-medium hover:underline"
>
{row.title}
</Link>
)
},
{
header: 'Genre',
accessor: 'genre',
cell: (row: any) => (
<Badge variant="secondary">
{row.genre_detections?.[0]?.primary_genre || 'Unknown'}
</Badge>
)
},
{
header: 'Score',
accessor: 'score',
cell: (row: any) => {
const score = row.manuscript_analyses?.[0]?.overall_score
if (!score) return <span className="text-muted-foreground">-</span>
const variant = score >= 85 ? 'success' :
score >= 70 ? 'warning' : 'destructive'
return <Badge variant={variant}>{score}%</Badge>
}
},
{
header: 'Words',
accessor: 'word_count',
cell: (row: any) => row.word_count?.toLocaleString() || '-'
},
{
header: 'Actions',
accessor: 'actions',
cell: (row: any) => (
<div className="flex gap-2">
<Button size="sm" variant="ghost" asChild>
<Link href={`/manuscripts/${row.id}/report`}>
<Eye className="h-4 w-4" />
</Link>
</Button>
<Button size="sm" variant="ghost" asChild>
<Link href={`/api/reports/${row.id}/export?format=pdf`}>
<Download className="h-4 w-4" />
</Link>
</Button>
<Button size="sm" variant="ghost" asChild>
<Link href={`/manuscripts/${row.id}/analyze`}>
<RefreshCw className="h-4 w-4" />
</Link>
</Button>
</div>
)
}
]
return (
<Card>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Recent Manuscripts</h3>
<Button variant="outline" size="sm" asChild>
<Link href="/manuscripts">View All</Link>
</Button>
</div>
<DataGrid
data={manuscripts}
columns={columns}
className="border-none"
/>
</div>
</Card>
)
}
// apps/analyzer-app/src/components/dashboard/QuickActions.tsx
import { Button } from '@mystoryflow/ui'
import { Upload, FileText, Users, HelpCircle } from 'lucide-react'
import Link from 'next/link'
export function QuickActions() {
return (
<div className="grid gap-4 md:grid-cols-4">
<Button size="lg" asChild>
<Link href="/manuscripts/upload">
<Upload className="mr-2 h-5 w-5" />
Upload Manuscript
</Link>
</Button>
<Button size="lg" variant="outline" asChild>
<Link href="/manuscripts">
<FileText className="mr-2 h-5 w-5" />
My Manuscripts
</Link>
</Button>
<Button size="lg" variant="outline" asChild>
<Link href="/coaches">
<Users className="mr-2 h-5 w-5" />
Find a Coach
</Link>
</Button>
<Button size="lg" variant="outline" asChild>
<Link href="/help">
<HelpCircle className="mr-2 h-5 w-5" />
Help Center
</Link>
</Button>
</div>
)
}
// apps/analyzer-app/src/components/dashboard/ActivityTimeline.tsx
import { Card } from '@mystoryflow/ui'
import { FileText, BarChart3 } from 'lucide-react'
import { formatDistanceToNow } from 'date-fns'
export function ActivityTimeline({ activities }: { activities: any[] }) {
const getActivityIcon = (type: string) => {
switch (type) {
case 'upload':
return <FileText className="h-4 w-4" />
case 'analysis':
return <BarChart3 className="h-4 w-4" />
default:
return null
}
}
const getActivityText = (activity: any) => {
switch (activity.activity_type) {
case 'upload':
return `Uploaded "${activity.entity_name}"`
case 'analysis':
return `Analyzed "${activity.entity_name}"`
default:
return activity.entity_name
}
}
return (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">Recent Activity</h3>
<div className="space-y-4">
{activities.map((activity, index) => (
<div key={index} className="flex gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-muted rounded-full flex items-center justify-center">
{getActivityIcon(activity.activity_type)}
</div>
<div className="flex-1">
<p className="text-sm">{getActivityText(activity)}</p>
<p className="text-xs text-muted-foreground">
{formatDistanceToNow(new Date(activity.activity_date), {
addSuffix: true
})}
</p>
</div>
</div>
))}
{activities.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-8">
No activity yet. Upload your first manuscript to get started!
</p>
)}
</div>
</Card>
)
}4. Navigation Update
// apps/analyzer-app/src/components/navigation/sidebar.tsx
import {
Home,
FileText,
Upload,
BarChart3,
Users,
Settings,
HelpCircle
} from 'lucide-react'
export const dashboardNav = [
{
title: 'Dashboard',
href: '/dashboard',
icon: Home
},
{
title: 'My Manuscripts',
href: '/manuscripts',
icon: FileText
},
{
title: 'Upload New',
href: '/manuscripts/upload',
icon: Upload
},
{
title: 'Analytics',
href: '/analytics',
icon: BarChart3
},
{
title: 'Find Coaches',
href: '/coaches',
icon: Users
},
{
title: 'Settings',
href: '/settings',
icon: Settings
},
{
title: 'Help',
href: '/help',
icon: HelpCircle
}
]MVP Acceptance Criteria
- Dashboard with key statistics
- Recent manuscripts list with actions
- Quick action buttons
- Activity timeline
- Mobile-responsive layout
- Real-time data updates
- Navigation sidebar
- Performance optimized queries
Post-MVP Enhancements
- Writing goal tracking
- Progress charts over time
- Manuscript comparison view
- Export analytics
- Custom dashboard widgets
- Writing streak tracking
- Community feed integration
- AI writing tips widget
Implementation Time
- Development: 1.5 days
- Testing: 0.5 days
- Total: 2 days
Dependencies
- User authentication system
- Manuscript management system
- Analysis data availability
Next Feature
After completion, update 00_IMPLEMENTATION_ROADMAP.md with all documentation links.