Save workspace changes

This commit is contained in:
2026-04-18 17:02:56 +02:00
parent f02ea9a711
commit 87d60af5a9
4220 changed files with 1388603 additions and 1554 deletions

View File

@@ -12,10 +12,13 @@ use App\Models\Story;
use App\Models\StoryLike;
use App\Models\StoryView;
use App\Models\User;
use App\Models\World;
use App\Models\WorldSubmission;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class LeaderboardService
{
@@ -68,6 +71,16 @@ class LeaderboardService
return $this->persistRows(Leaderboard::TYPE_GROUP, $normalizedPeriod, $rows, self::ENTITY_STORE_LIMIT);
}
public function calculateWorldLeaderboard(string $period): int
{
$normalizedPeriod = $this->normalizePeriod($period);
$rows = $normalizedPeriod === Leaderboard::PERIOD_ALL_TIME
? $this->allTimeWorldRows()
: $this->windowedWorldRows($this->periodStart($normalizedPeriod));
return $this->persistRows(Leaderboard::TYPE_WORLD, $normalizedPeriod, $rows, self::ENTITY_STORE_LIMIT);
}
public function refreshAll(): array
{
$results = [];
@@ -77,6 +90,7 @@ class LeaderboardService
Leaderboard::TYPE_ARTWORK,
Leaderboard::TYPE_GROUP,
Leaderboard::TYPE_STORY,
Leaderboard::TYPE_WORLD,
] as $type) {
foreach ($this->periods() as $period) {
$results[$type][$period] = match ($type) {
@@ -84,6 +98,7 @@ class LeaderboardService
Leaderboard::TYPE_ARTWORK => $this->calculateArtworkLeaderboard($period),
Leaderboard::TYPE_GROUP => $this->calculateGroupLeaderboard($period),
Leaderboard::TYPE_STORY => $this->calculateStoryLeaderboard($period),
Leaderboard::TYPE_WORLD => $this->calculateWorldLeaderboard($period),
};
}
}
@@ -121,6 +136,7 @@ class LeaderboardService
Leaderboard::TYPE_ARTWORK => $this->artworkEntities($items->pluck('entity_id')->all()),
Leaderboard::TYPE_GROUP => $this->groupEntities($items->pluck('entity_id')->all()),
Leaderboard::TYPE_STORY => $this->storyEntities($items->pluck('entity_id')->all()),
Leaderboard::TYPE_WORLD => $this->worldEntities($items->pluck('entity_id')->all()),
};
return [
@@ -205,6 +221,7 @@ class LeaderboardService
'artwork', 'artworks' => Leaderboard::TYPE_ARTWORK,
'group', 'groups' => Leaderboard::TYPE_GROUP,
'story', 'stories' => Leaderboard::TYPE_STORY,
'world', 'worlds' => Leaderboard::TYPE_WORLD,
default => Leaderboard::TYPE_CREATOR,
};
}
@@ -228,6 +245,7 @@ class LeaderboardService
Leaderboard::TYPE_ARTWORK => $this->calculateArtworkLeaderboard($period),
Leaderboard::TYPE_GROUP => $this->calculateGroupLeaderboard($period),
Leaderboard::TYPE_STORY => $this->calculateStoryLeaderboard($period),
Leaderboard::TYPE_WORLD => $this->calculateWorldLeaderboard($period),
};
}
@@ -585,6 +603,128 @@ class LeaderboardService
->values();
}
private function allTimeWorldRows(): Collection
{
if (! $this->worldTablesExist()) {
return collect();
}
return World::query()
->from('worlds')
->withCount([
'worldRelations as relations_count',
'worldRelations as featured_relations_count' => fn ($query) => $query->where('is_featured', true),
'worldSubmissions as approved_submissions_count' => fn ($query) => $query->where('status', WorldSubmission::STATUS_LIVE),
'worldSubmissions as featured_submissions_count' => fn ($query) => $query->where('status', WorldSubmission::STATUS_LIVE)->where('is_featured', true),
])
->publiclyVisible()
->get()
->map(fn (World $world): array => [
'entity_id' => (int) $world->id,
'score' => $this->scoreWorld(
(int) ($world->relations_count ?? 0),
(int) ($world->featured_relations_count ?? 0),
(int) ($world->approved_submissions_count ?? 0),
(int) ($world->featured_submissions_count ?? 0),
$world->isCurrent(),
(bool) $world->is_featured,
false,
),
])
->filter(fn (array $row): bool => $row['score'] > 0)
->values();
}
private function windowedWorldRows(CarbonImmutable $start): Collection
{
if (! $this->worldTablesExist()) {
return collect();
}
$relations = DB::table('world_relations')
->select('world_id', DB::raw('COUNT(*) as relations_count'))
->where('created_at', '>=', $start)
->groupBy('world_id');
$featuredRelations = DB::table('world_relations')
->select('world_id', DB::raw('COUNT(*) as featured_relations_count'))
->where('created_at', '>=', $start)
->where('is_featured', true)
->groupBy('world_id');
$approvedSubmissions = DB::table('world_submissions')
->select('world_id', DB::raw('COUNT(*) as approved_submissions_count'))
->where('status', WorldSubmission::STATUS_LIVE)
->where(function ($query) use ($start): void {
$query->where('reviewed_at', '>=', $start)
->orWhere(function ($fallback) use ($start): void {
$fallback->whereNull('reviewed_at')
->where('created_at', '>=', $start);
});
})
->groupBy('world_id');
$featuredSubmissions = DB::table('world_submissions')
->select('world_id', DB::raw('COUNT(*) as featured_submissions_count'))
->where('status', WorldSubmission::STATUS_LIVE)
->where('is_featured', true)
->where(function ($query) use ($start): void {
$query->where('reviewed_at', '>=', $start)
->orWhere(function ($fallback) use ($start): void {
$fallback->whereNull('reviewed_at')
->where('created_at', '>=', $start);
});
})
->groupBy('world_id');
return World::query()
->from('worlds')
->leftJoinSub($relations, 'relations', 'relations.world_id', '=', 'worlds.id')
->leftJoinSub($featuredRelations, 'featured_relations', 'featured_relations.world_id', '=', 'worlds.id')
->leftJoinSub($approvedSubmissions, 'approved_submissions', 'approved_submissions.world_id', '=', 'worlds.id')
->leftJoinSub($featuredSubmissions, 'featured_submissions', 'featured_submissions.world_id', '=', 'worlds.id')
->publiclyVisible()
->select([
'worlds.id',
'worlds.status',
'worlds.starts_at',
'worlds.ends_at',
'worlds.published_at',
'worlds.is_featured',
DB::raw('COALESCE(relations.relations_count, 0) as relations_count'),
DB::raw('COALESCE(featured_relations.featured_relations_count, 0) as featured_relations_count'),
DB::raw('COALESCE(approved_submissions.approved_submissions_count, 0) as approved_submissions_count'),
DB::raw('COALESCE(featured_submissions.featured_submissions_count, 0) as featured_submissions_count'),
DB::raw("CASE WHEN worlds.published_at >= '" . $start->toDateTimeString() . "' OR worlds.starts_at >= '" . $start->toDateTimeString() . "' THEN 1 ELSE 0 END as recent_launch_bonus"),
])
->get()
->map(function ($row): array {
$world = new World();
$world->forceFill([
'status' => (string) $row->status,
'starts_at' => $row->starts_at,
'ends_at' => $row->ends_at,
'published_at' => $row->published_at,
'is_featured' => (bool) $row->is_featured,
]);
return [
'entity_id' => (int) $row->id,
'score' => $this->scoreWorld(
(int) $row->relations_count,
(int) $row->featured_relations_count,
(int) $row->approved_submissions_count,
(int) $row->featured_submissions_count,
$world->isCurrent(),
(bool) $row->is_featured,
(bool) $row->recent_launch_bonus,
),
];
})
->filter(fn (array $row): bool => $row['score'] > 0)
->values();
}
private function windowedGroupRows(CarbonImmutable $start): Collection
{
$follows = DB::table('group_follows')
@@ -819,4 +959,95 @@ class LeaderboardService
})
->all();
}
private function worldEntities(array $ids): array
{
if (! $this->worldTablesExist()) {
return [];
}
return World::query()
->withCount([
'worldRelations as relations_count',
'worldSubmissions as approved_submissions_count' => fn ($query) => $query->where('status', WorldSubmission::STATUS_LIVE),
'worldSubmissions as featured_submissions_count' => fn ($query) => $query->where('status', WorldSubmission::STATUS_LIVE)->where('is_featured', true),
])
->whereIn('id', $ids)
->publiclyVisible()
->get()
->mapWithKeys(function (World $world): array {
return [
(int) $world->id => [
'id' => (int) $world->id,
'type' => Leaderboard::TYPE_WORLD,
'name' => (string) $world->title,
'summary' => (string) ($world->summary ?: $world->tagline ?: ''),
'url' => $world->publicUrl(),
'image' => $world->coverUrl(),
'timeframe_label' => $this->worldTimeframeLabel($world),
'badge_label' => (string) ($world->badge_label ?? ''),
'phase' => $world->isCurrent() ? 'active' : ((string) $world->status === World::STATUS_ARCHIVED ? 'archive' : 'published'),
'icon_name' => (string) ($world->icon_name ?: 'fa-solid fa-globe'),
'theme_label' => $this->worldThemeLabel($world),
'relations_count' => (int) ($world->relations_count ?? 0),
'approved_submissions_count' => (int) ($world->approved_submissions_count ?? 0),
'featured_submissions_count' => (int) ($world->featured_submissions_count ?? 0),
'is_featured' => (bool) $world->is_featured,
],
];
})
->all();
}
private function scoreWorld(
int $relationsCount,
int $featuredRelationsCount,
int $approvedSubmissionsCount,
int $featuredSubmissionsCount,
bool $isCurrent,
bool $isFeatured,
bool $isRecentLaunch,
): int {
return ($relationsCount * 22)
+ ($featuredRelationsCount * 10)
+ ($approvedSubmissionsCount * 16)
+ ($featuredSubmissionsCount * 26)
+ ($isCurrent ? 48 : 0)
+ ($isFeatured ? 70 : 0)
+ ($isRecentLaunch ? 24 : 0);
}
private function worldTablesExist(): bool
{
return Schema::hasTable('worlds')
&& Schema::hasTable('world_relations')
&& Schema::hasTable('world_submissions');
}
private function worldThemeLabel(World $world): string
{
return match ((string) $world->type) {
World::TYPE_EVENT => 'Event',
World::TYPE_CAMPAIGN => 'Campaign',
World::TYPE_TRIBUTE => 'Tribute',
default => 'Seasonal',
};
}
private function worldTimeframeLabel(World $world): ?string
{
if ($world->starts_at && $world->ends_at) {
return $world->starts_at->format('M j, Y') . ' - ' . $world->ends_at->format('M j, Y');
}
if ($world->starts_at) {
return 'Starts ' . $world->starts_at->format('M j, Y');
}
if ($world->ends_at) {
return 'Ends ' . $world->ends_at->format('M j, Y');
}
return null;
}
}