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'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'slug' => User::generateUniqueSlug($data['name']),
]);
}
}
@@ -10,6 +10,24 @@ use Illuminate\Support\Facades\Auth;
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.
*/
@@ -73,6 +73,11 @@ class ProfileController extends Controller
$validated['avatar'] = $avatarName;
}
// Regenerate slug if name changed
if ($request->name !== $user->name) {
$validated['slug'] = User::generateUniqueSlug($request->name);
}
// Update user profile
$user->update($validated);
+18
View File
@@ -10,6 +10,24 @@ use Illuminate\Support\Facades\Auth;
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.
*/
+28
View File
@@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
class User extends Authenticatable
{
@@ -23,6 +24,7 @@ class User extends Authenticatable
'name',
'email',
'password',
'slug',
'avatar',
'bio',
'website',
@@ -144,4 +146,30 @@ class User extends Authenticatable
{
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";
/* Dark mode configuration */
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
}
}
/* Custom Styles */
@theme {
/* Enable class-based dark mode */
--color-scheme: class;
--color-primary-50: #f0f9ff;
--color-primary-100: #e0f2fe;
--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="flex justify-between h-16">
<!-- Logo and Primary Navigation -->
@@ -6,23 +6,23 @@
<!-- Logo -->
<div class="flex-shrink-0 flex items-center">
<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>
</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>
</div>
<!-- Desktop Navigation -->
<div class="hidden sm:ml-8 sm:flex sm:space-x-8">
<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
</a>
@auth
@if(auth()->user()->hasAnyRole(['admin', 'moderator']))
<a href="{{ route('products.manage') }}"
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium {{ request()->routeIs('products.manage') ? 'border-primary-500 text-gray-900' : '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
</a>
@endif
@@ -41,7 +41,7 @@
<input type="text"
name="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...">
</div>
</form>
@@ -50,8 +50,11 @@
<!-- Right Side -->
<div class="hidden sm:ml-6 sm:flex sm:items-center sm:space-x-4">
<!-- Theme Toggle -->
@include('components.ui.theme-toggle')
@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
</a>
<a href="{{ route('register') }}" class="btn-primary">
@@ -86,21 +89,37 @@
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
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;">
<div class="px-4 py-2 border-b border-gray-100">
<p class="text-sm font-medium text-gray-900">{{ Auth::user()->name }}</p>
<p class="text-xs text-gray-500">{{ Auth::user()->email }}</p>
<div class="px-4 py-2 border-b border-gray-100 dark:border-gray-700">
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ Auth::user()->name }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ Auth::user()->email }}</p>
</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
</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
</a>
<form method="POST" action="{{ route('logout') }}">
@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
</button>
</form>
@@ -110,7 +129,10 @@
</div>
<!-- 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"
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">
@@ -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">
Your Profile
</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">
Settings
</a>
@@ -1,9 +1,9 @@
@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">
<!-- 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)
<img src="{{ $product->thumbnail_url }}"
alt="{{ $product->title }}"
@@ -11,7 +11,7 @@
loading="lazy">
@else
<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>
</svg>
</div>
@@ -23,8 +23,8 @@
<div class="p-4">
<div class="flex items-start justify-between mb-2">
<div class="flex-1 min-w-0">
<a href="{{ route('products.show', $product) }}" class="group-hover:text-primary-600 transition-colors">
<h3 class="text-lg font-semibold text-gray-900 truncate">{{ $product->title }}</h3>
<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 dark:text-white truncate">{{ $product->title }}</h3>
</a>
</div>
@@ -33,15 +33,15 @@
</div>
<!-- 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) }}
</p>
<!-- 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">
<!-- 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 }}
</span>
@@ -55,7 +55,7 @@
</div>
<!-- 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">
{{ strtoupper(substr($product->user->name, 0, 1)) }}
</div>
@@ -64,9 +64,9 @@
</div>
<!-- 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) }}"
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
<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>
@@ -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>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full bg-gray-50">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -9,6 +9,30 @@
<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 -->
<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">
@@ -22,7 +46,7 @@
<!-- Additional Head Content -->
@stack('styles')
</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">
<!-- Navigation -->
@include('components.layout.navbar')
@@ -33,55 +57,55 @@
</main>
<!-- 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="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Company Info -->
<div class="col-span-1">
<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>
</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>
<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.
</p>
</div>
<!-- Product Links -->
<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">
<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
<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
</ul>
</div>
<!-- Company Links -->
<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">
<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 hover:text-gray-900">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">About</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 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">Contact</a></li>
</ul>
</div>
<!-- Legal Links -->
<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">
<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 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">Privacy</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>
</div>
</div>
<div class="mt-8 pt-8 border-t border-gray-200">
<p class="text-sm text-gray-500 text-center">
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<p class="text-sm text-gray-500 dark:text-gray-400 text-center">
&copy; {{ date('Y') }} {{ config('app.name', 'BestTech') }}. All rights reserved.
</p>
</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')
<!-- 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="text-center">
<h1 class="text-4xl font-extrabold text-gray-900 sm:text-5xl md:text-6xl">
Discover the Best <span class="text-primary-600">Tech Products</span> from Europe
<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 dark:text-primary-400">Tech Products</span> from Europe
</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.
</p>
<div class="mt-5 max-w-md mx-auto sm:flex sm:justify-center md:mt-8">
@@ -21,14 +21,14 @@
@auth
<div class="mt-3 rounded-md shadow sm:mt-0 sm:ml-3">
<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
</a>
</div>
@else
<div class="mt-3 rounded-md shadow sm:mt-0 sm:ml-3">
<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
</a>
</div>
@@ -40,14 +40,14 @@
<!-- Trending Products Section -->
@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="flex items-center justify-between mb-8">
<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
</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>
@@ -62,17 +62,17 @@
<!-- Categories Section -->
@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="text-center mb-12">
<h2 class="text-3xl font-extrabold text-gray-900">Browse by Category</h2>
<p class="mt-2 text-lg text-gray-600">Explore products in your favorite categories</p>
<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 dark:text-gray-400">Explore products in your favorite categories</p>
</div>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
@foreach($categories as $category)
<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="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">
@@ -81,13 +81,13 @@
</div>
</div>
<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>
@endforeach
</div>
<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 →
</a>
</div>
@@ -97,16 +97,16 @@
<!-- Recent Products Section -->
@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="flex items-center justify-between mb-8">
<div>
<h2 class="text-3xl font-extrabold text-gray-900">
<h2 class="text-3xl font-extrabold text-gray-900 dark:text-white">
✨ Recently Added
</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>
<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 →
</a>
</div>
@@ -118,7 +118,7 @@
</div>
<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 →
</a>
</div>
@@ -160,29 +160,29 @@
</div>
<!-- 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="grid grid-cols-1 gap-8 sm:grid-cols-3">
<div class="text-center">
<div class="text-4xl font-extrabold text-primary-600">
{{ App\Models\Product::approved()->count() }}+
</div>
<div class="mt-2 text-lg font-medium text-gray-900">Products Listed</div>
<div class="mt-1 text-sm text-gray-500">Discover amazing tools</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 dark:text-gray-400">Discover amazing tools</div>
</div>
<div class="text-center">
<div class="text-4xl font-extrabold text-primary-600">
{{ App\Models\User::count() }}+
</div>
<div class="mt-2 text-lg font-medium text-gray-900">Community Members</div>
<div class="mt-1 text-sm text-gray-500">Join the community</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 dark:text-gray-400">Join the community</div>
</div>
<div class="text-center">
<div class="text-4xl font-extrabold text-primary-600">
{{ App\Models\Category::count() }}+
</div>
<div class="mt-2 text-lg font-medium text-gray-900">Categories</div>
<div class="mt-1 text-sm text-gray-500">Find your niche</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 dark:text-gray-400">Find your niche</div>
</div>
</div>
</div>
+2
View File
@@ -35,6 +35,7 @@ Route::middleware(['auth'])->group(function () {
// Upvotes
Route::post('/products/{product}/upvote', [UpvoteController::class, 'store'])->name('products.upvote');
Route::get('/upvotes', [UpvoteController::class, 'index'])->name('upvotes.index');
// Comments
Route::post('/products/{product}/comments', [CommentController::class, 'store'])->name('products.comments.store');
@@ -42,6 +43,7 @@ Route::middleware(['auth'])->group(function () {
// Bookmarks
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