Component

Module

Image Enhancement (Single Image)

Drag & Drop Upload, Crop and save as new Image

Directory

File Folder Link
backend.php \\SYNAS\Allan\DOCUMENTATION\Component\Image Enhancement (Single Image)\rckuc\wolf\app\layouts
form.php \\SYNAS\Allan\DOCUMENTATION\Component\Image Enhancement (Single Image)\rckuc\wolf\app\views\price_package
style.css \\SYNAS\Allan\DOCUMENTATION\Component\Image Enhancement (Single Image)\rckuc\wolf\admin\themes\black_and_white
img_handler.js & cropper.js \\SYNAS\Allan\DOCUMENTATION\Component\Image Enhancement (Single Image)\rckuc\wolf\admin\javascript
PricePackageController.php \\SYNAS\Allan\DOCUMENTATION\Component\Image Enhancement (Single Image)\rckuc\wolf\app\controllers

 

Step 1

Update: backend.php

Insert cropper.js and img_handler.js backend.php in <head> section 

<head> <!-- Include Cropper CSS and JS --> <script type="text/javascript" charset="utf-8" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/cropper-1.6.1.min.js"></script> <link rel="stylesheet" href="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/cropper-1.6.1.min.css"></link> <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/img_handler.js"></script> </head>
<head>
    <!-- Include Cropper CSS and JS -->
    <script type="text/javascript" charset="utf-8" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/cropper-1.6.1.min.js"></script>
    <link rel="stylesheet" href="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/cropper-1.6.1.min.css"></link>

    <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/img_handler.js"></script>
</head>

Step 2

Update: form.php (or your create.php and view.php)

Note : For <input type="file" class="imgFile"> , you can set these attribute to validate your image when upload:

  1. data-min-size = '500' (in kb)
  2. data-max-size = '500' (in kb)
  3. data-min-width = '500' (in px)
  4. data-max-width = '500' (in px)
  5. data-min-height = '500' (in px)
  6. data-max-height = '500' (in px)
  7. data-ratio = '4:3'
  8. data-crop = 'false' (to disable crop)
