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:
81
app/Services/Posts/PostAnalyticsService.php
Normal file
81
app/Services/Posts/PostAnalyticsService.php
Normal 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),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user