Support dashboard customization through settings
MODIFY
app/Http/Controllers/SettingsController.php
app/Http/Controllers/SettingsController.php
@@ -128,6 +128,11 @@ class SettingsController extends Controller
return view('settings.preferences', compact('languages'));
}
+ public function getDashboard()
+ {
+ return view('settings.dashboard');
+ }
+
public function getBilling(Request $request)
{
$user = $request->user();

ADD
app/Http/Livewire/WidgetList.php
+<?php
+
+namespace App\Http\Livewire;
+
+use App\Models\Widget;
+use Livewire\Component;
+
+class WidgetList extends Component
+{
+ public $widgets;
+
+ protected $listeners = [
+ 'widgetCreated' => 'reloadWidgets'
+ ];
+
+ public function __construct()
+ {
+ $this->reloadWidgets();
+ }
+
+ public function reloadWidgets()
+ {
+ $this->widgets = request()->user()
+ ->widgets()
+ ->orderBy('sorting_index')
+ ->get();
+ }
+
+ public function up(Widget $widget)
+ {
+ if ($widget->sorting_index > 0) {
+ $previousWidget = Widget::where('user_id', $widget->user_id)
+ ->where('sorting_index', $widget->sorting_index - 1)
+ ->first();
+
+ $previousWidget->update([
+ 'sorting_index' => $widget->sorting_index
+ ]);
+
+ $widget->update([
+ 'sorting_index' => $widget->sorting_index - 1
+ ]);
+ }
+
+ $this->reloadWidgets();
+ }
+
+ public function down(Widget $widget)
+ {
+ if ($widget->sorting_index < count($this->widgets) - 1) {
+ $nextWidget = Widget::where('user_id', $widget->user_id)
+ ->where('sorting_index', $widget->sorting_index + 1)
+ ->first();
+
+ $nextWidget->update([
+ 'sorting_index' => $widget->sorting_index
+ ]);
+
+ $widget->update([
+ 'sorting_index' => $widget->sorting_index + 1
+ ]);
+ }
+
+ $this->reloadWidgets();
+ }
+
+ public function delete(Widget $widget)
+ {
+ $widget->delete();
+
+ /**
+ * Move widgets with out-of-sync sorting index up by 1
+ */
+
+ $outOfSyncWidgets = Widget::where('user_id', $widget->user_id)
+ ->whereRaw('sorting_index > ?', [$widget->sorting_index])
+ ->get();
+
+ foreach ($outOfSyncWidgets as $outOfSyncWidget) {
+ $outOfSyncWidget->update([
+ 'sorting_index' => $outOfSyncWidget->sorting_index - 1
+ ]);
+ }
+
+ $this->reloadWidgets();
+ }
+
+ public function render()
+ {
+ return view('livewire.widget-list');
+ }
+}

ADD
app/Http/Livewire/WidgetWizard.php
+<?php
+
+namespace App\Http\Livewire;
+
+use App\Models\Widget;
+use Livewire\Component;
+
+class WidgetWizard extends Component
+{
+ public $visible;
+ public $types;
+ public $selectedType;
+ public $expectedProperties;
+ public $providedProperties;
+
+ public function __construct()
+ {
+ $this->visible = false;
+ $this->types = array_keys(config('widgets.types'));
+ $this->selectedType = $this->types[0];
+ $this->reloadExpectedAndProvidedProperties();
+ }
+
+ public function reloadExpectedAndProvidedProperties()
+ {
+ $this->expectedProperties = config('widgets.types.' . $this->selectedType . '.properties');
+
+ /**
+ * Reset the provided properties
+ */
+
+ $this->providedProperties = [];
+
+ foreach ($this->expectedProperties as $key => $value) {
+ $this->providedProperties[$key] = $value[0]; // Assuming that $value is an array, and that it has items
+ }
+ }
+
+ public function toggle()
+ {
+ $this->visible = !$this->visible;
+ }
+
+ public function persist()
+ {
+ $user = request()->user();
+
+ $existingWidgetsAmount = $user->widgets()->count();
+
+ Widget::create([
+ 'user_id' => $user->id,
+ 'type' => $this->selectedType,
+ 'sorting_index' => $existingWidgetsAmount,
+ 'properties' => $this->providedProperties
+ ]);
+
+ $this->emit('widgetCreated');
+
+ $this->visible = false;
+ }
+
+ public function updatedSelectedType()
+ {
+ $this->reloadExpectedAndProvidedProperties();
+ }
+
+ public function render()
+ {
+ return view('livewire.widget-wizard');
+ }
+}

MODIFY
composer.json
composer.json
"laravel/dusk": "^6.11",
"laravel/framework": "^8.0",
"laravel/tinker": "^2.0",
+ "livewire/livewire": "^2.3",
"nesbot/carbon": "^2.32",
"stripe/stripe-php": "^7.44",
"symfony/dom-crawler": "^5.1",

MODIFY
composer.lock
composer.lock
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "7e3eda7006d222763fc4e12d99f92d46",
+ "content-hash": "d58c33aa1359b90a8e0d3222a459bcc6",
"packages": [
{
"name": "asm89/stack-cors",
],
"time": "2020-10-18T11:50:25+00:00"
},
+ {
+ "name": "livewire/livewire",
+ "version": "v2.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/livewire/livewire.git",
+ "reference": "781a1250dc8eab9121fd856ff0b6efca8c32756f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/livewire/livewire/zipball/781a1250dc8eab9121fd856ff0b6efca8c32756f",
+ "reference": "781a1250dc8eab9121fd856ff0b6efca8c32756f",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/database": "^7.0|^8.0",
+ "illuminate/support": "^7.0|^8.0",
+ "illuminate/validation": "^7.0|^8.0",
+ "php": "^7.2.5|^8.0",
+ "symfony/http-kernel": "^5.0"
+ },
+ "require-dev": {
+ "calebporzio/sushi": "^2.1",
+ "laravel/framework": "^7.0|^8.0",
+ "mockery/mockery": "^1.3.1",
+ "orchestra/testbench": "^5.0|^6.0",
+ "orchestra/testbench-dusk": "^5.2|^6.0",
+ "phpunit/phpunit": "^8.4|^9.0",
+ "psy/psysh": "@stable"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Livewire\\LivewireServiceProvider"
+ ],
+ "aliases": {
+ "Livewire": "Livewire\\Livewire"
+ }
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Livewire\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Caleb Porzio",
+ "email": "calebporzio@gmail.com"
+ }
+ ],
+ "description": "A front-end framework for Laravel.",
+ "funding": [
+ {
+ "url": "https://github.com/calebporzio",
+ "type": "github"
+ }
+ ],
+ "time": "2020-12-01T20:51:29+00:00"
+ },
{
"name": "monolog/monolog",
"version": "2.2.0",

MODIFY
resources/views/layout.blade.php
resources/views/layout.blade.php
display: none;
}
</style>
+ @livewireStyles
</head>
<body class="theme-{{ Auth::check() ? Auth::user()->theme : 'light' }}">
<div id="app">
</div>
<script src="/js/app.js"></script>
@yield('scripts')
+ @livewireScripts
</body>
</html>

