243 lines
10 KiB
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);
|
|
}
|
|
} |