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,81 @@
<?php
namespace App\Services\Posts;
use App\Models\Post;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
/**
* Tracks post impressions (throttled per session) and computes engagement score.
*
* Impression throttle: 1 impression per post per session-key per hour.
* Engagement score: (reactions*2 + comments*3 + saves) / max(impressions, 1)
*/
class PostAnalyticsService
{
/**
* Record a post impression, throttled by a session key.
* Returns true if impression was counted, false if throttled.
*/
public function trackImpression(Post $post, string $sessionKey): bool
{
$cacheKey = "impression:{$post->id}:{$sessionKey}";
if (Cache::has($cacheKey)) {
return false; // already counted this hour
}
Cache::put($cacheKey, 1, now()->addHour());
Post::withoutTimestamps(function () use ($post) {
DB::table('posts')
->where('id', $post->id)
->increment('impressions_count');
});
// Recompute engagement score asynchronously via a quick DB update
$this->refreshEngagementScore($post->id);
return true;
}
/**
* Refresh the cached engagement_score = (reactions*2 + comments*3 + saves) / max(impressions, 1)
*/
public function refreshEngagementScore(int $postId): void
{
Post::withoutTimestamps(function () use ($postId) {
DB::table('posts')
->where('id', $postId)
->update([
'engagement_score' => DB::raw(
'(reactions_count * 2 + comments_count * 3 + saves_count) / GREATEST(impressions_count, 1)'
),
]);
});
}
/**
* Return analytics summary for a post (owner view).
*/
public function getSummary(Post $post): array
{
$reactions = $post->reactions_count;
$comments = $post->comments_count;
$saves = $post->saves_count;
$impressions = $post->impressions_count;
$rate = $impressions > 0
? round((($reactions + $comments + $saves) / $impressions) * 100, 2)
: 0.0;
return [
'impressions' => $impressions,
'reactions' => $reactions,
'comments' => $comments,
'saves' => $saves,
'engagement_rate' => $rate, // percentage
'engagement_score' => round($post->engagement_score, 4),
];
}
}