Files
landing-api/database/migrations/2026_08_15_090000_create_showcase_tables.php
Josep Piguillem 3897113313 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>
2026-08-19 17:55:27 +02:00

170 lines
6.3 KiB
PHP

<?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');
}
};