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,46 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
/**
* What somebody wants when they write in.
*
* A collective like this receives two very different messages through the same
* form — "I have a few hours a week" and "my association needs a website" — and
* they go to different people. Keeping them apart costs one column and saves
* the inbox from being triaged by hand every time.
*/
enum ApplicationKind: string implements HasColor, HasLabel
{
/** Offering time: an answer to a call, or a general "count me in". */
case Contribute = 'contribute';
/** Asking for time: an NGO or association with a project in mind. */
case Request = 'request';
public function getLabel(): string
{
return __("enums.application_kind.{$this->value}");
}
public function getColor(): string
{
return match ($this) {
self::Contribute => 'success',
self::Request => 'info',
};
}
/** @return array<string, string> */
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn (self $case) => [$case->value => $case->getLabel()])
->all();
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
/**
* Where a submission is in the pipeline.
*
* `Spam` is a status rather than a deletion because the anti-spam checks are
* heuristics: a real person whose browser autofilled the honeypot lands here,
* and someone can rescue them. Nothing that arrives is ever thrown away
* silently.
*/
enum ApplicationStatus: string implements HasColor, HasLabel
{
case New = 'new';
case Contacted = 'contacted';
case Accepted = 'accepted';
case Declined = 'declined';
case Spam = 'spam';
public function getLabel(): string
{
return __("enums.application_status.{$this->value}");
}
public function getColor(): string
{
return match ($this) {
self::New => 'warning',
self::Contacted => 'info',
self::Accepted => 'success',
self::Declined => 'gray',
self::Spam => 'danger',
};
}
/**
* Whether reaching this status means somebody dealt with the submission.
*/
public function isHandled(): bool
{
return $this !== self::New;
}
/** @return array<string, string> */
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn (self $case) => [$case->value => $case->getLabel()])
->all();
}
}

37
app/Enums/CallStatus.php Normal file
View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
enum CallStatus: string implements HasColor, HasLabel
{
case Open = 'open';
case Filled = 'filled';
case Closed = 'closed';
public function getLabel(): string
{
return __("enums.call_status.{$this->value}");
}
public function getColor(): string
{
return match ($this) {
self::Open => 'success',
self::Filled => 'info',
self::Closed => 'gray',
};
}
/** @return array<string, string> */
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn (self $case) => [$case->value => $case->getLabel()])
->all();
}
}

60
app/Enums/GitProvider.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use App\Services\Git\GitManager;
use Filament\Support\Contracts\HasLabel;
enum GitProvider: string implements HasLabel
{
case Gitea = 'gitea';
case GitHub = 'github';
case GitLab = 'gitlab';
public function getLabel(): string
{
return match ($this) {
self::Gitea => 'Gitea',
self::GitHub => 'GitHub',
self::GitLab => 'GitLab',
};
}
public static function default(): self
{
return self::tryFrom((string) config('git.default')) ?? self::Gitea;
}
/**
* A provider is usable only when it has a driver registered *and* that
* driver has what it needs.
*
* The distinction matters for self-hosted providers: Gitea has a driver,
* but without GITEA_URL there is no instance to talk to, and offering it
* in the panel would only produce failures later.
*/
public function isSupported(): bool
{
$config = config("git.providers.{$this->value}");
if (! is_array($config) || ! isset($config['driver'])) {
return false;
}
return rescue(
fn () => app(GitManager::class)->driver($this)->isConfigured(),
rescue: false,
report: false,
);
}
/** @return array<string, string> */
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn (self $case) => [$case->value => $case->getLabel()])
->all();
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
/**
* What has been decided about an address that does not resolve.
*
* `Ignored` exists so the list stays readable. Most of what a public site is
* asked for is a bot looking for somebody else's admin panel, and a list where
* the real broken links are buried under that noise is a list nobody opens.
* Ignoring keeps counting — it hides a row, it does not stop the record.
*/
enum MissingUrlStatus: string implements HasColor, HasLabel
{
case Pending = 'pending';
case Resolved = 'resolved';
case Ignored = 'ignored';
public function getLabel(): string
{
return __("enums.missing_url_status.{$this->value}");
}
public function getColor(): string
{
return match ($this) {
self::Pending => 'warning',
self::Resolved => 'success',
self::Ignored => 'gray',
};
}
/** @return array<string, string> */
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn (self $case) => [$case->value => $case->getLabel()])
->all();
}
}

