Component

Module

Image Enhancement (Multiple Images)

Handle Multiple Image Upload, Validation, Crop and Drag & Drop

Directory

File Folder Link
backend.php \\SYNAS\Allan\DOCUMENTATION\Component\Image Enhancement (Multiple Images)\rckuc\wolf\app\layouts
view.php \\SYNAS\Allan\DOCUMENTATION\Component\Image Enhancement (Multiple Images)\rckuc\wolf\app\views\newsevent
style.css \\SYNAS\Allan\DOCUMENTATION\Component\Image Enhancement (Multiple Images)\rckuc\wolf\admin\themes\black_and_white
newsevent.js & img_handler.js & cropper \\SYNAS\Allan\DOCUMENTATION\Component\Image Enhancement (Multiple Images)\rckuc\wolf\admin\javascript
NewseventController.php \\SYNAS\Allan\DOCUMENTATION\Component\Image Enhancement (Multiple Images)\rckuc\wolf\app\controllers

 

Step 1

Update: backend.php

Insert newsevent.js, 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/newsevent.js"></script> <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/img_tool.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/newsevent.js"></script>
    <script type="text/javascript" src="<?php echo URI_PUBLIC; ?>wolf/admin/javascripts/img_tool.js"></script>
</head>

Step 2

Update: newsevent -> view.php

Note : For <input id="imgFileMultiple"> , 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="newsevent" action="<?php echo get_url('newsevent/view/'.$newsevent->id); ?>" method="post" enctype="multipart/form-data" name="thisform"> <input type=hidden name="action" value="edit"> <input type=hidden id="upload_crop_url" value="<?php echo get_url('newsevent/upload_cropped_image/' . $newsevent->id); ?>"> <input type=hidden id="upload_crop_url_multiple" value="<?php echo get_url('newsevent/upload_cropped_image_multiple/gallery/' . $newsevent->id); ?>"> <input type=hidden id="refresh_gallery" value="<?php echo get_url('newsevent/refresh_gallery/' . $newsevent->id); ?>"> ... </form> <div id="boxes"> <div id="mask"></div> </div> <div id="drop-multiple-container"> <div id="drop-area-multiple"> <h3><?php echo __('Multiple Images Upload'); ?></h3> <input id="imgFileMultiple" type="file" name="upload_file" accept=".png, .jpg, .jpeg, .webp" data-max-size="800" data-max-width="1000" data-max-height="1000" multiple hidden /> <div class="multiplePreviewList"> <img class="img_icon" src="<?php echo URL_PUBLIC ?>wolf/admin/images/image-gallery.png" alt="img" /> </div> <p>Drag & Drop or <span class="clickUpload">Browse</span> <span class="note">(Maximun: 800KB)</span> <span class="note">(Maximum: 1000px x 1000px)</span> <span class="note">(Allowed file type : .png, .jpg, .jpeg, .webp)</span> </p> </div> <button type="button" id="multiple-upload-btn">Upload</button> <img class="close_icon" src="<?php echo URL_PUBLIC ?>wolf/admin/images/close.png" alt="close" /> </div> <div class="previewContainer previewTemplate"> <button class="previewImg" type="button"></button> <div class="sequence"></div> <div class="crop_icon"> <img src="<?php echo URL_PUBLIC ?>wolf/admin/images/crop.png" alt="crop" /> </div> <div class="remove_icon"> <img src="<?php echo URL_PUBLIC ?>wolf/admin/images/delete.png" alt="remove" /> </div> </div> <div id="upload-progress"> <div class="progress-container"> <p>Uploading</p> <div class="progress-info"> <span id="progress-percent">0%</span> <span id="progress-size">0 KB / 0 KB</span> </div> <div class="progress-bar-wrapper"> <div id="progress-bar" class="progress-bar-fill"></div> </div> </div> </div> <?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="newsevent" action="<?php echo get_url('newsevent/view/'.$newsevent->id); ?>" method="post" enctype="multipart/form-data" name="thisform">
    <input type=hidden name="action" value="edit">
    <input type=hidden id="upload_crop_url" value="<?php echo get_url('newsevent/upload_cropped_image/' . $newsevent->id); ?>">
    <input type=hidden id="upload_crop_url_multiple" value="<?php echo get_url('newsevent/upload_cropped_image_multiple/gallery/' . $newsevent->id); ?>">
    <input type=hidden id="refresh_gallery" value="<?php echo get_url('newsevent/refresh_gallery/' . $newsevent->id); ?>">
    ...

</form>

