@mundogamernetwork/shared-ui 1.3.3 → 1.3.7

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.
@@ -34,6 +34,23 @@ body:has(.kit-press) {
34
34
  color: var(--kit-muted, #8b92a0);
35
35
  font-size: 1rem;
36
36
  }
37
+ .kit-state--not-found {
38
+ min-height: 100vh;
39
+ justify-content: flex-start;
40
+ padding-top: 15vh;
41
+ width: 100%;
42
+ }
43
+ .kit-state__desc {
44
+ max-width: 420px;
45
+ text-align: center;
46
+ margin: -8px 0 4px;
47
+ font-size: 0.9rem;
48
+ line-height: 1.5;
49
+ }
50
+ .kit-more--not-found {
51
+ width: 100%;
52
+ margin-top: 40px;
53
+ }
37
54
  .kit-state h2 {
38
55
  font-size: 1.5rem;
39
56
  color: var(--kit-text, #f4f6fa);
@@ -11,6 +11,10 @@
11
11
 
12
12
  <!-- Bundle Form -->
13
13
  <div v-if="showForm" class="bundle-form">
14
+ <div class="visibility-note">
15
+ <MGIcon icon="info" />
16
+ <span>{{ $t("media_kit.bundles.visibility_note") }}</span>
17
+ </div>
14
18
  <div class="form-group">
15
19
  <label>{{ $t("media_kit.bundles.bundle_name") }}</label>
16
20
  <input v-model="form.name" type="text" :placeholder="$t('media_kit.bundles.name_placeholder')" />
@@ -105,6 +109,20 @@
105
109
  </div>
106
110
  </div>
107
111
 
112
+ <label class="visibility-checkbox" :class="{ disabled: !isSubscriber }">
113
+ <input
114
+ type="checkbox"
115
+ :checked="showPublicly"
116
+ :disabled="!isSubscriber"
117
+ @change="onToggleVisibility(($event.target as HTMLInputElement).checked)"
118
+ />
119
+ <span>{{ $t("media_kit.bundles.show_publicly") }}</span>
120
+ <button v-if="!isSubscriber" type="button" class="upgrade-link" @click.prevent="emit('paywall', 'rates')">
121
+ <MGIcon icon="lock" />
122
+ {{ $t("media_kit.editor.upgrade_to_show") }}
123
+ </button>
124
+ </label>
125
+
108
126
  <div class="form-actions">
109
127
  <button class="btn-cancel" @click="cancelForm">{{ $t("media_kit.editor.cancel") }}</button>
110
128
  <button class="btn-save" @click="saveBundle" :disabled="saving || form.rate_card_ids.length < 2">
@@ -186,19 +204,32 @@ const props = withDefaults(
186
204
  availableRateCards: RateCard[];
187
205
  influencerProInfoId: number;
188
206
  isEnabled?: boolean;
207
+ /** Whether the account has an active paid subscription — gates public visibility only, not creation. */
208
+ isSubscriber?: boolean;
209
+ /** Current value of the account-wide "show bundles publicly" setting. */
210
+ showPublicly?: boolean;
189
211
  }>(),
190
212
  {
191
213
  isEnabled: true,
214
+ isSubscriber: false,
215
+ showPublicly: true,
192
216
  }
193
217
  );
194
218
 
195
219
  const emit = defineEmits<{
196
220
  (e: "refresh"): void;
197
221
  (e: "error", message: string): void;
198
- /** Bundles remain a premium feature host app decides how to react (e.g. show its own paywall modal). */
222
+ /** Fired when a non-subscriber clicks the upgrade CTA next to the disabled checkbox, or when isEnabled is false at save time. */
199
223
  (e: "paywall", feature: string): void;
224
+ /** Fired when the "show publicly" checkbox changes (only reachable while isSubscriber). */
225
+ (e: "toggle-visibility", value: boolean): void;
200
226
  }>();
201
227
 
228
+ function onToggleVisibility(value: boolean) {
229
+ if (!props.isSubscriber) return;
230
+ emit('toggle-visibility', value);
231
+ }
232
+
202
233
  const showForm = ref(false);
203
234
  const editing = ref<number | null>(null);
204
235
  const saving = ref(false);
@@ -216,6 +247,53 @@ const defaultForm = {
216
247
 
217
248
  const form = ref({ ...defaultForm, quantities: {} });
218
249
 
250
+ // --- Draft persistence (new-item only) --------------------------------------
251
+ // Guards against losing a half-filled form on accidental navigation/refresh.
252
+ // Scoped per profile so switching accounts in the same browser can't leak a
253
+ // draft between them. Only the "new item" flow is persisted — editing an
254
+ // existing bundle already has server data as the source of truth.
255
+ const DRAFT_KEY = computed(() => `mgn_bundle_draft_${props.influencerProInfoId}`);
256
+
257
+ function loadDraft() {
258
+ if (typeof window === 'undefined') return null;
259
+ try {
260
+ const raw = window.localStorage.getItem(DRAFT_KEY.value);
261
+ return raw ? JSON.parse(raw) : null;
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
266
+
267
+ function saveDraft() {
268
+ if (typeof window === 'undefined' || editing.value) return;
269
+ try {
270
+ window.localStorage.setItem(DRAFT_KEY.value, JSON.stringify(form.value));
271
+ } catch {
272
+ // storage unavailable (private browsing, quota) — draft just won't persist
273
+ }
274
+ }
275
+
276
+ function clearDraft() {
277
+ if (typeof window === 'undefined') return;
278
+ try {
279
+ window.localStorage.removeItem(DRAFT_KEY.value);
280
+ } catch {
281
+ // ignore
282
+ }
283
+ }
284
+
285
+ watch(form, () => {
286
+ if (!editing.value) saveDraft();
287
+ }, { deep: true });
288
+
289
+ onMounted(() => {
290
+ const draft = loadDraft();
291
+ if (draft) {
292
+ form.value = { ...defaultForm, quantities: {}, ...draft };
293
+ showForm.value = true;
294
+ }
295
+ });
296
+
219
297
  function qtyFor(id: number): number {
220
298
  return form.value.quantities[id] || 1;
221
299
  }
@@ -254,6 +332,7 @@ function cancelForm() {
254
332
  showForm.value = false;
255
333
  editing.value = null;
256
334
  form.value = { ...defaultForm, rate_card_ids: [], quantities: {} };
335
+ clearDraft();
257
336
  }
258
337
 
259
338
  function editBundle(bundle: Bundle) {
@@ -353,7 +432,58 @@ async function removeBundle(id: number) {
353
432
  margin: -8px 0 0;
354
433
  }
355
434
 
435
+ .visibility-checkbox {
436
+ display: flex;
437
+ align-items: center;
438
+ gap: 8px;
439
+ font-size: 13px;
440
+ color: #E4E6EB;
441
+ cursor: pointer;
442
+
443
+ input[type="checkbox"] {
444
+ width: 16px;
445
+ height: 16px;
446
+ cursor: pointer;
447
+ }
448
+
449
+ &.disabled {
450
+ cursor: not-allowed;
451
+ color: #7A7D89;
452
+
453
+ input[type="checkbox"] { cursor: not-allowed; }
454
+ }
455
+
456
+ .upgrade-link {
457
+ display: inline-flex;
458
+ align-items: center;
459
+ gap: 4px;
460
+ margin-left: 4px;
461
+ padding: 3px 8px;
462
+ background: none;
463
+ border: 1px solid #FDB215;
464
+ color: #FDB215;
465
+ font-size: 12px;
466
+ cursor: pointer;
467
+
468
+ &:hover { background: rgba(253, 178, 21, 0.1); }
469
+ }
470
+ }
471
+
356
472
  .bundle-form {
473
+ .visibility-note {
474
+ display: flex;
475
+ align-items: flex-start;
476
+ gap: 8px;
477
+ padding: 10px 12px;
478
+ background: rgba(255, 255, 255, 0.04);
479
+ border: 1px solid rgba(255, 255, 255, 0.08);
480
+ border-radius: 4px;
481
+ font-size: 13px;
482
+ color: #B7BAC4;
483
+
484
+ svg { flex-shrink: 0; margin-top: 2px; }
485
+ }
486
+
357
487
  background: #1E1E22;
358
488
  padding: 20px;
359
489
  display: flex;
@@ -8,6 +8,11 @@
8
8
  </div>
9
9
 
10
10
  <div v-if="showForm" class="rate-card-form">
11
+ <div class="visibility-note">
12
+ <MGIcon icon="info" />
13
+ <span>{{ $t("media_kit.editor.visibility_note") }}</span>
14
+ </div>
15
+
11
16
  <div class="form-row">
12
17
  <div class="form-group">
13
18
  <label>{{ $t("media_kit.editor.content_format") }}</label>
@@ -79,6 +84,20 @@
79
84
  <textarea v-model="form.description" rows="3"
80
85
  :placeholder="$t('media_kit.editor.description_placeholder')"></textarea>
81
86
  </div>
87
+ <label class="visibility-checkbox" :class="{ disabled: !isSubscriber }">
88
+ <input
89
+ type="checkbox"
90
+ :checked="showPublicly"
91
+ :disabled="!isSubscriber"
92
+ @change="onToggleVisibility(($event.target as HTMLInputElement).checked)"
93
+ />
94
+ <span>{{ $t("media_kit.editor.show_publicly") }}</span>
95
+ <button v-if="!isSubscriber" type="button" class="upgrade-link" @click.prevent="emit('paywall', 'rates')">
96
+ <MGIcon icon="lock" />
97
+ {{ $t("media_kit.editor.upgrade_to_show") }}
98
+ </button>
99
+ </label>
100
+
82
101
  <div class="form-actions">
83
102
  <button class="btn-cancel" @click="cancelForm">{{ $t("media_kit.editor.cancel") }}</button>
84
103
  <button class="btn-save" @click="saveRateCard" :disabled="saving">
@@ -143,19 +162,34 @@ const props = withDefaults(
143
162
  commissionPercent?: number;
144
163
  /** Current plan name, used in the fee label. */
145
164
  planName?: string;
165
+ /** Whether the account has an active paid subscription — gates public visibility only, not creation. */
166
+ isSubscriber?: boolean;
167
+ /** Current value of the account-wide "show rate cards publicly" setting. */
168
+ showPublicly?: boolean;
146
169
  }>(),
147
170
  {
148
171
  isEnabled: true,
149
172
  commissionPercent: 20,
150
173
  planName: "Free",
174
+ isSubscriber: false,
175
+ showPublicly: true,
151
176
  }
152
177
  );
153
178
 
154
179
  const emit = defineEmits<{
155
180
  (e: "refresh"): void;
156
181
  (e: "error", message: string): void;
182
+ /** Fired when the "show publicly" checkbox changes (only reachable while isSubscriber). */
183
+ (e: "toggle-visibility", value: boolean): void;
184
+ /** Fired when a non-subscriber clicks the upgrade CTA next to the disabled checkbox. */
185
+ (e: "paywall", feature: string): void;
157
186
  }>();
158
187
 
188
+ function onToggleVisibility(value: boolean) {
189
+ if (!props.isSubscriber) return;
190
+ emit('toggle-visibility', value);
191
+ }
192
+
159
193
  const showForm = ref(false);
160
194
  const editing = ref<number | null>(null);
161
195
  const saving = ref(false);
@@ -173,10 +207,58 @@ const defaultForm = {
173
207
 
174
208
  const form = ref({ ...defaultForm });
175
209
 
210
+ // --- Draft persistence (new-item only) --------------------------------------
211
+ // Guards against losing a half-filled form on accidental navigation/refresh.
212
+ // Scoped per profile so switching accounts in the same browser can't leak a
213
+ // draft between them. Only the "new item" flow is persisted — editing an
214
+ // existing card already has server data as the source of truth.
215
+ const DRAFT_KEY = computed(() => `mgn_rate_card_draft_${props.influencerProInfoId}`);
216
+
217
+ function loadDraft() {
218
+ if (typeof window === 'undefined') return null;
219
+ try {
220
+ const raw = window.localStorage.getItem(DRAFT_KEY.value);
221
+ return raw ? JSON.parse(raw) : null;
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ function saveDraft() {
228
+ if (typeof window === 'undefined' || editing.value) return;
229
+ try {
230
+ window.localStorage.setItem(DRAFT_KEY.value, JSON.stringify(form.value));
231
+ } catch {
232
+ // storage unavailable (private browsing, quota) — draft just won't persist
233
+ }
234
+ }
235
+
236
+ function clearDraft() {
237
+ if (typeof window === 'undefined') return;
238
+ try {
239
+ window.localStorage.removeItem(DRAFT_KEY.value);
240
+ } catch {
241
+ // ignore
242
+ }
243
+ }
244
+
245
+ watch(form, () => {
246
+ if (!editing.value) saveDraft();
247
+ }, { deep: true });
248
+
249
+ onMounted(() => {
250
+ const draft = loadDraft();
251
+ if (draft) {
252
+ form.value = { ...defaultForm, ...draft };
253
+ showForm.value = true;
254
+ }
255
+ });
256
+
176
257
  const cancelForm = () => {
177
258
  showForm.value = false;
178
259
  editing.value = null;
179
260
  form.value = { ...defaultForm };
261
+ clearDraft();
180
262
  };
181
263
 
182
264
  const editCard = (card: RateCard) => {
@@ -284,6 +366,57 @@ const netDisplay = computed(() => {
284
366
  flex-direction: column;
285
367
  gap: 16px;
286
368
 
369
+ .visibility-note {
370
+ display: flex;
371
+ align-items: flex-start;
372
+ gap: 8px;
373
+ padding: 10px 12px;
374
+ background: rgba(255, 255, 255, 0.04);
375
+ border: 1px solid rgba(255, 255, 255, 0.08);
376
+ border-radius: 4px;
377
+ font-size: 13px;
378
+ color: #B7BAC4;
379
+
380
+ svg { flex-shrink: 0; margin-top: 2px; }
381
+ }
382
+
383
+ .visibility-checkbox {
384
+ display: flex;
385
+ align-items: center;
386
+ gap: 8px;
387
+ font-size: 13px;
388
+ color: #E4E6EB;
389
+ cursor: pointer;
390
+
391
+ input[type="checkbox"] {
392
+ width: 16px;
393
+ height: 16px;
394
+ cursor: pointer;
395
+ }
396
+
397
+ &.disabled {
398
+ cursor: not-allowed;
399
+ color: #7A7D89;
400
+
401
+ input[type="checkbox"] { cursor: not-allowed; }
402
+ }
403
+
404
+ .upgrade-link {
405
+ display: inline-flex;
406
+ align-items: center;
407
+ gap: 4px;
408
+ margin-left: 4px;
409
+ padding: 3px 8px;
410
+ background: none;
411
+ border: 1px solid #FDB215;
412
+ color: #FDB215;
413
+ font-size: 12px;
414
+ cursor: pointer;
415
+
416
+ &:hover { background: rgba(253, 178, 21, 0.1); }
417
+ }
418
+ }
419
+
287
420
  .mgn-fee-note {
288
421
  display: flex;
289
422
  flex-direction: column;
@@ -120,7 +120,7 @@ const detailRoute = computed(() =>
120
120
  </div>
121
121
 
122
122
  <!-- Tier-locked badge -->
123
- <div v-else-if="card.is_tier_locked && card.access_tier !== 'free'" class="kc-card__exclusive-badge">
123
+ <div v-else-if="card.is_tier_locked && card.access_tier !== 'free' && !userCanAccess" class="kc-card__exclusive-badge">
124
124
  <i class="fa-solid fa-crown" /> {{ cardTierLabel(card.access_tier) }}
125
125
  </div>
126
126
 
package/locales/de.json CHANGED
@@ -45,6 +45,8 @@
45
45
  "upcoming": "Coming soon",
46
46
  "back": "Back",
47
47
  "search_placeholder": "Search campaigns...",
48
+ "sort_recent": "Neueste",
49
+ "sort_popular": "Beliebteste",
48
50
  "seo": {
49
51
  "title_generic": "Key Campaigns"
50
52
  },
package/locales/en.json CHANGED
@@ -45,6 +45,8 @@
45
45
  "upcoming": "Coming soon",
46
46
  "back": "Back",
47
47
  "search_placeholder": "Search campaigns...",
48
+ "sort_recent": "Most recent",
49
+ "sort_popular": "Most popular",
48
50
  "seo": {
49
51
  "title_generic": "Key Campaigns"
50
52
  },
@@ -45,6 +45,8 @@
45
45
  "upcoming": "Coming soon",
46
46
  "back": "Back",
47
47
  "search_placeholder": "Search campaigns...",
48
+ "sort_recent": "Mais recentes",
49
+ "sort_popular": "Mais populares",
48
50
  "seo": {
49
51
  "title_generic": "Key Campaigns"
50
52
  },
@@ -133,7 +135,7 @@
133
135
  "rp_label": "Redeemed by",
134
136
  "who_redeem": "Who redeemed",
135
137
  "created_by": "Published by",
136
- "no_data": "Esta campanha ainda nao foi associada a um jogo no sistema",
138
+ "no_data": "Esta campanha ainda não foi associada a um jogo no sistema",
137
139
  "title": "Campanha",
138
140
  "about": "Sobre o jogo",
139
141
  "screenshot_alt": "Captura de tela do jogo",
package/locales/ro.json CHANGED
@@ -45,6 +45,8 @@
45
45
  "upcoming": "Coming soon",
46
46
  "back": "Back",
47
47
  "search_placeholder": "Search campaigns...",
48
+ "sort_recent": "Cele mai recente",
49
+ "sort_popular": "Cele mai populare",
48
50
  "seo": {
49
51
  "title_generic": "Key Campaigns"
50
52
  },
@@ -133,7 +135,7 @@
133
135
  "rp_label": "Redeemed by",
134
136
  "who_redeem": "Who redeemed",
135
137
  "created_by": "Published by",
136
- "no_data": "Aceasta campanie nu este inca asociata cu un joc in sistem",
138
+ "no_data": "Această campanie nu este încă asociată cu un joc în sistem",
137
139
  "title": "Campanie",
138
140
  "about": "Despre joc",
139
141
  "screenshot_alt": "Captură de ecran a jocului",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.3.3",
3
+ "version": "1.3.7",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -75,14 +75,24 @@
75
75
  </template>
76
76
  </div>
77
77
  <div class="bar">
78
- <input
79
- type="text"
80
- v-model="searchTerm"
81
- :placeholder="$t('keys.campaigns.search_placeholder')"
82
- />
83
- <button type="button">
84
- <MGIcon icon="search" />
85
- </button>
78
+ <select
79
+ class="platform-select sort-mode-select"
80
+ :value="sortMode"
81
+ @change="setSortMode(($event.target as HTMLSelectElement).value as 'recent' | 'popular')"
82
+ >
83
+ <option value="recent">{{ $t("keys.campaigns.sort_recent") }}</option>
84
+ <option value="popular">{{ $t("keys.campaigns.sort_popular") }}</option>
85
+ </select>
86
+ <div class="search-box">
87
+ <input
88
+ type="text"
89
+ v-model="searchTerm"
90
+ :placeholder="$t('keys.campaigns.search_placeholder')"
91
+ />
92
+ <button type="button">
93
+ <MGIcon icon="search" />
94
+ </button>
95
+ </div>
86
96
  </div>
87
97
  </div>
88
98
 
@@ -134,7 +144,7 @@
134
144
  </div>
135
145
 
136
146
  <div
137
- v-else-if="card.is_tier_locked && card.access_tier !== 'free'"
147
+ v-else-if="card.is_tier_locked && card.access_tier !== 'free' && !card.user_can_access"
138
148
  class="exclusive-badge"
139
149
  >
140
150
  <i class="fa-solid fa-crown" /> {{ cardTierLabel(card.access_tier) }}
@@ -405,6 +415,7 @@ const platformList = ref<Array<{ id: number; name: string; slug: string }>>([])
405
415
  const activeFilter = ref<number | null>(null)
406
416
  const activePlatformFilter = ref<number | null>(null)
407
417
  const redeemedStatusFilter = ref<number | null>(null)
418
+ const sortMode = ref<"recent" | "popular">("recent")
408
419
 
409
420
  // ------------------------------------------------------------------
410
421
  // Data
@@ -513,7 +524,10 @@ async function loadFeaturedCampaigns() {
513
524
  try {
514
525
  const response = await campaignsStore.fetchFeaturedCampaigns(mgPlatform)
515
526
  sliderImages.value = (response.data || [])
516
- .filter((item: any) => truthyFlag(item.featured) || truthyFlag(item.highlight))
527
+ // Featured/highlight alone isn't enough — a campaign that's ended or
528
+ // sold out but still flagged featured must not show in the hero
529
+ // carousel. is_open already accounts for status/dates/available_count.
530
+ .filter((item: any) => (truthyFlag(item.featured) || truthyFlag(item.highlight)) && truthyFlag(item.is_open))
517
531
  .map((item: any) => ({
518
532
  id: item.id,
519
533
  cover: item.game?.url_banner_src || item.cover_src,
@@ -542,6 +556,7 @@ async function loadCampaigns(page = 1) {
542
556
  per_page: perPage.value,
543
557
  sort: "id",
544
558
  order: "desc",
559
+ sort_mode: sortMode.value,
545
560
  }
546
561
 
547
562
  if (activeFilter.value !== null) params["filter[type_id]"] = activeFilter.value
@@ -682,6 +697,14 @@ function setPlatformFilter(platformId: number | null) {
682
697
  loadCampaigns(1)
683
698
  }
684
699
 
700
+ function setSortMode(mode: "recent" | "popular") {
701
+ if (sortMode.value === mode) return
702
+ sortMode.value = mode
703
+ currentPage.value = 1
704
+ hasMoreItems.value = true
705
+ loadCampaigns(1)
706
+ }
707
+
685
708
  const loadMoreItems = async () => {
686
709
  if (!hasMoreItems.value || loading.value || loadingMore.value) return
687
710
  await loadCampaigns(currentPage.value + 1)
@@ -837,7 +860,7 @@ onUnmounted(() => {
837
860
 
838
861
  .keys-badge {
839
862
  position: absolute;
840
- bottom: 8px;
863
+ top: 8px;
841
864
  right: 8px;
842
865
  background: var(--key-accent-dark, var(--key-accent-bg, rgba(107, 21, 172, 0.9)));
843
866
  color: #fff;
@@ -1092,6 +1115,18 @@ onUnmounted(() => {
1092
1115
  }
1093
1116
 
1094
1117
  .bar {
1118
+ position: relative;
1119
+ display: flex;
1120
+ align-items: center;
1121
+ gap: 8px;
1122
+
1123
+ .sort-mode-select {
1124
+ flex-shrink: 0;
1125
+ height: 44px;
1126
+ }
1127
+ }
1128
+
1129
+ .search-box {
1095
1130
  position: relative;
1096
1131
  display: flex;
1097
1132
  width: 339px;
@@ -1270,6 +1305,7 @@ onUnmounted(() => {
1270
1305
  flex-direction: column;
1271
1306
  gap: 12px;
1272
1307
  .bar { width: 100%; }
1308
+ .search-box { flex: 1; width: auto; }
1273
1309
  }
1274
1310
  .cards {
1275
1311
  flex-direction: column;
@@ -205,8 +205,33 @@ useSeoMeta({
205
205
  <template>
206
206
  <div class="kit-press" :data-tpl="tpl" :data-layout="layout">
207
207
  <div v-if="loading" class="kit-state">{{ $t('kit.loading') }}</div>
208
- <div v-else-if="error || !kit" class="kit-state">
208
+ <div v-else-if="error || !kit" class="kit-state kit-state--not-found">
209
209
  <h2>{{ $t('kit.press.not_found') }}</h2>
210
+ <p class="kit-state__desc">{{ $t('kit.press.not_found_desc') }}</p>
211
+ <a :href="`${agencyBase}/presskit`" target="_blank" rel="noopener" class="kit-footer__cta">
212
+ {{ $t('kit.press.create_yours_cta') }}
213
+ </a>
214
+
215
+ <section v-if="moreKits.length" class="kit-more kit-more--not-found">
216
+ <h2 class="kit-more__title">{{ $t('kit.press.more_games') }}</h2>
217
+ <div class="kit-more__track">
218
+ <a
219
+ v-for="k in moreKits"
220
+ :key="k.slug"
221
+ :href="`/presskit/game/${k.slug}`"
222
+ class="kit-more__card"
223
+ >
224
+ <div
225
+ class="kit-more__cover"
226
+ :style="k.cover_image_url ? { backgroundImage: `url(${k.cover_image_url})` } : {}"
227
+ >
228
+ <div v-if="!k.cover_image_url" class="kit-more__cover-fallback">{{ k.name?.charAt(0) }}</div>
229
+ </div>
230
+ <div class="kit-more__name">{{ k.name }}</div>
231
+ <div v-if="k.developer" class="kit-more__dev">{{ k.developer }}</div>
232
+ </a>
233
+ </div>
234
+ </section>
210
235
  </div>
211
236
 
212
237
  <template v-else>