initial commit
This commit is contained in:
462
app/dashboard/page.tsx
Normal file
462
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,462 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user