feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile
Forum: - TipTap WYSIWYG editor with full toolbar - @emoji-mart/react emoji picker (consistent with tweets) - @mention autocomplete with user search API - Fix PHP 8.4 parse errors in Blade templates - Fix thread data display (paginator items) - Align forum page widths to max-w-5xl Discover: - Extract shared _nav.blade.php partial - Add missing nav links to for-you page - Add Following link for authenticated users Feed/Posts: - Post model, controllers, policies, migrations - Feed page components (PostComposer, FeedCard, etc) - Post reactions, comments, saves, reports, sharing - Scheduled publishing support - Link preview controller Profile: - Profile page components (ProfileHero, ProfileTabs) - Profile API controller Uploads: - Upload wizard enhancements - Scheduled publish picker - Studio status bar and readiness checklist
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Add scheduled-publishing columns to artworks table.
|
||||
*
|
||||
* Fields added:
|
||||
* - publish_at Nullable UTC datetime – when the artwork becomes public (scheduled publish)
|
||||
* - artwork_status Enum string: draft | scheduled | published | archived
|
||||
* - artwork_timezone Nullable IANA timezone string for display purposes only
|
||||
*
|
||||
* Note: we use `artwork_status` to avoid collision with any reserved word `status`
|
||||
* across DB adapters. A DB-level check constraint enforces valid values.
|
||||
*
|
||||
* Rules:
|
||||
* - publish_at set in the future → artwork_status = 'scheduled'
|
||||
* - scheduler job publishes when publish_at <= now():
|
||||
* sets published_at = now(), artwork_status = 'published'
|
||||
* - Immediate publish keeps existing flow (published_at = now(), artwork_status = 'published')
|
||||
* - Draft state is pre-publish (artwork_status = 'draft', published_at = null)
|
||||
*/
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('artworks', function (Blueprint $table) {
|
||||
// When this artwork should become publicly visible (UTC)
|
||||
$table->dateTime('publish_at')->nullable()->after('published_at');
|
||||
|
||||
// Lifecycle status
|
||||
$table->string('artwork_status', 20)->default('draft')->after('publish_at');
|
||||
|
||||
// User's display timezone (IANA), stored for UX display only
|
||||
$table->string('artwork_timezone', 50)->nullable()->after('artwork_status');
|
||||
|
||||
// Index for scheduler job: find artworks whose publish_at has passed
|
||||
$table->index(['artwork_status', 'publish_at'], 'idx_artworks_scheduled_publish');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artworks', function (Blueprint $table) {
|
||||
$table->dropIndex('idx_artworks_scheduled_publish');
|
||||
$table->dropColumn(['publish_at', 'artwork_status', 'artwork_timezone']);
|
||||
});
|
||||
}
|
||||
};
|
||||
33
database/migrations/2026_03_02_000001_create_posts_table.php
Normal file
33
database/migrations/2026_03_02_000001_create_posts_table.php
Normal file
@@ -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('posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('type', 32)->default('text'); // text | artwork_share | upload | achievement
|
||||
$table->string('visibility', 16)->default('public'); // public | followers | private
|
||||
$table->longText('body')->nullable();
|
||||
$table->json('meta')->nullable();
|
||||
$table->unsignedInteger('reactions_count')->default(0);
|
||||
$table->unsignedInteger('comments_count')->default(0);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['user_id', 'created_at']);
|
||||
$table->index(['type', 'created_at']);
|
||||
$table->index(['visibility', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('posts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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('post_targets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('target_type', 32); // artwork | collection
|
||||
$table->unsignedBigInteger('target_id');
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index('post_id');
|
||||
$table->index(['target_type', 'target_id']);
|
||||
$table->unique(['post_id', 'target_type', 'target_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('post_targets');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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('post_reactions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('reaction', 16)->default('like');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('post_id');
|
||||
$table->index('user_id');
|
||||
$table->unique(['post_id', 'user_id', 'reaction']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('post_reactions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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('post_comments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->text('body');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['post_id', 'created_at']);
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('post_comments');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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('post_saves', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index('post_id');
|
||||
$table->index('user_id');
|
||||
$table->unique(['post_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('post_saves');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?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('post_reports', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('reporter_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('reason', 64);
|
||||
$table->text('message')->nullable();
|
||||
$table->string('status', 16)->default('open'); // open | reviewed | actioned
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('post_id');
|
||||
$table->index('reporter_user_id');
|
||||
$table->index('status');
|
||||
$table->unique(['post_id', 'reporter_user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('post_reports');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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('posts', function (Blueprint $table) {
|
||||
// Pinned posts
|
||||
$table->boolean('is_pinned')->default(false)->after('meta');
|
||||
$table->unsignedTinyInteger('pinned_order')->nullable()->after('is_pinned');
|
||||
|
||||
// Scheduled posts
|
||||
$table->timestamp('publish_at')->nullable()->after('pinned_order');
|
||||
$table->string('status', 16)->default('published')->after('publish_at'); // draft | scheduled | published
|
||||
|
||||
// Analytics
|
||||
$table->unsignedBigInteger('impressions_count')->default(0)->after('comments_count');
|
||||
$table->float('engagement_score')->default(0.0)->after('impressions_count');
|
||||
$table->unsignedInteger('saves_count')->default(0)->after('engagement_score');
|
||||
|
||||
$table->index(['is_pinned', 'user_id', 'pinned_order']);
|
||||
$table->index(['status', 'publish_at']);
|
||||
$table->index(['status', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('posts', function (Blueprint $table) {
|
||||
$table->dropIndex(['is_pinned', 'user_id', 'pinned_order']);
|
||||
$table->dropIndex(['status', 'publish_at']);
|
||||
$table->dropIndex(['status', 'created_at']);
|
||||
$table->dropColumn(['is_pinned', 'pinned_order', 'publish_at', 'status',
|
||||
'impressions_count', 'engagement_score', 'saves_count']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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('post_hashtags', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete(); // denormalised for fast author diversity queries
|
||||
$table->string('tag', 64)->index();
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
|
||||
$table->index(['tag', 'created_at']); // trending query
|
||||
$table->index(['post_id', 'tag']);
|
||||
$table->unique(['post_id', 'tag']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('post_hashtags');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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('post_comments', function (Blueprint $table) {
|
||||
$table->boolean('is_highlighted')->default(false)->after('body');
|
||||
$table->index(['post_id', 'is_highlighted']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('post_comments', function (Blueprint $table) {
|
||||
$table->dropIndex(['post_id', 'is_highlighted']);
|
||||
$table->dropColumn('is_highlighted');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('user_profiles', function (Blueprint $table) {
|
||||
$table->boolean('auto_post_upload')->default(true)->after('website');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('user_profiles', function (Blueprint $table) {
|
||||
$table->dropColumn('auto_post_upload');
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user