Use a common trait to provide the timestamp function to both the DashboardModel and User classes, add an optional license key to the dashboard library_credits that adds a (license) link beside the project and add the license (as required by their license) to the fontawesome entry, and implement user profile image view, upload and deletion (with a default black question mark fallback) in the dashboard settings page

This commit is contained in:
Kevin MacMartin 2018-04-25 01:22:33 -04:00
parent 83a8dede40
commit 6afe85c2d9
13 changed files with 368 additions and 104 deletions

View file

@ -50,7 +50,7 @@ class Dashboard
*/ */
public static $library_credits = [ public static $library_credits = [
[ 'name' => 'Bootstrap', 'url' => 'https://getbootstrap.com' ], [ 'name' => 'Bootstrap', 'url' => 'https://getbootstrap.com' ],
[ 'name' => 'Font Awesome', 'url' => 'https://fontawesome.com' ], [ 'name' => 'Font Awesome', 'url' => 'https://fontawesome.com', 'license' => 'https://fontawesome.com/license' ],
[ 'name' => 'GreenSock', 'url' => 'https://greensock.com/gsap' ], [ 'name' => 'GreenSock', 'url' => 'https://greensock.com/gsap' ],
[ 'name' => 'jQuery', 'url' => 'https://jquery.org' ], [ 'name' => 'jQuery', 'url' => 'https://jquery.org' ],
[ 'name' => 'List.js', 'url' => 'http://listjs.com' ], [ 'name' => 'List.js', 'url' => 'http://listjs.com' ],

View file

@ -2,11 +2,11 @@
use App\Http\Requests; use App\Http\Requests;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Auth; use Auth;
use File; use File;
use Image; use Image;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use App\User; use App\User;
use App\Dashboard; use App\Dashboard;
@ -21,34 +21,14 @@ class DashboardController extends Controller {
} }
/** /**
* Dashboard home * Dashboard CMS
*/ */
public function getIndex() public function getIndex()
{ {
return view('dashboard.pages.home'); return view('dashboard.pages.home');
} }
/** // View Model Data
* Project credits
*/
public function getCredits()
{
return view('dashboard.pages.credits');
}
/**
* Dashboard settings
*/
public function getSettings()
{
return view('dashboard.pages.settings', [
'user' => User::find(Auth::id())
]);
}
/**
* Dashboard View
*/
public function getView($model) public function getView($model)
{ {
$model_class = Dashboard::getModel($model, 'view'); $model_class = Dashboard::getModel($model, 'view');
@ -66,9 +46,7 @@ class DashboardController extends Controller {
} }
} }
/** // Edit List of Model Rows
* Dashboard Edit List
*/
public function getEditList($model) public function getEditList($model)
{ {
$model_class = Dashboard::getModel($model, 'edit'); $model_class = Dashboard::getModel($model, 'edit');
@ -91,9 +69,7 @@ class DashboardController extends Controller {
} }
} }
/** // Create and Edit Model Item
* Dashboard Edit Item
*/
public function getEditItem($model, $id = 'new') public function getEditItem($model, $id = 'new')
{ {
$model_class = Dashboard::getModel($model, 'edit'); $model_class = Dashboard::getModel($model, 'edit');
@ -126,9 +102,7 @@ class DashboardController extends Controller {
} }
} }
/** // Export Spreadsheet of Model Data
* Dashboard Export: Export data as a spreadsheet
*/
public function getExport($model) public function getExport($model)
{ {
$model_class = Dashboard::getModel($model); $model_class = Dashboard::getModel($model);
@ -151,9 +125,7 @@ class DashboardController extends Controller {
} }
} }
/** // Reorder Model Rows
* Dashboard Reorder: Reorder rows
*/
public function postReorder(Request $request) public function postReorder(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
@ -181,9 +153,7 @@ class DashboardController extends Controller {
} }
} }
/** // Create and Update Model Item Data
* Dashboard Update: Create and update rows
*/
public function postUpdate(Request $request) public function postUpdate(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
@ -222,9 +192,7 @@ class DashboardController extends Controller {
} }
} }
/** // Upload Model Item Image
* Dashboard Image Upload: Upload images
*/
public function postImageUpload(Request $request) public function postImageUpload(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
@ -257,9 +225,7 @@ class DashboardController extends Controller {
} }
} }
/** // Upload Model Item File
* Dashboard File Upload: Upload files
*/
public function postFileUpload(Request $request) public function postFileUpload(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
@ -292,26 +258,7 @@ class DashboardController extends Controller {
} }
} }
/** // Delete Model Item
* 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
*/
public function deleteDelete(Request $request) public function deleteDelete(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
@ -357,9 +304,7 @@ class DashboardController extends Controller {
} }
} }
/** // Delete Model Item Image
* Dashboard Image Delete: Delete images
*/
public function deleteImageDelete(Request $request) public function deleteImageDelete(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
@ -390,9 +335,7 @@ class DashboardController extends Controller {
} }
} }
/** // Delete Model Item File
* Dashboard File Delete: Delete files
*/
public function deleteFileDelete(Request $request) public function deleteFileDelete(Request $request)
{ {
$this->validate($request, [ $this->validate($request, [
@ -424,4 +367,92 @@ class DashboardController extends Controller {
} }
} }
/**
* Dashboard settings
*/
public function getSettings()
{
return view('dashboard.pages.settings', [
'user' => User::find(Auth::id())
]);
}
// User Password Update
public function postUserPasswordUpdate(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';
}
}
// User Profile Image Upload
public function postUserProfileImageUpload(Request $request)
{
if ($request->hasFile('file')) {
$user = User::find(Auth::id());
if ($user !== null) {
$image = Image::make($request->file('file'));
$max_width = User::$profile_image_max['width'];
$max_height = User::$profile_image_max['height'];
if ($image->width() > $max_width || $image->height() > $max_height) {
$new_width = $max_width;
$new_height = ($new_width / $image->width()) * $image->height();
if ($new_height > $max_height) {
$new_height = $max_height;
$new_width = ($new_height / $image->height()) * $image->width();
}
$image->resize($new_width, $new_height);
}
file::makeDirectory(base_path() . '/public' . User::$profile_image_dir, 0755, true, true);
$image->save($user->profileImage(true, true));
$user->touch();
return $user->profileImage() . '?version=' . $user->timestamp();
} else {
return 'record-access-fail';
}
} else {
return 'file-upload-fail';
}
}
// User Profile Image Delete
public function deleteUserProfileImageDelete(Request $request)
{
$user = User::find(Auth::id());
if ($user !== null) {
$profile_image = $user->profileImage(true);
if ($profile_image === null) {
return 'image-not-exists-fail';
} else if (!unlink($profile_image)) {
return 'image-delete-fail';
}
return 'success';
} else {
return 'record-access-fail';
}
}
/**
* Credits Page
*/
public function getCredits()
{
return view('dashboard.pages.credits');
}
} }

