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:
2026-03-03 09:48:31 +01:00
parent 1266f81d35
commit dc51d65440
178 changed files with 14308 additions and 665 deletions

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Http\Controllers\Api\Posts;
use App\Http\Controllers\Controller;
use App\Models\Post;
use App\Models\PostReaction;
use App\Services\Posts\PostCountersService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
class PostReactionController extends Controller
{
public function __construct(private PostCountersService $counters) {}
/**
* POST /api/posts/{id}/reactions
* payload: { reaction: 'like' }
*/
public function store(Request $request, int $id): JsonResponse
{
$user = $request->user();
$key = 'react_post:' . $user->id;
if (RateLimiter::tooManyAttempts($key, 60)) {
return response()->json(['message' => 'Too many reactions. Please slow down.'], 429);
}
RateLimiter::hit($key, 3600);
$post = Post::findOrFail($id);
$reaction = $request->input('reaction', 'like');
$existing = PostReaction::where('post_id', $post->id)
->where('user_id', $user->id)
->where('reaction', $reaction)
->first();
if ($existing) {
return response()->json(['message' => 'Already reacted.', 'reactions_count' => $post->reactions_count], 200);
}
PostReaction::create([
'post_id' => $post->id,
'user_id' => $user->id,
'reaction' => $reaction,
]);
$this->counters->incrementReactions($post);
$post->refresh();
return response()->json(['reactions_count' => $post->reactions_count, 'viewer_liked' => true], 201);
}
/**
* DELETE /api/posts/{id}/reactions/{reaction}
*/
public function destroy(Request $request, int $id, string $reaction = 'like'): JsonResponse
{
$user = $request->user();
$post = Post::findOrFail($id);
$deleted = PostReaction::where('post_id', $post->id)
->where('user_id', $user->id)
->where('reaction', $reaction)
->delete();
if ($deleted) {
$this->counters->decrementReactions($post);
$post->refresh();
}
return response()->json(['reactions_count' => $post->reactions_count, 'viewer_liked' => false]);
}
}