Files
filament-short-url/src/Filament/Resources/ShortUrlResource/Pages/ViewShortUrlStats.php
Bartłomiej Janczak eda4794cac feat: v1.2.0 — password protection, warning pages, smart targeting, rate limiting, daily stats aggregation
- Add password-protected links with session-based unlock flow
- Add redirect warning (interstitial) pages before external URLs
- Add smart targeting rules: device-based, country/geo, A/B weighted rotation
- Add configurable per-IP rate limiting with 429 + Retry-After headers
- Add daily stats aggregation & log pruning (short-url:aggregate-and-prune)
- Add IncrementVisitJob as queue-based counter buffering fallback
- Add ShortUrlDailyStats model with JSON stat columns per day
- Add two new migrations: targeting/security fields, daily_stats table
- Add password-prompt.blade.php and warning.blade.php views
- Extend Settings GUI with Performance & Security tab (aggregation + rate limiting)
- Extend ShortUrlForm with Targeting & Security section
- Add POST route for password form submission (was GET-only → 405)
- Replace enum(device_type) with string(20) for cross-DB compatibility
- Remove ->after() MySQL-only hints from ALTER TABLE migrations
- Fix aggregation test: use whereDate() instead of assertDatabaseHas for date column
- Extend en/pl translations for all new features
- Expand README.md with full v1.2.0 documentation (476 lines)
2026-06-01 13:54:13 +02:00

134 lines
4.8 KiB
PHP

<?php
namespace Bjanczak\FilamentShortUrl\Filament\Resources\ShortUrlResource\Pages;
use Bjanczak\FilamentShortUrl\Filament\Resources\ShortUrlResource;
use Bjanczak\FilamentShortUrl\Models\ShortUrl;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Resources\Pages\Page;
use Filament\Schemas\Schema;
class ViewShortUrlStats extends Page implements HasForms
{
use InteractsWithForms;
protected static string $resource = ShortUrlResource::class;
protected string $view = 'filament-short-url::stats';
protected static ?string $title = 'Statistics';
public ShortUrl $record;
public int $totalVisits = 0;
public ?array $filterData = [];
public function mount(ShortUrl $record): void
{
$this->record = $record;
$this->form->fill([
'preset' => '30_days',
'date_from' => now()->subDays(29)->format('Y-m-d'),
'date_to' => now()->format('Y-m-d'),
]);
if (empty($this->filterData)) {
$this->filterData = [
'preset' => '30_days',
'date_from' => now()->subDays(29)->format('Y-m-d'),
'date_to' => now()->format('Y-m-d'),
];
}
$this->totalVisits = $record->getCachedStats($this->filterData['date_from'], $this->filterData['date_to'])['totalVisits'] ?? 0;
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Select::make('preset')
->hiddenLabel()
->options([
'24_hours' => __('filament-short-url::default.stats_preset_24_hours'),
'7_days' => __('filament-short-url::default.stats_preset_7_days'),
'30_days' => __('filament-short-url::default.stats_preset_30_days'),
'90_days' => __('filament-short-url::default.stats_preset_90_days'),
'custom' => __('filament-short-url::default.stats_preset_custom'),
])
->live()
->columnSpan(fn ($get) => $get('preset') === 'custom' ? 1 : 'full')
->afterStateUpdated(function ($state, $set) {
$to = now()->format('Y-m-d');
$from = match ($state) {
'24_hours' => now()->subDay()->format('Y-m-d'),
'7_days' => now()->subDays(6)->format('Y-m-d'),
'30_days' => now()->subDays(29)->format('Y-m-d'),
'90_days' => now()->subDays(89)->format('Y-m-d'),
'custom' => now()->subDays(29)->format('Y-m-d'),
default => null,
};
if ($from) {
$set('date_from', $from);
$set('date_to', $to);
}
}),
DatePicker::make('date_from')
->hiddenLabel()
->placeholder(__('filament-short-url::default.stats_filter_visited_from'))
->native(false)
->live()
->columnSpan(1)
->visible(fn ($get) => $get('preset') === 'custom')
->required(),
DatePicker::make('date_to')
->hiddenLabel()
->placeholder(__('filament-short-url::default.stats_filter_visited_until'))
->native(false)
->live()
->columnSpan(1)
->visible(fn ($get) => $get('preset') === 'custom')
->required(),
])
->columns(3)
->statePath('filterData');
}
protected function getHeaderWidgets(): array
{
return [];
}
protected function getHeaderActions(): array
{
return [
Action::make('back')
->label(__('filament-short-url::default.stats_btn_back'))
->icon('heroicon-o-arrow-left')
->color('gray')
->url(ShortUrlResource::getUrl()),
Action::make('copy_url')
->label(__('filament-short-url::default.stats_btn_copy'))
->icon('heroicon-o-clipboard')
->color('gray')
->extraAttributes([
'x-on:click' => 'navigator.clipboard.writeText("'.$this->record->getShortUrl().'")',
]),
];
}
public function getTitle(): string
{
return __('filament-short-url::default.stats_title').' — '.$this->record->url_key;
}
}