Files
SkinbaseNova/app/Services/Studio/CreatorStudioGrowthService.php

243 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Studio;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use function collect;
final class CreatorStudioGrowthService
{
public function __construct(
private readonly CreatorStudioAnalyticsService $analytics,
private readonly CreatorStudioContentService $content,
private readonly CreatorStudioPreferenceService $preferences,
) {
}
public function build(User $user, int $days = 30): array
{
$analytics = $this->analytics->overview($user, $days);
$preferences = $this->preferences->forUser($user);
$user->loadMissing(['profile', 'statistics']);
$socialLinksCount = (int) DB::table('user_social_links')->where('user_id', $user->id)->count();
$publishedInRange = (int) collect($analytics['comparison'])->sum('published_count');
$challengeEntries = (int) DB::table('nova_cards')
->where('user_id', $user->id)
->whereNull('deleted_at')
->sum('challenge_entries_count');
$profileScore = $this->profileScore($user, $socialLinksCount);
$cadenceScore = $this->ratioScore($publishedInRange, 6);
$engagementScore = $this->ratioScore(
(int) ($analytics['totals']['appreciation'] ?? 0)
+ (int) ($analytics['totals']['comments'] ?? 0)
+ (int) ($analytics['totals']['shares'] ?? 0)
+ (int) ($analytics['totals']['saves'] ?? 0),
max(3, (int) ($analytics['totals']['content_count'] ?? 0) * 12)
);
$curationScore = $this->ratioScore(count($preferences['featured_modules']), 4);
return [
'summary' => [
'followers' => (int) ($analytics['totals']['followers'] ?? 0),
'published_in_range' => $publishedInRange,
'engagement_actions' => (int) ($analytics['totals']['appreciation'] ?? 0)
+ (int) ($analytics['totals']['comments'] ?? 0)
+ (int) ($analytics['totals']['shares'] ?? 0)
+ (int) ($analytics['totals']['saves'] ?? 0),
'profile_completion' => $profileScore,
'challenge_entries' => $challengeEntries,
'featured_modules' => count($preferences['featured_modules']),
],
'module_focus' => $this->moduleFocus($analytics),
'checkpoints' => [
$this->checkpoint('profile', 'Profile presentation', $profileScore, 'Profile, cover, and social details shape how new visitors read your work.', route('studio.profile'), 'Update profile'),
$this->checkpoint('cadence', 'Publishing cadence', $cadenceScore, sprintf('You published %d items in the last %d days.', $publishedInRange, $days), route('studio.calendar'), 'Open calendar'),
$this->checkpoint('engagement', 'Audience response', $engagementScore, 'Comments, reactions, saves, and shares show whether your output is creating momentum.', route('studio.inbox'), 'Open inbox'),
$this->checkpoint('curation', 'Featured curation', $curationScore, 'Featured modules make your strongest work easier to discover across the profile surface.', route('studio.featured'), 'Manage featured'),
$this->checkpoint('challenges', 'Challenge participation', $this->ratioScore($challengeEntries, 5), 'Challenge submissions create discoverable surfaces beyond your core publishing flow.', route('studio.challenges'), 'Open challenges'),
],
'opportunities' => $this->opportunities($profileScore, $publishedInRange, $challengeEntries, $preferences),
'milestones' => [
$this->milestone('followers', 'Follower milestone', (int) ($analytics['totals']['followers'] ?? 0), $this->nextMilestone((int) ($analytics['totals']['followers'] ?? 0), [10, 25, 50, 100, 250, 500, 1000, 2500, 5000])),
$this->milestone('publishing', sprintf('Published in %d days', $days), $publishedInRange, $this->nextMilestone($publishedInRange, [3, 5, 8, 12, 20, 30])),
$this->milestone('challenges', 'Challenge submissions', $challengeEntries, $this->nextMilestone($challengeEntries, [1, 3, 5, 10, 25, 50])),
],
'momentum' => [
'views_trend' => $analytics['views_trend'],
'engagement_trend' => $analytics['engagement_trend'],
'publishing_timeline' => $analytics['publishing_timeline'],
],
'top_content' => collect($analytics['top_content'])->take(5)->values()->all(),
'range_days' => $days,
];
}
private function moduleFocus(array $analytics): array
{
$totalViews = max(1, (int) ($analytics['totals']['views'] ?? 0));
$totalEngagement = max(1,
(int) ($analytics['totals']['appreciation'] ?? 0)
+ (int) ($analytics['totals']['comments'] ?? 0)
+ (int) ($analytics['totals']['shares'] ?? 0)
+ (int) ($analytics['totals']['saves'] ?? 0)
);
$comparison = collect($analytics['comparison'] ?? [])->keyBy('key');
return collect($analytics['module_breakdown'] ?? [])
->map(function (array $item) use ($comparison, $totalViews, $totalEngagement): array {
$engagementValue = (int) ($item['appreciation'] ?? 0)
+ (int) ($item['comments'] ?? 0)
+ (int) ($item['shares'] ?? 0)
+ (int) ($item['saves'] ?? 0);
$publishedCount = (int) ($comparison->get($item['key'])['published_count'] ?? 0);
return [
'key' => $item['key'],
'label' => $item['label'],
'icon' => $item['icon'],
'views' => (int) ($item['views'] ?? 0),
'engagement' => $engagementValue,
'published_count' => $publishedCount,
'draft_count' => (int) ($item['draft_count'] ?? 0),
'view_share' => (int) round(((int) ($item['views'] ?? 0) / $totalViews) * 100),
'engagement_share' => (int) round(($engagementValue / $totalEngagement) * 100),
'href' => $item['index_url'],
];
})
->values()
->all();
}
private function opportunities(int $profileScore, int $publishedInRange, int $challengeEntries, array $preferences): array
{
$items = [];
if ($profileScore < 80) {
$items[] = [
'title' => 'Tighten creator presentation',
'body' => 'A more complete profile helps new followers understand the work behind the metrics.',
'href' => route('studio.profile'),
'cta' => 'Update profile',
];
}
if ($publishedInRange < 3) {
$items[] = [
'title' => 'Increase publishing cadence',
'body' => 'The calendar is still the clearest place to turn draft backlog into visible output.',
'href' => route('studio.calendar'),
'cta' => 'Plan schedule',
];
}
if (count($preferences['featured_modules'] ?? []) < 3) {
$items[] = [
'title' => 'Expand featured module coverage',
'body' => 'Featured content gives your strongest modules a cleaner discovery path from the public profile.',
'href' => route('studio.featured'),
'cta' => 'Manage featured',
];
}
if ($challengeEntries === 0) {
$items[] = [
'title' => 'Use challenges as a growth surface',
'body' => 'Challenge runs create another discovery path for cards beyond your normal publishing feed.',
'href' => route('studio.challenges'),
'cta' => 'Review challenges',
];
}
$items[] = [
'title' => 'Stay close to response signals',
'body' => 'Inbox and comments are still the fastest route from passive reach to actual creator retention.',
'href' => route('studio.inbox'),
'cta' => 'Open inbox',
];
return collect($items)->take(4)->values()->all();
}
private function checkpoint(string $key, string $label, int $score, string $detail, string $href, string $cta): array
{
return [
'key' => $key,
'label' => $label,
'score' => $score,
'status' => $score >= 80 ? 'strong' : ($score >= 55 ? 'building' : 'needs_attention'),
'detail' => $detail,
'href' => $href,
'cta' => $cta,
];
}
private function milestone(string $key, string $label, int $current, int $target): array
{
return [
'key' => $key,
'label' => $label,
'current' => $current,
'target' => $target,
'progress' => $target > 0 ? min(100, (int) round(($current / $target) * 100)) : 100,
];
}
private function ratioScore(int $current, int $target): int
{
if ($target <= 0) {
return 100;
}
return max(0, min(100, (int) round(($current / $target) * 100)));
}
private function nextMilestone(int $current, array $steps): int
{
foreach ($steps as $step) {
if ($current < $step) {
return $step;
}
}
return max($current, 1);
}
private function profileScore(User $user, int $socialLinksCount): int
{
$score = 0;
if (filled($user->name)) {
$score += 15;
}
if (filled($user->profile?->description)) {
$score += 20;
}
if (filled($user->profile?->about)) {
$score += 20;
}
if (filled($user->profile?->website)) {
$score += 15;
}
if (filled($user->profile?->avatar_url)) {
$score += 10;
}
if (filled($user->cover_hash) && filled($user->cover_ext)) {
$score += 10;
}
if ($socialLinksCount > 0) {
$score += 10;
}
return min(100, $score);
}
}