@commonpub/layer 0.8.2 → 0.8.4
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/ArticleEditor.vue +19 -1
- 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/layouts/default.vue +7 -7
- package/middleware/feature-gate.global.ts +24 -0
- package/package.json +8 -8
- package/pages/[type]/index.vue +4 -3
- package/pages/admin/audit.vue +3 -2
- package/pages/admin/federation.vue +9 -1
- 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/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/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/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
|
@@ -3,34 +3,65 @@ definePageMeta({ middleware: 'auth' });
|
|
|
3
3
|
|
|
4
4
|
const route = useRoute();
|
|
5
5
|
const slug = route.params.slug as string;
|
|
6
|
+
const { user } = useAuth();
|
|
6
7
|
|
|
7
8
|
import type { Serialized, ContestDetail, ContestEntryItem } from '@commonpub/server';
|
|
8
9
|
|
|
9
10
|
const { data: contest } = useLazyFetch<Serialized<ContestDetail>>(`/api/contests/${slug}`);
|
|
10
|
-
const { data: entriesData, refresh: refreshEntries } = useLazyFetch<{ items: Serialized<ContestEntryItem>[]; total: number }>(
|
|
11
|
+
const { data: entriesData, refresh: refreshEntries } = useLazyFetch<{ items: (Serialized<ContestEntryItem> & { judgeScores?: Array<{ judgeId: string; score: number; feedback?: string }> })[]; total: number }>(
|
|
12
|
+
`/api/contests/${slug}/entries`,
|
|
13
|
+
{ query: { includeJudgeScores: true } },
|
|
14
|
+
);
|
|
11
15
|
|
|
12
16
|
useSeoMeta({ title: () => `Judge: ${contest.value?.title || 'Contest'} — ${useSiteName()}` });
|
|
13
17
|
|
|
18
|
+
const isJudge = computed(() => {
|
|
19
|
+
if (!contest.value || !user.value) return false;
|
|
20
|
+
return ((contest.value.judges ?? []) as string[]).includes(user.value.id);
|
|
21
|
+
});
|
|
22
|
+
|
|
14
23
|
const entryList = computed(() => {
|
|
15
24
|
const items = entriesData.value?.items ?? [];
|
|
16
|
-
return items.map((entry) =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
return items.map((entry) => {
|
|
26
|
+
const myScore = entry.judgeScores?.find((s) => s.judgeId === user.value?.id);
|
|
27
|
+
return {
|
|
28
|
+
id: entry.id,
|
|
29
|
+
contentId: entry.contentId,
|
|
30
|
+
contentSlug: entry.contentSlug,
|
|
31
|
+
contentType: entry.contentType,
|
|
32
|
+
contentTitle: entry.contentTitle,
|
|
33
|
+
authorName: entry.authorName,
|
|
34
|
+
authorUsername: entry.authorUsername,
|
|
35
|
+
score: entry.score ?? null,
|
|
36
|
+
rank: entry.rank ?? null,
|
|
37
|
+
myScore: myScore?.score ?? null,
|
|
38
|
+
myFeedback: myScore?.feedback ?? '',
|
|
39
|
+
};
|
|
40
|
+
});
|
|
27
41
|
});
|
|
28
42
|
|
|
43
|
+
const scoredCount = computed(() => entryList.value.filter((e) => e.myScore !== null).length);
|
|
44
|
+
const totalCount = computed(() => entryList.value.length);
|
|
45
|
+
const progressPct = computed(() => totalCount.value > 0 ? Math.round((scoredCount.value / totalCount.value) * 100) : 0);
|
|
46
|
+
|
|
29
47
|
const scoring = ref<Record<string, number>>({});
|
|
48
|
+
const feedback = ref<Record<string, string>>({});
|
|
30
49
|
const submitting = ref<string | null>(null);
|
|
31
50
|
const error = ref('');
|
|
32
51
|
const success = ref('');
|
|
33
52
|
|
|
53
|
+
// Pre-fill from existing scores
|
|
54
|
+
watch(entryList, (list) => {
|
|
55
|
+
for (const entry of list) {
|
|
56
|
+
if (entry.myScore !== null && scoring.value[entry.id] === undefined) {
|
|
57
|
+
scoring.value[entry.id] = entry.myScore;
|
|
58
|
+
}
|
|
59
|
+
if (entry.myFeedback && feedback.value[entry.id] === undefined) {
|
|
60
|
+
feedback.value[entry.id] = entry.myFeedback;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}, { immediate: true });
|
|
64
|
+
|
|
34
65
|
async function submitScore(entryId: string): Promise<void> {
|
|
35
66
|
const score = scoring.value[entryId];
|
|
36
67
|
if (score === undefined || score < 1 || score > 100) {
|
|
@@ -45,9 +76,13 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
45
76
|
try {
|
|
46
77
|
await $fetch(`/api/contests/${slug}/judge`, {
|
|
47
78
|
method: 'POST',
|
|
48
|
-
body: {
|
|
79
|
+
body: {
|
|
80
|
+
entryId,
|
|
81
|
+
score,
|
|
82
|
+
feedback: feedback.value[entryId] || undefined,
|
|
83
|
+
},
|
|
49
84
|
});
|
|
50
|
-
success.value =
|
|
85
|
+
success.value = 'Score submitted for entry.';
|
|
51
86
|
await refreshEntries();
|
|
52
87
|
} catch (err: unknown) {
|
|
53
88
|
error.value = (err as { data?: { message?: string } })?.data?.message || 'Failed to submit score.';
|
|
@@ -67,51 +102,84 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
67
102
|
<i class="fa-solid fa-gavel cpub-judge-icon"></i>
|
|
68
103
|
Judge: {{ contest?.title || 'Contest' }}
|
|
69
104
|
</h1>
|
|
70
|
-
<p class="cpub-judge-desc">Score each entry from 1 to 100. Scores are saved immediately.</p>
|
|
105
|
+
<p class="cpub-judge-desc">Score each entry from 1 to 100. Add optional feedback. Scores are saved immediately.</p>
|
|
71
106
|
</header>
|
|
72
107
|
|
|
73
|
-
|
|
74
|
-
<div v-if="
|
|
108
|
+
<!-- Loading -->
|
|
109
|
+
<div v-if="!contest" class="cpub-judge-empty">
|
|
110
|
+
<p>Loading...</p>
|
|
111
|
+
</div>
|
|
75
112
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<
|
|
113
|
+
<!-- Auth guard -->
|
|
114
|
+
<div v-else-if="!isJudge" class="cpub-judge-unauthorized">
|
|
115
|
+
<i class="fa-solid fa-lock"></i>
|
|
116
|
+
<p>You are not a judge for this contest.</p>
|
|
117
|
+
<NuxtLink :to="`/contests/${slug}`" class="cpub-btn cpub-btn-sm">Back to Contest</NuxtLink>
|
|
79
118
|
</div>
|
|
80
119
|
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
<template v-else>
|
|
121
|
+
<!-- Progress bar -->
|
|
122
|
+
<div v-if="totalCount > 0" class="cpub-judge-progress">
|
|
123
|
+
<div class="cpub-judge-progress-label">
|
|
124
|
+
Scored <strong>{{ scoredCount }}</strong> / <strong>{{ totalCount }}</strong> entries
|
|
125
|
+
</div>
|
|
126
|
+
<div class="cpub-judge-progress-bar">
|
|
127
|
+
<div class="cpub-judge-progress-fill" :style="{ width: `${progressPct}%` }"></div>
|
|
89
128
|
</div>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div v-if="error" class="cpub-judge-alert cpub-judge-alert--error" role="alert">{{ error }}</div>
|
|
132
|
+
<div v-if="success" class="cpub-judge-alert cpub-judge-alert--success">{{ success }}</div>
|
|
133
|
+
|
|
134
|
+
<div v-if="entryList.length === 0" class="cpub-judge-empty">
|
|
135
|
+
<i class="fa-solid fa-inbox"></i>
|
|
136
|
+
<p>No entries to judge yet.</p>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div v-else class="cpub-judge-entries">
|
|
140
|
+
<div v-for="entry in entryList" :key="entry.id" class="cpub-judge-entry">
|
|
141
|
+
<div class="cpub-judge-entry-info">
|
|
142
|
+
<div class="cpub-judge-entry-title">{{ entry.contentTitle }}</div>
|
|
143
|
+
<div class="cpub-judge-entry-author">by {{ entry.authorName }}</div>
|
|
144
|
+
<NuxtLink :to="`/u/${entry.authorUsername}/${entry.contentType}/${entry.contentSlug}`" class="cpub-judge-entry-link" target="_blank">
|
|
145
|
+
<i class="fa-solid fa-arrow-up-right-from-square"></i> View entry
|
|
146
|
+
</NuxtLink>
|
|
94
147
|
</div>
|
|
95
|
-
<div class="cpub-judge-
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
148
|
+
<div class="cpub-judge-entry-scoring">
|
|
149
|
+
<div v-if="entry.myScore !== null" class="cpub-judge-current-score">
|
|
150
|
+
<span class="cpub-judge-score-label">Your Score</span>
|
|
151
|
+
<span class="cpub-judge-score-value">{{ entry.myScore }}</span>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="cpub-judge-score-controls">
|
|
154
|
+
<div class="cpub-judge-score-input-wrap">
|
|
155
|
+
<input
|
|
156
|
+
v-model.number="scoring[entry.id]"
|
|
157
|
+
type="number"
|
|
158
|
+
class="cpub-judge-score-input"
|
|
159
|
+
min="1"
|
|
160
|
+
max="100"
|
|
161
|
+
placeholder="1-100"
|
|
162
|
+
/>
|
|
163
|
+
<button
|
|
164
|
+
class="cpub-judge-score-btn"
|
|
165
|
+
:disabled="submitting === entry.id"
|
|
166
|
+
@click="submitScore(entry.id)"
|
|
167
|
+
>
|
|
168
|
+
{{ submitting === entry.id ? '...' : entry.myScore !== null ? 'Update' : 'Score' }}
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
<textarea
|
|
172
|
+
v-model="feedback[entry.id]"
|
|
173
|
+
class="cpub-judge-feedback"
|
|
174
|
+
placeholder="Optional feedback (max 2000 chars)"
|
|
175
|
+
maxlength="2000"
|
|
176
|
+
rows="2"
|
|
177
|
+
></textarea>
|
|
178
|
+
</div>
|
|
111
179
|
</div>
|
|
112
180
|
</div>
|
|
113
181
|
</div>
|
|
114
|
-
</
|
|
182
|
+
</template>
|
|
115
183
|
</div>
|
|
116
184
|
</template>
|
|
117
185
|
|
|
@@ -124,16 +192,25 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
124
192
|
.cpub-judge-icon { color: var(--accent); font-size: 18px; }
|
|
125
193
|
.cpub-judge-desc { font-size: 13px; color: var(--text-dim); margin-top: 6px; }
|
|
126
194
|
|
|
195
|
+
.cpub-judge-unauthorized { text-align: center; padding: 48px 0; color: var(--text-faint); font-size: 13px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
|
196
|
+
.cpub-judge-unauthorized i { font-size: 24px; }
|
|
197
|
+
|
|
198
|
+
.cpub-judge-progress { margin-bottom: 20px; }
|
|
199
|
+
.cpub-judge-progress-label { font-size: 12px; color: var(--text-dim); font-family: var(--font-mono); margin-bottom: 6px; }
|
|
200
|
+
.cpub-judge-progress-bar { height: 6px; background: var(--surface2); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
201
|
+
.cpub-judge-progress-fill { height: 100%; background: var(--accent); transition: width 0.3s ease; }
|
|
202
|
+
|
|
127
203
|
.cpub-judge-alert { padding: 10px 14px; font-size: 12px; border: var(--border-width-default) solid; margin-bottom: 16px; }
|
|
128
204
|
.cpub-judge-alert--error { background: var(--red-bg); color: var(--red); border-color: var(--red); }
|
|
129
205
|
.cpub-judge-alert--success { background: var(--green-bg); color: var(--green); border-color: var(--green); }
|
|
130
206
|
|
|
131
207
|
.cpub-judge-empty { text-align: center; padding: 48px 0; color: var(--text-faint); font-size: 13px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
208
|
+
.cpub-judge-empty i { font-size: 24px; }
|
|
132
209
|
|
|
133
|
-
.cpub-judge-entries { display: flex; flex-direction: column; gap:
|
|
210
|
+
.cpub-judge-entries { display: flex; flex-direction: column; gap: 12px; }
|
|
134
211
|
.cpub-judge-entry {
|
|
135
|
-
display: flex; align-items:
|
|
136
|
-
padding:
|
|
212
|
+
display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
|
|
213
|
+
padding: 16px; background: var(--surface); border: var(--border-width-default) solid var(--border);
|
|
137
214
|
box-shadow: var(--shadow-md);
|
|
138
215
|
}
|
|
139
216
|
.cpub-judge-entry-info { flex: 1; min-width: 0; }
|
|
@@ -142,10 +219,11 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
142
219
|
.cpub-judge-entry-link { font-size: 10px; color: var(--accent); text-decoration: none; display: inline-flex; align-items: center; gap: 4px; margin-top: 4px; }
|
|
143
220
|
.cpub-judge-entry-link:hover { text-decoration: underline; }
|
|
144
221
|
|
|
145
|
-
.cpub-judge-entry-scoring { display: flex;
|
|
222
|
+
.cpub-judge-entry-scoring { display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; min-width: 220px; }
|
|
146
223
|
.cpub-judge-current-score { text-align: center; }
|
|
147
224
|
.cpub-judge-score-label { display: block; font-family: var(--font-mono); font-size: 9px; color: var(--text-faint); text-transform: uppercase; }
|
|
148
225
|
.cpub-judge-score-value { font-size: 20px; font-weight: 700; color: var(--accent); font-family: var(--font-mono); }
|
|
226
|
+
.cpub-judge-score-controls { display: flex; flex-direction: column; gap: 6px; }
|
|
149
227
|
.cpub-judge-score-input-wrap { display: flex; gap: 0; }
|
|
150
228
|
.cpub-judge-score-input {
|
|
151
229
|
width: 70px; padding: 6px 8px; border: var(--border-width-default) solid var(--border); background: var(--surface);
|
|
@@ -158,4 +236,14 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
158
236
|
}
|
|
159
237
|
.cpub-judge-score-btn:hover { opacity: 0.9; }
|
|
160
238
|
.cpub-judge-score-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
239
|
+
.cpub-judge-feedback {
|
|
240
|
+
width: 100%; padding: 6px 8px; border: var(--border-width-default) solid var(--border); background: var(--surface);
|
|
241
|
+
color: var(--text); font-size: 11px; font-family: inherit; resize: vertical; outline: none;
|
|
242
|
+
}
|
|
243
|
+
.cpub-judge-feedback:focus { border-color: var(--accent); }
|
|
244
|
+
|
|
245
|
+
@media (max-width: 768px) {
|
|
246
|
+
.cpub-judge-entry { flex-direction: column; }
|
|
247
|
+
.cpub-judge-entry-scoring { min-width: 100%; }
|
|
248
|
+
}
|
|
161
249
|
</style>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Serialized, ContestDetail, ContestEntryItem } from '@commonpub/server';
|
|
3
|
+
|
|
4
|
+
const route = useRoute();
|
|
5
|
+
const slug = route.params.slug as string;
|
|
6
|
+
|
|
7
|
+
const { data: contest } = useLazyFetch<Serialized<ContestDetail>>(`/api/contests/${slug}`);
|
|
8
|
+
const { data: entriesData } = useLazyFetch<{ items: Serialized<ContestEntryItem>[]; total: number }>(`/api/contests/${slug}/entries`);
|
|
9
|
+
|
|
10
|
+
useSeoMeta({
|
|
11
|
+
title: () => `Results: ${contest.value?.title || 'Contest'} — ${useSiteName()}`,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const rankedEntries = computed(() => {
|
|
15
|
+
const items = [...(entriesData.value?.items ?? [])];
|
|
16
|
+
items.sort((a, b) => (a.rank ?? 999) - (b.rank ?? 999));
|
|
17
|
+
return items;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const podium = computed(() => rankedEntries.value.filter((e) => e.rank && e.rank <= 3));
|
|
21
|
+
const leaderboard = computed(() => rankedEntries.value);
|
|
22
|
+
|
|
23
|
+
const prizes = computed(() => contest.value?.prizes ?? []);
|
|
24
|
+
|
|
25
|
+
function prizeForRank(rank: number): { title: string; value?: string } | null {
|
|
26
|
+
const prize = prizes.value.find((p: { place: number; title: string; value?: string }) => p.place === rank);
|
|
27
|
+
return prize ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function medalIcon(rank: number): string {
|
|
31
|
+
if (rank === 1) return 'fa-trophy';
|
|
32
|
+
if (rank === 2) return 'fa-medal';
|
|
33
|
+
if (rank === 3) return 'fa-award';
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function medalColor(rank: number): string {
|
|
38
|
+
if (rank === 1) return 'var(--gold)';
|
|
39
|
+
if (rank === 2) return 'var(--silver)';
|
|
40
|
+
if (rank === 3) return 'var(--bronze)';
|
|
41
|
+
return 'var(--text-dim)';
|
|
42
|
+
}
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div class="cpub-results-page">
|
|
47
|
+
<header class="cpub-results-header">
|
|
48
|
+
<NuxtLink :to="`/contests/${slug}`" class="cpub-results-back">
|
|
49
|
+
<i class="fa-solid fa-arrow-left"></i> Back to contest
|
|
50
|
+
</NuxtLink>
|
|
51
|
+
<h1 class="cpub-results-title">
|
|
52
|
+
<i class="fa-solid fa-ranking-star" style="color: var(--yellow);"></i>
|
|
53
|
+
{{ contest?.title || 'Contest' }} — Results
|
|
54
|
+
</h1>
|
|
55
|
+
</header>
|
|
56
|
+
|
|
57
|
+
<!-- Not completed -->
|
|
58
|
+
<div v-if="contest && contest.status !== 'completed'" class="cpub-results-pending">
|
|
59
|
+
<i class="fa-solid fa-hourglass-half"></i>
|
|
60
|
+
<p>Results are not available yet. The contest is still <strong>{{ contest.status }}</strong>.</p>
|
|
61
|
+
<NuxtLink :to="`/contests/${slug}`" class="cpub-btn cpub-btn-sm">Back to Contest</NuxtLink>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<template v-else-if="contest">
|
|
65
|
+
<!-- PODIUM -->
|
|
66
|
+
<div v-if="podium.length > 0" class="cpub-podium">
|
|
67
|
+
<div
|
|
68
|
+
v-for="entry in podium"
|
|
69
|
+
:key="entry.id"
|
|
70
|
+
class="cpub-podium-card"
|
|
71
|
+
:class="`cpub-podium-${entry.rank ?? 0}`"
|
|
72
|
+
>
|
|
73
|
+
<div class="cpub-podium-medal" :style="{ color: medalColor(entry.rank ?? 0) }">
|
|
74
|
+
<i class="fa-solid" :class="medalIcon(entry.rank ?? 0)"></i>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="cpub-podium-rank">#{{ entry.rank }}</div>
|
|
77
|
+
<div class="cpub-podium-thumb">
|
|
78
|
+
<img v-if="entry.contentCoverImageUrl" :src="entry.contentCoverImageUrl" :alt="entry.contentTitle" />
|
|
79
|
+
<div v-else class="cpub-podium-placeholder"><i class="fa-solid fa-microchip"></i></div>
|
|
80
|
+
</div>
|
|
81
|
+
<NuxtLink :to="`/u/${entry.authorUsername}/${entry.contentType}/${entry.contentSlug}`" class="cpub-podium-title">{{ entry.contentTitle }}</NuxtLink>
|
|
82
|
+
<NuxtLink :to="`/u/${entry.authorUsername}`" class="cpub-podium-author">{{ entry.authorName }}</NuxtLink>
|
|
83
|
+
<div class="cpub-podium-score">Score: {{ entry.score ?? '—' }}</div>
|
|
84
|
+
<template v-if="entry.rank && prizeForRank(entry.rank)">
|
|
85
|
+
<div class="cpub-podium-prize">
|
|
86
|
+
<i class="fa-solid fa-gift"></i> {{ prizeForRank(entry.rank)?.title }}
|
|
87
|
+
<span v-if="prizeForRank(entry.rank)?.value" class="cpub-podium-prize-val">{{ prizeForRank(entry.rank)?.value }}</span>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<!-- LEADERBOARD -->
|
|
94
|
+
<div v-if="leaderboard.length > 0" class="cpub-leaderboard">
|
|
95
|
+
<h2 class="cpub-leaderboard-title">Full Leaderboard</h2>
|
|
96
|
+
<table class="cpub-leaderboard-table">
|
|
97
|
+
<thead>
|
|
98
|
+
<tr>
|
|
99
|
+
<th>Rank</th>
|
|
100
|
+
<th>Entry</th>
|
|
101
|
+
<th>Author</th>
|
|
102
|
+
<th>Score</th>
|
|
103
|
+
</tr>
|
|
104
|
+
</thead>
|
|
105
|
+
<tbody>
|
|
106
|
+
<tr v-for="entry in leaderboard" :key="entry.id" :class="{ 'cpub-lb-top3': entry.rank && entry.rank <= 3 }">
|
|
107
|
+
<td class="cpub-lb-rank">
|
|
108
|
+
<span v-if="entry.rank && entry.rank <= 3" :style="{ color: medalColor(entry.rank) }">
|
|
109
|
+
<i class="fa-solid" :class="medalIcon(entry.rank)"></i>
|
|
110
|
+
</span>
|
|
111
|
+
{{ entry.rank ?? '—' }}
|
|
112
|
+
</td>
|
|
113
|
+
<td>
|
|
114
|
+
<NuxtLink :to="`/u/${entry.authorUsername}/${entry.contentType}/${entry.contentSlug}`" class="cpub-lb-entry-link">{{ entry.contentTitle }}</NuxtLink>
|
|
115
|
+
</td>
|
|
116
|
+
<td>
|
|
117
|
+
<NuxtLink :to="`/u/${entry.authorUsername}`" class="cpub-lb-author-link">{{ entry.authorName }}</NuxtLink>
|
|
118
|
+
</td>
|
|
119
|
+
<td class="cpub-lb-score">{{ entry.score ?? '—' }}</td>
|
|
120
|
+
</tr>
|
|
121
|
+
</tbody>
|
|
122
|
+
</table>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div v-else class="cpub-results-empty">
|
|
126
|
+
<i class="fa-solid fa-inbox"></i>
|
|
127
|
+
<p>No entries were submitted to this contest.</p>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
|
+
|
|
133
|
+
<style scoped>
|
|
134
|
+
.cpub-results-page { max-width: 900px; margin: 0 auto; padding: 32px 24px; }
|
|
135
|
+
.cpub-results-header { margin-bottom: 32px; }
|
|
136
|
+
.cpub-results-back { font-size: 12px; color: var(--text-faint); text-decoration: none; display: inline-flex; align-items: center; gap: 6px; margin-bottom: 12px; }
|
|
137
|
+
.cpub-results-back:hover { color: var(--accent); }
|
|
138
|
+
.cpub-results-title { font-size: 22px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
|
|
139
|
+
|
|
140
|
+
.cpub-results-pending { text-align: center; padding: 48px 0; color: var(--text-faint); font-size: 13px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
|
141
|
+
.cpub-results-pending i { font-size: 24px; }
|
|
142
|
+
|
|
143
|
+
/* PODIUM */
|
|
144
|
+
.cpub-podium { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
|
145
|
+
.cpub-podium-card { background: var(--surface); border: var(--border-width-default) solid var(--border); border-radius: var(--radius); padding: 20px; text-align: center; box-shadow: var(--shadow-md); }
|
|
146
|
+
.cpub-podium-1 { box-shadow: var(--shadow-accent); border-color: var(--yellow-border); }
|
|
147
|
+
.cpub-podium-medal { font-size: 28px; margin-bottom: 6px; }
|
|
148
|
+
.cpub-podium-rank { font-size: 11px; font-family: var(--font-mono); font-weight: 700; letter-spacing: .08em; margin-bottom: 12px; color: var(--text-dim); }
|
|
149
|
+
.cpub-podium-thumb { width: 100%; height: 100px; overflow: hidden; margin-bottom: 12px; border-radius: var(--radius); background: var(--surface2); display: flex; align-items: center; justify-content: center; }
|
|
150
|
+
.cpub-podium-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
|
151
|
+
.cpub-podium-placeholder { font-size: 24px; color: var(--text-faint); opacity: .5; }
|
|
152
|
+
.cpub-podium-title { font-size: 13px; font-weight: 600; display: block; margin-bottom: 4px; color: var(--text); text-decoration: none; }
|
|
153
|
+
.cpub-podium-title:hover { color: var(--accent); }
|
|
154
|
+
.cpub-podium-author { font-size: 11px; color: var(--text-dim); text-decoration: none; display: block; margin-bottom: 6px; }
|
|
155
|
+
.cpub-podium-author:hover { color: var(--accent); }
|
|
156
|
+
.cpub-podium-score { font-size: 11px; font-family: var(--font-mono); color: var(--text-faint); margin-bottom: 6px; }
|
|
157
|
+
.cpub-podium-prize { font-size: 11px; font-family: var(--font-mono); color: var(--accent); display: flex; align-items: center; justify-content: center; gap: 4px; }
|
|
158
|
+
.cpub-podium-prize-val { font-weight: 700; color: var(--yellow); }
|
|
159
|
+
|
|
160
|
+
/* LEADERBOARD */
|
|
161
|
+
.cpub-leaderboard { margin-bottom: 32px; }
|
|
162
|
+
.cpub-leaderboard-title { font-size: 16px; font-weight: 700; margin-bottom: 14px; }
|
|
163
|
+
.cpub-leaderboard-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
164
|
+
.cpub-leaderboard-table th { text-align: left; font-size: 10px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); padding: 8px 12px; border-bottom: var(--border-width-default) solid var(--border); }
|
|
165
|
+
.cpub-leaderboard-table td { padding: 10px 12px; border-bottom: var(--border-width-default) solid var(--border); }
|
|
166
|
+
.cpub-lb-top3 { background: var(--surface2); }
|
|
167
|
+
.cpub-lb-rank { font-family: var(--font-mono); font-weight: 700; display: flex; align-items: center; gap: 6px; }
|
|
168
|
+
.cpub-lb-score { font-family: var(--font-mono); font-weight: 600; color: var(--accent); }
|
|
169
|
+
.cpub-lb-entry-link { color: var(--text); text-decoration: none; font-weight: 500; }
|
|
170
|
+
.cpub-lb-entry-link:hover { color: var(--accent); }
|
|
171
|
+
.cpub-lb-author-link { color: var(--text-dim); text-decoration: none; }
|
|
172
|
+
.cpub-lb-author-link:hover { color: var(--accent); }
|
|
173
|
+
|
|
174
|
+
.cpub-results-empty { text-align: center; padding: 48px 0; color: var(--text-faint); font-size: 13px; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
175
|
+
.cpub-results-empty i { font-size: 24px; }
|
|
176
|
+
|
|
177
|
+
@media (max-width: 768px) {
|
|
178
|
+
.cpub-podium { grid-template-columns: 1fr; }
|
|
179
|
+
.cpub-leaderboard-table { font-size: 11px; }
|
|
180
|
+
.cpub-leaderboard-table th, .cpub-leaderboard-table td { padding: 8px; }
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
@@ -69,82 +69,82 @@ async function handleCreate(): Promise<void> {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
const placeLabels = ['1st', '2nd', '3rd', '4th', '5th', '6th'];
|
|
72
|
-
const placeColors = ['var(--
|
|
72
|
+
const placeColors = ['var(--gold)', 'var(--silver)', 'var(--bronze)', 'var(--accent)', 'var(--accent)', 'var(--accent)'];
|
|
73
73
|
</script>
|
|
74
74
|
|
|
75
75
|
<template>
|
|
76
|
-
<div class="contest-create">
|
|
76
|
+
<div class="cpub-contest-create">
|
|
77
77
|
<NuxtLink to="/contests" class="cpub-back-link"><i class="fa-solid fa-arrow-left"></i> Contests</NuxtLink>
|
|
78
|
-
<h1 class="page-title">Create Contest</h1>
|
|
78
|
+
<h1 class="cpub-page-title">Create Contest</h1>
|
|
79
79
|
|
|
80
|
-
<form class="contest-form" @submit.prevent="handleCreate" aria-label="Create contest">
|
|
80
|
+
<form class="cpub-contest-form" @submit.prevent="handleCreate" aria-label="Create contest">
|
|
81
81
|
<!-- Basic Info -->
|
|
82
|
-
<section class="form-section">
|
|
83
|
-
<h2 class="form-section-title">Contest Details</h2>
|
|
84
|
-
<div class="form-field">
|
|
85
|
-
<label for="contest-title" class="form-label">Title</label>
|
|
86
|
-
<input id="contest-title" v-model="title" type="text" class="form-input" required placeholder="Maker Challenge 2026" />
|
|
82
|
+
<section class="cpub-form-section">
|
|
83
|
+
<h2 class="cpub-form-section-title">Contest Details</h2>
|
|
84
|
+
<div class="cpub-form-field">
|
|
85
|
+
<label for="contest-title" class="cpub-form-label">Title</label>
|
|
86
|
+
<input id="contest-title" v-model="title" type="text" class="cpub-form-input" required placeholder="Maker Challenge 2026" />
|
|
87
87
|
</div>
|
|
88
|
-
<div class="form-field">
|
|
89
|
-
<label for="contest-desc" class="form-label">Description</label>
|
|
90
|
-
<textarea id="contest-desc" v-model="description" class="form-textarea" rows="3" placeholder="Describe your contest..." />
|
|
88
|
+
<div class="cpub-form-field">
|
|
89
|
+
<label for="contest-desc" class="cpub-form-label">Description</label>
|
|
90
|
+
<textarea id="contest-desc" v-model="description" class="cpub-form-textarea" rows="3" placeholder="Describe your contest..." />
|
|
91
91
|
</div>
|
|
92
|
-
<div class="form-field">
|
|
93
|
-
<label for="contest-rules" class="form-label">Rules</label>
|
|
94
|
-
<textarea id="contest-rules" v-model="rules" class="form-textarea" rows="4" placeholder="Contest rules and requirements..." />
|
|
92
|
+
<div class="cpub-form-field">
|
|
93
|
+
<label for="contest-rules" class="cpub-form-label">Rules</label>
|
|
94
|
+
<textarea id="contest-rules" v-model="rules" class="cpub-form-textarea" rows="4" placeholder="Contest rules and requirements..." />
|
|
95
95
|
</div>
|
|
96
96
|
</section>
|
|
97
97
|
|
|
98
98
|
<!-- Dates -->
|
|
99
|
-
<section class="form-section">
|
|
100
|
-
<h2 class="form-section-title">Schedule</h2>
|
|
101
|
-
<div class="form-row">
|
|
102
|
-
<div class="form-field">
|
|
103
|
-
<label for="start-date" class="form-label">Start Date</label>
|
|
104
|
-
<input id="start-date" v-model="startDate" type="datetime-local" class="form-input" required />
|
|
99
|
+
<section class="cpub-form-section">
|
|
100
|
+
<h2 class="cpub-form-section-title">Schedule</h2>
|
|
101
|
+
<div class="cpub-form-row">
|
|
102
|
+
<div class="cpub-form-field">
|
|
103
|
+
<label for="start-date" class="cpub-form-label">Start Date</label>
|
|
104
|
+
<input id="start-date" v-model="startDate" type="datetime-local" class="cpub-form-input" required />
|
|
105
105
|
</div>
|
|
106
|
-
<div class="form-field">
|
|
107
|
-
<label for="end-date" class="form-label">End Date</label>
|
|
108
|
-
<input id="end-date" v-model="endDate" type="datetime-local" class="form-input" required />
|
|
106
|
+
<div class="cpub-form-field">
|
|
107
|
+
<label for="end-date" class="cpub-form-label">End Date</label>
|
|
108
|
+
<input id="end-date" v-model="endDate" type="datetime-local" class="cpub-form-input" required />
|
|
109
109
|
</div>
|
|
110
|
-
<div class="form-field">
|
|
111
|
-
<label for="judging-date" class="form-label">Judging Ends</label>
|
|
112
|
-
<input id="judging-date" v-model="judgingEndDate" type="datetime-local" class="form-input" />
|
|
110
|
+
<div class="cpub-form-field">
|
|
111
|
+
<label for="judging-date" class="cpub-form-label">Judging Ends</label>
|
|
112
|
+
<input id="judging-date" v-model="judgingEndDate" type="datetime-local" class="cpub-form-input" />
|
|
113
113
|
</div>
|
|
114
114
|
</div>
|
|
115
115
|
</section>
|
|
116
116
|
|
|
117
117
|
<!-- Prizes -->
|
|
118
|
-
<section class="form-section">
|
|
119
|
-
<div class="form-section-header">
|
|
120
|
-
<h2 class="form-section-title">Prizes</h2>
|
|
118
|
+
<section class="cpub-form-section">
|
|
119
|
+
<div class="cpub-form-section-header">
|
|
120
|
+
<h2 class="cpub-form-section-title">Prizes</h2>
|
|
121
121
|
<button type="button" class="cpub-btn cpub-btn-sm" @click="addPrize">
|
|
122
122
|
<i class="fa-solid fa-plus"></i> Add Prize
|
|
123
123
|
</button>
|
|
124
124
|
</div>
|
|
125
125
|
|
|
126
|
-
<div v-for="(prize, idx) in prizes" :key="idx" class="prize-card">
|
|
127
|
-
<div class="prize-header">
|
|
128
|
-
<span class="prize-place" :style="{ color: placeColors[idx] || 'var(--accent)' }">
|
|
126
|
+
<div v-for="(prize, idx) in prizes" :key="idx" class="cpub-prize-card">
|
|
127
|
+
<div class="cpub-prize-header">
|
|
128
|
+
<span class="cpub-prize-place" :style="{ color: placeColors[idx] || 'var(--accent)' }">
|
|
129
129
|
<i class="fa-solid fa-trophy"></i> {{ placeLabels[idx] || `${idx + 1}th` }} Place
|
|
130
130
|
</span>
|
|
131
131
|
<button v-if="prizes.length > 1" type="button" class="cpub-delete-btn" @click="removePrize(idx)">
|
|
132
132
|
<i class="fa-solid fa-xmark"></i>
|
|
133
133
|
</button>
|
|
134
134
|
</div>
|
|
135
|
-
<div class="form-row">
|
|
136
|
-
<div class="form-field" style="flex: 2">
|
|
137
|
-
<label class="form-label">Title</label>
|
|
138
|
-
<input v-model="prize.title" type="text" class="form-input" placeholder="Prize title" />
|
|
135
|
+
<div class="cpub-form-row">
|
|
136
|
+
<div class="cpub-form-field" style="flex: 2">
|
|
137
|
+
<label class="cpub-form-label">Title</label>
|
|
138
|
+
<input v-model="prize.title" type="text" class="cpub-form-input" placeholder="Prize title" />
|
|
139
139
|
</div>
|
|
140
|
-
<div class="form-field" style="flex: 1">
|
|
141
|
-
<label class="form-label">Value</label>
|
|
142
|
-
<input v-model="prize.value" type="text" class="form-input" placeholder="$500" />
|
|
140
|
+
<div class="cpub-form-field" style="flex: 1">
|
|
141
|
+
<label class="cpub-form-label">Value</label>
|
|
142
|
+
<input v-model="prize.value" type="text" class="cpub-form-input" placeholder="$500" />
|
|
143
143
|
</div>
|
|
144
144
|
</div>
|
|
145
|
-
<div class="form-field">
|
|
146
|
-
<label class="form-label">Description</label>
|
|
147
|
-
<input v-model="prize.description" type="text" class="form-input" placeholder="What the winner receives..." />
|
|
145
|
+
<div class="cpub-form-field">
|
|
146
|
+
<label class="cpub-form-label">Description</label>
|
|
147
|
+
<input v-model="prize.description" type="text" class="cpub-form-input" placeholder="What the winner receives..." />
|
|
148
148
|
</div>
|
|
149
149
|
</div>
|
|
150
150
|
</section>
|
|
@@ -157,36 +157,36 @@ const placeColors = ['var(--yellow)', 'var(--text-faint)', '#a0724a', 'var(--acc
|
|
|
157
157
|
</template>
|
|
158
158
|
|
|
159
159
|
<style scoped>
|
|
160
|
-
.contest-create { max-width: 720px; margin: 0 auto; padding: 32px; }
|
|
160
|
+
.cpub-contest-create { max-width: 720px; margin: 0 auto; padding: 32px; }
|
|
161
161
|
|
|
162
162
|
.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; }
|
|
163
163
|
.cpub-back-link:hover { color: var(--accent); }
|
|
164
164
|
|
|
165
|
-
.page-title { font-size: 24px; font-weight: 700; margin-bottom: 24px; letter-spacing: -0.02em; }
|
|
165
|
+
.cpub-page-title { font-size: 24px; font-weight: 700; margin-bottom: 24px; letter-spacing: -0.02em; }
|
|
166
166
|
|
|
167
|
-
.contest-form { display: flex; flex-direction: column; gap: 20px; }
|
|
167
|
+
.cpub-contest-form { display: flex; flex-direction: column; gap: 20px; }
|
|
168
168
|
|
|
169
|
-
.form-section { border: var(--border-width-default) solid var(--border); background: var(--surface); padding: 20px; box-shadow: var(--shadow-md); }
|
|
170
|
-
.form-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
|
171
|
-
.form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 16px; }
|
|
172
|
-
.form-section-header .form-section-title { margin-bottom: 0; }
|
|
169
|
+
.cpub-form-section { border: var(--border-width-default) solid var(--border); background: var(--surface); padding: 20px; box-shadow: var(--shadow-md); }
|
|
170
|
+
.cpub-form-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
|
171
|
+
.cpub-form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 16px; }
|
|
172
|
+
.cpub-form-section-header .cpub-form-section-title { margin-bottom: 0; }
|
|
173
173
|
|
|
174
|
-
.form-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
|
|
175
|
-
.form-field:last-child { margin-bottom: 0; }
|
|
176
|
-
.form-label { font-size: 10px; font-weight: 600; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
|
|
177
|
-
.form-input, .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; }
|
|
178
|
-
.form-input:focus, .form-textarea:focus { border-color: var(--accent); outline: none; }
|
|
179
|
-
.form-textarea { resize: vertical; }
|
|
180
|
-
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
|
|
174
|
+
.cpub-form-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
|
|
175
|
+
.cpub-form-field:last-child { margin-bottom: 0; }
|
|
176
|
+
.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); }
|
|
177
|
+
.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; }
|
|
178
|
+
.cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; }
|
|
179
|
+
.cpub-form-textarea { resize: vertical; }
|
|
180
|
+
.cpub-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
|
|
181
181
|
|
|
182
|
-
.prize-card { border: var(--border-width-default) solid var(--border); padding: 14px; margin-bottom: 10px; background: var(--surface2); }
|
|
183
|
-
.prize-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
|
184
|
-
.prize-place { font-size: 12px; font-weight: 700; font-family: var(--font-mono); display: flex; align-items: center; gap: 6px; }
|
|
182
|
+
.cpub-prize-card { border: var(--border-width-default) solid var(--border); padding: 14px; margin-bottom: 10px; background: var(--surface2); }
|
|
183
|
+
.cpub-prize-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
|
184
|
+
.cpub-prize-place { font-size: 12px; font-weight: 700; font-family: var(--font-mono); display: flex; align-items: center; gap: 6px; }
|
|
185
185
|
|
|
186
186
|
@media (max-width: 768px) {
|
|
187
|
-
.contest-create { padding: 16px; }
|
|
188
|
-
.page-title { font-size: 20px; }
|
|
189
|
-
.form-section { padding: 14px; }
|
|
190
|
-
.form-row { grid-template-columns: 1fr; }
|
|
187
|
+
.cpub-contest-create { padding: 16px; }
|
|
188
|
+
.cpub-page-title { font-size: 20px; }
|
|
189
|
+
.cpub-form-section { padding: 14px; }
|
|
190
|
+
.cpub-form-row { grid-template-columns: 1fr; }
|
|
191
191
|
}
|
|
192
192
|
</style>
|