@commonpub/layer 0.8.3 → 0.8.5
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/ContentCard.vue +1 -1
- package/components/ImageUpload.vue +1 -1
- package/components/ShareToHubModal.vue +1 -1
- package/components/blocks/BlockCodeView.vue +26 -25
- package/components/contest/ContestEntries.vue +112 -0
- package/components/contest/ContestHero.vue +204 -0
- package/components/contest/ContestJudges.vue +51 -0
- package/components/contest/ContestPrizes.vue +82 -0
- package/components/contest/ContestRules.vue +34 -0
- package/components/contest/ContestSidebar.vue +83 -0
- package/components/editors/BlogEditor.vue +1 -1
- package/components/editors/DocsPageTree.vue +10 -0
- package/components/hub/HubHero.vue +1 -1
- package/composables/useSanitize.ts +112 -9
- package/composables/useTheme.ts +8 -0
- package/layouts/default.vue +7 -7
- package/middleware/feature-gate.global.ts +24 -0
- package/package.json +6 -6
- package/pages/[type]/index.vue +4 -3
- package/pages/admin/audit.vue +3 -2
- package/pages/admin/federation.vue +33 -13
- package/pages/admin/index.vue +7 -1
- package/pages/admin/reports.vue +152 -36
- package/pages/admin/settings.vue +17 -5
- package/pages/admin/theme.vue +5 -3
- package/pages/auth/forgot-password.vue +35 -35
- package/pages/auth/login.vue +6 -5
- package/pages/auth/reset-password.vue +44 -32
- package/pages/contests/[slug]/edit.vue +238 -56
- package/pages/contests/[slug]/index.vue +54 -450
- package/pages/contests/[slug]/judge.vue +141 -53
- package/pages/contests/[slug]/results.vue +182 -0
- package/pages/contests/create.vue +64 -64
- package/pages/contests/index.vue +2 -1
- package/pages/docs/[siteSlug]/[...pagePath].vue +6 -5
- package/pages/docs/[siteSlug]/edit.vue +58 -2
- package/pages/docs/[siteSlug]/index.vue +6 -5
- package/pages/federated-hubs/[id]/posts/[postId].vue +2 -2
- package/pages/hubs/index.vue +3 -2
- package/pages/index.vue +25 -7
- package/pages/learn/index.vue +1 -1
- package/pages/mirror/[id].vue +3 -3
- package/pages/notifications.vue +15 -1
- package/pages/products/[slug].vue +5 -2
- package/pages/settings/notifications.vue +7 -1
- package/pages/tags/[slug].vue +3 -2
- package/pages/tags/index.vue +3 -2
- package/pages/videos/[id].vue +18 -0
- package/server/api/admin/content/[id].patch.ts +1 -1
- package/server/api/admin/federation/mirrors/[id]/backfill.post.ts +1 -1
- package/server/api/admin/federation/refederate.post.ts +7 -3
- package/server/api/admin/federation/repair-types.post.ts +2 -45
- package/server/api/admin/federation/retry.post.ts +7 -4
- package/server/api/admin/reports.get.ts +1 -0
- package/server/api/auth/federated/login.post.ts +22 -2
- package/server/api/auth/sign-in-username.post.ts +42 -0
- package/server/api/content/[id]/products-sync.post.ts +7 -6
- package/server/api/contests/[slug]/entries/[entryId].delete.ts +14 -0
- package/server/api/contests/[slug]/entries.get.ts +6 -1
- package/server/api/contests/[slug]/judge.post.ts +8 -2
- package/server/api/docs/[siteSlug]/nav.get.ts +1 -1
- package/server/api/docs/[siteSlug]/pages/[pageId]/duplicate.post.ts +16 -0
- package/server/api/docs/[siteSlug]/pages/reorder.post.ts +4 -1
- package/server/api/docs/migrate-content.post.ts +1 -7
- package/server/api/federation/hub-follow-status.get.ts +2 -18
- package/server/api/federation/hub-follow.post.ts +9 -27
- package/server/api/federation/hub-post-like.post.ts +9 -98
- package/server/api/federation/hub-post-likes.get.ts +3 -13
- package/server/api/notifications/read.post.ts +6 -1
- package/server/api/profile/theme.put.ts +23 -0
- package/server/api/search/index.get.ts +2 -2
- package/server/api/search/trending.get.ts +3 -3
- package/server/api/users/index.get.ts +9 -2
- package/server/middleware/content-ap.ts +2 -2
- package/server/routes/.well-known/webfinger.ts +2 -2
- package/theme/base.css +23 -0
- package/components/EditorPropertiesPanel.vue +0 -393
- package/components/views/BlogView.vue +0 -735
- package/server/api/resolve-identity.post.ts +0 -34
|
@@ -5,8 +5,10 @@ const route = useRoute();
|
|
|
5
5
|
const slug = route.params.slug as string;
|
|
6
6
|
const toast = useToast();
|
|
7
7
|
const { extract: extractError } = useApiError();
|
|
8
|
+
const { user, isAdmin } = useAuth();
|
|
8
9
|
|
|
9
10
|
const { data: contest, refresh } = useLazyFetch(`/api/contests/${slug}`);
|
|
11
|
+
const isOwner = computed(() => isAdmin.value || !!(user.value?.id && contest.value?.createdById === user.value.id));
|
|
10
12
|
useSeoMeta({ title: () => `Edit: ${contest.value?.title ?? 'Contest'} — ${useSiteName()}` });
|
|
11
13
|
|
|
12
14
|
const saving = ref(false);
|
|
@@ -15,20 +17,95 @@ const description = ref('');
|
|
|
15
17
|
const rules = ref('');
|
|
16
18
|
const startDate = ref('');
|
|
17
19
|
const endDate = ref('');
|
|
20
|
+
const judgingEndDate = ref('');
|
|
21
|
+
const prizes = ref<Array<{ place: number; title: string; description: string; value: string }>>([]);
|
|
22
|
+
const judgeIds = ref<string[]>([]);
|
|
23
|
+
const judgeSearch = ref('');
|
|
24
|
+
const judgeSearchResults = ref<Array<{ id: string; username: string; displayName: string | null }>>([]);
|
|
25
|
+
const searchingJudges = ref(false);
|
|
26
|
+
|
|
27
|
+
interface JudgeDisplay { id: string; username: string; displayName: string | null }
|
|
28
|
+
const resolvedJudges = ref<JudgeDisplay[]>([]);
|
|
18
29
|
|
|
19
30
|
// Load current data
|
|
20
|
-
watch(contest, (c) => {
|
|
31
|
+
watch(contest, async (c) => {
|
|
21
32
|
if (!c) return;
|
|
22
33
|
title.value = c.title ?? '';
|
|
23
34
|
description.value = c.description ?? '';
|
|
24
35
|
rules.value = c.rules ?? '';
|
|
25
36
|
startDate.value = c.startDate ? new Date(c.startDate).toISOString().slice(0, 16) : '';
|
|
26
37
|
endDate.value = c.endDate ? new Date(c.endDate).toISOString().slice(0, 16) : '';
|
|
38
|
+
judgingEndDate.value = c.judgingEndDate ? new Date(c.judgingEndDate).toISOString().slice(0, 16) : '';
|
|
39
|
+
prizes.value = (c.prizes ?? []).map((p: { place: number; title: string; description?: string; value?: string }) => ({
|
|
40
|
+
place: p.place,
|
|
41
|
+
title: p.title,
|
|
42
|
+
description: p.description ?? '',
|
|
43
|
+
value: p.value ?? '',
|
|
44
|
+
}));
|
|
45
|
+
judgeIds.value = [...(c.judges ?? [])];
|
|
46
|
+
// Resolve judge IDs to display info
|
|
47
|
+
if (judgeIds.value.length > 0) {
|
|
48
|
+
try {
|
|
49
|
+
const data = await $fetch<{ items: JudgeDisplay[] }>('/api/users', { query: { ids: judgeIds.value.join(',') } });
|
|
50
|
+
resolvedJudges.value = data.items;
|
|
51
|
+
} catch { /* ignore */ }
|
|
52
|
+
}
|
|
27
53
|
}, { immediate: true });
|
|
28
54
|
|
|
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
|
+
function addPrize(): void {
|
|
81
|
+
const nextPlace = prizes.value.length + 1;
|
|
82
|
+
prizes.value.push({ place: nextPlace, title: '', description: '', value: '' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function removePrize(index: number): void {
|
|
86
|
+
prizes.value.splice(index, 1);
|
|
87
|
+
prizes.value.forEach((p, i) => { p.place = i + 1; });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function placeLabel(place: number): string {
|
|
91
|
+
if (place === 1) return '1st Place';
|
|
92
|
+
if (place === 2) return '2nd Place';
|
|
93
|
+
if (place === 3) return '3rd Place';
|
|
94
|
+
return `${place}th Place`;
|
|
95
|
+
}
|
|
96
|
+
|
|
29
97
|
async function handleSave(): Promise<void> {
|
|
30
98
|
saving.value = true;
|
|
31
99
|
try {
|
|
100
|
+
const prizeData = prizes.value
|
|
101
|
+
.filter((p) => p.title.trim())
|
|
102
|
+
.map((p) => ({
|
|
103
|
+
place: p.place,
|
|
104
|
+
title: p.title,
|
|
105
|
+
description: p.description || undefined,
|
|
106
|
+
value: p.value || undefined,
|
|
107
|
+
}));
|
|
108
|
+
|
|
32
109
|
await $fetch(`/api/contests/${slug}`, {
|
|
33
110
|
method: 'PUT',
|
|
34
111
|
body: {
|
|
@@ -37,6 +114,9 @@ async function handleSave(): Promise<void> {
|
|
|
37
114
|
rules: rules.value || undefined,
|
|
38
115
|
startDate: startDate.value ? new Date(startDate.value).toISOString() : undefined,
|
|
39
116
|
endDate: endDate.value ? new Date(endDate.value).toISOString() : undefined,
|
|
117
|
+
judgingEndDate: judgingEndDate.value ? new Date(judgingEndDate.value).toISOString() : undefined,
|
|
118
|
+
prizes: prizeData.length > 0 ? prizeData : undefined,
|
|
119
|
+
judges: judgeIds.value.length > 0 ? judgeIds.value : undefined,
|
|
40
120
|
},
|
|
41
121
|
});
|
|
42
122
|
toast.success('Contest updated');
|
|
@@ -49,7 +129,10 @@ async function handleSave(): Promise<void> {
|
|
|
49
129
|
}
|
|
50
130
|
|
|
51
131
|
async function transitionStatus(newStatus: string): Promise<void> {
|
|
52
|
-
|
|
132
|
+
const msg = newStatus === 'cancelled'
|
|
133
|
+
? 'Cancel this contest? This cannot be undone.'
|
|
134
|
+
: `Change contest status to "${newStatus}"?`;
|
|
135
|
+
if (!confirm(msg)) return;
|
|
53
136
|
try {
|
|
54
137
|
await $fetch(`/api/contests/${slug}/transition`, {
|
|
55
138
|
method: 'POST',
|
|
@@ -64,56 +147,129 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
64
147
|
</script>
|
|
65
148
|
|
|
66
149
|
<template>
|
|
67
|
-
<div v-if="contest" class="
|
|
150
|
+
<div v-if="contest && !isOwner" class="cpub-not-found">
|
|
151
|
+
<p>You don't have permission to edit this contest.</p>
|
|
152
|
+
<NuxtLink :to="`/contests/${slug}`" class="cpub-btn cpub-btn-sm">Back to Contest</NuxtLink>
|
|
153
|
+
</div>
|
|
154
|
+
<div v-else-if="contest" class="cpub-contest-edit">
|
|
68
155
|
<NuxtLink :to="`/contests/${slug}`" class="cpub-back-link"><i class="fa-solid fa-arrow-left"></i> Back to contest</NuxtLink>
|
|
69
|
-
<h1 class="
|
|
70
|
-
<p class="
|
|
71
|
-
Status: <span class="status-badge" :class="`status-${contest.status}`">{{ contest.status }}</span>
|
|
156
|
+
<h1 class="cpub-edit-title">Edit Contest</h1>
|
|
157
|
+
<p class="cpub-edit-subtitle">
|
|
158
|
+
Status: <span class="cpub-status-badge" :class="`cpub-status-${contest.status}`">{{ contest.status }}</span>
|
|
72
159
|
</p>
|
|
73
160
|
|
|
74
|
-
<form class="edit-form" @submit.prevent="handleSave">
|
|
75
|
-
<section class="form-section">
|
|
76
|
-
<h2 class="form-section-title">Details</h2>
|
|
77
|
-
<div class="form-field">
|
|
78
|
-
<label class="form-label">Title</label>
|
|
79
|
-
<input v-model="title" type="text" class="form-input" />
|
|
161
|
+
<form class="cpub-edit-form" @submit.prevent="handleSave">
|
|
162
|
+
<section class="cpub-form-section">
|
|
163
|
+
<h2 class="cpub-form-section-title">Details</h2>
|
|
164
|
+
<div class="cpub-form-field">
|
|
165
|
+
<label class="cpub-form-label">Title</label>
|
|
166
|
+
<input v-model="title" type="text" class="cpub-form-input" />
|
|
167
|
+
</div>
|
|
168
|
+
<div class="cpub-form-field">
|
|
169
|
+
<label class="cpub-form-label">Description</label>
|
|
170
|
+
<textarea v-model="description" class="cpub-form-textarea" rows="3" />
|
|
171
|
+
</div>
|
|
172
|
+
<div class="cpub-form-field">
|
|
173
|
+
<label class="cpub-form-label">Rules</label>
|
|
174
|
+
<textarea v-model="rules" class="cpub-form-textarea" rows="4" placeholder="One rule per line" />
|
|
175
|
+
</div>
|
|
176
|
+
</section>
|
|
177
|
+
|
|
178
|
+
<section class="cpub-form-section">
|
|
179
|
+
<h2 class="cpub-form-section-title">Schedule</h2>
|
|
180
|
+
<div class="cpub-form-row">
|
|
181
|
+
<div class="cpub-form-field">
|
|
182
|
+
<label class="cpub-form-label">Start Date</label>
|
|
183
|
+
<input v-model="startDate" type="datetime-local" class="cpub-form-input" />
|
|
184
|
+
</div>
|
|
185
|
+
<div class="cpub-form-field">
|
|
186
|
+
<label class="cpub-form-label">End Date</label>
|
|
187
|
+
<input v-model="endDate" type="datetime-local" class="cpub-form-input" />
|
|
188
|
+
</div>
|
|
80
189
|
</div>
|
|
81
|
-
<div class="form-field">
|
|
82
|
-
<label class="form-label">
|
|
83
|
-
<
|
|
190
|
+
<div class="cpub-form-field">
|
|
191
|
+
<label class="cpub-form-label">Judging End Date</label>
|
|
192
|
+
<input v-model="judgingEndDate" type="datetime-local" class="cpub-form-input" />
|
|
84
193
|
</div>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
194
|
+
</section>
|
|
195
|
+
|
|
196
|
+
<section class="cpub-form-section">
|
|
197
|
+
<h2 class="cpub-form-section-title">Prizes</h2>
|
|
198
|
+
<div v-for="(prize, i) in prizes" :key="i" class="cpub-prize-row">
|
|
199
|
+
<div class="cpub-prize-header">
|
|
200
|
+
<span class="cpub-prize-label">{{ placeLabel(prize.place) }}</span>
|
|
201
|
+
<button type="button" class="cpub-prize-remove" @click="removePrize(i)"><i class="fa-solid fa-times"></i></button>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="cpub-form-row">
|
|
204
|
+
<div class="cpub-form-field">
|
|
205
|
+
<label class="cpub-form-label">Title</label>
|
|
206
|
+
<input v-model="prize.title" type="text" class="cpub-form-input" placeholder="e.g. Gold Prize" />
|
|
207
|
+
</div>
|
|
208
|
+
<div class="cpub-form-field">
|
|
209
|
+
<label class="cpub-form-label">Value</label>
|
|
210
|
+
<input v-model="prize.value" type="text" class="cpub-form-input" placeholder="e.g. $500" />
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="cpub-form-field">
|
|
214
|
+
<label class="cpub-form-label">Description</label>
|
|
215
|
+
<input v-model="prize.description" type="text" class="cpub-form-input" placeholder="Optional description" />
|
|
216
|
+
</div>
|
|
88
217
|
</div>
|
|
218
|
+
<button type="button" class="cpub-btn cpub-btn-sm" @click="addPrize"><i class="fa-solid fa-plus"></i> Add Prize</button>
|
|
89
219
|
</section>
|
|
90
220
|
|
|
91
|
-
<section class="form-section">
|
|
92
|
-
<h2 class="form-section-title">
|
|
93
|
-
<div class="
|
|
94
|
-
<div class="
|
|
95
|
-
<
|
|
96
|
-
<
|
|
221
|
+
<section class="cpub-form-section">
|
|
222
|
+
<h2 class="cpub-form-section-title">Judges</h2>
|
|
223
|
+
<div v-if="resolvedJudges.length" class="cpub-judge-list">
|
|
224
|
+
<div v-for="judge in resolvedJudges" :key="judge.id" class="cpub-judge-chip">
|
|
225
|
+
<span>{{ judge.displayName || judge.username }}</span>
|
|
226
|
+
<button type="button" class="cpub-judge-chip-remove" @click="removeJudge(judge.id)"><i class="fa-solid fa-times"></i></button>
|
|
97
227
|
</div>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
228
|
+
</div>
|
|
229
|
+
<div v-else class="cpub-judge-empty">No judges assigned.</div>
|
|
230
|
+
<div class="cpub-judge-search">
|
|
231
|
+
<input
|
|
232
|
+
v-model="judgeSearch"
|
|
233
|
+
type="text"
|
|
234
|
+
class="cpub-form-input"
|
|
235
|
+
placeholder="Search users by name or username..."
|
|
236
|
+
@input="searchJudge"
|
|
237
|
+
/>
|
|
238
|
+
<div v-if="judgeSearchResults.length" class="cpub-judge-dropdown">
|
|
239
|
+
<button
|
|
240
|
+
v-for="u in judgeSearchResults"
|
|
241
|
+
:key="u.id"
|
|
242
|
+
type="button"
|
|
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>
|
|
101
249
|
</div>
|
|
102
250
|
</div>
|
|
103
251
|
</section>
|
|
104
252
|
|
|
105
|
-
<section class="form-section">
|
|
106
|
-
<h2 class="form-section-title">Status Transitions</h2>
|
|
107
|
-
<div class="status-actions">
|
|
108
|
-
<button v-if="contest.status === 'upcoming'" type="button" class="cpub-btn
|
|
253
|
+
<section class="cpub-form-section">
|
|
254
|
+
<h2 class="cpub-form-section-title">Status Transitions</h2>
|
|
255
|
+
<div class="cpub-status-actions">
|
|
256
|
+
<button v-if="contest.status === 'upcoming'" type="button" class="cpub-btn cpub-transition-btn cpub-transition-activate" @click="transitionStatus('active')">
|
|
109
257
|
<i class="fa-solid fa-play"></i> Start Contest
|
|
110
258
|
</button>
|
|
111
|
-
<button v-if="contest.status === 'active'" type="button" class="cpub-btn
|
|
259
|
+
<button v-if="contest.status === 'active'" type="button" class="cpub-btn cpub-transition-btn cpub-transition-judging" @click="transitionStatus('judging')">
|
|
112
260
|
<i class="fa-solid fa-gavel"></i> Begin Judging
|
|
113
261
|
</button>
|
|
114
|
-
<button v-if="contest.status === 'judging'" type="button" class="cpub-btn
|
|
262
|
+
<button v-if="contest.status === 'judging'" type="button" class="cpub-btn cpub-transition-btn cpub-transition-complete" @click="transitionStatus('completed')">
|
|
115
263
|
<i class="fa-solid fa-flag-checkered"></i> Complete
|
|
116
264
|
</button>
|
|
265
|
+
<button
|
|
266
|
+
v-if="contest.status !== 'completed' && contest.status !== 'cancelled'"
|
|
267
|
+
type="button"
|
|
268
|
+
class="cpub-btn cpub-transition-btn cpub-transition-cancel"
|
|
269
|
+
@click="transitionStatus('cancelled')"
|
|
270
|
+
>
|
|
271
|
+
<i class="fa-solid fa-ban"></i> Cancel Contest
|
|
272
|
+
</button>
|
|
117
273
|
</div>
|
|
118
274
|
</section>
|
|
119
275
|
|
|
@@ -122,32 +278,58 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
122
278
|
</button>
|
|
123
279
|
</form>
|
|
124
280
|
</div>
|
|
125
|
-
<div v-else class="not-found"><p>Contest not found</p></div>
|
|
281
|
+
<div v-else class="cpub-not-found"><p>Contest not found</p></div>
|
|
126
282
|
</template>
|
|
127
283
|
|
|
128
284
|
<style scoped>
|
|
129
|
-
.contest-edit { max-width: 700px; margin: 0 auto; padding: 32px; }
|
|
285
|
+
.cpub-contest-edit { max-width: 700px; margin: 0 auto; padding: 32px; }
|
|
130
286
|
.cpub-back-link { font-size: 11px; font-family: var(--font-mono); color: var(--text-faint); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; margin-bottom: 16px; }
|
|
131
287
|
.cpub-back-link:hover { color: var(--accent); }
|
|
132
|
-
.
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
.status-
|
|
136
|
-
.status-
|
|
137
|
-
.status-
|
|
138
|
-
.status-
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
.
|
|
143
|
-
.form-
|
|
144
|
-
.form-
|
|
145
|
-
.form-
|
|
146
|
-
.
|
|
147
|
-
.form-
|
|
148
|
-
.form-textarea {
|
|
149
|
-
.form-
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
288
|
+
.cpub-edit-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
|
|
289
|
+
.cpub-edit-subtitle { font-size: 13px; color: var(--text-dim); margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
|
|
290
|
+
|
|
291
|
+
.cpub-status-badge { font-size: 10px; font-family: var(--font-mono); text-transform: uppercase; padding: 2px 8px; border: var(--border-width-default) solid; }
|
|
292
|
+
.cpub-status-upcoming { color: var(--yellow); border-color: var(--yellow-border); background: var(--yellow-bg); }
|
|
293
|
+
.cpub-status-active { color: var(--green); border-color: var(--green-border); background: var(--green-bg); }
|
|
294
|
+
.cpub-status-judging { color: var(--accent); border-color: var(--accent-border); background: var(--accent-bg); }
|
|
295
|
+
.cpub-status-completed { color: var(--text-faint); border-color: var(--border2); background: var(--surface2); }
|
|
296
|
+
.cpub-status-cancelled { color: var(--red); border-color: var(--red-border); background: var(--red-bg); }
|
|
297
|
+
|
|
298
|
+
.cpub-edit-form { display: flex; flex-direction: column; gap: 16px; }
|
|
299
|
+
.cpub-form-section { border: var(--border-width-default) solid var(--border); background: var(--surface); padding: 20px; box-shadow: var(--shadow-md); }
|
|
300
|
+
.cpub-form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 14px; }
|
|
301
|
+
.cpub-form-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
|
|
302
|
+
.cpub-form-field:last-child { margin-bottom: 0; }
|
|
303
|
+
.cpub-form-label { font-size: 10px; font-weight: 600; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
|
|
304
|
+
.cpub-form-input, .cpub-form-textarea { padding: 8px 10px; border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: 13px; font-family: inherit; }
|
|
305
|
+
.cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; }
|
|
306
|
+
.cpub-form-textarea { resize: vertical; }
|
|
307
|
+
.cpub-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
308
|
+
|
|
309
|
+
.cpub-prize-row { border: var(--border-width-default) solid var(--border); padding: 14px; margin-bottom: 10px; background: var(--surface2); }
|
|
310
|
+
.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; }
|
|
312
|
+
.cpub-prize-remove { background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: 12px; }
|
|
313
|
+
.cpub-prize-remove:hover { color: var(--red); }
|
|
314
|
+
|
|
315
|
+
.cpub-status-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
316
|
+
.cpub-transition-btn { display: inline-flex; align-items: center; gap: 6px; }
|
|
317
|
+
.cpub-transition-activate { color: var(--green); border-color: var(--green-border); }
|
|
318
|
+
.cpub-transition-judging { color: var(--yellow); border-color: var(--yellow-border); }
|
|
319
|
+
.cpub-transition-complete { color: var(--accent); border-color: var(--accent-border); }
|
|
320
|
+
.cpub-transition-cancel { color: var(--red); border-color: var(--red-border); }
|
|
321
|
+
|
|
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
|
+
.cpub-not-found { text-align: center; padding: 64px; color: var(--text-dim); display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
|
153
335
|
</style>
|