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:
169
database/migrations/2026_08_15_090000_create_showcase_tables.php
Normal file
169
database/migrations/2026_08_15_090000_create_showcase_tables.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* The pieces a landing page is made of.
|
||||
*
|
||||
* Until now the site could say what the collective *built* (projects) and who
|
||||
* builds it (members), but not what it *offers*, who it was for, who vouches
|
||||
* for it, or where it has been seen. Those are four different questions and
|
||||
* they get four tables — deliberately not a page-builder: the frontend decides
|
||||
* what to compose and in what order, this only holds the pieces.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
/*
|
||||
* What the collective offers, as opposed to what it has already done.
|
||||
* A landing page is built on these; a portfolio without them is a list
|
||||
* of things with no explanation of what you would be hiring.
|
||||
*/
|
||||
Schema::create('services', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->jsonb('title');
|
||||
$table->jsonb('slug');
|
||||
$table->jsonb('tagline')->nullable();
|
||||
$table->jsonb('description')->nullable();
|
||||
|
||||
$table->jsonb('meta_title')->nullable();
|
||||
$table->jsonb('meta_description')->nullable();
|
||||
|
||||
$table->string('icon')->nullable();
|
||||
|
||||
$table->boolean('is_published')->default(false);
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->unsignedInteger('position')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['is_published', 'position']);
|
||||
});
|
||||
|
||||
/*
|
||||
* The organisations the work was for.
|
||||
*
|
||||
* This was two free-text columns on `projects`, which meant the same
|
||||
* association typed twice was two different entities, and there was
|
||||
* nowhere to keep a logo. A table gives the logo wall for free and
|
||||
* stops the duplicates.
|
||||
*/
|
||||
Schema::create('clients', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// A proper noun: an association is called what it is called in
|
||||
// every language, so the name is not translatable. Only the prose
|
||||
// about it is.
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->jsonb('sector')->nullable();
|
||||
|
||||
$table->string('logo')->nullable();
|
||||
$table->string('url')->nullable();
|
||||
|
||||
// Not everybody wants to appear on somebody else's website.
|
||||
$table->boolean('is_public')->default(true);
|
||||
$table->unsignedInteger('position')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
/*
|
||||
* What somebody else says about the work, which is worth more than
|
||||
* anything the collective says about itself.
|
||||
*/
|
||||
Schema::create('testimonials', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->jsonb('quote');
|
||||
|
||||
/*
|
||||
* The language it was actually said in. Translating a quote means
|
||||
* putting words in somebody's mouth, so the frontend can say
|
||||
* "translated from Spanish" rather than pretending they said it
|
||||
* that way.
|
||||
*/
|
||||
$table->string('original_locale', 5)->default('es');
|
||||
|
||||
$table->string('author_name');
|
||||
$table->jsonb('author_role')->nullable();
|
||||
$table->string('avatar')->nullable();
|
||||
|
||||
// What it vouches for. Attached to a client, a project or a
|
||||
// service rather than to "the home page", so the frontend asks for
|
||||
// the ones belonging to whatever it is rendering.
|
||||
$table->foreignId('client_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->foreignId('project_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->foreignId('service_id')->nullable()->constrained()->nullOnDelete();
|
||||
|
||||
$table->boolean('is_public')->default(true);
|
||||
$table->unsignedInteger('position')->default(0);
|
||||
$table->date('given_at')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
/*
|
||||
* Conference talks and the like: the cheapest content there is to keep
|
||||
* a site looking alive, and the only kind that says the collective
|
||||
* exists outside its own website.
|
||||
*/
|
||||
Schema::create('talks', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// A talk's title is the name of that talk, given in one language.
|
||||
// Translating it would invent a talk that was never given.
|
||||
$table->string('title');
|
||||
$table->string('language', 5)->default('es');
|
||||
|
||||
$table->jsonb('summary')->nullable();
|
||||
|
||||
$table->string('event');
|
||||
$table->string('city')->nullable();
|
||||
$table->date('held_at')->nullable();
|
||||
|
||||
$table->string('video_url')->nullable();
|
||||
$table->string('slides_url')->nullable();
|
||||
|
||||
$table->foreignId('member_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->foreignId('project_id')->nullable()->constrained()->nullOnDelete();
|
||||
|
||||
$table->boolean('is_public')->default(true);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['is_public', 'held_at']);
|
||||
});
|
||||
|
||||
Schema::create('project_service', function (Blueprint $table) {
|
||||
$table->foreignId('project_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('service_id')->constrained()->cascadeOnDelete();
|
||||
$table->primary(['project_id', 'service_id']);
|
||||
});
|
||||
|
||||
Schema::create('service_technology', function (Blueprint $table) {
|
||||
$table->foreignId('service_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('technology_id')->constrained()->cascadeOnDelete();
|
||||
$table->primary(['service_id', 'technology_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('service_technology');
|
||||
Schema::dropIfExists('project_service');
|
||||
Schema::dropIfExists('talks');
|
||||
Schema::dropIfExists('testimonials');
|
||||
Schema::dropIfExists('clients');
|
||||
Schema::dropIfExists('services');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user