ai-wpa/app/dashboard/page.tsx

463 lines
16 KiB
TypeScript

'use client'
import { RequireAuth } from '@/components/auth/RequireAuth'
import { useAuth } from '@/contexts/AuthContext'
import { Header } from '@/components/header'
import { Footer } from '@/components/footer'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import Link from 'next/link'
import { useQuery } from '@tanstack/react-query'
import { useState, useMemo } from 'react'
import {
BookOpen,
Edit3,
Eye,
Heart,
TrendingUp,
Clock,
Users,
Plus,
RefreshCw,
Trash2
} from 'lucide-react'
// Types matching the API response from /api/dashboard
interface DashboardStats {
totalTopics: number
publishedTopics: number
draftTopics: number
totalViews: number
}
interface UserTopic {
id: string
title: string
slug: string
publishedAt: number
isDraft: boolean
views?: number
excerpt: string
tags: { name: string }[]
}
interface DashboardResponse {
success: boolean
data: {
stats: DashboardStats
userTopics: UserTopic[]
}
}
type FilterType = 'all' | 'published' | 'drafts'
// Fetch dashboard data from the API
const fetchDashboardData = async (): Promise<DashboardResponse> => {
const response = await fetch('/api/dashboard', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Include cookies for authentication
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }))
throw new Error(errorData.error?.message || `HTTP ${response.status}: Failed to fetch dashboard data`)
}
return response.json()
}
function DashboardContent() {
const { user } = useAuth()
const [filterType, setFilterType] = useState<FilterType>('all')
const [deletingTopic, setDeletingTopic] = useState<string | null>(null)
// Use TanStack Query for data fetching with automatic caching and refetching
const {
data: dashboardData,
isLoading,
error,
refetch,
isRefetching,
} = useQuery({
queryKey: ['dashboard', user?.id],
queryFn: fetchDashboardData,
enabled: !!user, // Only fetch when user is authenticated
staleTime: 0, // Always consider data stale - fetch fresh data
gcTime: 0, // Don't cache data
retry: (failureCount, error: any) => {
// Don't retry on authentication errors
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
return false
}
return failureCount < 3
},
})
// Extract data from the response
const stats = dashboardData?.data?.stats
const userTopics = dashboardData?.data?.userTopics || []
// Filter topics based on the selected filter type
const filteredTopics = useMemo(() => {
switch (filterType) {
case 'published':
return userTopics.filter(topic => !topic.isDraft)
case 'drafts':
return userTopics.filter(topic => topic.isDraft)
default:
return userTopics
}
}, [userTopics, filterType])
// Handle topic deletion
const handleDeleteTopic = async (topicId: string, topicTitle: string) => {
if (!confirm(`Are you sure you want to delete "${topicTitle}"? This action cannot be undone.`)) {
return
}
try {
setDeletingTopic(topicId)
const response = await fetch(`/api/topic?id=${topicId}`, {
method: 'DELETE',
credentials: 'include',
})
const result = await response.json()
if (!response.ok || !result.success) {
throw new Error(result.error?.message || 'Failed to delete topic')
}
// Refetch dashboard data to update the UI
refetch()
// Optional: Show success message
// You can add a toast notification here if you have a toast system
} catch (error) {
console.error('Failed to delete topic:', error)
alert(`Failed to delete topic: ${error instanceof Error ? error.message : 'Unknown error'}`)
} finally {
setDeletingTopic(null)
}
}
// Handle loading state
if (isLoading) {
return (
<div className="container pt-24 pb-8">
<div className="max-w-7xl mx-auto space-y-8">
<div>
<Skeleton className="h-8 w-64 mb-2" />
<Skeleton className="h-4 w-96" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardContent className="p-6">
<Skeleton className="h-8 w-8 mb-4" />
<Skeleton className="h-6 w-16 mb-2" />
<Skeleton className="h-4 w-24" />
</CardContent>
</Card>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<Card>
<CardHeader>
<Skeleton className="h-6 w-32" />
</CardHeader>
<CardContent className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="flex justify-between items-center">
<div>
<Skeleton className="h-4 w-48 mb-1" />
<Skeleton className="h-3 w-24" />
</div>
<Skeleton className="h-6 w-12" />
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<Skeleton className="h-6 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-10 w-full mb-4" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-3/4" />
</CardContent>
</Card>
</div>
</div>
</div>
)
}
// Handle error state
if (error) {
return (
<div className="container pt-24 pb-8">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col items-center justify-center min-h-[400px] text-center">
<div className="text-red-500 mb-4">
<BookOpen className="w-12 h-12 mx-auto mb-2" />
<h3 className="text-lg font-semibold">Failed to Load Dashboard</h3>
<p className="text-sm text-muted-foreground mt-1">
{error.message || 'An unexpected error occurred'}
</p>
</div>
<Button onClick={() => refetch()} disabled={isRefetching}>
{isRefetching ? (
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
) : (
<RefreshCw className="w-4 h-4 mr-2" />
)}
Try Again
</Button>
</div>
</div>
</div>
)
}
return (
<div className="container pt-24 pb-8">
<div className="max-w-7xl mx-auto space-y-8">
{/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 className="text-3xl font-bold">Dashboard</h1>
<p className="text-muted-foreground mt-1">
Welcome back, {user?.name}! Here's your topics overview.
</p>
</div>
<div className="flex items-center gap-2">
<Button
onClick={() => refetch()}
disabled={isRefetching}
variant="outline"
size="sm"
>
{isRefetching ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<RefreshCw className="w-4 h-4" />
)}
</Button>
<Button asChild size="sm">
<Link href="/topics/new">
<Plus className="w-4 h-4 mr-2" />
New Topic
</Link>
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<BookOpen className="h-8 w-8 text-blue-600" />
<div className="ml-4">
<p className="text-2xl font-bold">{stats?.totalTopics || 0}</p>
<p className="text-xs text-muted-foreground">Total Topics</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<Edit3 className="h-8 w-8 text-green-600" />
<div className="ml-4">
<p className="text-2xl font-bold">{stats?.publishedTopics || 0}</p>
<p className="text-xs text-muted-foreground">Published</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<Eye className="h-8 w-8 text-purple-600" />
<div className="ml-4">
<p className="text-2xl font-bold">{stats?.totalViews?.toLocaleString() || '0'}</p>
<p className="text-xs text-muted-foreground">Total Views</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<Heart className="h-8 w-8 text-red-600" />
<div className="ml-4">
<p className="text-2xl font-bold">0</p>
<p className="text-xs text-muted-foreground">Total Likes</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Recent Topics with Filter */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Clock className="w-5 h-5" />
Recent Topics
</CardTitle>
<CardDescription>Your latest topics</CardDescription>
</div>
<Select value={filterType} onValueChange={(value: FilterType) => setFilterType(value)}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Topics</SelectItem>
<SelectItem value="published">Published</SelectItem>
<SelectItem value="drafts">Drafts</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent>
{filteredTopics.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<BookOpen className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p>
{filterType === 'all'
? 'No topics yet. Start creating your first topic!'
: filterType === 'drafts'
? 'No draft topics found.'
: 'No published topics found.'}
</p>
</div>
) : (
<div className="space-y-3 max-h-96 overflow-y-auto pr-2">
{filteredTopics.map((topic) => (
<div key={topic.id} className="flex justify-between items-center p-3 border rounded-lg hover:bg-accent/50 transition-colors">
<div className="flex-1 min-w-0">
<h4 className="font-medium hover:text-primary transition-colors truncate">
{topic.title}
</h4>
<div className="flex items-center gap-4 mt-1">
<p className="text-sm text-muted-foreground">
{new Date(topic.publishedAt).toLocaleDateString()}
</p>
<span className={`text-xs px-2 py-1 rounded ${
topic.isDraft
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
}`}>
{topic.isDraft ? 'Draft' : 'Published'}
</span>
</div>
<p className="text-xs text-muted-foreground mt-1 line-clamp-1">
{topic.excerpt}
</p>
</div>
<div className="flex items-center gap-2 ml-4">
{!topic.isDraft && (
<div className="flex items-center text-sm text-muted-foreground">
<Eye className="w-4 h-4 mr-1" />
{topic.views || 0}
</div>
)}
<Button
asChild
variant="outline"
size="sm"
>
<Link href={`/topics/edit/${topic.id}`}>
<Edit3 className="w-4 h-4" />
</Link>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteTopic(topic.id, topic.title)}
disabled={deletingTopic === topic.id}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
>
{deletingTopic === topic.id ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-600"></div>
) : (
<Trash2 className="w-4 h-4" />
)}
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Quick Actions - Cleaned up */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="w-5 h-5" />
Quick Actions
</CardTitle>
<CardDescription>Get things done faster</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Button asChild className="w-full justify-start h-auto p-4">
<Link href="/topics/new">
<Edit3 className="w-5 h-5 mr-3" />
<div className="text-left">
<div className="font-medium">Create New Topic</div>
<div className="text-xs text-muted-foreground">Start creating fresh content</div>
</div>
</Link>
</Button>
<Button asChild variant="outline" className="w-full justify-start h-auto p-4">
<Link href="/profile">
<Users className="w-5 h-5 mr-3" />
<div className="text-left">
<div className="font-medium">Profile Settings</div>
<div className="text-xs text-muted-foreground">Update your information</div>
</div>
</Link>
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
export default function DashboardPage() {
return (
<div className="min-h-screen bg-background">
<Header />
<RequireAuth>
<DashboardContent />
</RequireAuth>
<Footer />
</div>
)
}