PageSourceSearch

https://drivetimedevotions.com/scripts/MyGroups/Archive/GroupImage…?v=067bac55dc3729ccedc88375e13966800226cff8

js drivetimedevotions.com collected 2026-09-26 03:08:23 UTC 18,355 bytes, 521 lines download raw bytes

1import Modal from '/scripts/Modals/BasicModalMixin.js';
2
3export default {
4  name: 'GroupImageModal',
5
6  components: {
7    Modal,
8  },
9
10  props: ['visible', 'groupId', 'healthyChurchApi', 'healthyChurchSmallGroupsApi', 'authHeaders', 'rockDomain', 'currentImageUrl'],
11
12  emits: ['close', 'uploaded'],
13
14  template: `
15    <Modal 
16        v-show="visible" 
17        v-on:close="$emit('close')" 
18        class="group-image-modal"
19    >
20        <template v-slot:header>
21            <!-- Error Alert -->
22            <div v-if="errorMessage" class="error-alert">
23                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" style="flex-shrink: 0; min-width: 24px; min-height: 24px;">
24                  <mask id="mask0_2694_13268" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
25                    <rect width="24" height="24" fill="#D9D9D9"/>
26                  </mask>
27                  <g mask="url(#mask0_2694_13268)">
28                    <path d="M1 21L12 2L23 21H1ZM12 18C12.2833 18 12.5208 17.9042 12.7125 17.7125C12.9042 17.5208 13 17.2833 13 17C13 16.7167 12.9042 16.4792 12.7125 16.2875C12.5208 16.0958 12.2833 16 12 16C11.7167 16 11.4792 16.0958 11.2875 16.2875C11.0958 16.4792 11 16.7167 11 17C11 17.2833 11.0958 17.5208 11.2875 17.7125C11.4792 17.9042 11.7167 18 12 18ZM11 15H13V10H11V15Z" fill="white"/>
29                  </g>
30                </svg>
31                <span>{{ errorMessage }}</span>
32            </div>  
33
34            <div class="group-image-header">
35                <h3>Group Image</h3>
36            </div>
37        </template>
38
39        <template v-slot:body>
40            <div class="group-image-body">
41                <!-- Current image display -->
42                <div v-if="currentImageUrl && !selectedFile" class="current-image-container">
43                    <img :src="currentImageUrl" alt="Current Group Image" class="current-image">
44                    <p class="requirements-text" v-if="!selectedFile">Image Size Max: 1920x1080, < 1MB</p>
45                </div>
46
47                <!-- Image preview (only show after cropping is done) -->
48                <div v-if="imagePreview && showUploadButton" class="img-div">
49                    <img :src="imagePreview" alt="Cropped Preview" class="preview-image">
50                </div>
51
52                <!-- Cropper container (visible during cropping) -->
53                <div id="group-cropme-container" class="modal-body mb-2" v-show="showCropButton">
54                    <div id="group-imgModal-cropme-wrp" class="justify-content-center">
55                        <div id="group-imgModal-cropme" class=""></div>
56                    </div>
57                </div>
58
59                <!-- File upload -->
60                <div v-if="!selectedFile" class="modal-footer" :class="{ 'has-error': errorMessage }">
61                    <label for="groupFileUpload" class="btn">Choose File</label>
62                    <input style="display: none !important;" class="file-upload-btn" type="file" id="groupFileUpload" accept="image/*" 
63                        v-on:change="handleFileSelect" />
64                    <button type="button" class="btn invert btn-info" v-on:click="$emit('close')">Cancel</button>
65                </div>
66
67                <!-- Action buttons -->
68                <div v-if="selectedFile" class="modal-footer justify-content-center">
69                    <button v-if="showCropButton" id="groupApplyCrop" type="button" class="btn btn-info" 
70                        v-on:click="applyCrop">Crop</button>
71                    <button v-if="showUploadButton" class="btn btn-primary" v-on:click="handleUpload" 
72                        :disabled="isUploading">{{ isUploading ? 'Uploading...' : 'Save Photo' }}</button>
73                    <button type="button" class="btn invert btn-info" v-on:click="$emit('close')">Cancel</button>
74                </div>
75            </div>
76        </template>
77    </Modal>
78    `,
79
80  data() {
81    return {
82      selectedFile: null,
83      imagePreview: null,
84      showCropButton: false,
85      showUploadButton: false,
86      isUploading: false,
87      cropmeInstance: null,
88      cropmeLoaded: false,
89      imgHolder: null,
90      errorMessage: null,
91    };
92  },
93
94  watch: {
95    visible(newVal, oldVal) {
96      if (!oldVal && newVal) {
97        // Modal is opening - reset everything first
98        this.resetState();
99        document.body.classList.add('modal-open');
100      } else if (oldVal && !newVal) {
101        // Modal is closing - cleanup
102        this.cleanup();
103        document.body.classList.remove('modal-open');
104      }
105    },
106  },
107
108  beforeUnmount() {
109    this.cleanup();
110    document.body.classList.remove('modal-open');
111  },
112
113  methods: {
114    // Load the correct cropme library (lowercase 'c') - same as your working profile
115    async loadCropMe() {
116      if (this.cropmeLoaded && window.Cropme) return;
117
118      try {
119        // Load CSS
120        if (!document.querySelector('link[href*="cropme"]')) {
121          const cssLink = document.createElement('link');
122          cssLink.rel = 'stylesheet';
123          cssLink.href = 'https://cdn.jsdelivr.net/npm/cropme@latest/dist/cropme.min.css';
124          document.head.appendChild(cssLink);
125        }
126
127        // Load JS - using the SAME URL as your working profile code
128        if (!window.Cropme) {
129          await new Promise((resolve, reject) => {
130            const script = document.createElement('script');
131            script.src = 'https://cdn.jsdelivr.net/npm/cropme@latest/dist/cropme.min.js';
132            script.onload = () => {
133              // Wait longer and check multiple times
134              let attempts = 0;
135              const checkCropme = () => {
136                attempts++;
137                if (window.Cropme) {
138                  console.log('Cropme loaded successfully after', attempts, 'attempts');
139                  resolve();
140                } else if (attempts < 10) {
141                  setTimeout(checkCropme, 100);
142                } else {
143                  reject(new Error('Cropme library loaded but not available after 10 attempts'));
144                }
145              };
146              checkCropme();
147            };
148            script.onerror = () => reject(new Error('Failed to load cropme script'));
149            document.head.appendChild(script);
150          });
151        }
152
153        this.cropmeLoaded = true;
154      } catch (error) {
155        console.error('Error loading cropme:', error);
156        throw error;
157      }
158    },
159
160    async handleFileSelect(event) {
161      // Clear any previous error messages
162      this.errorMessage = null;
163
164      const file = event.target.files[0];
165      if (!file || !file.type.match('image.*')) {
166        this.errorMessage = 'Please select a valid image file (JPG, PNG, GIF, etc.)';
167        return;
168      }
169
170      // Check file size (1MB = 1048576 bytes)
171      if (file.size > 1048576) {
172        this.errorMessage = 'Image file size is too large. Please choose an image smaller than 1MB';
173        event.target.value = ''; // Clear the input
174        return;
175      }
176
177      // Check image dimensions
178      const isValidDimensions = await this.validateImageDimensions(file);
179      if (!isValidDimensions) {
180        event.target.value = ''; // Clear the input
181        return;
182      }
183
184      this.selectedFile = file;
185      
186      try {
187        // Load cropme library
188        await this.loadCropMe();
189        
190        // Read file and show preview
191        await this.readFileFromInput(file);
192        
193        // Initialize cropper
194        await this.initializeCropper();
195        
196      } catch (error) {
197        console.error('Error initializing cropper:', error);
198        this.errorMessage = 'Error loading image editor. Please try again or choose a different image';
199      }
200    },
201
202    validateImageDimensions(file) {
203      return new Promise((resolve) => {
204        const img = new Image();
205        img.onload = () => {
206          if (img.width > 1920 || img.height > 1080) {
207            this.errorMessage = `Image dimensions (${img.width}×${img.height}) are too large. Maximum allowed: 1920×1080 pixels`;
208            resolve(false);
209          } else {
210            resolve(true);
211          }
212        };
213        img.onerror = () => {
214          this.errorMessage = 'Unable to read image file. Please try a different image';
215          resolve(false);
216        };
217        img.src = URL.createObjectURL(file);
218      });
219    },
220
221    readFileFromInput(file) {
222      return new Promise((resolve, reject) => {
223        if (!file) {
224          reject(new Error('No file provided'));
225          return;
226        }
227
228        const reader = new FileReader();
229        
230        // Create new image holder for each file
231        this.imgHolder = new Image();
232        this.imgHolder.onload = () => {
233          // Show preview
234          this.imagePreview = this.imgHolder.src;
235          resolve();
236        };
237        this.imgHolder.onerror = reject;
238        
239        reader.onload = (e) => {
240          this.imgHolder.src = e.target.result;
241        };
242        
243        reader.onerror = reject;
244        reader.readAsDataURL(file);
245      });
246    },
247
248    async initializeCropper() {
249      // Clean up any existing instance first
250      if (this.cropmeInstance) {
251        try {
252          this.cropmeInstance.destroy();
253        } catch (e) {
254          console.warn('Error destroying previous cropme instance:', e);
255        }
256        this.cropmeInstance = null;
257      }
258
259      const container = document.getElementById('group-cropme-container');
260      const cropmeDiv = document.getElementById('group-imgModal-cropme');
261      
262      if (!container || !cropmeDiv) {
263        throw new Error('Cropper container not found');
264      }
265
266      // Clear the cropper div content
267      cropmeDiv.innerHTML = '';
268
269      // Show the cropper container
270      container.style.display = 'block';
271      
272      // Wait a bit for DOM updates
273      await new Promise(resolve => setTimeout(resolve, 100));
274      
275      // Calculate responsive container dimensions with max-width 378px
276      const maxWidth = 378;
277      const targetAspectRatio = 16 / 9; // 1.78
278      
279      // Get the modal width to calculate responsive sizing
280      const modalElement = container.closest('.modal');
281      const modalWidth = modalElement ? modalElement.offsetWidth : 660;
282      const availableWidth = Math.min(modalWidth - 40, maxWidth); // 20px padding each side, max 378px
283      
284      const containerWidth = availableWidth;
285      const containerHeight = containerWidth / targetAspectRatio;
286      
287      // Viewport fills most of the container (leaving some padding)
288      const viewportWidth = containerWidth - 20; // 10px padding on each side
289      const viewportHeight = viewportWidth / targetAspectRatio;
290      
291      // Update options for group image dimensions
292      const cropmeOptions = {
293        container: { width: containerWidth, height: containerHeight },
294        viewport: {
295          width: viewportWidth,     // Full container width
296          height: viewportHeight,   // Calculated to maintain 16:9 ratio
297          type: 'rectangle',
298          border: {
299            enable: true,
300            width: 2,
301            color: '#fff'
302          }
303        },
304        zoom: { enable: true, mouseWheel: true, slider: false },
305        rotation: { slider: false, enable: false },
306        transformOrigin: 'viewport',
307        customClass: 'group-image-cropper'
308      };
309
310      try {
311        // Initialize Cropme (note the capital C)
312        this.cropmeInstance = new Cropme(cropmeDiv, cropmeOptions);
313        
314        // Wait for instance to be ready before binding
315        await new Promise(resolve => setTimeout(resolve, 50));
316        
317        // Bind the image
318        this.cropmeInstance.bind({ url: this.imgHolder.src });
319        
320        this.showCropButton = true;
321        this.showUploadButton = false;
322        
323        console.log('Cropper initialized successfully');
324      } catch (error) {
325        console.error('Error initializing cropme instance:', error);
326        throw error;
327      }
328    },
329
330    applyCrop() {
331      if (!this.cropmeInstance) {
332        console.error('No cropme instance available');
333        return;
334      }
335
336      this.cropmeInstance.crop({
337        width: 2800, // Target group image width
338      }).then((result) => {
339        // Update preview with cropped image
340        this.imagePreview = result;
341        
342        // Hide cropper, show upload button
343        document.getElementById('group-cropme-container').style.display = 'none';
344        this.showCropButton = false;
345        this.showUploadButton = true;
346        
347        // Clean up cropper instance
348        if (this.cropmeInstance) {
349          try {
350            this.cropmeInstance.destroy();
351          } catch (e) {
352            console.warn('Error destroying cropme instance after crop:', e);
353          }
354          this.cropmeInstance = null;
355        }
356      }).catch((error) => {
357        console.error('Error cropping image:', error);
358        this.errorMessage = 'Error cropping image. Please try again or choose a different image';
359      });
360    },
361
362    async handleUpload() {
363      if (!this.imagePreview) return;
364
365      this.isUploading = true;
366
367      try {
368        // Convert data URL to blob
369        const response = await fetch(this.imagePreview);
370        const blob = await response.blob();
371        
372        // Check if we need to compress the image
373        let finalBlob = blob;
374        if (blob.size > 1048576) { // 1MB limit
375          finalBlob = await this.compressImage(blob);
376        }
377        
378        // Create file from blob
379        const file = new File([finalBlob], this.selectedFile.name, {
380          type: 'image/jpeg',
381          lastModified: Date.now(),
382        });
383
384        const formData = new FormData();
385        formData.append('file', file, file.name);
386
387        await axios.post(
388          `${this.healthyChurchApi}/api/v4/smallgroups/${this.groupId}/photo`, 
389          formData, 
390          this.authHeaders
391        );
392
393        // Success! Emit uploaded event
394        this.$emit('uploaded');
395        
396        // Reset state completely before closing
397        this.resetState();
398        
399        // Close the modal
400        this.$emit('close');
401        
402      } catch (error) {
403        console.error('Error uploading image:', error);
404        this.errorMessage = 'Failed to upload image. Please check your connection and try again';
405      } finally {
406        this.isUploading = false;
407      }
408    },
409
410    cancelChanges() {
411      this.cleanup();
412      this.resetState();
413    },
414
415    resetState() {
416      // Clean up cropper instance first
417      this.cleanup();
418      
419      // Reset all reactive data properties
420      this.selectedFile = null;
421      this.imagePreview = null;
422      this.showCropButton = false;
423      this.showUploadButton = false;
424      this.isUploading = false;
425      this.imgHolder = null;
426      this.errorMessage = null;
427      
428      // Reset file input
429      const fileInput = document.getElementById('groupFileUpload');
430      if (fileInput) {
431        fileInput.value = '';
432      }
433      
434      // Hide and clear cropper container
435      const container = document.getElementById('group-cropme-container');
436      if (container) {
437        container.style.display = 'none';
438      }
439      
440      const cropmeDiv = document.getElementById('group-imgModal-cropme');
441      if (cropmeDiv) {
442        cropmeDiv.innerHTML = '';
443      }
444    },
445
446    compressImage(blob, quality = 0.8) {
447      return new Promise((resolve) => {
448        const canvas = document.createElement('canvas');
449        const ctx = canvas.getContext('2d');
450        const img = new Image();
451        
452        img.onload = () => {
453          // Calculate dimensions to fit within 1920x1080 while maintaining aspect ratio
454          let { width, height } = img;
455          const maxWidth = 1920;
456          const maxHeight = 1080;
457          
458          if (width > maxWidth || height > maxHeight) {
459            const aspectRatio = width / height;
460            
461            if (width / maxWidth > height / maxHeight) {
462              // Width is the limiting factor
463              width = maxWidth;
464              height = width / aspectRatio;
465            } else {
466              // Height is the limiting factor
467              height = maxHeight;
468              width = height * aspectRatio;
469            }
470          }
471          
472          // Set canvas dimensions to calculated dimensions
473          canvas.width = width;
474          canvas.height = height;
475          
476          // Draw image to canvas with new dimensions
477          ctx.drawImage(img, 0, 0, width, height);
478          
479          // Convert to blob with compression
480          canvas.toBlob((compressedBlob) => {
481            // If still too large in file size, try lower quality
482            if (compressedBlob.size > 1048576 && quality > 0.3) {
483              resolve(this.compressImage(blob, quality - 0.1));
484            } else {
485              resolve(compressedBlob);
486            }
487          }, 'image/jpeg', quality);
488        };
489        
490        img.src = URL.createObjectURL(blob);
491      });
492    },
493
494    cleanup() {
495      // Clean up cropper instance
496      if (this.cropmeInstance) {
497        try {
498          this.cropmeInstance.destroy();
499        } catch (e) {
500          console.warn('Error destroying cropme instance during cleanup:', e);
501        }
502        this.cropmeInstance = null;
503      }
504      
505      // Clean up any created URLs
506      if (this.imagePreview && this.imagePreview.startsWith('blob:')) {
507        try {
508          URL.revokeObjectURL(this.imagePreview);
509        } catch (e) {
510          console.warn('Error revoking object URL:', e);
511        }
512      }
513      
514      // Clear the cropper div
515      const cropmeDiv = document.getElementById('group-imgModal-cropme');
516      if (cropmeDiv) {
517        cropmeDiv.innerHTML = '';
518      }
519    },
520  },
521};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.