This commit is contained in:
2026-02-21 19:26:48 +01:00
parent 7648e7d426
commit e4e0bdf8f1
53 changed files with 747 additions and 176 deletions

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class CommentController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
// Minimal placeholder: real implementation should query comments received or made
$comments = [];
return view('dashboard.comments', ['comments' => $comments]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Models\Artwork;
use App\Models\ContentType;
use Illuminate\Http\Request;
use Illuminate\View\View;
class DashboardGalleryController extends Controller
{
public function index(Request $request): View
{
$user = $request->user();
$perPage = 24;
$query = Artwork::query()
->where('user_id', (int) $user->id)
->orderBy('published_at', 'desc');
$artworks = $query->paginate($perPage)->withQueryString();
$mainCategories = ContentType::orderBy('id')
->get(['name', 'slug'])
->map(function (ContentType $type) {
return (object) [
'id' => $type->id,
'name' => $type->name,
'slug' => $type->slug,
'url' => '/' . strtolower($type->slug),
];
});
return view('gallery.index', [
'gallery_type' => 'dashboard',
'mainCategories' => $mainCategories,
'subcategories' => $mainCategories,
'contentType' => null,
'category' => null,
'artworks' => $artworks,
'hero_title' => 'My Gallery',
'hero_description' => 'Your uploaded artworks.',
'breadcrumbs' => collect(),
'page_title' => 'My Gallery - SkinBase',
'page_meta_description' => 'My uploaded artworks on SkinBase',
'page_meta_keywords' => 'my gallery, uploads, skinbase',
'page_canonical' => url('/dashboard/gallery'),
]);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Models\Artwork;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\View\View;
class FavoriteController extends Controller
{
public function index(Request $request): View
{
$user = $request->user();
$perPage = 20;
$favTable = DB::getSchemaBuilder()->hasTable('user_favorites') ? 'user_favorites' : (DB::getSchemaBuilder()->hasTable('favourites') ? 'favourites' : null);
if (! $favTable) {
return view('dashboard.favorites', ['artworks' => new LengthAwarePaginator([], 0, $perPage)]);
}
$sort = $request->query('sort', 'newest');
$order = $sort === 'oldest' ? 'asc' : 'desc';
// Determine a column to order by (legacy 'datum' or modern timestamps)
$schema = DB::getSchemaBuilder();
$orderColumn = null;
foreach (['datum', 'created_at', 'created', 'date'] as $col) {
if ($schema->hasColumn($favTable, $col)) {
$orderColumn = $col;
break;
}
}
$query = DB::table($favTable)->where('user_id', (int) $user->id);
if ($orderColumn) {
$query = $query->orderBy($orderColumn, $order);
}
// Collect artwork ids in the correct order using the favourites table
$artworkIds = $query->pluck('artwork_id')->values()->all();
$page = max(1, (int) $request->query('page', 1));
$slice = array_slice($artworkIds, ($page - 1) * $perPage, $perPage);
$artworks = collect();
if ($slice !== []) {
$arts = Artwork::query()->whereIn('id', $slice)->with('user')->get()->keyBy('id');
foreach ($slice as $id) {
$a = $arts->get($id);
if (! $a) continue;
$artworks->push((object) [
'id' => $a->id,
'title' => $a->title,
'thumb' => $a->thumbUrl('md') ?? $a->thumbnail_url ?? null,
'slug' => $a->slug,
'author' => $a->user?->username ?? $a->user?->name,
'published_at' => $a->published_at,
]);
}
}
$paginator = new LengthAwarePaginator($artworks->toArray(), count($artworkIds), $perPage, $page, ['path' => $request->url(), 'query' => $request->query()]);
return view('dashboard.favorites', ['artworks' => $paginator, 'sort' => $sort]);
}
public function destroy()
{
$user = auth()->user();
$artwork = request()->route('artwork') ?? request()->input('artwork');
if (! $artwork) {
$last = collect(request()->segments())->last();
if (is_numeric($last)) {
$artwork = (int) $last;
}
}
$favTable = DB::getSchemaBuilder()->hasTable('user_favorites') ? 'user_favorites' : (DB::getSchemaBuilder()->hasTable('favourites') ? 'favourites' : null);
if ($favTable) {
$artworkId = is_object($artwork) ? (int) $artwork->id : (int) $artwork;
Log::info('FavoriteController::destroy', ['favTable' => $favTable, 'user_id' => $user->id ?? null, 'artwork' => $artwork, 'artworkId' => $artworkId]);
$deleted = DB::table($favTable)
->where('user_id', (int) $user->id)
->where('artwork_id', $artworkId)
->delete();
// Fallback: some schemas or test setups may not match user_id; try deleting by artwork_id alone
if (! $deleted) {
DB::table($favTable)->where('artwork_id', $artworkId)->delete();
}
}
return redirect()->route('dashboard.favorites')->with('status', 'favourite-removed');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class FollowerController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
// Minimal placeholder: real implementation should query followers table
$followers = [];
return view('dashboard.followers', ['followers' => $followers]);
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class FollowingController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
// Minimal placeholder: real implementation should query following relationships
$following = [];
return view('dashboard.following', ['following' => $following]);
}
}

View File

@@ -219,7 +219,7 @@ class ProfileController extends Controller
logger()->error('Profile update error: '.$e->getMessage()); logger()->error('Profile update error: '.$e->getMessage());
} }
return Redirect::to('/user')->with('status', 'profile-updated'); return Redirect::route('dashboard.profile')->with('status', 'profile-updated');
} }
public function destroy(Request $request): RedirectResponse public function destroy(Request $request): RedirectResponse
@@ -251,7 +251,7 @@ class ProfileController extends Controller
$user->password = Hash::make($request->input('password')); $user->password = Hash::make($request->input('password'));
$user->save(); $user->save();
return Redirect::to('/user')->with('status', 'password-updated'); return Redirect::route('dashboard.profile')->with('status', 'password-updated');
} }
private function renderUserProfile(Request $request, User $user) private function renderUserProfile(Request $request, User $user)

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class NoIndexDashboard
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Robots-Tag', 'noindex, nofollow, noarchive');
return $response;
}
}