<form id="price_package_form" action="<?php echo $action=='edit' ? get_url('price_package/editpricepackage/'.$price_package->id) : get_url('price_package/addpricepackage'); ?>" method="post" enctype="multipart/form-data" style="position: relative;"> <input type=hidden name="action" value="add"> <div class="float_right_photo" style="<?php echo $action=='edit' ? '' : 'margin-top:65px;'?>"> <h3><?php echo __('Package Icon (Optional)'); ?></h3> <div id="meta-pages" class="pages"> <div class="drop-area" data-class="package_icon"> <input type="hidden" name="crop_package_icon" id="hidden_package_icon"> <input type="file" class="imgFile" name="package_icon" accept=".png, .jpg, .jpeg, .webp" data-max-size="500" data-max-width="500" data-max-height="500" hidden /> <img class="ori_img" src="<?php echo !empty($price_package) && $price_package->package_icon ? (URL_PUBLIC . 'public/price_package/package_icon/' . $price_package->id . '/' . rawurlencode($price_package->package_icon)) : 'none'; ?>"></img> <div class="previewContainer"> <button class="previewImg" type="button" data-url="<?php echo !empty($price_package) && $price_package->package_icon ? (URL_PUBLIC . 'public/price_package/package_icon/' . $price_package->id . '/' . rawurlencode($price_package->package_icon)) : 'none'; ?>" style="<?php echo !empty($price_package) && $price_package->package_icon ? 'background-image: url(' . (URL_PUBLIC . 'public/price_package/package_icon/' . $price_package->id . '/' . rawurlencode($price_package->package_icon)) . ');' : 'none'; ?>"> </button> <span class="plusIcon">+</span> <div class="crop_icon"> <img src="<?php echo URL_PUBLIC ?>wolf/admin/images/crop.png" alt="crop" /> </div> </div> <p>Drag & Drop or <span class="clickUpload">Browse</span></p> <span class="note">(Maximun Size: 500KB)</span> <span class="note">(Maximum: 500px x 500px)</span> <span class="note">(Allowed file type : .png, .jpg, .jpeg, .webp)</span> </div> </div> </div> </form> <?php $ratios = Option::findByCategory("ratio"); usort($ratios, function($a, $b) { // Put "Free" on top if ($a->name === 'Free' && $b->name !== 'Free') return -1; if ($b->name === 'Free' && $a->name !== 'Free') return 1; // Otherwise sort alphabetically return strcasecmp($a->name, $b->name); }); ?> <div id="cropModal"> <div class="modal-content"> <h2 class="modal-title">Crop Image</h2> <div class="aspect-ratio-selector"> <label for="aspectRatio">Aspect Ratio:</label> <select id="aspectRatio"> <?php foreach ($ratios as $ratio) : ?> <option value="<?php echo $ratio->code; ?>"><?php echo $ratio->name; ?></option> <?php endforeach ?> </select> </div> <div class="crop-area"> <img id="cropImage" /> </div> <div class="modal-actions"> <button id="cropConfirm" type="button">Crop & Save</button> <button id="closeCropModal" type="button">Close</button> </div> </div> </div>
<form id="price_package_form" action="<?php echo $action=='edit' ? get_url('price_package/editpricepackage/'.$price_package->id) : get_url('price_package/addpricepackage'); ?>" method="post" enctype="multipart/form-data" style="position: relative;">
	<input type=hidden name="action" value="add">

    <div class="float_right_photo" style="<?php echo $action=='edit' ? '' : 'margin-top:65px;'?>">
        <h3><?php echo __('Package Icon (Optional)'); ?></h3>
        <div id="meta-pages" class="pages">
            <div class="drop-area" data-class="package_icon">
                <input type="hidden" name="crop_package_icon" id="hidden_package_icon">
                <input type="file" class="imgFile" name="package_icon" accept=".png, .jpg, .jpeg, .webp" data-max-size="500" data-max-width="500" data-max-height="500" hidden />
                <img class="ori_img" src="<?php echo !empty($price_package) && $price_package->package_icon  ? (URL_PUBLIC . 'public/price_package/package_icon/' . $price_package->id . '/' . rawurlencode($price_package->package_icon)) : 'none'; ?>"></img>
                <div class="previewContainer">
                    <button class="previewImg" type="button" data-url="<?php echo !empty($price_package) && $price_package->package_icon  ? (URL_PUBLIC . 'public/price_package/package_icon/' . $price_package->id . '/' . rawurlencode($price_package->package_icon))  : 'none'; ?>"
						style="<?php echo !empty($price_package) && $price_package->package_icon  ? 'background-image: url(' . (URL_PUBLIC . 'public/price_package/package_icon/' . $price_package->id . '/' . rawurlencode($price_package->package_icon)) . ');'  : 'none'; ?>">
                    </button>
                    <span class="plusIcon">+</span>
					<div class="crop_icon">
						<img src="<?php echo URL_PUBLIC ?>wolf/admin/images/crop.png" alt="crop" />
					</div>
                </div>
                <p>Drag & Drop or <span class="clickUpload">Browse</span></p>
                <span class="note">(Maximun Size: 500KB)</span>
                <span class="note">(Maximum: 500px x 500px)</span>
                <span class="note">(Allowed file type : .png, .jpg, .jpeg, .webp)</span>
            </div>
        </div>
	</div>

</form>

<?php $ratios = Option::findByCategory("ratio"); 
        usort($ratios, function($a, $b) {
            // Put "Free" on top
            if ($a->name === 'Free' && $b->name !== 'Free') return -1;
            if ($b->name === 'Free' && $a->name !== 'Free') return 1;

            // Otherwise sort alphabetically
            return strcasecmp($a->name, $b->name);
        });
?>
<div id="cropModal">
  <div class="modal-content">
    <h2 class="modal-title">Crop Image</h2>
    <div class="aspect-ratio-selector">
        <label for="aspectRatio">Aspect Ratio:</label>
        <select id="aspectRatio">
            <?php foreach ($ratios as $ratio) : ?>
                <option value="<?php echo $ratio->code; ?>"><?php echo $ratio->name; ?></option>
            <?php endforeach ?>
        </select>
    </div>
    <div class="crop-area">
      <img id="cropImage" />
    </div>
    <div class="modal-actions">
      <button id="cropConfirm" type="button">Crop & Save</button>
      <button id="closeCropModal" type="button">Close</button>
    </div>
  </div>
</div>

Step 3

Update: style.css

/* Drag and Drop Image Design */ .drop-area, #drop-area-multiple { border: 2px dashed #d0d5dd; padding: 2.5rem 1rem 1rem 1rem; text-align: center; border-radius: 12px; background-color: #ffffff; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); transition: border-color 0.3s ease, background-color 0.3s ease; position: relative; width: max-content; } .drop-area { cursor: pointer; } #drop-multiple-container { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); height: 80%; width: 80%; display: none; align-items: center; justify-content: center; background-color: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); z-index: 100; } #drop-area-multiple { height: 80%; width: 80%; display: flex; flex-direction: column; align-items: center; justify-content: space-around; } .drop-area.dragging, #drop-area-multiple.dragging { border-color: #3b82f6; background-color: #f0f8ff; } .previewImg, .previewVideo { box-sizing: border-box; width: 200px; height: 200px; background: center / contain no-repeat; background-color: rgb(237, 237, 237); border: 2px solid #e5e7eb; border-radius: 12px; cursor: pointer; transition: transform 0.2s ease; overflow: hidden; } #drop-area-multiple .previewImg { cursor: unset; } .previewImg:hover, .previewVideo:hover { transform: scale(1.02); } .previewContainer, .multiplePreviewList { position: relative; display: inline-block; } .previewTemplate { display: none; } .multiplePreviewList { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 2.5rem 1.5rem; padding: 2rem; overflow-y: auto; border-radius: 12px; margin: 1rem 0; /* min-height: 15rem; */ } .img_icon { cursor: pointer; height: 10rem; width: 10rem; opacity: 0.1; transition: transform 0.2s ease; } .img_icon:hover { transform: scale(1.02); } .sequence { height: 1rem; width: 1rem; position: absolute; top: -1.8rem; left: 0.2rem; padding: 0.3rem; border-radius: 100%; background-color: lightgray; } .close_icon { position: absolute; top: 0; right: 0; height: 1.5rem; width: 1.5rem; padding: 1rem; cursor: pointer; } .close_icon:hover { opacity: 0.7; } #multiple-upload-btn { display: none; background-color: #007bff; color: white; border: none; padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 8px; cursor: pointer; transition: background-color 0.3s ease; position: absolute; bottom: 1rem; right: 1rem; } #multiple-upload-btn:hover { background-color: #0056b3; } .plusIcon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 2.5rem; color: #9ca3af; pointer-events: none; transition: opacity 0.3s ease; opacity: 1; } .previewImg[style*="background-image"]:not([style*='none']) + .plusIcon { opacity: 0; } .previewVideo:has(source[src]):not(:has(source[src=""])) + .plusIcon { opacity: 0; } #banner_image, #banner_video { display: none; } .drop-area p, #drop-area-multiple p { font-size: 0.95rem; color: #6b7280; margin: 0.5rem 0; } .drop-area span.clickUpload, #drop-area-multiple span.clickUpload { color: #3b82f6; text-decoration: underline; font-weight: 500; cursor: pointer; transition: color 0.2s ease; } .drop-area span.clickUpload:hover, #drop-area-multiple span.clickUpload:hover { color: #2563eb; } .drop-area .note, #drop-area-multiple .note { display: block; font-size: 0.75rem; color: #9ca3af; margin-top: 0.5rem; } /* For Cropper */ #cropModal { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.5); /* dark overlay */ display: none; z-index: 9999; } .modal-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); padding: 2rem; width: 90%; max-width: 600px; max-height: 90vh; display: flex; flex-direction: column; align-items: center; } .modal-title { margin-bottom: 1rem; font-size: 1.5rem; font-weight: 600; color: #333; } .crop-area { max-height: 400px; overflow: hidden; margin-bottom: 1.5rem; } .crop-area img { max-width: 100%; height: auto; display: block; } .modal-actions { display: flex; justify-content: center; gap: 1rem; width: 100%; } .modal-actions button#cropConfirm, .modal-actions button#closeCropModal { background-color: #007bff; color: white; border: none; padding: 0.75rem 1.5rem; font-size: 1rem; border-radius: 8px; cursor: pointer; transition: background-color 0.3s ease; } .modal-actions button#closeCropModal { background-color: darkgray; } .modal-actions button#cropConfirm:hover { background-color: #0056b3; } .modal-actions button#closeCropModal:hover { background-color: gray; } .aspect-ratio-selector { margin-bottom: 1rem; display: flex; align-items: center; justify-content: center; gap: 1rem; } .aspect-ratio-selector select { padding: 0.5rem 0.75rem; border: 1px solid #ccc; border-radius: 8px; background-color: #fff; outline: none; min-width: 200px; } .crop_icon, .remove_icon { border-radius: 100%; position: absolute; top: 0.2rem; right: 0.2rem; padding: 0.3rem; height: 1.2rem; width: 1.2rem; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; cursor: pointer; } .crop_icon { right: 2.5rem; } .crop_icon > img, .remove_icon > img { height: 100%; width: 100%; } .drop-area .crop_icon, .drop-area .remove_icon { display: none; } .crop_icon:hover, .remove_icon:hover { transform: scale(1.2); } #drop-area-multiple .crop_icon, #drop-area-multiple .remove_icon { top: -1.8rem; } .ori_img { display: none; } /* FOR UPLOAD PROGRESS BAR */ #upload-progress { display: none; } .progress-container { position: absolute; top: 0; left: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 101; background-color: rgba(0, 0, 0, 0.3); backdrop-filter: blur(10px); } .progress-container p { font-size: 1.5rem; font-weight: 700; margin: 0 0 3rem 0; color: white; } .progress-container p::after { content: ''; display: inline-block; width: 1.2em; text-align: left; animation: dots 1s steps(4, end) infinite; } @keyframes dots { 0% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75%,100% { content: '...'; } } .progress-info { display: flex; justify-content: space-between; margin-bottom: 1rem; font-size: 1rem; color: white; width: 65%; } .progress-bar-wrapper { width: 70%; height: 10px; background-color: #e5e7eb; border-radius: 999px; overflow: hidden; margin-bottom: 5rem; } .progress-bar-fill { height: 100%; width: 0%; background-color: #3b82f6; transition: width 0.2s ease; }
/* Drag and Drop Image Design */
.drop-area,
#drop-area-multiple {
    border: 2px dashed #d0d5dd;
    padding: 2.5rem 1rem 1rem 1rem;
    text-align: center;
    border-radius: 12px;
    background-color: #ffffff;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
    transition: border-color 0.3s ease, background-color 0.3s ease;
    position: relative;
    width: max-content;
}

.drop-area {
    cursor: pointer;
}

#drop-multiple-container {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  height: 80%;
  width: 80%;
  display: none;
  align-items: center;
  justify-content: center;
  background-color: white;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
  z-index: 100;
}

