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:
124
routes/api.php
124
routes/api.php
@@ -446,3 +446,127 @@ Route::middleware(['web', 'auth', 'normalize.username', 'throttle:60,1'])
|
||||
Route::middleware(['web', 'auth', 'normalize.username', 'throttle:60,1'])
|
||||
->post('reports', [\App\Http\Controllers\Api\ReportController::class, 'store'])
|
||||
->name('api.reports.store');
|
||||
|
||||
// ── Profile API (public, throttled) ─────────────────────────────────────────
|
||||
// GET /api/profile/{username}/artworks?sort=latest|trending|rising|views|favs&cursor=...
|
||||
// GET /api/profile/{username}/favourites?cursor=...
|
||||
// GET /api/profile/{username}/stats
|
||||
Route::middleware(['web', 'throttle:60,1'])
|
||||
->prefix('profile/{username}')
|
||||
->name('api.profile.')
|
||||
->where(['username' => '[A-Za-z0-9_-]{3,20}'])
|
||||
->group(function () {
|
||||
Route::get('artworks', [\App\Http\Controllers\Api\ProfileApiController::class, 'artworks'])->name('artworks');
|
||||
Route::get('favourites', [\App\Http\Controllers\Api\ProfileApiController::class, 'favourites'])->name('favourites');
|
||||
Route::get('stats', [\App\Http\Controllers\Api\ProfileApiController::class, 'stats'])->name('stats');
|
||||
});
|
||||
|
||||
// ── Link Preview (auth, throttled) ─────────────────────────────────────────────
|
||||
// GET /api/link-preview?url=... → fetch OG tags for a URL
|
||||
|
||||
Route::middleware(['web', 'auth', 'throttle:30,1'])
|
||||
->get('link-preview', \App\Http\Controllers\Api\LinkPreviewController::class)
|
||||
->name('api.link-preview');
|
||||
|
||||
// ── Posts / Feed System ───────────────────────────────────────────────────────
|
||||
// Public: profile feed (respects visibility)
|
||||
// Auth : create/edit/delete posts, following feed, reactions, comments, reports, shares
|
||||
|
||||
Route::middleware(['web', 'throttle:60,1'])
|
||||
->prefix('posts')
|
||||
->name('api.posts.')
|
||||
->group(function () {
|
||||
// Profile feed (public, visibility-filtered)
|
||||
Route::get('profile/{username}', [\App\Http\Controllers\Api\Posts\PostFeedController::class, 'profile'])
|
||||
->where('username', '[A-Za-z0-9_-]{3,20}')
|
||||
->name('profile');
|
||||
|
||||
// Per-post comments (public)
|
||||
Route::get('{id}/comments', [\App\Http\Controllers\Api\Posts\PostCommentController::class, 'index'])
|
||||
->whereNumber('id')
|
||||
->name('comments.index');
|
||||
});
|
||||
|
||||
Route::middleware(['web', 'auth', 'normalize.username'])
|
||||
->prefix('posts')
|
||||
->name('api.posts.')
|
||||
->group(function () {
|
||||
// Following feed (auth)
|
||||
Route::get('following', [\App\Http\Controllers\Api\Posts\PostFeedController::class, 'following'])
|
||||
->middleware('throttle:60,1')
|
||||
->name('following');
|
||||
|
||||
// CRUD
|
||||
Route::post('/', [\App\Http\Controllers\Api\Posts\PostController::class, 'store'])->name('store');
|
||||
Route::patch('{id}', [\App\Http\Controllers\Api\Posts\PostController::class, 'update'])->whereNumber('id')->name('update');
|
||||
Route::delete('{id}', [\App\Http\Controllers\Api\Posts\PostController::class, 'destroy'])->whereNumber('id')->name('destroy');
|
||||
|
||||
// Share artwork
|
||||
Route::post('share/artwork/{artwork_id}', [\App\Http\Controllers\Api\Posts\PostShareController::class, 'shareArtwork'])
|
||||
->whereNumber('artwork_id')
|
||||
->middleware('throttle:30,1')
|
||||
->name('share.artwork');
|
||||
|
||||
// Reactions
|
||||
Route::post('{id}/reactions', [\App\Http\Controllers\Api\Posts\PostReactionController::class, 'store'])
|
||||
->whereNumber('id')
|
||||
->middleware('throttle:60,1')
|
||||
->name('reactions.store');
|
||||
Route::delete('{id}/reactions/{reaction}', [\App\Http\Controllers\Api\Posts\PostReactionController::class, 'destroy'])
|
||||
->whereNumber('id')
|
||||
->middleware('throttle:60,1')
|
||||
->name('reactions.destroy');
|
||||
|
||||
// Comments
|
||||
Route::post('{id}/comments', [\App\Http\Controllers\Api\Posts\PostCommentController::class, 'store'])->whereNumber('id')->name('comments.store');
|
||||
Route::delete('{id}/comments/{comment_id}', [\App\Http\Controllers\Api\Posts\PostCommentController::class, 'destroy'])->whereNumber(['id', 'comment_id'])->name('comments.destroy');
|
||||
|
||||
// Reports
|
||||
Route::post('{id}/report', [\App\Http\Controllers\Api\Posts\PostReportController::class, 'store'])
|
||||
->whereNumber('id')
|
||||
->middleware('throttle:10,1')
|
||||
->name('report');
|
||||
|
||||
// ── Feed 2.0 ───────────────────────────────────────────────────────
|
||||
|
||||
// Pinned posts
|
||||
Route::post('{id}/pin', [\App\Http\Controllers\Api\Posts\PostPinController::class, 'pin'])->whereNumber('id')->name('pin');
|
||||
Route::delete('{id}/pin', [\App\Http\Controllers\Api\Posts\PostPinController::class, 'unpin'])->whereNumber('id')->name('unpin');
|
||||
|
||||
// Saves / bookmarks
|
||||
Route::post('{id}/save', [\App\Http\Controllers\Api\Posts\PostSaveController::class, 'save'])->whereNumber('id')->middleware('throttle:60,1')->name('save');
|
||||
Route::delete('{id}/save', [\App\Http\Controllers\Api\Posts\PostSaveController::class, 'unsave'])->whereNumber('id')->middleware('throttle:60,1')->name('unsave');
|
||||
Route::get('saved', [\App\Http\Controllers\Api\Posts\PostSaveController::class, 'index'])->middleware('throttle:60,1')->name('saved');
|
||||
|
||||
// Analytics
|
||||
Route::post('{id}/impression', [\App\Http\Controllers\Api\Posts\PostAnalyticsController::class, 'impression'])->whereNumber('id')->middleware('throttle:120,1')->name('impression');
|
||||
Route::get('{id}/analytics', [\App\Http\Controllers\Api\Posts\PostAnalyticsController::class, 'show'])->whereNumber('id')->name('analytics');
|
||||
|
||||
// Comment highlight
|
||||
Route::post('{post_id}/comments/{comment_id}/highlight', [\App\Http\Controllers\Api\Posts\PostCommentHighlightController::class, 'highlight'])->whereNumber(['post_id','comment_id'])->name('comments.highlight');
|
||||
Route::delete('{post_id}/comments/{comment_id}/highlight', [\App\Http\Controllers\Api\Posts\PostCommentHighlightController::class, 'unhighlight'])->whereNumber(['post_id','comment_id'])->name('comments.unhighlight');
|
||||
});
|
||||
|
||||
// ── Feed 2.0: Trending + Hashtag + Search (public) ───────────────────────────
|
||||
Route::middleware(['web', 'throttle:60,1'])
|
||||
->prefix('feed')
|
||||
->name('api.feed.')
|
||||
->group(function () {
|
||||
Route::get('trending', [\App\Http\Controllers\Api\Posts\PostTrendingFeedController::class, 'trending'])->name('trending');
|
||||
Route::get('hashtag/{tag}', [\App\Http\Controllers\Api\Posts\PostTrendingFeedController::class, 'hashtag'])->name('hashtag');
|
||||
Route::get('hashtags/trending', [\App\Http\Controllers\Api\Posts\PostTrendingFeedController::class, 'trendingHashtags'])->name('hashtags.trending');
|
||||
Route::get('search', [\App\Http\Controllers\Api\Posts\PostSearchController::class, 'search'])->name('search');
|
||||
});
|
||||
|
||||
// ── Notifications (digest) ────────────────────────────────────────────────────
|
||||
Route::middleware(['web', 'auth'])
|
||||
->prefix('notifications')
|
||||
->name('api.notifications.')
|
||||
->group(function () {
|
||||
Route::get('/', [\App\Http\Controllers\Api\NotificationController::class, 'index'])->middleware('throttle:30,1')->name('index');
|
||||
Route::post('read-all', [\App\Http\Controllers\Api\NotificationController::class, 'readAll'])->name('read-all');
|
||||
Route::post('{id}/read', [\App\Http\Controllers\Api\NotificationController::class, 'markRead'])->name('mark-read');
|
||||
});
|
||||
|
||||
// ── Artwork search for share modal (public, throttled) ────────────────────────
|
||||
// GET /api/search/artworks?q=...&shareable=1 → reuses existing ArtworkSearchController
|
||||
|
||||
@@ -87,6 +87,20 @@ Schedule::job(new \App\Jobs\RecComputeSimilarHybridJob())
|
||||
->name('rec-compute-hybrid')
|
||||
->withoutOverlapping();
|
||||
|
||||
// ── Feed 2.0: Scheduled Posts ─────────────────────────────────────────────────
|
||||
// Publish queued posts every minute.
|
||||
Schedule::command('posts:publish-scheduled')
|
||||
->everyMinute()
|
||||
->name('publish-scheduled-posts')
|
||||
->withoutOverlapping();
|
||||
|
||||
// ── Feed 2.0: Trending Cache Warm-up ─────────────────────────────────────────
|
||||
// Warm the post trending cache every 2 minutes (complements the 2-min TTL).
|
||||
Schedule::command('posts:warm-trending')
|
||||
->everyTwoMinutes()
|
||||
->name('warm-post-trending')
|
||||
->withoutOverlapping();
|
||||
|
||||
// ── Ranking Engine V2 ──────────────────────────────────────────────────────────
|
||||
// Recalculate ranking_score + engagement_velocity every 30 minutes.
|
||||
// Also syncs V2 scores to rank_artwork_scores so list builds benefit.
|
||||
|
||||
@@ -398,3 +398,27 @@ Route::middleware(['auth', 'ensure.onboarding.complete'])->prefix('messages')->n
|
||||
// ── Community Activity Feed ───────────────────────────────────────────────────
|
||||
Route::get('/community/activity', [\App\Http\Controllers\Web\CommunityActivityController::class, 'index'])
|
||||
->name('community.activity');
|
||||
|
||||
// ── Posts / Following Feed ────────────────────────────────────────────────────
|
||||
// /feed/following – Inertia page for the ranked, diversified following feed
|
||||
Route::middleware(['auth', 'ensure.onboarding.complete'])
|
||||
->get('/feed/following', [\App\Http\Controllers\Web\Posts\FollowingFeedController::class, 'index'])
|
||||
->name('feed.following');
|
||||
|
||||
// ── Feed 2.0: Trending Feed ───────────────────────────────────────────────────
|
||||
Route::get('/feed/trending', [\App\Http\Controllers\Web\Posts\TrendingFeedController::class, 'index'])
|
||||
->name('feed.trending');
|
||||
|
||||
// ── Feed 2.0: Hashtag Feed ────────────────────────────────────────────────────
|
||||
Route::get('/tags/{tag}', [\App\Http\Controllers\Web\Posts\HashtagFeedController::class, 'index'])
|
||||
->where('tag', '[A-Za-z][A-Za-z0-9_]{1,63}')
|
||||
->name('feed.hashtag');
|
||||
|
||||
// ── Feed 2.0: Saved Posts ─────────────────────────────────────────────────────
|
||||
Route::middleware(['auth'])
|
||||
->get('/feed/saved', [\App\Http\Controllers\Web\Posts\SavedFeedController::class, 'index'])
|
||||
->name('feed.saved');
|
||||
|
||||
// ── Feed 2.0: Post Search ─────────────────────────────────────────────────────
|
||||
Route::get('/feed/search', [\App\Http\Controllers\Web\Posts\SearchFeedController::class, 'index'])
|
||||
->name('feed.search');
|
||||
|
||||
Reference in New Issue
Block a user