PageSourceSearch

https://www.lemonink.co/assets/controllers/file_upload_controller-21cde4bb.js

js lemonink.co collected 2026-10-02 06:19:40 UTC 5,409 bytes, 191 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = [
5    "fileInput", "masterIdInput", "progressFill",
6    "completeForm", "uploadArea", "removeButton", "dropzone", "uploadTurboFrame"
7  ]
8  static values = {
9    createUrl: String,
10    masterId: String
11  }
12
13  connect() {
14    this.currentUploadRequests = {}
15    this.setupDragAndDrop()
16  }
17
18  setupDragAndDrop() {
19    if (!this.hasDropzoneTarget) return
20
21    // Add drag and drop event listeners
22    this.dropzoneTarget.addEventListener('dragover', this.handleDragOver.bind(this))
23    this.dropzoneTarget.addEventListener('drop', this.handleDrop.bind(this))
24    this.dropzoneTarget.addEventListener('dragenter', this.handleDragEnter.bind(this))
25    this.dropzoneTarget.addEventListener('dragleave', this.handleDragLeave.bind(this))
26  }
27
28  triggerFileSelect() {
29    this.fileInputTarget.click()
30  }
31
32  async handleFileSelect(event) {
33    const file = event.target.files[0]
34    if (!file) return
35
36    await this.processFile(file)
37  }
38
39  // Drag and drop handlers
40  handleDragOver(event) {
41    event.preventDefault()
42    event.dataTransfer.dropEffect = 'copy'
43    this.dropzoneTarget.classList.add('drag-over')
44  }
45
46  handleDragEnter(event) {
47    event.preventDefault()
48    this.dropzoneTarget.classList.add('drag-over')
49  }
50
51  handleDragLeave(event) {
52    event.preventDefault()
53    // Only remove drag-over if we're actually leaving the drop area
54    const rect = this.dropzoneTarget.getBoundingClientRect()
55    if (event.clientX < rect.left || event.clientX >= rect.right ||
56        event.clientY < rect.top || event.clientY >= rect.bottom) {
57      this.dropzoneTarget.classList.remove('drag-over')
58    }
59  }
60
61  handleDrop(event) {
62    event.preventDefault()
63    this.dropzoneTarget.classList.remove('drag-over')
64
65    const file = event.dataTransfer.files[0]
66    if (file) {
67      this.processFile(file)
68    }
69  }
70
71  async processFile(file) {
72    this.currentFile = file
73
74    try {
75      const formData = new FormData()
76      formData.append('filename', file.name)
77      if (this.masterIdValue) {
78        formData.append('master_id', this.masterIdValue)
79      }
80
81      const authResponse = await fetch(this.createUrlValue, {
82        method: 'POST',
83        headers: {
84          'Accept': 'application/json',
85          'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
86        },
87        body: formData
88      })
89
90      if (!authResponse.ok) {
91        throw new Error('Failed to authorize upload')
92      }
93
94      const uploadData = await authResponse.json()
95
96      this.loadUpload(uploadData.upload_id);
97      await this.uploadToS3(file, uploadData)
98      await this.notifyUploadComplete(uploadData.upload_id, file)
99
100    } catch (error) {
101      console.error('Upload error:', error)
102      // Show empty state again if upload fails (for new masters)
103      if (this.hasDropzoneTarget) {
104        this.dropzoneTarget.style.display = 'block'
105      }
106    }
107  }
108
109  loadUpload(uploadId) {
110    // Hide empty state when upload starts (for new masters)
111    if (this.hasDropzoneTarget) {
112      this.dropzoneTarget.style.display = 'none'
113    }
114    if (this.hasUploadAreaTarget) {
115      this.uploadAreaTarget.style.display = 'block'
116    }
117    if (this.hasUploadTurboFrameTarget) {
118      this.uploadTurboFrameTarget.id = `upload_${uploadId}`
119      this.uploadTurboFrameTarget.src = `/uploads/${uploadId}`
120    }
121  }
122
123  async uploadToS3(file, uploadData) {
124    return new Promise((resolve, reject) => {
125      const formData = new FormData()
126      Object.keys(uploadData.fields).forEach(key => {
127        formData.append(key, uploadData.fields[key])
128      })
129      formData.append('Content-Type', file.type)
130      formData.append('file', file)
131
132      const uploadRequest = new XMLHttpRequest()
133      this.currentUploadRequests[uploadData.upload_id] = uploadRequest
134
135      uploadRequest.upload.addEventListener('progress', (e) => {
136        if (e.lengthComputable) {
137          const percentComplete = (e.loaded / e.total) * 100
138          this.updateProgress(uploadData.upload_id, percentComplete)
139        }
140      })
141
142      uploadRequest.addEventListener('load', () => {
143        if (uploadRequest.status === 204) {
144          resolve()
145        } else {
146          reject(new Error('S3 upload failed'))
147        }
148      })
149
150      uploadRequest.addEventListener('error', () => {
151        console.log("error")
152        reject(new Error('S3 upload failed'))
153      })
154
155      uploadRequest.open('POST', uploadData.url)
156      uploadRequest.send(formData)
157    })
158  }
159
160  async notifyUploadComplete(uploadId, file) {
161    try {
162      if (this.hasCompleteFormTarget) {
163        this.masterIdInputTarget.value = this.masterIdValue
164        this.completeFormTarget.requestSubmit()
165      }
166    } catch (error) {
167      console.error('Upload completion error:', error)
168    }
169  }
170
171  updateProgress(uploadId, percentComplete) {
172    if (this.hasProgressFillTarget) {
173      this.progressFillTargets.forEach(target => {
174        if (target.dataset.uploadId === uploadId) {
175          target.style.width = `${percentComplete}%`
176        }
177      })
178    }
179  }
180
181  cancelUpload(event) {
182    const uploadId = event.target.dataset.uploadId
183
184    if (this.currentUploadRequests[uploadId]) {
185      // Cancel the ongoing S3 upload
186      this.currentUploadRequests[uploadId].abort()
187      delete this.currentUploadRequests[uploadId]
188    }
189  }
190
191}

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.