Link to the dashboard credits page from a new footer element instead of the user dropdown, organize the dashboard blades by folder now that we have so many of them, and implement user password reset functionality

This commit is contained in:
Kevin MacMartin 2018-04-24 20:38:04 -04:00
parent d06cae1c67
commit 83a8dede40
17 changed files with 292 additions and 56 deletions

View file

@ -2,10 +2,12 @@
use App\Http\Requests;
use Illuminate\Http\Request;
use Auth;
use File;
use Image;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use App\User;
use App\Dashboard;
class DashboardController extends Controller {
@ -23,7 +25,7 @@ class DashboardController extends Controller {
*/
public function getIndex()
{
return view('dashboard.home');
return view('dashboard.pages.home');
}
/**
@ -31,7 +33,17 @@ class DashboardController extends Controller {
*/
public function getCredits()
{
return view('dashboard.credits');
return view('dashboard.pages.credits');
}
/**
* Dashboard settings
*/
public function getSettings()
{
return view('dashboard.pages.settings', [
'user' => User::find(Auth::id())
]);
}
/**
@ -42,7 +54,7 @@ class DashboardController extends Controller {
$model_class = Dashboard::getModel($model, 'view');
if ($model_class != null) {
return view('dashboard.view', [
return view('dashboard.pages.view', [
'heading' => $model_class::getDashboardHeading($model),
'column_headings' => $model_class::getDashboardColumnData('headings'),
'model' => $model,
@ -62,7 +74,7 @@ class DashboardController extends Controller {
$model_class = Dashboard::getModel($model, 'edit');
if ($model_class != null) {
return view('dashboard.edit-list', [
return view('dashboard.pages.edit-list', [
'heading' => $model_class::getDashboardHeading($model),
'model' => $model,
'rows' => $model_class::getDashboardData(),
@ -101,7 +113,7 @@ class DashboardController extends Controller {
}
}
return view('dashboard.edit-item', [
return view('dashboard.pages.edit-item', [
'heading' => $model_class::getDashboardHeading($model),
'model' => $model,
'id' => $id,
@ -280,6 +292,23 @@ class DashboardController extends Controller {
}
}
/**
* User Password: Change the current user's password
*/
public function postUserPassword(Request $request)
{
$this->validate($request, [
'oldpass' => 'required|string|min:6',
'newpass' => 'required|string|min:6|confirmed'
]);
if (User::find(Auth::id())->updatePassword($request['oldpass'], $request['newpass'])) {
return 'success';
} else {
return 'old-password-fail';
}
}
/**
* Dashboard Delete: Delete rows
*/

View file

@ -4,6 +4,7 @@ namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Hash;
class User extends Authenticatable
{
@ -26,4 +27,20 @@ class User extends Authenticatable
protected $hidden = [
'password', 'remember_token', 'api_token'
];
/**
* Update the user's password
*
* @var string
*/
public function updatePassword($oldpass, $newpass)
{
if (Hash::check($oldpass, $this->password)) {
$this->password = Hash::make($newpass);
$this->save();
return true;
}
return false;
}
}

View file

@ -125,4 +125,4 @@ router.afterEach((to, from) => {
const App = new Vue({
router,
store
}).$mount("#page-content");
}).$mount("#vue-container");

View file

@ -1,3 +1,23 @@
const $loadingModal = $("#loading-modal"),
fadeTime = 250;
// show the loading modal
const showLoadingModal = function() {
$loadingModal.css({
visibility: "visible",
opacity: 1
});
};
// hide the loading modal
const hideLoadingModal = function() {
$loadingModal.css({ opacity: 0 });
setTimeout(function() {
$loadingModal.css({ visibility: "hidden" });
}, fadeTime);
};
// declare a reverse function for jquery
jQuery.fn.reverse = [].reverse;
@ -6,8 +26,7 @@ function askConfirmation(message, command, cancelCommand) {
const $confirmationModal = $("#confirmation-modal"),
$heading = $confirmationModal.find(".card-header"),
$cancelButton = $confirmationModal.find(".btn.cancel-button"),
$confirmButton = $confirmationModal.find(".btn.confirm-button"),
fadeTime = 250;
$confirmButton = $confirmationModal.find(".btn.confirm-button");
// close the confirmation modal and unbind its events
const closeConfirmationModal = function() {
@ -21,7 +40,10 @@ function askConfirmation(message, command, cancelCommand) {
// hide the modal
$confirmationModal.css({ opacity: 0 });
setTimeout(function() { $confirmationModal.css({ visibility: "hidden" }); }, fadeTime);
setTimeout(function() {
$confirmationModal.css({ visibility: "hidden" });
}, fadeTime);
};
// close the modal if the escape button is pressed
@ -67,8 +89,7 @@ function askConfirmation(message, command, cancelCommand) {
function showAlert(message, command) {
const $alertModal = $("#alert-modal"),
$message = $alertModal.find(".message"),
$acceptButton = $alertModal.find(".btn.accept-button"),
fadeTime = 250;
$acceptButton = $alertModal.find(".btn.accept-button");
// close the alert modal and unbind its events
const closeAlertModal = function() {
@ -160,7 +181,7 @@ function editListInit() {
if (response === "success") {
$listItem.slideUp(150, function() { $listItem.remove(); });
} else {
showAlert("ERROR: Failed to delete record");
showAlert("Failed to delete record");
}
});
});
@ -225,7 +246,7 @@ function editListInit() {
}
}).always(function(response) {
if (response !== "success") {
showAlert("ERROR: Sorting failed", function() {
showAlert("Sorting failed", function() {
document.location.reload(true);
});
}
@ -277,8 +298,6 @@ function editItemInit() {
$fileUploads = $(".file-upload"),
$imgUploads = $(".image-upload"),
$token = $("#token"),
$spinner = $("#loading-modal"),
fadeTime = 250,
model = $editItem.data("model"),
id = $editItem.data("id"),
operation = id === "new" ? "create" : "update";
@ -291,20 +310,6 @@ function editItemInit() {
minutes,
changes = false;
// show the loading modal
const showLoadingModal = function() {
$spinner.css({
visibility: "visible",
opacity: 1
});
};
// hide the loading modal
const hideLoadingModal = function() {
$spinner.css({ opacity: 0 });
setTimeout(function() { $spinner.css({ visibility: "hidden" }); }, fadeTime);
};
// fill the formData object with data from all the form fields
const getFormData = function() {
// function to add a column and value to the formData object
@ -390,7 +395,7 @@ function editItemInit() {
uploadFile(row_id, currentFile + 1);
} else {
hideLoadingModal();
showAlert("ERROR: Failed to upload file");
showAlert("Failed to upload file");
console.log(response.responseText);
submitting = false;
}
@ -436,7 +441,7 @@ function editItemInit() {
uploadImage(row_id, currentImage + 1);
} else {
hideLoadingModal();
showAlert("ERROR: Failed to upload image");
showAlert("Failed to upload image");
submitting = false;
}
});
@ -475,7 +480,7 @@ function editItemInit() {
if (response === "success") {
$(`#current-image-${name}`).slideUp(200);
} else {
showAlert("ERROR: Failed to delete the image: " + response);
showAlert("Failed to delete image: " + response);
}
submitting = false;
@ -510,7 +515,7 @@ function editItemInit() {
if (response === "success") {
$(`#current-file-${name}`).slideUp(200);
} else {
showAlert("ERROR: Failed to delete the file: " + response);
showAlert("Failed to delete file: " + response);
}
submitting = false;
@ -566,13 +571,16 @@ function editItemInit() {
});
setTimeout(function() {
// load the initial value into the editor
simplemde[column].value($this.attr("value"));
simplemde[column].codemirror.refresh();
// watch for changes to simplemde editor contents
simplemde[column].codemirror.on("change", contentChanged);
}, 500);
});
// initialize change events for back button
// watch for changes to input and select element contents
$editItem.find("input, select").on("input change", contentChanged);
// initialize back button
@ -609,7 +617,7 @@ function editItemInit() {
uploadImage(response.replace(/^id:/, ""), 0);
} else {
hideLoadingModal();
showAlert("ERROR: Failed to " + operation + " record");
showAlert("Failed to " + operation + " record");
submitting = false;
}
});
@ -617,11 +625,111 @@ function editItemInit() {
});
}
function userPasswordInit() {
const $form = $("#user-password"),
$submit = $form.find(".submit-button"),
$inputs = $form.find("input"),
$oldpass = $("#oldpass"),
$newpass = $("#newpass"),
$newpassConfirmation = $("#newpass_confirmation"),
$token = $("#token");
let formData = {},
submitting = false;
const getFormData = function() {
formData = {
oldpass: $oldpass.val(),
newpass: $newpass.val(),
newpass_confirmation: $newpassConfirmation.val(),
_token: $token.val()
};
};
// remove the error class from inputs and enable submit if all inputs have data when changes are made
$inputs.on("input change", function() {
let enableSubmit = true;
for (let i = 0; i < $inputs.length; i++) {
if ($inputs[i].value === "") {
enableSubmit = false;
break;
}
}
if (enableSubmit) {
$submit.removeClass("no-input");
} else {
$submit.addClass("no-input");
}
$inputs.removeClass("error");
});
// initialize submit button
$submit.on("click", function() {
if (!submitting) {
submitting = true;
// remove the error class from inputs
$inputs.removeClass("error");
// show the loading modal
showLoadingModal();
// populate the formData object
getFormData();
if (formData.newpass !== formData.newpass_confirmation) {
// fail with an error if the newpass and newpass_confirmation don't match
$newpassConfirmation.val("");
$newpass.addClass("error");
$newpassConfirmation.addClass("error");
showAlert("New passwords do not match");
submitting = false;
} else {
// submit the update
$.ajax({
type: "POST",
url: "/dashboard/user-password",
data: formData
}).always(function(response) {
if (response === "success") {
hideLoadingModal();
showAlert("Password updated successfully", function() {
$inputs.val("").trigger("change");
});
} else {
submitting = false;
if (response === "old-password-fail") {
$oldpass.addClass("error");
showAlert("Old password is not correct");
} else {
$newpass.addClass("error");
$newpassConfirmation.val("");
hideLoadingModal();
showAlert("New password must be at least 6 characters");
}
}
});
}
}
});
}
// run once the document is ready
$(document).ready(function() {
if ($("#edit-list").length) {
editListInit();
} else if ($("#edit-item").length) {
}
if ($("#edit-item").length) {
editItemInit();
}
if ($("#user-password").length) {
userPasswordInit();
}
});

View file

@ -10,7 +10,9 @@
// Colours
$c-text: #111; // text
$c-text-inactive: fade-out($c-text, 0.25); // inactive text
$c-text-light: #fff; // light text
$c-text-light-inactive: fade-out($c-text-light, 0.25); // inactive light text
$c-input-bg: #fff; // white
$c-dashboard-error: #a80000;
$c-dashboard-dark: #3e6087;
@ -33,6 +35,16 @@ body {
-webkit-overflow-scrolling: touch;
}
.site-content {
display: flex;
min-height: 100vh;
flex-direction: column;
.page-content {
flex-grow: 1;
}
}
.navbar {
margin-bottom: $grid-gutter-width;
border: 0;
@ -103,7 +115,8 @@ body {
}
.nav-link {
color: fade-out($c-text-light, 0.25);
color: $c-text-light-inactive;
transition: color 150ms;
&.active {
color: $c-text-light;
@ -138,9 +151,37 @@ body {
}
&.active, &:hover, &:focus, &:active {
background-color: fade-out(#000, 0.95);
color: $c-text;
}
&:hover, &:focus {
background-color: fade-out(#000, 0.97);
}
&.active {
background-color: fade-out(#000, 0.93);
}
}
}
}
.dashboard-footer {
margin-top: $grid-gutter-width;
width: 100%;
padding: 8px ($grid-gutter-width / 2);
background-color: $c-dashboard-dark;
text-align: right;
a {
color: $c-text-light-inactive;
transition: color 150ms;
&:hover, &:focus {
text-decoration: none;
}
&.active {
color: $c-text-light;
}
}
}
@ -663,6 +704,10 @@ body {
border: 1px solid darken($c-dashboard-light, 10%);
border-radius: 2px;
transition: border-color 150ms;
&.error {
border-color: $c-dashboard-error;
}
}
&[type="file"] {

View file

@ -0,0 +1,23 @@
@extends('dashboard.core', [
'heading' => 'Settings'
])
@section('dashboard-body')
<input type="hidden" id="token" value="{{ csrf_token() }}" />
<div class="container-fluid">
<div class="row">
<div class="col-12 col-md-8">
</div>
<div class="col-12 col-md-4">
<form id="user-password" class="edit-item">
<input class="text-input" type="password" name="oldpass" id="oldpass" placeholder="Old Password" value="" />
<input class="text-input" type="password" name="newpass" id="newpass" placeholder="New Password" value="" />
<input class="text-input" type="password" name="newpass_confirmation" id="newpass_confirmation" placeholder="Repeat New Password" value="" />
<button type="button" class="submit-button no-horizontal-margins btn btn-primary no-input">Update Password</button>
</form>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,3 @@
<footer class="dashboard-footer">
<a class="{{ $current_page == 'credits' ? 'active' : '' }}" href="/dashboard/credits">Credits</a>
</footer>

View file

@ -1,6 +1,4 @@
<nav class="navbar navbar-expand-lg">
@set('current_page', preg_replace([ '/^.*\//', '/\/.*/' ], [ '', '' ], Request::url()))
<a class="navbar-brand" href="/dashboard">
{{ env('APP_NAME') }} Dashboard
</a>
@ -27,7 +25,7 @@
@set('dropdown_id', preg_replace([ '/\ \ */', '/[^a-z\-]/' ], [ '-', '' ], strtolower($menu_item['title'])))
<li class="nav-item dropdown">
<span id="menu-dropdown-{{ $dropdown_id }}" class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span id="menu-dropdown-{{ $dropdown_id }}" class="nav-link dropdown-toggle {{ array_search($current_page, array_column($menu_item['submenu'], 'model')) !== false ? 'active' : '' }}" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ $menu_item['title'] }} <span class="caret"></span>
</span>
@ -47,12 +45,12 @@
@endforeach
<li class="nav-item dropdown">
<a id="user-dropdown" class="nav-link dropdown-toggle" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<a id="user-dropdown" class="nav-link dropdown-toggle {{ $current_page == 'settings' ? 'active' : '' }}" href="#" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ Auth::user()->name }} <span class="caret"></span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="user-dropdown">
<a class="dropdown-item {{ $current_page == 'credits' ? 'active' : '' }}" href="/dashboard/credits">Credits</a>
<a class="dropdown-item {{ $current_page == 'settings' ? 'active' : '' }}" href="/dashboard/settings">Settings</a>
<a class="dropdown-item" href="/logout">Logout</a>
</div>
</li>

View file

@ -36,14 +36,18 @@
</head>
<body class="{{ $device_mobile ? 'mobile-browser' : 'desktop-browser' }}">
@yield('page-top')
<div class="flex-wrapper">
<div class="site-content">
@yield('page-top')
<div id="page-content">
@yield('page-content')
<div class="page-content">
@yield('page-content')
</div>
@yield('page-bottom')
</div>
</div>
@yield('page-bottom')
@if(Config::get('app.debug'))
<script id="__bs_script__">//<![CDATA[
document.write("<script async src='http://{{ env('BS_HOST', 'localhost') }}:3000/browser-sync/browser-sync-client.js?version={{ env('CACHE_BUST') }}'><\/script>".replace("HOST", location.hostname));

View file

@ -1,4 +1,5 @@
@extends('templates.base', [ 'title' => 'Dashboard' ])
@set('current_page', preg_replace([ '/^.*\//', '/\/.*/' ], [ '', '' ], Request::url()))
@section('page-includes')
<script src="/js/lib-dashboard.js?version={{ env('CACHE_BUST') }}"></script>
@ -7,5 +8,9 @@
@endsection
@section('page-top')
@include('dashboard.nav')
@include('dashboard.sections.nav')
@endsection
@section('page-bottom')
@include('dashboard.sections.footer')
@endsection

View file

@ -7,15 +7,17 @@
@endsection
@section('page-content')
<nav-component></nav-component>
<div id="vue-container">
<nav-component></nav-component>
<div class="flex-wrapper">
<div class="page-container">
<div id="router-view" class="main-content">
<router-view></router-view>
<div class="flex-wrapper">
<div class="page-container">
<div id="router-view" class="main-content">
<router-view></router-view>
</div>
<footer-component></footer-component>
</div>
<footer-component></footer-component>
</div>
</div>
@endsection

View file

@ -31,6 +31,7 @@ Route::get('/logout', 'Auth\LoginController@logout');
Route::group([ 'prefix' => 'dashboard' ], function() {
Route::get('/', 'DashboardController@getIndex');
Route::get('/credits', 'DashboardController@getCredits');
Route::get('/settings', 'DashboardController@getSettings');
Route::get('/view/{model}', 'DashboardController@getView');
Route::get('/edit/{model}', 'DashboardController@getEditList');
Route::get('/edit/{model}/{id}', 'DashboardController@getEditItem');
@ -39,6 +40,7 @@ Route::group([ 'prefix' => 'dashboard' ], function() {
Route::post('/file-upload', 'DashboardController@postFileUpload');
Route::post('/update', 'DashboardController@postUpdate');
Route::post('/reorder', 'DashboardController@postReorder');
Route::post('/user-password', 'DashboardController@postUserPassword');
Route::delete('/delete', 'DashboardController@deleteDelete');
Route::delete('/image-delete', 'DashboardController@deleteImageDelete');
Route::delete('/file-delete', 'DashboardController@deleteFileDelete');