id(); // Proper nouns — "Laravel", "Rust" — so the name is NOT translatable. $table->string('name'); $table->string('slug')->unique(); // Only the prose about a technology is translated. $table->jsonb('description')->nullable(); $table->jsonb('radar_note')->nullable(); $table->string('category', 30)->default('tool'); // Tech radar placement. Null keeps a technology off the radar while // still letting projects and members reference it. $table->string('radar_ring', 20)->nullable(); $table->string('color', 7)->nullable(); $table->string('icon')->nullable(); $table->string('website')->nullable(); $table->unsignedInteger('position')->default(0); $table->timestamps(); $table->index('radar_ring'); }); Schema::create('members', function (Blueprint $table) { $table->id(); // Optional: most members never need panel access, so a member is // not a user. When they do sign in, this links the two. $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); $table->string('name'); $table->string('slug')->unique(); $table->jsonb('headline')->nullable(); $table->jsonb('bio')->nullable(); $table->string('avatar')->nullable(); $table->string('email')->nullable(); // Drives the commit-attribution side of the git integration. $table->string('github_username')->nullable()->index(); $table->jsonb('links')->nullable(); $table->boolean('is_public')->default(true); $table->boolean('is_founder')->default(false); $table->date('joined_at')->nullable(); $table->date('left_at')->nullable(); $table->unsignedInteger('position')->default(0); $table->timestamps(); $table->softDeletes(); }); Schema::create('projects', function (Blueprint $table) { $table->id(); $table->foreignId('lead_member_id')->nullable() ->constrained('members')->nullOnDelete(); $table->jsonb('title'); $table->jsonb('slug'); $table->jsonb('tagline')->nullable(); $table->jsonb('description')->nullable(); // The differentiator: what we decided and why, and what went wrong. // A real consultancy cannot publish these; a hobby collective can. $table->jsonb('decisions')->nullable(); $table->jsonb('postmortem')->nullable(); $table->jsonb('meta_title')->nullable(); $table->jsonb('meta_description')->nullable(); $table->string('status', 20)->default('idea'); // Who the work is for — an NGO, an association, or ourselves. $table->string('beneficiary')->nullable(); $table->string('beneficiary_url')->nullable(); $table->string('demo_url')->nullable(); $table->string('cover_image')->nullable(); $table->boolean('is_featured')->default(false); // Surfaces the project in the "we need help" listings even when it // has no specific open call yet. $table->boolean('needs_help')->default(false); $table->date('started_at')->nullable(); $table->date('ended_at')->nullable(); $table->timestamp('published_at')->nullable(); $table->unsignedInteger('position')->default(0); $table->timestamps(); $table->softDeletes(); $table->index(['status', 'published_at']); }); Schema::create('calls', function (Blueprint $table) { $table->id(); $table->foreignId('project_id')->nullable() ->constrained()->cascadeOnDelete(); $table->jsonb('title'); $table->jsonb('slug'); $table->jsonb('description')->nullable(); // The honest exchange for unpaid work: what a contributor gets out // of it. Stated openly rather than left implied. $table->jsonb('what_you_get')->nullable(); $table->string('status', 20)->default('open'); // Time is the scarce resource in a hobby collective, not money. // "~4h/week for 2 months" lets someone decide in ten seconds. $table->unsignedTinyInteger('hours_per_week')->nullable(); $table->unsignedSmallInteger('duration_weeks')->nullable(); $table->string('seniority', 20)->nullable(); $table->date('opens_at')->nullable(); $table->date('closes_at')->nullable(); $table->timestamps(); $table->index(['status', 'closes_at']); }); Schema::create('project_technology', function (Blueprint $table) { $table->foreignId('project_id')->constrained()->cascadeOnDelete(); $table->foreignId('technology_id')->constrained()->cascadeOnDelete(); $table->primary(['project_id', 'technology_id']); }); Schema::create('member_technology', function (Blueprint $table) { $table->foreignId('member_id')->constrained()->cascadeOnDelete(); $table->foreignId('technology_id')->constrained()->cascadeOnDelete(); $table->primary(['member_id', 'technology_id']); }); Schema::create('call_technology', function (Blueprint $table) { $table->foreignId('call_id')->constrained()->cascadeOnDelete(); $table->foreignId('technology_id')->constrained()->cascadeOnDelete(); $table->primary(['call_id', 'technology_id']); }); Schema::create('member_project', function (Blueprint $table) { $table->foreignId('member_id')->constrained()->cascadeOnDelete(); $table->foreignId('project_id')->constrained()->cascadeOnDelete(); $table->string('role')->nullable(); $table->primary(['member_id', 'project_id']); }); } public function down(): void { Schema::dropIfExists('member_project'); Schema::dropIfExists('call_technology'); Schema::dropIfExists('member_technology'); Schema::dropIfExists('project_technology'); Schema::dropIfExists('calls'); Schema::dropIfExists('projects'); Schema::dropIfExists('members'); Schema::dropIfExists('technologies'); } };