@commonpub/layer 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonpub/layer",
3
- "version": "0.58.0",
3
+ "version": "0.59.0",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -53,16 +53,16 @@
53
53
  "vue": "^3.4.0",
54
54
  "vue-router": "^4.3.0",
55
55
  "zod": "^4.3.6",
56
+ "@commonpub/auth": "0.8.0",
56
57
  "@commonpub/config": "0.18.0",
58
+ "@commonpub/docs": "0.6.3",
57
59
  "@commonpub/editor": "0.7.11",
58
60
  "@commonpub/explainer": "0.7.15",
59
- "@commonpub/learning": "0.5.2",
60
61
  "@commonpub/schema": "0.33.0",
61
- "@commonpub/auth": "0.8.0",
62
62
  "@commonpub/server": "2.80.0",
63
+ "@commonpub/ui": "0.9.2",
63
64
  "@commonpub/protocol": "0.13.0",
64
- "@commonpub/docs": "0.6.3",
65
- "@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",
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { ContestStage } from '@commonpub/schema';
3
+ import type { Serialized, ContestEntryItem } from '@commonpub/server';
3
4
 
4
5
  definePageMeta({ middleware: 'auth' });
5
6
 
@@ -226,6 +227,35 @@ const statusAction = contestStatusAction;
226
227
  // Phase B2 — advancement cuts. Operates on the PERSISTED stages (contest.value),
227
228
  // not the editable `stages` ref, since it acts on real entries.
228
229
  const reviewStages = computed(() => (contest.value?.stages ?? []).filter((s) => s.kind === 'review'));
230
+
231
+ // Entries (the cohort) — for the manual advancement picker. The eligible set is
232
+ // everyone not already eliminated by a prior round's cut.
233
+ const { data: entriesData, refresh: refreshEntries } = useLazyFetch<{ items: Serialized<ContestEntryItem>[] }>(`/api/contests/${slug}/entries`);
234
+ const eligibleEntries = computed(() => (entriesData.value?.items ?? []).filter((e) => !e.eliminated));
235
+ const advanceMode = ref<Record<string, 'topN' | 'manual'>>({});
236
+ const manualPick = ref<Record<string, string[]>>({});
237
+ function toggleManual(stageId: string, entryId: string): void {
238
+ const cur = manualPick.value[stageId] ?? [];
239
+ manualPick.value[stageId] = cur.includes(entryId) ? cur.filter((x) => x !== entryId) : [...cur, entryId];
240
+ }
241
+ async function advanceStageManual(stageId: string): Promise<void> {
242
+ const ids = manualPick.value[stageId] ?? [];
243
+ if (!ids.length) { toast.error('Select at least one entry to advance.'); return; }
244
+ if (!confirm(`Advance the ${ids.length} selected ${ids.length === 1 ? 'entry' : 'entries'}? The rest of the cohort is marked "not advanced" and drops out of later judging + final results.`)) return;
245
+ advancing.value = stageId;
246
+ try {
247
+ const r = await $fetch<{ advancedCount: number; eliminatedCount: number }>(`/api/contests/${slug}/advance`, {
248
+ method: 'POST',
249
+ body: { reviewStageId: stageId, mode: 'manual', advancedEntryIds: ids },
250
+ });
251
+ toast.success(`${r.advancedCount} advanced, ${r.eliminatedCount} not advanced.`);
252
+ await Promise.all([refresh(), refreshEntries()]);
253
+ } catch (err: unknown) {
254
+ toast.error(extractError(err));
255
+ } finally {
256
+ advancing.value = null;
257
+ }
258
+ }
229
259
  async function advanceStage(stageId: string): Promise<void> {
230
260
  const topN = advanceN.value[stageId];
231
261
  if (!topN || topN < 1) { toast.error('Enter how many entries advance.'); return; }
@@ -237,7 +267,7 @@ async function advanceStage(stageId: string): Promise<void> {
237
267
  body: { reviewStageId: stageId, mode: 'topN', topN },
238
268
  });
239
269
  toast.success(`${r.advancedCount} advanced, ${r.eliminatedCount} not advanced.`);
