| import React, { useEffect, useState } from 'react'; |
| import { useDispatch, useSelector } from 'react-redux'; |
| import { useNavigate } from 'react-router-dom'; |
| import { fetchSources } from '../store/reducers/sourcesSlice'; |
| import { fetchAccounts } from '../store/reducers/accountsSlice'; |
| import { fetchPosts } from '../store/reducers/postsSlice'; |
| import { fetchSchedules } from '../store/reducers/schedulesSlice'; |
|
|
| const Dashboard = () => { |
| const dispatch = useDispatch(); |
| const navigate = useNavigate(); |
| const { user } = useSelector(state => state.auth); |
| const { items: sources, loading: sourcesLoading } = useSelector(state => state.sources); |
| const { items: accounts, loading: accountsLoading } = useSelector(state => state.accounts); |
| const { items: posts, loading: postsLoading } = useSelector(state => state.posts); |
| const { items: schedules, loading: schedulesLoading } = useSelector(state => state.schedules); |
|
|
| const [mounted, setMounted] = useState(false); |
|
|
| useEffect(() => { |
| |
| if (user) { |
| |
| dispatch(fetchSources()); |
| dispatch(fetchAccounts()); |
| dispatch(fetchPosts()); |
| dispatch(fetchSchedules()); |
| setMounted(true); |
| } else { |
| |
| navigate('/login'); |
| } |
| }, [dispatch, user, navigate]); |
|
|
| |
| const publishedPosts = posts.filter(post => post.is_published).length; |
|
|
| |
| if (!user) { |
| |
| window.location.href = '/login'; |
| return null; |
| } |
|
|
| |
| const StatCard = ({ title, value, icon: Icon, loading }) => ( |
| <div className="relative overflow-hidden bg-white rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 animate-scale-in"> |
| {loading && ( |
| <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/50 to-transparent animate-pulse-slow" /> |
| )} |
| <div className="p-4 sm:p-6"> |
| <div className="flex items-center justify-between mb-3 sm:mb-4"> |
| <div className="p-2 sm:p-3 bg-primary-100 rounded-xl"> |
| <Icon className="w-5 h-5 sm:w-6 sm:h-6 text-primary-600" /> |
| </div> |
| </div> |
| <h3 className="text-xs sm:text-sm font-medium text-secondary-600 mb-1">{title}</h3> |
| <div className={`text-xl sm:text-3xl font-bold text-secondary-900 ${ |
| loading ? 'flex items-center space-x-1' : '' |
| }`}> |
| {loading ? ( |
| <> |
| <div className="w-3 h-4 sm:w-4 sm:h-6 bg-secondary-200 rounded animate-pulse"></div> |
| <div className="w-3 h-4 sm:w-4 sm:h-6 bg-secondary-200 rounded animate-pulse"></div> |
| <div className="w-3 h-4 sm:w-4 sm:h-6 bg-secondary-200 rounded animate-pulse"></div> |
| </> |
| ) : ( |
| value |
| )} |
| </div> |
| </div> |
| </div> |
| ); |
|
|
| |
| const ActivityItem = ({ post }) => ( |
| <div className="group relative bg-white rounded-xl p-3 sm:p-4 mb-3 hover:shadow-md transition-all duration-300 animate-slide-up"> |
| <div className="flex items-start space-x-3"> |
| <div className="flex-shrink-0"> |
| <div className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-primary-500 to-primary-600 rounded-full flex items-center justify-center"> |
| <svg className="w-4 h-4 sm:w-5 sm:h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> |
| </svg> |
| </div> |
| </div> |
| <div className="flex-1 min-w-0"> |
| <p className="text-xs sm:text-sm text-secondary-800 mb-1 line-clamp-2"> |
| {post.Text_content ? post.Text_content.substring(0, 100) + '...' : 'No content available'} |
| </p> |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2"> |
| <span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${ |
| post.is_published |
| ? 'bg-green-100 text-green-800' |
| : 'bg-green-100 text-green-800' |
| }`}> |
| Published |
| </span> |
| <span className="text-xs text-secondary-500"> |
| {post.created_at ? new Date(post.created_at).toLocaleDateString() : 'No date'} |
| </span> |
| </div> |
| </div> |
| </div> |
| </div> |
| ); |
|
|
| return ( |
| <div className="min-h-screen bg-gradient-to-br from-accent-50 via-white to-primary-50"> |
| {/* Skip to content link for accessibility */} |
| <a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-2 sm:top-4 focus:left-2 sm:left-4 bg-primary-600 text-white px-3 sm:px-4 py-2 rounded-lg text-sm"> |
| Skip to main content |
| </a> |
| |
| <div className="container mx-auto px-3 sm:px-4 py-4 sm:py-8 max-w-7xl"> |
| {/* Dashboard Header */} |
| <header className="mb-6 sm:mb-8 animate-fade-in"> |
| <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> |
| <div> |
| <h1 className="text-2xl sm:text-4xl font-bold text-secondary-900 mb-1 sm:mb-2">Dashboard</h1> |
| <p className="text-base sm:text-lg text-secondary-600"> |
| Welcome back, <span className="font-semibold text-primary-600">{user?.email}</span> |
| </p> |
| </div> |
| <div className="hidden sm:block"> |
| <div className="bg-white rounded-2xl shadow-lg p-3 sm:p-4 border border-secondary-200"> |
| <div className="flex items-center space-x-3"> |
| <div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-primary-500 to-primary-600 rounded-full flex items-center justify-center"> |
| <span className="text-white font-semibold text-base sm:text-lg"> |
| {user?.email?.charAt(0).toUpperCase()} |
| </span> |
| </div> |
| <div> |
| <p className="text-xs sm:text-sm font-medium text-secondary-900">Account</p> |
| <p className="text-xs text-secondary-600">Active</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </header> |
| |
| {/* Statistics Cards */} |
| <div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 gap-3 sm:gap-6 mb-6 sm:mb-8"> |
| <StatCard |
| title="Sources" |
| value={sources.length} |
| icon={(props) => ( |
| <svg {...props} fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /> |
| </svg> |
| )} |
| loading={sourcesLoading} |
| /> |
| |
| <StatCard |
| title="Accounts" |
| value={accounts.length} |
| icon={(props) => ( |
| <svg {...props} fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> |
| </svg> |
| )} |
| loading={accountsLoading} |
| /> |
| |
| <StatCard |
| title="Published Posts" |
| value={publishedPosts} |
| icon={(props) => ( |
| <svg {...props} fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> |
| </svg> |
| )} |
| loading={postsLoading} |
| /> |
| </div> |
| |
| {/* Main Content Grid */} |
| <div className="grid grid-cols-1 xl:grid-cols-3 gap-6 sm:gap-8"> |
| {/* Recent Activity Section */} |
| <div className="xl:col-span-3"> |
| <div className="bg-white rounded-2xl shadow-lg p-4 sm:p-6 animate-slide-up"> |
| <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4 sm:mb-6 gap-2"> |
| <h2 className="text-xl sm:text-2xl font-bold text-secondary-900">Recent Activity</h2> |
| <button className="text-xs sm:text-sm text-primary-600 hover:text-primary-700 font-medium transition-colors text-left sm:text-right"> |
| View All |
| </button> |
| </div> |
| |
| <div className="activity-list"> |
| {postsLoading ? ( |
| <div className="space-y-3"> |
| {[...Array(3)].map((_, i) => ( |
| <div key={i} className="bg-gray-50 rounded-xl p-3 sm:p-4 animate-pulse"> |
| <div className="flex items-start space-x-3"> |
| <div className="w-8 h-8 sm:w-10 sm:h-10 bg-gray-200 rounded-full flex-shrink-0"></div> |
| <div className="flex-1"> |
| <div className="h-3 sm:h-4 bg-gray-200 rounded mb-2"></div> |
| <div className="h-2 sm:h-3 bg-gray-200 rounded w-3/4"></div> |
| </div> |
| </div> |
| </div> |
| ))} |
| </div> |
| ) : posts.length === 0 ? ( |
| <div className="text-center py-8 sm:py-12"> |
| <div className="w-12 h-12 sm:w-16 sm:h-16 bg-accent-100 rounded-full flex items-center justify-center mx-auto mb-3 sm:mb-4"> |
| <svg className="w-6 h-6 sm:w-8 sm:h-8 text-accent-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> |
| </svg> |
| </div> |
| <h3 className="text-base sm:text-lg font-semibold text-secondary-900 mb-2">No posts yet</h3> |
| <p className="text-sm sm:text-base text-secondary-600 mb-4">Create your first post to get started!</p> |
| <button |
| onClick={() => navigate('/posts')} |
| className="btn btn-primary text-sm sm:text-base px-4 sm:px-6 py-2 sm:py-3" |
| > |
| Create Your First Post |
| </button> |
| </div> |
| ) : ( |
| // Sort posts by creation date (newest first) |
| [...posts] |
| .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) |
| .slice(0, 5) |
| .map((post, index) => ( |
| <ActivityItem key={post.id} post={post} style={{ animationDelay: `${index * 0.1}s` }} /> |
| )) |
| )} |
| </div> |
| </div> |
| </div> |
| |
| {/* Quick Actions Section - REMOVED */} |
| |
| {/* Additional Info Card - REMOVED */} |
| </div> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default Dashboard; |