View file

@ -4,9 +4,12 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Auth; use Auth;
use App\Traits\Timestamp;
class DashboardModel extends Model class DashboardModel extends Model
{ {
use Timestamp;
/* /*
* The dashboard page type * The dashboard page type
* *
@ -168,13 +171,4 @@ class DashboardModel extends Model
return $user_check; return $user_check;
} }
/**
* Returns the Unix timestamp of the latest update
*
* @return number
*/
public function timestamp() {
return strtotime($this->updated_at);
}
} }

15
app/Traits/Timestamp.php Normal file
View file

@ -0,0 +1,15 @@
<?php
namespace App\Traits;
trait Timestamp
{
/**
* Returns the Unix timestamp of the latest update
*
* @return number
*/
public function timestamp() {
return strtotime($this->updated_at);
}
}

View file

@ -5,10 +5,12 @@ namespace App;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Hash; use Hash;
use App\Traits\Timestamp;
class User extends Authenticatable class User extends Authenticatable
{ {
use Notifiable; use Notifiable;
use Timestamp;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -28,6 +30,30 @@ class User extends Authenticatable
'password', 'remember_token', 'api_token' 'password', 'remember_token', 'api_token'
]; ];
/**
* The default user profile image
*
* @var string
*/
public static $default_profile_image = '/img/profile.png';
/**
* The directory user profile uploads are stored in
*
* @var string
*/
public static $profile_image_dir = '/uploads/user/img/';
/**
* The maximum profile image width and height
*
* @var array
*/
public static $profile_image_max = [
'width' => 512,
'height' => 512
];
/** /**
* Update the user's password * Update the user's password
* *
@ -43,4 +69,21 @@ class User extends Authenticatable
return false; return false;
} }
/**
* Get user profile image
*
* @var string
*/
public function profileImage($show_full_path = false, $always_return_path = false)
{
$site_path = self::$profile_image_dir . $this->id . '-profile.png';
$file_path = base_path() . '/public' . $site_path;
if (file_exists($file_path) || $always_return_path) {
return $show_full_path ? $file_path : $site_path;
} else {
return null;
}
}
} }

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M0 84V56c0-13.3 10.7-24 24-24h112l9.4-18.7c4-8.2 12.3-13.3 21.4-13.3h114.3c9.1 0 17.4 5.1 21.5 13.3L312 32h112c13.3 0 24 10.7 24 24v28c0 6.6-5.4 12-12 12H12C5.4 96 0 90.6 0 84zm416 56v324c0 26.5-21.5 48-48 48H80c-26.5 0-48-21.5-48-48V140c0-6.6 5.4-12 12-12h360c6.6 0 12 5.4 12 12zm-272 68c0-8.8-7.2-16-16-16s-16 7.2-16 16v224c0 8.8 7.2 16 16 16s16-7.2 16-16V208zm96 0c0-8.8-7.2-16-16-16s-16 7.2-16 16v224c0 8.8 7.2 16 16 16s16-7.2 16-16V208zm96 0c0-8.8-7.2-16-16-16s-16 7.2-16 16v224c0 8.8 7.2 16 16 16s16-7.2 16-16V208z"/></svg>

After

Width:  |  Height:  |  Size: 600 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M296 384h-80c-13.3 0-24-10.7-24-24V192h-87.7c-17.8 0-26.7-21.5-14.1-34.1L242.3 5.7c7.5-7.5 19.8-7.5 27.3 0l152.2 152.2c12.6 12.6 3.7 34.1-14.1 34.1H320v168c0 13.3-10.7 24-24 24zm216-8v112c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V376c0-13.3 10.7-24 24-24h136v8c0 30.9 25.1 56 56 56h80c30.9 0 56-25.1 56-56v-8h136c13.3 0 24 10.7 24 24zm-124 88c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20zm64 0c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20z"/></svg>

After

Width:  |  Height:  |  Size: 533 B

BIN
public/img/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -625,6 +625,83 @@ function editItemInit() {
}); });
} }
function userProfileImageInit() {
const $form = $("#user-profile-image"),
$upload = $("#profile-image-upload"),
$delete = $("#profile-image-delete"),
$token = $("#token"),
$displayInner = $form.find(".image-display-inner").first();
let file,
submitting = false;
$upload.on("change", function() {
if ($upload.val() !== "" && !submitting) {
submitting = true;
askConfirmation("Update your user profile image?", function() {
// show the loading modal
showLoadingModal();
// add the image to the form data
file = new FormData();
file.append("file", $upload[0].files[0]);
// submit the form data
$.ajax({
type: "POST",
url: "/dashboard/user/profile-image-upload",
data: file,
processData: false,
contentType: false,
beforeSend: function(xhr) { xhr.setRequestHeader("X-CSRF-TOKEN", $token.val()); }
}).always(function(response) {
hideLoadingModal();
submitting = false;
if (/\.png\?version=/.test(response)) {
$displayInner.css({ backgroundImage: `url(${response})` });
$delete.removeClass("inactive");
} else {
showAlert("Failed to upload image");
}
});
}, function() {
$upload.val("");
submitting = false;
});
}
});
$delete.on("click", function() {
if (!submitting) {
submitting = true;
askConfirmation("Delete your profile image?", function() {
// delete the profile image
$.ajax({
type: "DELETE",
url: "/dashboard/user/profile-image-delete",
data: {
_token: $token.val()
}
}).always(function(response) {
if (response === "success") {
$displayInner.css({ backgroundImage: "none" });
$delete.addClass("inactive");
} else {
showAlert("Failed to delete profile image");
}
submitting = false;
});
}, function() {
submitting = false;
});
}
});
}
function userPasswordInit() { function userPasswordInit() {
const $form = $("#user-password"), const $form = $("#user-password"),
$submit = $form.find(".submit-button"), $submit = $form.find(".submit-button"),
@ -691,28 +768,23 @@ function userPasswordInit() {
// submit the update // submit the update
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "/dashboard/user-password", url: "/dashboard/user/password-update",
data: formData data: formData
}).always(function(response) { }).always(function(response) {
if (response === "success") {
hideLoadingModal(); hideLoadingModal();
showAlert("Password updated successfully", function() {
$inputs.val("").trigger("change");
});
} else {
submitting = false; submitting = false;
if (response === "old-password-fail") { if (response === "success") {
$inputs.val("").trigger("change");
showAlert("Password updated successfully");
} else if (response === "old-password-fail") {
$oldpass.addClass("error"); $oldpass.addClass("error");
showAlert("Old password is not correct"); showAlert("Old password is not correct");
} else { } else {
$newpass.addClass("error"); $newpass.addClass("error");
$newpassConfirmation.val(""); $newpassConfirmation.val("");
hideLoadingModal();
showAlert("New password must be at least 6 characters"); showAlert("New password must be at least 6 characters");
} }
}
}); });
} }
} }
@ -729,6 +801,10 @@ $(document).ready(function() {
editItemInit(); editItemInit();
} }
if ($("#user-profile-image").length) {
userProfileImageInit();
}
if ($("#user-password").length) { if ($("#user-password").length) {
userPasswordInit(); userPasswordInit();
} }

View file

@ -8,6 +8,9 @@
// Core // Core
@import "_fonts"; @import "_fonts";
// Supplementary
@import "mixins/**/*.scss";
// Colours // Colours
$c-text: #111; // text $c-text: #111; // text
$c-text-inactive: fade-out($c-text, 0.25); // inactive text $c-text-inactive: fade-out($c-text, 0.25); // inactive text
@ -802,6 +805,80 @@ body {
} }
} }
} }
&.user-profile-image {
display: block;
width: 100%;
max-width: 150px;
.image-display {
@include aspect-ratio(1, 1);
position: relative;
width: 100%;
border: 1px solid darken($c-dashboard-light, 10%);
border-radius: 3px;
background-position: center center;
background-size: cover;
background-repeat: no-repeat;
&-inner {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background-position: center center;
background-size: cover;
background-repeat: no-repeat;
}
}
.image-buttons {
margin-top: 20px;
display: flex;
justify-content: space-around;
input {
display: none;
}
.image-upload-button, .image-delete-button {
@include aspect-ratio(1, 1);
display: block;
width: 40px;
min-height: 0;
border: 1px solid darken($c-dashboard-light, 14%);
border-radius: 3px;
background-color: darken($c-dashboard-light, 10%);
background-position: center center;
background-size: 50% auto;
background-repeat: no-repeat;
font-size: 0px;
line-height: 1;
cursor: pointer;
&:hover {
background-color: darken($c-dashboard-light, 7%);
}
}
.image-upload-button {
background-image: url("/img/dashboard/upload.svg");
transition: background-color 150ms;
}
.image-delete-button {
background-image: url("/img/dashboard/trash-alt.svg");
opacity: 1;
transition: background-color 150ms, opacity 150ms;
&.inactive {
opacity: 0.35;
pointer-events: none;
}
}
}
}
} }
#loading-modal { #loading-modal {

