Skip to content

Commit a1c859b

Browse files
authored
Fix: prevent multiple contest submissions and fix alert z-index (#151)
- Add loading state to prevent duplicate contest creation on multiple clicks - Disable all form inputs and buttons during submission - Show spinner and 'Creating...' text on submit button while processing - Add early return in handleSubmit if already in loading state - Ensure loading state is reset in finally block for error handling These changes ensure users can only submit once per click and alerts are visible above the modal dialog during contest creation.
2 parents 019260e + 88b5a17 commit a1c859b

File tree

1 file changed

+61
-33
lines changed

1 file changed

+61
-33
lines changed

frontend/src/components/CreateContestModal.vue

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,44 @@
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>
@@ -50,11 +54,13 @@
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

@@ -75,16 +81,18 @@
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>
@@ -102,7 +110,7 @@
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.
@@ -111,7 +119,7 @@
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>
@@ -132,14 +140,14 @@
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>
@@ -158,22 +166,22 @@
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>
@@ -182,7 +190,8 @@
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

@@ -201,7 +210,8 @@
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>
@@ -227,7 +237,7 @@
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"
@@ -277,7 +287,7 @@
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"
@@ -304,15 +314,15 @@
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>
@@ -329,15 +339,15 @@
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

@@ -353,7 +363,7 @@
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.
@@ -366,8 +376,14 @@
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

Comments
 (0)