diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 16df1c77a..f5e185c3f 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -61,6 +61,15 @@ class ProfileController extends BasePageController $this->userdata->style = 'Using the admin selected theme.'; } + // Get 24-hour hourly data for downloads and API requests + $downloadsHourly = UserDownload::getHourlyDownloads($userID); + $apiRequestsHourly = UserRequest::getHourlyApiRequests($userID); + + // Get role limits + $userRole = $this->userdata->roles->first(); + $downloadLimit = $userRole->downloadrequests ?? 0; + $apiLimit = $userRole->apirequests ?? 0; + $this->viewData = array_merge($this->viewData, [ 'downloadlist' => UserDownload::getDownloadRequestsForUser($userID), 'apirequests' => UserRequest::getApiRequests($userID), @@ -72,6 +81,10 @@ class ProfileController extends BasePageController 'privileged' => $privileged, 'isadmin' => $this->userdata->hasRole('Admin'), 'commentslist' => ReleaseComment::getCommentsForUserRange($userID), + 'downloadsHourly' => $downloadsHourly, + 'apiRequestsHourly' => $apiRequestsHourly, + 'downloadLimit' => $downloadLimit, + 'apiLimit' => $apiLimit, 'meta_title' => 'View User Profile', 'meta_keywords' => 'view,profile,user,details', 'meta_description' => 'View User Profile for '.$this->userdata->username, diff --git a/app/Models/UserDownload.php b/app/Models/UserDownload.php index b879de1fa..1faa37d16 100644 --- a/app/Models/UserDownload.php +++ b/app/Models/UserDownload.php @@ -70,6 +70,39 @@ class UserDownload extends Model return $value === false ? 0 : $value; } + /** + * Get hourly download counts for the last 24 hours. + * + * @return array Array of hourly counts indexed by hour + * + * @throws \Exception + */ + public static function getHourlyDownloads(int $userID): array + { + $hourlyData = []; + $now = now(); + + // Initialize all 24 hours with 0 + for ($i = 23; $i >= 0; $i--) { + $hour = $now->copy()->subHours($i); + $hourlyData[$hour->format('H:00')] = 0; + } + + // Get downloads from the last 24 hours grouped by hour + $downloads = self::whereUsersId($userID) + ->where('timestamp', '>', $now->subDay()) + ->get(); + + foreach ($downloads as $download) { + $hourKey = \Carbon\Carbon::parse($download->timestamp)->format('H:00'); + if (isset($hourlyData[$hourKey])) { + $hourlyData[$hourKey]++; + } + } + + return $hourlyData; + } + /** * @return \Illuminate\Database\Eloquent\Collection|static[] */ diff --git a/app/Models/UserRequest.php b/app/Models/UserRequest.php index 67ae21a01..973ba901b 100644 --- a/app/Models/UserRequest.php +++ b/app/Models/UserRequest.php @@ -80,6 +80,40 @@ class UserRequest extends Model return ! $requests ? 0 : $requests; } + /** + * Get hourly API request counts for the last 24 hours. + * + * @return array Array of hourly counts indexed by hour + * + * @throws \Exception + */ + public static function getHourlyApiRequests(int $userID): array + { + $hourlyData = []; + $now = now(); + + // Initialize all 24 hours with 0 + for ($i = 23; $i >= 0; $i--) { + $hour = $now->copy()->subHours($i); + $hourlyData[$hour->format('H:00')] = 0; + } + + // Get API requests from the last 24 hours grouped by hour + $requests = self::query() + ->where('users_id', $userID) + ->where('timestamp', '>', $now->subDay()) + ->get(); + + foreach ($requests as $request) { + $hourKey = \Carbon\Carbon::parse($request->timestamp)->format('H:00'); + if (isset($hourlyData[$hourKey])) { + $hourlyData[$hourKey]++; + } + } + + return $hourlyData; + } + /** * If a user accesses the API, log it. * diff --git a/resources/css/csp-safe.css b/resources/css/csp-safe.css index 87630fe8d..48cd9cc45 100644 --- a/resources/css/csp-safe.css +++ b/resources/css/csp-safe.css @@ -10,10 +10,20 @@ /* Chart Container Responsive Sizing */ .chart-container { position: relative; - height: 250px; + height: 16rem; /* 256px - updated for profile charts */ width: 100%; } +/* Ensure canvas fills container properly */ +.chart-container canvas { + max-height: 100%; +} + +/* Admin dashboard specific chart height */ +#adminDashboard .chart-container { + height: 250px; +} + /* Smooth transitions for metric updates */ [data-metric] { transition: color 0.3s ease-in-out, transform 0.3s ease-in-out; @@ -42,3 +52,67 @@ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } +/* Profile Charts - Progress Bars */ +.progress-bar { + transition: width 0.3s ease-in-out; + width: 0; /* Start at 0 for animation */ +} + +/* Profile Charts - API Token Scrollbar */ +code.api-token { + overflow-x: auto; + word-break: break-all; +} + +code.api-token::-webkit-scrollbar { + height: 6px; +} + +code.api-token::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 3px; +} + +code.api-token::-webkit-scrollbar-thumb { + background: rgba(0, 255, 0, 0.3); + border-radius: 3px; +} + +code.api-token::-webkit-scrollbar-thumb:hover { + background: rgba(0, 255, 0, 0.5); +} +/* Profile Page Styles */ + +/* Tab content visibility */ +.tab-content.hidden-by-default { + display: none; +} + +/* Progress bar animations */ +.progress-bar { + transition: width 0.3s ease-in-out; +} + +/* Custom scrollbar for API token code blocks */ +code.api-token { + overflow-x: auto; + word-break: break-all; +} + +code.api-token::-webkit-scrollbar { + height: 6px; +} + +code.api-token::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 3px; +} + +code.api-token::-webkit-scrollbar-thumb { + background: rgba(0, 255, 0, 0.3); + border-radius: 3px; +} + +code.api-token::-webkit-scrollbar-thumb:hover { + background: rgba(0, 255, 0, 0.5); +} diff --git a/resources/js/csp-safe.js b/resources/js/csp-safe.js index d774c5404..887584831 100644 --- a/resources/js/csp-safe.js +++ b/resources/js/csp-safe.js @@ -35,6 +35,7 @@ document.addEventListener('DOMContentLoaded', function() { initDetailsPageImageModal(); initAddToCart(); initMoviesLayoutToggle(); + initProfileCharts(); }); // Event delegation for dynamically added elements @@ -4363,3 +4364,218 @@ function initMoviesLayoutToggle() { } } +/** + * Profile Charts - User Download and API Request Charts + * Initialize charts, progress bars, and tab switching for profile page + */ +function initProfileCharts() { + initProfileProgressBars(); + initProfileChartsData(); +} + +/** + * Initialize progress bar animations + */ +function initProfileProgressBars() { + const progressBars = document.querySelectorAll('.progress-bar'); + progressBars.forEach(bar => { + const width = bar.dataset.width; + if (width) { + // Small delay to trigger CSS transition + setTimeout(() => { + bar.style.width = width + '%'; + }, 100); + } + }); +} + +/** + * Initialize Chart.js charts for profile page + */ +function initProfileChartsData() { + // Get data from data attributes + const chartContainer = document.getElementById('profile-charts-container'); + if (!chartContainer) return; + + const downloadsHourlyData = JSON.parse(chartContainer.dataset.downloadsHourly || '{}'); + const apiRequestsHourlyData = JSON.parse(chartContainer.dataset.apiRequestsHourly || '{}'); + const downloadLimit = parseInt(chartContainer.dataset.downloadLimit || '0'); + const apiLimit = parseInt(chartContainer.dataset.apiLimit || '0'); + + const labels = Object.keys(downloadsHourlyData); + const downloadsValues = Object.values(downloadsHourlyData); + const apiValues = Object.values(apiRequestsHourlyData); + + // Check if user is in dark mode + const isDarkMode = document.documentElement.classList.contains('dark'); + const gridColor = isDarkMode ? 'rgba(75, 85, 99, 0.3)' : 'rgba(200, 200, 200, 0.3)'; + const textColor = isDarkMode ? 'rgba(156, 163, 175, 1)' : 'rgba(75, 85, 99, 1)'; + + // Create Downloads Chart + createProfileDownloadsChart(labels, downloadsValues, downloadLimit, gridColor, textColor); + + // Create API Requests Chart + createProfileApiRequestsChart(labels, apiValues, apiLimit, gridColor, textColor); +} + +/** + * Create Downloads Chart for profile page + */ +function createProfileDownloadsChart(labels, data, limit, gridColor, textColor) { + const downloadsCtx = document.getElementById('downloadsChart'); + if (!downloadsCtx) return; + + new Chart(downloadsCtx, { + type: 'line', + data: { + labels: labels, + datasets: [{ + label: 'Downloads', + data: data, + borderColor: 'rgb(59, 130, 246)', + backgroundColor: 'rgba(59, 130, 246, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4, + pointBackgroundColor: 'rgb(59, 130, 246)', + pointBorderColor: '#fff', + pointBorderWidth: 2, + pointRadius: 3, + pointHoverRadius: 5 + }, { + label: 'Limit', + data: Array(labels.length).fill(limit), + borderColor: 'rgba(239, 68, 68, 0.5)', + borderWidth: 2, + borderDash: [5, 5], + fill: false, + pointRadius: 0, + pointHoverRadius: 0 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + labels: { + color: textColor + } + }, + tooltip: { + mode: 'index', + intersect: false, + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 1, + color: textColor + }, + grid: { + color: gridColor + } + }, + x: { + ticks: { + color: textColor, + maxRotation: 45, + minRotation: 45 + }, + grid: { + color: gridColor + } + } + }, + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false + } + } + }); +} + +/** + * Create API Requests Chart for profile page + */ +function createProfileApiRequestsChart(labels, data, limit, gridColor, textColor) { + const apiCtx = document.getElementById('apiRequestsChart'); + if (!apiCtx) return; + + new Chart(apiCtx, { + type: 'line', + data: { + labels: labels, + datasets: [{ + label: 'API Requests', + data: data, + borderColor: 'rgb(168, 85, 247)', + backgroundColor: 'rgba(168, 85, 247, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4, + pointBackgroundColor: 'rgb(168, 85, 247)', + pointBorderColor: '#fff', + pointBorderWidth: 2, + pointRadius: 3, + pointHoverRadius: 5 + }, { + label: 'Limit', + data: Array(labels.length).fill(limit), + borderColor: 'rgba(239, 68, 68, 0.5)', + borderWidth: 2, + borderDash: [5, 5], + fill: false, + pointRadius: 0, + pointHoverRadius: 0 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + labels: { + color: textColor + } + }, + tooltip: { + mode: 'index', + intersect: false, + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 1, + color: textColor + }, + grid: { + color: gridColor + } + }, + x: { + ticks: { + color: textColor, + maxRotation: 45, + minRotation: 45 + }, + grid: { + color: gridColor + } + } + }, + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false + } + } + }); +} diff --git a/resources/views/profile/index.blade.php b/resources/views/profile/index.blade.php index aa03c9d92..35b5cccc6 100644 --- a/resources/views/profile/index.blade.php +++ b/resources/views/profile/index.blade.php @@ -239,7 +239,54 @@
+ {{ number_format($downloadLimit - $grabstoday) }} downloads remaining +
++ {{ number_format($apiLimit - $apirequests) }} requests remaining +
+{{ $user->api_token }}
+ {{ $user->api_token }}