bottom chart and responsive changes

This commit is contained in:
itsAzaria
2026-02-20 20:55:58 +00:00
parent ee3b46e468
commit a0c65ca3d7
6 changed files with 278 additions and 192 deletions
@@ -61,6 +61,8 @@ class HomepageController
'top_movers' => $topMovers, 'top_movers' => $topMovers,
'top_static' => $aggregate['top_static'] ?? collect(), 'top_static' => $aggregate['top_static'] ?? collect(),
'top_animated' => $aggregate['top_animated'] ?? collect(), 'top_animated' => $aggregate['top_animated'] ?? collect(),
'bottom_static' => $aggregate['bottom_static'] ?? collect(),
'bottom_animated' => $aggregate['bottom_animated'] ?? collect(),
'top_unicode' => $aggregate['top_unicode'] ?? collect(), 'top_unicode' => $aggregate['top_unicode'] ?? collect(),
]; ];
} }
@@ -0,0 +1,34 @@
<?php
namespace App\Models\Concerns;
trait BuildsEmoteAggregates
{
protected static function aggregateSummaryAndUsageByType(
$baseQuery,
string $usageCountColumn,
string $emoteIdColumn,
?string $userIdColumn = null
): array {
$summaryQuery = (clone $baseQuery)
->selectRaw("COALESCE(SUM({$usageCountColumn}), 0) as total_usage")
->selectRaw("COUNT(DISTINCT {$emoteIdColumn}) as unique_emotes");
if ($userIdColumn !== null) {
$summaryQuery->selectRaw("COUNT(DISTINCT {$userIdColumn}) as unique_users");
}
$summary = $summaryQuery->first();
$usageByType = (clone $baseQuery)
->select('emotes.type')
->selectRaw("COALESCE(SUM({$usageCountColumn}), 0) as total_usage")
->groupBy('emotes.type')
->pluck('total_usage', 'type');
return [
'summary' => $summary,
'usage_by_type' => $usageByType,
];
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\Models\Concerns;
use Illuminate\Support\Collection;
trait BuildsEmoteUsageLists
{
protected static function emotesByType(
$baseQuery,
string $type,
string $usageCountColumn,
string $emoteIdColumn,
string $direction = 'desc'
): Collection {
$query = (clone $baseQuery)
->where('emotes.type', $type)
->select(
$emoteIdColumn.' as emote_id',
'emotes.emote_name',
'emotes.type',
'emotes.image'
)
->selectRaw("COALESCE(SUM({$usageCountColumn}), 0) as total_usage")
->groupBy(
$emoteIdColumn,
'emotes.emote_name',
'emotes.type',
'emotes.image'
);
if ($direction === 'asc') {
$query->orderBy('total_usage');
} else {
$query->orderByDesc('total_usage');
}
return $query
->limit(10)
->get();
}
}
+32 -46
View File
@@ -2,11 +2,15 @@
namespace App\Models; namespace App\Models;
use App\Models\Concerns\BuildsEmoteAggregates;
use App\Models\Concerns\BuildsEmoteUsageLists;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class EmoteGuildStat extends Model class EmoteGuildStat extends Model
{ {
use BuildsEmoteAggregates;
use BuildsEmoteUsageLists;
protected $fillable = [ protected $fillable = [
'emote_id', 'emote_id',
'guild_id', 'guild_id',
@@ -27,24 +31,19 @@ class EmoteGuildStat extends Model
); );
} }
/**
* Build aggregate stats for all users.
*/
public static function dashboardAggregate(): array public static function dashboardAggregate(): array
{ {
$baseQuery = self::query() $baseQuery = self::query()
->join('emotes', 'emote_guild_stats.emote_id', '=', 'emotes.emote_id'); ->join('emotes', 'emote_guild_stats.emote_id', '=', 'emotes.emote_id');
$summary = (clone $baseQuery) $aggregate = self::aggregateSummaryAndUsageByType(
->selectRaw('COALESCE(SUM(emote_guild_stats.usage_count), 0) as total_usage') $baseQuery,
->selectRaw('COUNT(DISTINCT emote_guild_stats.emote_id) as unique_emotes') 'emote_guild_stats.usage_count',
->first(); 'emote_guild_stats.emote_id'
);
$usageByType = (clone $baseQuery) $summary = $aggregate['summary'];
->select('emotes.type') $usageByType = $aggregate['usage_by_type'];
->selectRaw('COALESCE(SUM(emote_guild_stats.usage_count), 0) as total_usage')
->groupBy('emotes.type')
->pluck('total_usage', 'type');
$unicodeUsage = (int) EmoteLog::query() $unicodeUsage = (int) EmoteLog::query()
->join('emotes', 'emote_logs.emote_id', '=', 'emotes.emote_id') ->join('emotes', 'emote_logs.emote_id', '=', 'emotes.emote_id')
@@ -88,48 +87,35 @@ class EmoteGuildStat extends Model
'ANIMATED' => (int) ($usageByType->get('ANIMATED') ?? 0), 'ANIMATED' => (int) ($usageByType->get('ANIMATED') ?? 0),
'UNICODE' => $unicodeUsage, 'UNICODE' => $unicodeUsage,
], ],
'top_static' => self::topEmotesByType( 'top_static' => self::emotesByType(
$baseQuery, $baseQuery,
'STATIC', 'STATIC',
'emote_guild_stats.usage_count', 'emote_guild_stats.usage_count',
'emote_guild_stats.emote_id' 'emote_guild_stats.emote_id',
'desc'
), ),
'top_animated' => self::topEmotesByType( 'top_animated' => self::emotesByType(
$baseQuery, $baseQuery,
'ANIMATED', 'ANIMATED',
'emote_guild_stats.usage_count', 'emote_guild_stats.usage_count',
'emote_guild_stats.emote_id' 'emote_guild_stats.emote_id',
'desc'
),
'bottom_static' => self::emotesByType(
$baseQuery,
'STATIC',
'emote_guild_stats.usage_count',
'emote_guild_stats.emote_id',
'asc'
),
'bottom_animated' => self::emotesByType(
$baseQuery,
'ANIMATED',
'emote_guild_stats.usage_count',
'emote_guild_stats.emote_id',
'asc'
), ),
'top_unicode' => $topUnicode, 'top_unicode' => $topUnicode,
]; ];
} }
/**
* Build a top-10 emote list by type.
*/
protected static function topEmotesByType(
$baseQuery,
string $type,
string $usageCountColumn,
string $emoteIdColumn
): Collection {
return (clone $baseQuery)
->where('emotes.type', $type)
->select(
$emoteIdColumn.' as emote_id',
'emotes.emote_name',
'emotes.type',
'emotes.image'
)
->selectRaw("COALESCE(SUM({$usageCountColumn}), 0) as total_usage")
->groupBy(
$emoteIdColumn,
'emotes.emote_name',
'emotes.type',
'emotes.image'
)
->orderByDesc('total_usage')
->limit(10)
->get();
}
} }
+36 -49
View File
@@ -2,11 +2,15 @@
namespace App\Models; namespace App\Models;
use App\Models\Concerns\BuildsEmoteAggregates;
use App\Models\Concerns\BuildsEmoteUsageLists;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class UserGuildStat extends Model class UserGuildStat extends Model
{ {
use BuildsEmoteAggregates;
use BuildsEmoteUsageLists;
protected $fillable = [ protected $fillable = [
'user_id', 'user_id',
'guild_id', 'guild_id',
@@ -25,26 +29,21 @@ class UserGuildStat extends Model
return $this->belongsTo(User::class, 'user_id', 'discord_id'); return $this->belongsTo(User::class, 'user_id', 'discord_id');
} }
/**
* Build aggregate stats for a single user.
*/
public static function dashboardAggregateForUser(string $userId): array public static function dashboardAggregateForUser(string $userId): array
{ {
$baseQuery = self::query() $baseQuery = self::query()
->join('emotes', 'user_guild_stats.emote_id', '=', 'emotes.emote_id') ->join('emotes', 'user_guild_stats.emote_id', '=', 'emotes.emote_id')
->where('user_guild_stats.user_id', $userId); ->where('user_guild_stats.user_id', $userId);
$summary = (clone $baseQuery) $aggregate = self::aggregateSummaryAndUsageByType(
->selectRaw('COALESCE(SUM(user_guild_stats.usage_count), 0) as total_usage') $baseQuery,
->selectRaw('COUNT(DISTINCT user_guild_stats.emote_id) as unique_emotes') 'user_guild_stats.usage_count',
->selectRaw('COUNT(DISTINCT user_guild_stats.user_id) as unique_users') 'user_guild_stats.emote_id',
->first(); 'user_guild_stats.user_id'
);
$usageByType = (clone $baseQuery) $summary = $aggregate['summary'];
->select('emotes.type') $usageByType = $aggregate['usage_by_type'];
->selectRaw('COALESCE(SUM(user_guild_stats.usage_count), 0) as total_usage')
->groupBy('emotes.type')
->pluck('total_usage', 'type');
return [ return [
'total_usage' => (int) ($summary->total_usage ?? 0), 'total_usage' => (int) ($summary->total_usage ?? 0),
@@ -55,53 +54,41 @@ class UserGuildStat extends Model
'ANIMATED' => (int) ($usageByType->get('ANIMATED') ?? 0), 'ANIMATED' => (int) ($usageByType->get('ANIMATED') ?? 0),
'UNICODE' => (int) ($usageByType->get('UNICODE') ?? 0), 'UNICODE' => (int) ($usageByType->get('UNICODE') ?? 0),
], ],
'top_static' => self::topEmotesByType( 'top_static' => self::emotesByType(
$baseQuery, $baseQuery,
'STATIC', 'STATIC',
'user_guild_stats.usage_count', 'user_guild_stats.usage_count',
'user_guild_stats.emote_id' 'user_guild_stats.emote_id',
'desc'
), ),
'top_animated' => self::topEmotesByType( 'top_animated' => self::emotesByType(
$baseQuery, $baseQuery,
'ANIMATED', 'ANIMATED',
'user_guild_stats.usage_count', 'user_guild_stats.usage_count',
'user_guild_stats.emote_id' 'user_guild_stats.emote_id',
'desc'
), ),
'top_unicode' => self::topEmotesByType( 'bottom_static' => self::emotesByType(
$baseQuery,
'STATIC',
'user_guild_stats.usage_count',
'user_guild_stats.emote_id',
'asc'
),
'bottom_animated' => self::emotesByType(
$baseQuery,
'ANIMATED',
'user_guild_stats.usage_count',
'user_guild_stats.emote_id',
'asc'
),
'top_unicode' => self::emotesByType(
$baseQuery, $baseQuery,
'UNICODE', 'UNICODE',
'user_guild_stats.usage_count', 'user_guild_stats.usage_count',
'user_guild_stats.emote_id' 'user_guild_stats.emote_id',
'desc'
), ),
]; ];
} }
/**
* Build a top-10 emote list by type.
*/
protected static function topEmotesByType(
$baseQuery,
string $type,
string $usageCountColumn,
string $emoteIdColumn
): Collection {
return (clone $baseQuery)
->where('emotes.type', $type)
->select(
$emoteIdColumn.' as emote_id',
'emotes.emote_name',
'emotes.type',
'emotes.image'
)
->selectRaw("COALESCE(SUM({$usageCountColumn}), 0) as total_usage")
->groupBy(
$emoteIdColumn,
'emotes.emote_name',
'emotes.type',
'emotes.image'
)
->orderByDesc('total_usage')
->limit(10)
->get();
}
} }
+103 -68
View File
@@ -35,6 +35,29 @@
'values' => data_get($stats, 'usage_over_time.values', []), 'values' => data_get($stats, 'usage_over_time.values', []),
], ],
]; ];
$customUsageTables = [
[
'title' => 'Top 10 Static Emotes',
'list' => $stats['top_static'] ?? collect(),
'empty' => 'No static emote usage found.',
],
[
'title' => 'Top 10 Animated Emotes',
'list' => $stats['top_animated'] ?? collect(),
'empty' => 'No animated emote usage found.',
],
[
'title' => 'Bottom 10 Static Emotes',
'list' => $stats['bottom_static'] ?? collect(),
'empty' => 'No static emote usage found.',
],
[
'title' => 'Bottom 10 Animated Emotes',
'list' => $stats['bottom_animated'] ?? collect(),
'empty' => 'No animated emote usage found.',
],
];
@endphp @endphp
<x-layouts.app> <x-layouts.app>
@@ -69,36 +92,36 @@
@endif @endif
</div> </div>
<div class="mb-8 overflow-x-auto lg:overflow-visible"> <div class="mb-8">
<div class="flex gap-3 pb-1 min-w-max lg:min-w-0 lg:w-full lg:justify-between"> <div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6">
<div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-[180px] lg:min-w-0 lg:flex-1"> <div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-0">
<p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Total Emoji Usage</p> <p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Total Emoji Usage</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ $stats['total_usage'] ?? 0 }}</p> <p class="mt-2 text-2xl font-semibold text-white break-words">{{ $stats['total_usage'] ?? 0 }}</p>
</div> </div>
<div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-[180px] lg:min-w-0 lg:flex-1"> <div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-0">
<p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Unique Emotes</p> <p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Unique Emotes</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ $stats['unique_emotes'] ?? 0 }}</p> <p class="mt-2 text-2xl font-semibold text-white break-words">{{ $stats['unique_emotes'] ?? 0 }}</p>
</div> </div>
<div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-[180px] lg:min-w-0 lg:flex-1"> <div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-0">
<p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Users Included</p> <p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Users Included</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ $stats['unique_users'] ?? 0 }}</p> <p class="mt-2 text-2xl font-semibold text-white break-words">{{ $stats['unique_users'] ?? 0 }}</p>
</div> </div>
<div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-[180px] lg:min-w-0 lg:flex-1"> <div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-0">
<p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Static Usage</p> <p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Static Usage</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ $stats['usage_by_type']['STATIC'] ?? 0 }}</p> <p class="mt-2 text-2xl font-semibold text-white break-words">{{ $stats['usage_by_type']['STATIC'] ?? 0 }}</p>
</div> </div>
<div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-[180px] lg:min-w-0 lg:flex-1"> <div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-0">
<p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Animated Usage</p> <p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Animated Usage</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ $stats['usage_by_type']['ANIMATED'] ?? 0 }}</p> <p class="mt-2 text-2xl font-semibold text-white break-words">{{ $stats['usage_by_type']['ANIMATED'] ?? 0 }}</p>
</div> </div>
<div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-[180px] lg:min-w-0 lg:flex-1"> <div class="p-4 border rounded-lg bg-[#2b2d31] border-[#3f4147] min-w-0">
<p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Unicode Usage</p> <p class="text-xs font-medium uppercase tracking-wide text-[#b5bac1]">Unicode Usage</p>
<p class="mt-2 text-2xl font-semibold text-white">{{ $stats['usage_by_type']['UNICODE'] ?? 0 }}</p> <p class="mt-2 text-2xl font-semibold text-white break-words">{{ $stats['usage_by_type']['UNICODE'] ?? 0 }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -107,26 +130,29 @@
<div class="space-y-3"> <div class="space-y-3">
<h3 class="text-xl font-semibold text-white">Emote Usage Over Time (Last 30 Days)</h3> <h3 class="text-xl font-semibold text-white">Emote Usage Over Time (Last 30 Days)</h3>
<div class="p-6 border rounded-lg bg-[#2b2d31] border-[#3f4147] md:p-7"> <div class="p-4 overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147] md:p-5">
<canvas id="emoji-usage-over-time-chart"></canvas> <div class="h-44 min-w-0 sm:h-56 md:h-64 lg:h-72">
<canvas id="emoji-usage-over-time-chart" class="w-full h-full"></canvas>
</div>
</div> </div>
</div> </div>
<div class="grid gap-8 lg:grid-cols-2">
<div class="space-y-3"> <div class="space-y-3">
<h3 class="text-xl font-semibold text-white">Top Movers (Last 7 Days vs Previous 7 Days)</h3> <h3 class="text-xl font-semibold text-white">Top Movers (Last 7 Days vs Previous 7 Days)</h3>
<div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]"> <div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]">
@forelse (($stats['top_movers'] ?? collect()) as $emote) @forelse (($stats['top_movers'] ?? collect()) as $emote)
<div class="flex items-center justify-between px-4 py-3.5 border-b border-[#3f4147] last:border-b-0"> <div class="flex flex-col items-start gap-2 px-4 py-3.5 border-b border-[#3f4147] sm:flex-row sm:items-center sm:justify-between sm:gap-3 last:border-b-0">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3 min-w-0">
@if (($emote->type ?? null) === 'UNICODE') @if (($emote->type ?? null) === 'UNICODE')
<span class="font-medium text-[#dbdee1]">{{ $emote->emote_name }}</span> <span class="font-medium truncate text-[#dbdee1]">{{ $emote->emote_name }}</span>
@else @else
<img src="{{ $emoteImageSrc($emote->image) }}" alt="{{ $emote->emote_name }}" class="object-cover w-9 h-9 border rounded border-[#3f4147]" /> <img src="{{ $emoteImageSrc($emote->image) }}" alt="{{ $emote->emote_name }}" class="object-cover w-9 h-9 border rounded border-[#3f4147]" />
<span class="font-medium text-[#dbdee1]">{{ $emote->emote_name }}</span> <span class="font-medium truncate text-[#dbdee1]">{{ $emote->emote_name }}</span>
@endif @endif
</div> </div>
<div class="text-right"> <div class="text-left sm:text-right sm:shrink-0">
<p class="text-sm text-[#b5bac1]">{{ $emote->previous_count }} {{ $emote->current_count }}</p> <p class="text-sm text-[#b5bac1]">{{ $emote->previous_count }} {{ $emote->current_count }}</p>
<p class="text-sm font-medium {{ $emote->delta >= 0 ? 'text-[#57f287]' : 'text-[#ed4245]' }}"> <p class="text-sm font-medium {{ $emote->delta >= 0 ? 'text-[#57f287]' : 'text-[#ed4245]' }}">
{{ $emote->delta >= 0 ? '+' : '' }}{{ $emote->delta }} {{ $emote->delta >= 0 ? '+' : '' }}{{ $emote->delta }}
@@ -139,55 +165,17 @@
</div> </div>
</div> </div>
<div class="space-y-3">
<h3 class="text-xl font-semibold text-white">Top 10 Static Emotes</h3>
<div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]">
@forelse (($stats['top_static'] ?? collect()) as $emote)
<div class="flex items-center justify-between px-4 py-3.5 border-b border-[#3f4147] last:border-b-0">
<div class="flex items-center gap-3">
<span class="w-8 text-sm font-semibold text-[#b5bac1]">#{{ $loop->iteration }}</span>
<img src="{{ $emoteImageSrc($emote->image) }}" alt="{{ $emote->emote_name }}" class="object-cover w-9 h-9 border rounded border-[#3f4147]" />
<span class="font-medium text-[#dbdee1]">{{ $emote->emote_name }}</span>
</div>
<span class="text-sm font-medium text-[#b5bac1]">{{ $emote->total_usage }}</span>
</div>
@empty
<p class="px-4 py-3 text-sm text-[#b5bac1]">No static emote usage found.</p>
@endforelse
</div>
</div>
<div class="space-y-3">
<h3 class="text-xl font-semibold text-white">Top 10 Animated Emotes</h3>
<div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]">
@forelse (($stats['top_animated'] ?? collect()) as $emote)
<div class="flex items-center justify-between px-4 py-3.5 border-b border-[#3f4147] last:border-b-0">
<div class="flex items-center gap-3">
<span class="w-8 text-sm font-semibold text-[#b5bac1]">#{{ $loop->iteration }}</span>
<img src="{{ $emoteImageSrc($emote->image) }}" alt="{{ $emote->emote_name }}" class="object-cover w-9 h-9 border rounded border-[#3f4147]" />
<span class="font-medium text-[#dbdee1]">{{ $emote->emote_name }}</span>
</div>
<span class="text-sm font-medium text-[#b5bac1]">{{ $emote->total_usage }}</span>
</div>
@empty
<p class="px-4 py-3 text-sm text-[#b5bac1]">No animated emote usage found.</p>
@endforelse
</div>
</div>
<div class="space-y-3"> <div class="space-y-3">
<h3 class="text-xl font-semibold text-white">Top 10 Unicode Emotes</h3> <h3 class="text-xl font-semibold text-white">Top 10 Unicode Emotes</h3>
<div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]"> <div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]">
@forelse (($stats['top_unicode'] ?? collect()) as $emote) @forelse (($stats['top_unicode'] ?? collect()) as $emote)
<div class="flex items-center justify-between px-4 py-3.5 border-b border-[#3f4147] last:border-b-0"> <div class="flex flex-col items-start gap-2 px-4 py-3.5 border-b border-[#3f4147] sm:flex-row sm:items-center sm:justify-between sm:gap-3 last:border-b-0">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3 min-w-0">
<span class="w-8 text-sm font-semibold text-[#b5bac1]">#{{ $loop->iteration }}</span> <span class="w-8 text-sm font-semibold text-[#b5bac1]">#{{ $loop->iteration }}</span>
<span class="font-medium text-[#dbdee1]">{{ $emote->emote_name }}</span> <span class="font-medium truncate text-[#dbdee1]">{{ $emote->emote_name }}</span>
</div> </div>
<span class="text-sm font-medium text-[#b5bac1]">{{ $emote->total_usage }}</span> <span class="text-sm font-medium text-[#b5bac1] sm:shrink-0">{{ $emote->total_usage }}</span>
</div> </div>
@empty @empty
<p class="px-4 py-3 text-sm text-[#b5bac1]">No unicode emote usage found.</p> <p class="px-4 py-3 text-sm text-[#b5bac1]">No unicode emote usage found.</p>
@@ -196,6 +184,34 @@
</div> </div>
</div> </div>
<div class="space-y-3">
<h3 class="text-xl font-semibold text-white">Custom Emote Usage (Top & Bottom)</h3>
<div class="grid gap-4 md:grid-cols-2">
@foreach ($customUsageTables as $table)
<div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]">
<div class="px-4 py-3 border-b border-[#3f4147]">
<h4 class="text-sm font-semibold text-white">{{ $table['title'] }}</h4>
</div>
@forelse ($table['list'] as $emote)
<div class="flex flex-col items-start gap-2 px-4 py-3.5 border-b border-[#3f4147] sm:flex-row sm:items-center sm:justify-between sm:gap-3 last:border-b-0">
<div class="flex items-center gap-3 min-w-0">
<span class="w-8 text-sm font-semibold text-[#b5bac1]">#{{ $loop->iteration }}</span>
<img src="{{ $emoteImageSrc($emote->image) }}" alt="{{ $emote->emote_name }}" class="object-cover w-9 h-9 border rounded border-[#3f4147]" />
<span class="font-medium truncate text-[#dbdee1]">{{ $emote->emote_name }}</span>
</div>
<span class="text-sm font-medium text-[#b5bac1] sm:shrink-0">{{ $emote->total_usage }}</span>
</div>
@empty
<p class="px-4 py-3 text-sm text-[#b5bac1]">{{ $table['empty'] }}</p>
@endforelse
</div>
@endforeach
</div>
</div>
</div>
<script id="homepage-chart-json" type="application/json">@json($chartPayload)</script> <script id="homepage-chart-json" type="application/json">@json($chartPayload)</script>
@once @once
@@ -237,6 +253,7 @@
const usageOverTimeCanvas = document.getElementById('emoji-usage-over-time-chart'); const usageOverTimeCanvas = document.getElementById('emoji-usage-over-time-chart');
const usageOverTime = payload.usage_over_time || { labels: [], values: [] }; const usageOverTime = payload.usage_over_time || { labels: [], values: [] };
const isMobileViewport = window.matchMedia('(max-width: 639px)').matches;
if (usageOverTimeCanvas) { if (usageOverTimeCanvas) {
hermesDestroyChart('usageOverTime'); hermesDestroyChart('usageOverTime');
@@ -250,15 +267,23 @@
tension: 0.3, tension: 0.3,
fill: false, fill: false,
borderColor: '#5865f2', borderColor: '#5865f2',
borderWidth: 2,
pointBackgroundColor: '#5865f2', pointBackgroundColor: '#5865f2',
pointBorderColor: '#5865f2' pointBorderColor: '#5865f2',
pointRadius: isMobileViewport ? 0 : 2,
pointHoverRadius: isMobileViewport ? 2 : 4
}] }]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: true, maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: { plugins: {
legend: { legend: {
display: !isMobileViewport,
labels: { labels: {
color: '#dbdee1' color: '#dbdee1'
} }
@@ -267,17 +292,23 @@
scales: { scales: {
x: { x: {
ticks: { ticks: {
color: '#b5bac1' color: '#b5bac1',
autoSkip: true,
maxTicksLimit: isMobileViewport ? 4 : 8,
maxRotation: 0,
minRotation: 0
}, },
grid: { grid: {
color: '#3f4147' color: '#3f4147',
display: !isMobileViewport
} }
}, },
y: { y: {
beginAtZero: true, beginAtZero: true,
ticks: { ticks: {
precision: 0, precision: 0,
color: '#b5bac1' color: '#b5bac1',
maxTicksLimit: isMobileViewport ? 5 : 8
}, },
grid: { grid: {
color: '#3f4147' color: '#3f4147'
@@ -291,6 +322,10 @@
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
hermesRenderOverTimeChart(); hermesRenderOverTimeChart();
window.addEventListener('resize', function () {
hermesRenderOverTimeChart();
});
}); });
</script> </script>
@endonce @endonce