@commonpub/layer 0.55.0 → 0.56.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.
|
@@ -45,6 +45,25 @@ function onDate(i: number, field: 'startsAt' | 'endsAt', e: Event): void {
|
|
|
45
45
|
setField(i, { [field]: v } as Partial<ContestStage>);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Per-round rubric (review stages). Immutable updates via setField.
|
|
49
|
+
type StageCriterion = { label: string; weight?: number; description?: string };
|
|
50
|
+
function addCriterion(i: number): void {
|
|
51
|
+
const cur = (stages.value[i]?.criteria ?? []) as StageCriterion[];
|
|
52
|
+
setField(i, { criteria: [...cur, { label: '' }] });
|
|
53
|
+
}
|
|
54
|
+
function setCriterion(i: number, ci: number, patch: Partial<StageCriterion>): void {
|
|
55
|
+
const cur = (stages.value[i]?.criteria ?? []).map((c, idx) => (idx === ci ? { ...c, ...patch } : c));
|
|
56
|
+
setField(i, { criteria: cur });
|
|
57
|
+
}
|
|
58
|
+
function removeCriterion(i: number, ci: number): void {
|
|
59
|
+
const cur = (stages.value[i]?.criteria ?? []).filter((_, idx) => idx !== ci);
|
|
60
|
+
setField(i, { criteria: cur.length ? cur : undefined });
|
|
61
|
+
}
|
|
62
|
+
function critWeightInput(i: number, ci: number, e: Event): void {
|
|
63
|
+
const v = (e.target as HTMLInputElement).value;
|
|
64
|
+
setCriterion(i, ci, { weight: v === '' ? undefined : Math.max(0, Math.min(100, Math.round(Number(v)))) });
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
// Array operations live as pure functions in utils/contestStages.ts (unit-tested).
|
|
49
68
|
function addStage(): void {
|
|
50
69
|
commit(withStageAdded(stages.value));
|
|
@@ -147,6 +166,8 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
|
|
|
147
166
|
</div>
|
|
148
167
|
</div>
|
|
149
168
|
|
|
169
|
+
<p class="cpub-stage-kind-help"><i class="fa-solid fa-circle-info"></i> {{ STAGE_KIND_HELP[stage.kind] }}</p>
|
|
170
|
+
|
|
150
171
|
<div class="cpub-form-row">
|
|
151
172
|
<div class="cpub-form-field">
|
|
152
173
|
<label class="cpub-form-label">Starts</label>
|
|
@@ -169,6 +190,20 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
|
|
|
169
190
|
/>
|
|
170
191
|
</div>
|
|
171
192
|
|
|
193
|
+
<!-- Per-round judging rubric (review stages) -->
|
|
194
|
+
<div v-if="stage.kind === 'review'" class="cpub-stage-criteria">
|
|
195
|
+
<div class="cpub-stage-criteria-head">
|
|
196
|
+
<span class="cpub-form-label" style="margin: 0;">Judging criteria — this round</span>
|
|
197
|
+
<button type="button" class="cpub-btn cpub-btn-sm" @click="addCriterion(i)"><i class="fa-solid fa-plus"></i> Add</button>
|
|
198
|
+
</div>
|
|
199
|
+
<p class="cpub-form-hint" style="margin: 4px 0;">Optional — leave empty to use the contest’s default criteria. Set per-round criteria for multi-round contests (e.g. judge proposals on Feasibility, prototypes on Deployment readiness).</p>
|
|
200
|
+
<div v-for="(crit, ci) in (stage.criteria ?? [])" :key="ci" class="cpub-stage-crit-row">
|
|
201
|
+
<input :value="crit.label" type="text" class="cpub-form-input" placeholder="Criterion (e.g. Community impact)" @input="setCriterion(i, ci, { label: ($event.target as HTMLInputElement).value })" />
|
|
202
|
+
<input :value="crit.weight ?? ''" type="number" min="0" max="100" class="cpub-form-input cpub-stage-crit-pts" placeholder="pts" @input="critWeightInput(i, ci, $event)" />
|
|
203
|
+
<button type="button" class="cpub-stage-iconbtn cpub-stage-del" aria-label="Remove criterion" @click="removeCriterion(i, ci)"><i class="fa-solid fa-xmark"></i></button>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
172
207
|
<div v-if="stage.kind === 'event'" class="cpub-form-row">
|
|
173
208
|
<div class="cpub-form-field">
|
|
174
209
|
<label class="cpub-form-label">Location</label>
|
|
@@ -216,4 +251,11 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
|
|
|
216
251
|
.cpub-stage-del:hover { border-color: var(--red-border); color: var(--red); }
|
|
217
252
|
.cpub-stage-toolbar { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
|
|
218
253
|
.cpub-stage-reset { color: var(--text-faint); }
|
|
254
|
+
.cpub-stage-kind-help { font-size: 11px; color: var(--text-faint); line-height: 1.5; margin: 0 0 4px; display: flex; gap: 6px; }
|
|
255
|
+
.cpub-stage-kind-help i { color: var(--accent); margin-top: 2px; flex-shrink: 0; }
|
|
256
|
+
.cpub-stage-criteria { border: var(--border-width-default) dashed var(--border2); padding: 10px; margin-top: 4px; background: var(--surface); }
|
|
257
|
+
.cpub-stage-criteria-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
258
|
+
.cpub-stage-crit-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; }
|
|
259
|
+
.cpub-stage-crit-row .cpub-form-input { margin: 0; }
|
|
260
|
+
.cpub-stage-crit-pts { max-width: 70px; flex-shrink: 0; }
|
|
219
261
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.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/docs": "0.6.3",
|
|
59
|
+
"@commonpub/editor": "0.7.11",
|
|
59
60
|
"@commonpub/explainer": "0.7.15",
|
|
61
|
+
"@commonpub/schema": "0.31.0",
|
|
62
|
+
"@commonpub/server": "2.78.0",
|
|
60
63
|
"@commonpub/protocol": "0.13.0",
|
|
61
|
-
"@commonpub/
|
|
62
|
-
"@commonpub/learning": "0.5.2"
|
|
63
|
-
"@commonpub/server": "2.77.0",
|
|
64
|
-
"@commonpub/schema": "0.30.0",
|
|
65
|
-
"@commonpub/ui": "0.9.2"
|
|
64
|
+
"@commonpub/ui": "0.9.2",
|
|
65
|
+
"@commonpub/learning": "0.5.2"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -15,9 +15,23 @@ const { data: entriesData, refresh: refreshEntries } = useLazyFetch<{ items: (Se
|
|
|
15
15
|
{ query: { includeJudgeScores: true } },
|
|
16
16
|
);
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
const
|
|
18
|
+
// The current review stage (multi-round contests). Drives the round label + the
|
|
19
|
+
// per-round rubric; falls back to the contest-level rubric when the stage has none.
|
|
20
|
+
const currentReviewStage = computed(() => {
|
|
21
|
+
const c = contest.value;
|
|
22
|
+
if (!c || !c.stages?.length) return null;
|
|
23
|
+
const cid = currentStageId(c);
|
|
24
|
+
const st = c.stages.find((s) => s.id === cid);
|
|
25
|
+
return st && st.kind === 'review' ? st : null;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Judging rubric: per-round criteria if the current review stage defines them,
|
|
29
|
+
// else the contest-level rubric. Judges score each criterion (0..max); the overall
|
|
30
|
+
// is the normalized weighted sum (computed server-side).
|
|
31
|
+
const criteria = computed(() => {
|
|
32
|
+
const stageCrit = currentReviewStage.value?.criteria;
|
|
33
|
+
return (stageCrit && stageCrit.length ? stageCrit : contest.value?.judgingCriteria) ?? [];
|
|
34
|
+
});
|
|
21
35
|
const hasCriteria = computed(() => criteria.value.length > 0);
|
|
22
36
|
function critMax(i: number): number {
|
|
23
37
|
const w = criteria.value[i]?.weight;
|
|
@@ -48,7 +62,9 @@ async function acceptInvite(): Promise<void> {
|
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
const entryList = computed(() => {
|
|
51
|
-
|
|
65
|
+
// Cohort scope: once a review stage has culled the field, judges only score the
|
|
66
|
+
// surviving cohort (eliminated entries drop out of later rounds).
|
|
67
|
+
const items = (entriesData.value?.items ?? []).filter((e) => !e.eliminated);
|
|
52
68
|
return items.map((entry) => {
|
|
53
69
|
const myScore = entry.judgeScores?.find((s) => s.judgeId === user.value?.id);
|
|
54
70
|
return {
|
|
@@ -161,8 +177,12 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
161
177
|
<h1 class="cpub-judge-title">
|
|
162
178
|
<i class="fa-solid fa-gavel cpub-judge-icon"></i>
|
|
163
179
|
Judge: {{ contest?.title || 'Contest' }}
|
|
180
|
+
<span v-if="currentReviewStage" class="cpub-judge-round">{{ currentReviewStage.name }}</span>
|
|
164
181
|
</h1>
|
|
165
|
-
<p class="cpub-judge-desc">
|
|
182
|
+
<p class="cpub-judge-desc">
|
|
183
|
+
Score each entry from 1 to 100. Add optional feedback. Scores are saved immediately.
|
|
184
|
+
<template v-if="currentReviewStage"> You're judging the <strong>{{ entryList.length }}</strong> {{ entryList.length === 1 ? 'entry' : 'entries' }} still in this round.</template>
|
|
185
|
+
</p>
|
|
166
186
|
</header>
|
|
167
187
|
|
|
168
188
|
<!-- Loading -->
|
|
@@ -200,9 +220,9 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
200
220
|
Scoring opens when the contest enters the judging phase (currently <strong>{{ contest.status }}</strong>).
|
|
201
221
|
</div>
|
|
202
222
|
|
|
203
|
-
<!-- Rubric guidance -->
|
|
204
|
-
<div v-if="
|
|
205
|
-
<ContestJudgingCriteria :criteria="
|
|
223
|
+
<!-- Rubric guidance (per-round criteria when the current review stage defines them) -->
|
|
224
|
+
<div v-if="criteria.length" class="cpub-judge-rubric">
|
|
225
|
+
<ContestJudgingCriteria :criteria="criteria" compact />
|
|
206
226
|
</div>
|
|
207
227
|
|
|
208
228
|
<!-- Progress bar -->
|
|
@@ -298,6 +318,7 @@ async function submitScore(entryId: string): Promise<void> {
|
|
|
298
318
|
.cpub-judge-back:hover { color: var(--accent); }
|
|
299
319
|
.cpub-judge-title { font-size: 20px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
|
|
300
320
|
.cpub-judge-icon { color: var(--accent); font-size: 18px; }
|
|
321
|
+
.cpub-judge-round { font-size: 10px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .08em; color: var(--accent); border: var(--border-width-default) solid var(--accent-border); background: var(--accent-bg); padding: 3px 9px; border-radius: var(--radius); }
|
|
301
322
|
.cpub-judge-desc { font-size: 13px; color: var(--text-dim); margin-top: 6px; }
|
|
302
323
|
|
|
303
324
|
.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; }
|
package/utils/contestStages.ts
CHANGED
|
@@ -112,3 +112,14 @@ export const STAGE_KIND_LABEL: Record<ContestStage['kind'], string> = {
|
|
|
112
112
|
event: 'Event / Showcase',
|
|
113
113
|
custom: 'Custom milestone',
|
|
114
114
|
};
|
|
115
|
+
|
|
116
|
+
/** What each stage kind actually DOES — shown under the editor's type picker so
|
|
117
|
+
* organisers understand the behaviour they're choosing. */
|
|
118
|
+
export const STAGE_KIND_HELP: Record<ContestStage['kind'], string> = {
|
|
119
|
+
submission: 'Entrants submit (or, in a later round, refine) entries. The hero countdown targets this stage’s end date.',
|
|
120
|
+
review: 'Judges score entries on a rubric. End a review stage with an Advancement cut (Top-N) to pick who continues. Add per-round criteria below for multi-round contests.',
|
|
121
|
+
interim: 'A working period — e.g. a build sprint. The surviving cohort refines their existing entries; no new entrants.',
|
|
122
|
+
results: 'Final standings are published (ranks calculated from the latest judging round).',
|
|
123
|
+
event: 'A real-world milestone or showcase (date + location). Informational — no entry/judging behaviour.',
|
|
124
|
+
custom: 'An arbitrary dated milestone. No behaviour — just appears on the timeline.',
|
|
125
|
+
};
|