@commonpub/layer 0.48.0 → 0.49.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.
|
@@ -61,91 +61,102 @@ const isEnded = computed(() => c.value?.status === 'completed' || c.value?.statu
|
|
|
61
61
|
const tagline = computed<string>(() => {
|
|
62
62
|
const sub = (c.value?.subheading ?? '').trim();
|
|
63
63
|
if (sub) return sub;
|
|
64
|
-
return markdownToExcerpt(c.value?.description) || '
|
|
64
|
+
return markdownToExcerpt(c.value?.description) || '';
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const dateRange = computed<string>(() => {
|
|
68
|
+
const fmt = (d: string, withYear = false) =>
|
|
69
|
+
new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', ...(withYear ? { year: 'numeric' } : {}) });
|
|
70
|
+
const start = c.value?.startDate ? fmt(c.value.startDate) : '';
|
|
71
|
+
const end = c.value?.endDate ? fmt(c.value.endDate, true) : '';
|
|
72
|
+
if (start && end) return `${start} — ${end}`;
|
|
73
|
+
return start || end;
|
|
65
74
|
});
|
|
66
75
|
</script>
|
|
67
76
|
|
|
68
77
|
<template>
|
|
69
78
|
<div class="cpub-hero">
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
<!-- Banner band — full-width image at the top, the same way other content
|
|
80
|
+
pages render their hero banner (clean band, never overlaid by text). -->
|
|
81
|
+
<div v-if="c?.bannerUrl" class="cpub-hero-banner">
|
|
82
|
+
<img :src="c.bannerUrl" :alt="`${c?.title || 'Contest'} banner`" />
|
|
73
83
|
</div>
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<i class="fa-solid fa-ban"></i> This contest has been cancelled.
|
|
85
|
+
<!-- Hero body — the contest's dark, patterned section. Two columns:
|
|
86
|
+
title + details on the left, the countdown on the right. -->
|
|
87
|
+
<div class="cpub-hero-body">
|
|
88
|
+
<div class="cpub-hero-pattern" aria-hidden="true">
|
|
89
|
+
<div class="cpub-hero-dots"></div>
|
|
90
|
+
<div class="cpub-hero-lines"></div>
|
|
82
91
|
</div>
|
|
83
92
|
|
|
84
|
-
<div class="cpub-hero-
|
|
85
|
-
<
|
|
86
|
-
|
|
93
|
+
<div class="cpub-hero-inner">
|
|
94
|
+
<div v-if="c?.status === 'cancelled'" class="cpub-cancelled-banner">
|
|
95
|
+
<i class="fa-solid fa-ban"></i> This contest has been cancelled.
|
|
96
|
+
</div>
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
<div class="cpub-hero-grid">
|
|
99
|
+
<!-- LEFT: title + details + actions -->
|
|
100
|
+
<div class="cpub-hero-main">
|
|
101
|
+
<div class="cpub-hero-eyebrow">
|
|
102
|
+
<span class="cpub-contest-badge"><i class="fa fa-trophy"></i> Contest</span>
|
|
103
|
+
<span class="cpub-status-pill" :data-status="c?.status || 'upcoming'">{{ c?.status || 'upcoming' }}</span>
|
|
104
|
+
</div>
|
|
90
105
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<i class="fa fa-calendar"></i>
|
|
94
|
-
{{ c?.startDate ? new Date(c.startDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '' }}{{ c?.startDate && c?.endDate ? ' — ' : '' }}{{ c?.endDate ? new Date(c.endDate).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '' }}
|
|
95
|
-
</span>
|
|
96
|
-
<span v-if="c?.startDate || c?.endDate" class="cpub-hero-meta-sep">|</span>
|
|
97
|
-
<span class="cpub-hero-meta-item"><i class="fa fa-folder-open"></i> {{ c?.entryCount ?? 0 }} entries</span>
|
|
98
|
-
</div>
|
|
106
|
+
<h1 class="cpub-hero-title">{{ c?.title || 'Contest' }}</h1>
|
|
107
|
+
<p v-if="tagline" class="cpub-hero-tagline">{{ tagline }}</p>
|
|
99
108
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<div class="cpub-countdown-block">
|
|
105
|
-
<div class="cpub-countdown-val">{{ countdown.days }}</div>
|
|
106
|
-
<div class="cpub-countdown-unit">Days</div>
|
|
107
|
-
</div>
|
|
108
|
-
<div class="cpub-countdown-sep">:</div>
|
|
109
|
-
<div class="cpub-countdown-block">
|
|
110
|
-
<div class="cpub-countdown-val">{{ countdown.hours }}</div>
|
|
111
|
-
<div class="cpub-countdown-unit">Hours</div>
|
|
112
|
-
</div>
|
|
113
|
-
<div class="cpub-countdown-sep">:</div>
|
|
114
|
-
<div class="cpub-countdown-block">
|
|
115
|
-
<div class="cpub-countdown-val">{{ countdown.mins }}</div>
|
|
116
|
-
<div class="cpub-countdown-unit">Minutes</div>
|
|
117
|
-
</div>
|
|
118
|
-
<div class="cpub-countdown-sep">:</div>
|
|
119
|
-
<div class="cpub-countdown-block">
|
|
120
|
-
<div class="cpub-countdown-val">{{ countdown.secs }}</div>
|
|
121
|
-
<div class="cpub-countdown-unit">Seconds</div>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
109
|
+
<div class="cpub-hero-meta">
|
|
110
|
+
<span v-if="dateRange" class="cpub-hero-meta-item"><i class="fa fa-calendar"></i> {{ dateRange }}</span>
|
|
111
|
+
<span class="cpub-hero-meta-item"><i class="fa fa-folder-open"></i> {{ c?.entryCount ?? 0 }} {{ (c?.entryCount ?? 0) === 1 ? 'entry' : 'entries' }}</span>
|
|
112
|
+
</div>
|
|
125
113
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
114
|
+
<div class="cpub-hero-cta">
|
|
115
|
+
<button v-if="isAuthenticated && c?.status === 'active'" class="cpub-btn cpub-btn-primary cpub-btn-lg" @click="emit('submit-entry')"><i class="fa fa-upload"></i> Submit Entry</button>
|
|
116
|
+
<button class="cpub-btn cpub-btn-lg cpub-btn-dark" @click="emit('copy-link')"><i class="fa fa-link"></i> Share</button>
|
|
117
|
+
</div>
|
|
130
118
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
119
|
+
<!-- Admin controls -->
|
|
120
|
+
<div v-if="isAdmin && c" class="cpub-admin-controls">
|
|
121
|
+
<span class="cpub-admin-controls-label"><i class="fa-solid fa-shield-halved"></i> Admin</span>
|
|
122
|
+
<button v-if="c.status === 'upcoming'" class="cpub-btn cpub-btn-sm" :disabled="transitioning" @click="emit('transition', 'active')"><i class="fa-solid fa-play"></i> Activate</button>
|
|
123
|
+
<button v-if="c.status === 'active'" class="cpub-btn cpub-btn-sm" :disabled="transitioning" @click="emit('transition', 'judging')"><i class="fa-solid fa-gavel"></i> Start Judging</button>
|
|
124
|
+
<button v-if="c.status === 'judging'" class="cpub-btn cpub-btn-sm" :disabled="transitioning" @click="emit('transition', 'completed')"><i class="fa-solid fa-check"></i> Complete</button>
|
|
125
|
+
<button v-if="c.status !== 'completed' && c.status !== 'cancelled'" class="cpub-btn cpub-btn-sm cpub-btn-cancel" :disabled="transitioning" @click="emit('transition', 'cancelled')"><i class="fa-solid fa-ban"></i> Cancel</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
140
128
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
129
|
+
<!-- RIGHT: countdown -->
|
|
130
|
+
<aside class="cpub-hero-side">
|
|
131
|
+
<div v-if="!isEnded" class="cpub-countdown-section">
|
|
132
|
+
<div class="cpub-countdown-label"><i class="fa fa-clock"></i> {{ countdownLabel }}</div>
|
|
133
|
+
<div class="cpub-countdown-row">
|
|
134
|
+
<div class="cpub-countdown-block">
|
|
135
|
+
<div class="cpub-countdown-val">{{ countdown.days }}</div>
|
|
136
|
+
<div class="cpub-countdown-unit">Days</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="cpub-countdown-sep">:</div>
|
|
139
|
+
<div class="cpub-countdown-block">
|
|
140
|
+
<div class="cpub-countdown-val">{{ countdown.hours }}</div>
|
|
141
|
+
<div class="cpub-countdown-unit">Hours</div>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="cpub-countdown-sep">:</div>
|
|
144
|
+
<div class="cpub-countdown-block">
|
|
145
|
+
<div class="cpub-countdown-val">{{ countdown.mins }}</div>
|
|
146
|
+
<div class="cpub-countdown-unit">Minutes</div>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="cpub-countdown-sep">:</div>
|
|
149
|
+
<div class="cpub-countdown-block">
|
|
150
|
+
<div class="cpub-countdown-val">{{ countdown.secs }}</div>
|
|
151
|
+
<div class="cpub-countdown-unit">Seconds</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
<div v-else class="cpub-countdown-ended">
|
|
156
|
+
<i class="fa-solid fa-flag-checkered"></i>
|
|
157
|
+
<span>{{ countdownLabel }}</span>
|
|
158
|
+
</div>
|
|
159
|
+
</aside>
|
|
149
160
|
</div>
|
|
150
161
|
</div>
|
|
151
162
|
</div>
|
|
@@ -157,37 +168,59 @@ const tagline = computed<string>(() => {
|
|
|
157
168
|
--hero-bg: var(--text);
|
|
158
169
|
--hero-text: var(--color-text-inverse);
|
|
159
170
|
--hero-text-dim: var(--text-faint);
|
|
160
|
-
/* Alpha of the hero foreground so the structure lines/surfaces track
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
/* Alpha of the hero foreground so the structure lines/surfaces track the
|
|
172
|
+
inverted hero in both themes (white-on-dark in light mode, dark-on-light
|
|
173
|
+
in dark mode) instead of vanishing white-on-white. */
|
|
163
174
|
--hero-border: color-mix(in srgb, var(--hero-text) 18%, transparent);
|
|
164
175
|
--hero-surface: color-mix(in srgb, var(--hero-text) 7%, transparent);
|
|
165
|
-
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ── BANNER BAND ── full-width, clean, like other content pages' hero banner. */
|
|
179
|
+
.cpub-hero-banner {
|
|
180
|
+
width: 100%;
|
|
181
|
+
background: var(--surface2);
|
|
182
|
+
border-bottom: var(--border-width-default) solid var(--border);
|
|
183
|
+
overflow: hidden;
|
|
184
|
+
}
|
|
185
|
+
.cpub-hero-banner img {
|
|
186
|
+
display: block;
|
|
187
|
+
width: 100%;
|
|
188
|
+
max-height: 300px;
|
|
189
|
+
object-fit: cover;
|
|
190
|
+
margin: 0 auto;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ── HERO BODY ── the contest's dark, patterned section. */
|
|
194
|
+
.cpub-hero-body {
|
|
195
|
+
position: relative;
|
|
196
|
+
overflow: hidden;
|
|
197
|
+
background: var(--hero-bg);
|
|
198
|
+
padding: 44px 0;
|
|
166
199
|
}
|
|
167
200
|
.cpub-hero-pattern { position: absolute; inset: 0; }
|
|
168
201
|
.cpub-hero-dots { position: absolute; inset: 0; background-image: radial-gradient(var(--accent-border) 1.5px, transparent 1.5px); background-size: 28px 28px; opacity: .3; }
|
|
169
202
|
.cpub-hero-lines { position: absolute; inset: 0; background-image: linear-gradient(var(--accent-bg) 1px, transparent 1px), linear-gradient(90deg, var(--accent-bg) 1px, transparent 1px); background-size: 56px 56px; }
|
|
170
203
|
.cpub-hero-inner { max-width: 1100px; margin: 0 auto; padding: 0 32px; position: relative; z-index: 1; }
|
|
171
|
-
|
|
172
|
-
.cpub-
|
|
204
|
+
|
|
205
|
+
.cpub-cancelled-banner { background: var(--red-bg); border: var(--border-width-default) solid var(--red-border); color: var(--red); padding: 10px 14px; font-size: 12px; font-weight: 600; display: flex; align-items: center; gap: 8px; margin-bottom: 20px; }
|
|
206
|
+
|
|
207
|
+
/* 2-column: details (flex) + countdown (auto width). */
|
|
208
|
+
.cpub-hero-grid { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 48px; align-items: start; }
|
|
209
|
+
.cpub-hero-main { min-width: 0; }
|
|
210
|
+
|
|
211
|
+
.cpub-hero-eyebrow { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
173
212
|
.cpub-contest-badge { font-size: 9px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; font-family: var(--font-mono); color: var(--accent); background: var(--accent-bg); border: var(--border-width-default) solid var(--accent); padding: 3px 10px; border-radius: var(--radius); display: inline-flex; align-items: center; gap: 5px; }
|
|
174
213
|
.cpub-contest-badge i { font-size: 8px; }
|
|
175
|
-
.cpub-
|
|
176
|
-
.cpub-
|
|
177
|
-
.cpub-
|
|
178
|
-
.cpub-
|
|
179
|
-
.cpub-
|
|
180
|
-
|
|
181
|
-
.cpub-countdown-section { margin-bottom: 28px; }
|
|
182
|
-
.cpub-countdown-label { font-size: 10px; font-family: var(--font-mono); color: var(--hero-text-dim); letter-spacing: .1em; text-transform: uppercase; margin-bottom: 10px; display: flex; align-items: center; gap: 4px; }
|
|
183
|
-
.cpub-countdown-label i { color: var(--accent); }
|
|
184
|
-
.cpub-countdown-row { display: flex; align-items: center; gap: 8px; }
|
|
185
|
-
.cpub-countdown-block { display: flex; flex-direction: column; align-items: center; background: var(--hero-surface); border: var(--border-width-default) solid var(--hero-border); border-radius: var(--radius); padding: 10px 16px; min-width: 60px; box-shadow: 4px 4px 0 var(--hero-surface); }
|
|
186
|
-
.cpub-countdown-val { font-size: 26px; font-weight: 700; font-family: var(--font-mono); color: var(--hero-text); line-height: 1; margin-bottom: 4px; }
|
|
187
|
-
.cpub-countdown-unit { font-size: 9px; text-transform: uppercase; letter-spacing: .1em; color: var(--hero-text-dim); font-family: var(--font-mono); }
|
|
188
|
-
.cpub-countdown-sep { font-size: 20px; font-weight: 700; color: var(--hero-border); margin-top: -8px; font-family: var(--font-mono); }
|
|
214
|
+
.cpub-status-pill { font-size: 9px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; font-family: var(--font-mono); padding: 3px 10px; border-radius: var(--radius); border: var(--border-width-default) solid var(--hero-border); color: var(--hero-text-dim); }
|
|
215
|
+
.cpub-status-pill[data-status="active"] { color: var(--green); border-color: var(--green); background: color-mix(in srgb, var(--green) 14%, transparent); }
|
|
216
|
+
.cpub-status-pill[data-status="judging"] { color: var(--accent); border-color: var(--accent); background: var(--accent-bg); }
|
|
217
|
+
.cpub-status-pill[data-status="upcoming"] { color: var(--yellow); border-color: var(--yellow); }
|
|
218
|
+
.cpub-status-pill[data-status="completed"], .cpub-status-pill[data-status="cancelled"] { color: var(--red); border-color: var(--red-border); }
|
|
189
219
|
|
|
190
|
-
.cpub-
|
|
220
|
+
.cpub-hero-title { font-size: 34px; font-weight: 800; letter-spacing: -.03em; line-height: 1.1; margin: 0 0 10px; color: var(--hero-text); }
|
|
221
|
+
.cpub-hero-tagline { font-size: 14px; color: var(--hero-text-dim); line-height: 1.55; max-width: 600px; margin: 0 0 20px; display: -webkit-box; -webkit-line-clamp: 4; line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
|
|
222
|
+
.cpub-hero-meta { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; font-size: 11px; color: var(--hero-text-dim); font-family: var(--font-mono); margin-bottom: 24px; }
|
|
223
|
+
.cpub-hero-meta-item { display: flex; align-items: center; gap: 6px; }
|
|
191
224
|
|
|
192
225
|
.cpub-hero-cta { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
193
226
|
.cpub-btn-lg { padding: 10px 22px; font-size: 13px; }
|
|
@@ -196,31 +229,40 @@ const tagline = computed<string>(() => {
|
|
|
196
229
|
.cpub-btn-cancel { color: var(--red); border-color: var(--red-border); }
|
|
197
230
|
.cpub-btn-cancel:hover { background: var(--red-bg); }
|
|
198
231
|
|
|
199
|
-
.cpub-
|
|
200
|
-
.cpub-hero-stat { display: flex; flex-direction: column; }
|
|
201
|
-
.cpub-hero-stat-val { font-size: 20px; font-weight: 700; font-family: var(--font-mono); color: var(--hero-text); }
|
|
202
|
-
.cpub-hero-stat-label { font-size: 10px; color: var(--hero-text-dim); text-transform: uppercase; letter-spacing: .1em; font-family: var(--font-mono); }
|
|
203
|
-
|
|
204
|
-
.cpub-admin-controls { display: flex; align-items: center; gap: 8px; margin-top: 16px; padding: 10px 14px; background: var(--accent-bg); border: var(--border-width-default) solid var(--accent-border); }
|
|
232
|
+
.cpub-admin-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 18px; padding: 10px 14px; background: var(--accent-bg); border: var(--border-width-default) solid var(--accent-border); }
|
|
205
233
|
.cpub-admin-controls-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); margin-right: 4px; font-family: var(--font-mono); }
|
|
206
|
-
.cpub-admin-status { font-size: 11px; color: var(--text-dim); margin-left: auto; font-family: var(--font-mono); }
|
|
207
|
-
.cpub-admin-status strong { color: var(--accent); text-transform: capitalize; }
|
|
208
234
|
|
|
235
|
+
/* ── COUNTDOWN (right column) ── */
|
|
236
|
+
.cpub-hero-side { display: flex; flex-direction: column; }
|
|
237
|
+
.cpub-countdown-section { background: var(--hero-surface); border: var(--border-width-default) solid var(--hero-border); border-radius: var(--radius); padding: 16px 18px; }
|
|
238
|
+
.cpub-countdown-label { font-size: 10px; font-family: var(--font-mono); color: var(--hero-text-dim); letter-spacing: .1em; text-transform: uppercase; margin-bottom: 12px; display: flex; align-items: center; gap: 5px; white-space: nowrap; }
|
|
239
|
+
.cpub-countdown-label i { color: var(--accent); }
|
|
240
|
+
.cpub-countdown-row { display: flex; align-items: flex-start; gap: 8px; }
|
|
241
|
+
.cpub-countdown-block { display: flex; flex-direction: column; align-items: center; background: var(--hero-bg); border: var(--border-width-default) solid var(--hero-border); border-radius: var(--radius); padding: 10px 14px; min-width: 56px; }
|
|
242
|
+
.cpub-countdown-val { font-size: 24px; font-weight: 700; font-family: var(--font-mono); color: var(--hero-text); line-height: 1; margin-bottom: 4px; }
|
|
243
|
+
.cpub-countdown-unit { font-size: 8px; text-transform: uppercase; letter-spacing: .1em; color: var(--hero-text-dim); font-family: var(--font-mono); }
|
|
244
|
+
.cpub-countdown-sep { font-size: 20px; font-weight: 700; color: var(--hero-border); font-family: var(--font-mono); padding-top: 10px; }
|
|
245
|
+
.cpub-countdown-ended { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 600; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .08em; color: var(--hero-text-dim); background: var(--hero-surface); border: var(--border-width-default) solid var(--hero-border); border-radius: var(--radius); padding: 14px 18px; }
|
|
246
|
+
.cpub-countdown-ended i { color: var(--accent); }
|
|
247
|
+
|
|
248
|
+
/* ── RESPONSIVE ── stack the countdown below the details. */
|
|
249
|
+
@media (max-width: 900px) {
|
|
250
|
+
.cpub-hero-grid { grid-template-columns: 1fr; gap: 28px; }
|
|
251
|
+
.cpub-hero-side { align-items: flex-start; }
|
|
252
|
+
}
|
|
209
253
|
@media (max-width: 768px) {
|
|
210
|
-
.cpub-hero { padding: 32px 0
|
|
254
|
+
.cpub-hero-body { padding: 32px 0; }
|
|
211
255
|
.cpub-hero-inner { padding: 0 16px; }
|
|
256
|
+
.cpub-hero-banner img { max-height: 200px; }
|
|
212
257
|
.cpub-hero-title { font-size: 24px; }
|
|
213
|
-
.cpub-hero-
|
|
214
|
-
.cpub-hero-meta { flex-wrap: wrap; gap: 10px; }
|
|
215
|
-
.cpub-countdown-block { padding: 8px 12px; min-width: 48px; }
|
|
216
|
-
.cpub-countdown-val { font-size: 20px; }
|
|
258
|
+
.cpub-hero-meta { gap: 10px; }
|
|
217
259
|
}
|
|
218
260
|
@media (max-width: 480px) {
|
|
219
261
|
.cpub-hero-title { font-size: 20px; }
|
|
220
262
|
.cpub-hero-tagline { font-size: 12px; margin-bottom: 16px; }
|
|
221
|
-
.cpub-hero-stats { flex-wrap: wrap; gap: 16px; }
|
|
222
|
-
.cpub-hero-stat-val { font-size: 16px; }
|
|
223
263
|
.cpub-hero-cta { flex-direction: column; align-items: stretch; }
|
|
224
264
|
.cpub-countdown-row { flex-wrap: wrap; justify-content: center; }
|
|
265
|
+
.cpub-countdown-block { min-width: 48px; padding: 8px 12px; }
|
|
266
|
+
.cpub-countdown-val { font-size: 20px; }
|
|
225
267
|
}
|
|
226
268
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"@commonpub/auth": "0.8.0",
|
|
57
57
|
"@commonpub/config": "0.18.0",
|
|
58
58
|
"@commonpub/learning": "0.5.2",
|
|
59
|
-
"@commonpub/docs": "0.6.3",
|
|
60
59
|
"@commonpub/explainer": "0.7.15",
|
|
60
|
+
"@commonpub/docs": "0.6.3",
|
|
61
61
|
"@commonpub/editor": "0.7.11",
|
|
62
62
|
"@commonpub/protocol": "0.13.0",
|
|
63
|
-
"@commonpub/
|
|
64
|
-
"@commonpub/server": "2.
|
|
65
|
-
"@commonpub/
|
|
63
|
+
"@commonpub/ui": "0.9.2",
|
|
64
|
+
"@commonpub/server": "2.74.0",
|
|
65
|
+
"@commonpub/schema": "0.27.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -17,6 +17,7 @@ const subheading = ref('');
|
|
|
17
17
|
const description = ref('');
|
|
18
18
|
const rules = ref('');
|
|
19
19
|
const bannerUrl = ref('');
|
|
20
|
+
const coverImageUrl = ref('');
|
|
20
21
|
const startDate = ref('');
|
|
21
22
|
const endDate = ref('');
|
|
22
23
|
const judgingEndDate = ref('');
|
|
@@ -56,6 +57,7 @@ watch(contest, (c) => {
|
|
|
56
57
|
description.value = c.description ?? '';
|
|
57
58
|
rules.value = c.rules ?? '';
|
|
58
59
|
bannerUrl.value = c.bannerUrl ?? '';
|
|
60
|
+
coverImageUrl.value = c.coverImageUrl ?? '';
|
|
59
61
|
startDate.value = c.startDate ? new Date(c.startDate).toISOString().slice(0, 16) : '';
|
|
60
62
|
endDate.value = c.endDate ? new Date(c.endDate).toISOString().slice(0, 16) : '';
|
|
61
63
|
judgingEndDate.value = c.judgingEndDate ? new Date(c.judgingEndDate).toISOString().slice(0, 16) : '';
|
|
@@ -144,6 +146,7 @@ async function handleSave(): Promise<void> {
|
|
|
144
146
|
description: description.value || undefined,
|
|
145
147
|
rules: rules.value || undefined,
|
|
146
148
|
bannerUrl: bannerUrl.value || undefined,
|
|
149
|
+
coverImageUrl: coverImageUrl.value || undefined,
|
|
147
150
|
startDate: startDate.value ? new Date(startDate.value).toISOString() : undefined,
|
|
148
151
|
endDate: endDate.value ? new Date(endDate.value).toISOString() : undefined,
|
|
149
152
|
judgingEndDate: judgingEndDate.value ? new Date(judgingEndDate.value).toISOString() : undefined,
|
|
@@ -231,7 +234,10 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
231
234
|
<p class="cpub-form-hint">Supports Markdown. Plain one-rule-per-line text is rendered as a numbered list.</p>
|
|
232
235
|
</div>
|
|
233
236
|
<div class="cpub-form-field">
|
|
234
|
-
<ImageUpload v-model="bannerUrl" purpose="banner" label="Banner Image" hint="Wide image
|
|
237
|
+
<ImageUpload v-model="bannerUrl" purpose="banner" label="Banner Image" hint="Wide hero image across the top of the contest page (~4:1)." />
|
|
238
|
+
</div>
|
|
239
|
+
<div class="cpub-form-field">
|
|
240
|
+
<ImageUpload v-model="coverImageUrl" purpose="cover" label="Cover Image (optional)" hint="Card/thumbnail image shown in listings (~4:3). Falls back to the banner if unset." />
|
|
235
241
|
</div>
|
|
236
242
|
</section>
|
|
237
243
|
|
|
@@ -12,6 +12,7 @@ const subheading = ref('');
|
|
|
12
12
|
const description = ref('');
|
|
13
13
|
const rules = ref('');
|
|
14
14
|
const bannerUrl = ref('');
|
|
15
|
+
const coverImageUrl = ref('');
|
|
15
16
|
const startDate = ref('');
|
|
16
17
|
const endDate = ref('');
|
|
17
18
|
const judgingEndDate = ref('');
|
|
@@ -48,11 +49,10 @@ interface Prize {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const prizesDescription = ref('');
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
]);
|
|
52
|
+
// Prizes are entirely optional — start empty so a contest has NO prizes unless
|
|
53
|
+
// the operator explicitly adds them (the old 3 pre-filled rows forced prizes
|
|
54
|
+
// onto every contest, since their non-empty titles survived the submit filter).
|
|
55
|
+
const prizes = ref<Prize[]>([]);
|
|
56
56
|
|
|
57
57
|
function addPrize(): void {
|
|
58
58
|
prizes.value.push({ place: null, category: '', title: '', description: '', value: '' });
|
|
@@ -96,6 +96,7 @@ async function handleCreate(): Promise<void> {
|
|
|
96
96
|
description: description.value || undefined,
|
|
97
97
|
rules: rules.value || undefined,
|
|
98
98
|
bannerUrl: bannerUrl.value || undefined,
|
|
99
|
+
coverImageUrl: coverImageUrl.value || undefined,
|
|
99
100
|
startDate: new Date(startDate.value).toISOString(),
|
|
100
101
|
endDate: new Date(endDate.value).toISOString(),
|
|
101
102
|
judgingEndDate: judgingEndDate.value ? new Date(judgingEndDate.value).toISOString() : undefined,
|
|
@@ -172,7 +173,10 @@ function prizeLabel(prize: Prize): string {
|
|
|
172
173
|
<p class="cpub-form-hint">Supports Markdown. Plain one-rule-per-line text is rendered as a numbered list.</p>
|
|
173
174
|
</div>
|
|
174
175
|
<div class="cpub-form-field">
|
|
175
|
-
<ImageUpload v-model="bannerUrl" purpose="banner" label="Banner Image" hint="Wide image
|
|
176
|
+
<ImageUpload v-model="bannerUrl" purpose="banner" label="Banner Image" hint="Wide hero image across the top of the contest page (~4:1)." />
|
|
177
|
+
</div>
|
|
178
|
+
<div class="cpub-form-field">
|
|
179
|
+
<ImageUpload v-model="coverImageUrl" purpose="cover" label="Cover Image (optional)" hint="Card/thumbnail image shown in listings (~4:3). Falls back to the banner if unset." />
|
|
176
180
|
</div>
|
|
177
181
|
</section>
|
|
178
182
|
|
|
@@ -281,13 +285,13 @@ function prizeLabel(prize: Prize): string {
|
|
|
281
285
|
<!-- Prizes -->
|
|
282
286
|
<section class="cpub-form-section">
|
|
283
287
|
<div class="cpub-form-section-header">
|
|
284
|
-
<h2 class="cpub-form-section-title">Prizes</h2>
|
|
288
|
+
<h2 class="cpub-form-section-title">Prizes <span style="color: var(--text-faint); font-weight: 400; font-size: 0.75em; font-family: var(--font-mono);">— optional</span></h2>
|
|
285
289
|
<button type="button" class="cpub-btn cpub-btn-sm" @click="addPrize">
|
|
286
290
|
<i class="fa-solid fa-plus"></i> Add Prize
|
|
287
291
|
</button>
|
|
288
292
|
</div>
|
|
289
293
|
|
|
290
|
-
<p class="cpub-form-hint">
|
|
294
|
+
<p class="cpub-form-hint">Contests don't need prizes — leave this empty to skip them entirely. If you do add prizes, every field is optional: use <strong>place</strong> for ranked prizes (1st/2nd/3rd), a <strong>category</strong> for themed awards (e.g. "Best in Show"), or just a <strong>description</strong>. Cash value is optional.</p>
|
|
291
295
|
<div class="cpub-form-field">
|
|
292
296
|
<label for="prizes-desc" class="cpub-form-label">Prizes overview (optional)</label>
|
|
293
297
|
<textarea id="prizes-desc" v-model="prizesDescription" class="cpub-form-textarea" rows="3" placeholder="Intro shown above the prize cards. Supports Markdown." />
|
package/pages/contests/index.vue
CHANGED
|
@@ -51,13 +51,15 @@ const canCreateContest = computed(() => {
|
|
|
51
51
|
:to="`/contests/${contest.slug}`"
|
|
52
52
|
class="cpub-card cpub-contest-card"
|
|
53
53
|
>
|
|
54
|
-
<!--
|
|
54
|
+
<!-- Card image: coverImageUrl (cover-cropped) → bannerUrl (contained, so a
|
|
55
|
+
wide hero/logo isn't crop-mangled) → trophy fallback. Status badge overlaid. -->
|
|
55
56
|
<div class="cpub-contest-thumb">
|
|
56
57
|
<img
|
|
57
|
-
v-if="coverFor(contest.bannerUrl)"
|
|
58
|
-
:src="coverFor(contest.bannerUrl)!"
|
|
58
|
+
v-if="coverFor(contest.coverImageUrl ?? contest.bannerUrl)"
|
|
59
|
+
:src="coverFor(contest.coverImageUrl ?? contest.bannerUrl)!"
|
|
59
60
|
:alt="contest.title"
|
|
60
61
|
class="cpub-contest-cover"
|
|
62
|
+
:class="{ 'cpub-contest-cover--contain': !contest.coverImageUrl && !!contest.bannerUrl }"
|
|
61
63
|
loading="lazy"
|
|
62
64
|
/>
|
|
63
65
|
<template v-else>
|
|
@@ -116,6 +118,8 @@ const canCreateContest = computed(() => {
|
|
|
116
118
|
overflow: hidden;
|
|
117
119
|
}
|
|
118
120
|
.cpub-contest-cover { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
121
|
+
/* Banner-as-fallback: contain (+ breathing room) so a wide hero/logo shows whole, not crop-mangled. */
|
|
122
|
+
.cpub-contest-cover--contain { object-fit: contain; padding: 12px; background: var(--surface2); }
|
|
119
123
|
.cpub-contest-thumb-grid {
|
|
120
124
|
position: absolute;
|
|
121
125
|
inset: 0;
|