#drop-area-multiple {
  height: 80%;
  width: 80%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-around;
}

.drop-area.dragging,
#drop-area-multiple.dragging {
    border-color: #3b82f6;
    background-color: #f0f8ff;
}

.previewImg,
.previewVideo {
    box-sizing: border-box;
    width: 200px;
    height: 200px;
    background: center / contain no-repeat;
    background-color: rgb(237, 237, 237);
    border: 2px solid #e5e7eb;
    border-radius: 12px;
    cursor: pointer;
    transition: transform 0.2s ease;
    overflow: hidden;
}

#drop-area-multiple .previewImg {
  cursor: unset;
}

.previewImg:hover,
.previewVideo:hover {
    transform: scale(1.02);
}

.previewContainer, .multiplePreviewList {
    position: relative;
    display: inline-block;
}

.previewTemplate {
  display: none;
}

.multiplePreviewList {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 2.5rem 1.5rem;
  padding: 2rem;
  overflow-y: auto;
  border-radius: 12px;
  margin: 1rem 0;
  /* min-height: 15rem; */
}

.img_icon {
  cursor: pointer;
  height: 10rem;
  width: 10rem;
  opacity: 0.1;
  transition: transform 0.2s ease;
}

.img_icon:hover {
  transform: scale(1.02);
}