ADD
resources/views/livewire/widget-list.blade.php
+<div class="box">
+ @foreach ($widgets as $widget)
+ <div wire:key="{{ $widget->id }}" class="box__section row">
+ <div class="row__column row__column--compact mr-1">
+ #{{ $widget->sorting_index + 1 }}
+ </div>
+ <div class="row__column">
+ {{ ucfirst($widget->type) }}
+ @if (count((array) $widget->properties))
+ ({{ ucfirst(str_replace('_', ' ', $widget->properties->{key($widget->properties)})) }})
+ @endif
+ </div>
+ <div class="row__column">
+ <button class="button link mr-1" wire:click="up({{ $widget }})" class="mr-1">
+ <i class="fas fa-arrow-alt-up"></i>
+ </button>
+ <button class="button link" wire:click="down({{ $widget }})">
+ <i class="fas fa-arrow-alt-down"></i>
+ </button>
+ </div>
+ <div class="row__column row__column--compact">
+ <button class="button link" wire:click="delete({{ $widget }})">
+ <i class="fas fa-times"></i>
+ </button>
+ </div>
+ </div>
+ @endforeach
+ @if (!count($widgets))
+ <div class="box__section text-center">There aren't any widgets yet</div>
+ @endif
+</div>

ADD
resources/views/livewire/widget-wizard.blade.php
+<div>
+ @if ($visible)
+ <div class="box">
+ <div class="box__section">
+ <div class="input input--small">
+ <label>Type</label>
+ <select wire:model="selectedType">
+ @foreach ($types as $type)
+ <option value="{{ $type }}">{{ ucfirst($type) }}</option>
+ @endforeach
+ </select>
+ </div>
+ @if (count($expectedProperties))
+ <div class="input input--small">
+ <label>Properties</label>
+ @foreach ($expectedProperties as $key => $value)
+ <div>
+ <div class="mb-05">{{ ucfirst($key) }}</div>
+ <select wire:model="providedProperties.{{ $key }}">
+ @foreach ($value as $y)
+ <option value="{{ $y }}">{{ ucfirst(str_replace('_', ' ', $y)) }}</option>
+ @endforeach
+ </select>
+ </div>
+ @endforeach
+ </div>
+ @endif
+ <div class="row">
+ <button class="button mr-1" wire:click="persist">Create</button>
+ <button class="button button--secondary" wire:click="toggle">Cancel</button>
+ </div>
+ </div>
+ </div>
+ @else
+ <button wire:click="toggle" class="button">Create</button>
+ @endif
+</div>

