@commonpub/layer 0.47.0 → 0.48.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.47.0",
3
+ "version": "0.48.0",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -55,10 +55,10 @@
55
55
  "zod": "^4.3.6",
56
56
  "@commonpub/auth": "0.8.0",
57
57
  "@commonpub/config": "0.18.0",
58
- "@commonpub/editor": "0.7.11",
58
+ "@commonpub/learning": "0.5.2",
59
59
  "@commonpub/docs": "0.6.3",
60
60
  "@commonpub/explainer": "0.7.15",
61
- "@commonpub/learning": "0.5.2",
61
+ "@commonpub/editor": "0.7.11",
62
62
  "@commonpub/protocol": "0.13.0",
63
63
  "@commonpub/schema": "0.26.0",
64
64
  "@commonpub/server": "2.73.0",
@@ -13,6 +13,20 @@ function cardBlurb(c: { subheading?: string | null; description?: string | null
13
13
  }
14
14
 
15
15
  const config = useRuntimeConfig();
16
+
17
+ // Contest banner thumbnail — proxy cross-origin images through our server
18
+ // (same pattern as ContentCard) for caching + faster loads.
19
+ function coverFor(url: string | null | undefined): string | null {
20
+ if (!url) return null;
21
+ const siteDomain = (config.public?.domain as string) || '';
22
+ try {
23
+ if (siteDomain && !url.includes(siteDomain)) {
24
+ return `/api/image-proxy?url=${encodeURIComponent(url)}&w=600`;
25
+ }
26
+ } catch { /* invalid URL — use as-is */ }
27
+ return url;
28
+ }
29
+
16
30
  const contestCreation = config.public.contestCreation as string || 'admin';
17
31
  const canCreateContest = computed(() => {
18
32
  if (!isAuthenticated.value) return false;
@@ -31,30 +45,45 @@ const canCreateContest = computed(() => {
31
45
  </NuxtLink>
32
46
  </div>
33
47
  <div v-if="contests?.items?.length" class="cpub-grid-3">
34
- <div v-for="contest in contests.items" :key="contest.id" class="cpub-card">
35
- <div class="cpub-card-body">
36
- <span class="cpub-badge" :class="{
48
+ <NuxtLink
49
+ v-for="contest in contests.items"
50
+ :key="contest.id"
51
+ :to="`/contests/${contest.slug}`"
52
+ class="cpub-card cpub-contest-card"
53
+ >
54
+ <!-- Banner thumbnail (contest.bannerUrl) with trophy fallback + status badge overlay -->
55
+ <div class="cpub-contest-thumb">
56
+ <img
57
+ v-if="coverFor(contest.bannerUrl)"
58
+ :src="coverFor(contest.bannerUrl)!"
59
+ :alt="contest.title"
60
+ class="cpub-contest-cover"
61
+ loading="lazy"
62
+ />
63
+ <template v-else>
64
+ <div class="cpub-contest-thumb-grid" />
65
+ <i class="fa-solid fa-trophy cpub-contest-thumb-icon" />
66
+ </template>
67
+ <span class="cpub-badge cpub-contest-thumb-badge" :class="{
37
68
  'cpub-badge-green': contest.status === 'active',
38
69
  'cpub-badge-yellow': contest.status === 'upcoming',
39
70
  'cpub-badge-accent': contest.status === 'judging',
40
71
  'cpub-badge-red': contest.status === 'completed' || contest.status === 'cancelled',
41
72
  }">{{ contest.status }}</span>
42
- <h3 style="font-size: 15px; font-weight: 600; margin: 8px 0">
43
- <NuxtLink :to="`/contests/${contest.slug}`" style="color: var(--text); text-decoration: none">
44
- {{ contest.title }}
45
- </NuxtLink>
46
- </h3>
47
- <p v-if="cardBlurb(contest)" class="cpub-contest-card-blurb" style="font-size: 12px; color: var(--text-dim); margin-bottom: 12px">
73
+ </div>
74
+ <div class="cpub-card-body">
75
+ <h3 class="cpub-contest-card-title">{{ contest.title }}</h3>
76
+ <p v-if="cardBlurb(contest)" class="cpub-contest-card-blurb">
48
77
  {{ cardBlurb(contest) }}
49
78
  </p>
50
79
  <div v-if="contest.endDate" style="margin-top: 8px">
51
80
  <CountdownTimer :target-date="contest.endDate" />
52
81
  </div>
53
- <div style="display: flex; align-items: center; gap: 8px; margin-top: 12px; font-size: 11px; color: var(--text-faint); font-family: var(--font-mono)">
82
+ <div class="cpub-contest-card-meta">
54
83
  <span><i class="fa-solid fa-users"></i> {{ contest.entryCount }} entries</span>
55
84
  </div>
56
85
  </div>
57
- </div>
86
+ </NuxtLink>
58
87
  </div>
59
88
  <div v-else class="cpub-empty-state">
60
89
  <div class="cpub-empty-state-icon"><i class="fa-solid fa-trophy"></i></div>
@@ -71,7 +100,42 @@ const canCreateContest = computed(() => {
71
100
  .cpub-card:hover { box-shadow: var(--shadow-lg); transform: translate(-1px, -1px); }
72
101
  .cpub-card-body { padding: 16px; }
73
102
 
103
+ /* Whole card is a link */
104
+ .cpub-contest-card { display: block; text-decoration: none; color: inherit; }
105
+
106
+ /* Banner thumbnail — wide (banner-shaped), cover-cropped, with a grid+trophy
107
+ fallback when a contest has no bannerUrl. */
108
+ .cpub-contest-thumb {
109
+ position: relative;
110
+ aspect-ratio: 16 / 9;
111
+ background: var(--surface2);
112
+ border-bottom: var(--border-width-default) solid var(--border);
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: center;
116
+ overflow: hidden;
117
+ }
118
+ .cpub-contest-cover { width: 100%; height: 100%; object-fit: cover; display: block; }
119
+ .cpub-contest-thumb-grid {
120
+ position: absolute;
121
+ inset: 0;
122
+ background-image:
123
+ linear-gradient(var(--border2) 1px, transparent 1px),
124
+ linear-gradient(90deg, var(--border2) 1px, transparent 1px);
125
+ background-size: 20px 20px;
126
+ opacity: 0.25;
127
+ }
128
+ .cpub-contest-thumb-icon { position: relative; z-index: 1; font-size: 36px; color: var(--accent); opacity: 0.45; }
129
+ .cpub-contest-thumb-badge { position: absolute; top: 10px; left: 10px; z-index: 2; box-shadow: var(--shadow-sm); }
130
+ .cpub-contest-card:hover .cpub-contest-cover { opacity: 0.92; }
131
+
132
+ .cpub-contest-card-title { font-size: 15px; font-weight: 600; margin: 0 0 6px; color: var(--text); }
133
+ .cpub-contest-card-meta { display: flex; align-items: center; gap: 8px; margin-top: 12px; font-size: 11px; color: var(--text-faint); font-family: var(--font-mono); }
134
+
74
135
  .cpub-contest-card-blurb {
136
+ font-size: 12px;
137
+ color: var(--text-dim);
138
+ margin-bottom: 12px;
75
139
  display: -webkit-box;
76
140
  -webkit-line-clamp: 3;
77
141
  line-clamp: 3;