.sequence {
  height: 1rem;
  width: 1rem;
  position: absolute;
  top: -1.8rem;
  left: 0.2rem;
  padding: 0.3rem;
  border-radius: 100%;
  background-color: lightgray;
}

.close_icon {
  position: absolute;
  top: 0;
  right: 0;
  height: 1.5rem;
  width: 1.5rem;
  padding: 1rem;
  cursor: pointer;
}

.close_icon:hover {
  opacity: 0.7;
}

#multiple-upload-btn {
  display: none;
  background-color: #007bff;
  color: white;
  border: none;
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 0.3s ease;
  position: absolute;
  bottom: 1rem;
  right: 1rem;
}

#multiple-upload-btn:hover {
  background-color: #0056b3;
}

.plusIcon {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 2.5rem;
    color: #9ca3af;
    pointer-events: none;
    transition: opacity 0.3s ease;
    opacity: 1;
}

.previewImg[style*="background-image"]:not([style*='none']) + .plusIcon {
    opacity: 0;
}

.previewVideo:has(source[src]):not(:has(source[src=""])) + .plusIcon {
  opacity: 0;
}

#banner_image,
#banner_video {
  display: none;
}

.drop-area p,
#drop-area-multiple p {
    font-size: 0.95rem;
    color: #6b7280;
    margin: 0.5rem 0;
}