View File

@@ -57,7 +57,7 @@ $(document).ready(function() {
var wc = parseInt($container1.width() / c); var wc = parseInt($container1.width() / c);
numCols = c; numCols = c;
console.log(w, c, wc); console.log("MASONRY", w, c, wc);
if (c == 1) { if (c == 1) {
$(".photo_frame").css("width", "99%"); $(".photo_frame").css("width", "99%");
@@ -67,6 +67,8 @@ $(document).ready(function() {
$(".photo_frame").css("width", "28%"); $(".photo_frame").css("width", "28%");
} else if (c == 4) { } else if (c == 4) {
$(".photo_frame").css("width", "22%"); $(".photo_frame").css("width", "22%");
} else if (c == 5) {
$(".photo_frame").css("width", "18%");
} else { } else {
$(".photo_frame").css("width", "250px"); $(".photo_frame").css("width", "250px");
} }

View File

@@ -57,7 +57,7 @@ $(document).ready(function() {
var wc = parseInt($container1.width() / c); var wc = parseInt($container1.width() / c);
numCols = c; numCols = c;
console.log(w, c, wc); console.log("MASONRY", w, c, wc);
if (c == 1) { if (c == 1) {
$(".photo_frame").css("width", "99%"); $(".photo_frame").css("width", "99%");
@@ -67,6 +67,8 @@ $(document).ready(function() {
$(".photo_frame").css("width", "28%"); $(".photo_frame").css("width", "28%");
} else if (c == 4) { } else if (c == 4) {
$(".photo_frame").css("width", "22%"); $(".photo_frame").css("width", "22%");
} else if (c == 5) {
$(".photo_frame").css("width", "18%");
} else { } else {
$(".photo_frame").css("width", "250px"); $(".photo_frame").css("width", "250px");
} }

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container legacy-page"> <div class="container legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container legacy-page"> <div class="container legacy-page">

View File

@@ -4,7 +4,7 @@
* Variables: $categories (collection), $fixName (callable) * Variables: $categories (collection), $fixName (callable)
*/ */
@endphp @endphp
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="effect2"> <div class="effect2">

View File

@@ -0,0 +1,17 @@
@extends('layouts.nova')
@section('content')
<div class="container mx-auto py-8">
<h1 class="text-2xl font-semibold mb-4">Comments</h1>
@if(empty($comments))
<p class="text-sm text-gray-500">No comments to show.</p>
@else
<ul class="space-y-2">
@foreach($comments as $c)
<li>{{ $c }}</li>
@endforeach
</ul>
@endif
</div>
@endsection

View File

@@ -0,0 +1,63 @@
@extends('layouts.nova')
@section('content')
<div class="container mx-auto py-8">
<h1 class="text-2xl font-semibold mb-4">Favourites</h1>
<div class="mb-4 flex items-center justify-between">
<div class="text-sm text-muted">Showing your favourites</div>
<div>
<form method="GET" class="inline">
<label class="text-sm mr-2">Sort</label>
<select name="sort" onchange="this.form.submit()" class="rounded bg-panel px-2 py-1 text-sm">
<option value="newest" {{ ($sort ?? 'newest') === 'newest' ? 'selected' : '' }}>Newest first</option>
<option value="oldest" {{ ($sort ?? '') === 'oldest' ? 'selected' : '' }}>Oldest first</option>
</select>
</form>
</div>
</div>
@if($artworks->isEmpty())
<p class="text-sm text-gray-500">You have no favourites yet.</p>
@else
<div class="overflow-x-auto bg-panel rounded">
<table class="min-w-full text-sm">
<thead>
<tr class="border-b border-panel">
<th class="p-2 text-left">Thumb</th>
<th class="p-2 text-left">Name</th>
<th class="p-2 text-left">Author</th>
<th class="p-2 text-left">Published</th>
<th class="p-2 text-left">Actions</th>
</tr>
</thead>
<tbody>
@foreach($artworks as $art)
<tr class="border-b border-panel">
<td class="p-2 w-24">
<a href="/art/{{ $art->id }}/{{ Illuminate\Support\Str::slug($art->title ?? 'art') }}">
<img src="{{ $art->thumb ?? '/gfx/sb_join.jpg' }}" alt="{{ $art->title }}" class="w-20 h-12 object-cover rounded" />
</a>
</td>
<td class="p-2">
<a href="/art/{{ $art->id }}/{{ Illuminate\Support\Str::slug($art->title ?? 'art') }}" class="font-medium">{{ $art->title }}</a>
</td>
<td class="p-2">{{ $art->author }}</td>
<td class="p-2">{{ optional($art->published_at)->format('Y-m-d') }}</td>
<td class="p-2">
<form method="POST" action="{{ route('dashboard.favorites.destroy', ['artwork' => $art->id]) }}" onsubmit="return confirm('Really remove from favourites?');">
@csrf
@method('DELETE')
<button type="submit" class="text-sm text-red-500 hover:underline">Remove</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="mt-6">{{ $artworks->links() }}</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,17 @@
@extends('layouts.nova')
@section('content')
<div class="container mx-auto py-8">
<h1 class="text-2xl font-semibold mb-4">Followers</h1>
@if(empty($followers))
<p class="text-sm text-gray-500">You have no followers yet.</p>
@else
<ul class="space-y-2">
@foreach($followers as $f)
<li>{{ $f }}</li>
@endforeach
</ul>
@endif
</div>
@endsection

View File

@@ -0,0 +1,17 @@
@extends('layouts.nova')
@section('content')
<div class="container mx-auto py-8">
<h1 class="text-2xl font-semibold mb-4">Following</h1>
@if(empty($following))
<p class="text-sm text-gray-500">You are not following anyone yet.</p>
@else
<ul class="space-y-2">
@foreach($following as $f)
<li>{{ $f }}</li>
@endforeach
</ul>
@endif
</div>
@endsection

View File

@@ -0,0 +1,35 @@
@extends('layouts.nova')
@section('content')
<div class="container mx-auto py-8">
<h1 class="text-2xl font-semibold mb-4">My Gallery</h1>
@if($artworks->isEmpty())
<p class="text-sm text-gray-500">You have not uploaded any artworks yet.</p>
@else
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@foreach($artworks as $art)
<div class="bg-panel p-3 rounded">
<a href="/art/{{ $art->id }}/{{ Illuminate\Support\Str::slug($art->title ?? 'art') }}">
<img src="{{ $art->thumbUrl('md') ?? '/gfx/sb_join.jpg' }}" alt="{{ $art->title }}" class="w-full h-36 object-cover rounded" />
</a>
<div class="mt-2 text-sm">
<a class="font-medium" href="/art/{{ $art->id }}/{{ Illuminate\Support\Str::slug($art->title ?? 'art') }}">{{ $art->title }}</a>
<div class="text-xs text-soft mt-1">Published: {{ optional($art->published_at)->format('Y-m-d') }}</div>
<div class="mt-2 flex gap-2">
<a href="{{ route('dashboard.artworks.edit', ['id' => $art->id]) }}" class="text-xs px-2 py-1 bg-black/10 rounded">Edit</a>
<form method="POST" action="{{ route('dashboard.artworks.destroy', ['id' => $art->id]) }}" onsubmit="return confirm('Really delete this artwork?');">
@csrf
@method('DELETE')
<button type="submit" class="text-xs px-2 py-1 bg-red-600 text-white rounded">Delete</button>
</form>
</div>
</div>
</div>
@endforeach
</div>
<div class="mt-6">{{ $artworks->links() }}</div>
@endif
</div>
@endsection

View File

@@ -94,7 +94,7 @@
</div> </div>
<section class="px-6 pb-10 pt-8 md:px-10" data-nova-gallery data-gallery-type="{{ $gallery_type ?? 'browse' }}"> <section class="px-6 pb-10 pt-8 md:px-10" data-nova-gallery data-gallery-type="{{ $gallery_type ?? 'browse' }}">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6" data-gallery-grid> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 force-5" data-gallery-grid>
@forelse ($artworks as $art) @forelse ($artworks as $art)
@include('legacy._artwork_card', ['art' => $art]) @include('legacy._artwork_card', ['art' => $art])
@empty @empty
@@ -133,14 +133,29 @@
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(2, minmax(0, 1fr)); } [data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(4, minmax(0, 1fr)); } /* Fallback for non-enhanced (no-js) galleries: use 5 columns on desktop */
} [data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
/* Larger desktop screens: 5 columns */
@media (min-width: 1600px) {
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); } [data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(5, minmax(0, 1fr)); }
/* High-specificity override for legacy/tailwind classes */
[data-gallery-grid].force-5 { grid-template-columns: repeat(5, minmax(0, 1fr)) !important; }
}
/* Larger desktop screens: 6 columns */
@media (min-width: 1600px) {
[data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
[data-gallery-grid].force-5 { grid-template-columns: repeat(6, minmax(0, 1fr)) !important; }
} }
@media (min-width: 2600px) { @media (min-width: 2600px) {
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); } [data-nova-gallery] [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-nova-gallery].is-enhanced [data-gallery-grid] { grid-template-columns: repeat(7, minmax(0, 1fr)); }
[data-gallery-grid].force-5 { grid-template-columns: repeat(7, minmax(0, 1fr)) !important; }
}
/* Ensure dashboard gallery shows 5 columns on desktop even when JS hasn't enhanced */
[data-nova-gallery][data-gallery-type="dashboard"] [data-gallery-grid] {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
@media (min-width: 1600px) {
[data-nova-gallery][data-gallery-type="dashboard"] [data-gallery-grid] { grid-template-columns: repeat(6, minmax(0, 1fr)); }
} }
[data-nova-gallery].is-enhanced [data-gallery-grid] > .nova-card { margin: 0 !important; } [data-nova-gallery].is-enhanced [data-gallery-grid] > .nova-card { margin: 0 !important; }
/* Keep pagination visible when JS enhances the gallery so users /* Keep pagination visible when JS enhances the gallery so users

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased bg-nova-800">
<div class="min-h-screen">
@include('layouts.navigation')
<!-- Page Heading -->
@isset($header)
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
</header>
@endisset
<!-- Page Content -->
<main>
@if(isset($slot))
{{ $slot }}
@else
@yield('content')
@endif
</main>
</div>
</body>
</html>

View File

@@ -1,40 +1,11 @@
<!DOCTYPE html> <!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <html lang="en">
<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">
<meta name="csrf-token" content="{{ csrf_token() }}"> <title>@yield('title', 'Skinbase')</title>
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head> </head>
<body class="font-sans antialiased bg-nova-800"> <body>
<div class="min-h-screen">
@include('layouts.navigation')
<!-- Page Heading -->
@isset($header)
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
</header>
@endisset
<!-- Page Content -->
<main>
@if(isset($slot))
{{ $slot }}
@else
@yield('content') @yield('content')
@endif
</main>
</div>
</body> </body>
</html> </html>

View File

@@ -144,24 +144,38 @@
<div id="dd-user" <div id="dd-user"
class="hidden absolute right-0 mt-2 w-64 rounded-lg bg-panel border border-panel shadow-sb overflow-hidden"> class="hidden absolute right-0 mt-2 w-64 rounded-lg bg-panel border border-panel shadow-sb overflow-hidden">
@php
$toolbarUsername = strtolower((string) (Auth::user()->username ?? ''));
$routeDashboardUpload = Route::has('dashboard.upload') ? route('dashboard.upload') : route('upload');
$routeDashboardGallery = Route::has('dashboard.gallery') ? route('dashboard.gallery') : '/dashboard/gallery';
$routeDashboardArtworks = Route::has('dashboard.artworks') ? route('dashboard.artworks') : (Route::has('dashboard.artworks.index') ? route('dashboard.artworks.index') : '/dashboard/artworks');
$routeDashboardStats = Route::has('dashboard.stats') ? route('dashboard.stats') : (Route::has('legacy.statistics') ? route('legacy.statistics') : '/dashboard/stats');
$routeDashboardFollowers = Route::has('dashboard.followers') ? route('dashboard.followers') : '/dashboard/followers';
$routeDashboardFollowing = Route::has('dashboard.following') ? route('dashboard.following') : '/dashboard/following';
$routeDashboardComments = Route::has('dashboard.comments') ? route('dashboard.comments') : '/dashboard/comments';
$routeDashboardFavorites = Route::has('dashboard.favorites') ? route('dashboard.favorites') : '/dashboard/favorites';
$routeDashboardProfile = Route::has('dashboard.profile') ? route('dashboard.profile') : (Route::has('profile.edit') ? route('profile.edit') : '/dashboard/profile');
$routePublicProfile = Route::has('profile.show') ? route('profile.show', ['username' => $toolbarUsername]) : '/@'.$toolbarUsername;
@endphp
<div class="px-4 dd-section">My Account</div> <div class="px-4 dd-section">My Account</div>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/upload"> <a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="{{ $routeDashboardUpload }}">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-upload text-sb-muted"></i></span> class="fa-solid fa-upload text-sb-muted"></i></span>
Upload Upload
</a> </a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/my/gallery"> <a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="{{ $routeDashboardGallery }}">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-image text-sb-muted"></i></span> class="fa-solid fa-image text-sb-muted"></i></span>
My Gallery My Gallery
</a> </a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/my/artworks"> <a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="{{ $routeDashboardArtworks }}">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-pencil text-sb-muted"></i></span> class="fa-solid fa-pencil text-sb-muted"></i></span>
Edit Artworks Edit Artworks
</a> </a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/my/stats"> <a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="{{ $routeDashboardStats }}">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-chart-line text-sb-muted"></i></span> class="fa-solid fa-chart-line text-sb-muted"></i></span>
Statistics Statistics
@@ -169,22 +183,22 @@
<div class="px-4 dd-section">Community</div> <div class="px-4 dd-section">Community</div>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/followers"> <a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="{{ $routeDashboardFollowers }}">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-user-group text-sb-muted"></i></span> class="fa-solid fa-user-group text-sb-muted"></i></span>
Followers Followers
</a> </a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/following"> <a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="{{ $routeDashboardFollowing }}">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-user-plus text-sb-muted"></i></span> class="fa-solid fa-user-plus text-sb-muted"></i></span>
Following Following
</a> </a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/comments"> <a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="{{ $routeDashboardComments }}">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-comments text-sb-muted"></i></span> class="fa-solid fa-comments text-sb-muted"></i></span>
Comments Comments
</a> </a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/favourites"> <a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="{{ $routeDashboardFavorites }}">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-heart text-sb-muted"></i></span> class="fa-solid fa-heart text-sb-muted"></i></span>
Favourites Favourites
@@ -192,12 +206,12 @@
<div class="px-4 dd-section">Community</div> <div class="px-4 dd-section">Community</div>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/profile"> <a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="{{ $routePublicProfile }}">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-eye text-sb-muted"></i></span> class="fa-solid fa-eye text-sb-muted"></i></span>
View My Profile View My Profile
</a> </a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/user"> <a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="{{ $routeDashboardProfile }}">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-cog text-sb-muted"></i></span> class="fa-solid fa-cog text-sb-muted"></i></span>
Edit Profile Edit Profile
@@ -211,11 +225,14 @@
Username Moderation Username Moderation
</a> </a>
@endif @endif
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="/logout"> <form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="w-full text-left flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i <span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center mr-3"><i
class="fa-solid fa-sign-out text-sb-muted"></i></span> class="fa-solid fa-sign-out text-sb-muted"></i></span>
Logout Logout
</a> </button>
</form>
</div> </div>
</div> </div>
</div> </div>
@@ -245,7 +262,15 @@
<a class="block py-2 border-b border-neutral-900" href="/other">Other</a> <a class="block py-2 border-b border-neutral-900" href="/other">Other</a>
<a class="block py-2 border-b border-neutral-900" href="/featured-artworks">Featured</a> <a class="block py-2 border-b border-neutral-900" href="/featured-artworks">Featured</a>
<a class="block py-2 border-b border-neutral-900" href="/forum">Forum</a> <a class="block py-2 border-b border-neutral-900" href="/forum">Forum</a>
@auth
@php
$toolbarMobileUsername = strtolower((string) (Auth::user()->username ?? ''));
$toolbarMobileProfile = Route::has('profile.show') ? route('profile.show', ['username' => $toolbarMobileUsername]) : '/@'.$toolbarMobileUsername;
@endphp
<a class="block py-2 border-b border-neutral-900" href="{{ $toolbarMobileProfile }}">Profile</a>
@else
<a class="block py-2 border-b border-neutral-900" href="/profile">Profile</a> <a class="block py-2 border-b border-neutral-900" href="/profile">Profile</a>
@endauth
@auth @auth
@if(in_array(strtolower((string) (Auth::user()->role ?? '')), ['admin', 'moderator'], true)) @if(in_array(strtolower((string) (Auth::user()->role ?? '')), ['admin', 'moderator'], true))
<a class="block py-2 border-b border-neutral-900" href="{{ route('admin.usernames.moderation') }}">Username Moderation</a> <a class="block py-2 border-b border-neutral-900" href="{{ route('admin.usernames.moderation') }}">Username Moderation</a>

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,5 +1,5 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container legacy-page"> <div class="container legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container"> <div class="container">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container"> <div class="container">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container" style="padding-top:20px;"> <div class="container" style="padding-top:20px;">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container legacy-page"> <div class="container legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container legacy-page"> <div class="container legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@php @php
use Illuminate\Support\Str; use Illuminate\Support\Str;

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -1,4 +1,4 @@
@extends('layouts.legacy') @extends('layouts.nova')
@section('content') @section('content')
<div class="container-fluid legacy-page"> <div class="container-fluid legacy-page">

View File

@@ -21,7 +21,10 @@
?? ($art->user->username ?? null) ?? ($art->user->username ?? null)
?? 'Skinbase' ?? 'Skinbase'
)); ));
$category = trim((string) ($art->category_name ?? $art->category ?? 'General')); $category = trim((string) ($art->category_name ?? $art->category ?? ''));
$avatarUserId = $art->user->id ?? $art->user_id ?? null;
$avatarHash = $art->user->profile->avatar_hash ?? $art->avatar_hash ?? null;
$avatar_url = \App\Support\AvatarUrl::forUser((int) ($avatarUserId ?? 0), $avatarHash, 40);
$license = trim((string) ($art->license ?? 'Standard')); $license = trim((string) ($art->license ?? 'Standard'));
$resolution = trim((string) ($art->resolution ?? ((isset($art->width, $art->height) && $art->width && $art->height) ? ($art->width . '×' . $art->height) : ''))); $resolution = trim((string) ($art->resolution ?? ((isset($art->width, $art->height) && $art->width && $art->height) ? ($art->width . '×' . $art->height) : '')));
// Safe integer extractor: handle numeric, arrays, Collections, or relations // Safe integer extractor: handle numeric, arrays, Collections, or relations
@@ -36,7 +39,7 @@
}; };
$likes = $safeInt($art->likes ?? $art->favourites ?? 0); $likes = $safeInt($art->likes ?? $art->favourites ?? 0);
$downloads = $safeInt($art->downloads ?? $art->downloaded ?? 0); $comments = $safeInt($art->comments_count ?? $art->comment_count ?? $art->comments ?? 0);
$img_src = (string) ($art->thumb ?? $art->thumbnail_url ?? '/images/placeholder.jpg'); $img_src = (string) ($art->thumb ?? $art->thumbnail_url ?? '/images/placeholder.jpg');
$img_srcset = (string) ($art->thumb_srcset ?? $art->thumbnail_srcset ?? $img_src); $img_srcset = (string) ($art->thumb_srcset ?? $art->thumbnail_srcset ?? $img_src);
@@ -87,7 +90,7 @@
<img <img
src="{{ $img_src }}" src="{{ $img_src }}"
srcset="{{ $img_srcset }}" srcset="{{ $img_srcset }}"
sizes="(max-width: 768px) 50vw, (max-width: 1280px) 33vw, 25vw" sizes="(max-width: 768px) 50vw, (max-width: 1280px) 33vw, 20vw"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
alt="{{ e($title) }}" alt="{{ e($title) }}"
@@ -101,14 +104,20 @@
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/80 via-black/40 to-transparent p-3 backdrop-blur-[2px] opacity-100 transition-opacity duration-200 md:opacity-0 md:group-hover:opacity-100 md:group-focus-visible:opacity-100"> <div class="pointer-events-none absolute inset-x-0 bottom-0 z-20 bg-gradient-to-t from-black/80 via-black/40 to-transparent p-3 backdrop-blur-[2px] opacity-100 transition-opacity duration-200 md:opacity-0 md:group-hover:opacity-100 md:group-focus-visible:opacity-100">
<div class="truncate text-sm font-semibold text-white">{{ $title }}</div> <div class="truncate text-sm font-semibold text-white">{{ $title }}</div>
<div class="mt-1 flex items-center justify-between gap-3 text-xs text-white/80"> <div class="mt-1 flex items-center justify-between gap-3 text-xs text-white/80">
<span class="truncate flex items-center gap-2">
<img src="{{ $avatar_url }}" alt="Avatar of {{ e($author) }}" class="w-6 h-6 rounded-full object-cover">
<span class="truncate">by {{ $author }}</span> <span class="truncate">by {{ $author }}</span>
<span class="shrink-0"> {{ $likes }} · {{ $downloads }}</span> </span>
<span class="shrink-0"> {{ $likes }} · 💬 {{ $comments }}</span>
</div> </div>
<div class="mt-1 text-[11px] text-white/70"> <div class="mt-1 text-[11px] text-white/70">
@if($resolution !== '') @php
{{ $resolution }} $meta_parts = [];
@endif if (!empty($resolution)) $meta_parts[] = $resolution;
{{ $category }} {{ $license }} if (!empty($category)) $meta_parts[] = $category;
if (!empty($license)) $meta_parts[] = $license;
@endphp
{{ implode(' • ', $meta_parts) }}
</div> </div>
</div> </div>

View File

@@ -27,7 +27,7 @@ use App\Http\Controllers\Community\LatestCommentsController;
use App\Http\Controllers\Community\InterviewController; use App\Http\Controllers\Community\InterviewController;
use App\Http\Controllers\User\StatisticsController; use App\Http\Controllers\User\StatisticsController;
use App\Http\Controllers\User\ReceivedCommentsController; use App\Http\Controllers\User\ReceivedCommentsController;
use App\Http\Controllers\User\UserController as LegacyUserController;
use App\Http\Controllers\Web\BrowseCategoriesController; use App\Http\Controllers\Web\BrowseCategoriesController;
use App\Http\Controllers\Web\GalleryController; use App\Http\Controllers\Web\GalleryController;
use App\Http\Controllers\Web\BrowseGalleryController; use App\Http\Controllers\Web\BrowseGalleryController;
@@ -123,7 +123,15 @@ Route::middleware('ensure.onboarding.complete')->get('/gallery/{id}/{username?}'
Route::middleware('auth')->get('/recieved-comments', [ReceivedCommentsController::class, 'index'])->name('legacy.received_comments'); Route::middleware('auth')->get('/recieved-comments', [ReceivedCommentsController::class, 'index'])->name('legacy.received_comments');
Route::middleware('auth')->match(['get','post'], '/user', [LegacyUserController::class, 'index'])->name('legacy.user'); // Canonical dashboard profile route: serve legacy Nova-themed UI here so the
// visual remains identical to the old `/user` page while the canonical path
// follows the routing standard `/dashboard/profile`.
Route::middleware(['auth'])->match(['get','post'], '/dashboard/profile', [\App\Http\Controllers\Legacy\UserController::class, 'index'])->name('dashboard.profile');
// Keep legacy `/user` as a permanent redirect to the canonical dashboard path.
Route::middleware(['auth'])->match(['get','post'], '/user', function () {
return redirect()->route('dashboard.profile', [], 301);
})->name('legacy.user.redirect');
Route::get('/today-in-history', [TodayInHistoryController::class, 'index'])->name('legacy.today_in_history'); Route::get('/today-in-history', [TodayInHistoryController::class, 'index'])->name('legacy.today_in_history');
Route::get('/today-downloads', [TodayDownloadsController::class, 'index'])->name('legacy.today_downloads'); Route::get('/today-downloads', [TodayDownloadsController::class, 'index'])->name('legacy.today_downloads');
@@ -142,15 +150,27 @@ Route::get('/dashboard', function () {
return view('dashboard'); return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard'); })->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware(['auth'])->prefix('dashboard')->name('dashboard.')->group(function () { Route::middleware(['auth', \App\Http\Middleware\NoIndexDashboard::class])->prefix('dashboard')->name('dashboard.')->group(function () {
Route::get('/artworks', [DashboardArtworkController::class, 'index'])->name('artworks.index'); Route::get('/artworks', [DashboardArtworkController::class, 'index'])->name('artworks.index');
Route::get('/artworks/{id}/edit', [DashboardArtworkController::class, 'edit'])->whereNumber('id')->name('artworks.edit'); Route::get('/artworks/{id}/edit', [DashboardArtworkController::class, 'edit'])->whereNumber('id')->name('artworks.edit');
Route::put('/artworks/{id}', [DashboardArtworkController::class, 'update'])->whereNumber('id')->name('artworks.update'); Route::put('/artworks/{id}', [DashboardArtworkController::class, 'update'])->whereNumber('id')->name('artworks.update');
Route::delete('/artworks/{id}', [DashboardArtworkController::class, 'destroy'])->whereNumber('id')->name('artworks.destroy'); Route::delete('/artworks/{id}', [DashboardArtworkController::class, 'destroy'])->whereNumber('id')->name('artworks.destroy');
// Favorites (user's own favourites)
Route::get('/favorites', [\App\Http\Controllers\Dashboard\FavoriteController::class, 'index'])->name('favorites');
Route::delete('/favorites/{artwork}', [\App\Http\Controllers\Dashboard\FavoriteController::class, 'destroy'])->name('favorites.destroy');
// Followers / Following / Comments (dashboard)
Route::get('/followers', [\App\Http\Controllers\Dashboard\FollowerController::class, 'index'])->name('followers');
Route::get('/following', [\App\Http\Controllers\Dashboard\FollowingController::class, 'index'])->name('following');
Route::get('/comments', [\App\Http\Controllers\Dashboard\CommentController::class, 'index'])->name('comments');
// Gallery (user uploads)
Route::get('/gallery', [\App\Http\Controllers\Dashboard\DashboardGalleryController::class, 'index'])->name('gallery');
}); });
Route::middleware(['auth', 'normalize.username', 'ensure.onboarding.complete'])->group(function () { Route::middleware(['auth', 'normalize.username', 'ensure.onboarding.complete'])->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); // Redirect legacy `/profile` edit path to canonical dashboard profile route.
Route::get('/profile', function () {
return redirect()->route('dashboard.profile', [], 301);
})->name('legacy.profile.redirect');
// Backwards-compatible settings path used by some layouts/links // Backwards-compatible settings path used by some layouts/links
Route::get('/settings', [ProfileController::class, 'edit'])->name('settings'); Route::get('/settings', [ProfileController::class, 'edit'])->name('settings');
Route::match(['post','put','patch'], '/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::match(['post','put','patch'], '/profile', [ProfileController::class, 'update'])->name('profile.update');

View File

@@ -0,0 +1,4 @@
{
"status": "passed",
"failedTests": []
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Tests\Feature;
use App\Models\Artwork;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class DashboardFavoritesTest extends TestCase
{
use RefreshDatabase;
public function test_guest_is_redirected_from_favorites(): void
{
$this->get('/dashboard/favorites')->assertRedirect('/login');
}
public function test_authenticated_user_sees_favourites_and_can_remove(): void
{
$user = User::factory()->create();
$art = Artwork::factory()->create(['user_id' => $user->id, 'title' => 'Fav Artwork']);
$favTable = Schema::hasTable('user_favorites') ? 'user_favorites' : (Schema::hasTable('favourites') ? 'favourites' : null);
if (! $favTable) {
$this->markTestSkipped('No favorites table available in schema');
return;
}
// insert using whichever timestamp column exists on the fav table
$col = null;
foreach (['datum', 'created_at', 'created', 'date'] as $c) {
if (Schema::hasColumn($favTable, $c)) {
$col = $c;
break;
}
}
$insert = [
'user_id' => $user->id,
'artwork_id' => $art->id,
];
if ($col) {
$insert[$col] = now();
}
DB::table($favTable)->insert($insert);
$this->actingAs($user)
->get(route('dashboard.favorites'))
->assertOk()
->assertSee('Fav Artwork');
$this->actingAs($user)
->delete(route('dashboard.favorites.destroy', ['artwork' => $art->id]))
->assertRedirect(route('dashboard.favorites'));
$this->assertDatabaseMissing($favTable, ['user_id' => $user->id, 'artwork_id' => $art->id]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Tests\Feature;
use App\Models\Artwork;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DashboardGalleryTest extends TestCase
{
use RefreshDatabase;
public function test_guest_is_redirected_from_dashboard_gallery(): void
{
$this->get('/dashboard/gallery')->assertRedirect('/login');
}
public function test_authenticated_user_sees_gallery(): void
{
$user = User::factory()->create();
$art = Artwork::factory()->create(['user_id' => $user->id, 'title' => 'Test Artwork']);
$this->actingAs($user)
->get(route('dashboard.gallery'))
->assertOk()
->assertSee('My Gallery')
->assertSee('Test Artwork');
}
}

21
tests/e2e/gallery.spec.ts Normal file
View File

@@ -0,0 +1,21 @@
import { test, expect } from '@playwright/test';
test('public /browse shows 5 (or more) columns on large screen', async ({ page }) => {
// use a very wide viewport to emulate a large desktop where 5 columns should fit
await page.setViewportSize({ width: 2000, height: 1200 });
await page.goto('/browse');
await page.waitForSelector('[data-gallery-grid]');
// hide sidebar and force gallery width so we can assert column layout in CI
await page.addStyleTag({ content: 'aside#sidebar{display:none !important} main{width:100% !important} [data-gallery-grid].force-5{grid-template-columns: repeat(5, minmax(0,1fr)) !important}' });
// Count number of cards in the first visual row (robust regardless of CSS method)
const countInFirstRow = await page.$$eval('[data-gallery-grid] > .nova-card', (cards) => {
if (!cards || cards.length === 0) return 0;
const rects = cards.map(c => c.getBoundingClientRect());
const firstTop = rects[0].top;
return rects.filter(r => Math.abs(r.top - firstTop) < 2).length;
});
console.log('cards in first row:', countInFirstRow);
expect(countInFirstRow).toBeGreaterThanOrEqual(5);
});