feat: leaderboard with top (by period) and new tabs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
25e1dd2413
commit
3f8fa9f22f
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class LeaderboardController extends Controller
|
||||||
|
{
|
||||||
|
private const PER_PAGE = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the leaderboard: top products by period, or the newest products.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$tab = $request->query('tab') === 'new' ? 'new' : 'top';
|
||||||
|
$period = array_key_exists((string) $request->query('period'), Product::LEADERBOARD_PERIODS)
|
||||||
|
? $request->query('period')
|
||||||
|
: 'today';
|
||||||
|
|
||||||
|
$query = Product::query()->with('tags');
|
||||||
|
|
||||||
|
$products = ($tab === 'new' ? $query->approved()->latest('products.created_at')->latest('products.id') : $query->topFor($period))
|
||||||
|
->paginate(self::PER_PAGE)
|
||||||
|
->appends($tab === 'new' ? ['tab' => 'new'] : ['tab' => 'top', 'period' => $period]);
|
||||||
|
|
||||||
|
return view('leaderboard.index', [
|
||||||
|
'products' => $products,
|
||||||
|
'tab' => $tab,
|
||||||
|
'period' => $period,
|
||||||
|
'periods' => Product::LEADERBOARD_PERIODS,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -284,6 +284,42 @@ class Product extends Model
|
|||||||
return $query->withCount('upvotes')->orderBy('upvotes_count', 'desc');
|
return $query->withCount('upvotes')->orderBy('upvotes_count', 'desc');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leaderboard periods: key => label.
|
||||||
|
*/
|
||||||
|
public const LEADERBOARD_PERIODS = [
|
||||||
|
'today' => 'Today',
|
||||||
|
'week' => 'This week',
|
||||||
|
'month' => 'This month',
|
||||||
|
'all' => 'All time',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approved products ranked by the upvotes they received in the period,
|
||||||
|
* then by total upvotes, then newest. Products with no upvotes in the
|
||||||
|
* period are left out. Adds `period_upvotes_count` and `total_upvotes_count`.
|
||||||
|
*/
|
||||||
|
public function scopeTopFor(Builder $query, string $period): Builder
|
||||||
|
{
|
||||||
|
$since = match ($period) {
|
||||||
|
'today' => now()->startOfDay(),
|
||||||
|
'week' => now()->subDays(7),
|
||||||
|
'month' => now()->subDays(30),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
$inPeriod = fn (Builder $upvotes) => $since ? $upvotes->where('created_at', '>=', $since) : $upvotes;
|
||||||
|
|
||||||
|
return $query->approved()
|
||||||
|
->whereHas('upvotes', $inPeriod)
|
||||||
|
->withCount(['upvotes as period_upvotes_count' => $inPeriod])
|
||||||
|
->withCount('upvotes as total_upvotes_count')
|
||||||
|
->orderByDesc('period_upvotes_count')
|
||||||
|
->orderByDesc('total_upvotes_count')
|
||||||
|
->orderByDesc('products.created_at')
|
||||||
|
->orderByDesc('products.id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scope a query to sort products.
|
* Scope a query to sort products.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
class Upvote extends Model
|
class Upvote extends Model
|
||||||
{
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if the model should be timestamped.
|
* Indicates if the model should be timestamped.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -19,6 +19,10 @@
|
|||||||
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' }}">
|
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>
|
||||||
|
<a href="{{ route('leaderboard') }}"
|
||||||
|
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium {{ request()->routeIs('leaderboard') ? '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-200' }}">
|
||||||
|
Leaderboard
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search Bar (Desktop) -->
|
<!-- Search Bar (Desktop) -->
|
||||||
@@ -230,6 +234,10 @@
|
|||||||
class="block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('products.index') ? 'border-primary-500 text-primary-700 bg-primary-50' : 'border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700' }}">
|
class="block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('products.index') ? 'border-primary-500 text-primary-700 bg-primary-50' : 'border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700' }}">
|
||||||
Discover
|
Discover
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ route('leaderboard') }}"
|
||||||
|
class="block pl-3 pr-4 py-2 border-l-4 text-base font-medium {{ request()->routeIs('leaderboard') ? 'border-primary-500 text-primary-700 dark:text-primary-200 bg-primary-50 dark:bg-primary-900' : 'border-transparent text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 hover:text-gray-700 dark:hover:text-gray-200' }}">
|
||||||
|
Leaderboard
|
||||||
|
</a>
|
||||||
@auth
|
@auth
|
||||||
@if(auth()->user()->hasRole('admin'))
|
@if(auth()->user()->hasRole('admin'))
|
||||||
<a href="{{ route('admin.dashboard') }}"
|
<a href="{{ route('admin.dashboard') }}"
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@php
|
||||||
|
$title = 'Leaderboard - ' . config('app.name');
|
||||||
|
$pill = fn (bool $active) => $active
|
||||||
|
? 'bg-primary-600 text-white border-primary-600'
|
||||||
|
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700';
|
||||||
|
$tabClass = fn (bool $active) => $active
|
||||||
|
? '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-200';
|
||||||
|
$periodLabel = ['today' => 'today', 'week' => 'this week', 'month' => 'this month'][$period] ?? null;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Leaderboard</h1>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">The most upvoted and the newest products from European makers.</p>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="mt-6 flex space-x-8 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<a href="{{ route('leaderboard', ['tab' => 'top', 'period' => $period]) }}"
|
||||||
|
class="inline-flex items-center px-1 pb-3 border-b-2 text-sm font-medium {{ $tabClass($tab === 'top') }}">Top</a>
|
||||||
|
<a href="{{ route('leaderboard', ['tab' => 'new']) }}"
|
||||||
|
class="inline-flex items-center px-1 pb-3 border-b-2 text-sm font-medium {{ $tabClass($tab === 'new') }}">New</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Period pills -->
|
||||||
|
@if($tab === 'top')
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
@foreach($periods as $key => $label)
|
||||||
|
<a href="{{ route('leaderboard', ['tab' => 'top', 'period' => $key]) }}"
|
||||||
|
class="inline-flex items-center px-3 py-1 rounded-full border text-sm font-medium {{ $pill($period === $key) }}">{{ $label }}</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- List -->
|
||||||
|
@if($products->isEmpty())
|
||||||
|
<div class="mt-8 text-center py-12 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
@if($tab === 'new')
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">No products yet.</p>
|
||||||
|
@else
|
||||||
|
<p class="text-gray-900 dark:text-white font-medium">No upvotes yet {{ $periodLabel ?? 'at all' }}</p>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Try a longer period.</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<ol class="mt-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
@foreach($products as $product)
|
||||||
|
<li class="flex items-center gap-4 p-4">
|
||||||
|
<span data-rank="{{ $products->firstItem() + $loop->index }}"
|
||||||
|
class="w-8 text-xl font-bold text-gray-400 dark:text-gray-500 text-center">{{ $products->firstItem() + $loop->index }}</span>
|
||||||
|
|
||||||
|
<a href="{{ route('products.show', $product) }}" class="flex-shrink-0">
|
||||||
|
@if($product->thumbnail_url)
|
||||||
|
<img src="{{ $product->thumbnail_url }}" alt="{{ $product->title }}" class="h-12 w-12 rounded-lg object-cover" loading="lazy">
|
||||||
|
@else
|
||||||
|
<div class="h-12 w-12 rounded-lg bg-gradient-to-br from-primary-100 to-primary-200 dark:from-primary-900 dark:to-primary-800 flex items-center justify-center text-primary-700 dark:text-primary-200 font-semibold">
|
||||||
|
{{ strtoupper(substr($product->title, 0, 1)) }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<a href="{{ route('products.show', $product) }}" class="text-base font-semibold text-gray-900 dark:text-white hover:text-primary-600 dark:hover:text-primary-400">{{ $product->title }}</a>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 truncate">{{ Str::limit($product->description, 100) }}</p>
|
||||||
|
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
@foreach($product->tags->take(3) as $tag)
|
||||||
|
<span>#{{ $tag->name }}</span>
|
||||||
|
@endforeach
|
||||||
|
<x-product.badges :product="$product" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-shrink-0 text-center">
|
||||||
|
@include('components.product.upvote-button', ['product' => $product, 'size' => 'md'])
|
||||||
|
@if($tab === 'top' && $periodLabel)
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ $product->period_upvotes_count }} {{ $periodLabel }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div class="mt-6">{{ $products->links() }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -7,6 +7,7 @@ use App\Http\Controllers\BookmarkController;
|
|||||||
use App\Http\Controllers\CategoryManagementController;
|
use App\Http\Controllers\CategoryManagementController;
|
||||||
use App\Http\Controllers\CommentController;
|
use App\Http\Controllers\CommentController;
|
||||||
use App\Http\Controllers\CommentModerationController;
|
use App\Http\Controllers\CommentModerationController;
|
||||||
|
use App\Http\Controllers\LeaderboardController;
|
||||||
use App\Http\Controllers\ProductController;
|
use App\Http\Controllers\ProductController;
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
use App\Http\Controllers\TagController;
|
use App\Http\Controllers\TagController;
|
||||||
@@ -34,6 +35,9 @@ Route::middleware(['auth'])->group(function () {
|
|||||||
|
|
||||||
Route::get('/profile/{user}', [ProfileController::class, 'show'])->name('profile.show');
|
Route::get('/profile/{user}', [ProfileController::class, 'show'])->name('profile.show');
|
||||||
|
|
||||||
|
// Leaderboard
|
||||||
|
Route::get('/leaderboard', [LeaderboardController::class, 'index'])->name('leaderboard');
|
||||||
|
|
||||||
// Product Routes
|
// Product Routes
|
||||||
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
|
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
|
||||||
Route::get('/products/{product}', [ProductController::class, 'show'])->name('products.show');
|
Route::get('/products/{product}', [ProductController::class, 'show'])->name('products.show');
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\Upvote;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Carbon::setTestNow(Carbon::parse('2026-10-17 15:00:00'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
Carbon::setTestNow();
|
||||||
|
});
|
||||||
|
|
||||||
|
function lbProduct(string $title, string $state = 'approved', array $attrs = []): Product
|
||||||
|
{
|
||||||
|
return Product::factory()->{$state}()->create(['title' => $title] + $attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lbUpvotes(Product $product, int $count, ?Carbon $at = null): void
|
||||||
|
{
|
||||||
|
Upvote::factory()->count($count)->create([
|
||||||
|
'product_id' => $product->id,
|
||||||
|
'created_at' => $at ?? now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('top today ranks by upvotes received today, not by product age', function () {
|
||||||
|
$old = lbProduct('Old Favourite', attrs: ['created_at' => now()->subYear()]);
|
||||||
|
$new = lbProduct('Fresh Launch');
|
||||||
|
lbUpvotes($old, 3);
|
||||||
|
lbUpvotes($new, 1);
|
||||||
|
|
||||||
|
$this->get(route('leaderboard', ['period' => 'today']))
|
||||||
|
->assertOk()
|
||||||
|
->assertSeeInOrder(['Old Favourite', 'Fresh Launch']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('today only counts upvotes since midnight', function () {
|
||||||
|
$a = lbProduct('Upvoted Yesterday');
|
||||||
|
$b = lbProduct('Upvoted Today');
|
||||||
|
lbUpvotes($a, 5, now()->startOfDay()->subMinute());
|
||||||
|
lbUpvotes($b, 1, now()->startOfDay());
|
||||||
|
|
||||||
|
$this->get(route('leaderboard', ['period' => 'today']))
|
||||||
|
->assertSee('Upvoted Today')
|
||||||
|
->assertDontSee('Upvoted Yesterday');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('week counts the last 7 days and month the last 30', function () {
|
||||||
|
$sixDays = lbProduct('Six Days Ago');
|
||||||
|
$eightDays = lbProduct('Eight Days Ago');
|
||||||
|
$twentyNine = lbProduct('Twenty Nine Days Ago');
|
||||||
|
$thirtyOne = lbProduct('Thirty One Days Ago');
|
||||||
|
lbUpvotes($sixDays, 1, now()->subDays(6));
|
||||||
|
lbUpvotes($eightDays, 1, now()->subDays(8));
|
||||||
|
lbUpvotes($twentyNine, 1, now()->subDays(29));
|
||||||
|
lbUpvotes($thirtyOne, 1, now()->subDays(31));
|
||||||
|
|
||||||
|
$this->get(route('leaderboard', ['period' => 'week']))
|
||||||
|
->assertSee('Six Days Ago')
|
||||||
|
->assertDontSee('Eight Days Ago')
|
||||||
|
->assertDontSee('Twenty Nine Days Ago');
|
||||||
|
|
||||||
|
$this->get(route('leaderboard', ['period' => 'month']))
|
||||||
|
->assertSee('Six Days Ago')
|
||||||
|
->assertSee('Eight Days Ago')
|
||||||
|
->assertSee('Twenty Nine Days Ago')
|
||||||
|
->assertDontSee('Thirty One Days Ago');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all time counts every upvote', function () {
|
||||||
|
$ancient = lbProduct('Ancient Product');
|
||||||
|
lbUpvotes($ancient, 1, now()->subYears(2));
|
||||||
|
|
||||||
|
$this->get(route('leaderboard', ['period' => 'all']))->assertSee('Ancient Product');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('only approved products with upvotes in the period are ranked', function () {
|
||||||
|
$ok = lbProduct('Ranked Product');
|
||||||
|
$pending = lbProduct('Pending Product', 'pending');
|
||||||
|
$rejected = lbProduct('Rejected Product', 'rejected');
|
||||||
|
lbProduct('Zero Upvotes');
|
||||||
|
lbUpvotes($ok, 1);
|
||||||
|
lbUpvotes($pending, 4);
|
||||||
|
lbUpvotes($rejected, 4);
|
||||||
|
|
||||||
|
$this->get(route('leaderboard'))
|
||||||
|
->assertSee('Ranked Product')
|
||||||
|
->assertDontSee('Pending Product')
|
||||||
|
->assertDontSee('Rejected Product')
|
||||||
|
->assertDontSee('Zero Upvotes');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ties are broken by total upvotes, then by newest', function () {
|
||||||
|
$moreTotal = lbProduct('More Total', attrs: ['created_at' => now()->subDays(10)]);
|
||||||
|
$fewerTotal = lbProduct('Fewer Total', attrs: ['created_at' => now()->subDays(5)]);
|
||||||
|
$newerTie = lbProduct('Newer Tie', attrs: ['created_at' => now()->subDay()]);
|
||||||
|
$olderTie = lbProduct('Older Tie', attrs: ['created_at' => now()->subDays(2)]);
|
||||||
|
lbUpvotes($moreTotal, 2);
|
||||||
|
lbUpvotes($moreTotal, 3, now()->subDays(40));
|
||||||
|
lbUpvotes($fewerTotal, 2);
|
||||||
|
lbUpvotes($newerTie, 1);
|
||||||
|
lbUpvotes($olderTie, 1);
|
||||||
|
|
||||||
|
$this->get(route('leaderboard', ['period' => 'today']))
|
||||||
|
->assertSeeInOrder(['More Total', 'Fewer Total', 'Newer Tie', 'Older Tie']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a removed upvote is not counted', function () {
|
||||||
|
$product = lbProduct('Changed Mind');
|
||||||
|
lbUpvotes($product, 2);
|
||||||
|
Upvote::where('product_id', $product->id)->first()->delete();
|
||||||
|
|
||||||
|
$this->get(route('leaderboard'))->assertSee('1 today', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('new tab lists approved products newest first', function () {
|
||||||
|
lbProduct('Oldest One', attrs: ['created_at' => now()->subDays(3)]);
|
||||||
|
lbProduct('Newest One', attrs: ['created_at' => now()->subHour()]);
|
||||||
|
lbProduct('Middle One', attrs: ['created_at' => now()->subDay()]);
|
||||||
|
lbProduct('Pending One', 'pending');
|
||||||
|
|
||||||
|
$this->get(route('leaderboard', ['tab' => 'new']))
|
||||||
|
->assertOk()
|
||||||
|
->assertSeeInOrder(['Newest One', 'Middle One', 'Oldest One'])
|
||||||
|
->assertDontSee('Pending One');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('lists 10 per page, ranks continue on page 2 and links keep the filters', function () {
|
||||||
|
foreach (range(1, 12) as $i) {
|
||||||
|
lbUpvotes(lbProduct("Product $i"), 20 - $i);
|
||||||
|
}
|
||||||
|
|
||||||
|
$page1 = $this->get(route('leaderboard', ['period' => 'week']));
|
||||||
|
$page1->assertSee('data-rank="10"', false)
|
||||||
|
->assertDontSee('data-rank="11"', false)
|
||||||
|
->assertSee('period=week', false)
|
||||||
|
->assertSee('page=2', false);
|
||||||
|
|
||||||
|
$this->get(route('leaderboard', ['period' => 'week', 'page' => 2]))
|
||||||
|
->assertSee('data-rank="11"', false)
|
||||||
|
->assertSee('data-rank="12"', false)
|
||||||
|
->assertSee('Product 12')
|
||||||
|
->assertDontSee('Product 1<', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid tab and period fall back to top today', function () {
|
||||||
|
$product = lbProduct('Fallback Product');
|
||||||
|
lbUpvotes($product, 1);
|
||||||
|
|
||||||
|
$this->get(route('leaderboard', ['tab' => 'nonsense', 'period' => 'decade']))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Fallback Product');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty period shows a message', function () {
|
||||||
|
$this->get(route('leaderboard', ['period' => 'today']))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('No upvotes yet today');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('navbar links to the leaderboard', function () {
|
||||||
|
$this->get(route('leaderboard'))->assertSee(route('leaderboard'), false);
|
||||||
|
$this->get('/products')->assertSee(route('leaderboard'), false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user