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

144
app/Models/Member.php Normal file
View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Models\Concerns\BumpsContentVersion;
use App\Models\Concerns\HasContentViews;
use App\Models\Concerns\HasImages;
use App\Models\Concerns\HasRevisions;
use Database\Factories\MemberFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Translatable\HasTranslations;
/**
* Someone in the collective.
*
* A member is not a panel user: most people who appear on the site never need
* admin access. `user_id` links the two only when they do, so removing
* someone's admin rights never removes them from the website.
*/
#[Fillable([
'user_id', 'name', 'slug', 'headline', 'bio', 'avatar', 'email',
'github_username', 'links', 'is_public', 'is_founder',
'joined_at', 'left_at', 'position',
])]
class Member extends Model
{
/** @use HasFactory<MemberFactory> */
use BumpsContentVersion, HasContentViews, HasFactory, HasImages, HasRevisions, HasTranslations, SoftDeletes;
/** @var array<int, string> */
public array $translatable = ['headline', 'bio'];
protected function casts(): array
{
return [
'links' => 'array',
'is_public' => 'boolean',
'is_founder' => 'boolean',
'joined_at' => 'date',
'left_at' => 'date',
'position' => 'integer',
];
}
/**
* @return array<int, string>
*/
public function imageAttributes(): array
{
return ['avatar'];
}
/**
* @return BelongsTo<Media, $this>
*/
public function avatarMedia(): BelongsTo
{
return $this->imageRelation('avatar');
}
/**
* A portrait's description is the person's name, so there is no separate
* field for it: one that had to be typed would only ever be typed wrong.
*/
public function avatarAlt(): string
{
return $this->name;
}
/**
* @return BelongsTo<User, $this>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* @return BelongsToMany<Technology, $this>
*/
public function technologies(): BelongsToMany
{
return $this->belongsToMany(Technology::class);
}
/**
* @return BelongsToMany<Project, $this>
*/
public function projects(): BelongsToMany
{
return $this->belongsToMany(Project::class)->withPivot('role');
}
/**
* @return HasMany<Talk, $this>
*/
public function talks(): HasMany
{
return $this->hasMany(Talk::class);
}
/**
* @return HasMany<Project, $this>
*/
public function ledProjects(): HasMany
{
return $this->hasMany(Project::class, 'lead_member_id');
}
/**
* Someone who has not left the collective.
*/
public function isActive(): bool
{
return $this->left_at === null;
}
/**
* @param Builder<$this> $query
* @return Builder<$this>
*/
public function scopePublic(Builder $query): Builder
{
return $query->where('is_public', true);
}
/**
* @param Builder<$this> $query
* @return Builder<$this>
*/
public function scopeCurrent(Builder $query): Builder
{
return $query->whereNull('left_at');
}
}