.drop-area span.clickUpload,
#drop-area-multiple span.clickUpload {
    color: #3b82f6;
    text-decoration: underline;
    font-weight: 500;
    cursor: pointer;
    transition: color 0.2s ease;
}

.drop-area span.clickUpload:hover,
#drop-area-multiple span.clickUpload:hover {
    color: #2563eb;
}

.drop-area .note,
#drop-area-multiple .note {
    display: block;
    font-size: 0.75rem;
    color: #9ca3af;
    margin-top: 0.5rem;
}


/* For Cropper */
#cropModal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(0, 0, 0, 0.5); /* dark overlay */
  display: none;
  z-index: 9999;
}

.modal-content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
  padding: 2rem;
  width: 90%;
  max-width: 600px;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.modal-title {
  margin-bottom: 1rem;
  font-size: 1.5rem;
  font-weight: 600;
  color: #333;
}

.crop-area {
  max-height: 400px;
  overflow: hidden;
  margin-bottom: 1.5rem;
}

.crop-area img {
  max-width: 100%;
  height: auto;
  display: block;
}

.modal-actions {
  display: flex;
  justify-content: center;
  gap: 1rem;
  width: 100%;
}

.modal-actions button#cropConfirm,
.modal-actions button#closeCropModal {
  background-color: #007bff;
  color: white;
  border: none;
  padding: 0.75rem 1.5rem;
  font-size: 1rem;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.modal-actions button#closeCropModal {
  background-color: darkgray;
}

.modal-actions button#cropConfirm:hover {
  background-color: #0056b3;
}

.modal-actions button#closeCropModal:hover {
  background-color: gray;
}

.aspect-ratio-selector {
  margin-bottom: 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
}

.aspect-ratio-selector select {
  padding: 0.5rem 0.75rem;
  border: 1px solid #ccc;
  border-radius: 8px;
  background-color: #fff;
  outline: none;
  min-width: 200px;
}

.crop_icon,
.remove_icon {
  border-radius: 100%;
  position: absolute;
  top: 0.2rem;
  right: 0.2rem;
  padding: 0.3rem;
  height: 1.2rem;
  width: 1.2rem;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.3s ease;
  cursor: pointer;
}

.crop_icon {
  right: 2.5rem;
}

.crop_icon > img,
.remove_icon > img {
  height: 100%;
  width: 100%;
}

.drop-area .crop_icon,
.drop-area .remove_icon {
  display: none;
}

.crop_icon:hover,
.remove_icon:hover {
  transform: scale(1.2);
}

#drop-area-multiple .crop_icon,
#drop-area-multiple .remove_icon {
  top: -1.8rem;
}

.ori_img {
  display: none;
}

/* FOR UPLOAD PROGRESS BAR */
#upload-progress {
  display: none;
}

.progress-container {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  z-index: 101;
  background-color: rgba(0, 0, 0, 0.3);
  backdrop-filter: blur(10px);
}

.progress-container p {
  font-size: 1.5rem;
  font-weight: 700;
  margin: 0 0 3rem 0;
  color: white;
}

.progress-container p::after {
  content: '';
  display: inline-block;
  width: 1.2em;
  text-align: left;
  animation: dots 1s steps(4, end) infinite;
}

@keyframes dots {
  0%   { content: '';    }
  25%  { content: '.';   }
  50%  { content: '..';  }
  75%,100% { content: '...'; }
}

.progress-info {
  display: flex;
  justify-content: space-between;
  margin-bottom: 1rem;
  font-size: 1rem;
  color: white;
  width: 65%;
}

