some progress: users slugs, bookmarks page, upvotes page, design

This commit is contained in:
Doar Mihai
2026-02-14 17:06:03 +02:00
parent 67de095a03
commit cf6310dc7e
16 changed files with 419 additions and 71 deletions
@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class GenerateUserSlugs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'users:generate-slugs';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate unique slugs for all users without slugs';
/**
* Execute the console command.
*/
public function handle()
{
$this->info('Generating slugs for users...');
$usersWithoutSlugs = User::whereNull('slug')->get();
if ($usersWithoutSlugs->isEmpty()) {
$this->info('All users already have slugs!');
return Command::SUCCESS;
}
$bar = $this->output->createProgressBar($usersWithoutSlugs->count());
$bar->start();
foreach ($usersWithoutSlugs as $user) {
$user->slug = User::generateUniqueSlug($user->name);
$user->save();
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Successfully generated slugs for {$usersWithoutSlugs->count()} users!");
return Command::SUCCESS;
}
}
@@ -67,6 +67,7 @@ class RegisterController extends Controller
'name' => $data['name'], 'name' => $data['name'],
'email' => $data['email'], 'email' => $data['email'],
'password' => Hash::make($data['password']), 'password' => Hash::make($data['password']),
'slug' => User::generateUniqueSlug($data['name']),
]); ]);
} }
} }
@@ -10,6 +10,24 @@ use Illuminate\Support\Facades\Auth;
class BookmarkController extends Controller class BookmarkController extends Controller
{ {
/**
* Display the user's bookmarked products.
*/
public function index()
{
$user = Auth::user();
$bookmarks = $user->bookmarks()
->with(['product.category', 'product.user'])
->whereHas('product', function ($query) {
$query->approved();
})
->latest()
->paginate(12);
return view('bookmarks.index', compact('bookmarks'));
}
/** /**
* Toggle bookmark for a product. * Toggle bookmark for a product.
*/ */
@@ -73,6 +73,11 @@ class ProfileController extends Controller
$validated['avatar'] = $avatarName; $validated['avatar'] = $avatarName;
} }
// Regenerate slug if name changed
if ($request->name !== $user->name) {
$validated['slug'] = User::generateUniqueSlug($request->name);
}
// Update user profile // Update user profile
$user->update($validated); $user->update($validated);
+18
View File
@@ -10,6 +10,24 @@ use Illuminate\Support\Facades\Auth;
class UpvoteController extends Controller class UpvoteController extends Controller
{ {
/**
* Display the user's upvoted products.
*/
public function index()
{
$user = Auth::user();
$upvotes = $user->upvotes()
->with(['product.category', 'product.user'])
->whereHas('product', function ($query) {
$query->approved();
})
->latest()
->paginate(12);
return view('upvotes.index', compact('upvotes'));
}
/** /**
* Toggle upvote for a product. * Toggle upvote for a product.
*/ */
+28
View File
@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
class User extends Authenticatable class User extends Authenticatable
{ {
@@ -23,6 +24,7 @@ class User extends Authenticatable
'name', 'name',
'email', 'email',
'password', 'password',
'slug',
'avatar', 'avatar',
'bio', 'bio',
'website', 'website',
@@ -144,4 +146,30 @@ class User extends Authenticatable
{ {
return $this->hasMany(Bookmark::class); return $this->hasMany(Bookmark::class);
} }
/**
* Generate a unique slug for the user based on their name.
*/
public static function generateUniqueSlug(string $name): string
{
$slug = Str::slug($name);
$originalSlug = $slug;
$count = 1;
// Keep appending numbers until we find a unique slug
while (static::where('slug', $slug)->exists()) {
$slug = $originalSlug . '-' . $count;
$count++;
}
return $slug;
}
/**
* Get the route key for the model.
*/
public function getRouteKeyName(): string
{
return 'slug';
}
} }
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('slug')->unique()->after('name')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('slug');
});
}
};
+9
View File
@@ -1,7 +1,16 @@
@import "tailwindcss"; @import "tailwindcss";
/* Dark mode configuration */
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
}
}
/* Custom Styles */ /* Custom Styles */
@theme { @theme {
/* Enable class-based dark mode */
--color-scheme: class;
--color-primary-50: #f0f9ff; --color-primary-50: #f0f9ff;
--color-primary-100: #e0f2fe; --color-primary-100: #e0f2fe;
--color-primary-200: #bae6fd; --color-primary-200: #bae6fd;
+60
View File
@@ -0,0 +1,60 @@
@extends('layouts.app')
@section('content')
<div class="bg-gradient-to-br from-primary-50 via-white to-accent-50 border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
<div class="max-w-3xl">
<h1 class="text-4xl sm:text-5xl font-bold text-gray-900 mb-4">
My Bookmarks
</h1>
<p class="text-lg text-gray-600">
Products you've saved for later
</p>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Results Summary -->
@if($bookmarks->total() > 0)
<div class="mb-6">
<p class="text-sm text-gray-600">
You have <span class="font-semibold text-gray-900">{{ $bookmarks->total() }}</span> bookmarked {{ Str::plural('product', $bookmarks->total()) }}
</p>
</div>
@endif
<!-- Bookmarked Products Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
@forelse($bookmarks as $bookmark)
@include('components.product.card', ['product' => $bookmark->product])
@empty
<!-- Empty State -->
<div class="col-span-full">
<div class="text-center py-16">
<svg class="mx-auto h-16 w-16 text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
</svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">No bookmarks yet</h3>
<p class="text-gray-600 mb-6">
Start bookmarking products to save them for later!
</p>
<a href="{{ route('products.index') }}" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
Browse Products
</a>
</div>
</div>
@endforelse
</div>
<!-- Pagination -->
@if($bookmarks->hasPages())
<div class="flex justify-center">
{{ $bookmarks->links() }}
</div>
@endif
</div>
@endsection
@@ -1,4 +1,4 @@
<nav class="bg-white border-b border-gray-200 sticky top-0 z-50" x-data="{ mobileMenuOpen: false, userMenuOpen: false }"> <nav class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-50" x-data="{ mobileMenuOpen: false, userMenuOpen: false }">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16"> <div class="flex justify-between h-16">
<!-- Logo and Primary Navigation --> <!-- Logo and Primary Navigation -->
@@ -6,23 +6,23 @@
<!-- Logo --> <!-- Logo -->
<div class="flex-shrink-0 flex items-center"> <div class="flex-shrink-0 flex items-center">
<a href="{{ route('homepage') }}" class="flex items-center space-x-2"> <a href="{{ route('homepage') }}" class="flex items-center space-x-2">
<svg class="h-8 w-8 text-primary-600" fill="currentColor" viewBox="0 0 24 24"> <svg class="h-8 w-8 text-primary-600 dark:text-primary-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path> <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
</svg> </svg>
<span class="text-xl font-bold text-gray-900">{{ config('app.name', 'BestTech') }}</span> <span class="text-xl font-bold text-gray-900 dark:text-white">{{ config('app.name', 'BestTech') }}</span>
</a> </a>
</div> </div>
<!-- Desktop Navigation --> <!-- Desktop Navigation -->
<div class="hidden sm:ml-8 sm:flex sm:space-x-8"> <div class="hidden sm:ml-8 sm:flex sm:space-x-8">
<a href="{{ route('products.index') }}" <a href="{{ route('products.index') }}"
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium {{ request()->routeIs('products.*') ? 'border-primary-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700' }}"> class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium {{ request()->routeIs('products.*') ? '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' }}">
Products Products
</a> </a>
@auth @auth
@if(auth()->user()->hasAnyRole(['admin', 'moderator'])) @if(auth()->user()->hasAnyRole(['admin', 'moderator']))
<a href="{{ route('products.manage') }}" <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' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700' }}"> 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' }}">
Manage Manage
</a> </a>
@endif @endif
@@ -41,7 +41,7 @@
<input type="text" <input type="text"
name="search" name="search"
value="{{ request('search') }}" value="{{ request('search') }}"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-primary-500 focus:border-primary-500 sm:text-sm" class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md leading-5 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="Search products..."> placeholder="Search products...">
</div> </div>
</form> </form>
@@ -50,8 +50,11 @@
<!-- Right Side --> <!-- Right Side -->
<div class="hidden sm:ml-6 sm:flex sm:items-center sm:space-x-4"> <div class="hidden sm:ml-6 sm:flex sm:items-center sm:space-x-4">
<!-- Theme Toggle -->
@include('components.ui.theme-toggle')
@guest @guest
<a href="{{ route('login') }}" class="text-gray-700 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"> <a href="{{ route('login') }}" class="text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-md text-sm font-medium">
Sign in Sign in
</a> </a>
<a href="{{ route('register') }}" class="btn-primary"> <a href="{{ route('register') }}" class="btn-primary">
@@ -86,21 +89,37 @@
x-transition:leave="transition ease-in duration-75" x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95" x-transition:leave-end="transform opacity-0 scale-95"
class="origin-top-right absolute right-0 mt-2 w-48 rounded-lg shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5" class="origin-top-right absolute right-0 mt-2 w-48 rounded-lg shadow-lg py-1 bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5"
style="display: none;"> style="display: none;">
<div class="px-4 py-2 border-b border-gray-100"> <div class="px-4 py-2 border-b border-gray-100 dark:border-gray-700">
<p class="text-sm font-medium text-gray-900">{{ Auth::user()->name }}</p> <p class="text-sm font-medium text-gray-900 dark:text-white">{{ Auth::user()->name }}</p>
<p class="text-xs text-gray-500">{{ Auth::user()->email }}</p> <p class="text-xs text-gray-500 dark:text-gray-400">{{ Auth::user()->email }}</p>
</div> </div>
<a href="{{ route('profile.show', Auth::user()) }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"> <a href="{{ route('profile.show', Auth::user()) }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
Your Profile Your Profile
</a> </a>
<a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"> <a href="{{ route('bookmarks.index') }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex items-center">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
</svg>
Bookmarks
</div>
</a>
<a href="{{ route('upvotes.index') }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex items-center">
<svg class="mr-2 h-4 w-4" 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>
Upvotes
</div>
</a>
<a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
Settings Settings
</a> </a>
<form method="POST" action="{{ route('logout') }}"> <form method="POST" action="{{ route('logout') }}">
@csrf @csrf
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"> <button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
Sign out Sign out
</button> </button>
</form> </form>
@@ -110,7 +129,10 @@
</div> </div>
<!-- Mobile menu button --> <!-- Mobile menu button -->
<div class="flex items-center sm:hidden"> <div class="flex items-center sm:hidden space-x-2">
<!-- Theme Toggle (Mobile) -->
@include('components.ui.theme-toggle')
<button @click="mobileMenuOpen = !mobileMenuOpen" <button @click="mobileMenuOpen = !mobileMenuOpen"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500"> class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500">
<svg class="h-6 w-6" :class="{'hidden': mobileMenuOpen, 'block': !mobileMenuOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6" :class="{'hidden': mobileMenuOpen, 'block': !mobileMenuOpen }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -168,6 +190,12 @@
<a href="{{ route('profile.show', Auth::user()) }}" class="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100"> <a href="{{ route('profile.show', Auth::user()) }}" class="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100">
Your Profile Your Profile
</a> </a>
<a href="{{ route('bookmarks.index') }}" class="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100">
Bookmarks
</a>
<a href="{{ route('upvotes.index') }}" class="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100">
Upvotes
</a>
<a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100"> <a href="{{ route('profile.edit') }}" class="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100">
Settings Settings
</a> </a>
@@ -1,9 +1,9 @@
@props(['product']) @props(['product'])
<div class="card group"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-md transition-shadow group">
<a href="{{ route('products.show', $product) }}" class="block"> <a href="{{ route('products.show', $product) }}" class="block">
<!-- Thumbnail --> <!-- Thumbnail -->
<div class="aspect-video bg-gradient-to-br from-gray-100 to-gray-200 overflow-hidden"> <div class="aspect-video bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-700 dark:to-gray-600 overflow-hidden">
@if($product->thumbnail_url) @if($product->thumbnail_url)
<img src="{{ $product->thumbnail_url }}" <img src="{{ $product->thumbnail_url }}"
alt="{{ $product->title }}" alt="{{ $product->title }}"
@@ -11,7 +11,7 @@
loading="lazy"> loading="lazy">
@else @else
<div class="w-full h-full flex items-center justify-center"> <div class="w-full h-full flex items-center justify-center">
<svg class="h-16 w-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-16 w-16 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg> </svg>
</div> </div>
@@ -23,8 +23,8 @@
<div class="p-4"> <div class="p-4">
<div class="flex items-start justify-between mb-2"> <div class="flex items-start justify-between mb-2">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<a href="{{ route('products.show', $product) }}" class="group-hover:text-primary-600 transition-colors"> <a href="{{ route('products.show', $product) }}" class="group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
<h3 class="text-lg font-semibold text-gray-900 truncate">{{ $product->title }}</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-white truncate">{{ $product->title }}</h3>
</a> </a>
</div> </div>
@@ -33,15 +33,15 @@
</div> </div>
<!-- Description --> <!-- Description -->
<p class="text-sm text-gray-600 line-clamp-2 mb-3"> <p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-3">
{{ Str::limit($product->description, 100) }} {{ Str::limit($product->description, 100) }}
</p> </p>
<!-- Meta Information --> <!-- Meta Information -->
<div class="flex items-center justify-between text-xs text-gray-500"> <div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<!-- Category Badge --> <!-- Category Badge -->
<span class="badge bg-gray-100 text-gray-700"> <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">
{{ $product->category->name }} {{ $product->category->name }}
</span> </span>
@@ -55,7 +55,7 @@
</div> </div>
<!-- User --> <!-- User -->
<a href="{{ route('profile.show', $product->user) }}" class="flex items-center space-x-1 hover:text-gray-900"> <a href="{{ route('profile.show', $product->user) }}" class="flex items-center space-x-1 hover:text-gray-900 dark:hover:text-white">
<div class="h-5 w-5 rounded-full bg-gradient-to-br from-primary-500 to-accent-500 flex items-center justify-center text-white text-xs font-semibold"> <div class="h-5 w-5 rounded-full bg-gradient-to-br from-primary-500 to-accent-500 flex items-center justify-center text-white text-xs font-semibold">
{{ strtoupper(substr($product->user->name, 0, 1)) }} {{ strtoupper(substr($product->user->name, 0, 1)) }}
</div> </div>
@@ -64,9 +64,9 @@
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="mt-3 pt-3 border-t border-gray-100 flex items-center justify-between"> <div class="mt-3 pt-3 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between">
<a href="{{ route('products.show', $product) }}" <a href="{{ route('products.show', $product) }}"
class="text-sm font-medium text-primary-600 hover:text-primary-700 flex items-center"> class="text-sm font-medium text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 flex items-center">
View details View details
<svg class="h-4 w-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-4 w-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
@@ -0,0 +1,14 @@
<button
type="button"
onclick="window.toggleTheme()"
class="p-2 rounded-md text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:focus:ring-offset-gray-900 transition-colors"
aria-label="Toggle theme">
<!-- Sun icon (visible in dark mode) -->
<svg class="h-5 w-5 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
<!-- Moon icon (visible in light mode) -->
<svg class="h-5 w-5 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
</button>
+42 -18
View File
@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full bg-gray-50"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -9,6 +9,30 @@
<title>{{ $title ?? config('app.name', 'BestTech') }}</title> <title>{{ $title ?? config('app.name', 'BestTech') }}</title>
<!-- Dark Mode Script (Must be in head to prevent flash) -->
<script>
// Check for saved theme preference or default to light mode
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Toggle theme function
window.toggleTheme = function() {
const html = document.documentElement;
const isDark = html.classList.contains('dark');
if (isDark) {
html.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
html.classList.add('dark');
localStorage.setItem('theme', 'dark');
}
}
</script>
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net"> <link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=inter:300,400,500,600,700,800" rel="stylesheet"> <link href="https://fonts.bunny.net/css?family=inter:300,400,500,600,700,800" rel="stylesheet">
@@ -22,7 +46,7 @@
<!-- Additional Head Content --> <!-- Additional Head Content -->
@stack('styles') @stack('styles')
</head> </head>
<body class="h-full antialiased"> <body class="h-full antialiased bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<div id="app" class="min-h-full flex flex-col"> <div id="app" class="min-h-full flex flex-col">
<!-- Navigation --> <!-- Navigation -->
@include('components.layout.navbar') @include('components.layout.navbar')
@@ -33,55 +57,55 @@
</main> </main>
<!-- Footer --> <!-- Footer -->
<footer class="bg-white border-t border-gray-200 mt-auto"> <footer class="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-auto">
<div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8"> <div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Company Info --> <!-- Company Info -->
<div class="col-span-1"> <div class="col-span-1">
<div class="flex items-center space-x-2 mb-4"> <div class="flex items-center space-x-2 mb-4">
<svg class="h-8 w-8 text-primary-600" fill="currentColor" viewBox="0 0 24 24"> <svg class="h-8 w-8 text-primary-600 dark:text-primary-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path> <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
</svg> </svg>
<span class="text-lg font-bold text-gray-900">{{ config('app.name', 'BestTech') }}</span> <span class="text-lg font-bold text-gray-900 dark:text-white">{{ config('app.name', 'BestTech') }}</span>
</div> </div>
<p class="text-sm text-gray-600"> <p class="text-sm text-gray-600 dark:text-gray-400">
Discover the best tech products from the EU. Discover the best tech products from the EU.
</p> </p>
</div> </div>
<!-- Product Links --> <!-- Product Links -->
<div> <div>
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Product</h3> <h3 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4">Product</h3>
<ul class="space-y-3"> <ul class="space-y-3">
<li><a href="{{ route('products.index') }}" class="text-sm text-gray-600 hover:text-gray-900">Discover</a></li> <li><a href="{{ route('products.index') }}" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">Discover</a></li>
@auth @auth
<li><a href="{{ route('products.create') }}" class="text-sm text-gray-600 hover:text-gray-900">Submit Product</a></li> <li><a href="{{ route('products.create') }}" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">Submit Product</a></li>
@endauth @endauth
</ul> </ul>
</div> </div>
<!-- Company Links --> <!-- Company Links -->
<div> <div>
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Company</h3> <h3 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4">Company</h3>
<ul class="space-y-3"> <ul class="space-y-3">
<li><a href="#" class="text-sm text-gray-600 hover:text-gray-900">About</a></li> <li><a href="#" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">About</a></li>
<li><a href="#" class="text-sm text-gray-600 hover:text-gray-900">Blog</a></li> <li><a href="#" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">Blog</a></li>
<li><a href="#" class="text-sm text-gray-600 hover:text-gray-900">Contact</a></li> <li><a href="#" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">Contact</a></li>
</ul> </ul>
</div> </div>
<!-- Legal Links --> <!-- Legal Links -->
<div> <div>
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-4">Legal</h3> <h3 class="text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider mb-4">Legal</h3>
<ul class="space-y-3"> <ul class="space-y-3">
<li><a href="#" class="text-sm text-gray-600 hover:text-gray-900">Privacy</a></li> <li><a href="#" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">Privacy</a></li>
<li><a href="#" class="text-sm text-gray-600 hover:text-gray-900">Terms</a></li> <li><a href="#" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">Terms</a></li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="mt-8 pt-8 border-t border-gray-200"> <div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<p class="text-sm text-gray-500 text-center"> <p class="text-sm text-gray-500 dark:text-gray-400 text-center">
&copy; {{ date('Y') }} {{ config('app.name', 'BestTech') }}. All rights reserved. &copy; {{ date('Y') }} {{ config('app.name', 'BestTech') }}. All rights reserved.
</p> </p>
</div> </div>
+60
View File
@@ -0,0 +1,60 @@
@extends('layouts.app')
@section('content')
<div class="bg-gradient-to-br from-primary-50 via-white to-accent-50 border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
<div class="max-w-3xl">
<h1 class="text-4xl sm:text-5xl font-bold text-gray-900 mb-4">
My Upvotes
</h1>
<p class="text-lg text-gray-600">
Products you've upvoted and supported
</p>
</div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Results Summary -->
@if($upvotes->total() > 0)
<div class="mb-6">
<p class="text-sm text-gray-600">
You have upvoted <span class="font-semibold text-gray-900">{{ $upvotes->total() }}</span> {{ Str::plural('product', $upvotes->total()) }}
</p>
</div>
@endif
<!-- Upvoted Products Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
@forelse($upvotes as $upvote)
@include('components.product.card', ['product' => $upvote->product])
@empty
<!-- Empty State -->
<div class="col-span-full">
<div class="text-center py-16">
<svg class="mx-auto h-16 w-16 text-gray-400 mb-4" 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>
<h3 class="text-lg font-medium text-gray-900 mb-2">No upvotes yet</h3>
<p class="text-gray-600 mb-6">
Start upvoting products you love to show your support!
</p>
<a href="{{ route('products.index') }}" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
<svg class="mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
Browse Products
</a>
</div>
</div>
@endforelse
</div>
<!-- Pagination -->
@if($upvotes->hasPages())
<div class="flex justify-center">
{{ $upvotes->links() }}
</div>
@endif
</div>
@endsection
+27 -27
View File
@@ -2,13 +2,13 @@
@section('content') @section('content')
<!-- Hero Section --> <!-- Hero Section -->
<div class="bg-gradient-to-br from-primary-50 to-white py-20"> <div class="bg-gradient-to-br from-primary-50 to-white dark:from-gray-900 dark:to-gray-800 py-20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center"> <div class="text-center">
<h1 class="text-4xl font-extrabold text-gray-900 sm:text-5xl md:text-6xl"> <h1 class="text-4xl font-extrabold text-gray-900 dark:text-white sm:text-5xl md:text-6xl">
Discover the Best <span class="text-primary-600">Tech Products</span> from Europe Discover the Best <span class="text-primary-600 dark:text-primary-400">Tech Products</span> from Europe
</h1> </h1>
<p class="mt-3 max-w-md mx-auto text-base text-gray-500 sm:text-lg md:mt-5 md:text-xl md:max-w-3xl"> <p class="mt-3 max-w-md mx-auto text-base text-gray-500 dark:text-gray-400 sm:text-lg md:mt-5 md:text-xl md:max-w-3xl">
The premier platform for discovering innovative tech products, tools, and services built by European makers and entrepreneurs. The premier platform for discovering innovative tech products, tools, and services built by European makers and entrepreneurs.
</p> </p>
<div class="mt-5 max-w-md mx-auto sm:flex sm:justify-center md:mt-8"> <div class="mt-5 max-w-md mx-auto sm:flex sm:justify-center md:mt-8">
@@ -21,14 +21,14 @@
@auth @auth
<div class="mt-3 rounded-md shadow sm:mt-0 sm:ml-3"> <div class="mt-3 rounded-md shadow sm:mt-0 sm:ml-3">
<a href="{{ route('products.create') }}" <a href="{{ route('products.create') }}"
class="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-primary-600 bg-white hover:bg-gray-50 md:py-4 md:text-lg md:px-10"> class="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-primary-600 dark:text-primary-400 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 md:py-4 md:text-lg md:px-10">
Submit Your Product Submit Your Product
</a> </a>
</div> </div>
@else @else
<div class="mt-3 rounded-md shadow sm:mt-0 sm:ml-3"> <div class="mt-3 rounded-md shadow sm:mt-0 sm:ml-3">
<a href="{{ route('register') }}" <a href="{{ route('register') }}"
class="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-primary-600 bg-white hover:bg-gray-50 md:py-4 md:text-lg md:px-10"> class="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-md text-primary-600 dark:text-primary-400 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 md:py-4 md:text-lg md:px-10">
Get Started Get Started
</a> </a>
</div> </div>
@@ -40,14 +40,14 @@
<!-- Trending Products Section --> <!-- Trending Products Section -->
@if($trendingProducts->count() > 0) @if($trendingProducts->count() > 0)
<div class="bg-white py-16"> <div class="bg-white dark:bg-gray-900 py-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between mb-8"> <div class="flex items-center justify-between mb-8">
<div> <div>
<h2 class="text-3xl font-extrabold text-gray-900"> <h2 class="text-3xl font-extrabold text-gray-900 dark:text-white">
🔥 Trending This Week 🔥 Trending This Week
</h2> </h2>
<p class="mt-2 text-lg text-gray-600">Most popular products of the last 7 days</p> <p class="mt-2 text-lg text-gray-600 dark:text-gray-400">Most popular products of the last 7 days</p>
</div> </div>
</div> </div>
@@ -62,17 +62,17 @@
<!-- Categories Section --> <!-- Categories Section -->
@if($categories->count() > 0) @if($categories->count() > 0)
<div class="bg-gray-50 py-16"> <div class="bg-gray-50 dark:bg-gray-800 py-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-12"> <div class="text-center mb-12">
<h2 class="text-3xl font-extrabold text-gray-900">Browse by Category</h2> <h2 class="text-3xl font-extrabold text-gray-900 dark:text-white">Browse by Category</h2>
<p class="mt-2 text-lg text-gray-600">Explore products in your favorite categories</p> <p class="mt-2 text-lg text-gray-600 dark:text-gray-400">Explore products in your favorite categories</p>
</div> </div>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4"> <div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
@foreach($categories as $category) @foreach($categories as $category)
<a href="{{ route('products.index', ['category' => $category->id]) }}" <a href="{{ route('products.index', ['category' => $category->id]) }}"
class="group relative bg-white rounded-lg border border-gray-200 hover:border-primary-500 hover:shadow-md transition-all p-6 flex flex-col items-center justify-center text-center"> class="group relative bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-primary-500 hover:shadow-md transition-all p-6 flex flex-col items-center justify-center text-center">
<div class="mb-3"> <div class="mb-3">
<div class="w-12 h-12 rounded-lg bg-primary-100 group-hover:bg-primary-200 flex items-center justify-center"> <div class="w-12 h-12 rounded-lg bg-primary-100 group-hover:bg-primary-200 flex items-center justify-center">
<svg class="h-6 w-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -81,13 +81,13 @@
</div> </div>
</div> </div>
<h3 class="text-sm font-semibold text-gray-900 group-hover:text-primary-600">{{ $category->name }}</h3> <h3 class="text-sm font-semibold text-gray-900 group-hover:text-primary-600">{{ $category->name }}</h3>
<p class="mt-1 text-xs text-gray-500">{{ $category->products_count }} {{ Str::plural('product', $category->products_count) }}</p> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ $category->products_count }} {{ Str::plural('product', $category->products_count) }}</p>
</a> </a>
@endforeach @endforeach
</div> </div>
<div class="mt-10 text-center"> <div class="mt-10 text-center">
<a href="{{ route('products.index') }}" class="text-primary-600 hover:text-primary-700 font-medium"> <a href="{{ route('products.index') }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-medium">
View all products → View all products →
</a> </a>
</div> </div>
@@ -97,16 +97,16 @@
<!-- Recent Products Section --> <!-- Recent Products Section -->
@if($recentProducts->count() > 0) @if($recentProducts->count() > 0)
<div class="bg-white py-16"> <div class="bg-white dark:bg-gray-900 py-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between mb-8"> <div class="flex items-center justify-between mb-8">
<div> <div>
<h2 class="text-3xl font-extrabold text-gray-900"> <h2 class="text-3xl font-extrabold text-gray-900 dark:text-white">
✨ Recently Added ✨ Recently Added
</h2> </h2>
<p class="mt-2 text-lg text-gray-600">Fresh products from the community</p> <p class="mt-2 text-lg text-gray-600 dark:text-gray-400">Fresh products from the community</p>
</div> </div>
<a href="{{ route('products.index') }}" class="text-primary-600 hover:text-primary-700 font-medium hidden sm:block"> <a href="{{ route('products.index') }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-medium hidden sm:block">
See all → See all →
</a> </a>
</div> </div>
@@ -118,7 +118,7 @@
</div> </div>
<div class="mt-10 text-center sm:hidden"> <div class="mt-10 text-center sm:hidden">
<a href="{{ route('products.index') }}" class="text-primary-600 hover:text-primary-700 font-medium"> <a href="{{ route('products.index') }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-medium">
See all products → See all products →
</a> </a>
</div> </div>
@@ -160,29 +160,29 @@
</div> </div>
<!-- Social Proof / Stats Section (Optional) --> <!-- Social Proof / Stats Section (Optional) -->
<div class="bg-gray-50 py-12"> <div class="bg-gray-50 dark:bg-gray-800 py-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 gap-8 sm:grid-cols-3"> <div class="grid grid-cols-1 gap-8 sm:grid-cols-3">
<div class="text-center"> <div class="text-center">
<div class="text-4xl font-extrabold text-primary-600"> <div class="text-4xl font-extrabold text-primary-600">
{{ App\Models\Product::approved()->count() }}+ {{ App\Models\Product::approved()->count() }}+
</div> </div>
<div class="mt-2 text-lg font-medium text-gray-900">Products Listed</div> <div class="mt-2 text-lg font-medium text-gray-900 dark:text-white">Products Listed</div>
<div class="mt-1 text-sm text-gray-500">Discover amazing tools</div> <div class="mt-1 text-sm text-gray-500 dark:text-gray-400">Discover amazing tools</div>
</div> </div>
<div class="text-center"> <div class="text-center">
<div class="text-4xl font-extrabold text-primary-600"> <div class="text-4xl font-extrabold text-primary-600">
{{ App\Models\User::count() }}+ {{ App\Models\User::count() }}+
</div> </div>
<div class="mt-2 text-lg font-medium text-gray-900">Community Members</div> <div class="mt-2 text-lg font-medium text-gray-900 dark:text-white">Community Members</div>
<div class="mt-1 text-sm text-gray-500">Join the community</div> <div class="mt-1 text-sm text-gray-500 dark:text-gray-400">Join the community</div>
</div> </div>
<div class="text-center"> <div class="text-center">
<div class="text-4xl font-extrabold text-primary-600"> <div class="text-4xl font-extrabold text-primary-600">
{{ App\Models\Category::count() }}+ {{ App\Models\Category::count() }}+
</div> </div>
<div class="mt-2 text-lg font-medium text-gray-900">Categories</div> <div class="mt-2 text-lg font-medium text-gray-900 dark:text-white">Categories</div>
<div class="mt-1 text-sm text-gray-500">Find your niche</div> <div class="mt-1 text-sm text-gray-500 dark:text-gray-400">Find your niche</div>
</div> </div>
</div> </div>
</div> </div>
+2
View File
@@ -35,6 +35,7 @@ Route::middleware(['auth'])->group(function () {
// Upvotes // Upvotes
Route::post('/products/{product}/upvote', [UpvoteController::class, 'store'])->name('products.upvote'); Route::post('/products/{product}/upvote', [UpvoteController::class, 'store'])->name('products.upvote');
Route::get('/upvotes', [UpvoteController::class, 'index'])->name('upvotes.index');
// Comments // Comments
Route::post('/products/{product}/comments', [CommentController::class, 'store'])->name('products.comments.store'); Route::post('/products/{product}/comments', [CommentController::class, 'store'])->name('products.comments.store');
@@ -42,6 +43,7 @@ Route::middleware(['auth'])->group(function () {
// Bookmarks // Bookmarks
Route::post('/products/{product}/bookmark', [BookmarkController::class, 'store'])->name('products.bookmark'); Route::post('/products/{product}/bookmark', [BookmarkController::class, 'store'])->name('products.bookmark');
Route::get('/bookmarks', [BookmarkController::class, 'index'])->name('bookmarks.index');
}); });
// Admin/Moderator Product Management Routes // Admin/Moderator Product Management Routes