mirror of
https://github.com/the-programmers-hangout/Hermes.git
synced 2026-09-04 09:15:59 +02:00
bottom chart and responsive changes
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,91 +130,84 @@
|
|||||||
<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 class="grid gap-8 lg:grid-cols-2">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<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]">
|
||||||
|
@forelse (($stats['top_movers'] ?? collect()) 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">
|
||||||
|
@if (($emote->type ?? null) === 'UNICODE')
|
||||||
|
<span class="font-medium truncate text-[#dbdee1]">{{ $emote->emote_name }}</span>
|
||||||
|
@else
|
||||||
|
<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>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<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 font-medium {{ $emote->delta >= 0 ? 'text-[#57f287]' : 'text-[#ed4245]' }}">
|
||||||
|
{{ $emote->delta >= 0 ? '+' : '' }}{{ $emote->delta }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="px-4 py-3 text-sm text-[#b5bac1]">No mover data available yet.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h3 class="text-xl font-semibold text-white">Top 10 Unicode Emotes</h3>
|
||||||
|
|
||||||
|
<div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]">
|
||||||
|
@forelse (($stats['top_unicode'] ?? collect()) 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>
|
||||||
|
<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]">No unicode emote usage found.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">Custom Emote Usage (Top & Bottom)</h3>
|
||||||
|
|
||||||
<div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
@forelse (($stats['top_movers'] ?? collect()) as $emote)
|
@foreach ($customUsageTables as $table)
|
||||||
<div class="flex items-center justify-between px-4 py-3.5 border-b border-[#3f4147] last:border-b-0">
|
<div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]">
|
||||||
<div class="flex items-center gap-3">
|
<div class="px-4 py-3 border-b border-[#3f4147]">
|
||||||
@if (($emote->type ?? null) === 'UNICODE')
|
<h4 class="text-sm font-semibold text-white">{{ $table['title'] }}</h4>
|
||||||
<span class="font-medium text-[#dbdee1]">{{ $emote->emote_name }}</span>
|
|
||||||
@else
|
|
||||||
<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>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<div class="text-right">
|
|
||||||
<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]' }}">
|
|
||||||
{{ $emote->delta >= 0 ? '+' : '' }}{{ $emote->delta }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
@empty
|
@endforeach
|
||||||
<p class="px-4 py-3 text-sm text-[#b5bac1]">No mover data available yet.</p>
|
|
||||||
@endforelse
|
|
||||||
</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">
|
|
||||||
<h3 class="text-xl font-semibold text-white">Top 10 Unicode Emotes</h3>
|
|
||||||
|
|
||||||
<div class="overflow-hidden border rounded-lg bg-[#2b2d31] border-[#3f4147]">
|
|
||||||
@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 items-center gap-3">
|
|
||||||
<span class="w-8 text-sm font-semibold text-[#b5bac1]">#{{ $loop->iteration }}</span>
|
|
||||||
<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 unicode emote usage found.</p>
|
|
||||||
@endforelse
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user