.progress-bar-wrapper {
  width: 70%;
  height: 10px;
  background-color: #e5e7eb;
  border-radius: 999px;
  overflow: hidden;
  margin-bottom: 5rem;
}

.progress-bar-fill {
  height: 100%;
  width: 0%;
  background-color: #3b82f6;
  transition: width 0.2s ease;
}

Step 4

Update: PricePackageController.php
Update your _add() ,_edit() and upload_file()

Add new function upload_img() & remove_image()

<?php private function _add(){ if($_POST["action"]=="add"){ ... if (!$price_package->save()) { ... } else { $price_package_id = $price_package->lastInsertId(); $overwrite = true; $failed = []; // Upload 1 type image (Add more field inside $fields if you have another image) // Make sure fields name match database name $fields = ['package_icon']; foreach ($fields as $field) { $failed[] = $this->upload_img('price_package', $field, $overwrite, $price_package_id, $price_package->$field); } if (!empty(array_filter($failed))) { Flash::set('error', __('Failed to upload: ' . implode(', ', $failed))); } else { Flash::set('success', __('Price Package has been added.')); } ... } redirect(get_url('price_package')); } } private function _edit($id){ ... if ($price_package->save()) { $overwrite = true; $failed = []; $failed_remove = []; // Upload 1 type image (Add more field inside $fields if you have another image) // Make sure fields name match database name $fields = ['package_icon']; $removeImage = $_POST['remove_image']; foreach ($fields as $field) { $failed[] = $this->upload_img('price_package', $field, $overwrite, $price_package->id, $price_package->$field); if (!empty($removeImage[$field])) { $failed_remove[] = $this->remove_image('price_package', $field, $price_package->id); } } if (!empty(array_filter($failed))) { Flash::set('error', __('Failed to update: ' . implode(', ', $failed))); } else if (!empty(array_filter($failed_remove))) { Flash::set('error', __('Failed to remove image: ' . implode(', ', $failed))); } else { Flash::set('success', __('Price Package has been updated.')); } } else { Flash::set('error', __('Price Package could not be saved.')); } ... } // Update upload_file() function function upload_file($origin, $dest, $tmp_name, $overwrite = false, $nid = null, $dbColumnName = null) { FileManagerController::_checkPermission(); // Config: max upload limit in bytes $max_upload_limit = Setting::get('max_img_size_upload'); $max_upload_bytes = $max_upload_limit * 1024 * 1024; $origin = basename($origin); $file_ext = (strpos($origin, '.') === false ? '' : '.' . pathinfo($origin, PATHINFO_EXTENSION)); $file_base = pathinfo($origin, PATHINFO_FILENAME); $file_base = preg_replace('/_\d+$/', '', $file_base); // remove _1, _2, etc. $full_dest = $dest . $origin; $file_name = $origin; // Check file size $uploaded_file_size = filesize($tmp_name); if ($uploaded_file_size > $max_upload_bytes) { Flash::set('error', __('Uploaded file exceeds the maximum allowed size of ' . $max_upload_limit . 'MB.')); return false; } // Ensure destination folder exists if (!file_exists($dest)) { if (!mkdir($dest, 0755, true)) { Flash::set('error', __('Failed to create destination directory.')); return false; } } $price_package = PricePackage::findById($nid); $oldFilename = $price_package->$dbColumnName; // Always generate a new unique filename if file already exists for ($i = 1; file_exists($full_dest); $i++) { $file_name = $file_base . '_' . $i . $file_ext; $full_dest = $dest . $file_name; } // Delete the old file only once, if overwrite is enabled if ($price_package && $overwrite && $oldFilename && $oldFilename !== $file_name) { @unlink($dest . $oldFilename); } // Move uploaded file if ((is_uploaded_file($tmp_name) && move_uploaded_file($tmp_name, $full_dest)) || (!is_uploaded_file($tmp_name) && rename($tmp_name, $full_dest))) { chmod($full_dest, 0644); // Update DB if needed if ($price_package && $dbColumnName) { $updated = $price_package->update('PricePackage',[$dbColumnName => $file_name], 'id=' . (int) $nid); if (!$updated) { Flash::set('error', __('Image has not been updated in Database: ' . $dbColumnName)); } } return $file_name; } return false; } // upload_file function upload_img($module, $field, $overwrite=null, $id, $old_file_name=null) { $failed = null; $croppedField = 'crop_'.$field; // Case 1: Cropped image sent as Base64 (hidden input) if (!empty($_POST[$croppedField])) { $data = $_POST[$croppedField]; if (strpos($data, ',') !== false) { list(, $data) = explode(',', $data, 2); } $data = base64_decode($data); // Create a temp file to mimic an uploaded file $tmpFile = tempnam(sys_get_temp_dir(), 'crop_'); file_put_contents($tmpFile, $data); $filename = ''; if (!empty($_FILES[$field]['name'])) { $filename = $_FILES[$field]['name']; } else if ($old_file_name) { $filename = $old_file_name; } else { $filename = 'crop_'.$field.'.webp'; } $path = FILES_DIR . '/'. $module .'/' . $field . '/' . $id . '/'; // Reuse your upload_file() $upload = $this->upload_file($filename,$path,$tmpFile,$overwrite,$id,$field); // Clean up the temp file unlink($tmpFile); if (!$upload) { $failed[] = 'Cropped ' . ucwords(str_replace('_', ' ', $field)); } // Case 2: Original file uploaded via input[type=file] } else if (!empty($_FILES[$field]['tmp_name']) && $_FILES[$field]['error'] === UPLOAD_ERR_OK) { $upload = $this->upload_file($_FILES[$field]['name'], FILES_DIR . '/'. $module .'/' . $field . '/' . $id . '/', $_FILES[$field]['tmp_name'], $overwrite, $id, $field ); if (!$upload) { $failed[] = ucwords(str_replace('_', ' ', $field)); } } return $failed; } function remove_image($module, $field, $id) { $failed = null; $class = str_replace(' ', '', ucwords(str_replace('_', ' ', $module))); // update database $record = $class::findById($id); if ($record) { $updated = $record->update($class,[$field => ''], 'id=' . (int) $id); if (!$updated) { $failed = ucwords(str_replace('_', ' ', $field)); } else { // delete image in server Utilities::delete_files($module, $field . '/', $id); } } return $failed; } ?>
<?php
private function _add(){
    if($_POST["action"]=="add"){
        ...

        if (!$price_package->save()) {
            ...
        } else {
            $price_package_id = $price_package->lastInsertId();
            $overwrite = true;
            $failed = [];

            // Upload 1 type image (Add more field inside $fields if you have another image)
            // Make sure fields name match database name 
            $fields = ['package_icon'];

            foreach ($fields as $field) {
                $failed[] = $this->upload_img('price_package', $field, $overwrite, $price_package_id, $price_package->$field);
            }

            if (!empty(array_filter($failed))) {
                Flash::set('error', __('Failed to upload: ' . implode(', ', $failed)));
            } else {
                Flash::set('success', __('Price Package has been added.'));
            }

            ...
        }
    redirect(get_url('price_package'));
    }
}

private function _edit($id){
    ...
    if ($price_package->save()) {
        $overwrite = true;
        $failed = [];
        $failed_remove = [];

        // Upload 1 type image (Add more field inside $fields if you have another image)
        // Make sure fields name match database name 
        $fields = ['package_icon'];
        $removeImage = $_POST['remove_image'];

        foreach ($fields as $field) {
            $failed[] = $this->upload_img('price_package', $field, $overwrite, $price_package->id, $price_package->$field);

            if (!empty($removeImage[$field])) {
                $failed_remove[] = $this->remove_image('price_package', $field, $price_package->id);
            }
        }

        if (!empty(array_filter($failed))) {
            Flash::set('error', __('Failed to update: ' . implode(', ', $failed)));
        } else if (!empty(array_filter($failed_remove))) {
            Flash::set('error', __('Failed to remove image: ' . implode(', ', $failed)));
        } else {
            Flash::set('success', __('Price Package has been updated.'));
        }
    }
    else {
        Flash::set('error', __('Price Package could not be saved.'));
    }
    ...
}

// Update upload_file() function
function upload_file($origin, $dest, $tmp_name, $overwrite = false, $nid = null, $dbColumnName = null) {
    FileManagerController::_checkPermission();

    // Config: max upload limit in bytes
    $max_upload_limit = Setting::get('max_img_size_upload');
    $max_upload_bytes = $max_upload_limit * 1024 * 1024;

    $origin     = basename($origin);
    $file_ext   = (strpos($origin, '.') === false ? '' : '.' . pathinfo($origin, PATHINFO_EXTENSION));
    $file_base  = pathinfo($origin, PATHINFO_FILENAME);
    $file_base  = preg_replace('/_\d+$/', '', $file_base); // remove _1, _2, etc.
    $full_dest  = $dest . $origin;
    $file_name  = $origin;

    // Check file size
    $uploaded_file_size = filesize($tmp_name);
    if ($uploaded_file_size > $max_upload_bytes) {
        Flash::set('error', __('Uploaded file exceeds the maximum allowed size of ' . $max_upload_limit . 'MB.'));
        return false;
    }

    // Ensure destination folder exists
    if (!file_exists($dest)) {
        if (!mkdir($dest, 0755, true)) {
            Flash::set('error', __('Failed to create destination directory.'));
            return false;
        }
    }

    $price_package = PricePackage::findById($nid);
    $oldFilename = $price_package->$dbColumnName;

    // Always generate a new unique filename if file already exists
    for ($i = 1; file_exists($full_dest); $i++) {
        $file_name = $file_base . '_' . $i . $file_ext;
        $full_dest = $dest . $file_name;
    }

    // Delete the old file only once, if overwrite is enabled
    if ($price_package && $overwrite && $oldFilename && $oldFilename !== $file_name) {
        @unlink($dest . $oldFilename);
    }

    // Move uploaded file
    if ((is_uploaded_file($tmp_name) && move_uploaded_file($tmp_name, $full_dest)) 
        || (!is_uploaded_file($tmp_name) && rename($tmp_name, $full_dest))) {
        chmod($full_dest, 0644);

        // Update DB if needed
        if ($price_package && $dbColumnName) {
            $updated = $price_package->update('PricePackage',[$dbColumnName => $file_name], 'id=' . (int) $nid);
            if (!$updated) {
                Flash::set('error', __('Image has not been updated in Database: ' . $dbColumnName));
            }
        }

        return $file_name;
    }

    return false;
} // upload_file

function upload_img($module, $field, $overwrite=null, $id, $old_file_name=null) {
    $failed = null;
    $croppedField = 'crop_'.$field;

    // Case 1: Cropped image sent as Base64 (hidden input)
    if (!empty($_POST[$croppedField])) {
        $data = $_POST[$croppedField];

        if (strpos($data, ',') !== false) {
            list(, $data) = explode(',', $data, 2);
        }

        $data = base64_decode($data);

        // Create a temp file to mimic an uploaded file
        $tmpFile = tempnam(sys_get_temp_dir(), 'crop_');
        file_put_contents($tmpFile, $data);

        $filename = '';

        if (!empty($_FILES[$field]['name'])) {
            $filename = $_FILES[$field]['name'];
        } else if ($old_file_name) {
            $filename = $old_file_name;
        } else {
            $filename = 'crop_'.$field.'.webp';
        }

        $path = FILES_DIR . '/'. $module .'/' . $field . '/' . $id . '/';

        // Reuse your upload_file()
        $upload = $this->upload_file($filename,$path,$tmpFile,$overwrite,$id,$field);

        // Clean up the temp file
        unlink($tmpFile);

        if (!$upload) {
            $failed[] = 'Cropped ' . ucwords(str_replace('_', ' ', $field));
        }

    // Case 2: Original file uploaded via input[type=file]
    } else if (!empty($_FILES[$field]['tmp_name']) && $_FILES[$field]['error'] === UPLOAD_ERR_OK) {
        $upload = $this->upload_file($_FILES[$field]['name'], FILES_DIR . '/'. $module .'/' . $field . '/' . $id . '/', $_FILES[$field]['tmp_name'], $overwrite, $id, $field
        );

        if (!$upload) {
            $failed[] = ucwords(str_replace('_', ' ', $field));
        }
    }

    return $failed;
}

function remove_image($module, $field, $id) {
    $failed = null;
    $class = str_replace(' ', '', ucwords(str_replace('_', ' ', $module)));
    // update database
    $record = $class::findById($id);
    if ($record) {
        $updated = $record->update($class,[$field => ''], 'id=' . (int) $id);
        if (!$updated) {
            $failed = ucwords(str_replace('_', ' ', $field));
        } else {
            // delete image in server
            Utilities::delete_files($module, $field . '/', $id);
        }
    }

    return $failed;
}
?>

Step 5

Move script files and images to your project folder

Code Copied To Clipboard!