Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
88
app/Filament/Widgets/MostRead.php
Normal file
88
app/Filament/Widgets/MostRead.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use App\Models\ContentView;
|
||||
use App\Support\ContentTypes;
|
||||
use Filament\Widgets\StatsOverviewWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* What the site is actually being read for, this month.
|
||||
*
|
||||
* The counters this panel started with said how many projects existed. This
|
||||
* says which three anybody opened, which is the number that changes what
|
||||
* somebody does next: it is the difference between a portfolio nobody visits
|
||||
* and one page carrying the whole site.
|
||||
*
|
||||
* Absent entirely until there is something to show. A widget reading "0, 0, 0"
|
||||
* on an installation whose frontend does not report views would be teaching
|
||||
* everybody to ignore that corner of the dashboard.
|
||||
*/
|
||||
class MostRead extends StatsOverviewWidget
|
||||
{
|
||||
protected static ?int $sort = 2;
|
||||
|
||||
protected const DAYS = 30;
|
||||
|
||||
public static function canView(): bool
|
||||
{
|
||||
return (Auth::user()?->can('panel.access') ?? false)
|
||||
&& ContentView::query()->since(self::DAYS)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Stat>
|
||||
*/
|
||||
protected function getStats(): array
|
||||
{
|
||||
return collect($this->top())
|
||||
->map(fn (array $row) => Stat::make($row['title'], (string) $row['count'])
|
||||
->description(__('panel.views.in_days', ['days' => self::DAYS]))
|
||||
->descriptionIcon('heroicon-m-eye')
|
||||
->color('info'))
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* The three most-read records of any kind, resolved to their titles.
|
||||
*
|
||||
* Cached for a few minutes: this is drawn on the dashboard, which is the
|
||||
* first page everybody lands on, and the answer does not change minute to
|
||||
* minute — the underlying figures are daily totals.
|
||||
*
|
||||
* @return array<int, array{title: string, count: int}>
|
||||
*/
|
||||
protected function top(): array
|
||||
{
|
||||
return Cache::remember('views:most-read', now()->addMinutes(5), function (): array {
|
||||
$rows = ContentView::query()
|
||||
->since(self::DAYS)
|
||||
->selectRaw('viewable_type, viewable_id, SUM(count) as total')
|
||||
->groupBy('viewable_type', 'viewable_id')
|
||||
->orderByDesc('total')
|
||||
->limit(3)
|
||||
->get();
|
||||
|
||||
// Loaded per group rather than with the relation, because these
|
||||
// are three rows of different types and eager loading a morph for
|
||||
// three records buys nothing.
|
||||
return $rows
|
||||
->map(function (ContentView $row): ?array {
|
||||
$record = $row->viewable;
|
||||
|
||||
return $record instanceof Model
|
||||
? ['title' => ContentTypes::title($record), 'count' => (int) $row->total]
|
||||
: null;
|
||||
})
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user