some progress: users slugs, bookmarks page, upvotes page, design
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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">
|
||||||
© {{ date('Y') }} {{ config('app.name', 'BestTech') }}. All rights reserved.
|
© {{ date('Y') }} {{ config('app.name', 'BestTech') }}. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user