52
app/Enums/PageStatus.php Normal file
View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum PageStatus: string implements HasColor, HasIcon, HasLabel
{
case Draft = 'draft';
case Review = 'review';
case Published = 'published';
case Archived = 'archived';
public function getLabel(): string
{
return __("enums.page_status.{$this->value}");
}
public function getColor(): string
{
return match ($this) {
self::Draft => 'gray',
self::Review => 'warning',
self::Published => 'success',
self::Archived => 'danger',
};
}
public function getIcon(): string
{
return match ($this) {
self::Draft => 'heroicon-o-pencil-square',
self::Review => 'heroicon-o-eye',
self::Published => 'heroicon-o-check-circle',
self::Archived => 'heroicon-o-archive-box',
};
}
/**
* @return array<string, string>
*/
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn (self $case) => [$case->value => $case->getLabel()])
->all();
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
enum ProjectStatus: string implements HasColor, HasIcon, HasLabel
{
case Idea = 'idea';
case Active = 'active';
case Paused = 'paused';
case Completed = 'completed';
case Archived = 'archived';
public function getLabel(): string
{
return __("enums.project_status.{$this->value}");
}
public function getColor(): string
{
return match ($this) {
self::Idea => 'gray',
self::Active => 'success',
self::Paused => 'warning',
self::Completed => 'info',
self::Archived => 'danger',
};
}
public function getIcon(): string
{
return match ($this) {
self::Idea => 'heroicon-o-light-bulb',
self::Active => 'heroicon-o-bolt',
self::Paused => 'heroicon-o-pause-circle',
self::Completed => 'heroicon-o-check-badge',
self::Archived => 'heroicon-o-archive-box',
};
}
/** @return array<string, string> */
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn (self $case) => [$case->value => $case->getLabel()])
->all();
}
}

42
app/Enums/RadarRing.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasLabel;
/**
* Tech radar placement, following the ThoughtWorks rings.
*/
enum RadarRing: string implements HasColor, HasLabel
{
case Adopt = 'adopt';
case Trial = 'trial';
case Assess = 'assess';
case Hold = 'hold';
public function getLabel(): string
{
return __("enums.radar_ring.{$this->value}");
}
public function getColor(): string
{
return match ($this) {
self::Adopt => 'success',
self::Trial => 'info',
self::Assess => 'warning',
self::Hold => 'danger',
};
}
/** @return array<string, string> */
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn (self $case) => [$case->value => $case->getLabel()])
->all();
}
}

58
app/Enums/ReviewEvent.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
/**
* What happened in a review, as the record's own thread tells it.
*
* Deliberately not a status field. The state of a review is one question —
* waiting or not — and it is answered by `review_requested_at`; these are the
* events that got it there, kept so the conversation reads as a conversation
* rather than as a column that changed value.
*/
enum ReviewEvent: string implements HasColor, HasIcon, HasLabel
{
case Requested = 'requested';
case Returned = 'returned';
case Published = 'published';
case Commented = 'commented';
public function getLabel(): string
{
return __("enums.review_event.{$this->value}");
}
public function getColor(): string
{
return match ($this) {
self::Requested => 'warning',
self::Returned => 'danger',
self::Published => 'success',
self::Commented => 'gray',
};
}
public function getIcon(): string
{
return match ($this) {
self::Requested => 'heroicon-o-hand-raised',
self::Returned => 'heroicon-o-arrow-uturn-left',
self::Published => 'heroicon-o-check-circle',
self::Commented => 'heroicon-o-chat-bubble-left-ellipsis',
};
}
/** @return array<string, string> */
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn (self $case) => [$case->value => $case->getLabel()])
->all();
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use Filament\Support\Contracts\HasLabel;
enum TechnologyCategory: string implements HasLabel
{
case Language = 'language';
case Framework = 'framework';
case Datastore = 'datastore';
case Infrastructure = 'infrastructure';
case Tool = 'tool';
case Practice = 'practice';
public function getLabel(): string
{
return __("enums.technology_category.{$this->value}");
}
/** @return array<string, string> */
public static function options(): array
{
return collect(self::cases())
->mapWithKeys(fn (self $case) => [$case->value => $case->getLabel()])
->all();
}
}