@commonpub/layer 0.34.0 → 0.36.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.
|
@@ -62,6 +62,14 @@ const heroCtas = computed<HeroCta[]>(() => {
|
|
|
62
62
|
];
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
// Optional hero logo/image (config-driven, per-site). When set, renders in a
|
|
66
|
+
// side column next to the hero copy — e.g. deveco's hexagon logo — without
|
|
67
|
+
// affecting sites that don't configure one. Additive: no logo unless set.
|
|
68
|
+
const heroLogo = computed(() => {
|
|
69
|
+
const cfg = props.config as { logoImageUrl?: string; logoAlt?: string };
|
|
70
|
+
return cfg.logoImageUrl?.trim() ? { src: cfg.logoImageUrl.trim(), alt: cfg.logoAlt?.trim() || '' } : null;
|
|
71
|
+
});
|
|
72
|
+
|
|
65
73
|
// Shared via useState so the dismiss sticks across component remounts.
|
|
66
74
|
// HomepageSectionRenderer's v-if wrappers can remount HeroSection when the
|
|
67
75
|
// `sections` useFetch revalidates on hydration or when feature flags flip
|
|
@@ -124,6 +132,9 @@ function dismissHero(): void {
|
|
|
124
132
|
</div>
|
|
125
133
|
</template>
|
|
126
134
|
</div>
|
|
135
|
+
<div v-if="heroLogo" class="cpub-hero-visual">
|
|
136
|
+
<img :src="heroLogo.src" :alt="heroLogo.alt" class="cpub-hero-logo-img" />
|
|
137
|
+
</div>
|
|
127
138
|
</div>
|
|
128
139
|
</section>
|
|
129
140
|
</template>
|
|
@@ -145,7 +156,12 @@ function dismissHero(): void {
|
|
|
145
156
|
.cpub-hero-title span { color: var(--accent); }
|
|
146
157
|
.cpub-hero-excerpt { font-size: 13px; color: var(--text-dim); line-height: 1.65; margin-bottom: 20px; max-width: 560px; }
|
|
147
158
|
.cpub-hero-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
159
|
+
.cpub-hero-visual { flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
|
|
160
|
+
.cpub-hero-logo-img { max-height: 240px; max-width: 320px; width: 100%; object-fit: contain; }
|
|
148
161
|
|
|
162
|
+
@media (max-width: 900px) {
|
|
163
|
+
.cpub-hero-visual { display: none; }
|
|
164
|
+
}
|
|
149
165
|
@media (max-width: 640px) {
|
|
150
166
|
.cpub-hero-inner { flex-direction: column; align-items: flex-start; gap: 20px; padding: 24px 16px; }
|
|
151
167
|
.cpub-hero-title { font-size: 19px; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"@commonpub/auth": "0.7.0",
|
|
57
57
|
"@commonpub/config": "0.16.0",
|
|
58
58
|
"@commonpub/editor": "0.7.11",
|
|
59
|
-
"@commonpub/protocol": "0.12.0",
|
|
60
|
-
"@commonpub/schema": "0.24.0",
|
|
61
59
|
"@commonpub/docs": "0.6.3",
|
|
62
|
-
"@commonpub/
|
|
63
|
-
"@commonpub/ui": "0.9.1",
|
|
60
|
+
"@commonpub/explainer": "0.7.15",
|
|
64
61
|
"@commonpub/learning": "0.5.2",
|
|
65
|
-
"@commonpub/
|
|
62
|
+
"@commonpub/schema": "0.24.0",
|
|
63
|
+
"@commonpub/protocol": "0.12.0",
|
|
64
|
+
"@commonpub/server": "2.66.0",
|
|
65
|
+
"@commonpub/ui": "0.9.1"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@testing-library/jest-dom": "^6.9.1",
|
|
@@ -117,20 +117,33 @@ const submitDialogRef = ref<HTMLElement | null>(null);
|
|
|
117
117
|
useFocusTrap(submitDialogRef, () => showSubmitDialog.value, () => { showSubmitDialog.value = false; });
|
|
118
118
|
const submitContentId = ref('');
|
|
119
119
|
const submitting = ref(false);
|
|
120
|
+
// MY own published content only — was fetching everyone's public content (the
|
|
121
|
+
// picker listed other people's projects, which submitContestEntry then rejects
|
|
122
|
+
// since you can only enter your own). authorId === me ⇒ the endpoint scopes to mine.
|
|
120
123
|
const { data: userContent } = useFetch('/api/content', {
|
|
121
|
-
query: { status: 'published', limit: 50 },
|
|
122
|
-
immediate:
|
|
124
|
+
query: computed(() => ({ status: 'published', authorId: user.value?.id, limit: 50 })),
|
|
125
|
+
immediate: !!user.value?.id,
|
|
126
|
+
watch: [() => user.value?.id],
|
|
123
127
|
});
|
|
124
128
|
const enteredContentIds = computed(() => new Set(entries.value.map((e) => e.contentId)));
|
|
125
129
|
|
|
126
130
|
// Restrict the submit picker to the contest's eligible content types (if set).
|
|
127
131
|
const eligibleTypes = computed<string[]>(() => (c.value?.eligibleContentTypes as string[] | undefined) ?? []);
|
|
128
132
|
const submittableContent = computed(() => {
|
|
129
|
-
const items = (userContent.value?.items ?? []) as Array<{ id: string; title: string; type: string }>;
|
|
133
|
+
const items = (userContent.value?.items ?? []) as Array<{ id: string; title: string; type: string; coverImageUrl?: string | null }>;
|
|
130
134
|
if (eligibleTypes.value.length === 0) return items;
|
|
131
135
|
return items.filter((i) => eligibleTypes.value.includes(i.type));
|
|
132
136
|
});
|
|
133
137
|
|
|
138
|
+
// "Create a new project for this contest" — lands in the editor with the contest
|
|
139
|
+
// association in the URL; on publish it auto-enters (server pending-entry hook).
|
|
140
|
+
const newProjectType = computed(() => (eligibleTypes.value[0] ?? 'project'));
|
|
141
|
+
const createForContestLink = computed(() =>
|
|
142
|
+
user.value?.username
|
|
143
|
+
? `/u/${user.value.username}/${newProjectType.value}/new/edit?contest=${slug}`
|
|
144
|
+
: `/auth/login?redirect=/contests/${slug}`,
|
|
145
|
+
);
|
|
146
|
+
|
|
134
147
|
function copyLink(): void {
|
|
135
148
|
if (typeof window !== 'undefined' && window.navigator?.clipboard) {
|
|
136
149
|
window.navigator.clipboard.writeText(window.location.href);
|
|
@@ -186,22 +199,38 @@ async function withdrawEntry(entryId: string): Promise<void> {
|
|
|
186
199
|
</div>
|
|
187
200
|
<div class="cpub-submit-body">
|
|
188
201
|
<p class="cpub-submit-hint">
|
|
189
|
-
|
|
202
|
+
Pick one of your published projects to enter — or start a new one.
|
|
190
203
|
<template v-if="eligibleTypes.length"> This contest accepts: {{ eligibleTypes.join(', ') }}.</template>
|
|
191
204
|
</p>
|
|
192
|
-
<
|
|
193
|
-
<
|
|
194
|
-
|
|
205
|
+
<div class="cpub-submit-gallery" role="radiogroup" aria-label="Select a project to submit">
|
|
206
|
+
<NuxtLink :to="createForContestLink" class="cpub-submit-new" @click="showSubmitDialog = false">
|
|
207
|
+
<div class="cpub-submit-new-icon"><i class="fa-solid fa-plus"></i></div>
|
|
208
|
+
<span>Create a new {{ newProjectType }}</span>
|
|
209
|
+
<small>Enters automatically when you publish it</small>
|
|
210
|
+
</NuxtLink>
|
|
211
|
+
<button
|
|
195
212
|
v-for="item in submittableContent"
|
|
196
213
|
:key="item.id"
|
|
197
|
-
|
|
214
|
+
type="button"
|
|
215
|
+
role="radio"
|
|
216
|
+
:aria-checked="submitContentId === item.id"
|
|
217
|
+
class="cpub-submit-tile"
|
|
218
|
+
:class="{ selected: submitContentId === item.id, entered: enteredContentIds.has(item.id) }"
|
|
198
219
|
:disabled="enteredContentIds.has(item.id)"
|
|
220
|
+
@click="submitContentId = item.id"
|
|
199
221
|
>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
222
|
+
<span class="cpub-submit-tile-thumb">
|
|
223
|
+
<img v-if="item.coverImageUrl" :src="item.coverImageUrl" :alt="item.title" />
|
|
224
|
+
<i v-else class="fa-solid fa-cube"></i>
|
|
225
|
+
<span v-if="enteredContentIds.has(item.id)" class="cpub-submit-tile-badge">Entered</span>
|
|
226
|
+
<span v-else-if="submitContentId === item.id" class="cpub-submit-tile-check"><i class="fa-solid fa-check"></i></span>
|
|
227
|
+
</span>
|
|
228
|
+
<span class="cpub-submit-tile-title">{{ item.title }}</span>
|
|
229
|
+
<span class="cpub-submit-tile-type">{{ item.type }}</span>
|
|
230
|
+
</button>
|
|
231
|
+
</div>
|
|
203
232
|
<p v-if="submittableContent.length === 0" class="cpub-submit-hint" style="margin-top: 10px; margin-bottom: 0;">
|
|
204
|
-
No eligible published content
|
|
233
|
+
No eligible published content yet — use “Create a new {{ newProjectType }}” above to start one.
|
|
205
234
|
</p>
|
|
206
235
|
</div>
|
|
207
236
|
<div class="cpub-submit-footer">
|
|
@@ -334,14 +363,29 @@ async function withdrawEntry(entryId: string): Promise<void> {
|
|
|
334
363
|
<style scoped>
|
|
335
364
|
/* SUBMIT DIALOG */
|
|
336
365
|
.cpub-submit-overlay { position: fixed; inset: 0; z-index: 200; background: var(--color-surface-overlay-light); display: flex; align-items: center; justify-content: center; }
|
|
337
|
-
.cpub-submit-dialog { background: var(--surface); border: var(--border-width-default) solid var(--border); box-shadow: var(--shadow-xl); width:
|
|
366
|
+
.cpub-submit-dialog { background: var(--surface); border: var(--border-width-default) solid var(--border); box-shadow: var(--shadow-xl); width: 560px; max-width: 92vw; }
|
|
338
367
|
.cpub-submit-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: var(--border-width-default) solid var(--border); }
|
|
339
368
|
.cpub-submit-header h2 { font-size: 14px; font-weight: 700; }
|
|
340
369
|
.cpub-submit-close { background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: 14px; }
|
|
341
370
|
.cpub-submit-body { padding: 16px; }
|
|
342
371
|
.cpub-submit-hint { font-size: 12px; color: var(--text-dim); margin-bottom: 12px; }
|
|
343
|
-
|
|
344
|
-
.cpub-submit-
|
|
372
|
+
/* Gallery picker (replaces the old <select>): scrollable grid of content tiles. */
|
|
373
|
+
.cpub-submit-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; max-height: 50vh; overflow-y: auto; padding: 2px; }
|
|
374
|
+
.cpub-submit-new { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; text-align: center; min-height: 150px; border: var(--border-width-default) dashed var(--accent-border); background: var(--accent-bg); color: var(--accent); text-decoration: none; padding: 10px; }
|
|
375
|
+
.cpub-submit-new:hover { border-color: var(--accent); }
|
|
376
|
+
.cpub-submit-new-icon { font-size: 20px; }
|
|
377
|
+
.cpub-submit-new span { font-size: 12px; font-weight: 600; }
|
|
378
|
+
.cpub-submit-new small { font-size: 10px; color: var(--text-dim); line-height: 1.3; }
|
|
379
|
+
.cpub-submit-tile { display: flex; flex-direction: column; text-align: left; padding: 0; border: var(--border-width-default) solid var(--border); background: var(--surface); cursor: pointer; overflow: hidden; }
|
|
380
|
+
.cpub-submit-tile:hover:not(:disabled) { border-color: var(--accent); }
|
|
381
|
+
.cpub-submit-tile.selected { border-color: var(--accent); box-shadow: var(--shadow-accent); }
|
|
382
|
+
.cpub-submit-tile.entered { opacity: 0.5; cursor: not-allowed; }
|
|
383
|
+
.cpub-submit-tile-thumb { position: relative; aspect-ratio: 4 / 3; background: var(--surface2); display: flex; align-items: center; justify-content: center; color: var(--text-faint); overflow: hidden; }
|
|
384
|
+
.cpub-submit-tile-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
|
385
|
+
.cpub-submit-tile-badge { position: absolute; top: 4px; right: 4px; font-size: 9px; font-family: var(--font-mono); text-transform: uppercase; background: var(--surface); border: var(--border-width-default) solid var(--border); padding: 1px 5px; color: var(--text-dim); }
|
|
386
|
+
.cpub-submit-tile-check { position: absolute; top: 4px; right: 4px; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; background: var(--accent); color: var(--color-text-inverse); font-size: 10px; }
|
|
387
|
+
.cpub-submit-tile-title { font-size: 12px; font-weight: 600; padding: 6px 8px 2px; line-height: 1.3; }
|
|
388
|
+
.cpub-submit-tile-type { font-size: 9px; font-family: var(--font-mono); text-transform: uppercase; color: var(--text-faint); padding: 0 8px 8px; }
|
|
345
389
|
.cpub-submit-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px; border-top: var(--border-width-default) solid var(--border); }
|
|
346
390
|
|
|
347
391
|
/* LAYOUT */
|
|
@@ -35,6 +35,11 @@ useSeoMeta({
|
|
|
35
35
|
|
|
36
36
|
const title = ref('');
|
|
37
37
|
const hubFromQuery = (route.query.hub as string) || '';
|
|
38
|
+
// Captured at setup because handleStarterSubmit's history.replaceState drops the
|
|
39
|
+
// query. If set (?contest=slug from a contest's "create new project" button),
|
|
40
|
+
// the project auto-enters that contest on publish.
|
|
41
|
+
const contestFromQuery = (route.query.contest as string) || '';
|
|
42
|
+
const submitToast = useToast();
|
|
38
43
|
const metadata = ref<Record<string, unknown>>({
|
|
39
44
|
description: '',
|
|
40
45
|
slug: '',
|
|
@@ -278,6 +283,15 @@ async function handleStarterSubmit(): Promise<void> {
|
|
|
278
283
|
// --- Publish with validation ---
|
|
279
284
|
async function handlePublish(): Promise<void> {
|
|
280
285
|
await doPublish(validate);
|
|
286
|
+
// Auto-enter into the contest this project was created for (if any). The
|
|
287
|
+
// submit endpoint re-validates published status, so a premature call (publish
|
|
288
|
+
// blocked by validation) just fails harmlessly and is swallowed.
|
|
289
|
+
if (contestFromQuery && contentId.value && !isNew.value) {
|
|
290
|
+
try {
|
|
291
|
+
await $fetch(`/api/contests/${contestFromQuery}/entries`, { method: 'POST', body: { contentId: contentId.value } });
|
|
292
|
+
submitToast.success('Entered into the contest!');
|
|
293
|
+
} catch { /* non-blocking — user can submit manually from the contest page */ }
|
|
294
|
+
}
|
|
281
295
|
}
|
|
282
296
|
|
|
283
297
|
// --- Preview mode ---
|