started on the admin dashboard

This commit is contained in:
Doar Mihai
2026-02-14 17:20:50 +02:00
parent cf6310dc7e
commit 4bc77d455b
4 changed files with 350 additions and 4 deletions
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\User;
use App\Models\Category;
use App\Models\Comment;
use App\Models\Upvote;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AdminController extends Controller
{
/**
* Display the admin dashboard with statistics.
*/
public function dashboard()
{
// General Stats
$stats = [
'total_users' => User::count(),
'total_products' => Product::count(),
'approved_products' => Product::approved()->count(),
'pending_products' => Product::pending()->count(),
'rejected_products' => Product::rejected()->count(),
'total_categories' => Category::count(),
'total_comments' => Comment::count(),
'total_upvotes' => Upvote::count(),
];
// User Growth (last 7 days)
$userGrowth = User::where('created_at', '>=', now()->subDays(7))
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
->groupBy('date')
->orderBy('date')
->get();
// Product Growth (last 7 days)
$productGrowth = Product::where('created_at', '>=', now()->subDays(7))
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
->groupBy('date')
->orderBy('date')
->get();
// Top Categories (by product count)
$topCategories = Category::withCount('products')
->orderBy('products_count', 'desc')
->limit(5)
->get();
// Recent Users
$recentUsers = User::with('roles')
->latest()
->limit(5)
->get();
// Recent Products
$recentProducts = Product::with(['user', 'category'])
->latest()
->limit(5)
->get();
// Most Upvoted Products
$topProducts = Product::approved()
->withCount('upvotes')
->orderBy('upvotes_count', 'desc')
->limit(5)
->get();
return view('admin.dashboard', compact(
'stats',
'userGrowth',
'productGrowth',
'topCategories',
'recentUsers',
'recentProducts',
'topProducts'
));
}
}
+254
View File
@@ -0,0 +1,254 @@
@extends('layouts.app')
@section('content')
<div class="bg-gray-50 dark:bg-gray-900 min-h-screen">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Admin Dashboard</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Overview of your platform statistics and activity</p>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4 mb-8">
<!-- Total Users -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm rounded-lg border border-gray-200 dark:border-gray-700">
<div class="p-6">
<div class="flex items-center">
<div class="flex-shrink-0 bg-primary-100 dark:bg-primary-900 rounded-md p-3">
<svg class="h-6 w-6 text-primary-600 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Total Users</dt>
<dd class="text-2xl font-semibold text-gray-900 dark:text-white">{{ number_format($stats['total_users']) }}</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- Total Products -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm rounded-lg border border-gray-200 dark:border-gray-700">
<div class="p-6">
<div class="flex items-center">
<div class="flex-shrink-0 bg-green-100 dark:bg-green-900 rounded-md p-3">
<svg class="h-6 w-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Total Products</dt>
<dd class="text-2xl font-semibold text-gray-900 dark:text-white">{{ number_format($stats['total_products']) }}</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- Pending Products -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm rounded-lg border border-gray-200 dark:border-gray-700">
<div class="p-6">
<div class="flex items-center">
<div class="flex-shrink-0 bg-yellow-100 dark:bg-yellow-900 rounded-md p-3">
<svg class="h-6 w-6 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Pending Review</dt>
<dd class="text-2xl font-semibold text-gray-900 dark:text-white">{{ number_format($stats['pending_products']) }}</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- Total Upvotes -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm rounded-lg border border-gray-200 dark:border-gray-700">
<div class="p-6">
<div class="flex items-center">
<div class="flex-shrink-0 bg-purple-100 dark:bg-purple-900 rounded-md p-3">
<svg class="h-6 w-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"></path>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">Total Upvotes</dt>
<dd class="text-2xl font-semibold text-gray-900 dark:text-white">{{ number_format($stats['total_upvotes']) }}</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<!-- Secondary Stats -->
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4 mb-8">
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Approved Products</div>
<div class="mt-1 text-xl font-semibold text-green-600 dark:text-green-400">{{ number_format($stats['approved_products']) }}</div>
</div>
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Rejected Products</div>
<div class="mt-1 text-xl font-semibold text-red-600 dark:text-red-400">{{ number_format($stats['rejected_products']) }}</div>
</div>
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Comments</div>
<div class="mt-1 text-xl font-semibold text-gray-900 dark:text-white">{{ number_format($stats['total_comments']) }}</div>
</div>
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Categories</div>
<div class="mt-1 text-xl font-semibold text-gray-900 dark:text-white">{{ number_format($stats['total_categories']) }}</div>
</div>
</div>
<!-- Two Column Layout -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- Top Categories -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Top Categories</h3>
</div>
<div class="p-6">
<div class="space-y-4">
@forelse($topCategories as $category)
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 rounded-lg bg-primary-100 dark:bg-primary-900 flex items-center justify-center">
<span class="text-primary-600 dark:text-primary-400 font-semibold">{{ $loop->iteration }}</span>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ $category->name }}</p>
</div>
</div>
<div class="text-sm font-semibold text-gray-900 dark:text-white">
{{ $category->products_count }} {{ Str::plural('product', $category->products_count) }}
</div>
</div>
@empty
<p class="text-sm text-gray-500 dark:text-gray-400">No categories yet</p>
@endforelse
</div>
</div>
</div>
<!-- Most Upvoted Products -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Most Upvoted Products</h3>
</div>
<div class="p-6">
<div class="space-y-4">
@forelse($topProducts as $product)
<div class="flex items-center justify-between">
<div class="flex items-center min-w-0 flex-1">
<div class="flex-shrink-0">
@if($product->thumbnail_url)
<img src="{{ $product->thumbnail_url }}" alt="{{ $product->title }}" class="h-10 w-10 rounded object-cover">
@else
<div class="h-10 w-10 rounded bg-gray-200 dark:bg-gray-700"></div>
@endif
</div>
<div class="ml-4 min-w-0 flex-1">
<a href="{{ route('products.show', $product) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-primary-600 dark:hover:text-primary-400 truncate block">
{{ $product->title }}
</a>
</div>
</div>
<div class="ml-4 flex-shrink-0 text-sm font-semibold text-gray-900 dark:text-white">
{{ $product->upvotes_count }} 👍
</div>
</div>
@empty
<p class="text-sm text-gray-500 dark:text-gray-400">No products yet</p>
@endforelse
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Recent Users -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Users</h3>
</div>
<div class="p-6">
<div class="space-y-4">
@forelse($recentUsers as $user)
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 rounded-full bg-gradient-to-br from-primary-500 to-accent-500 flex items-center justify-center text-white font-semibold">
{{ strtoupper(substr($user->name, 0, 1)) }}
</div>
<div class="ml-4">
<a href="{{ route('profile.show', $user) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-primary-600 dark:hover:text-primary-400">
{{ $user->name }}
</a>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ $user->created_at->diffForHumans() }}</p>
</div>
</div>
@if($user->roles->isNotEmpty())
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200">
{{ $user->roles->first()->name }}
</span>
@endif
</div>
@empty
<p class="text-sm text-gray-500 dark:text-gray-400">No users yet</p>
@endforelse
</div>
</div>
</div>
<!-- Recent Products -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Products</h3>
</div>
<div class="p-6">
<div class="space-y-4">
@forelse($recentProducts as $product)
<div class="flex items-center justify-between">
<div class="flex items-center min-w-0 flex-1">
<div class="flex-shrink-0">
@if($product->thumbnail_url)
<img src="{{ $product->thumbnail_url }}" alt="{{ $product->title }}" class="h-10 w-10 rounded object-cover">
@else
<div class="h-10 w-10 rounded bg-gray-200 dark:bg-gray-700"></div>
@endif
</div>
<div class="ml-4 min-w-0 flex-1">
<a href="{{ route('products.show', $product) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-primary-600 dark:hover:text-primary-400 truncate block">
{{ $product->title }}
</a>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ $product->created_at->diffForHumans() }}</p>
</div>
</div>
@if($product->isPending())
<span class="ml-4 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200">
Pending
</span>
@elseif($product->isApproved())
<span class="ml-4 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
Approved
</span>
@endif
</div>
@empty
<p class="text-sm text-gray-500 dark:text-gray-400">No products yet</p>
@endforelse
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@@ -20,6 +20,12 @@
Products
</a>
@auth
@if(auth()->user()->hasRole('admin'))
<a href="{{ route('admin.dashboard') }}"
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium {{ request()->routeIs('admin.*') ? 'border-primary-500 text-gray-900 dark:text-white' : 'border-transparent text-gray-500 dark:text-gray-400 hover:border-gray-300 dark:hover:border-gray-600 hover:text-gray-700 dark:hover:text-gray-300' }}">
Dashboard
</a>
@endif
@if(auth()->user()->hasAnyRole(['admin', 'moderator']))
<a href="{{ route('products.manage') }}"
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium {{ request()->routeIs('products.manage') ? 'border-primary-500 text-gray-900 dark:text-white' : 'border-transparent text-gray-500 dark:text-gray-400 hover:border-gray-300 dark:hover:border-gray-600 hover:text-gray-700 dark:hover:text-gray-300' }}">
@@ -162,6 +168,12 @@
Discover
</a>
@auth
@if(auth()->user()->hasRole('admin'))
<a href="{{ route('admin.dashboard') }}"
class="block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('admin.*') ? 'border-primary-500 text-primary-700 bg-primary-50' : 'border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700' }}">
Dashboard
</a>
@endif
@if(auth()->user()->hasAnyRole(['admin', 'moderator']))
<a href="{{ route('products.manage') }}"
class="block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('products.manage') ? 'border-primary-500 text-primary-700 bg-primary-50' : 'border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700' }}">
+3 -4
View File
@@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\AdminController;
use App\Http\Controllers\BookmarkController;
use App\Http\Controllers\CommentController;
use App\Http\Controllers\ProductController;
@@ -53,11 +54,9 @@ Route::middleware(['auth', 'moderator'])->group(function () {
Route::post('/products/{product}/reject', [ProductController::class, 'reject'])->name('products.reject');
});
// Admin Routes (example)
// Admin Routes
Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/dashboard', function () {
return 'Admin Dashboard';
})->name('dashboard');
Route::get('/dashboard', [AdminController::class, 'dashboard'])->name('dashboard');
Route::get('/users', function () {
return 'User Management (Admin Only)';