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:
2026-08-19 17:55:27 +02:00
commit 3897113313
534 changed files with 64050 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace App\Support;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* A warning, not a rule: something that already exists under almost the same
* name.
*
* The slug already has a hard uniqueness rule — two records cannot answer at
* the same address. This catches what that rule cannot: two records with
* different addresses and nearly the same title, which is what actually
* happens when nobody remembers a project already exists and starts a second
* one. A soft nudge on the way past is worth more here than a validation rule
* would be — sometimes the second one really is a different thing that
* happens to be called almost the same.
*/
final class DuplicateTitles
{
/**
* Below this, two titles are merely both about similar things — not the
* near-duplicate anybody would benefit from being told about.
*/
protected const THRESHOLD = 80.0;
/**
* Compute the match and warn about it in one call — the only way this is
* ever used, from the title field's `afterStateUpdated` on three forms.
*
* @param class-string<Model> $model
* @param class-string<resource> $resource
*/
public static function warnIfSimilar(
string $model,
string $resource,
?string $title,
string $locale,
int|string|null $excludeId,
): void {
$matches = self::similarTo($model, (string) $title, $locale, $excludeId);
if ($matches->isEmpty()) {
return;
}
Notification::make()
->warning()
->title(__('panel.duplicates.title'))
->body(__('panel.duplicates.body', [
'titles' => $matches
->map(fn (Model $match) => (string) $match->getTranslation('title', $locale, false))
->implode(' · '),
]))
->actions([
Action::make('open')
->label(__('panel.duplicates.open'))
->url($resource::getUrl('edit', ['record' => $matches->first()]))
->openUrlInNewTab(),
])
->send();
}
/**
* The closest few existing records whose title, in this language, reads
* almost the same as this one.
*
* Compared in PHP over every record of the type rather than in SQL:
* there is no "how alike are these two strings" operator that both
* SQLite and Postgres agree on, and at the scale a collective's content
* actually reaches — dozens to a few hundred records of one type — the
* whole table fits in memory without anyone noticing the cost. This runs
* once per blur of a title field, not in a loop or a background job.
*
* @param class-string<Model> $model
* @return Collection<int, Model>
*/
public static function similarTo(string $model, string $title, string $locale, int|string|null $excludeId): Collection
{
$title = trim($title);
if ($title === '') {
return collect();
}
return $model::query()
->when($excludeId !== null, fn ($query) => $query->whereKeyNot($excludeId))
->get(['id', 'title'])
->map(fn (Model $record) => [
$record,
self::similarity($title, (string) $record->getTranslation('title', $locale, false)),
])
->filter(fn (array $pair) => $pair[1] >= self::THRESHOLD)
->sortByDesc(fn (array $pair) => $pair[1])
->take(3)
->map(fn (array $pair) => $pair[0])
->values();
}
/**
* Percentage of characters in common, case-insensitively. Blunt on
* purpose — this only ever has to catch "basically the same words",
* not near-misses a real spell-checker would.
*/
protected static function similarity(string $a, string $b): float
{
if ($b === '') {
return 0.0;
}
similar_text(mb_strtolower($a), mb_strtolower($b), $percent);
return $percent;
}
}