Build world campaigns rewards and recaps
This commit is contained in:
@@ -28,7 +28,10 @@ class WorldFactory extends Factory
|
||||
'slug' => Str::slug($title),
|
||||
'tagline' => $this->faker->sentence(6),
|
||||
'summary' => $this->faker->sentence(12),
|
||||
'teaser_title' => null,
|
||||
'teaser_summary' => null,
|
||||
'description' => $this->faker->paragraphs(2, true),
|
||||
'teaser_image_path' => null,
|
||||
'theme_key' => 'summer',
|
||||
'accent_color' => '#22c55e',
|
||||
'accent_color_secondary' => '#0f172a',
|
||||
@@ -38,7 +41,12 @@ class WorldFactory extends Factory
|
||||
'type' => World::TYPE_SEASONAL,
|
||||
'starts_at' => $startsAt,
|
||||
'ends_at' => $endsAt,
|
||||
'promotion_starts_at' => $startsAt,
|
||||
'promotion_ends_at' => $endsAt,
|
||||
'is_featured' => false,
|
||||
'is_active_campaign' => false,
|
||||
'is_homepage_featured' => false,
|
||||
'campaign_priority' => null,
|
||||
'accepts_submissions' => true,
|
||||
'participation_mode' => World::PARTICIPATION_MODE_MANUAL_APPROVAL,
|
||||
'submission_starts_at' => $startsAt->copy()->subDay(),
|
||||
@@ -53,6 +61,7 @@ class WorldFactory extends Factory
|
||||
'cta_label' => 'Explore world',
|
||||
'cta_url' => '/worlds',
|
||||
'badge_label' => null,
|
||||
'campaign_label' => null,
|
||||
'badge_description' => null,
|
||||
'badge_url' => null,
|
||||
'seo_title' => $title,
|
||||
@@ -73,6 +82,27 @@ class WorldFactory extends Factory
|
||||
]);
|
||||
}
|
||||
|
||||
public function activeCampaign(int $priority = 100): self
|
||||
{
|
||||
return $this->state(fn (): array => [
|
||||
'is_active_campaign' => true,
|
||||
'campaign_priority' => $priority,
|
||||
'promotion_starts_at' => Carbon::now()->subDays(2),
|
||||
'promotion_ends_at' => Carbon::now()->addDays(5),
|
||||
]);
|
||||
}
|
||||
|
||||
public function homepageFeatured(int $priority = 100): self
|
||||
{
|
||||
return $this->state(fn (): array => [
|
||||
'is_active_campaign' => true,
|
||||
'is_homepage_featured' => true,
|
||||
'campaign_priority' => $priority,
|
||||
'promotion_starts_at' => Carbon::now()->subDays(2),
|
||||
'promotion_ends_at' => Carbon::now()->addDays(5),
|
||||
]);
|
||||
}
|
||||
|
||||
public function current(): self
|
||||
{
|
||||
return $this->state(fn (): array => [
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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('is_active_campaign')->default(false)->after('is_featured');
|
||||
$table->boolean('is_homepage_featured')->default(false)->after('is_active_campaign');
|
||||
$table->integer('campaign_priority')->nullable()->after('is_homepage_featured');
|
||||
$table->string('campaign_label', 120)->nullable()->after('badge_label');
|
||||
$table->string('teaser_title', 180)->nullable()->after('summary');
|
||||
$table->string('teaser_summary', 320)->nullable()->after('teaser_title');
|
||||
$table->string('teaser_image_path', 2048)->nullable()->after('cover_path');
|
||||
$table->timestamp('promotion_starts_at')->nullable()->after('ends_at');
|
||||
$table->timestamp('promotion_ends_at')->nullable()->after('promotion_starts_at');
|
||||
|
||||
$table->index(['status', 'is_active_campaign', 'campaign_priority'], 'worlds_activation_idx');
|
||||
$table->index(['is_homepage_featured', 'campaign_priority'], 'worlds_homepage_featured_idx');
|
||||
$table->index(['promotion_starts_at', 'promotion_ends_at'], 'worlds_promotion_window_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->dropIndex('worlds_activation_idx');
|
||||
$table->dropIndex('worlds_homepage_featured_idx');
|
||||
$table->dropIndex('worlds_promotion_window_idx');
|
||||
$table->dropColumn([
|
||||
'is_active_campaign',
|
||||
'is_homepage_featured',
|
||||
'campaign_priority',
|
||||
'campaign_label',
|
||||
'teaser_title',
|
||||
'teaser_summary',
|
||||
'teaser_image_path',
|
||||
'promotion_starts_at',
|
||||
'promotion_ends_at',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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('world_reward_grants', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('world_id')->constrained('worlds')->cascadeOnDelete();
|
||||
$table->foreignId('artwork_id')->nullable()->constrained('artworks')->nullOnDelete();
|
||||
$table->foreignId('world_submission_id')->nullable()->constrained('world_submissions')->nullOnDelete();
|
||||
$table->foreignId('granted_by_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('reward_type', 32);
|
||||
$table->string('grant_source', 16)->default('manual');
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamp('granted_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'world_id', 'reward_type'], 'world_reward_grants_unique_reward');
|
||||
$table->index(['world_id', 'reward_type']);
|
||||
$table->index(['artwork_id']);
|
||||
$table->index(['world_submission_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('world_reward_grants');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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::table('worlds', function (Blueprint $table): void {
|
||||
$table->dropIndex('worlds_recurrence_idx');
|
||||
$table->unique(['recurrence_key', 'edition_year'], 'worlds_recurrence_year_unique');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->dropUnique('worlds_recurrence_year_unique');
|
||||
$table->index(['recurrence_key', 'edition_year'], 'worlds_recurrence_idx');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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::table('worlds', function (Blueprint $table): void {
|
||||
$table->json('hidden_linked_challenge_artwork_ids_json')
|
||||
->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->dropColumn('hidden_linked_challenge_artwork_ids_json');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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
|
||||
{
|
||||
$afterColumn = Schema::hasColumn('worlds', 'recap_intro')
|
||||
? 'recap_intro'
|
||||
: (Schema::hasColumn('worlds', 'published_at') ? 'published_at' : null);
|
||||
$hasRecapEditorNote = Schema::hasColumn('worlds', 'recap_editor_note');
|
||||
$hasRecapCoverPath = Schema::hasColumn('worlds', 'recap_cover_path');
|
||||
|
||||
Schema::table('worlds', function (Blueprint $table) use ($afterColumn, $hasRecapEditorNote, $hasRecapCoverPath): void {
|
||||
if (! $hasRecapEditorNote) {
|
||||
$column = $table->text('recap_editor_note')->nullable();
|
||||
|
||||
if ($afterColumn !== null) {
|
||||
$column->after($afterColumn);
|
||||
}
|
||||
}
|
||||
|
||||
if (! $hasRecapCoverPath) {
|
||||
$column = $table->string('recap_cover_path', 2048)->nullable();
|
||||
|
||||
if (! $hasRecapEditorNote) {
|
||||
$column->after('recap_editor_note');
|
||||
} elseif ($afterColumn !== null) {
|
||||
$column->after($afterColumn);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$columns = array_values(array_filter([
|
||||
Schema::hasColumn('worlds', 'recap_editor_note') ? 'recap_editor_note' : null,
|
||||
Schema::hasColumn('worlds', 'recap_cover_path') ? 'recap_cover_path' : null,
|
||||
]));
|
||||
|
||||
if ($columns !== []) {
|
||||
$table->dropColumn($columns);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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('world_editorial_suggestion_states', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('world_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('related_type', 40);
|
||||
$table->unsignedBigInteger('related_id');
|
||||
$table->string('status', 40);
|
||||
$table->string('section_key', 80)->nullable();
|
||||
$table->foreignId('acted_by_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['world_id', 'related_type', 'related_id'], 'world_editorial_suggestion_states_unique');
|
||||
$table->index(['world_id', 'status'], 'world_editorial_suggestion_states_status_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('world_editorial_suggestion_states');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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::table('worlds', function (Blueprint $table): void {
|
||||
$table->foreignId('linked_challenge_id')->nullable()->after('parent_world_id')->constrained('group_challenges')->nullOnDelete();
|
||||
$table->boolean('show_linked_challenge_section')->default(true)->after('linked_challenge_id');
|
||||
$table->boolean('show_linked_challenge_entries')->default(true)->after('show_linked_challenge_section');
|
||||
$table->boolean('show_linked_challenge_winners')->default(true)->after('show_linked_challenge_entries');
|
||||
$table->boolean('show_linked_challenge_finalists')->default(true)->after('show_linked_challenge_winners');
|
||||
$table->boolean('auto_grant_challenge_world_rewards')->default(true)->after('show_linked_challenge_finalists');
|
||||
$table->text('challenge_teaser_override')->nullable()->after('auto_grant_challenge_world_rewards');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->dropConstrainedForeignId('linked_challenge_id');
|
||||
$table->dropColumn([
|
||||
'show_linked_challenge_section',
|
||||
'show_linked_challenge_entries',
|
||||
'show_linked_challenge_winners',
|
||||
'show_linked_challenge_finalists',
|
||||
'auto_grant_challenge_world_rewards',
|
||||
'challenge_teaser_override',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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('group_challenge_outcomes', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_challenge_id')->constrained('group_challenges')->cascadeOnDelete();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('outcome_type', 32);
|
||||
$table->unsignedInteger('position')->nullable();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->string('title_override', 120)->nullable();
|
||||
$table->text('note')->nullable();
|
||||
$table->foreignId('awarded_by_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('awarded_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_challenge_id', 'artwork_id', 'outcome_type'], 'group_challenge_outcomes_unique');
|
||||
$table->index(['group_challenge_id', 'outcome_type', 'sort_order'], 'group_challenge_outcomes_type_sort_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('group_challenge_outcomes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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_analytics_events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('world_id')->index();
|
||||
$table->string('event_type', 40)->index();
|
||||
$table->string('world_slug', 160)->index();
|
||||
$table->string('world_type', 40)->nullable()->index();
|
||||
$table->string('recurrence_key', 120)->nullable()->index();
|
||||
$table->unsignedSmallInteger('edition_year')->nullable()->index();
|
||||
$table->string('section_key', 80)->nullable()->index();
|
||||
$table->string('cta_key', 80)->nullable()->index();
|
||||
$table->string('entity_type', 40)->nullable()->index();
|
||||
$table->unsignedBigInteger('entity_id')->nullable()->index();
|
||||
$table->string('entity_title', 180)->nullable();
|
||||
$table->unsignedBigInteger('challenge_id')->nullable()->index();
|
||||
$table->string('source_surface', 80)->nullable()->index();
|
||||
$table->string('source_detail', 80)->nullable()->index();
|
||||
$table->string('viewer_type', 16)->index();
|
||||
$table->unsignedBigInteger('user_id')->nullable()->index();
|
||||
$table->string('visitor_key', 64)->index();
|
||||
$table->json('meta')->nullable();
|
||||
$table->timestamp('occurred_at')->useCurrent()->index();
|
||||
|
||||
$table->index(['world_id', 'event_type', 'occurred_at'], 'world_analytics_world_event_occurred_idx');
|
||||
$table->index(['world_id', 'source_surface', 'occurred_at'], 'world_analytics_world_source_occurred_idx');
|
||||
$table->index(['world_id', 'section_key', 'occurred_at'], 'world_analytics_world_section_occurred_idx');
|
||||
|
||||
$table->foreign('world_id')->references('id')->on('worlds')->cascadeOnDelete();
|
||||
$table->foreign('user_id')->references('id')->on('users')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('world_analytics_events');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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::table('worlds', function (Blueprint $table): void {
|
||||
$table->string('recap_status', 24)->default('draft')->after('published_at');
|
||||
$table->string('recap_title', 180)->nullable()->after('recap_status');
|
||||
$table->string('recap_summary', 320)->nullable()->after('recap_title');
|
||||
$table->text('recap_intro')->nullable()->after('recap_summary');
|
||||
$table->foreignId('recap_article_id')->nullable()->after('recap_intro')->constrained('news_articles')->nullOnDelete();
|
||||
$table->json('recap_stats_snapshot_json')->nullable()->after('recap_article_id');
|
||||
$table->timestamp('recap_published_at')->nullable()->after('recap_stats_snapshot_json');
|
||||
|
||||
$table->index(['recap_status', 'recap_published_at'], 'worlds_recap_status_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('worlds', function (Blueprint $table): void {
|
||||
$table->dropIndex('worlds_recap_status_idx');
|
||||
$table->dropConstrainedForeignId('recap_article_id');
|
||||
$table->dropColumn([
|
||||
'recap_status',
|
||||
'recap_title',
|
||||
'recap_summary',
|
||||
'recap_intro',
|
||||
'recap_stats_snapshot_json',
|
||||
'recap_published_at',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -62,10 +62,95 @@ final class WorldLaunchSeeder extends Seeder
|
||||
$now = now();
|
||||
$currentYear = (int) $now->year;
|
||||
|
||||
$springVibes = $this->upsertWorld('spring-vibes-' . $currentYear, [
|
||||
'title' => 'Spring Vibes ' . $currentYear,
|
||||
'tagline' => 'Fresh palettes, softer light, and a friendly live participation moment across Skinbase.',
|
||||
'summary' => 'A live seasonal world for bright artwork, curated collections, creator spotlights, and active submissions.',
|
||||
'teaser_title' => 'Now live: Spring Vibes',
|
||||
'teaser_summary' => 'Fresh seasonal artwork, open submissions, and curated highlights are all running inside the current Spring Vibes campaign.',
|
||||
'description' => 'Spring Vibes is the first fully activated world. It is meant to feel like a live public campaign across homepage, worlds discovery, and upload participation rather than a buried editorial page.',
|
||||
'theme_key' => 'seasonal',
|
||||
'accent_color' => '#84cc16',
|
||||
'accent_color_secondary' => '#14532d',
|
||||
'background_motif' => 'bloomwave',
|
||||
'icon_name' => 'fa-solid fa-seedling',
|
||||
'status' => World::STATUS_PUBLISHED,
|
||||
'type' => World::TYPE_SEASONAL,
|
||||
'starts_at' => $now->copy()->subDays(6)->startOfDay(),
|
||||
'ends_at' => $now->copy()->addDays(18)->endOfDay(),
|
||||
'promotion_starts_at' => $now->copy()->subDays(2)->startOfDay(),
|
||||
'promotion_ends_at' => $now->copy()->addDays(12)->endOfDay(),
|
||||
'is_featured' => true,
|
||||
'is_active_campaign' => true,
|
||||
'is_homepage_featured' => true,
|
||||
'campaign_priority' => 900,
|
||||
'campaign_label' => 'Live now',
|
||||
'accepts_submissions' => true,
|
||||
'participation_mode' => World::PARTICIPATION_MODE_MANUAL_APPROVAL,
|
||||
'submission_starts_at' => $now->copy()->subDays(4)->startOfDay(),
|
||||
'submission_ends_at' => $now->copy()->addDays(10)->endOfDay(),
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'spring-vibes',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->month,
|
||||
'edition_year' => $currentYear,
|
||||
'cta_label' => 'Join Spring Vibes',
|
||||
'cta_url' => '/worlds/spring-vibes-' . $currentYear,
|
||||
'badge_label' => 'Homepage spotlight',
|
||||
'badge_description' => 'The primary live world promoted across homepage, upload, and worlds discovery surfaces.',
|
||||
'badge_url' => '/worlds',
|
||||
'seo_title' => 'Spring Vibes ' . $currentYear . ' on Skinbase',
|
||||
'seo_description' => 'Spring Vibes is the live seasonal world on Skinbase right now, with open participation and editorially promoted discovery.',
|
||||
'related_tags_json' => ['spring', 'seasonal', 'fresh', 'bloom'],
|
||||
'section_order_json' => ['featured_artworks', 'featured_collections', 'featured_creators', 'featured_groups', 'news', 'cards'],
|
||||
'created_by_user_id' => $editor->id,
|
||||
'published_at' => $now->copy()->subDays(9),
|
||||
]);
|
||||
|
||||
$springArchive = $this->upsertWorld('spring-vibes-' . ($currentYear - 1), [
|
||||
'title' => 'Spring Vibes ' . ($currentYear - 1),
|
||||
'tagline' => 'Last year\'s edition of the recurring spring campaign.',
|
||||
'summary' => 'Archived spring edition kept visible as part of the recurring worlds record.',
|
||||
'teaser_title' => 'Spring Vibes archive',
|
||||
'teaser_summary' => 'The previous edition remains browsable so recurring worlds build continuity over time.',
|
||||
'description' => 'Spring Vibes archive keeps the prior edition public so the recurring seasonal world feels like a real continuing program.',
|
||||
'theme_key' => 'seasonal',
|
||||
'accent_color' => '#65a30d',
|
||||
'accent_color_secondary' => '#365314',
|
||||
'background_motif' => 'bloomwave',
|
||||
'icon_name' => 'fa-solid fa-seedling',
|
||||
'status' => World::STATUS_ARCHIVED,
|
||||
'type' => World::TYPE_SEASONAL,
|
||||
'starts_at' => $now->copy()->subYear()->subDays(6)->startOfDay(),
|
||||
'ends_at' => $now->copy()->subYear()->addDays(18)->endOfDay(),
|
||||
'is_featured' => false,
|
||||
'is_active_campaign' => false,
|
||||
'is_homepage_featured' => false,
|
||||
'campaign_priority' => null,
|
||||
'campaign_label' => 'Archive edition',
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'spring-vibes',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->month,
|
||||
'edition_year' => $currentYear - 1,
|
||||
'cta_label' => 'Browse archive',
|
||||
'cta_url' => '/worlds/spring-vibes-' . ($currentYear - 1),
|
||||
'badge_label' => 'Archive edition',
|
||||
'badge_description' => 'The prior Spring Vibes edition remains visible for continuity.',
|
||||
'badge_url' => '/worlds',
|
||||
'seo_title' => 'Spring Vibes ' . ($currentYear - 1) . ' archive',
|
||||
'seo_description' => 'The previous Spring Vibes edition remains public as part of the recurring worlds archive.',
|
||||
'related_tags_json' => ['spring', 'archive'],
|
||||
'section_order_json' => ['featured_artworks', 'featured_creators', 'news'],
|
||||
'parent_world_id' => $springVibes->id,
|
||||
'created_by_user_id' => $editor->id,
|
||||
'published_at' => $now->copy()->subYear()->subDays(12),
|
||||
]);
|
||||
|
||||
$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.',
|
||||
'teaser_title' => 'Explore Retro Month',
|
||||
'teaser_summary' => 'A live supporting campaign for glossy nostalgia, synth palettes, and editorially curated retro culture.',
|
||||
'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',
|
||||
@@ -76,7 +161,13 @@ final class WorldLaunchSeeder extends Seeder
|
||||
'type' => World::TYPE_CAMPAIGN,
|
||||
'starts_at' => $now->copy()->subDays(10)->startOfDay(),
|
||||
'ends_at' => $now->copy()->addDays(18)->endOfDay(),
|
||||
'promotion_starts_at' => $now->copy()->subDays(5)->startOfDay(),
|
||||
'promotion_ends_at' => $now->copy()->addDays(9)->endOfDay(),
|
||||
'is_featured' => true,
|
||||
'is_active_campaign' => true,
|
||||
'is_homepage_featured' => false,
|
||||
'campaign_priority' => 450,
|
||||
'campaign_label' => 'Supporting campaign',
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'retro-month',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->month,
|
||||
@@ -109,6 +200,10 @@ final class WorldLaunchSeeder extends Seeder
|
||||
'starts_at' => $now->copy()->subYear()->subDays(10)->startOfDay(),
|
||||
'ends_at' => $now->copy()->subYear()->addDays(18)->endOfDay(),
|
||||
'is_featured' => false,
|
||||
'is_active_campaign' => false,
|
||||
'is_homepage_featured' => false,
|
||||
'campaign_priority' => null,
|
||||
'campaign_label' => 'Archive edition',
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'retro-month',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->month,
|
||||
@@ -131,6 +226,8 @@ final class WorldLaunchSeeder extends Seeder
|
||||
'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.',
|
||||
'teaser_title' => 'Pixel Week is coming up',
|
||||
'teaser_summary' => 'The next campaign is queued with a tight pixel-art brief, creator spotlights, and themed collections.',
|
||||
'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',
|
||||
@@ -141,7 +238,13 @@ final class WorldLaunchSeeder extends Seeder
|
||||
'type' => World::TYPE_EVENT,
|
||||
'starts_at' => $now->copy()->addDays(24)->startOfDay(),
|
||||
'ends_at' => $now->copy()->addDays(31)->endOfDay(),
|
||||
'promotion_starts_at' => $now->copy()->addDays(18)->startOfDay(),
|
||||
'promotion_ends_at' => $now->copy()->addDays(31)->endOfDay(),
|
||||
'is_featured' => false,
|
||||
'is_active_campaign' => true,
|
||||
'is_homepage_featured' => false,
|
||||
'campaign_priority' => 300,
|
||||
'campaign_label' => 'Upcoming campaign',
|
||||
'is_recurring' => true,
|
||||
'recurrence_key' => 'pixel-week',
|
||||
'recurrence_rule' => 'FREQ=YEARLY;BYMONTH=' . $now->copy()->addDays(24)->month,
|
||||
@@ -255,6 +358,22 @@ final class WorldLaunchSeeder extends Seeder
|
||||
'published_at' => $now->copy()->subDays(7),
|
||||
]);
|
||||
|
||||
$this->syncRelations($springVibes, [
|
||||
$this->relation('featured_artworks', WorldRelation::TYPE_ARTWORK, $artworks[2]?->id, 'Live seasonal feature', true, 0),
|
||||
$this->relation('featured_artworks', WorldRelation::TYPE_ARTWORK, $artworks[1]?->id, 'Community highlight', false, 1),
|
||||
$this->relation('featured_collections', WorldRelation::TYPE_COLLECTION, $collections[1]?->id, 'Fresh picks', true, 0),
|
||||
$this->relation('featured_creators', WorldRelation::TYPE_USER, $sceneCreator->id, 'Spring spotlight creator', true, 0),
|
||||
$this->relation('featured_groups', WorldRelation::TYPE_GROUP, $groups[0]?->id, 'Collaborative feature', true, 0),
|
||||
$this->relation('news', WorldRelation::TYPE_NEWS, ($newsArticles[0] ?? null)?->id, 'Launch coverage', true, 0),
|
||||
$this->relation('cards', WorldRelation::TYPE_CARD, ($cards[0] ?? null)?->id, 'Campaign card', true, 0),
|
||||
]);
|
||||
|
||||
$this->syncRelations($springArchive, [
|
||||
$this->relation('featured_artworks', WorldRelation::TYPE_ARTWORK, $artworks[2]?->id, 'Archive favorite', true, 0),
|
||||
$this->relation('featured_creators', WorldRelation::TYPE_USER, $sceneCreator->id, 'Returning creator', true, 0),
|
||||
$this->relation('news', WorldRelation::TYPE_NEWS, ($newsArticles[0] ?? null)?->id, 'Previous launch notes', true, 0),
|
||||
]);
|
||||
|
||||
$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),
|
||||
|
||||
Reference in New Issue
Block a user