@xenterprises/nuxt-x-marketing 1.0.1 → 1.0.2
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/.claude/settings.local.json +23 -0
- package/.playground/app.vue +12 -6
- package/.playground/package-lock.json +6 -0
- package/.playground/pages/index.vue +2 -2
- package/.playground/pages/layout/x-legal.vue +12 -12
- package/LICENSE +60 -0
- package/README.md +210 -64
- package/app/app.config.ts +2 -72
- package/app/components/X/Footer/index.vue +3 -4
- package/app/components/X/Mark/Affiliate/ComparisonTable.vue +382 -0
- package/app/components/X/Mark/Affiliate/Disclosure.vue +138 -0
- package/app/components/X/Mark/Affiliate/ProductCard.vue +431 -0
- package/app/components/X/Mark/Affiliate/ProductDetail.vue +410 -0
- package/app/components/X/Mark/Affiliate/ProductGrid.vue +101 -0
- package/app/components/X/Mark/Blog/Author.vue +26 -20
- package/app/components/X/Mark/Blog/Card.vue +28 -15
- package/app/components/X/Mark/Blog/Detail.vue +177 -169
- package/app/components/X/Mark/Blog/List.vue +17 -16
- package/app/components/X/Mark/CTA/index.vue +74 -70
- package/app/components/X/Mark/Card/Promo.vue +149 -139
- package/app/components/X/Mark/Directory/CategoryHero.vue +124 -0
- package/app/components/X/Mark/Directory/ListingCard.vue +336 -0
- package/app/components/X/Mark/Directory/ListingDetail.vue +306 -0
- package/app/components/X/Mark/Directory/ListingGrid.vue +190 -0
- package/app/components/X/Mark/Directory/SubmitCTA.vue +201 -0
- package/app/components/X/Mark/FAQ/index.vue +191 -0
- package/app/components/X/Mark/Features/index.vue +87 -35
- package/app/components/X/Mark/Hero/index.vue +41 -38
- package/app/components/X/Mark/Layout/AnnouncementBar.vue +20 -28
- package/app/components/X/Mark/Modal/Chat.vue +124 -124
- package/app/components/X/Mark/Modal/Video.vue +46 -36
- package/app/components/X/Mark/Pricing/Comparison.vue +76 -21
- package/app/components/X/Mark/Pricing/Plans.vue +75 -25
- package/app/components/X/Mark/SocialProof/Testimonials.vue +89 -21
- package/app/components/X/Mark/SocialProof/Toast.vue +146 -138
- package/app/components/X/X/Footer/index.vue +101 -0
- package/app/components/X/X/Header/Nav/index.vue +62 -0
- package/app/components/X/X/Header/index.vue +72 -0
- package/app/components/X/X/Legal/index.vue +94 -0
- package/app/composables/useParallax.ts +32 -19
- package/app/composables/useScrollReveal.ts +20 -13
- package/app/pages/index.vue +230 -28
- package/componentList.json +1856 -238
- package/package.json +2 -1
- package/screenshots/playground-home.png +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section
|
|
3
|
+
class="relative overflow-hidden bg-neutral-900 dark:bg-neutral-950 py-16 sm:py-20 lg:py-24"
|
|
4
|
+
>
|
|
5
|
+
<!-- Subtle background pattern -->
|
|
6
|
+
<XMarkPatternBg
|
|
7
|
+
pattern="dots"
|
|
8
|
+
:opacity="0.04"
|
|
9
|
+
class="absolute inset-0 pointer-events-none"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
<!-- Gradient overlay -->
|
|
13
|
+
<div
|
|
14
|
+
class="absolute inset-0 bg-gradient-to-b from-primary-900/20 via-transparent to-neutral-900/60 pointer-events-none"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
class="relative z-10 max-w-3xl mx-auto px-4 sm:px-6 text-center xFadeUp"
|
|
19
|
+
>
|
|
20
|
+
<!-- Category Icon -->
|
|
21
|
+
<div
|
|
22
|
+
v-if="category.icon"
|
|
23
|
+
class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary-500/20 border border-primary-500/30 mb-6"
|
|
24
|
+
>
|
|
25
|
+
<UIcon :name="category.icon" class="w-8 h-8 text-primary-400" />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Category Name -->
|
|
29
|
+
<h1 class="xText-headline text-white">
|
|
30
|
+
{{ category.name }}
|
|
31
|
+
</h1>
|
|
32
|
+
|
|
33
|
+
<!-- Count -->
|
|
34
|
+
<p
|
|
35
|
+
v-if="category.count !== undefined"
|
|
36
|
+
class="mt-3 text-sm font-medium text-neutral-400 uppercase tracking-wider"
|
|
37
|
+
>
|
|
38
|
+
{{ category.count.toLocaleString() }} listing{{
|
|
39
|
+
category.count !== 1 ? "s" : ""
|
|
40
|
+
}}
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<!-- Description -->
|
|
44
|
+
<p
|
|
45
|
+
v-if="category.description"
|
|
46
|
+
class="mt-4 text-lg text-neutral-300 leading-relaxed max-w-xl mx-auto"
|
|
47
|
+
>
|
|
48
|
+
{{ category.description }}
|
|
49
|
+
</p>
|
|
50
|
+
|
|
51
|
+
<!-- Search Input -->
|
|
52
|
+
<div v-if="hasSearch" class="mt-8 max-w-lg mx-auto">
|
|
53
|
+
<div class="relative">
|
|
54
|
+
<UInput
|
|
55
|
+
v-model="searchQuery"
|
|
56
|
+
:placeholder="searchPlaceholder"
|
|
57
|
+
icon="i-lucide-search"
|
|
58
|
+
size="lg"
|
|
59
|
+
class="w-full"
|
|
60
|
+
:ui="{
|
|
61
|
+
base: 'bg-white/10 dark:bg-white/5 border-white/20 dark:border-white/10 text-white placeholder:text-neutral-400 focus:border-primary-400 focus:ring-primary-400/20',
|
|
62
|
+
}"
|
|
63
|
+
@keyup.enter="emitSearch"
|
|
64
|
+
/>
|
|
65
|
+
<UButton
|
|
66
|
+
v-if="searchQuery"
|
|
67
|
+
icon="i-lucide-x"
|
|
68
|
+
color="neutral"
|
|
69
|
+
variant="ghost"
|
|
70
|
+
size="sm"
|
|
71
|
+
class="absolute right-2 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white"
|
|
72
|
+
@click="clearSearch"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</section>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<script setup>
|
|
81
|
+
// Initialize scroll reveal animations
|
|
82
|
+
useScrollReveal();
|
|
83
|
+
|
|
84
|
+
const props = defineProps({
|
|
85
|
+
/** Category object { name, icon?, count?, description? } */
|
|
86
|
+
category: {
|
|
87
|
+
type: Object,
|
|
88
|
+
default: () => ({
|
|
89
|
+
name: "All Listings",
|
|
90
|
+
icon: "i-lucide-grid",
|
|
91
|
+
count: 0,
|
|
92
|
+
description: "Browse all listings in this category.",
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
/** Show search input */
|
|
96
|
+
hasSearch: {
|
|
97
|
+
type: Boolean,
|
|
98
|
+
default: true,
|
|
99
|
+
},
|
|
100
|
+
/** Search input placeholder */
|
|
101
|
+
searchPlaceholder: {
|
|
102
|
+
type: String,
|
|
103
|
+
default: "Search listings...",
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const emit = defineEmits(["search"]);
|
|
108
|
+
|
|
109
|
+
const searchQuery = ref("");
|
|
110
|
+
|
|
111
|
+
const emitSearch = () => {
|
|
112
|
+
emit("search", searchQuery.value);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const clearSearch = () => {
|
|
116
|
+
searchQuery.value = "";
|
|
117
|
+
emit("search", "");
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Emit on each keystroke for live filtering
|
|
121
|
+
watch(searchQuery, (val) => {
|
|
122
|
+
emit("search", val);
|
|
123
|
+
});
|
|
124
|
+
</script>
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Card Layout -->
|
|
3
|
+
<article
|
|
4
|
+
v-if="layout === 'card'"
|
|
5
|
+
class="group relative flex flex-col rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 overflow-hidden transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-[0_12px_40px_rgba(128,128,128,0.15)] dark:hover:shadow-[0_12px_40px_rgba(255,255,255,0.07)]"
|
|
6
|
+
>
|
|
7
|
+
<!-- Badge -->
|
|
8
|
+
<div v-if="showBadge && listing.badge" class="absolute top-3 right-3 z-10">
|
|
9
|
+
<UBadge
|
|
10
|
+
:label="listing.badge"
|
|
11
|
+
:color="badgeColor"
|
|
12
|
+
variant="solid"
|
|
13
|
+
size="sm"
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Logo / Header -->
|
|
18
|
+
<div class="p-5 pb-0 flex items-start gap-4">
|
|
19
|
+
<figure
|
|
20
|
+
v-if="listing.logo"
|
|
21
|
+
class="w-14 h-14 rounded-xl overflow-hidden flex-shrink-0 border border-neutral-100 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800"
|
|
22
|
+
>
|
|
23
|
+
<img
|
|
24
|
+
:src="listing.logo"
|
|
25
|
+
:alt="listing.name"
|
|
26
|
+
class="w-full h-full object-contain"
|
|
27
|
+
/>
|
|
28
|
+
</figure>
|
|
29
|
+
<div
|
|
30
|
+
v-else
|
|
31
|
+
class="w-14 h-14 rounded-xl flex-shrink-0 bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center"
|
|
32
|
+
>
|
|
33
|
+
<span class="text-primary-600 dark:text-primary-400 font-bold text-xl">
|
|
34
|
+
{{ listing.name.charAt(0) }}
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="flex-1 min-w-0 pt-1">
|
|
39
|
+
<h3
|
|
40
|
+
class="font-semibold text-neutral-900 dark:text-white truncate group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors"
|
|
41
|
+
>
|
|
42
|
+
{{ listing.name }}
|
|
43
|
+
</h3>
|
|
44
|
+
<p
|
|
45
|
+
v-if="listing.category"
|
|
46
|
+
class="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5"
|
|
47
|
+
>
|
|
48
|
+
{{ listing.category }}
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Description -->
|
|
54
|
+
<div class="px-5 pt-3 flex-1">
|
|
55
|
+
<p
|
|
56
|
+
v-if="listing.description"
|
|
57
|
+
class="text-sm text-neutral-600 dark:text-neutral-400 line-clamp-2"
|
|
58
|
+
>
|
|
59
|
+
{{ listing.description }}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- Tags -->
|
|
64
|
+
<div
|
|
65
|
+
v-if="showTags && listing.tags?.length"
|
|
66
|
+
class="px-5 pt-3 flex flex-wrap gap-1.5"
|
|
67
|
+
>
|
|
68
|
+
<UBadge
|
|
69
|
+
v-for="tag in listing.tags.slice(0, 4)"
|
|
70
|
+
:key="tag"
|
|
71
|
+
:label="tag"
|
|
72
|
+
variant="subtle"
|
|
73
|
+
color="neutral"
|
|
74
|
+
size="xs"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Rating -->
|
|
79
|
+
<div
|
|
80
|
+
v-if="showRating && listing.rating"
|
|
81
|
+
class="px-5 pt-3 flex items-center gap-2"
|
|
82
|
+
>
|
|
83
|
+
<div class="flex items-center gap-0.5">
|
|
84
|
+
<UIcon
|
|
85
|
+
v-for="i in 5"
|
|
86
|
+
:key="i"
|
|
87
|
+
name="i-lucide-star"
|
|
88
|
+
class="w-3.5 h-3.5"
|
|
89
|
+
:class="
|
|
90
|
+
i <= Math.round(listing.rating)
|
|
91
|
+
? 'text-yellow-400'
|
|
92
|
+
: 'text-neutral-300 dark:text-neutral-600'
|
|
93
|
+
"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
<span class="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
|
97
|
+
{{ listing.rating.toFixed(1) }}
|
|
98
|
+
</span>
|
|
99
|
+
<span
|
|
100
|
+
v-if="listing.reviewCount"
|
|
101
|
+
class="text-xs text-neutral-500 dark:text-neutral-400"
|
|
102
|
+
>
|
|
103
|
+
({{ listing.reviewCount }})
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- CTA -->
|
|
108
|
+
<div class="p-5 pt-4 mt-auto">
|
|
109
|
+
<UButton
|
|
110
|
+
:to="listing.to || '#'"
|
|
111
|
+
:label="ctaLabel"
|
|
112
|
+
color="primary"
|
|
113
|
+
variant="outline"
|
|
114
|
+
size="sm"
|
|
115
|
+
block
|
|
116
|
+
class="justify-center"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
</article>
|
|
120
|
+
|
|
121
|
+
<!-- Row Layout -->
|
|
122
|
+
<article
|
|
123
|
+
v-else-if="layout === 'row'"
|
|
124
|
+
class="group flex items-center gap-4 p-4 rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 transition-all duration-300 ease-out hover:-translate-y-0.5 hover:shadow-[0_8px_24px_rgba(128,128,128,0.12)] dark:hover:shadow-[0_8px_24px_rgba(255,255,255,0.06)]"
|
|
125
|
+
>
|
|
126
|
+
<!-- Logo -->
|
|
127
|
+
<figure
|
|
128
|
+
v-if="listing.logo"
|
|
129
|
+
class="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0 border border-neutral-100 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800"
|
|
130
|
+
>
|
|
131
|
+
<img
|
|
132
|
+
:src="listing.logo"
|
|
133
|
+
:alt="listing.name"
|
|
134
|
+
class="w-full h-full object-contain"
|
|
135
|
+
/>
|
|
136
|
+
</figure>
|
|
137
|
+
<div
|
|
138
|
+
v-else
|
|
139
|
+
class="w-12 h-12 rounded-lg flex-shrink-0 bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center"
|
|
140
|
+
>
|
|
141
|
+
<span class="text-primary-600 dark:text-primary-400 font-bold text-lg">
|
|
142
|
+
{{ listing.name.charAt(0) }}
|
|
143
|
+
</span>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- Content -->
|
|
147
|
+
<div class="flex-1 min-w-0">
|
|
148
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
149
|
+
<h3
|
|
150
|
+
class="font-semibold text-neutral-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors"
|
|
151
|
+
>
|
|
152
|
+
{{ listing.name }}
|
|
153
|
+
</h3>
|
|
154
|
+
<UBadge
|
|
155
|
+
v-if="showBadge && listing.badge"
|
|
156
|
+
:label="listing.badge"
|
|
157
|
+
:color="badgeColor"
|
|
158
|
+
variant="solid"
|
|
159
|
+
size="xs"
|
|
160
|
+
/>
|
|
161
|
+
<UBadge
|
|
162
|
+
v-if="listing.category"
|
|
163
|
+
:label="listing.category"
|
|
164
|
+
variant="subtle"
|
|
165
|
+
color="neutral"
|
|
166
|
+
size="xs"
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
<p
|
|
170
|
+
v-if="listing.description"
|
|
171
|
+
class="text-sm text-neutral-600 dark:text-neutral-400 mt-0.5 truncate"
|
|
172
|
+
>
|
|
173
|
+
{{ listing.description }}
|
|
174
|
+
</p>
|
|
175
|
+
<div
|
|
176
|
+
v-if="showRating && listing.rating"
|
|
177
|
+
class="flex items-center gap-1.5 mt-1"
|
|
178
|
+
>
|
|
179
|
+
<div class="flex items-center gap-0.5">
|
|
180
|
+
<UIcon
|
|
181
|
+
v-for="i in 5"
|
|
182
|
+
:key="i"
|
|
183
|
+
name="i-lucide-star"
|
|
184
|
+
class="w-3 h-3"
|
|
185
|
+
:class="
|
|
186
|
+
i <= Math.round(listing.rating)
|
|
187
|
+
? 'text-yellow-400'
|
|
188
|
+
: 'text-neutral-300 dark:text-neutral-600'
|
|
189
|
+
"
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
<span class="text-xs text-neutral-500 dark:text-neutral-400">
|
|
193
|
+
{{ listing.rating.toFixed(1) }}
|
|
194
|
+
<span v-if="listing.reviewCount">({{ listing.reviewCount }})</span>
|
|
195
|
+
</span>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<!-- Tags (row) -->
|
|
200
|
+
<div
|
|
201
|
+
v-if="showTags && listing.tags?.length"
|
|
202
|
+
class="hidden sm:flex flex-wrap gap-1 flex-shrink-0"
|
|
203
|
+
>
|
|
204
|
+
<UBadge
|
|
205
|
+
v-for="tag in listing.tags.slice(0, 2)"
|
|
206
|
+
:key="tag"
|
|
207
|
+
:label="tag"
|
|
208
|
+
variant="subtle"
|
|
209
|
+
color="neutral"
|
|
210
|
+
size="xs"
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<!-- CTA -->
|
|
215
|
+
<UButton
|
|
216
|
+
:to="listing.to || '#'"
|
|
217
|
+
:label="ctaLabel"
|
|
218
|
+
color="primary"
|
|
219
|
+
variant="outline"
|
|
220
|
+
size="sm"
|
|
221
|
+
class="flex-shrink-0"
|
|
222
|
+
/>
|
|
223
|
+
</article>
|
|
224
|
+
|
|
225
|
+
<!-- Compact Layout -->
|
|
226
|
+
<article
|
|
227
|
+
v-else-if="layout === 'compact'"
|
|
228
|
+
class="group flex items-center gap-3 py-3 border-b border-neutral-100 dark:border-neutral-800 last:border-0 transition-colors hover:bg-neutral-50 dark:hover:bg-neutral-900/50 px-2 rounded-lg"
|
|
229
|
+
>
|
|
230
|
+
<!-- Logo -->
|
|
231
|
+
<figure
|
|
232
|
+
v-if="listing.logo"
|
|
233
|
+
class="w-9 h-9 rounded-lg overflow-hidden flex-shrink-0 border border-neutral-100 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-800"
|
|
234
|
+
>
|
|
235
|
+
<img
|
|
236
|
+
:src="listing.logo"
|
|
237
|
+
:alt="listing.name"
|
|
238
|
+
class="w-full h-full object-contain"
|
|
239
|
+
/>
|
|
240
|
+
</figure>
|
|
241
|
+
<div
|
|
242
|
+
v-else
|
|
243
|
+
class="w-9 h-9 rounded-lg flex-shrink-0 bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center"
|
|
244
|
+
>
|
|
245
|
+
<span class="text-primary-600 dark:text-primary-400 font-bold text-sm">
|
|
246
|
+
{{ listing.name.charAt(0) }}
|
|
247
|
+
</span>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<!-- Name + Category -->
|
|
251
|
+
<div class="flex-1 min-w-0">
|
|
252
|
+
<NuxtLink
|
|
253
|
+
:to="listing.to || '#'"
|
|
254
|
+
class="font-medium text-sm text-neutral-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors truncate block"
|
|
255
|
+
>
|
|
256
|
+
{{ listing.name }}
|
|
257
|
+
</NuxtLink>
|
|
258
|
+
<p
|
|
259
|
+
v-if="listing.category"
|
|
260
|
+
class="text-xs text-neutral-500 dark:text-neutral-400 truncate"
|
|
261
|
+
>
|
|
262
|
+
{{ listing.category }}
|
|
263
|
+
</p>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<!-- Rating -->
|
|
267
|
+
<div
|
|
268
|
+
v-if="showRating && listing.rating"
|
|
269
|
+
class="flex items-center gap-1 flex-shrink-0"
|
|
270
|
+
>
|
|
271
|
+
<UIcon name="i-lucide-star" class="w-3 h-3 text-yellow-400" />
|
|
272
|
+
<span class="text-xs font-medium text-neutral-700 dark:text-neutral-300">
|
|
273
|
+
{{ listing.rating.toFixed(1) }}
|
|
274
|
+
</span>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<!-- Badge -->
|
|
278
|
+
<UBadge
|
|
279
|
+
v-if="showBadge && listing.badge"
|
|
280
|
+
:label="listing.badge"
|
|
281
|
+
:color="badgeColor"
|
|
282
|
+
variant="solid"
|
|
283
|
+
size="xs"
|
|
284
|
+
class="flex-shrink-0"
|
|
285
|
+
/>
|
|
286
|
+
</article>
|
|
287
|
+
</template>
|
|
288
|
+
|
|
289
|
+
<script setup>
|
|
290
|
+
const props = defineProps({
|
|
291
|
+
/** Listing object { name, description?, logo?, category?, tags?, rating?, reviewCount?, badge?, to? } */
|
|
292
|
+
listing: {
|
|
293
|
+
type: Object,
|
|
294
|
+
default: () => ({
|
|
295
|
+
name: "Example Listing",
|
|
296
|
+
description: "A great product or service.",
|
|
297
|
+
category: "General",
|
|
298
|
+
rating: 4.5,
|
|
299
|
+
reviewCount: 42,
|
|
300
|
+
}),
|
|
301
|
+
},
|
|
302
|
+
/** Card display layout */
|
|
303
|
+
layout: {
|
|
304
|
+
type: String,
|
|
305
|
+
default: "card",
|
|
306
|
+
validator: (v) => ["card", "row", "compact"].includes(v),
|
|
307
|
+
},
|
|
308
|
+
/** Show star rating */
|
|
309
|
+
showRating: {
|
|
310
|
+
type: Boolean,
|
|
311
|
+
default: true,
|
|
312
|
+
},
|
|
313
|
+
/** Show tags */
|
|
314
|
+
showTags: {
|
|
315
|
+
type: Boolean,
|
|
316
|
+
default: true,
|
|
317
|
+
},
|
|
318
|
+
/** Show badge (Featured, Sponsored, etc.) */
|
|
319
|
+
showBadge: {
|
|
320
|
+
type: Boolean,
|
|
321
|
+
default: true,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const ctaLabel = computed(() => {
|
|
326
|
+
return props.listing.ctaLabel || "View Listing";
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const badgeColor = computed(() => {
|
|
330
|
+
const badge = props.listing.badge?.toLowerCase();
|
|
331
|
+
if (badge === "featured") return "primary";
|
|
332
|
+
if (badge === "sponsored") return "warning";
|
|
333
|
+
if (badge === "new") return "success";
|
|
334
|
+
return "primary";
|
|
335
|
+
});
|
|
336
|
+
</script>
|