From a0c65ca3d70c18aad568c7b178b0ab826293870c Mon Sep 17 00:00:00 2001 From: itsAzaria Date: Fri, 20 Feb 2026 20:55:58 +0000 Subject: [PATCH] bottom chart and responsive changes --- app/Http/Controllers/HomepageController.php | 2 + app/Models/Concerns/BuildsEmoteAggregates.php | 34 +++ app/Models/Concerns/BuildsEmoteUsageLists.php | 42 ++++ app/Models/EmoteGuildStat.php | 78 +++--- app/Models/UserGuildStat.php | 85 +++---- resources/views/components/homepage.blade.php | 229 ++++++++++-------- 6 files changed, 278 insertions(+), 192 deletions(-) create mode 100644 app/Models/Concerns/BuildsEmoteAggregates.php create mode 100644 app/Models/Concerns/BuildsEmoteUsageLists.php diff --git a/app/Http/Controllers/HomepageController.php b/app/Http/Controllers/HomepageController.php index b4cc392..fa1153b 100644 --- a/app/Http/Controllers/HomepageController.php +++ b/app/Http/Controllers/HomepageController.php @@ -61,6 +61,8 @@ class HomepageController 'top_movers' => $topMovers, 'top_static' => $aggregate['top_static'] ?? 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(), ]; } diff --git a/app/Models/Concerns/BuildsEmoteAggregates.php b/app/Models/Concerns/BuildsEmoteAggregates.php new file mode 100644 index 0000000..f93b8cb --- /dev/null +++ b/app/Models/Concerns/BuildsEmoteAggregates.php @@ -0,0 +1,34 @@ +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, + ]; + } +} diff --git a/app/Models/Concerns/BuildsEmoteUsageLists.php b/app/Models/Concerns/BuildsEmoteUsageLists.php new file mode 100644 index 0000000..a10ee8c --- /dev/null +++ b/app/Models/Concerns/BuildsEmoteUsageLists.php @@ -0,0 +1,42 @@ +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(); + } +} diff --git a/app/Models/EmoteGuildStat.php b/app/Models/EmoteGuildStat.php index ebaf606..f3a0690 100644 --- a/app/Models/EmoteGuildStat.php +++ b/app/Models/EmoteGuildStat.php @@ -2,11 +2,15 @@ namespace App\Models; +use App\Models\Concerns\BuildsEmoteAggregates; +use App\Models\Concerns\BuildsEmoteUsageLists; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Collection; class EmoteGuildStat extends Model { + use BuildsEmoteAggregates; + use BuildsEmoteUsageLists; + protected $fillable = [ 'emote_id', 'guild_id', @@ -27,24 +31,19 @@ class EmoteGuildStat extends Model ); } - /** - * Build aggregate stats for all users. - */ public static function dashboardAggregate(): array { $baseQuery = self::query() ->join('emotes', 'emote_guild_stats.emote_id', '=', 'emotes.emote_id'); - $summary = (clone $baseQuery) - ->selectRaw('COALESCE(SUM(emote_guild_stats.usage_count), 0) as total_usage') - ->selectRaw('COUNT(DISTINCT emote_guild_stats.emote_id) as unique_emotes') - ->first(); + $aggregate = self::aggregateSummaryAndUsageByType( + $baseQuery, + 'emote_guild_stats.usage_count', + 'emote_guild_stats.emote_id' + ); - $usageByType = (clone $baseQuery) - ->select('emotes.type') - ->selectRaw('COALESCE(SUM(emote_guild_stats.usage_count), 0) as total_usage') - ->groupBy('emotes.type') - ->pluck('total_usage', 'type'); + $summary = $aggregate['summary']; + $usageByType = $aggregate['usage_by_type']; $unicodeUsage = (int) EmoteLog::query() ->join('emotes', 'emote_logs.emote_id', '=', 'emotes.emote_id') @@ -88,48 +87,35 @@ class EmoteGuildStat extends Model 'ANIMATED' => (int) ($usageByType->get('ANIMATED') ?? 0), 'UNICODE' => $unicodeUsage, ], - 'top_static' => self::topEmotesByType( + 'top_static' => self::emotesByType( $baseQuery, 'STATIC', '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, 'ANIMATED', '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, ]; } - - /** - * 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(); - } } diff --git a/app/Models/UserGuildStat.php b/app/Models/UserGuildStat.php index c749a07..6d4ac3a 100644 --- a/app/Models/UserGuildStat.php +++ b/app/Models/UserGuildStat.php @@ -2,11 +2,15 @@ namespace App\Models; +use App\Models\Concerns\BuildsEmoteAggregates; +use App\Models\Concerns\BuildsEmoteUsageLists; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Collection; class UserGuildStat extends Model { + use BuildsEmoteAggregates; + use BuildsEmoteUsageLists; + protected $fillable = [ 'user_id', 'guild_id', @@ -25,26 +29,21 @@ class UserGuildStat extends Model return $this->belongsTo(User::class, 'user_id', 'discord_id'); } - /** - * Build aggregate stats for a single user. - */ public static function dashboardAggregateForUser(string $userId): array { $baseQuery = self::query() ->join('emotes', 'user_guild_stats.emote_id', '=', 'emotes.emote_id') ->where('user_guild_stats.user_id', $userId); - $summary = (clone $baseQuery) - ->selectRaw('COALESCE(SUM(user_guild_stats.usage_count), 0) as total_usage') - ->selectRaw('COUNT(DISTINCT user_guild_stats.emote_id) as unique_emotes') - ->selectRaw('COUNT(DISTINCT user_guild_stats.user_id) as unique_users') - ->first(); + $aggregate = self::aggregateSummaryAndUsageByType( + $baseQuery, + 'user_guild_stats.usage_count', + 'user_guild_stats.emote_id', + 'user_guild_stats.user_id' + ); - $usageByType = (clone $baseQuery) - ->select('emotes.type') - ->selectRaw('COALESCE(SUM(user_guild_stats.usage_count), 0) as total_usage') - ->groupBy('emotes.type') - ->pluck('total_usage', 'type'); + $summary = $aggregate['summary']; + $usageByType = $aggregate['usage_by_type']; return [ 'total_usage' => (int) ($summary->total_usage ?? 0), @@ -55,53 +54,41 @@ class UserGuildStat extends Model 'ANIMATED' => (int) ($usageByType->get('ANIMATED') ?? 0), 'UNICODE' => (int) ($usageByType->get('UNICODE') ?? 0), ], - 'top_static' => self::topEmotesByType( + 'top_static' => self::emotesByType( $baseQuery, 'STATIC', '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, 'ANIMATED', '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, 'UNICODE', '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(); - } } diff --git a/resources/views/components/homepage.blade.php b/resources/views/components/homepage.blade.php index b74d3c1..2d7b4d2 100644 --- a/resources/views/components/homepage.blade.php +++ b/resources/views/components/homepage.blade.php @@ -35,6 +35,29 @@ '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 @@ -69,36 +92,36 @@ @endif -
-
-
+
+
+

Total Emoji Usage

-

{{ $stats['total_usage'] ?? 0 }}

+

{{ $stats['total_usage'] ?? 0 }}

-
+

Unique Emotes

-

{{ $stats['unique_emotes'] ?? 0 }}

+

{{ $stats['unique_emotes'] ?? 0 }}

-
+

Users Included

-

{{ $stats['unique_users'] ?? 0 }}

+

{{ $stats['unique_users'] ?? 0 }}

-
+

Static Usage

-

{{ $stats['usage_by_type']['STATIC'] ?? 0 }}

+

{{ $stats['usage_by_type']['STATIC'] ?? 0 }}

-
+

Animated Usage

-

{{ $stats['usage_by_type']['ANIMATED'] ?? 0 }}

+

{{ $stats['usage_by_type']['ANIMATED'] ?? 0 }}

-
+

Unicode Usage

-

{{ $stats['usage_by_type']['UNICODE'] ?? 0 }}

+

{{ $stats['usage_by_type']['UNICODE'] ?? 0 }}

@@ -107,91 +130,84 @@

Emote Usage Over Time (Last 30 Days)

-
- +
+
+ +
+
+
+ +
+
+

Top Movers (Last 7 Days vs Previous 7 Days)

+ +
+ @forelse (($stats['top_movers'] ?? collect()) as $emote) +
+
+ @if (($emote->type ?? null) === 'UNICODE') + {{ $emote->emote_name }} + @else + {{ $emote->emote_name }} + {{ $emote->emote_name }} + @endif +
+
+

{{ $emote->previous_count }} → {{ $emote->current_count }}

+

+ {{ $emote->delta >= 0 ? '+' : '' }}{{ $emote->delta }} +

+
+
+ @empty +

No mover data available yet.

+ @endforelse +
+
+ +
+

Top 10 Unicode Emotes

+ +
+ @forelse (($stats['top_unicode'] ?? collect()) as $emote) +
+
+ #{{ $loop->iteration }} + {{ $emote->emote_name }} +
+ {{ $emote->total_usage }} +
+ @empty +

No unicode emote usage found.

+ @endforelse +
-

Top Movers (Last 7 Days vs Previous 7 Days)

+

Custom Emote Usage (Top & Bottom)

-
- @forelse (($stats['top_movers'] ?? collect()) as $emote) -
-
- @if (($emote->type ?? null) === 'UNICODE') - {{ $emote->emote_name }} - @else - {{ $emote->emote_name }} - {{ $emote->emote_name }} - @endif -
-
-

{{ $emote->previous_count }} → {{ $emote->current_count }}

-

- {{ $emote->delta >= 0 ? '+' : '' }}{{ $emote->delta }} -

+
+ @foreach ($customUsageTables as $table) +
+
+

{{ $table['title'] }}

+ + @forelse ($table['list'] as $emote) +
+
+ #{{ $loop->iteration }} + {{ $emote->emote_name }} + {{ $emote->emote_name }} +
+ {{ $emote->total_usage }} +
+ @empty +

{{ $table['empty'] }}

+ @endforelse
- @empty -

No mover data available yet.

- @endforelse -
-
- -
-

Top 10 Static Emotes

- -
- @forelse (($stats['top_static'] ?? collect()) as $emote) -
-
- #{{ $loop->iteration }} - {{ $emote->emote_name }} - {{ $emote->emote_name }} -
- {{ $emote->total_usage }} -
- @empty -

No static emote usage found.

- @endforelse -
-
- -
-

Top 10 Animated Emotes

- -
- @forelse (($stats['top_animated'] ?? collect()) as $emote) -
-
- #{{ $loop->iteration }} - {{ $emote->emote_name }} - {{ $emote->emote_name }} -
- {{ $emote->total_usage }} -
- @empty -

No animated emote usage found.

- @endforelse -
-
- -
-

Top 10 Unicode Emotes

- -
- @forelse (($stats['top_unicode'] ?? collect()) as $emote) -
-
- #{{ $loop->iteration }} - {{ $emote->emote_name }} -
- {{ $emote->total_usage }} -
- @empty -

No unicode emote usage found.

- @endforelse + @endforeach
@@ -237,6 +253,7 @@ const usageOverTimeCanvas = document.getElementById('emoji-usage-over-time-chart'); const usageOverTime = payload.usage_over_time || { labels: [], values: [] }; + const isMobileViewport = window.matchMedia('(max-width: 639px)').matches; if (usageOverTimeCanvas) { hermesDestroyChart('usageOverTime'); @@ -250,15 +267,23 @@ tension: 0.3, fill: false, borderColor: '#5865f2', + borderWidth: 2, pointBackgroundColor: '#5865f2', - pointBorderColor: '#5865f2' + pointBorderColor: '#5865f2', + pointRadius: isMobileViewport ? 0 : 2, + pointHoverRadius: isMobileViewport ? 2 : 4 }] }, options: { responsive: true, - maintainAspectRatio: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false + }, plugins: { legend: { + display: !isMobileViewport, labels: { color: '#dbdee1' } @@ -267,17 +292,23 @@ scales: { x: { ticks: { - color: '#b5bac1' + color: '#b5bac1', + autoSkip: true, + maxTicksLimit: isMobileViewport ? 4 : 8, + maxRotation: 0, + minRotation: 0 }, grid: { - color: '#3f4147' + color: '#3f4147', + display: !isMobileViewport } }, y: { beginAtZero: true, ticks: { precision: 0, - color: '#b5bac1' + color: '#b5bac1', + maxTicksLimit: isMobileViewport ? 5 : 8 }, grid: { color: '#3f4147' @@ -291,6 +322,10 @@ document.addEventListener('DOMContentLoaded', function () { hermesRenderOverTimeChart(); + + window.addEventListener('resize', function () { + hermesRenderOverTimeChart(); + }); }); @endonce