Last active
February 25, 2024 16:30
-
-
Save whoisthisstud/1feab97e61b7a8e2bd591332770ed3ec to your computer and use it in GitHub Desktop.
Laravel Livewire "Dashboard" component + PeriodScopesTrait for dynamic model metric cards
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace App\Http\Livewire; | |
| use Livewire\Component; | |
| class Dashboard extends Component | |
| { | |
| /** | |
| * The selected period | |
| * | |
| * Potential options are: | |
| * 'Today', 'Week', 'Month', 'Quarter', 'Year', 'All Time' | |
| * | |
| * Default is 'Year' | |
| * | |
| * @var string $period | |
| */ | |
| public $period = 'Year'; | |
| /** | |
| * Function to get current metrics using PeriodScopes Trait. | |
| * | |
| * Pass the classname(-.php) of any model class within App\Models\ | |
| * Return metrics based on selected $period | |
| * | |
| * @param string $model | |
| * @return array | |
| */ | |
| protected function getMetrics($model): array | |
| { | |
| if( is_null($model) ) { | |
| return [ | |
| 'total' => 0, | |
| 'change' => 0, | |
| 'percentage' => 0, | |
| ]; | |
| } | |
| $class = "\App\Models\\$model"; | |
| $query = $class::query(); | |
| $prevQuery = $class::query(); | |
| switch ($this->period) { | |
| case 'Today': | |
| $total = $query->today()->count(); | |
| $prevTotal = $prevQuery->yesterday()->count(); | |
| break; | |
| case 'Week': | |
| $total = $query->thisWeek()->count(); | |
| $prevTotal = $prevQuery->previousWeek()->count(); | |
| break; | |
| case 'Month': | |
| $total = $query->thisMonth()->count(); | |
| $prevTotal = $prevQuery->previousMonth()->count(); | |
| break; | |
| case 'Quarter': | |
| $total = $query->thisQuarter()->count(); | |
| $prevTotal = $prevQuery->previousQuarter()->count(); | |
| break; | |
| case 'Year': | |
| $total = $query->thisYear()->count(); | |
| $prevTotal = $prevQuery->previousYear()->count(); | |
| break; | |
| default: | |
| $total = $query->count(); | |
| $prevTotal = 0; | |
| break; | |
| } | |
| $change = $total - $prevTotal; // period over period | |
| $percentage = ($prevTotal > 0) | |
| ? round(($total / $prevTotal) * 100) | |
| : ($total > 0 | |
| ? 100 | |
| : 0); | |
| return [ | |
| 'total' => $total, | |
| 'change' => $change, | |
| 'percentage' => $percentage, | |
| ]; | |
| } | |
| public function render() | |
| { | |
| return view('livewire.dashboard') | |
| ->with([ | |
| 'userMetrics' => $this->getMetrics('User'), | |
| 'postMetrics' => $this->getMetrics('Post'), | |
| 'commentMetrics' => $this->getMetrics('Comment'), | |
| 'salesMetrics' => $this->getMetrics('Sale'), | |
| ]); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| namespace App\Scopes; | |
| use Carbon\Carbon; | |
| trait PeriodScopes { | |
| /** | |
| * Scope a query to include records created today. | |
| * | |
| * @param \Illuminate\Database\Eloquent\Builder $query | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| public function scopeToday($query) | |
| { | |
| $startOfDay = now()->startOfDay(); | |
| return $query->whereBetween('created_at', [$startOfDay, now()]); | |
| } | |
| /** | |
| * Scope a query to include records created yesterday. | |
| * | |
| * @param \Illuminate\Database\Eloquent\Builder $query | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| public function scopeYesterday($query) | |
| { | |
| $startOfYesterday = now()->subDay()->startOfDay(); | |
| $endOfYesterday = now()->subDay()->endOfDay(); | |
| return $query->whereBetween('created_at', [$startOfYesterday, $endOfYesterday]); | |
| } | |
| /** | |
| * Scope a query to include records created this week. | |
| * | |
| * @param \Illuminate\Database\Eloquent\Builder $query | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| public function scopeThisWeek($query) | |
| { | |
| $startOfWeek = now()->startOfWeek(Carbon::SUNDAY)->startOfDay(); | |
| return $query->whereBetween('created_at', [$startOfWeek, now()]); | |
| } | |
| /** | |
| * Scope a query to include records created last week. | |
| * | |
| * @param \Illuminate\Database\Eloquent\Builder $query | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| public function scopePreviousWeek($query) | |
| { | |
| $startOfWeek = now()->subDays(7)->startOfWeek(Carbon::SUNDAY)->startOfDay(); | |
| $endOfWeek = now()->subDays(7)->endOfWeek(Carbon::SATURDAY)->endOfDay(); | |
| return $query->whereBetween('created_at', [$startOfWeek, $endOfWeek]); | |
| } | |
| /** | |
| * Scope a query to include records created this month. | |
| * | |
| * @param \Illuminate\Database\Eloquent\Builder $query | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| public function scopeThisMonth($query) | |
| { | |
| $startOfMonth = now()->startOfMonth()->startOfDay(); | |
| return $query->whereBetween('created_at', [$startOfMonth, now()]); | |
| } | |
| /** | |
| * Scope a query to include records created last month. | |
| * | |
| * @param \Illuminate\Database\Eloquent\Builder $query | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| public function scopePreviousMonth($query) | |
| { | |
| $startOfMonth = now()->startOfMonth()->subMonthsNoOverflow()->startOfDay(); | |
| $endOfMonth = now()->startOfMonth()->subMonthsNoOverflow()->endOfMonth()->endOfDay(); | |
| return $query->whereBetween('created_at', [$startOfMonth, $endOfMonth]); | |
| } | |
| /** | |
| * Scope a query to include records created this quarter. | |
| * | |
| * @param \Illuminate\Database\Eloquent\Builder $query | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| public function scopeThisQuarter($query) | |
| { | |
| $startOfQuarter = now()->firstOfQuarter()->startOfDay(); | |
| return $query->whereBetween('created_at', [$startOfQuarter, now()]); | |
| } | |
| /** | |
| * Scope a query to include records created last quarter. | |
| * | |
| * @param \Illuminate\Database\Eloquent\Builder $query | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| public function scopePreviousQuarter($query) | |
| { | |
| $startOfQuarter = now()->firstOfQuarter()->subMonthsNoOverflow(3)->startOfDay(); | |
| $endOfQuarter = now()->firstOfQuarter()->subMonthsNoOverflow(3)->lastOfQuarter()->endOfDay(); | |
| return $query->whereBetween('created_at', [$startOfQuarter, $endOfQuarter]); | |
| } | |
| /** | |
| * Scope a query to include records created this year. | |
| * | |
| * @param \Illuminate\Database\Eloquent\Builder $query | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| public function scopeThisYear($query) | |
| { | |
| $startOfYear = now()->startOfYear()->startOfDay(); | |
| return $query->whereBetween('created_at', [$startOfYear, now()]); | |
| } | |
| /** | |
| * Scope a query to include records created last year. | |
| * | |
| * @param \Illuminate\Database\Eloquent\Builder $query | |
| * @return \Illuminate\Database\Eloquent\Builder | |
| */ | |
| public function scopePreviousYear($query) | |
| { | |
| $startOfYear = now()->startOfYear()->subDays(2)->startOfYear()->startOfDay(); | |
| $endOfYear = now()->startOfYear()->subDays(2)->endOfYear()->endOfDay(); | |
| return $query->whereBetween('created_at', [$startOfYear, $endOfYear]); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @props([ | |
| 'percentage', | |
| 'strokeColor' | |
| ]) | |
| <svg viewBox="0 0 36 36" class="circular-chart"> | |
| <path class="circle-chart-bg" | |
| d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" | |
| /> | |
| <path class="circle-chart-fg {{ $strokeColor ?? 'stroke-green-500' }} transition-all duration-75 ease-linear" | |
| stroke-dasharray="{{ $percentage ?? 0 }}, 100" | |
| d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" | |
| /> | |
| <text x="18" y="20.35" class="percentage">{{ $percentage ?? 0 }}%</text> | |
| </svg> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div class="relative rounded-md bg-white dark:bg-slate-800/20 py-3 px-4 dark:border dark:border-indigo-900/20 sm:py-4 sm:px-6 overflow-visible"> | |
| <div wire:loading class="absolute inset-0"> | |
| <div wire:loading x-cloak> | |
| <svg class="z-10 absolute top-1 left-1 w-4 h-4 text-indigo-500 animate-spin opacity-80" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> | |
| </div> | |
| <div wire:loading wire:loading.absolute class="z-[109] inset-0 bg-white/10 dark:bg-black/[4%] backdrop-blur-[1px] rounded-md"></div> | |
| </div> | |
| {{ $slot }} | |
| <dt> | |
| <div class="flex flex-row justify-start items-center"> | |
| {{ $icon }} | |
| <p class="text-sm text-slate-500">{{ $title }}</p> | |
| </div> | |
| </dt> | |
| <dd class="mt-1 flex justify-between items-center"> | |
| <div class="flex items-baseline"> | |
| <p class="text-2xl font-semibold text-slate-900 dark:text-slate-100">{{ $mainMetric }}</p> | |
| <p class="ml-2 flex items-baseline text-sm font-semibold"> | |
| {{ $details }} | |
| </p> | |
| </div> | |
| </dd> | |
| @if ( isset($chart) ) | |
| <div class="absolute inset-y-0 w-24 right-4 p-0.5"> | |
| {{ $chart }} | |
| </div> | |
| @endif | |
| </div> | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div> | |
| <div class="w-full flex justify-between items-center"> | |
| <h3 class="block text-sm font-semibold tracking-tight leading-6 text-indigo-900 dark:text-slate-400">Statistics</h3> | |
| <div> | |
| <select wire:model="period" id="period"> | |
| <option value="Today">Today</option> | |
| <option value="Week">Week</option> | |
| <option value="Month">Month</option> | |
| <option value="Quarter">Quarter</option> | |
| <option value="Year">Year</option> | |
| <option value="All Time">All Time</option> | |
| </select> | |
| </div> | |
| </div> | |
| <dl class="mt-2.5 grid grid-cols-1 gap-3 lg:gap-5 2xl:grid-cols-3 overflow-visible"> | |
| <!-- start: User Metrics --> | |
| <x-metric-card > | |
| <x-slot:icon> | |
| <!-- User Icon here --> | |
| </x-slot:icon> | |
| <x-slot:title>Total Users</x-slot:title> | |
| <x-slot:mainMetric> | |
| {{ number_format($userMetrics['total'],0) }} | |
| </x-slot:mainMetric> | |
| <x-slot:details> | |
| <div class="{{ $userMetrics['change'] < 0 ? 'text-red-500' : 'text-green-500' }} flex text-[12px]"> | |
| <svg class="h-4 w-4 shrink-0 self-center {{ $userMetrics['change'] < 0 ? 'rotate-180' : ($userMetrics['change'] === 0 ? 'hidden' : '') }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M10 17a.75.75 0 01-.75-.75V5.612L5.29 9.77a.75.75 0 01-1.08-1.04l5.25-5.5a.75.75 0 011.08 0l5.25 5.5a.75.75 0 11-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0110 17z" clip-rule="evenodd" /></svg> | |
| <span class="sr-only"> Increased by </span> {{ $userMetrics['change'] }} | |
| </div> | |
| </x-slot:details> | |
| <x-slot:chart> | |
| <div class="flex w-full h-full self-center"> | |
| <x-circle-chart strokeColor="stroke-green-500" percentage="{{ $userMetrics['percentage'] }}" /> | |
| </div> | |
| </x-slot:chart> | |
| </x-metric-card> | |
| <!-- end: User Metrics --> | |
| <!-- start: Post Metrics --> | |
| <x-metric-card > | |
| <x-slot:icon> | |
| <!-- Post Icon here --> | |
| </x-slot:icon> | |
| <x-slot:title>Total Posts</x-slot:title> | |
| <x-slot:mainMetric> | |
| {{ number_format($postMetrics['total'],0) }} | |
| </x-slot:mainMetric> | |
| <x-slot:details> | |
| <div class="{{ $postMetrics['change'] < 0 ? 'text-red-500' : 'text-green-500' }} flex text-[12px]"> | |
| <svg class="h-4 w-4 shrink-0 self-center {{ $postMetrics['change'] < 0 ? 'rotate-180' : ($postMetrics['change'] === 0 ? 'hidden' : '') }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M10 17a.75.75 0 01-.75-.75V5.612L5.29 9.77a.75.75 0 01-1.08-1.04l5.25-5.5a.75.75 0 011.08 0l5.25 5.5a.75.75 0 11-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0110 17z" clip-rule="evenodd" /></svg> | |
| <span class="sr-only"> Increased by </span> {{ $postMetrics['change'] }} | |
| </div> | |
| </x-slot:details> | |
| <x-slot:chart> | |
| <div class="flex w-full h-full self-center"> | |
| <x-circle-chart strokeColor="stroke-green-500" percentage="{{ $postMetrics['percentage'] }}" /> | |
| </div> | |
| </x-slot:chart> | |
| </x-metric-card> | |
| <!-- end: Post Metrics --> | |
| <!-- start: Comment Metrics --> | |
| ... | |
| <!-- end: Comment Metrics --> | |
| </dl> | |
| </div> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment