diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 860dda6..f385c54 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -7,7 +7,9 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Redirect; +use Illuminate\Support\Facades\Storage; use Illuminate\View\View; +use Random\RandomException; class ProfileController extends Controller { @@ -23,10 +25,27 @@ public function edit(Request $request): View /** * Update the user's profile information. + * + * @throws RandomException */ public function update(ProfileUpdateRequest $request): RedirectResponse { - $request->user()->fill($request->validated()); + $data = $request->validated(); + + if (isset($data['image']) || $data['image'] === null) { + unset($data['image']); + } + + if ($request->hasFile('image')) { + if ($request->user()->image) { + Storage::delete("public/{$request->user()->image}"); + } + + $data['image'] = time().random_int(0, PHP_INT_MAX).'.'.$request->file('image')->extension(); + Storage::putFileAs('public', $request->file('image'), $data['image']); + } + + $request->user()->fill($data); if ($request->user()->isDirty('email')) { $request->user()->email_verified_at = null; diff --git a/app/Http/Controllers/TeacherController.php b/app/Http/Controllers/TeacherController.php index 1d3047a..22a35d8 100644 --- a/app/Http/Controllers/TeacherController.php +++ b/app/Http/Controllers/TeacherController.php @@ -10,6 +10,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; use Illuminate\View\View; use Yajra\DataTables\DataTables; @@ -39,8 +40,9 @@ class="btn p-0 dropdown-toggle hide-arrow" </div> </div>'; }) + ->editColumn('image', fn ($row) => '<a data-fslightbox href="'.$row->image_url.'"><img src="'.$row->image_url.'" alt="user-avatar" class="d-block rounded" height="30" width="30"></a>') ->editColumn('gender', fn ($row) => __('label.'.$row->gender)) - ->rawColumns(['action']) + ->rawColumns(['action', 'image']) ->make(); } @@ -65,6 +67,11 @@ public function store(StoreTeacherRequest $request): RedirectResponse $data['role'] = Role::TEACHER; $data['password'] = bcrypt('password'); + if ($request->hasFile('image')) { + $data['image'] = time().random_int(0, PHP_INT_MAX).'.'.$request->file('image')->extension(); + Storage::putFileAs('public', $request->file('image'), $data['image']); + } + User::create($data); return redirect()->route('teacher.index')->with('notification', $this->successNotification('notification.success_create', 'menu.teacher')); @@ -101,7 +108,18 @@ public function edit(User $teacher): View public function update(UpdateTeacherRequest $request, User $teacher): RedirectResponse { try { - $teacher->update($request->validated()); + $data = $request->validated(); + + if ($request->hasFile('image')) { + if ($teacher->image) { + Storage::delete("public/{$teacher->image}"); + } + + $data['image'] = time().random_int(0, PHP_INT_MAX).'.'.$request->file('image')->extension(); + Storage::putFileAs('public', $request->file('image'), $data['image']); + } + + $teacher->update($data); return back()->with('notification', $this->successNotification('notification.success_update', 'menu.teacher')); } catch (\Throwable $throwable) { diff --git a/app/Http/Requests/ProfileUpdateRequest.php b/app/Http/Requests/ProfileUpdateRequest.php index 7f6e549..78c6669 100644 --- a/app/Http/Requests/ProfileUpdateRequest.php +++ b/app/Http/Requests/ProfileUpdateRequest.php @@ -18,6 +18,11 @@ public function rules(): array return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], + 'image' => ['nullable', 'image', 'max:2048'], + 'phone' => ['required'], + 'address' => ['nullable'], + 'marital_status' => ['nullable'], + 'gender' => ['required'], ]; } @@ -26,6 +31,11 @@ public function attributes(): array return [ 'name' => __('field.name'), 'email' => __('field.email'), + 'image' => __('field.image'), + 'phone' => __('field.phone'), + 'address' => __('field.address'), + 'marital_status' => __('field.marital_status'), + 'gender' => __('field.gender'), ]; } } diff --git a/app/Models/User.php b/app/Models/User.php index c412b83..7dd2388 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Facades\Storage; class User extends Authenticatable { @@ -45,6 +46,16 @@ class User extends Authenticatable 'image_url', ]; + protected static function boot(): void + { + parent::boot(); + static::deleting(function (User $user) { + if ($user->image) { + Storage::delete("public/$user->image"); + } + }); + } + /** * Get the attributes that should be cast. * @@ -61,7 +72,13 @@ protected function casts(): array public function imageUrl(): Attribute { return new Attribute( - get: fn () => 'https://ui-avatars.com/api/?name='.$this->name, + get: function () { + if ($this->image) { + return asset('storage/'.$this->image); + } + + return asset('404_Black.jpg'); + } ); } diff --git a/lang/en/button.php b/lang/en/button.php index c1e4ba9..4fc921f 100644 --- a/lang/en/button.php +++ b/lang/en/button.php @@ -10,5 +10,6 @@ 'login' => 'Sign In', 'register' => 'Sign Up', 'delete_permanently' => 'Delete My Account', + 'upload' => 'Upload', ]; diff --git a/lang/en/label.php b/lang/en/label.php index 448ec1f..6f90594 100644 --- a/lang/en/label.php +++ b/lang/en/label.php @@ -41,4 +41,5 @@ 'male' => 'Male', 'female' => 'Female', 'required' => 'Required', + 'allowed_image_upload' => 'Allowed JPG or PNG. Max size of 2MB', ]; diff --git a/lang/id/button.php b/lang/id/button.php index 5812aee..08b8893 100644 --- a/lang/id/button.php +++ b/lang/id/button.php @@ -10,5 +10,6 @@ 'login' => 'Masuk', 'register' => 'Buat Akun', 'delete_permanently' => 'Hapus Akun Permanen', + 'upload' => 'Unggah', ]; diff --git a/lang/id/label.php b/lang/id/label.php index d1f81e4..8622315 100644 --- a/lang/id/label.php +++ b/lang/id/label.php @@ -41,4 +41,5 @@ 'male' => 'Laki-laki', 'female' => 'Perempuan', 'required' => 'Wajib', + 'allowed_image_upload' => 'Hanya boleh JPG atau PNG. Ukuran maksimal 2MB', ]; diff --git a/resources/views/layouts/dashboard.blade.php b/resources/views/layouts/dashboard.blade.php index a959cfb..4e0467e 100644 --- a/resources/views/layouts/dashboard.blade.php +++ b/resources/views/layouts/dashboard.blade.php @@ -47,6 +47,7 @@ @push('script') <script src="{{ asset('assets/vendor/libs/select2/select2.min.js') }}"></script> <script src="{{ asset('assets/vendor/libs/sweetalert2/sweetalert2.all.min.js') }}"></script> + <script src="{{ asset('assets/vendor/libs/fslightbox/fslightbox.js') }}"></script> <script> $('.select2').select2({theme: 'bootstrap-5'}); diff --git a/resources/views/pages/account/change-password.blade.php b/resources/views/pages/account/change-password.blade.php index f75b4b5..13b7fa7 100644 --- a/resources/views/pages/account/change-password.blade.php +++ b/resources/views/pages/account/change-password.blade.php @@ -26,17 +26,19 @@ @method('put') <div class="card-body"> <div class="row"> - <div class="mb-3 col-md-12"> + <div class="mb-3 col-md-6"> <x-forms.input-password name="current_password"/> </div> - <div class="mb-3 col-md-12"> + </div> + <div class="row"> + <div class="mb-3 col-md-6"> <x-forms.input-password name="password"/> </div> - <div class="mb-3 col-md-12"> + <div class="mb-3 col-md-6"> <x-forms.input-password name="password_confirmation"/> </div> </div> - <div class="mt-2"> + <div class="mt-4"> <button type="submit" class="btn btn-primary me-2">{{ __('button.submit') }}</button> <button type="reset" class="btn btn-outline-secondary">{{ __('button.reset') }}</button> </div> diff --git a/resources/views/pages/account/profile.blade.php b/resources/views/pages/account/profile.blade.php index ff9bed0..2adfa45 100644 --- a/resources/views/pages/account/profile.blade.php +++ b/resources/views/pages/account/profile.blade.php @@ -20,26 +20,63 @@ </li> </ul> <div class="card mb-4"> - <h5 class="card-header">{{ __('label.profile_information') }}</h5> - <!-- Account --> - <div class="card-body"> - <form id="formAccountSettings" action="{{ route('account.profile.update') }}" method="POST"> + <form id="formAccountSettings" action="{{ route('account.profile.update') }}" method="POST" + enctype="multipart/form-data"> + <h5 class="card-header">{{ __('label.profile_information') }}</h5> + <!-- Account --> + <div class="card-body"> + <div class="d-flex align-items-start align-items-sm-center gap-4"> + <a data-fslightbox href="{{ $user->image_url }}"> + <img src="{{ $user->image_url }}" + alt="user-avatar" class="d-block rounded" height="100" width="100" + id="uploadImage"> + </a> + <div class="button-wrapper"> + <label for="upload" class="btn btn-primary me-2 mb-4" tabindex="0"> + <span class="d-none d-sm-block">{{ __('button.upload') }}</span> + <i class="bx bx-upload d-block d-sm-none"></i> + <input type="file" id="upload" class="account-file-input" hidden="" name="image" + accept="image/png,image/jpeg"> + </label> + <button type="button" class="btn btn-outline-secondary account-image-reset mb-4"> + <i class="bx bx-reset d-block d-sm-none"></i> + <span class="d-none d-sm-block">{{ __('button.reset') }}</span> + </button> + <span class="error d-block">{{ $errors->first('image') }}</span> + <small class="text-muted mb-0 d-block">{{ __('label.allowed_image_upload') }}</small> + </div> + </div> + </div> + <hr class="m-0"> + <div class="card-body"> @csrf @method('patch') - <div class="row"> - <div class="mb-3 col-md-6"> - <x-forms.input name="name" :value="$user->name"/> - </div> - <div class="mb-3 col-md-6"> - <x-forms.input name="email" type="email" :value="$user->email"/> - </div> + <div class="mb-3"> + <x-forms.input name="name" :value="$user->name" required/> + </div> + <div class="mb-3"> + <x-forms.input name="email" type="email" :value="$user->email" required/> </div> - <div class="mt-2"> + <div class="mb-3"> + <x-forms.input-select2 name="gender" required :value="$user->gender" + :options="[ ['male', __('label.male')], ['female', __('label.female')] ]"/> + </div> + <div class="mb-3"> + <x-forms.input name="phone" type="phone" required :value="$user->phone"/> + </div> + <div class="mb-3"> + <x-forms.input-textarea name="address" :value="$user->address"/> + </div> + <div class="mb-3"> + <x-forms.input-select2 name="marital_status" :value="$user->marital_status" + :options="[ ['single', __('label.single')], ['married', __('label.married')], ['divorced', __('label.divorced')], ['widowed', __('label.widowed')] ]"/> + </div> + <div class="mt-4"> <button type="submit" class="btn btn-primary me-2">{{ __('button.submit') }}</button> <button type="reset" class="btn btn-outline-secondary">{{ __('button.reset') }}</button> </div> - </form> - </div> + </div> + </form> <!-- /Account --> </div> <div class="card"> @@ -87,6 +124,25 @@ accountActivationButton.attr('disabled', !checkBox.prop('checked')); }) - console.log($('#accountActivation')); + const imageElement = document.getElementById("uploadImage"); + const imageInputElement = document.querySelector(".account-file-input"); + const imageResetEl = document.querySelector(".account-image-reset"); + + if (imageElement) { + const originalImage = imageElement.src; + + imageInputElement.onchange = () => { + imageInputElement.files[0] && (imageElement.src = window.URL.createObjectURL(imageInputElement.files[0])); + imageElement.closest('a').setAttribute('href', imageElement.src); + refreshFsLightbox(); + } + + imageResetEl.onclick = () => { + imageInputElement.value = ""; + imageElement.src = originalImage; + imageElement.closest('a').setAttribute('href', imageElement.src); + refreshFsLightbox(); + } + } </script> @endpush diff --git a/resources/views/pages/teacher/create.blade.php b/resources/views/pages/teacher/create.blade.php index 165207f..993eed8 100644 --- a/resources/views/pages/teacher/create.blade.php +++ b/resources/views/pages/teacher/create.blade.php @@ -14,31 +14,87 @@ </div> <div class="card mb-4"> - <div class="card-body"> - <form action="{{ route('teacher.store') }}" method="post"> - @csrf + <form action="{{ route('teacher.store') }}" method="post" enctype="multipart/form-data"> + @csrf + <div class="card-header"> + <div class="d-flex align-items-start align-items-sm-center gap-4"> + <a data-fslightbox href="{{ asset('404_Black.jpg') }}"> + <img src="{{ asset('404_Black.jpg') }}" + alt="user-avatar" class="d-block rounded" height="100" width="100" id="uploadImage"> + </a> + <div class="button-wrapper"> + <label for="upload" class="btn btn-primary me-2 mb-4" tabindex="0"> + <span class="d-none d-sm-block">{{ __('button.upload') }}</span> + <i class="bx bx-upload d-block d-sm-none"></i> + <input type="file" id="upload" class="account-file-input" hidden="" name="image" + accept="image/png,image/jpeg"> + </label> + <button type="button" class="btn btn-outline-secondary account-image-reset mb-4"> + <i class="bx bx-reset d-block d-sm-none"></i> + <span class="d-none d-sm-block">{{ __('button.reset') }}</span> + </button> + + <small class="text-muted mb-0 d-block">{{ __('label.allowed_image_upload') }}</small> + </div> + </div> + </div> + <hr class="m-0"> + <div class="card-body"> <div class="mb-3"> - <x-forms.input name="name" required /> + <x-forms.input name="name" required/> </div> <div class="mb-3"> - <x-forms.input name="email" type="email" required /> + <x-forms.input name="email" type="email" required/> </div> <div class="mb-3"> - <x-forms.input-select2 name="gender" required :options="[ ['male', __('label.male')], ['female', __('label.female')] ]" /> + <x-forms.input-select2 name="gender" required + :options="[ ['male', __('label.male')], ['female', __('label.female')] ]"/> </div> <div class="mb-3"> - <x-forms.input name="phone" type="phone" required /> + <x-forms.input name="phone" type="phone" required/> </div> <div class="mb-3"> - <x-forms.input-textarea name="address" /> + <x-forms.input-textarea name="address"/> </div> <div class="mb-3"> - <x-forms.input-select2 name="marital_status" :options="[ ['single', __('label.single')], ['married', __('label.married')], ['divorced', __('label.divorced')], ['widowed', __('label.widowed')] ]" /> + <x-forms.input-select2 name="marital_status" + :options="[ ['single', __('label.single')], ['married', __('label.married')], ['divorced', __('label.divorced')], ['widowed', __('label.widowed')] ]"/> </div> - <button type="submit" class="btn btn-primary">{{ __('button.submit') }}</button> - <button type="reset" class="btn btn-secondary">{{ __('button.reset') }}</button> - </form> - </div> + <div class="mt-4"> + <button type="submit" class="btn btn-primary me-2">{{ __('button.submit') }}</button> + <button type="reset" class="btn btn-outline-secondary">{{ __('button.reset') }}</button> + </div> + </div> + </form> </div> @endsection + +@push('script') + <script> + document.addEventListener("DOMContentLoaded", (function (e) { + + const imageElement = document.getElementById("uploadImage"); + const imageInputElement = document.querySelector(".account-file-input"); + const imageResetEl = document.querySelector(".account-image-reset"); + + if (imageElement) { + const originalImage = imageElement.src; + + imageInputElement.onchange = () => { + imageInputElement.files[0] && (imageElement.src = window.URL.createObjectURL(imageInputElement.files[0])); + imageElement.closest('a').setAttribute('href', imageElement.src); + refreshFsLightbox(); + } + + imageResetEl.onclick = () => { + imageInputElement.value = ""; + imageElement.src = originalImage; + imageElement.closest('a').setAttribute('href', imageElement.src); + refreshFsLightbox(); + } + } + + })); + </script> +@endpush diff --git a/resources/views/pages/teacher/edit.blade.php b/resources/views/pages/teacher/edit.blade.php index 09afd6a..e709fd7 100644 --- a/resources/views/pages/teacher/edit.blade.php +++ b/resources/views/pages/teacher/edit.blade.php @@ -14,33 +14,89 @@ </div> <div class="card mb-4"> - <div class="card-body"> - <form action="{{ route('teacher.update', $teacher) }}" method="post"> + <form action="{{ route('teacher.update', $teacher) }}" method="post" enctype="multipart/form-data"> + <div class="card-header"> + <div class="d-flex align-items-start align-items-sm-center gap-4"> + <a data-fslightbox href="{{ $teacher->image_url }}"> + <img src="{{ $teacher->image_url }}" + alt="user-avatar" class="d-block rounded" height="100" width="100" id="uploadImage"> + </a> + <div class="button-wrapper"> + <label for="upload" class="btn btn-primary me-2 mb-4" tabindex="0"> + <span class="d-none d-sm-block">{{ __('button.upload') }}</span> + <i class="bx bx-upload d-block d-sm-none"></i> + <input type="file" id="upload" class="account-file-input" hidden="" name="image" + accept="image/png,image/jpeg"> + </label> + <button type="button" class="btn btn-outline-secondary account-image-reset mb-4"> + <i class="bx bx-reset d-block d-sm-none"></i> + <span class="d-none d-sm-block">{{ __('button.reset') }}</span> + </button> + + <small class="text-muted mb-0 d-block">{{ __('label.allowed_image_upload') }}</small> + </div> + </div> + </div> + <hr class="m-0"> + <div class="card-body"> @csrf @method('PUT') <input type="hidden" name="id" value="{{ $teacher->id }}"> <div class="mb-3"> - <x-forms.input name="name" required :value="$teacher->name" /> + <x-forms.input name="name" required :value="$teacher->name"/> </div> <div class="mb-3"> - <x-forms.input name="email" type="email" required :value="$teacher->email" /> + <x-forms.input name="email" type="email" required :value="$teacher->email"/> </div> <div class="mb-3"> - <x-forms.input-select2 name="gender" required :value="$teacher->gender" :options="[ ['male', __('label.male')], ['female', __('label.female')] ]" /> + <x-forms.input-select2 name="gender" required :value="$teacher->gender" + :options="[ ['male', __('label.male')], ['female', __('label.female')] ]"/> </div> <div class="mb-3"> - <x-forms.input name="phone" type="phone" required :value="$teacher->phone" /> + <x-forms.input name="phone" type="phone" required :value="$teacher->phone"/> </div> <div class="mb-3"> - <x-forms.input-textarea name="address" :value="$teacher->address" /> + <x-forms.input-textarea name="address" :value="$teacher->address"/> </div> <div class="mb-3"> - <x-forms.input-select2 name="marital_status" :value="$teacher->marital_status" :options="[ ['single', __('label.single')], ['married', __('label.married')], ['divorced', __('label.divorced')], ['widowed', __('label.widowed')] ]" /> + <x-forms.input-select2 name="marital_status" :value="$teacher->marital_status" + :options="[ ['single', __('label.single')], ['married', __('label.married')], ['divorced', __('label.divorced')], ['widowed', __('label.widowed')] ]"/> </div> - <button type="submit" class="btn btn-primary">{{ __('button.submit') }}</button> - <button type="reset" class="btn btn-secondary">{{ __('button.reset') }}</button> - </form> - </div> + <div class="mt-4"> + <button type="submit" class="btn btn-primary me-2">{{ __('button.submit') }}</button> + <button type="reset" class="btn btn-outline-secondary">{{ __('button.reset') }}</button> + </div> + </div> + </form> </div> @endsection + +@push('script') + <script> + document.addEventListener("DOMContentLoaded", (function (e) { + + const imageElement = document.getElementById("uploadImage"); + const imageInputElement = document.querySelector(".account-file-input"); + const imageResetEl = document.querySelector(".account-image-reset"); + + if (imageElement) { + const originalImage = imageElement.src; + + imageInputElement.onchange = () => { + imageInputElement.files[0] && (imageElement.src = window.URL.createObjectURL(imageInputElement.files[0])); + imageElement.closest('a').setAttribute('href', imageElement.src); + refreshFsLightbox(); + } + + imageResetEl.onclick = () => { + imageInputElement.value = ""; + imageElement.src = originalImage; + imageElement.closest('a').setAttribute('href', imageElement.src); + refreshFsLightbox(); + } + } + + })); + </script> +@endpush diff --git a/resources/views/pages/teacher/index.blade.php b/resources/views/pages/teacher/index.blade.php index 3e4c0ca..802269c 100644 --- a/resources/views/pages/teacher/index.blade.php +++ b/resources/views/pages/teacher/index.blade.php @@ -15,6 +15,7 @@ <table class="table"> <thead> <tr> + <th></th> <th>{{ __('field.name') }}</th> <th>{{ __('field.gender') }}</th> <th>{{ __('field.email') }}</th> @@ -38,21 +39,27 @@ <script> const locale = document.querySelector('html').getAttribute('lang'); - $('table.table').DataTable({ + const datatableObject = $('table.table').DataTable({ processing: true, serverSide: true, ajax: window.location.href, columns: [ + {data: 'image', name: 'image', orderable: false, searchable: false}, {data: 'name', name: 'name'}, {data: 'gender', name: 'gender', orderable: false, searchable: false}, {data: 'email', name: 'email'}, {data: 'phone', name: 'phone'}, {data: 'action', orderable: false, searchable: false}, ], + order: [[1, "desc"]], dom: '<"row px-4 my-3"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6 d-flex justify-content-center justify-content-md-end"f>>t<"row px-4 my-3"<"col-sm-12 col-md-6"i><"col-sm-12 col-md-6 d-flex justify-content-end"p>>', language: { url: `/assets/vendor/libs/datatables/language/${locale}.json` } }); + + datatableObject.on('draw', () => { + refreshFsLightbox(); + }); </script> @endpush