66 <!-- Modal header with Wikipedia primary color -->
77 <div class =" modal-header" >
88 <h5 class =" modal-title" >Create New Contest</h5 >
9- <button type =" button" class =" btn-close" data-bs-dismiss =" modal" ></button >
9+ <button type =" button" class =" btn-close" data-bs-dismiss =" modal" :disabled = " loading " ></button >
1010 </div >
1111 <div class =" modal-body" >
1212 <form @submit.prevent =" handleSubmit" >
1313 <!-- Basic contest information: name and project -->
1414 <div class =" row" >
1515 <div class =" col-md-6 mb-3" >
1616 <label for =" contestName" class =" form-label" >Contest Name *</label >
17- <input type =" text" class =" form-control" id =" contestName" v-model =" formData.name" required />
17+ <input type =" text" class =" form-control" id =" contestName" v-model =" formData.name" required
18+ :disabled =" loading" />
1819 </div >
1920 <div class =" col-md-6 mb-3" >
2021 <label for =" projectName" class =" form-label" >Project Name *</label >
21- <input type =" text" class =" form-control" id =" projectName" v-model =" formData.project_name" required />
22+ <input type =" text" class =" form-control" id =" projectName" v-model =" formData.project_name" required
23+ :disabled =" loading" />
2224 </div >
2325 </div >
2426
2527 <!-- Contest description field -->
2628 <div class =" mb-3" >
2729 <label for =" contestDescription" class =" form-label" >Description</label >
28- <textarea class =" form-control" id =" contestDescription" rows =" 3" v-model =" formData.description" ></textarea >
30+ <textarea class =" form-control" id =" contestDescription" rows =" 3" v-model =" formData.description"
31+ :disabled =" loading" ></textarea >
2932 </div >
3033
3134 <!-- Contest rules - required field -->
3235 <div class =" mb-3" >
3336 <label for =" contestRules" class =" form-label" >Contest Rules *</label >
3437 <textarea class =" form-control" id =" contestRules" rows =" 4"
35- placeholder =" Write rules about how articles must be submitted." v-model =" formData.rules_text"
36- required ></textarea >
38+ placeholder =" Write rules about how articles must be submitted." v-model =" formData.rules_text" required
39+ :disabled = " loading " ></textarea >
3740 </div >
3841
3942 <!-- Submission type selector: new, expansion, or both -->
4043 <div class =" mb-3" >
4144 <label for =" allowedType" class =" form-label" >Allowed Submission Type</label >
42- <select id =" allowedType" class =" form-control" v-model =" formData.allowed_submission_type" >
45+ <select id =" allowedType" class =" form-control" v-model =" formData.allowed_submission_type"
46+ :disabled =" loading" >
4347 <option value =" new" >New Article Only</option >
4448 <option value =" expansion" >Improved Article Only</option >
4549 <option value =" both" >Both(New Article + Improved Article)</option >
5054 <div class =" row" >
5155 <div class =" col-md-6 mb-3" >
5256 <label for =" startDate" class =" form-label" >Start Date *</label >
53- <input type =" date" class =" form-control" id =" startDate" v-model =" formData.start_date" required />
57+ <input type =" date" class =" form-control" id =" startDate" v-model =" formData.start_date" required
58+ :disabled =" loading" />
5459 </div >
5560 <div class =" col-md-6 mb-3" >
5661 <label for =" endDate" class =" form-label" >End Date *</label >
57- <input type =" date" class =" form-control" id =" endDate" v-model =" formData.end_date" required />
62+ <input type =" date" class =" form-control" id =" endDate" v-model =" formData.end_date" required
63+ :disabled =" loading" />
5864 </div >
5965 </div >
6066
7581 <!-- Simple Scoring Option -->
7682 <div class =" col-md-6" >
7783 <label :class =" { 'active': !enableMultiParameterScoring }" >
78- <input type =" radio" name =" scoringMode" :value =" false" v-model =" enableMultiParameterScoring" />
79- <h6 >Simple Scoring</h6 >
84+ <input type =" radio" name =" scoringMode" :value =" false" v-model =" enableMultiParameterScoring"
85+ :disabled =" loading" />
86+ <h6 >Simple Scoring</h6 >
8087 </label >
8188 </div >
8289
8390 <!-- Multi-Parameter Scoring Option -->
8491 <div class =" col-md-6" >
8592 <label :class =" { 'active': enableMultiParameterScoring }" >
86- <input type =" radio" name =" scoringMode" :value =" true" v-model =" enableMultiParameterScoring" />
87- <div >
93+ <input type =" radio" name =" scoringMode" :value =" true" v-model =" enableMultiParameterScoring"
94+ :disabled =" loading" />
95+ <div >
8896 <h6 >Multi-Parameter Scoring</h6 >
8997 </div >
9098 </label >
102110 <div class =" col-md-6 mb-3" >
103111 <label for =" marksAccepted" class =" form-label" >Points for Accepted Submissions *</label >
104112 <input type =" number" class =" form-control" id =" marksAccepted"
105- v-model.number =" formData.marks_setting_accepted" min =" 0" required />
113+ v-model.number =" formData.marks_setting_accepted" min =" 0" required :disabled = " loading " />
106114 <small class =" form-text text-muted" >
107115 Maximum points that can be awarded. Jury can assign points from 0 up to this value for accepted
108116 submissions.
111119 <div class =" col-md-6 mb-3" >
112120 <label for =" marksRejected" class =" form-label" >Points for Rejected Submissions *</label >
113121 <input type =" number" class =" form-control" id =" marksRejected"
114- v-model.number =" formData.marks_setting_rejected" min =" 0" required />
122+ v-model.number =" formData.marks_setting_rejected" min =" 0" required :disabled = " loading " />
115123 <small class =" form-text text-muted" >
116124 Fixed points awarded automatically for rejected submissions (usually 0 or negative).
117125 </small >
132140 <div class =" col-md-6" >
133141 <label class =" form-label" >Maximum Score (Accepted Submissions) *</label >
134142 <input type =" number" class =" form-control" v-model.number =" maxScore" min =" 1" max =" 100"
135- placeholder =" 0" required />
143+ placeholder =" 0" required :disabled = " loading " />
136144 <small class =" text-muted" >Final calculated score will be scaled to this value</small >
137145 </div >
138146
139147 <div class =" col-md-6" >
140148 <label class =" form-label" >Minimum Score (Rejected Submissions) *</label >
141149 <input type =" number" class =" form-control" v-model.number =" minScore" min =" 0" max =" 100"
142- placeholder =" 0" required />
150+ placeholder =" 0" required :disabled = " loading " />
143151 <small class =" text-muted" >Score for rejected submissions</small >
144152 </div >
145153 </div >
158166 <div class =" row align-items-center" >
159167 <div class =" col-md-3" >
160168 <input type =" text" class =" form-control" v-model =" param.name" placeholder =" Parameter name"
161- required />
169+ required :disabled = " loading " />
162170 </div >
163171 <div class =" col-md-3" >
164172 <div class =" input-group" >
165173 <input type =" number" class =" form-control" v-model.number =" param.weight" min =" 0" max =" 100"
166- placeholder =" Weight" required />
174+ placeholder =" Weight" required :disabled = " loading " />
167175 <span class =" input-group-text" >%</span >
168176 </div >
169177 </div >
170178 <div class =" col-md-5" >
171179 <input type =" text" class =" form-control" v-model =" param.description"
172- placeholder =" Description (optional)" />
180+ placeholder =" Description (optional)" :disabled = " loading " />
173181 </div >
174182 <div class =" col-md-1 text-end" >
175183 <button type =" button" class =" btn btn-sm btn-outline-danger" @click =" removeParameter(index)"
176- :disabled =" scoringParameters.length <= 1" >
184+ :disabled =" scoringParameters.length <= 1 || loading " >
177185 <i class =" fas fa-times" ></i >
178186 </button >
179187 </div >
182190 </div >
183191 </div >
184192
185- <button type =" button" class =" btn btn-sm btn-outline-primary mt-2" @click =" addParameter" >
193+ <button type =" button" class =" btn btn-sm btn-outline-primary mt-2" @click =" addParameter"
194+ :disabled =" loading" >
186195 <i class =" fas fa-plus me-1" ></i >Add Parameter
187196 </button >
188197
201210 </div >
202211
203212 <!-- Reset to default parameters -->
204- <button type =" button" class =" btn btn-sm btn-outline-secondary" @click =" loadDefaultParameters" >
213+ <button type =" button" class =" btn btn-sm btn-outline-secondary" @click =" loadDefaultParameters"
214+ :disabled =" loading" >
205215 <i class =" fas fa-redo me-1" ></i >Load Default Parameters
206216 </button >
207217 </div >
227237 <!-- Organizer search input with autocomplete dropdown -->
228238 <div style =" position : relative ;" >
229239 <input type =" text" class =" form-control" v-model =" organizerSearchQuery" @input =" searchOrganizers"
230- placeholder =" Type username to add additional organizers..." autocomplete =" off" />
240+ placeholder =" Type username to add additional organizers..." autocomplete =" off" :disabled = " loading " />
231241
232242 <!-- Autocomplete results dropdown -->
233243 <div v-if =" organizerSearchResults.length > 0 && organizerSearchQuery.length >= 2"
277287 <!-- Jury search input with autocomplete dropdown -->
278288 <div style =" position : relative ;" >
279289 <input type =" text" class =" form-control" id =" juryInput" v-model =" jurySearchQuery" @input =" searchJury"
280- placeholder =" Type username to search..." autocomplete =" off" />
290+ placeholder =" Type username to search..." autocomplete =" off" :disabled = " loading " />
281291 <!-- Autocomplete results with self-selection warning -->
282292 <div v-if =" jurySearchResults.length > 0 && jurySearchQuery.length >= 2"
283293 class =" jury-autocomplete position-absolute w-100 border rounded-bottom"
304314 <div class =" mb-3" >
305315 <label for =" minByteCount" class =" form-label" >Minimum Byte Count *</label >
306316 <input type =" number" class =" form-control" id =" minByteCount" v-model.number =" formData.min_byte_count"
307- min =" 0" placeholder =" e.g., 1000" required />
317+ min =" 0" placeholder =" e.g., 1000" required :disabled = " loading " />
308318 <small class =" form-text text-muted" >Articles must have at least this many bytes</small >
309319 </div >
310320
311321 <!-- Minimum reference/citation requirement -->
312322 <div class =" mb-3" >
313323 <label for =" minReferenceCount" class =" form-label" >Minimum Reference Count</label >
314324 <input type =" number" class =" form-control" id =" minReferenceCount"
315- v-model.number =" formData.min_reference_count" min =" 0" placeholder =" e.g., 5" />
325+ v-model.number =" formData.min_reference_count" min =" 0" placeholder =" e.g., 5" :disabled = " loading " />
316326 <small class =" form-text text-muted" >
317327 Articles must have at least this many references (external links). Leave as 0 for no requirement.
318328 </small >
329339 <div class =" input-group" >
330340 <input type =" url" class =" form-control" v-model =" formData.categories[index]"
331341 :placeholder =" index === 0 ? 'https://en.wikipedia.org/wiki/Category:Example' : 'Add another category URL'"
332- required />
342+ required :disabled = " loading " />
333343 <button v-if =" formData.categories.length > 1" type =" button" class =" btn btn-outline-danger"
334- @click =" removeCategory(index)" title =" Remove category" >
344+ @click =" removeCategory(index)" title =" Remove category" :disabled = " loading " >
335345 <i class =" fas fa-times" ></i >
336346 </button >
337347 </div >
338348 </div >
339349
340- <button type =" button" class =" btn btn-outline-primary btn-sm" @click =" addCategory" >
350+ <button type =" button" class =" btn btn-outline-primary btn-sm" @click =" addCategory" :disabled = " loading " >
341351 <i class =" fas fa-plus me-1" ></i >Add Category
342352 </button >
343353
353363 <span class =" badge bg-secondary ms-1" >Optional</span >
354364 </label >
355365 <input type =" url" class =" form-control" id =" templateLink" v-model =" formData.template_link"
356- placeholder =" https://en.wikipedia.org/wiki/Template:YourContestTemplate" />
366+ placeholder =" https://en.wikipedia.org/wiki/Template:YourContestTemplate" :disabled = " loading " />
357367 <small class =" form-text text-muted d-block mt-2" >
358368 <i class =" fas fa-info-circle me-1" ></i >
359369 If set, this template will be automatically added to submitted articles that don't already have it.
366376
367377 <!-- Modal footer with action buttons -->
368378 <div class =" modal-footer" >
369- <button type =" button" class =" btn btn-secondary" data-bs-dismiss =" modal" >Cancel</button >
370- <button type =" submit" class =" btn btn-primary" @click =" handleSubmit" >Create Contest</button >
379+ <button type =" button" class =" btn btn-secondary" data-bs-dismiss =" modal" :disabled =" loading" >Cancel</button >
380+ <button type =" submit" class =" btn btn-primary" @click =" handleSubmit" :disabled =" loading" >
381+ <span v-if =" loading" >
382+ <span class =" spinner-border spinner-border-sm me-2" role =" status" aria-hidden =" true" ></span >
383+ Creating...
384+ </span >
385+ <span v-else >Create Contest</span >
386+ </button >
371387 </div >
372388 </div >
373389 </div >
@@ -694,6 +710,11 @@ export default {
694710
695711 // Validate and submit contest creation form
696712 const handleSubmit = async () => {
713+ // Prevent multiple submissions
714+ if (loading .value ) {
715+ return
716+ }
717+
697718 // Basic validation
698719 if (! formData .name .trim ()) {
699720 showAlert (' Contest name is required' , ' warning' )
@@ -792,7 +813,9 @@ export default {
792813 }
793814 }
794815
816+ // Set loading state to prevent multiple submissions
795817 loading .value = true
818+
796819 try {
797820 // Build scoring parameters payload
798821 let scoringParametersPayload = null
@@ -855,13 +878,17 @@ export default {
855878 if (result .success ) {
856879 showAlert (' Contest created successfully!' , ' success' )
857880 emit (' created' )
881+
882+ // Wait a bit for alert to show
858883 await new Promise (resolve => setTimeout (resolve, 100 ))
884+
859885 // Close modal programmatically
860886 const modalElement = document .getElementById (' createContestModal' )
861887 const modal = bootstrap .Modal .getInstance (modalElement)
862888 if (modal) {
863889 modal .hide ()
864890 }
891+
865892 // Reset form
866893 resetForm ()
867894 } else {
@@ -870,6 +897,7 @@ export default {
870897 } catch (error) {
871898 showAlert (' Failed to create contest: ' + error .message , ' danger' )
872899 } finally {
900+ // Always reset loading state
873901 loading .value = false
874902 }
875903 }
0 commit comments