View file

@ -16,7 +16,13 @@
<ul> <ul>
@foreach(App\Dashboard::$library_credits as $credit) @foreach(App\Dashboard::$library_credits as $credit)
<li><a href="{{ $credit['url'] }}" target="_blank" rel="noreferrer">{{ $credit['name'] }}</a></li> <li>
<a href="{{ $credit['url'] }}" target="_blank" rel="noreferrer">{{ $credit['name'] }}</a>
@if(array_key_exists('license', $credit))
(<a href="{{ $credit['license'] }}" target="_blank" rel="noreferrer">License</a>)
@endif
</li>
@endforeach @endforeach
</ul> </ul>
</div> </div>

View file

@ -8,6 +8,19 @@
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
<form id="user-profile-image" class="edit-item user-profile-image">
@set('profile_image', $user->profileImage())
<div class="image-display" style="background-image: url('{{ App\User::$default_profile_image }}')">
<div class="image-display-inner" style="background-image: url('{{ $profile_image !== null ? $profile_image : App\User::$default_profile_image }}')"></div>
</div>
<div class="image-buttons">
<input id="profile-image-upload" name="profile-image-upload" type="file" />
<label for="profile-image-upload" class="image-upload-button">Upload Profile Image</label>
<span id="profile-image-delete" class="image-delete-button {{ $profile_image === null ? 'inactive' : '' }}">Delete Profile Image</span>
</div>
</form>
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">

View file

@ -29,21 +29,28 @@ Route::get('/logout', 'Auth\LoginController@logout');
*/ */
Route::group([ 'prefix' => 'dashboard' ], function() { Route::group([ 'prefix' => 'dashboard' ], function() {
// Dashboard CMS
Route::get('/', 'DashboardController@getIndex'); Route::get('/', 'DashboardController@getIndex');
Route::get('/credits', 'DashboardController@getCredits');
Route::get('/settings', 'DashboardController@getSettings');
Route::get('/view/{model}', 'DashboardController@getView'); Route::get('/view/{model}', 'DashboardController@getView');
Route::get('/edit/{model}', 'DashboardController@getEditList'); Route::get('/edit/{model}', 'DashboardController@getEditList');
Route::get('/edit/{model}/{id}', 'DashboardController@getEditItem'); Route::get('/edit/{model}/{id}', 'DashboardController@getEditItem');
Route::get('/export/{model}', 'DashboardController@getExport'); Route::get('/export/{model}', 'DashboardController@getExport');
Route::post('/reorder', 'DashboardController@postReorder');
Route::post('/update', 'DashboardController@postUpdate');
Route::post('/image-upload', 'DashboardController@postImageUpload'); Route::post('/image-upload', 'DashboardController@postImageUpload');
Route::post('/file-upload', 'DashboardController@postFileUpload'); 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('/delete', 'DashboardController@deleteDelete');
Route::delete('/image-delete', 'DashboardController@deleteImageDelete'); Route::delete('/image-delete', 'DashboardController@deleteImageDelete');
Route::delete('/file-delete', 'DashboardController@deleteFileDelete'); Route::delete('/file-delete', 'DashboardController@deleteFileDelete');
// Dashboard Settings
Route::get('/settings', 'DashboardController@getSettings');
Route::post('/user/password-update', 'DashboardController@postUserPasswordUpdate');
Route::post('/user/profile-image-upload', 'DashboardController@postUserProfileImageUpload');
Route::delete('/user/profile-image-delete', 'DashboardController@deleteUserProfileImageDelete');
// Credits Page
Route::get('/credits', 'DashboardController@getCredits');
}); });
/* /*