Save workspace changes
This commit is contained in:
105
database/factories/WorldFactory.php
Normal file
105
database/factories/WorldFactory.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\World;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<World>
|
||||
*/
|
||||
class WorldFactory extends Factory
|
||||
{
|
||||
protected $model = World::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$title = Str::title($this->faker->unique()->words(3, true));
|
||||
$startsAt = Carbon::now()->subDays(5);
|
||||
$endsAt = Carbon::now()->addDays(10);
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'slug' => Str::slug($title),
|
||||
'tagline' => $this->faker->sentence(6),
|
||||
'summary' => $this->faker->sentence(12),
|
||||
'description' => $this->faker->paragraphs(2, true),
|
||||
'theme_key' => 'summer',
|
||||
'accent_color' => '#22c55e',
|
||||
'accent_color_secondary' => '#0f172a',
|
||||
'background_motif' => 'sunburst',
|
||||
'icon_name' => 'fa-solid fa-stars',
|
||||
'status' => World::STATUS_PUBLISHED,
|
||||
'type' => World::TYPE_SEASONAL,
|
||||
'starts_at' => $startsAt,
|
||||
'ends_at' => $endsAt,
|
||||
'is_featured' => false,
|
||||
'accepts_submissions' => true,
|
||||
'participation_mode' => World::PARTICIPATION_MODE_MANUAL_APPROVAL,
|
||||
'submission_starts_at' => $startsAt->copy()->subDay(),
|
||||
'submission_ends_at' => $endsAt->copy(),
|
||||
'submission_note_enabled' => true,
|
||||
'community_section_enabled' => true,
|
||||
'allow_readd_after_removal' => true,
|
||||
'is_recurring' => false,
|
||||
'recurrence_key' => null,
|
||||
'recurrence_rule' => null,
|
||||
'edition_year' => (int) $startsAt->year,
|
||||
'cta_label' => 'Explore world',
|
||||
'cta_url' => '/worlds',
|
||||
'badge_label' => null,
|
||||
'badge_description' => null,
|
||||
'badge_url' => null,
|
||||
'seo_title' => $title,
|
||||
'seo_description' => $this->faker->sentence(14),
|
||||
'og_image_path' => null,
|
||||
'related_tags_json' => [],
|
||||
'section_order_json' => array_values((array) config('worlds.default_section_order', [])),
|
||||
'parent_world_id' => null,
|
||||
'created_by_user_id' => User::factory(),
|
||||
'published_at' => Carbon::now()->subDays(10),
|
||||
];
|
||||
}
|
||||
|
||||
public function featured(): self
|
||||
{
|
||||
return $this->state(fn (): array => [
|
||||
'is_featured' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function current(): self
|
||||
{
|
||||
return $this->state(fn (): array => [
|
||||
'status' => World::STATUS_PUBLISHED,
|
||||
'starts_at' => Carbon::now()->subDays(7),
|
||||
'ends_at' => Carbon::now()->addDays(14),
|
||||
'published_at' => Carbon::now()->subDays(14),
|
||||
]);
|
||||
}
|
||||
|
||||
public function upcoming(): self
|
||||
{
|
||||
return $this->state(fn (): array => [
|
||||
'status' => World::STATUS_PUBLISHED,
|
||||
'starts_at' => Carbon::now()->addDays(21),
|
||||
'ends_at' => Carbon::now()->addDays(35),
|
||||
'published_at' => Carbon::now()->subDays(2),
|
||||
]);
|
||||
}
|
||||
|
||||
public function archived(): self
|
||||
{
|
||||
return $this->state(fn (): array => [
|
||||
'status' => World::STATUS_ARCHIVED,
|
||||
'starts_at' => Carbon::now()->subDays(45),
|
||||
'ends_at' => Carbon::now()->subDays(20),
|
||||
'published_at' => Carbon::now()->subDays(60),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('creator_ai_biographies', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id')->index();
|
||||
$table->text('text')->nullable();
|
||||
$table->string('source_hash', 64)->nullable()->index();
|
||||
$table->string('model', 120)->nullable();
|
||||
$table->string('status', 30)->default('generated'); // generated|approved|edited|hidden|failed
|
||||
$table->boolean('is_active')->default(false)->index();
|
||||
$table->boolean('is_hidden')->default(false);
|
||||
$table->boolean('is_user_edited')->default(false);
|
||||
$table->timestamp('generated_at')->nullable();
|
||||
$table->timestamp('approved_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('creator_ai_biographies');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* AI Biography v1.1 metadata columns.
|
||||
*
|
||||
* Adds:
|
||||
* prompt_version — which prompt template was used (e.g. "v1.1")
|
||||
* input_quality_tier — classified creator data richness: rich|medium|sparse
|
||||
* generation_reason — why generation was triggered (e.g. "manual_regenerate")
|
||||
* needs_review — flagged for admin inspection (poor output, low-signal, etc.)
|
||||
* last_attempted_at — timestamp of the most recent generation attempt
|
||||
* last_error_code — short machine-readable error code (e.g. "validation_failed")
|
||||
* last_error_reason — human-readable failure description from the last attempt
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('creator_ai_biographies', function (Blueprint $table): void {
|
||||
$table->string('prompt_version', 20)->nullable()->after('model');
|
||||
$table->string('input_quality_tier', 20)->nullable()->after('prompt_version');
|
||||
$table->string('generation_reason', 50)->nullable()->after('input_quality_tier');
|
||||
$table->boolean('needs_review')->default(false)->after('is_user_edited');
|
||||
$table->timestamp('last_attempted_at')->nullable()->after('approved_at');
|
||||
$table->string('last_error_code', 30)->nullable()->after('last_attempted_at');
|
||||
$table->text('last_error_reason')->nullable()->after('last_error_code');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('creator_ai_biographies', function (Blueprint $table): void {
|
||||
$table->dropColumn([
|
||||
'prompt_version',
|
||||
'input_quality_tier',
|
||||
'generation_reason',
|
||||
'needs_review',
|
||||
'last_attempted_at',
|
||||
'last_error_code',
|
||||
'last_error_reason',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('artwork_versions', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('artwork_versions', 'snapshot_json')) {
|
||||
$table->json('snapshot_json')->nullable()->after('change_note');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artwork_versions', function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('artwork_versions', 'snapshot_json')) {
|
||||
$table->dropColumn('snapshot_json');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('worlds', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('title', 180);
|
||||
$table->string('slug', 180)->unique();
|
||||
$table->string('tagline', 220)->nullable();
|
||||
$table->string('summary', 320)->nullable();
|
||||
$table->longText('description')->nullable();
|
||||
$table->string('cover_path', 2048)->nullable();
|
||||
$table->string('theme_key', 80)->nullable();
|
||||
$table->string('accent_color', 16)->nullable();
|
||||
$table->string('accent_color_secondary', 16)->nullable();
|
||||
$table->string('background_motif', 80)->nullable();
|
||||
$table->string('icon_name', 120)->nullable();
|
||||
$table->string('status', 24)->default('draft');
|
||||
$table->string('type', 32)->default('seasonal');
|
||||
$table->timestamp('starts_at')->nullable();
|
||||
$table->timestamp('ends_at')->nullable();
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->boolean('is_recurring')->default(false);
|
||||
$table->string('recurrence_key', 120)->nullable();
|
||||
$table->string('recurrence_rule', 160)->nullable();
|
||||
$table->unsignedSmallInteger('edition_year')->nullable();
|
||||
$table->string('cta_label', 120)->nullable();
|
||||
$table->string('cta_url', 2048)->nullable();
|
||||
$table->string('badge_label', 120)->nullable();
|
||||
$table->text('badge_description')->nullable();
|
||||
$table->string('badge_url', 2048)->nullable();
|
||||
$table->string('seo_title', 255)->nullable();
|
||||
$table->string('seo_description', 300)->nullable();
|
||||
$table->string('og_image_path', 2048)->nullable();
|
||||
$table->json('related_tags_json')->nullable();
|
||||
$table->json('section_order_json')->nullable();
|
||||
$table->foreignId('parent_world_id')->nullable()->constrained('worlds')->nullOnDelete();
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['status', 'is_featured'], 'worlds_status_featured_idx');
|
||||
$table->index(['starts_at', 'ends_at'], 'worlds_window_idx');
|
||||
$table->index(['recurrence_key', 'edition_year'], 'worlds_recurrence_idx');
|
||||
$table->index(['theme_key', 'status'], 'worlds_theme_status_idx');
|
||||
});
|
||||
|
||||
Schema::create('world_relations', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('world_id')->constrained('worlds')->cascadeOnDelete();
|
||||
$table->string('related_type', 32);
|
||||
$table->unsignedBigInteger('related_id');
|
||||
$table->string('section_key', 48);
|
||||
$table->string('context_label', 120)->nullable();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['world_id', 'section_key', 'related_type', 'related_id'], 'world_relations_unique');
|
||||
$table->index(['world_id', 'section_key', 'sort_order'], 'world_relations_section_sort_idx');
|
||||
$table->index(['related_type', 'related_id'], 'world_relations_entity_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('world_relations');
|
||||
Schema::dropIfExists('worlds');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->json('section_visibility_json')->nullable()->after('section_order_json');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->dropColumn('section_visibility_json');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->boolean('accepts_submissions')->default(false)->after('is_featured');
|
||||
$table->timestamp('submission_starts_at')->nullable()->after('ends_at');
|
||||
$table->timestamp('submission_ends_at')->nullable()->after('submission_starts_at');
|
||||
$table->boolean('submission_note_enabled')->default(true)->after('accepts_submissions');
|
||||
$table->boolean('community_section_enabled')->default(true)->after('submission_note_enabled');
|
||||
$table->text('submission_guidelines')->nullable()->after('badge_description');
|
||||
|
||||
$table->index(['accepts_submissions', 'submission_starts_at', 'submission_ends_at'], 'worlds_submission_window_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->dropIndex('worlds_submission_window_idx');
|
||||
$table->dropColumn([
|
||||
'accepts_submissions',
|
||||
'submission_starts_at',
|
||||
'submission_ends_at',
|
||||
'submission_note_enabled',
|
||||
'community_section_enabled',
|
||||
'submission_guidelines',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('world_submissions', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('world_id')->constrained('worlds')->cascadeOnDelete();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->foreignId('submitted_by_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('status', 24)->default('pending');
|
||||
$table->text('note')->nullable();
|
||||
$table->text('reviewer_note')->nullable();
|
||||
$table->foreignId('reviewed_by_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('reviewed_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['world_id', 'artwork_id'], 'world_submissions_world_artwork_unique');
|
||||
$table->index(['world_id', 'status', 'reviewed_at'], 'world_submissions_world_status_idx');
|
||||
$table->index(['artwork_id', 'status'], 'world_submissions_artwork_status_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('world_submissions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->string('participation_mode', 32)->default('closed')->after('accepts_submissions');
|
||||
$table->boolean('allow_readd_after_removal')->default(true)->after('community_section_enabled');
|
||||
|
||||
$table->index(['participation_mode', 'submission_starts_at', 'submission_ends_at'], 'worlds_participation_mode_window_idx');
|
||||
});
|
||||
|
||||
DB::table('worlds')->orderBy('id')->chunkById(250, function ($rows): void {
|
||||
foreach ($rows as $row) {
|
||||
DB::table('worlds')
|
||||
->where('id', (int) $row->id)
|
||||
->update([
|
||||
'participation_mode' => (bool) $row->accepts_submissions ? 'manual_approval' : 'closed',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('world_submissions', function (Blueprint $table): void {
|
||||
$table->boolean('is_featured')->default(false)->after('status');
|
||||
$table->string('mode_snapshot', 32)->nullable()->after('is_featured');
|
||||
$table->text('moderation_reason')->nullable()->after('reviewer_note');
|
||||
$table->timestamp('removed_at')->nullable()->after('reviewed_at');
|
||||
$table->timestamp('blocked_at')->nullable()->after('removed_at');
|
||||
$table->timestamp('featured_at')->nullable()->after('blocked_at');
|
||||
|
||||
$table->index(['world_id', 'status', 'is_featured'], 'world_submissions_world_status_featured_idx');
|
||||
});
|
||||
|
||||
DB::table('world_submissions')->orderBy('id')->chunkById(250, function ($rows): void {
|
||||
foreach ($rows as $row) {
|
||||
$status = (string) $row->status;
|
||||
$mappedStatus = match ($status) {
|
||||
'approved', 'featured' => 'live',
|
||||
'rejected' => 'removed',
|
||||
default => 'pending',
|
||||
};
|
||||
|
||||
$isFeatured = $status === 'featured';
|
||||
|
||||
DB::table('world_submissions')
|
||||
->where('id', (int) $row->id)
|
||||
->update([
|
||||
'status' => $mappedStatus,
|
||||
'is_featured' => $isFeatured,
|
||||
'moderation_reason' => $row->reviewer_note,
|
||||
'removed_at' => $mappedStatus === 'removed' ? ($row->reviewed_at ?? now()) : null,
|
||||
'featured_at' => $isFeatured ? ($row->reviewed_at ?? now()) : null,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('world_submissions')->orderBy('id')->chunkById(250, function ($rows): void {
|
||||
foreach ($rows as $row) {
|
||||
$legacyStatus = match ((string) $row->status) {
|
||||
'live' => (bool) $row->is_featured ? 'featured' : 'approved',
|
||||
'removed', 'blocked' => 'rejected',
|
||||
default => 'pending',
|
||||
};
|
||||
|
||||
DB::table('world_submissions')
|
||||
->where('id', (int) $row->id)
|
||||
->update([
|
||||
'status' => $legacyStatus,
|
||||
'reviewer_note' => $row->moderation_reason,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('world_submissions', function (Blueprint $table): void {
|
||||
$table->dropIndex('world_submissions_world_status_featured_idx');
|
||||
$table->dropColumn(['is_featured', 'mode_snapshot', 'moderation_reason', 'removed_at', 'blocked_at', 'featured_at']);
|
||||
});
|
||||
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->dropIndex('worlds_participation_mode_window_idx');
|
||||
$table->dropColumn(['participation_mode', 'allow_readd_after_removal']);
|
||||
});
|
||||
}
|
||||
};
|
||||
2134
database/projekti_old_skinbase_structure.sql
Normal file
2134
database/projekti_old_skinbase_structure.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ use Database\Seeders\NovaCardCategorySeeder;
|
||||
use Database\Seeders\NovaCardDemoSeeder;
|
||||
use Database\Seeders\NovaCardTemplateSeeder;
|
||||
use Database\Seeders\NewsLaunchSeeder;
|
||||
use Database\Seeders\WorldLaunchSeeder;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
@@ -26,6 +27,7 @@ class DatabaseSeeder extends Seeder
|
||||
NovaCardTemplateSeeder::class,
|
||||
NovaCardDemoSeeder::class,
|
||||
NewsLaunchSeeder::class,
|
||||
WorldLaunchSeeder::class,
|
||||
]);
|
||||
|
||||
User::factory()->create([
|
||||
|
||||
437
database/seeders/WorldLaunchSeeder.php
Normal file
437
database/seeders/WorldLaunchSeeder.php
Normal file
@@ -0,0 +1,437 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\Collection;
|
||||
use App\Models\Group;
|
||||
use App\Models\NovaCard;
|
||||
use App\Models\User;
|
||||
use App\Models\World;
|
||||
use App\Models\WorldRelation;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use cPad\Plugins\News\Models\NewsArticle;
|
||||
|
||||
final class WorldLaunchSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
Model::withoutEvents(function (): void {
|
||||
$editor = User::query()->firstWhere('email', 'newsroom@skinbase.local')
|
||||
?? User::query()->updateOrCreate(
|
||||
['email' => 'newsroom@skinbase.local'],
|
||||
[
|
||||
'name' => 'Skinbase Editorial',
|
||||
'username' => 'skinbaseeditorial',
|
||||
'role' => 'moderator',
|
||||
'password' => bcrypt('password'),
|
||||
'email_verified_at' => now(),
|
||||
'onboarding_step' => 'complete',
|
||||
'username_changed_at' => now()->subDays(180),
|
||||
]
|
||||
);
|
||||
|
||||
$retroCreator = $this->upsertCreator('retrogrid', 'Retro Grid', 'retrogrid@skinbase.local');
|
||||
$pixelCreator = $this->upsertCreator('pixelpilot', 'Pixel Pilot', 'pixelpilot@skinbase.local');
|
||||
$sceneCreator = $this->upsertCreator('scenelab', 'Scene Lab', 'scenelab@skinbase.local');
|
||||
|
||||
$artworks = [
|
||||
$this->upsertArtwork($retroCreator, 'Retro Grid Dreams', 'retro-grid-dreams', 'Chrome gradients, CRT glow, and 90s interface nostalgia.'),
|
||||
$this->upsertArtwork($pixelCreator, 'Pixel Transit', 'pixel-transit', 'A city-night loop built around tiny lights and sharp palette control.'),
|
||||
$this->upsertArtwork($sceneCreator, 'Signal Bloom', 'signal-bloom', 'Abstract demoscene energy translated into layered light fields.'),
|
||||
];
|
||||
|
||||
$collections = [
|
||||
$this->upsertCollection($retroCreator, 'Retro Month Essentials', 'retro-month-essentials', 'Editorial picks for glossy nostalgia, synth palettes, and interface-era mood.'),
|
||||
$this->upsertCollection($pixelCreator, 'Pixel Week Highlights', 'pixel-week-highlights', 'A compact collection of crisp sprite work, handheld vibes, and tiny-scale craft.'),
|
||||
];
|
||||
|
||||
$groups = [
|
||||
$this->upsertGroup($sceneCreator, 'Signal Foundry', 'signal-foundry', 'A collaborative demoscene-inspired group for experiments, releases, and themed drops.'),
|
||||
$this->upsertGroup($retroCreator, 'Afterglow Archive', 'afterglow-archive', 'Creators building glossy retro-futurist interfaces, wallpapers, and cover art.'),
|
||||
];
|
||||
|
||||
$newsArticles = $this->seedableNewsArticles();
|
||||
$cards = $this->seedableCards();
|
||||
|
||||
$now = now();
|
||||
$currentYear = (int) $now->year;
|
||||
|
||||
$retroMonth = $this->upsertWorld('retro-month-' . $currentYear, [
|
||||
'title' => 'Retro Month ' . $currentYear,
|
||||
'tagline' => 'Chrome, scanlines, glossy interfaces, and warm-digital nostalgia.',
|
||||
'summary' => 'A featured editorial world that packages retro-inspired artworks, collections, creators, groups, news, and Nova cards into one recurring seasonal destination.',
|
||||
'description' => "Retro Month curates the surface language of nostalgia into a single destination. It highlights polished throwback artwork, creator identity, collaborative groups, and the editorial context that makes seasonal programming feel intentional instead of accidental.",
|
||||
'theme_key' => 'retro-month',
|
||||
'accent_color' => '#f97316',
|
||||
'accent_color_secondary' => '#0f172a',
|
||||
'background_motif' => 'grid-horizon',
|
||||
'icon_name' => 'fa-solid fa-compact-disc',
|
||||
'status' => World::STATUS_PUBLISHED,
|
||||
'type' => World::TYPE_CAMPAIGN,
|
||||
'starts_at' => $now->copy()->subDays(10)->startOfDay(),
|
||||
'ends_at' => $now->copy()->addDays(18)->endOfDay(),
|
||||
'is_featured' => true,
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'retro-month',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->month,
|
||||
'edition_year' => $currentYear,
|
||||
'cta_label' => 'Enter Retro Month',
|
||||
'cta_url' => '/worlds/retro-month-' . $currentYear,
|
||||
'badge_label' => 'Featured now',
|
||||
'badge_description' => 'The active spotlight world on the public homepage and Worlds index.',
|
||||
'badge_url' => '/worlds',
|
||||
'seo_title' => 'Retro Month ' . $currentYear . ' on Skinbase',
|
||||
'seo_description' => 'Retro Month bundles curated artwork, collections, groups, creators, news, and cards into a single recurring editorial world.',
|
||||
'related_tags_json' => ['retro', 'synthwave', 'interface', 'nostalgia'],
|
||||
'section_order_json' => ['featured_artworks', 'featured_collections', 'featured_creators', 'featured_groups', 'news', 'cards'],
|
||||
'created_by_user_id' => $editor->id,
|
||||
'published_at' => $now->copy()->subDays(14),
|
||||
]);
|
||||
|
||||
$retroArchive = $this->upsertWorld('retro-month-' . ($currentYear - 1), [
|
||||
'title' => 'Retro Month ' . ($currentYear - 1),
|
||||
'tagline' => 'The previous edition of the recurring retro programming world.',
|
||||
'summary' => 'Archived edition kept online for continuity and discoverability.',
|
||||
'description' => 'The previous Retro Month edition remains visible so the recurrence feels like a true archive instead of a disposable campaign.',
|
||||
'theme_key' => 'retro-month',
|
||||
'accent_color' => '#fb7185',
|
||||
'accent_color_secondary' => '#312e81',
|
||||
'background_motif' => 'sunset-grid',
|
||||
'icon_name' => 'fa-solid fa-compact-disc',
|
||||
'status' => World::STATUS_ARCHIVED,
|
||||
'type' => World::TYPE_CAMPAIGN,
|
||||
'starts_at' => $now->copy()->subYear()->subDays(10)->startOfDay(),
|
||||
'ends_at' => $now->copy()->subYear()->addDays(18)->endOfDay(),
|
||||
'is_featured' => false,
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'retro-month',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->month,
|
||||
'edition_year' => $currentYear - 1,
|
||||
'cta_label' => 'Browse archive',
|
||||
'cta_url' => '/worlds/retro-month-' . ($currentYear - 1),
|
||||
'badge_label' => 'Archive edition',
|
||||
'badge_description' => 'Previous recurring edition kept available for context and archival browsing.',
|
||||
'badge_url' => '/worlds',
|
||||
'seo_title' => 'Retro Month ' . ($currentYear - 1) . ' archive',
|
||||
'seo_description' => 'The archived Retro Month edition remains public as part of the recurring Worlds archive.',
|
||||
'related_tags_json' => ['retro', 'archive'],
|
||||
'section_order_json' => ['featured_artworks', 'featured_creators', 'news'],
|
||||
'parent_world_id' => $retroMonth->id,
|
||||
'created_by_user_id' => $editor->id,
|
||||
'published_at' => $now->copy()->subYear()->subDays(18),
|
||||
]);
|
||||
|
||||
$pixelWeek = $this->upsertWorld('pixel-week-' . $currentYear, [
|
||||
'title' => 'Pixel Week ' . $currentYear,
|
||||
'tagline' => 'Small-scale craft, tight palettes, and highly legible form.',
|
||||
'summary' => 'An upcoming themed week focused on pixel art, sprites, handheld aesthetics, and compact visual systems.',
|
||||
'description' => 'Pixel Week is scheduled as the next clear editorial world, giving the public navigation a real forward-looking destination instead of a dead module stub.',
|
||||
'theme_key' => 'pixel-week',
|
||||
'accent_color' => '#38bdf8',
|
||||
'accent_color_secondary' => '#0f172a',
|
||||
'background_motif' => 'pixel-burst',
|
||||
'icon_name' => 'fa-solid fa-gamepad',
|
||||
'status' => World::STATUS_PUBLISHED,
|
||||
'type' => World::TYPE_EVENT,
|
||||
'starts_at' => $now->copy()->addDays(24)->startOfDay(),
|
||||
'ends_at' => $now->copy()->addDays(31)->endOfDay(),
|
||||
'is_featured' => false,
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'pixel-week',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->copy()->addDays(24)->month,
|
||||
'edition_year' => (int) $now->copy()->addDays(24)->year,
|
||||
'cta_label' => 'Preview Pixel Week',
|
||||
'cta_url' => '/worlds/pixel-week-' . $currentYear,
|
||||
'badge_label' => 'Upcoming',
|
||||
'badge_description' => 'A seeded upcoming world so the public index is populated beyond a single spotlight.',
|
||||
'badge_url' => '/worlds',
|
||||
'seo_title' => 'Pixel Week ' . $currentYear,
|
||||
'seo_description' => 'Upcoming Pixel Week world for pixel art, creator spotlights, groups, and themed collections.',
|
||||
'related_tags_json' => ['pixel-art', 'sprites', 'handheld'],
|
||||
'section_order_json' => ['featured_artworks', 'featured_collections', 'featured_creators', 'news'],
|
||||
'created_by_user_id' => $editor->id,
|
||||
'published_at' => $now->copy()->subDays(7),
|
||||
]);
|
||||
|
||||
$summerWorld = $this->upsertWorld('summer-world-' . $currentYear, [
|
||||
'title' => 'Summer World ' . $currentYear,
|
||||
'tagline' => 'Bright color, open-air mood, and high-energy seasonal programming.',
|
||||
'summary' => 'A seeded summer world for warm palettes, standout drops, and lightweight seasonal storytelling.',
|
||||
'description' => 'Summer World exists to make the Worlds feature feel like a living editorial calendar instead of a one-off campaign tool.',
|
||||
'theme_key' => 'summer',
|
||||
'accent_color' => '#f59e0b',
|
||||
'accent_color_secondary' => '#0f766e',
|
||||
'background_motif' => 'heat-haze',
|
||||
'icon_name' => 'fa-solid fa-sun',
|
||||
'status' => World::STATUS_PUBLISHED,
|
||||
'type' => World::TYPE_SEASONAL,
|
||||
'starts_at' => $now->copy()->addDays(60)->startOfDay(),
|
||||
'ends_at' => $now->copy()->addDays(120)->endOfDay(),
|
||||
'is_featured' => false,
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'summer-world',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->copy()->addDays(60)->month,
|
||||
'edition_year' => (int) $now->copy()->addDays(60)->year,
|
||||
'cta_label' => 'See summer lineup',
|
||||
'cta_url' => '/worlds/summer-world-' . $currentYear,
|
||||
'badge_label' => 'Seasonal program',
|
||||
'badge_description' => 'Summer launch world seeded for editorial planning and public browsing.',
|
||||
'badge_url' => '/worlds',
|
||||
'seo_title' => 'Summer World ' . $currentYear,
|
||||
'seo_description' => 'Summer World curates warm-season artwork, collections, groups, and related editorial stories.',
|
||||
'related_tags_json' => ['summer', 'bright', 'seasonal'],
|
||||
'section_order_json' => ['featured_artworks', 'featured_collections', 'featured_groups', 'news'],
|
||||
'created_by_user_id' => $editor->id,
|
||||
'published_at' => $now->copy()->subDays(7),
|
||||
]);
|
||||
|
||||
$halloweenWorld = $this->upsertWorld('halloween-world-' . $currentYear, [
|
||||
'title' => 'Halloween World ' . $currentYear,
|
||||
'tagline' => 'Dark color, eerie polish, and a platform-wide seasonal event shell.',
|
||||
'summary' => 'An annual Halloween world with room for creepy artwork, challenge hooks, groups, and seasonal editorial coverage.',
|
||||
'description' => 'Halloween World gives the Worlds system a recognizable recurring flagship outside the currently active spotlight.',
|
||||
'theme_key' => 'halloween',
|
||||
'accent_color' => '#f97316',
|
||||
'accent_color_secondary' => '#3f3cbb',
|
||||
'background_motif' => 'moon-fog',
|
||||
'icon_name' => 'fa-solid fa-ghost',
|
||||
'status' => World::STATUS_PUBLISHED,
|
||||
'type' => World::TYPE_SEASONAL,
|
||||
'starts_at' => $now->copy()->addDays(170)->startOfDay(),
|
||||
'ends_at' => $now->copy()->addDays(205)->endOfDay(),
|
||||
'is_featured' => false,
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'halloween-world',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->copy()->addDays(170)->month,
|
||||
'edition_year' => (int) $now->copy()->addDays(170)->year,
|
||||
'cta_label' => 'Plan Halloween world',
|
||||
'cta_url' => '/worlds/halloween-world-' . $currentYear,
|
||||
'badge_label' => 'Recurring seasonal',
|
||||
'badge_description' => 'Seeded so editors have a real recurring Halloween world from day one.',
|
||||
'badge_url' => '/worlds',
|
||||
'seo_title' => 'Halloween World ' . $currentYear,
|
||||
'seo_description' => 'Recurring Halloween World for seasonal artwork, groups, and themed editorial programming.',
|
||||
'related_tags_json' => ['halloween', 'dark', 'seasonal'],
|
||||
'section_order_json' => ['featured_artworks', 'featured_groups', 'news'],
|
||||
'created_by_user_id' => $editor->id,
|
||||
'published_at' => $now->copy()->subDays(7),
|
||||
]);
|
||||
|
||||
$christmasWorld = $this->upsertWorld('christmas-world-' . $currentYear, [
|
||||
'title' => 'Christmas World ' . $currentYear,
|
||||
'tagline' => 'Snowlight, soft-glow palettes, and year-end editorial programming.',
|
||||
'summary' => 'A seeded Christmas world that rounds out the launch calendar with a second major recurring seasonal destination.',
|
||||
'description' => 'Christmas World completes the initial recurring lineup so the Worlds system launches with a believable editorial calendar.',
|
||||
'theme_key' => 'christmas',
|
||||
'accent_color' => '#22c55e',
|
||||
'accent_color_secondary' => '#b91c1c',
|
||||
'background_motif' => 'snowfall',
|
||||
'icon_name' => 'fa-solid fa-tree',
|
||||
'status' => World::STATUS_PUBLISHED,
|
||||
'type' => World::TYPE_SEASONAL,
|
||||
'starts_at' => $now->copy()->addDays(235)->startOfDay(),
|
||||
'ends_at' => $now->copy()->addDays(275)->endOfDay(),
|
||||
'is_featured' => false,
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'christmas-world',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->copy()->addDays(235)->month,
|
||||
'edition_year' => (int) $now->copy()->addDays(235)->year,
|
||||
'cta_label' => 'Preview Christmas world',
|
||||
'cta_url' => '/worlds/christmas-world-' . $currentYear,
|
||||
'badge_label' => 'Holiday program',
|
||||
'badge_description' => 'Seeded holiday world for recurring year-end curation and promotion.',
|
||||
'badge_url' => '/worlds',
|
||||
'seo_title' => 'Christmas World ' . $currentYear,
|
||||
'seo_description' => 'Christmas World packages holiday artwork, collections, creators, groups, and editorial context.',
|
||||
'related_tags_json' => ['christmas', 'holiday', 'winter'],
|
||||
'section_order_json' => ['featured_artworks', 'featured_collections', 'featured_creators', 'news'],
|
||||
'created_by_user_id' => $editor->id,
|
||||
'published_at' => $now->copy()->subDays(7),
|
||||
]);
|
||||
|
||||
$this->syncRelations($retroMonth, [
|
||||
$this->relation('featured_artworks', WorldRelation::TYPE_ARTWORK, $artworks[0]?->id, 'Signature piece', true, 0),
|
||||
$this->relation('featured_artworks', WorldRelation::TYPE_ARTWORK, $artworks[2]?->id, 'Editorial pick', false, 1),
|
||||
$this->relation('featured_collections', WorldRelation::TYPE_COLLECTION, $collections[0]?->id, 'Start here', true, 0),
|
||||
$this->relation('featured_creators', WorldRelation::TYPE_USER, $retroCreator->id, 'Resident creator', true, 0),
|
||||
$this->relation('featured_creators', WorldRelation::TYPE_USER, $sceneCreator->id, 'Guest spotlight', false, 1),
|
||||
$this->relation('featured_groups', WorldRelation::TYPE_GROUP, $groups[0]?->id, 'Group spotlight', true, 0),
|
||||
$this->relation('featured_groups', WorldRelation::TYPE_GROUP, $groups[1]?->id, 'Archive-minded collective', false, 1),
|
||||
$this->relation('news', WorldRelation::TYPE_NEWS, ($newsArticles[0] ?? null)?->id, 'Editorial context', true, 0),
|
||||
$this->relation('news', WorldRelation::TYPE_NEWS, ($newsArticles[1] ?? null)?->id, 'Related update', false, 1),
|
||||
$this->relation('cards', WorldRelation::TYPE_CARD, ($cards[0] ?? null)?->id, 'Theme card', true, 0),
|
||||
]);
|
||||
|
||||
$this->syncRelations($retroArchive, [
|
||||
$this->relation('featured_artworks', WorldRelation::TYPE_ARTWORK, $artworks[0]?->id, 'Archive favorite', true, 0),
|
||||
$this->relation('featured_creators', WorldRelation::TYPE_USER, $retroCreator->id, 'Returning creator', true, 0),
|
||||
$this->relation('news', WorldRelation::TYPE_NEWS, ($newsArticles[0] ?? null)?->id, 'Launch notes', true, 0),
|
||||
]);
|
||||
|
||||
$this->syncRelations($pixelWeek, [
|
||||
$this->relation('featured_artworks', WorldRelation::TYPE_ARTWORK, $artworks[1]?->id, 'Pixel craft', true, 0),
|
||||
$this->relation('featured_collections', WorldRelation::TYPE_COLLECTION, $collections[1]?->id, 'Week collection', true, 0),
|
||||
$this->relation('featured_creators', WorldRelation::TYPE_USER, $pixelCreator->id, 'Week host', true, 0),
|
||||
$this->relation('news', WorldRelation::TYPE_NEWS, ($newsArticles[2] ?? null)?->id, 'Preparation guide', true, 0),
|
||||
]);
|
||||
|
||||
$this->syncRelations($summerWorld, [
|
||||
$this->relation('featured_artworks', WorldRelation::TYPE_ARTWORK, $artworks[2]?->id, 'Summer signal', true, 0),
|
||||
$this->relation('featured_collections', WorldRelation::TYPE_COLLECTION, $collections[0]?->id, 'Warm palette roundup', true, 0),
|
||||
$this->relation('featured_groups', WorldRelation::TYPE_GROUP, $groups[1]?->id, 'Collaborative feature', true, 0),
|
||||
$this->relation('news', WorldRelation::TYPE_NEWS, ($newsArticles[3] ?? null)?->id, 'Publishing guide', true, 0),
|
||||
]);
|
||||
|
||||
$this->syncRelations($halloweenWorld, [
|
||||
$this->relation('featured_artworks', WorldRelation::TYPE_ARTWORK, $artworks[0]?->id, 'Seasonal teaser', true, 0),
|
||||
$this->relation('featured_groups', WorldRelation::TYPE_GROUP, $groups[0]?->id, 'Event collaborators', true, 0),
|
||||
$this->relation('news', WorldRelation::TYPE_NEWS, ($newsArticles[4] ?? null)?->id, 'Spotlight coverage', true, 0),
|
||||
]);
|
||||
|
||||
$this->syncRelations($christmasWorld, [
|
||||
$this->relation('featured_artworks', WorldRelation::TYPE_ARTWORK, $artworks[2]?->id, 'Holiday mood', true, 0),
|
||||
$this->relation('featured_collections', WorldRelation::TYPE_COLLECTION, $collections[0]?->id, 'Gift-guide curation', true, 0),
|
||||
$this->relation('featured_creators', WorldRelation::TYPE_USER, $sceneCreator->id, 'Seasonal creator', true, 0),
|
||||
$this->relation('news', WorldRelation::TYPE_NEWS, ($newsArticles[5] ?? null)?->id, 'Release coverage', true, 0),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
private function upsertCreator(string $username, string $name, string $email): User
|
||||
{
|
||||
return User::query()->updateOrCreate(
|
||||
['email' => $email],
|
||||
[
|
||||
'username' => $username,
|
||||
'username_changed_at' => now()->subDays(120),
|
||||
'onboarding_step' => 'complete',
|
||||
'name' => $name,
|
||||
'password' => bcrypt('password'),
|
||||
'email_verified_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function upsertArtwork(User $creator, string $title, string $slug, string $description): Artwork
|
||||
{
|
||||
$attributes = Artwork::factory()->make([
|
||||
'user_id' => $creator->id,
|
||||
'uploaded_by_user_id' => $creator->id,
|
||||
'primary_author_user_id' => $creator->id,
|
||||
'published_as_type' => Artwork::PUBLISHED_AS_USER,
|
||||
'published_as_id' => $creator->id,
|
||||
'title' => $title,
|
||||
'slug' => $slug,
|
||||
'description' => $description,
|
||||
'visibility' => Artwork::VISIBILITY_PUBLIC,
|
||||
'is_public' => true,
|
||||
'is_approved' => true,
|
||||
'published_at' => now()->subDays(12),
|
||||
'artwork_status' => 'published',
|
||||
])->getAttributes();
|
||||
|
||||
return Artwork::query()->updateOrCreate(['slug' => $slug], $attributes);
|
||||
}
|
||||
|
||||
private function upsertCollection(User $creator, string $title, string $slug, string $description): Collection
|
||||
{
|
||||
$attributes = Collection::factory()->make([
|
||||
'user_id' => $creator->id,
|
||||
'title' => $title,
|
||||
'slug' => $slug,
|
||||
'description' => $description,
|
||||
'summary' => $description,
|
||||
'visibility' => Collection::VISIBILITY_PUBLIC,
|
||||
'published_at' => now()->subDays(10),
|
||||
])->getAttributes();
|
||||
|
||||
return Collection::query()->updateOrCreate(['slug' => $slug], $attributes);
|
||||
}
|
||||
|
||||
private function upsertGroup(User $owner, string $name, string $slug, string $bio): Group
|
||||
{
|
||||
$attributes = Group::factory()->make([
|
||||
'owner_user_id' => $owner->id,
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
'headline' => $bio,
|
||||
'bio' => $bio,
|
||||
'visibility' => Group::VISIBILITY_PUBLIC,
|
||||
'status' => Group::LIFECYCLE_ACTIVE,
|
||||
])->getAttributes();
|
||||
|
||||
return Group::query()->updateOrCreate(['slug' => $slug], $attributes);
|
||||
}
|
||||
|
||||
private function upsertWorld(string $slug, array $attributes): World
|
||||
{
|
||||
return World::query()->updateOrCreate(['slug' => $slug], $attributes);
|
||||
}
|
||||
|
||||
private function syncRelations(World $world, array $relations): void
|
||||
{
|
||||
$world->worldRelations()->delete();
|
||||
|
||||
foreach (array_values(array_filter($relations)) as $relation) {
|
||||
$world->worldRelations()->create($relation);
|
||||
}
|
||||
}
|
||||
|
||||
private function relation(string $sectionKey, string $relatedType, ?int $relatedId, string $contextLabel, bool $featured, int $sortOrder): ?array
|
||||
{
|
||||
if (! $relatedId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'section_key' => $sectionKey,
|
||||
'related_type' => $relatedType,
|
||||
'related_id' => $relatedId,
|
||||
'context_label' => $contextLabel,
|
||||
'sort_order' => $sortOrder,
|
||||
'is_featured' => $featured,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, NewsArticle>
|
||||
*/
|
||||
private function seedableNewsArticles(): array
|
||||
{
|
||||
if (! Schema::hasTable((new NewsArticle())->getTable())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return NewsArticle::query()
|
||||
->published()
|
||||
->orderByDesc('published_at')
|
||||
->limit(6)
|
||||
->get()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, NovaCard>
|
||||
*/
|
||||
private function seedableCards(): array
|
||||
{
|
||||
if (! Schema::hasTable((new NovaCard())->getTable())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return NovaCard::query()
|
||||
->whereNotNull('published_at')
|
||||
->orderByDesc('published_at')
|
||||
->limit(3)
|
||||
->get()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user