@commonpub/layer 0.25.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/contest/ContestHero.vue +6 -2
- package/components/contest/ContestJudgeManager.vue +6 -2
- package/components/contest/ContestJudges.vue +16 -20
- package/components/contest/ContestJudgingCriteria.vue +51 -0
- package/components/contest/ContestPrizes.vue +22 -17
- package/components/contest/ContestSidebar.vue +64 -7
- package/components/homepage/ContestsSection.vue +6 -1
- package/package.json +8 -8
- package/pages/contests/[slug]/edit.vue +163 -102
- package/pages/contests/[slug]/index.vue +170 -38
- package/pages/contests/[slug]/judge.vue +65 -10
- package/pages/contests/[slug]/results.vue +40 -2
- package/pages/contests/create.vue +160 -21
- package/server/api/contests/[slug]/entries/[entryId].delete.ts +1 -1
- package/server/api/contests/[slug]/entries.get.ts +17 -2
- package/server/api/contests/[slug]/votes.get.ts +3 -2
- package/server/api/contests/index.post.ts +9 -3
|
@@ -15,96 +15,106 @@ const saving = ref(false);
|
|
|
15
15
|
const title = ref('');
|
|
16
16
|
const description = ref('');
|
|
17
17
|
const rules = ref('');
|
|
18
|
+
const bannerUrl = ref('');
|
|
18
19
|
const startDate = ref('');
|
|
19
20
|
const endDate = ref('');
|
|
20
21
|
const judgingEndDate = ref('');
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const judgeSearch = ref('');
|
|
24
|
-
const judgeSearchResults = ref<Array<{ id: string; username: string; displayName: string | null }>>([]);
|
|
25
|
-
const searchingJudges = ref(false);
|
|
22
|
+
const communityVotingEnabled = ref(false);
|
|
23
|
+
const judgingVisibility = ref<'public' | 'judges-only' | 'private'>('judges-only');
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
const
|
|
25
|
+
const { enabledTypeMeta } = useContentTypes();
|
|
26
|
+
const eligibleContentTypes = ref<string[]>([]);
|
|
27
|
+
const maxEntriesPerUser = ref<number | null>(null);
|
|
28
|
+
function toggleType(type: string): void {
|
|
29
|
+
const i = eligibleContentTypes.value.indexOf(type);
|
|
30
|
+
if (i >= 0) eligibleContentTypes.value.splice(i, 1);
|
|
31
|
+
else eligibleContentTypes.value.push(type);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Prize { place: number | null; category: string; title: string; description: string; value: string }
|
|
35
|
+
const prizes = ref<Prize[]>([]);
|
|
36
|
+
|
|
37
|
+
interface Criterion { label: string; weight: number | null; description: string }
|
|
38
|
+
const criteria = ref<Criterion[]>([]);
|
|
29
39
|
|
|
30
40
|
// Load current data
|
|
31
|
-
watch(contest,
|
|
41
|
+
watch(contest, (c) => {
|
|
32
42
|
if (!c) return;
|
|
33
43
|
title.value = c.title ?? '';
|
|
34
44
|
description.value = c.description ?? '';
|
|
35
45
|
rules.value = c.rules ?? '';
|
|
46
|
+
bannerUrl.value = c.bannerUrl ?? '';
|
|
36
47
|
startDate.value = c.startDate ? new Date(c.startDate).toISOString().slice(0, 16) : '';
|
|
37
48
|
endDate.value = c.endDate ? new Date(c.endDate).toISOString().slice(0, 16) : '';
|
|
38
49
|
judgingEndDate.value = c.judgingEndDate ? new Date(c.judgingEndDate).toISOString().slice(0, 16) : '';
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
communityVotingEnabled.value = !!c.communityVotingEnabled;
|
|
51
|
+
judgingVisibility.value = (c.judgingVisibility as typeof judgingVisibility.value) ?? 'judges-only';
|
|
52
|
+
eligibleContentTypes.value = [...(c.eligibleContentTypes ?? [])];
|
|
53
|
+
maxEntriesPerUser.value = c.maxEntriesPerUser ?? null;
|
|
54
|
+
prizes.value = (c.prizes ?? []).map((p: { place?: number; category?: string; title: string; description?: string; value?: string }) => ({
|
|
55
|
+
place: p.place ?? null,
|
|
56
|
+
category: p.category ?? '',
|
|
41
57
|
title: p.title,
|
|
42
58
|
description: p.description ?? '',
|
|
43
59
|
value: p.value ?? '',
|
|
44
60
|
}));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
resolvedJudges.value = data.items;
|
|
51
|
-
} catch { /* ignore */ }
|
|
52
|
-
}
|
|
61
|
+
criteria.value = (c.judgingCriteria ?? []).map((cr: { label: string; weight?: number; description?: string }) => ({
|
|
62
|
+
label: cr.label,
|
|
63
|
+
weight: cr.weight ?? null,
|
|
64
|
+
description: cr.description ?? '',
|
|
65
|
+
}));
|
|
53
66
|
}, { immediate: true });
|
|
54
67
|
|
|
55
|
-
async function searchJudge(): Promise<void> {
|
|
56
|
-
const q = judgeSearch.value.trim();
|
|
57
|
-
if (!q || q.length < 2) { judgeSearchResults.value = []; return; }
|
|
58
|
-
searchingJudges.value = true;
|
|
59
|
-
try {
|
|
60
|
-
const data = await $fetch<{ items: Array<{ id: string; username: string; displayName: string | null }> }>('/api/users', { query: { q, limit: 5 } });
|
|
61
|
-
judgeSearchResults.value = data.items.filter((u) => !judgeIds.value.includes(u.id));
|
|
62
|
-
} catch { judgeSearchResults.value = []; }
|
|
63
|
-
finally { searchingJudges.value = false; }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function addJudge(u: { id: string; username: string; displayName: string | null }): void {
|
|
67
|
-
if (!judgeIds.value.includes(u.id)) {
|
|
68
|
-
judgeIds.value.push(u.id);
|
|
69
|
-
resolvedJudges.value.push(u);
|
|
70
|
-
}
|
|
71
|
-
judgeSearch.value = '';
|
|
72
|
-
judgeSearchResults.value = [];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function removeJudge(id: string): void {
|
|
76
|
-
judgeIds.value = judgeIds.value.filter((jid) => jid !== id);
|
|
77
|
-
resolvedJudges.value = resolvedJudges.value.filter((j) => j.id !== id);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
68
|
function addPrize(): void {
|
|
81
|
-
|
|
82
|
-
prizes.value.push({ place: nextPlace, title: '', description: '', value: '' });
|
|
69
|
+
prizes.value.push({ place: null, category: '', title: '', description: '', value: '' });
|
|
83
70
|
}
|
|
84
|
-
|
|
85
71
|
function removePrize(index: number): void {
|
|
86
72
|
prizes.value.splice(index, 1);
|
|
87
|
-
|
|
73
|
+
}
|
|
74
|
+
function prizeLabel(prize: Prize, idx: number): string {
|
|
75
|
+
if (prize.category.trim()) return prize.category;
|
|
76
|
+
const labels = ['1st', '2nd', '3rd', '4th', '5th', '6th'];
|
|
77
|
+
return prize.place ? `${labels[prize.place - 1] || `${prize.place}th`} Place` : `${labels[idx] || `${idx + 1}th`} Place`;
|
|
88
78
|
}
|
|
89
79
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
80
|
+
function addCriterion(): void {
|
|
81
|
+
criteria.value.push({ label: '', weight: null, description: '' });
|
|
82
|
+
}
|
|
83
|
+
function removeCriterion(index: number): void {
|
|
84
|
+
criteria.value.splice(index, 1);
|
|
95
85
|
}
|
|
86
|
+
const criteriaTotal = computed(() => criteria.value.reduce((s, c) => s + (c.weight ?? 0), 0));
|
|
87
|
+
|
|
88
|
+
const dateError = computed(() => {
|
|
89
|
+
if (startDate.value && endDate.value && new Date(endDate.value) <= new Date(startDate.value)) {
|
|
90
|
+
return 'End date must be after the start date.';
|
|
91
|
+
}
|
|
92
|
+
if (judgingEndDate.value && endDate.value && new Date(judgingEndDate.value) < new Date(endDate.value)) {
|
|
93
|
+
return 'Judging end date must be on or after the end date.';
|
|
94
|
+
}
|
|
95
|
+
return '';
|
|
96
|
+
});
|
|
96
97
|
|
|
97
98
|
async function handleSave(): Promise<void> {
|
|
99
|
+
if (dateError.value) { toast.error(dateError.value); return; }
|
|
98
100
|
saving.value = true;
|
|
99
101
|
try {
|
|
100
102
|
const prizeData = prizes.value
|
|
101
103
|
.filter((p) => p.title.trim())
|
|
102
104
|
.map((p) => ({
|
|
103
|
-
place: p.place,
|
|
105
|
+
place: typeof p.place === 'number' && Number.isFinite(p.place) && p.place > 0 ? p.place : undefined,
|
|
106
|
+
category: p.category.trim() || undefined,
|
|
104
107
|
title: p.title,
|
|
105
108
|
description: p.description || undefined,
|
|
106
109
|
value: p.value || undefined,
|
|
107
110
|
}));
|
|
111
|
+
const criteriaData = criteria.value
|
|
112
|
+
.filter((c) => c.label.trim())
|
|
113
|
+
.map((c) => ({
|
|
114
|
+
label: c.label.trim(),
|
|
115
|
+
weight: typeof c.weight === 'number' && Number.isFinite(c.weight) ? c.weight : undefined,
|
|
116
|
+
description: c.description.trim() || undefined,
|
|
117
|
+
}));
|
|
108
118
|
|
|
109
119
|
await $fetch(`/api/contests/${slug}`, {
|
|
110
120
|
method: 'PUT',
|
|
@@ -112,11 +122,16 @@ async function handleSave(): Promise<void> {
|
|
|
112
122
|
title: title.value,
|
|
113
123
|
description: description.value || undefined,
|
|
114
124
|
rules: rules.value || undefined,
|
|
125
|
+
bannerUrl: bannerUrl.value || undefined,
|
|
115
126
|
startDate: startDate.value ? new Date(startDate.value).toISOString() : undefined,
|
|
116
127
|
endDate: endDate.value ? new Date(endDate.value).toISOString() : undefined,
|
|
117
128
|
judgingEndDate: judgingEndDate.value ? new Date(judgingEndDate.value).toISOString() : undefined,
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
communityVotingEnabled: communityVotingEnabled.value,
|
|
130
|
+
judgingVisibility: judgingVisibility.value,
|
|
131
|
+
eligibleContentTypes: eligibleContentTypes.value,
|
|
132
|
+
maxEntriesPerUser: maxEntriesPerUser.value && maxEntriesPerUser.value > 0 ? maxEntriesPerUser.value : undefined,
|
|
133
|
+
prizes: prizeData,
|
|
134
|
+
judgingCriteria: criteriaData,
|
|
120
135
|
},
|
|
121
136
|
});
|
|
122
137
|
toast.success('Contest updated');
|
|
@@ -134,10 +149,7 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
134
149
|
: `Change contest status to "${newStatus}"?`;
|
|
135
150
|
if (!confirm(msg)) return;
|
|
136
151
|
try {
|
|
137
|
-
await $fetch(`/api/contests/${slug}/transition`, {
|
|
138
|
-
method: 'POST',
|
|
139
|
-
body: { status: newStatus },
|
|
140
|
-
});
|
|
152
|
+
await $fetch(`/api/contests/${slug}/transition`, { method: 'POST', body: { status: newStatus } });
|
|
141
153
|
toast.success(`Status changed to ${newStatus}`);
|
|
142
154
|
await refresh();
|
|
143
155
|
} catch (err: unknown) {
|
|
@@ -173,6 +185,10 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
173
185
|
<label class="cpub-form-label">Rules</label>
|
|
174
186
|
<textarea v-model="rules" class="cpub-form-textarea" rows="4" placeholder="One rule per line" />
|
|
175
187
|
</div>
|
|
188
|
+
<div class="cpub-form-field">
|
|
189
|
+
<label class="cpub-form-label">Banner Image URL</label>
|
|
190
|
+
<input v-model="bannerUrl" type="url" class="cpub-form-input" placeholder="https://..." />
|
|
191
|
+
</div>
|
|
176
192
|
</section>
|
|
177
193
|
|
|
178
194
|
<section class="cpub-form-section">
|
|
@@ -191,14 +207,43 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
191
207
|
<label class="cpub-form-label">Judging End Date</label>
|
|
192
208
|
<input v-model="judgingEndDate" type="datetime-local" class="cpub-form-input" />
|
|
193
209
|
</div>
|
|
210
|
+
<p v-if="dateError" class="cpub-form-error" role="alert">{{ dateError }}</p>
|
|
211
|
+
</section>
|
|
212
|
+
|
|
213
|
+
<section class="cpub-form-section">
|
|
214
|
+
<h2 class="cpub-form-section-title">Entries</h2>
|
|
215
|
+
<div class="cpub-form-field">
|
|
216
|
+
<span class="cpub-form-label">Eligible content types</span>
|
|
217
|
+
<p class="cpub-form-hint">Leave all unchecked to accept any published content the entrant owns.</p>
|
|
218
|
+
<div class="cpub-type-options" role="group" aria-label="Eligible content types">
|
|
219
|
+
<label v-for="t in enabledTypeMeta" :key="t.type" class="cpub-form-check">
|
|
220
|
+
<input type="checkbox" :checked="eligibleContentTypes.includes(t.type)" @change="toggleType(t.type)" />
|
|
221
|
+
<span>{{ t.label }}</span>
|
|
222
|
+
</label>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="cpub-form-field">
|
|
226
|
+
<label class="cpub-form-label">Max entries per person</label>
|
|
227
|
+
<input v-model.number="maxEntriesPerUser" type="number" min="1" class="cpub-form-input" placeholder="Unlimited" style="max-width: 160px;" />
|
|
228
|
+
</div>
|
|
194
229
|
</section>
|
|
195
230
|
|
|
196
231
|
<section class="cpub-form-section">
|
|
197
232
|
<h2 class="cpub-form-section-title">Prizes</h2>
|
|
198
233
|
<div v-for="(prize, i) in prizes" :key="i" class="cpub-prize-row">
|
|
199
234
|
<div class="cpub-prize-header">
|
|
200
|
-
<span class="cpub-prize-label">{{
|
|
201
|
-
<button type="button" class="cpub-prize-remove" @click="removePrize(i)"><i class="fa-solid fa-times"></i></button>
|
|
235
|
+
<span class="cpub-prize-label">{{ prizeLabel(prize, i) }}</span>
|
|
236
|
+
<button type="button" class="cpub-prize-remove" aria-label="Remove prize" @click="removePrize(i)"><i class="fa-solid fa-times"></i></button>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="cpub-form-row">
|
|
239
|
+
<div class="cpub-form-field">
|
|
240
|
+
<label class="cpub-form-label">Place</label>
|
|
241
|
+
<input v-model.number="prize.place" type="number" min="1" class="cpub-form-input" placeholder="1" />
|
|
242
|
+
</div>
|
|
243
|
+
<div class="cpub-form-field">
|
|
244
|
+
<label class="cpub-form-label">Category (optional)</label>
|
|
245
|
+
<input v-model="prize.category" type="text" class="cpub-form-input" placeholder="e.g. Best in Show" />
|
|
246
|
+
</div>
|
|
202
247
|
</div>
|
|
203
248
|
<div class="cpub-form-row">
|
|
204
249
|
<div class="cpub-form-field">
|
|
@@ -219,37 +264,49 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
219
264
|
</section>
|
|
220
265
|
|
|
221
266
|
<section class="cpub-form-section">
|
|
222
|
-
<h2 class="cpub-form-section-title">
|
|
223
|
-
<div
|
|
224
|
-
<
|
|
225
|
-
|
|
226
|
-
<
|
|
227
|
-
|
|
267
|
+
<h2 class="cpub-form-section-title">Judging</h2>
|
|
268
|
+
<div class="cpub-form-field">
|
|
269
|
+
<label class="cpub-form-label">Score Visibility</label>
|
|
270
|
+
<select v-model="judgingVisibility" class="cpub-form-input">
|
|
271
|
+
<option value="judges-only">Judges only — scores hidden until results</option>
|
|
272
|
+
<option value="public">Public — show scores during judging</option>
|
|
273
|
+
<option value="private">Private — scores stay with organizers</option>
|
|
274
|
+
</select>
|
|
275
|
+
</div>
|
|
276
|
+
<label class="cpub-form-check">
|
|
277
|
+
<input v-model="communityVotingEnabled" type="checkbox" />
|
|
278
|
+
<span>Enable community voting</span>
|
|
279
|
+
</label>
|
|
280
|
+
|
|
281
|
+
<div class="cpub-subhead">
|
|
282
|
+
<h3 class="cpub-form-subtitle">Judging Criteria <span v-if="criteriaTotal" class="cpub-form-hint-inline">{{ criteriaTotal }} pts</span></h3>
|
|
283
|
+
<button type="button" class="cpub-btn cpub-btn-sm" @click="addCriterion"><i class="fa-solid fa-plus"></i> Add Criterion</button>
|
|
228
284
|
</div>
|
|
229
|
-
<div v-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
<button
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
class="cpub-judge-option"
|
|
244
|
-
@click="addJudge(u)"
|
|
245
|
-
>
|
|
246
|
-
<strong>{{ u.displayName || u.username }}</strong>
|
|
247
|
-
<span class="cpub-judge-option-username">@{{ u.username }}</span>
|
|
248
|
-
</button>
|
|
285
|
+
<div v-for="(crit, ci) in criteria" :key="ci" class="cpub-criterion-row">
|
|
286
|
+
<div class="cpub-form-row">
|
|
287
|
+
<div class="cpub-form-field" style="flex: 3">
|
|
288
|
+
<label class="cpub-form-label">Criterion</label>
|
|
289
|
+
<input v-model="crit.label" type="text" class="cpub-form-input" placeholder="e.g. Documentation" />
|
|
290
|
+
</div>
|
|
291
|
+
<div class="cpub-form-field" style="flex: 1">
|
|
292
|
+
<label class="cpub-form-label">Points</label>
|
|
293
|
+
<input v-model.number="crit.weight" type="number" min="0" max="100" class="cpub-form-input" placeholder="20" />
|
|
294
|
+
</div>
|
|
295
|
+
<button type="button" class="cpub-prize-remove cpub-criterion-del" aria-label="Remove criterion" @click="removeCriterion(ci)"><i class="fa-solid fa-times"></i></button>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="cpub-form-field">
|
|
298
|
+
<input v-model="crit.description" type="text" class="cpub-form-input" placeholder="What judges look for (optional)" />
|
|
249
299
|
</div>
|
|
250
300
|
</div>
|
|
251
301
|
</section>
|
|
252
302
|
|
|
303
|
+
<!-- Judge panel (single source of truth: contest_judges table) -->
|
|
304
|
+
<section class="cpub-form-section">
|
|
305
|
+
<h2 class="cpub-form-section-title">Judges</h2>
|
|
306
|
+
<p class="cpub-form-hint">Invited judges receive a notification and must accept before they can score.</p>
|
|
307
|
+
<ContestJudgeManager :contest-slug="slug" :is-owner="true" />
|
|
308
|
+
</section>
|
|
309
|
+
|
|
253
310
|
<section class="cpub-form-section">
|
|
254
311
|
<h2 class="cpub-form-section-title">Status Transitions</h2>
|
|
255
312
|
<div class="cpub-status-actions">
|
|
@@ -273,7 +330,7 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
273
330
|
</div>
|
|
274
331
|
</section>
|
|
275
332
|
|
|
276
|
-
<button type="submit" class="cpub-btn cpub-btn-primary" :disabled="saving || !title.trim()">
|
|
333
|
+
<button type="submit" class="cpub-btn cpub-btn-primary" :disabled="saving || !title.trim() || !!dateError">
|
|
277
334
|
<i class="fa-solid fa-floppy-disk"></i> {{ saving ? 'Saving...' : 'Save Changes' }}
|
|
278
335
|
</button>
|
|
279
336
|
</form>
|
|
@@ -306,11 +363,22 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
306
363
|
.cpub-form-textarea { resize: vertical; }
|
|
307
364
|
.cpub-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
308
365
|
|
|
309
|
-
.cpub-
|
|
366
|
+
.cpub-form-error { font-size: 12px; color: var(--red); margin-top: 8px; }
|
|
367
|
+
.cpub-form-check { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-dim); cursor: pointer; }
|
|
368
|
+
.cpub-form-check input { width: 14px; height: 14px; }
|
|
369
|
+
.cpub-type-options { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 6px; }
|
|
370
|
+
.cpub-subhead { display: flex; align-items: center; justify-content: space-between; margin: 18px 0 10px; }
|
|
371
|
+
.cpub-form-subtitle { font-size: 12px; font-weight: 700; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .06em; color: var(--text-dim); display: flex; align-items: center; gap: 8px; }
|
|
372
|
+
.cpub-form-hint-inline { font-size: 10px; color: var(--accent); }
|
|
373
|
+
.cpub-form-hint { font-size: 11px; color: var(--text-faint); margin: 0 0 12px; line-height: 1.5; }
|
|
374
|
+
|
|
375
|
+
.cpub-prize-row, .cpub-criterion-row { border: var(--border-width-default) solid var(--border); padding: 14px; margin-bottom: 10px; background: var(--surface2); }
|
|
310
376
|
.cpub-prize-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
|
311
|
-
.cpub-prize-label { font-size: 11px; font-weight: 700; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em; }
|
|
377
|
+
.cpub-prize-label { font-size: 11px; font-weight: 700; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--accent); }
|
|
312
378
|
.cpub-prize-remove { background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: 12px; }
|
|
313
379
|
.cpub-prize-remove:hover { color: var(--red); }
|
|
380
|
+
.cpub-criterion-row .cpub-form-row { align-items: flex-end; }
|
|
381
|
+
.cpub-criterion-del { align-self: flex-end; margin-bottom: 12px; }
|
|
314
382
|
|
|
315
383
|
.cpub-status-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
316
384
|
.cpub-transition-btn { display: inline-flex; align-items: center; gap: 6px; }
|
|
@@ -319,17 +387,10 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
319
387
|
.cpub-transition-complete { color: var(--accent); border-color: var(--accent-border); }
|
|
320
388
|
.cpub-transition-cancel { color: var(--red); border-color: var(--red-border); }
|
|
321
389
|
|
|
322
|
-
.cpub-judge-list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
|
323
|
-
.cpub-judge-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: var(--accent-bg); border: var(--border-width-default) solid var(--accent-border); font-size: 11px; font-family: var(--font-mono); color: var(--accent); }
|
|
324
|
-
.cpub-judge-chip-remove { background: none; border: none; color: var(--accent); cursor: pointer; font-size: 10px; padding: 0; }
|
|
325
|
-
.cpub-judge-chip-remove:hover { color: var(--red); }
|
|
326
|
-
.cpub-judge-empty { font-size: 12px; color: var(--text-faint); margin-bottom: 12px; }
|
|
327
|
-
.cpub-judge-search { position: relative; }
|
|
328
|
-
.cpub-judge-dropdown { position: absolute; z-index: 10; top: 100%; left: 0; right: 0; background: var(--surface); border: var(--border-width-default) solid var(--border); box-shadow: var(--shadow-lg); max-height: 200px; overflow-y: auto; }
|
|
329
|
-
.cpub-judge-option { display: flex; align-items: center; gap: 8px; width: 100%; padding: 8px 12px; background: none; border: none; border-bottom: var(--border-width-default) solid var(--border); cursor: pointer; font-size: 12px; color: var(--text); text-align: left; }
|
|
330
|
-
.cpub-judge-option:last-child { border-bottom: none; }
|
|
331
|
-
.cpub-judge-option:hover { background: var(--surface2); }
|
|
332
|
-
.cpub-judge-option-username { color: var(--text-faint); font-family: var(--font-mono); font-size: 11px; }
|
|
333
|
-
|
|
334
390
|
.cpub-not-found { text-align: center; padding: 64px; color: var(--text-dim); display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
|
391
|
+
|
|
392
|
+
@media (max-width: 768px) {
|
|
393
|
+
.cpub-contest-edit { padding: 16px; }
|
|
394
|
+
.cpub-form-row { grid-template-columns: 1fr; }
|
|
395
|
+
}
|
|
335
396
|
</style>
|