463 lines
16 KiB
TypeScript
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>
|
|
)
|
|
}
|