240
- await refresh();
270
+ await Promise.all([refresh(), refreshEntries()]);
241
271
  } catch (err: unknown) {
242
272
  toast.error(extractError(err));
243
273
  } finally {
@@ -344,15 +374,36 @@ async function transitionStatus(newStatus: string): Promise<void> {
344
374
  <section v-if="reviewStages.length" class="cpub-form-section">
345
375
  <h2 class="cpub-form-section-title">Advancement</h2>
346
376
  <p class="cpub-form-hint">Multi-round contests: after judging a review stage, advance the top entries to the next stage. Entries below the cut are marked "not advanced" and excluded from later judging + final results. Re-running re-computes the cut. (Save any stage changes above first.)</p>
347
- <div v-for="rs in reviewStages" :key="rs.id" class="cpub-advance-row">
348
- <span class="cpub-advance-name"><i class="fa-solid fa-gavel"></i> {{ rs.name }}</span>
349
- <div class="cpub-advance-ctl">
377
+ <div v-for="rs in reviewStages" :key="rs.id" class="cpub-advance-block">
378
+ <div class="cpub-advance-row">
379
+ <span class="cpub-advance-name"><i class="fa-solid fa-gavel"></i> {{ rs.name }}</span>
380
+ <div class="cpub-advance-mode">
381
+ <label class="cpub-form-check"><input type="radio" :name="`mode-${rs.id}`" :checked="(advanceMode[rs.id] ?? 'topN') === 'topN'" @change="advanceMode[rs.id] = 'topN'" /> <span>Top N</span></label>
382
+ <label class="cpub-form-check"><input type="radio" :name="`mode-${rs.id}`" :checked="advanceMode[rs.id] === 'manual'" @change="advanceMode[rs.id] = 'manual'" /> <span>Pick manually</span></label>
383
+ </div>
384
+ </div>
385
+
386
+ <div v-if="(advanceMode[rs.id] ?? 'topN') === 'topN'" class="cpub-advance-ctl">
350
387
  <label class="cpub-form-label" :for="`adv-${rs.id}`">Advance top</label>
351
388
  <input :id="`adv-${rs.id}`" v-model.number="advanceN[rs.id]" type="number" min="1" class="cpub-form-input cpub-advance-n" placeholder="50" />
352
389
  <button type="button" class="cpub-btn cpub-btn-sm" :disabled="advancing === rs.id" @click="advanceStage(rs.id)">
353
390
  <i class="fa-solid fa-arrow-up-right-dots"></i> {{ advancing === rs.id ? 'Advancing…' : 'Advance' }}
354
391
  </button>
355
392
  </div>
393
+
394
+ <div v-else class="cpub-advance-manual">
395
+ <p v-if="!eligibleEntries.length" class="cpub-form-hint" style="margin: 0;">No entries in the current cohort to pick from yet.</p>
396
+ <template v-else>
397
+ <label v-for="e in eligibleEntries" :key="e.id" class="cpub-advance-pick">
398
+ <input type="checkbox" :checked="(manualPick[rs.id] ?? []).includes(e.id)" @change="toggleManual(rs.id, e.id)" />
399
+ <span class="cpub-advance-pick-title">{{ e.contentTitle }}</span>
400
+ <span v-if="e.score != null" class="cpub-advance-pick-score">{{ e.score }}</span>
401
+ </label>
402
+ <button type="button" class="cpub-btn cpub-btn-sm" :disabled="advancing === rs.id || !(manualPick[rs.id] ?? []).length" @click="advanceStageManual(rs.id)">
403
+ <i class="fa-solid fa-arrow-up-right-dots"></i> {{ advancing === rs.id ? 'Advancing…' : `Advance ${(manualPick[rs.id] ?? []).length} selected` }}
404
+ </button>
405
+ </template>
406
+ </div>
356
407
  </div>
357
408
  </section>
358
409
 
@@ -638,13 +689,20 @@ async function transitionStatus(newStatus: string): Promise<void> {
638
689
  border-top: 2px solid var(--border);
639
690
  box-shadow: var(--shadow-lg);
640
691
  }
641
- .cpub-advance-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 10px 0; border-top: var(--border-width-default) solid var(--border); }
642
- .cpub-advance-row:first-of-type { border-top: 0; }
692
+ .cpub-advance-block { padding: 12px 0; border-top: var(--border-width-default) solid var(--border); }
693
+ .cpub-advance-block:first-of-type { border-top: 0; }
694
+ .cpub-advance-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
643
695
  .cpub-advance-name { font-size: 13px; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; }
644
696
  .cpub-advance-name i { color: var(--accent); font-size: 11px; }
645
- .cpub-advance-ctl { display: inline-flex; align-items: center; gap: 8px; }
697
+ .cpub-advance-mode { display: inline-flex; gap: 12px; }
698
+ .cpub-advance-ctl { display: inline-flex; align-items: center; gap: 8px; margin-top: 10px; }
646
699
  .cpub-advance-ctl .cpub-form-label { margin: 0; }
647
700
  .cpub-advance-n { width: 80px; }
701
+ .cpub-advance-manual { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }
702
+ .cpub-advance-pick { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-dim); padding: 4px 8px; border: var(--border-width-default) solid var(--border); background: var(--surface2); cursor: pointer; }
703
+ .cpub-advance-pick-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
704
+ .cpub-advance-pick-score { font-family: var(--font-mono); font-size: 11px; color: var(--accent); flex-shrink: 0; }
705
+ .cpub-advance-manual .cpub-btn { align-self: flex-start; margin-top: 6px; }
648
706
  .cpub-edit-actionbar-status { font-size: 11px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); display: flex; align-items: center; gap: 8px; }
649
707
  .cpub-edit-actionbar-btns { display: flex; align-items: center; gap: 8px; }
650
708