<div id="boxes">
 	<div id="mask"></div>
</div>

<div id="drop-multiple-container">
	<div id="drop-area-multiple">
		<h3><?php echo __('Multiple Images Upload'); ?></h3>
		<input id="imgFileMultiple" type="file" name="upload_file" accept=".png, .jpg, .jpeg, .webp" data-max-size="800" data-max-width="1000" data-max-height="1000" multiple hidden />
		<div class="multiplePreviewList">
			<img class="img_icon" src="<?php echo URL_PUBLIC ?>wolf/admin/images/image-gallery.png" alt="img" />
		</div>
		<p>Drag & Drop or 
			<span class="clickUpload">Browse</span>
			<span class="note">(Maximun: 800KB)</span>
			<span class="note">(Maximum: 1000px x 1000px)</span>
			<span class="note">(Allowed file type : .png, .jpg, .jpeg, .webp)</span>
		</p>
	</div>
	<button type="button" id="multiple-upload-btn">Upload</button>
	<img class="close_icon" src="<?php echo URL_PUBLIC ?>wolf/admin/images/close.png" alt="close" />
</div>

<div class="previewContainer previewTemplate">
	<button class="previewImg" type="button"></button>
	<div class="sequence"></div>
	<div class="crop_icon">
		<img src="<?php echo URL_PUBLIC ?>wolf/admin/images/crop.png" alt="crop" />
	</div>
	<div class="remove_icon">
		<img src="<?php echo URL_PUBLIC ?>wolf/admin/images/delete.png" alt="remove" />
	</div>
</div>

<div id="upload-progress">
  <div class="progress-container">
    <p>Uploading</p>

    <div class="progress-info">
      <span id="progress-percent">0%</span>
      <span id="progress-size">0 KB / 0 KB</span>
    </div>

    <div class="progress-bar-wrapper">
      <div id="progress-bar" class="progress-bar-fill"></div>
    </div>
  </div>
</div>

<?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: NewseventController.php
Update upload_file() function 

Add 2 new functions:
1. upload_cropped_image_multiple()

