@commonpub/layer 0.53.0 → 0.55.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.
|
@@ -20,11 +20,6 @@ const props = defineProps<{
|
|
|
20
20
|
|
|
21
21
|
const KINDS: ContestStage['kind'][] = ['submission', 'review', 'interim', 'results', 'event', 'custom'];
|
|
22
22
|
|
|
23
|
-
function newId(): string {
|
|
24
|
-
const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;
|
|
25
|
-
return c?.randomUUID?.() ?? `s-${Date.now()}-${Math.round(Math.random() * 1e6)}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
23
|
// datetime-local <-> ISO (mirrors the rest of the contest forms' convention).
|
|
29
24
|
function toLocal(iso?: string): string {
|
|
30
25
|
if (!iso) return '';
|
|
@@ -50,39 +45,28 @@ function onDate(i: number, field: 'startsAt' | 'endsAt', e: Event): void {
|
|
|
50
45
|
setField(i, { [field]: v } as Partial<ContestStage>);
|
|
51
46
|
}
|
|
52
47
|
|
|
48
|
+
// Array operations live as pure functions in utils/contestStages.ts (unit-tested).
|
|
53
49
|
function addStage(): void {
|
|
54
|
-
commit(
|
|
50
|
+
commit(withStageAdded(stages.value));
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
function duplicateStage(i: number): void {
|
|
58
|
-
|
|
59
|
-
if (!src) return;
|
|
60
|
-
const copy: ContestStage = { ...src, id: newId(), name: `${src.name} (copy)`, core: false };
|
|
61
|
-
commit([...stages.value.slice(0, i + 1), copy, ...stages.value.slice(i + 1)]);
|
|
54
|
+
commit(withStageDuplicated(stages.value, i));
|
|
62
55
|
}
|
|
63
56
|
|
|
64
57
|
function removeStage(i: number): void {
|
|
65
58
|
const removed = stages.value[i];
|
|
66
|
-
commit(stages.value
|
|
59
|
+
commit(withStageRemoved(stages.value, i));
|
|
67
60
|
if (removed && currentId.value === removed.id) currentId.value = null;
|
|
68
61
|
}
|
|
69
62
|
|
|
70
63
|
function move(i: number, dir: -1 | 1): void {
|
|
71
|
-
|
|
72
|
-
if (j < 0 || j >= stages.value.length) return;
|
|
73
|
-
const next = [...stages.value];
|
|
74
|
-
[next[i], next[j]] = [next[j]!, next[i]!];
|
|
75
|
-
commit(next);
|
|
64
|
+
commit(withStageMoved(stages.value, i, dir));
|
|
76
65
|
}
|
|
77
66
|
|
|
78
67
|
// Seed the editor with the standard three stages so the owner has a starting point.
|
|
79
68
|
function customize(): void {
|
|
80
|
-
|
|
81
|
-
commit([
|
|
82
|
-
{ id: newId(), name: 'Submissions', kind: 'submission', startsAt: iso(props.startDate), endsAt: iso(props.endDate) },
|
|
83
|
-
{ id: newId(), name: 'Judging', kind: 'review', endsAt: iso(props.judgingEndDate) ?? iso(props.endDate) },
|
|
84
|
-
{ id: newId(), name: 'Results', kind: 'results' },
|
|
85
|
-
]);
|
|
69
|
+
commit(seedStandardStages(props));
|
|
86
70
|
}
|
|
87
71
|
|
|
88
72
|
function resetToStandard(): void {
|
|
@@ -108,6 +92,13 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
|
|
|
108
92
|
</div>
|
|
109
93
|
|
|
110
94
|
<template v-else>
|
|
95
|
+
<div class="cpub-stage-tophead">
|
|
96
|
+
<span class="cpub-stage-count">{{ stages.length }} stage{{ stages.length === 1 ? '' : 's' }}</span>
|
|
97
|
+
<div class="cpub-stage-toolbar">
|
|
98
|
+
<button type="button" class="cpub-btn cpub-btn-sm" @click="addStage"><i class="fa-solid fa-plus"></i> Add stage</button>
|
|
99
|
+
<button type="button" class="cpub-btn cpub-btn-sm cpub-stage-reset" @click="resetToStandard"><i class="fa-solid fa-rotate-left"></i> Reset to standard</button>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
111
102
|
<p v-if="missingSubmission" class="cpub-form-error" role="alert" style="margin: 0 0 10px;">
|
|
112
103
|
Add at least one <strong>Submissions</strong> stage, or reset to the standard flow.
|
|
113
104
|
</p>
|
|
@@ -193,14 +184,25 @@ const missingSubmission = computed(() => stages.value.length > 0 && !stages.valu
|
|
|
193
184
|
|
|
194
185
|
<div class="cpub-stage-toolbar">
|
|
195
186
|
<button type="button" class="cpub-btn cpub-btn-sm" @click="addStage"><i class="fa-solid fa-plus"></i> Add stage</button>
|
|
196
|
-
<button type="button" class="cpub-btn cpub-btn-sm cpub-stage-reset" @click="resetToStandard"><i class="fa-solid fa-rotate-left"></i> Reset to standard flow</button>
|
|
197
187
|
</div>
|
|
198
188
|
</template>
|
|
199
189
|
</div>
|
|
200
190
|
</template>
|
|
201
191
|
|
|
202
192
|
<style scoped>
|
|
193
|
+
/* Self-contained form-control styles (tokenised) — Vue scoped CSS doesn't cross
|
|
194
|
+
component boundaries, so this extracted editor styles its own inputs rather than
|
|
195
|
+
relying on the parent page. Mirrors the contest pages' controls. */
|
|
196
|
+
.cpub-form-field { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-3); }
|
|
197
|
+
.cpub-form-field:last-child { margin-bottom: 0; }
|
|
198
|
+
.cpub-form-input, .cpub-form-textarea { width: 100%; padding: var(--space-2) var(--space-3); border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: var(--text-sm); font-family: var(--font-sans); }
|
|
199
|
+
.cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; box-shadow: var(--shadow-accent); }
|
|
200
|
+
.cpub-form-textarea { resize: vertical; }
|
|
201
|
+
.cpub-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-3); }
|
|
202
|
+
|
|
203
203
|
.cpub-stages-standard { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
|
|
204
|
+
.cpub-stage-tophead { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
205
|
+
.cpub-stage-count { font-size: 11px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); }
|
|
204
206
|
.cpub-stage-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
|
|
205
207
|
.cpub-stage-row { border: var(--border-width-default) solid var(--border); background: var(--surface2); padding: 12px; }
|
|
206
208
|
.cpub-stage-row-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.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",
|
|
57
|
-
"@commonpub/learning": "0.5.2",
|
|
58
|
-
"@commonpub/editor": "0.7.11",
|
|
59
|
-
"@commonpub/schema": "0.30.0",
|
|
60
|
-
"@commonpub/protocol": "0.13.0",
|
|
61
58
|
"@commonpub/docs": "0.6.3",
|
|
62
59
|
"@commonpub/explainer": "0.7.15",
|
|
60
|
+
"@commonpub/protocol": "0.13.0",
|
|
61
|
+
"@commonpub/editor": "0.7.11",
|
|
62
|
+
"@commonpub/learning": "0.5.2",
|
|
63
63
|
"@commonpub/server": "2.77.0",
|
|
64
|
-
"@commonpub/
|
|
65
|
-
"@commonpub/
|
|
64
|
+
"@commonpub/schema": "0.30.0",
|
|
65
|
+
"@commonpub/ui": "0.9.2"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -268,6 +268,8 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
268
268
|
</p>
|
|
269
269
|
|
|
270
270
|
<form class="cpub-edit-form" @submit.prevent="handleSave">
|
|
271
|
+
<div class="cpub-edit-layout">
|
|
272
|
+
<div class="cpub-edit-main">
|
|
271
273
|
<section class="cpub-form-section">
|
|
272
274
|
<h2 class="cpub-form-section-title">Details</h2>
|
|
273
275
|
<div class="cpub-form-field">
|
|
@@ -348,24 +350,6 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
348
350
|
</div>
|
|
349
351
|
</section>
|
|
350
352
|
|
|
351
|
-
<section class="cpub-form-section">
|
|
352
|
-
<h2 class="cpub-form-section-title">Entries</h2>
|
|
353
|
-
<div class="cpub-form-field">
|
|
354
|
-
<span class="cpub-form-label">Eligible content types</span>
|
|
355
|
-
<p class="cpub-form-hint">Leave all unchecked to accept any published content the entrant owns.</p>
|
|
356
|
-
<div class="cpub-type-options" role="group" aria-label="Eligible content types">
|
|
357
|
-
<label v-for="t in enabledTypeMeta" :key="t.type" class="cpub-form-check">
|
|
358
|
-
<input type="checkbox" :checked="eligibleContentTypes.includes(t.type)" @change="toggleType(t.type)" />
|
|
359
|
-
<span>{{ t.label }}</span>
|
|
360
|
-
</label>
|
|
361
|
-
</div>
|
|
362
|
-
</div>
|
|
363
|
-
<div class="cpub-form-field">
|
|
364
|
-
<label class="cpub-form-label">Max entries per person</label>
|
|
365
|
-
<input v-model.number="maxEntriesPerUser" type="number" min="1" class="cpub-form-input" placeholder="Unlimited" style="max-width: 160px;" />
|
|
366
|
-
</div>
|
|
367
|
-
</section>
|
|
368
|
-
|
|
369
353
|
<section class="cpub-form-section">
|
|
370
354
|
<h2 class="cpub-form-section-title">Prizes</h2>
|
|
371
355
|
<label class="cpub-form-check" style="margin-bottom: 10px;">
|
|
@@ -482,6 +466,26 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
482
466
|
<p class="cpub-form-hint">Invited judges receive a notification and must accept before they can score.</p>
|
|
483
467
|
<ContestJudgeManager :contest-slug="slug" :is-owner="true" />
|
|
484
468
|
</section>
|
|
469
|
+
</div><!-- /cpub-edit-main -->
|
|
470
|
+
|
|
471
|
+
<aside class="cpub-edit-side">
|
|
472
|
+
<section class="cpub-form-section">
|
|
473
|
+
<h2 class="cpub-form-section-title">Entries</h2>
|
|
474
|
+
<div class="cpub-form-field">
|
|
475
|
+
<span class="cpub-form-label">Eligible content types</span>
|
|
476
|
+
<p class="cpub-form-hint">Leave all unchecked to accept any published content the entrant owns.</p>
|
|
477
|
+
<div class="cpub-type-options" role="group" aria-label="Eligible content types">
|
|
478
|
+
<label v-for="t in enabledTypeMeta" :key="t.type" class="cpub-form-check">
|
|
479
|
+
<input type="checkbox" :checked="eligibleContentTypes.includes(t.type)" @change="toggleType(t.type)" />
|
|
480
|
+
<span>{{ t.label }}</span>
|
|
481
|
+
</label>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
<div class="cpub-form-field">
|
|
485
|
+
<label class="cpub-form-label">Max entries per person</label>
|
|
486
|
+
<input v-model.number="maxEntriesPerUser" type="number" min="1" class="cpub-form-input" placeholder="Unlimited" />
|
|
487
|
+
</div>
|
|
488
|
+
</section>
|
|
485
489
|
|
|
486
490
|
<section class="cpub-form-section">
|
|
487
491
|
<h2 class="cpub-form-section-title">Stage & Status</h2>
|
|
@@ -526,6 +530,8 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
526
530
|
</button>
|
|
527
531
|
</div>
|
|
528
532
|
</section>
|
|
533
|
+
</aside><!-- /cpub-edit-side -->
|
|
534
|
+
</div><!-- /cpub-edit-layout -->
|
|
529
535
|
|
|
530
536
|
<!-- Sticky save bar — always reachable without scrolling to the bottom. -->
|
|
531
537
|
<div class="cpub-edit-actionbar">
|
|
@@ -545,7 +551,7 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
545
551
|
</template>
|
|
546
552
|
|
|
547
553
|
<style scoped>
|
|
548
|
-
.cpub-contest-edit { max-width:
|
|
554
|
+
.cpub-contest-edit { max-width: 1080px; margin: 0 auto; padding: 32px; }
|
|
549
555
|
.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; }
|
|
550
556
|
.cpub-back-link:hover { color: var(--accent); }
|
|
551
557
|
.cpub-edit-title { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
|
|
@@ -561,15 +567,19 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
561
567
|
.cpub-status-cancelled { color: var(--red); border-color: var(--red-border); background: var(--red-bg); }
|
|
562
568
|
|
|
563
569
|
.cpub-edit-form { display: flex; flex-direction: column; gap: 16px; }
|
|
570
|
+
/* Two-column editor: wide content column + a sticky meta rail (Stage & Status,
|
|
571
|
+
Entry rules, Danger Zone) so lifecycle controls stay reachable while editing. */
|
|
572
|
+
.cpub-edit-layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 16px; align-items: start; }
|
|
573
|
+
.cpub-edit-main { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
|
574
|
+
.cpub-edit-side { display: flex; flex-direction: column; gap: 16px; position: sticky; top: 76px; }
|
|
564
575
|
.cpub-form-section { border: var(--border-width-default) solid var(--border); background: var(--surface); padding: 20px; box-shadow: var(--shadow-md); }
|
|
565
576
|
.cpub-form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 14px; }
|
|
566
|
-
.cpub-form-field { display: flex; flex-direction: column; gap:
|
|
577
|
+
.cpub-form-field { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-3); }
|
|
567
578
|
.cpub-form-field:last-child { margin-bottom: 0; }
|
|
568
|
-
.cpub-form-
|
|
569
|
-
.cpub-form-input, .cpub-form-textarea {
|
|
570
|
-
.cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; }
|
|
579
|
+
.cpub-form-input, .cpub-form-textarea { width: 100%; padding: var(--space-2) var(--space-3); border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: var(--text-sm); font-family: var(--font-sans); }
|
|
580
|
+
.cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; box-shadow: var(--shadow-accent); }
|
|
571
581
|
.cpub-form-textarea { resize: vertical; }
|
|
572
|
-
.cpub-form-row { display: grid; grid-template-columns:
|
|
582
|
+
.cpub-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-3); }
|
|
573
583
|
|
|
574
584
|
.cpub-form-error { font-size: 12px; color: var(--red); margin-top: 8px; }
|
|
575
585
|
.cpub-form-check { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-dim); cursor: pointer; }
|
|
@@ -632,6 +642,11 @@ async function transitionStatus(newStatus: string): Promise<void> {
|
|
|
632
642
|
.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; }
|
|
633
643
|
.cpub-edit-actionbar-btns { display: flex; align-items: center; gap: 8px; }
|
|
634
644
|
|
|
645
|
+
/* Collapse the meta rail under the main column on narrower viewports. */
|
|
646
|
+
@media (max-width: 900px) {
|
|
647
|
+
.cpub-edit-layout { grid-template-columns: 1fr; }
|
|
648
|
+
.cpub-edit-side { position: static; }
|
|
649
|
+
}
|
|
635
650
|
@media (max-width: 768px) {
|
|
636
651
|
.cpub-contest-edit { padding: 16px; }
|
|
637
652
|
.cpub-form-row { grid-template-columns: 1fr; }
|
|
@@ -403,13 +403,12 @@ function prizeLabel(prize: Prize): string {
|
|
|
403
403
|
.cpub-form-section-title { font-size: 14px; font-weight: 700; margin-bottom: 16px; }
|
|
404
404
|
.cpub-form-section-header .cpub-form-section-title { margin-bottom: 0; }
|
|
405
405
|
|
|
406
|
-
.cpub-form-field { display: flex; flex-direction: column; gap:
|
|
406
|
+
.cpub-form-field { display: flex; flex-direction: column; gap: var(--space-1); margin-bottom: var(--space-3); }
|
|
407
407
|
.cpub-form-field:last-child { margin-bottom: 0; }
|
|
408
|
-
.cpub-form-
|
|
409
|
-
.cpub-form-input, .cpub-form-textarea {
|
|
410
|
-
.cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; }
|
|
408
|
+
.cpub-form-input, .cpub-form-textarea { width: 100%; padding: var(--space-2) var(--space-3); border: var(--border-width-default) solid var(--border); background: var(--surface); color: var(--text); font-size: var(--text-sm); font-family: var(--font-sans); }
|
|
409
|
+
.cpub-form-input:focus, .cpub-form-textarea:focus { border-color: var(--accent); outline: none; box-shadow: var(--shadow-accent); }
|
|
411
410
|
.cpub-form-textarea { resize: vertical; }
|
|
412
|
-
.cpub-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:
|
|
411
|
+
.cpub-form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-3); }
|
|
413
412
|
|
|
414
413
|
.cpub-prize-card { border: var(--border-width-default) solid var(--border); padding: 14px; margin-bottom: 10px; background: var(--surface2); }
|
|
415
414
|
.cpub-prize-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
package/utils/contestStages.ts
CHANGED
|
@@ -49,6 +49,50 @@ export function currentStageId(c: StageSource): string | null {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// ─── Pure stage-array operations (used by ContestStagesEditor; unit-tested) ───
|
|
53
|
+
|
|
54
|
+
export function newStageId(): string {
|
|
55
|
+
const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;
|
|
56
|
+
return c?.randomUUID?.() ?? `s-${Date.now()}-${Math.round(Math.random() * 1e6)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function blankStage(): ContestStage {
|
|
60
|
+
return { id: newStageId(), name: '', kind: 'custom' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The three standard stages seeded when an operator chooses to customise. */
|
|
64
|
+
export function seedStandardStages(c: { startDate?: string | null; endDate?: string | null; judgingEndDate?: string | null }): ContestStage[] {
|
|
65
|
+
const i = (d?: string | null): string | undefined => (d ? new Date(d).toISOString() : undefined);
|
|
66
|
+
return [
|
|
67
|
+
{ id: newStageId(), name: 'Submissions', kind: 'submission', startsAt: i(c.startDate), endsAt: i(c.endDate) },
|
|
68
|
+
{ id: newStageId(), name: 'Judging', kind: 'review', endsAt: i(c.judgingEndDate) ?? i(c.endDate) },
|
|
69
|
+
{ id: newStageId(), name: 'Results', kind: 'results' },
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function withStageAdded(stages: ContestStage[]): ContestStage[] {
|
|
74
|
+
return [...stages, blankStage()];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function withStageDuplicated(stages: ContestStage[], i: number): ContestStage[] {
|
|
78
|
+
const src = stages[i];
|
|
79
|
+
if (!src) return stages;
|
|
80
|
+
const copy: ContestStage = { ...src, id: newStageId(), name: `${src.name} (copy)`, core: false };
|
|
81
|
+
return [...stages.slice(0, i + 1), copy, ...stages.slice(i + 1)];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function withStageRemoved(stages: ContestStage[], i: number): ContestStage[] {
|
|
85
|
+
return stages.filter((_, idx) => idx !== i);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function withStageMoved(stages: ContestStage[], i: number, dir: -1 | 1): ContestStage[] {
|
|
89
|
+
const j = i + dir;
|
|
90
|
+
if (j < 0 || j >= stages.length) return stages;
|
|
91
|
+
const next = [...stages];
|
|
92
|
+
[next[i], next[j]] = [next[j]!, next[i]!];
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
|
|
52
96
|
/** FontAwesome icon (no `fa-solid` prefix) for each stage kind. */
|
|
53
97
|
export const STAGE_KIND_ICON: Record<ContestStage['kind'], string> = {
|
|
54
98
|
submission: 'fa-pen-to-square',
|