integrate image cropper for avatar upload/delete option/retouch

This commit is contained in:
2026-07-22 10:10:06 +07:00
parent 8afe7f9b38
commit 12f8d0bbda
5 changed files with 206 additions and 15 deletions
@@ -224,6 +224,13 @@ class PetaniController extends Controller
$validatedData = $this->validatePetani($request); $validatedData = $this->validatePetani($request);
if ($request->input('hapus_foto') == '1') {
if ($petani->foto && Storage::exists($petani->foto)) {
Storage::delete($petani->foto);
}
$validatedData['foto'] = null;
}
if ($request->hasFile('file_ktp')) { if ($request->hasFile('file_ktp')) {
$file_ktp = $request->file('file_ktp'); $file_ktp = $request->file('file_ktp');
$validatedData['path_ktp'] = $file_ktp->store('petani/ktp'); $validatedData['path_ktp'] = $file_ktp->store('petani/ktp');
@@ -235,6 +242,10 @@ class PetaniController extends Controller
$validatedData['file_kk'] = $file_kk->getClientOriginalName(); $validatedData['file_kk'] = $file_kk->getClientOriginalName();
} }
if ($request->hasFile('foto')) { if ($request->hasFile('foto')) {
if ($petani->foto && Storage::exists($petani->foto)) {
Storage::delete($petani->foto);
}
$file_foto = $request->file('foto'); $file_foto = $request->file('foto');
$validatedData['foto'] = $file_foto->store('petani/foto'); $validatedData['foto'] = $file_foto->store('petani/foto');
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -1,3 +1,17 @@
@push('style')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.min.css" />
<style>
#croppie-container {
width: 100%;
height: 350px;
}
</style>
@endpush
@push('scripts')
<script src="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.min.js"></script>
@extends('layout.main') @extends('layout.main')
@section('content') @section('content')
@@ -29,7 +43,7 @@
<div class=" d-flex align-items-center flex-column"> <div class=" d-flex align-items-center flex-column">
<img id="fotoPreview" <img id="fotoPreview"
class="img-fluid rounded mb-4" class="img-fluid rounded mb-4"
src="{{ isset($petani) && $petani?->foto ? asset('storage/' . $petani->foto) : asset('assets/img/avatars/1.png') }}" src="{{ isset($petani) && $petani?->foto ? asset('storage/' . $petani->foto) : asset('assets/img/avatars/3.jpg') }}"
height="120" width="120" height="120" width="120"
alt="Foto profil petani" alt="Foto profil petani"
data-bs-toggle="tooltip" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-placement="top"
@@ -230,6 +244,24 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="cropModal" tabindex="-1" aria-labelledby="cropModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="cropModalLabel">Sesuaikan Foto Profil</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="croppie-container"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="button" class="btn btn-primary" id="cropImageBtn">Potong & Simpan</button>
</div>
</div>
</div>
</div>
@endsection @endsection
@push('scripts') @push('scripts')
@@ -301,23 +333,73 @@
}); });
$('#add-petani-desa').select2(); $('#add-petani-desa').select2();
let croppieInstance = null;
$('#fotoPreview').on('click', function () { $('#fotoPreview').on('click', function () {
$('#foto').val('');
$('#foto').click(); $('#foto').click();
}); });
$('#foto').on('change', function (e) { $('#foto').on('change', function (e) {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
if(!file.type.match(/^image\/(png|jpeg|jpg)$/)) {
return;
}
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function (event) { reader.onload = function (event) {
$('#fotoPreview').attr('src', event.target.result); $('#cropModal').modal('show');
if (!croppieInstance) {
const el = document.getElementById('croppie-container');
croppieInstance = new Croppie(el, {
viewport: { width: 200, height: 200,},
boundary: { width: 300, height: 300 },
showZoomer: true,
enableOrientation: true
});
}
setTimeout(function() {
croppieInstance.bind({
url: event.target.result
});
}, 200);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
$('#cropImageBtn').on('click', function() {
croppieInstance.result({
type: 'base64',
format: 'png',
size: 'viewport'
}).then(function(base64) {
$('#fotoPreview').attr('src', base64);
function dataURLtoFile(dataurl, filename) {
let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type: mime});
}
let croppedFile = dataURLtoFile(base64, 'foto_profil_crop.png');
let dataTransfer = new DataTransfer();
dataTransfer.items.add(croppedFile);
document.getElementById('foto').files = dataTransfer.files;
$('#cropModal').modal('hide');
});
});
// 1 MB = 1048576 bytes // 1 MB = 1048576 bytes
$.validator.addMethod('maxfilesize', function (value, element, param) { $.validator.addMethod('maxfilesize', function (value, element, param) {
return this.optional(element) || (element.files[0] && element.files[0].size <= param); return this.optional(element) || (element.files[0] && element.files[0].size <= param);
@@ -1,5 +1,19 @@
@extends('layout.main') @extends('layout.main')
@push('style')
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.min.css" />
<style>
#croppie-container {
width: 100%;
height: 350px;
}
</style>
@endpush
@push('scripts')
<script src="https://cdnjs.cloudflare.com/ajax/libs/croppie/2.6.5/croppie.min.js"></script>
@section('content') @section('content')
<div class="container-xxl flex-grow-1 container-p-y"> <div class="container-xxl flex-grow-1 container-p-y">
@if ($errors->any()) @if ($errors->any())
@@ -23,19 +37,23 @@
<form id="editPetaniForm" action="{{ route('petani.update', $petani->id) }}" method="POST" enctype="multipart/form-data"> <form id="editPetaniForm" action="{{ route('petani.update', $petani->id) }}" method="POST" enctype="multipart/form-data">
@csrf @csrf
@method('PUT') @method('PUT')
<input type="hidden" name="hapus_foto" id="hapus_foto" value="0">
<input type="file" id="foto" name="foto" accept="image/png,image/jpeg,image/jpg" hidden> <input type="file" id="foto" name="foto" accept="image/png,image/jpeg,image/jpg" hidden>
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-3">
<div class="user-avatar-section"> <div class="user-avatar-section">
<div class=" d-flex align-items-center flex-column"> <div class=" d-flex align-items-center flex-column">
<img id="fotoPreview" <img id="fotoPreview"
class="img-fluid rounded mb-4" class="mb-3 shadow-sm rounded-3"
src="{{ $petani->foto ? route('petani.show-image', ['id' => $petani->id, 'field' => 'foto']) : asset('assets/img/avatars/1.png') }}" src="{{ $petani->foto ? route('petani.show-image', ['id' => $petani->id, 'field' => 'foto']) : asset('assets/img/avatars/3.jpg') }}"
height="120" width="120"
alt="Foto profil petani" alt="Foto profil petani"
data-bs-toggle="tooltip" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-placement="top"
data-bs-original-title="Klik untuk mengganti foto profil petani" data-bs-original-title="Klik untuk mengganti foto profil petani"
style="cursor:pointer; object-fit:cover;" /> style="width: 120px !important; height: 120px !important; cursor:pointer; object-fit:cover; display:block; margin: 0 auto;" />
<button type="button" id="btnHapusFoto" class="btn btn-sm btn-outline-danger mb-4" style="display: {{ $petani->foto ? 'block' : 'none' }};">
Hapus Foto
</button>
<div class="user-info text-center"> <div class="user-info text-center">
<h5><i>{{ $petani->nama }}</i></h5> <h5><i>{{ $petani->nama }}</i></h5>
<span class="badge bg-label-secondary">Petani {{ $petani->komoditas?->nama }}</span> <span class="badge bg-label-secondary">Petani {{ $petani->komoditas?->nama }}</span>
@@ -234,6 +252,24 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="cropModal" tabindex="-1" aria-labelledby="cropModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="cropModalLabel">Sesuaikan Foto Profil</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="croppie-container"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
<button type="button" class="btn btn-primary" id="cropImageBtn">Potong & Simpan</button>
</div>
</div>
</div>
</div>
@endsection @endsection
@push('scripts') @push('scripts')
@@ -306,24 +342,86 @@
}); });
$('#edit-petani-desa').select2(); $('#edit-petani-desa').select2();
let croppieInstance = null;
$('#fotoPreview').on('click', function () { $('#fotoPreview').on('click', function () {
$('#foto').val('');
$('#foto').click(); $('#foto').click();
}); });
$('#foto').on('change', function (e) { $('#foto').on('change', function (e) {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
if(!file.type.match(/^image\/(png|jpeg|jpg)$/)) {
return;
}
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function (event) { reader.onload = function (event) {
$('#fotoPreview').attr('src', event.target.result); $('#cropModal').modal('show');
if (!croppieInstance) {
const el = document.getElementById('croppie-container');
croppieInstance = new Croppie(el, {
viewport: { width: 200, height: 200 },
boundary: { width: '100%', height: 280 },
showZoomer: true,
enableOrientation: true
});
}
setTimeout(function() {
croppieInstance.bind({
url: event.target.result
});
}, 200);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
// 1 MB = 1048576 bytes // 1 MB = 1048576 bytes
$('#cropImageBtn').on('click', function() {
croppieInstance.result({
type: 'base64',
format: 'png',
size: 'viewport',
}).then(function(base64) {
$('#fotoPreview').attr('src', base64);
$('#hapus_foto').val('0');
$('#btnHapusFoto').show();
function dataURLtoFile(dataurl, filename) {
let arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while(n--){
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type: mime});
}
let croppedFile = dataURLtoFile(base64, 'foto_profil_crop.png');
let dataTransfer = new DataTransfer();
dataTransfer.items.add(croppedFile);
document.getElementById('foto').files = dataTransfer.files;
$('#cropModal').modal('hide');
});
});
$('#btnHapusFoto').on('click', function() {
$('#hapus_foto').val('1');
$('#foto').val('');
$('#fotoPreview').attr('src', "{{ asset('assets/img/avatars/3.jpg') }}");
$(this).hide();
});
$.validator.addMethod('maxfilesize', function (value, element, param) { $.validator.addMethod('maxfilesize', function (value, element, param) {
return this.optional(element) || (element.files[0] && element.files[0].size <= param); return this.optional(element) || (element.files[0] && element.files[0].size <= param);
}, 'File size must be less than {0} bytes.'); }, 'File size must be less than {0} bytes.');
@@ -7,10 +7,10 @@
<div class="d-flex justify-content-start align-items-center mb-3"> <div class="d-flex justify-content-start align-items-center mb-3">
<div class="avatar me-2"> <div class="avatar me-2">
@if (!isset($petani)) @if (!isset($petani))
<img src="{{ asset('assets/img/avatars/1.png') }}" alt="Foto Petani" class="rounded-circle" /> <img src="{{ asset('assets/img/avatars/3.jpg') }}" alt="Foto Petani"/>
@else @else
<img class="rounded-circle" alt="Foto Petani" <img alt="Foto Petani"
src="{{ $petani->foto ? route('petani.show-image', ['id' => $petani->id, 'field' => 'foto']) : asset('assets/img/avatars/1.png') }}" /> src="{{ $petani->foto ? route('petani.show-image', ['id' => $petani->id, 'field' => 'foto']) : asset('assets/img/avatars/3.jpg') }}" class="rounded-3"/>
@endif @endif
</div> </div>
<div class="d-flex flex-column"> <div class="d-flex flex-column">