2. refresh_gallery()

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; } } $newsevent = Newsevent::findById($nid); $oldFilename = $newsevent->$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 ($newsevent && $overwrite && $oldFilename && $oldFilename !== $file_name) { @unlink($dest . $oldFilename); } // Move uploaded file if (is_uploaded_file($tmp_name) && move_uploaded_file($tmp_name, $full_dest)) { chmod($full_dest, 0644); // Update DB if needed if ($newsevent && $dbColumnName && $dbColumnName != 'gallery') { $updated = $newsevent->update('Newsevent',[$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 public function upload_cropped_image_multiple($folder, $id) { ob_start(); // Prevent unwanted output $content_type = 'image'; $responses = []; if (isset($_FILES['upload_file'])) { $count = count($_FILES['upload_file']['name']); for ($i = 0; $i < $count; $i++) { $origin = $_FILES['upload_file']['name'][$i]; $tmp_name = $_FILES['upload_file']['tmp_name'][$i]; $error = $_FILES['upload_file']['error'][$i]; if ($error === UPLOAD_ERR_OK) { $dest = FILES_DIR . '/newsevent/' . $folder . '/' . $id . '/'; $result = $this->upload_file($origin, $dest, $tmp_name, false, $id, 'gallery'); if ($result !== false) { $newsevent_content = new NewseventImage(); $newsevent_content->newsevent_id = $id; $newsevent_content->filename = $result; $newsevent_content->source = URL_PUBLIC . 'public/newsevent/' . $folder . '/' . $id . '/' . $result; if (!$newsevent_content->save()) { $responses[] = [ 'success' => false, 'filename' => $origin, 'error' => 'DB save failed' ]; } else { $responses[] = [ 'success' => true, 'filename' => $result ]; } } else { $responses[] = [ 'success' => false, 'filename' => $origin, 'error' => 'Upload failed' ]; } } else { $responses[] = [ 'success' => false, 'filename' => $origin, 'error' => 'Upload error code: ' . $error ]; } } ob_clean(); header('Content-Type: application/json'); echo json_encode($responses); exit; } else { ob_clean(); http_response_code(400); header('Content-Type: application/json'); echo json_encode(['success' => false, 'error' => 'No files received']); exit; } } public function refresh_gallery($id) { $this->_checkPermission(); $newsevent_images = NewseventImage::findByNewseventId($id); header('Content-Type: text/html'); foreach ($newsevent_images as $image) { echo '<tr class="node ' . odd_even() . '">'; echo '<td class="user">'; if ($image->filename != '') { echo '<img src="' . BASE_FILES_DIR . '/newsevent/gallery/' . $image->newsevent_id . '/' . $image->filename . '" width="100" />'; } echo '</td>'; echo '<td>'; echo '<input type="hidden" name="newsevent_image_id[]" value="' . $image->id . '">'; echo '<input type="text" name="order[]" value="' . $image->sequence . '" size="1" style="text-align:right;">'; echo '</td>'; echo '<td>'; echo '<a href="' . get_url('newsevent/delete_newsevent_image/' . $image->id) . '" onclick="return confirm(\'Are you sure you wish to delete this image: ' . $image->filename . '?\');">'; echo '<img src="' . URI_PUBLIC . 'wolf/admin/images/icon-remove.gif" alt="delete image" title="Delete Image" />'; echo '</a>'; echo '</td>'; echo '</tr>'; } exit; }
	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;
			}
		}

		$newsevent = Newsevent::findById($nid);
		$oldFilename = $newsevent->$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 ($newsevent && $overwrite && $oldFilename && $oldFilename !== $file_name) {
			@unlink($dest . $oldFilename);
		}

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

			// Update DB if needed
			if ($newsevent && $dbColumnName && $dbColumnName != 'gallery') {
				$updated = $newsevent->update('Newsevent',[$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


	public function upload_cropped_image_multiple($folder, $id) {
		ob_start(); // Prevent unwanted output

		$content_type = 'image';
		$responses = [];

		if (isset($_FILES['upload_file'])) {
			$count = count($_FILES['upload_file']['name']);

			for ($i = 0; $i < $count; $i++) {
				$origin = $_FILES['upload_file']['name'][$i];
				$tmp_name = $_FILES['upload_file']['tmp_name'][$i];
				$error = $_FILES['upload_file']['error'][$i];

				if ($error === UPLOAD_ERR_OK) {
					$dest = FILES_DIR . '/newsevent/' . $folder . '/' . $id . '/';
					$result = $this->upload_file($origin, $dest, $tmp_name, false, $id, 'gallery');

					if ($result !== false) {
						$newsevent_content = new NewseventImage();
						$newsevent_content->newsevent_id = $id;
						$newsevent_content->filename = $result;
						$newsevent_content->source = URL_PUBLIC . 'public/newsevent/' . $folder . '/' . $id . '/' . $result;

						if (!$newsevent_content->save()) {
							$responses[] = [
								'success' => false,
								'filename' => $origin,
								'error' => 'DB save failed'
							];
						} else {
							$responses[] = [
								'success' => true,
								'filename' => $result
							];
						}
					} else {
						$responses[] = [
							'success' => false,
							'filename' => $origin,
							'error' => 'Upload failed'
						];
					}
				} else {
					$responses[] = [
						'success' => false,
						'filename' => $origin,
						'error' => 'Upload error code: ' . $error
					];
				}
			}

			ob_clean();
			header('Content-Type: application/json');
			echo json_encode($responses);
			exit;
		} else {
			ob_clean();
			http_response_code(400);
			header('Content-Type: application/json');
			echo json_encode(['success' => false, 'error' => 'No files received']);
			exit;
		}
	}


	public function refresh_gallery($id) {
		$this->_checkPermission();

		$newsevent_images = NewseventImage::findByNewseventId($id);

		header('Content-Type: text/html');

		foreach ($newsevent_images as $image) {
			echo '<tr class="node ' . odd_even() . '">';
			echo '<td class="user">';
			if ($image->filename != '') {
				echo '<img src="' . BASE_FILES_DIR . '/newsevent/gallery/' . $image->newsevent_id . '/' . $image->filename . '" width="100" />';
			}
			echo '</td>';
			echo '<td>';
			echo '<input type="hidden" name="newsevent_image_id[]" value="' . $image->id . '">';
			echo '<input type="text" name="order[]" value="' . $image->sequence . '" size="1" style="text-align:right;">';
			echo '</td>';
			echo '<td>';
			echo '<a href="' . get_url('newsevent/delete_newsevent_image/' . $image->id) . '" onclick="return confirm(\'Are you sure you wish to delete this image: ' . $image->filename . '?\');">';
			echo '<img src="' . URI_PUBLIC . 'wolf/admin/images/icon-remove.gif" alt="delete image" title="Delete Image" />';
			echo '</a>';
			echo '</td>';
			echo '</tr>';
		}

		exit;
	}

Step 5

Move script files and images to your project folder

Code Copied To Clipboard!