ADD
resources/views/settings/dashboard.blade.php
+@extends('settings.layout')
+
+@section('settings_title')
+ <h2 class="mb-3">{{ __('general.dashboard') }}</h2>
+@endsection
+
+@section('settings_body_formless')
+ <div class="mb-2">
+ <livewire:widget-wizard />
+ </div>
+ <livewire:widget-list />
+@endsection

MODIFY
resources/views/settings/layout.blade.php
resources/views/settings/layout.blade.php
<li><a href="/settings/profile"><i class="fas fa-user fa-sm"></i> {{ __('general.profile') }}</a></li>
<li><a href="/settings/account"><i class="fas fa-lock fa-sm"></i> {{ __('general.account') }}</a></li>
<li><a href="/settings/preferences"><i class="fas fa-sliders-h fa-sm"></i> {{ __('general.preferences') }}</a></li>
+ <li><a href="/settings/dashboard"><i class="fas fa-home fa-sm"></i> {{ __('general.dashboard') }}</a></li>
@if ($arePlansEnabled)
<li><a href="/settings/billing"><i class="fas fa-wallet fa-sm"></i> {{ __('general.billing') }}</a></li>
@endif

MODIFY
routes/web.php
routes/web.php
@@ -121,6 +121,7 @@ Route::group(['middleware' => ['auth']], function () {
Route::get('/settings/profile', [SettingsController::class, 'getProfile'])->name('profile');
Route::get('/settings/account', [SettingsController::class, 'getAccount'])->name('account');
Route::get('/settings/preferences', [SettingsController::class, 'getPreferences'])->name('preferences');
+ Route::get('/settings/dashboard', [SettingsController::class, 'getDashboard'])->name('dashboard');
Route::get('/settings/billing', [SettingsController::class, 'getBilling'])->name('billing')->middleware('stripe');
Route::post('/settings/billing/upgrade', [SettingsController::class, 'postUpgrade'])->name('billing.upgrade')->middleware('stripe');
Route::post('/settings/billing/cancel', [SettingsController::class, 'postCancel'])->name('billing.cancel')->middleware('stripe');

Support dashboard customization through settings