From fffa320c08b78933c58d3252be49c6a73d30797f Mon Sep 17 00:00:00 2001 From: w33b Date: Wed, 8 Oct 2025 16:52:40 +0200 Subject: [PATCH] Add monthly views chart --- app/Helpers/CacheHelper.php | 7 -- .../Controllers/Api/HentaiApiController.php | 24 +++++- app/Http/Controllers/HomeController.php | 1 - package-lock.json | 50 +++++++++++-- package.json | 1 + resources/js/stats.js | 73 +++++++++++++++++++ resources/views/home/stats.blade.php | 17 ++--- routes/web.php | 1 + 8 files changed, 144 insertions(+), 30 deletions(-) create mode 100644 resources/js/stats.js diff --git a/app/Helpers/CacheHelper.php b/app/Helpers/CacheHelper.php index bba7f61..155e044 100644 --- a/app/Helpers/CacheHelper.php +++ b/app/Helpers/CacheHelper.php @@ -60,13 +60,6 @@ class CacheHelper }); } - public static function getTotalMonthlyViews() - { - return Cache::remember("total_monthly_view_count", now()->addMinutes(60), function () { - return PopularMonthly::count(); - }); - } - public static function getPopularAllTime(bool $guest) { $guestString = $guest ? 'guest' : 'authed'; diff --git a/app/Http/Controllers/Api/HentaiApiController.php b/app/Http/Controllers/Api/HentaiApiController.php index d8cc228..8fa8356 100644 --- a/app/Http/Controllers/Api/HentaiApiController.php +++ b/app/Http/Controllers/Api/HentaiApiController.php @@ -2,16 +2,18 @@ namespace App\Http\Controllers\Api; -use App\Models\Downloads; use App\Models\Hentai; +use App\Models\PopularMonthly; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; use App\Http\Controllers\Controller; class HentaiApiController extends Controller { - public function index(Request $request) + /** + * Get a list of all hentai with it's episodes + */ + public function index() { // Cache for 10 minutes $data = Cache::remember('api_hentai_list', now()->addMinutes(10), function () { @@ -35,4 +37,20 @@ class HentaiApiController extends Controller return response()->json($data); } + + /** + * Get monthly views by day for stats + */ + public function getMonthlyViews() + { + // Cache for 60 minutes + $data = Cache::remember('api_hentai_list', now()->addMinutes(60), function () { + return PopularMonthly::selectRaw('DATE(created_at) as date, COUNT(*) as count') + ->groupBy('date') + ->orderBy('date', 'asc') + ->get(); + }); + + return response()->json($data); + } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index e6695d3..848cd11 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -87,7 +87,6 @@ class HomeController extends Controller 'viewCount' => CacheHelper::getTotalViewCount(), 'episodeCount' => CacheHelper::getTotalEpisodeCount(), 'hentaiCount' => CacheHelper::getTotalHentaiCount(), - 'monthlyCount' => CacheHelper::getTotalMonthlyViews() ]); } diff --git a/package-lock.json b/package-lock.json index e08f086..246257e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "@fortawesome/fontawesome-free": "^6.5.1", "@jellyfin/libass-wasm": "^4.1.1", "@yaireo/tagify": "^4.21.2", + "chart.js": "^4.5.0", "dashjs": "^5.0.0", "hammerjs": "^2.0.8", "plyr": "^3.7.8", @@ -572,6 +573,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1267,10 +1274,16 @@ "license": "CC-BY-4.0" }, "node_modules/chart.js": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", - "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==", - "license": "MIT" + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/chartjs-plugin-datalabels": { "version": "2.2.0", @@ -3142,6 +3155,12 @@ "tailwindcss": "3.3.0" } }, + "node_modules/tw-elements/node_modules/chart.js": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==", + "license": "MIT" + }, "node_modules/tw-elements/node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -3258,6 +3277,15 @@ "postcss": "^8.0.9" } }, + "node_modules/tw-elements/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/ua-parser-js": { "version": "1.0.41", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", @@ -3615,12 +3643,18 @@ } }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14.6" } } } diff --git a/package.json b/package.json index b458610..da8f3b1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@fortawesome/fontawesome-free": "^6.5.1", "@jellyfin/libass-wasm": "^4.1.1", "@yaireo/tagify": "^4.21.2", + "chart.js": "^4.5.0", "dashjs": "^5.0.0", "hammerjs": "^2.0.8", "plyr": "^3.7.8", diff --git a/resources/js/stats.js b/resources/js/stats.js new file mode 100644 index 0000000..d0b002a --- /dev/null +++ b/resources/js/stats.js @@ -0,0 +1,73 @@ +import Chart from 'chart.js/auto'; + +// Theming +if (localStorage.theme !== 'light') { + Chart.defaults.color = "#ADBABD"; + Chart.defaults.borderColor = "rgba(255,255,255,0.1)"; + Chart.defaults.backgroundColor = "rgba(255,255,0,0.1)"; + Chart.defaults.elements.line.borderColor = "rgba(255,255,0,0.4)"; +} + +// Get Tags from API +window.axios.get('/v1/monthly-views').then(function (response) { + if (response.status != 200) { + return; + } + + const data = { + labels: response.data.map((entry) => { return entry.date }), + datasets: [{ + label: 'Views', + fill: false, + backgroundColor: 'rgba(190, 18, 60, 0.3)', + borderColor: 'rgba(190, 18, 60, 1.0)', + cubicInterpolationMode: 'monotone', + data: response.data.map((entry) => { return entry.count }), + }] + } + + const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Views the last 30 days', + font: { + size: 18 + } + }, + }, + interaction: { + intersect: false, + }, + scales: { + x: { + display: true, + title: { + display: true + } + }, + y: { + display: true, + title: { + display: true, + text: 'Views' + }, + suggestedMin: 0, + suggestedMax: 40000 + } + } + }, + }; + + const monthlyViewChart = new Chart( + document.getElementById('monthlyChart'), + config + ); +}).catch(function (error) { + console.log(error); +}); + diff --git a/resources/views/home/stats.blade.php b/resources/views/home/stats.blade.php index ded9113..27824dd 100644 --- a/resources/views/home/stats.blade.php +++ b/resources/views/home/stats.blade.php @@ -4,7 +4,7 @@
hstream.moe Logo
-
+
{{ number_format($viewCount) }} @@ -14,15 +14,7 @@
-
- {{ number_format($monthlyCount) }} -
-
- views the last 30 days -
-
-
-
+
{{ $episodeCount }}
@@ -46,7 +38,10 @@
+ -

Cached for 60 Minutes

+ @vite(['resources/js/stats.js']) diff --git a/routes/web.php b/routes/web.php index 9633879..953bff5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,6 +26,7 @@ Route::get('/banned', [HomeController::class, 'banned'])->name('home.banned'); // API Endpoint Route::get('/v1/hentai-list', [HentaiApiController::class, 'index'])->name('api.hentai.index'); +Route::get('/v1/monthly-views', [HentaiApiController::class, 'getMonthlyViews'])->name('api.hentai.monthly'); // Stream Page Route::get('/hentai/{title}', [StreamController::class, 'index'])->name('hentai.index');