@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,306 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<article class="listing-detail">
|
|
3
|
+
<!-- Header -->
|
|
4
|
+
<header class="mb-10">
|
|
5
|
+
<div class="flex flex-col sm:flex-row sm:items-start gap-5">
|
|
6
|
+
<!-- Logo -->
|
|
7
|
+
<figure
|
|
8
|
+
v-if="listing.logo"
|
|
9
|
+
class="w-20 h-20 rounded-2xl overflow-hidden flex-shrink-0 border border-neutral-200 dark:border-neutral-800 bg-neutral-50 dark:bg-neutral-900 shadow-sm"
|
|
10
|
+
>
|
|
11
|
+
<img
|
|
12
|
+
:src="listing.logo"
|
|
13
|
+
:alt="listing.name"
|
|
14
|
+
class="w-full h-full object-contain"
|
|
15
|
+
/>
|
|
16
|
+
</figure>
|
|
17
|
+
<div
|
|
18
|
+
v-else
|
|
19
|
+
class="w-20 h-20 rounded-2xl flex-shrink-0 bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center shadow-sm"
|
|
20
|
+
>
|
|
21
|
+
<span
|
|
22
|
+
class="text-primary-600 dark:text-primary-400 font-bold text-3xl"
|
|
23
|
+
>
|
|
24
|
+
{{ listing.name.charAt(0) }}
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<!-- Name + Meta -->
|
|
29
|
+
<div class="flex-1 min-w-0">
|
|
30
|
+
<div class="flex flex-wrap items-center gap-2 mb-1">
|
|
31
|
+
<h1
|
|
32
|
+
class="text-2xl sm:text-3xl font-bold text-neutral-900 dark:text-white"
|
|
33
|
+
>
|
|
34
|
+
{{ listing.name }}
|
|
35
|
+
</h1>
|
|
36
|
+
<UBadge
|
|
37
|
+
v-if="listing.badge"
|
|
38
|
+
:label="listing.badge"
|
|
39
|
+
color="primary"
|
|
40
|
+
variant="solid"
|
|
41
|
+
size="sm"
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Category + Rating -->
|
|
46
|
+
<div class="flex flex-wrap items-center gap-3 mt-2">
|
|
47
|
+
<UBadge
|
|
48
|
+
v-if="listing.category"
|
|
49
|
+
:label="listing.category"
|
|
50
|
+
variant="subtle"
|
|
51
|
+
color="neutral"
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
<div v-if="listing.rating" class="flex items-center gap-1.5">
|
|
55
|
+
<div class="flex items-center gap-0.5">
|
|
56
|
+
<UIcon
|
|
57
|
+
v-for="i in 5"
|
|
58
|
+
:key="i"
|
|
59
|
+
name="i-lucide-star"
|
|
60
|
+
class="w-4 h-4"
|
|
61
|
+
:class="
|
|
62
|
+
i <= Math.round(listing.rating)
|
|
63
|
+
? 'text-yellow-400'
|
|
64
|
+
: 'text-neutral-300 dark:text-neutral-600'
|
|
65
|
+
"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<span
|
|
69
|
+
class="text-sm font-semibold text-neutral-700 dark:text-neutral-300"
|
|
70
|
+
>
|
|
71
|
+
{{ listing.rating.toFixed(1) }}
|
|
72
|
+
</span>
|
|
73
|
+
<span
|
|
74
|
+
v-if="listing.reviewCount"
|
|
75
|
+
class="text-sm text-neutral-500 dark:text-neutral-400"
|
|
76
|
+
>
|
|
77
|
+
({{ listing.reviewCount }} reviews)
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- Tags -->
|
|
83
|
+
<div v-if="listing.tags?.length" class="flex flex-wrap gap-1.5 mt-3">
|
|
84
|
+
<UBadge
|
|
85
|
+
v-for="tag in listing.tags"
|
|
86
|
+
:key="tag"
|
|
87
|
+
:label="tag"
|
|
88
|
+
variant="subtle"
|
|
89
|
+
color="neutral"
|
|
90
|
+
size="sm"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</header>
|
|
96
|
+
|
|
97
|
+
<!-- Content Grid -->
|
|
98
|
+
<div
|
|
99
|
+
:class="[
|
|
100
|
+
'grid gap-10 lg:gap-14',
|
|
101
|
+
hasSidebar ? 'lg:grid-cols-[1fr_300px]' : '',
|
|
102
|
+
]"
|
|
103
|
+
>
|
|
104
|
+
<!-- Main Content -->
|
|
105
|
+
<div class="min-w-0">
|
|
106
|
+
<!-- Description -->
|
|
107
|
+
<section v-if="listing.description" class="mb-8">
|
|
108
|
+
<div class="prose prose-neutral dark:prose-invert max-w-none">
|
|
109
|
+
<p
|
|
110
|
+
class="text-lg text-neutral-700 dark:text-neutral-300 leading-relaxed"
|
|
111
|
+
>
|
|
112
|
+
{{ listing.description }}
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
</section>
|
|
116
|
+
|
|
117
|
+
<!-- After Description Slot -->
|
|
118
|
+
<slot name="after-description" />
|
|
119
|
+
|
|
120
|
+
<!-- Screenshots Gallery -->
|
|
121
|
+
<section
|
|
122
|
+
v-if="hasScreenshots && listing.screenshots?.length"
|
|
123
|
+
class="mb-10"
|
|
124
|
+
>
|
|
125
|
+
<h2
|
|
126
|
+
class="text-lg font-semibold text-neutral-900 dark:text-white mb-4"
|
|
127
|
+
>
|
|
128
|
+
Screenshots
|
|
129
|
+
</h2>
|
|
130
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
131
|
+
<figure
|
|
132
|
+
v-for="(screenshot, index) in listing.screenshots"
|
|
133
|
+
:key="index"
|
|
134
|
+
class="aspect-video rounded-xl overflow-hidden border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-900 cursor-pointer group"
|
|
135
|
+
>
|
|
136
|
+
<img
|
|
137
|
+
:src="
|
|
138
|
+
typeof screenshot === 'string' ? screenshot : screenshot.src
|
|
139
|
+
"
|
|
140
|
+
:alt="
|
|
141
|
+
typeof screenshot === 'string'
|
|
142
|
+
? `Screenshot ${index + 1}`
|
|
143
|
+
: screenshot.alt || `Screenshot ${index + 1}`
|
|
144
|
+
"
|
|
145
|
+
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
|
146
|
+
/>
|
|
147
|
+
</figure>
|
|
148
|
+
</div>
|
|
149
|
+
</section>
|
|
150
|
+
|
|
151
|
+
<!-- Features List -->
|
|
152
|
+
<section v-if="listing.features?.length" class="mb-10">
|
|
153
|
+
<h2
|
|
154
|
+
class="text-lg font-semibold text-neutral-900 dark:text-white mb-4"
|
|
155
|
+
>
|
|
156
|
+
Features
|
|
157
|
+
</h2>
|
|
158
|
+
<ul class="grid sm:grid-cols-2 gap-3">
|
|
159
|
+
<li
|
|
160
|
+
v-for="(feature, index) in listing.features"
|
|
161
|
+
:key="index"
|
|
162
|
+
class="flex items-start gap-3"
|
|
163
|
+
>
|
|
164
|
+
<span
|
|
165
|
+
class="flex-shrink-0 w-5 h-5 rounded-full bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center mt-0.5"
|
|
166
|
+
>
|
|
167
|
+
<UIcon
|
|
168
|
+
name="i-lucide-check"
|
|
169
|
+
class="w-3 h-3 text-primary-600 dark:text-primary-400"
|
|
170
|
+
/>
|
|
171
|
+
</span>
|
|
172
|
+
<span class="text-sm text-neutral-700 dark:text-neutral-300">
|
|
173
|
+
{{ feature }}
|
|
174
|
+
</span>
|
|
175
|
+
</li>
|
|
176
|
+
</ul>
|
|
177
|
+
</section>
|
|
178
|
+
|
|
179
|
+
<!-- Reviews Slot -->
|
|
180
|
+
<section v-if="hasReviews">
|
|
181
|
+
<slot name="reviews" />
|
|
182
|
+
</section>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- Sidebar -->
|
|
186
|
+
<aside v-if="hasSidebar" class="hidden lg:block">
|
|
187
|
+
<div class="sticky top-24 space-y-6">
|
|
188
|
+
<!-- Default Sidebar Content -->
|
|
189
|
+
<slot name="sidebar">
|
|
190
|
+
<!-- Website CTA -->
|
|
191
|
+
<div
|
|
192
|
+
class="p-5 rounded-xl bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200/60 dark:border-neutral-800"
|
|
193
|
+
>
|
|
194
|
+
<UButton
|
|
195
|
+
v-if="listing.website"
|
|
196
|
+
:to="listing.website"
|
|
197
|
+
label="Visit Website"
|
|
198
|
+
icon="i-lucide-external-link"
|
|
199
|
+
color="primary"
|
|
200
|
+
variant="solid"
|
|
201
|
+
block
|
|
202
|
+
target="_blank"
|
|
203
|
+
rel="noopener noreferrer"
|
|
204
|
+
class="justify-center mb-4"
|
|
205
|
+
/>
|
|
206
|
+
|
|
207
|
+
<!-- Category -->
|
|
208
|
+
<div
|
|
209
|
+
v-if="listing.category"
|
|
210
|
+
class="flex items-center justify-between py-2.5 border-b border-neutral-200 dark:border-neutral-800"
|
|
211
|
+
>
|
|
212
|
+
<span
|
|
213
|
+
class="text-xs font-semibold uppercase tracking-wider text-neutral-500"
|
|
214
|
+
>
|
|
215
|
+
Category
|
|
216
|
+
</span>
|
|
217
|
+
<UBadge
|
|
218
|
+
:label="listing.category"
|
|
219
|
+
variant="subtle"
|
|
220
|
+
color="neutral"
|
|
221
|
+
size="sm"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<!-- Rating Summary -->
|
|
226
|
+
<div
|
|
227
|
+
v-if="listing.rating"
|
|
228
|
+
class="flex items-center justify-between py-2.5 border-b border-neutral-200 dark:border-neutral-800"
|
|
229
|
+
>
|
|
230
|
+
<span
|
|
231
|
+
class="text-xs font-semibold uppercase tracking-wider text-neutral-500"
|
|
232
|
+
>
|
|
233
|
+
Rating
|
|
234
|
+
</span>
|
|
235
|
+
<div class="flex items-center gap-1.5">
|
|
236
|
+
<UIcon name="i-lucide-star" class="w-4 h-4 text-yellow-400" />
|
|
237
|
+
<span
|
|
238
|
+
class="text-sm font-semibold text-neutral-900 dark:text-white"
|
|
239
|
+
>
|
|
240
|
+
{{ listing.rating.toFixed(1) }}
|
|
241
|
+
</span>
|
|
242
|
+
<span
|
|
243
|
+
v-if="listing.reviewCount"
|
|
244
|
+
class="text-xs text-neutral-500"
|
|
245
|
+
>
|
|
246
|
+
({{ listing.reviewCount }})
|
|
247
|
+
</span>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<!-- Tags -->
|
|
252
|
+
<div v-if="listing.tags?.length" class="pt-3">
|
|
253
|
+
<span
|
|
254
|
+
class="text-xs font-semibold uppercase tracking-wider text-neutral-500 block mb-2"
|
|
255
|
+
>
|
|
256
|
+
Tags
|
|
257
|
+
</span>
|
|
258
|
+
<div class="flex flex-wrap gap-1.5">
|
|
259
|
+
<UBadge
|
|
260
|
+
v-for="tag in listing.tags"
|
|
261
|
+
:key="tag"
|
|
262
|
+
:label="tag"
|
|
263
|
+
variant="subtle"
|
|
264
|
+
color="neutral"
|
|
265
|
+
size="xs"
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</slot>
|
|
271
|
+
</div>
|
|
272
|
+
</aside>
|
|
273
|
+
</div>
|
|
274
|
+
</article>
|
|
275
|
+
</template>
|
|
276
|
+
|
|
277
|
+
<script setup>
|
|
278
|
+
const props = defineProps({
|
|
279
|
+
/** Listing object { name, description?, logo?, website?, screenshots?, tags?, category?, rating?, reviewCount?, features? } */
|
|
280
|
+
listing: {
|
|
281
|
+
type: Object,
|
|
282
|
+
default: () => ({
|
|
283
|
+
name: "Listing Name",
|
|
284
|
+
description: "Full description of this listing.",
|
|
285
|
+
category: "General",
|
|
286
|
+
rating: 4.5,
|
|
287
|
+
reviewCount: 42,
|
|
288
|
+
}),
|
|
289
|
+
},
|
|
290
|
+
/** Show screenshots gallery */
|
|
291
|
+
hasScreenshots: {
|
|
292
|
+
type: Boolean,
|
|
293
|
+
default: true,
|
|
294
|
+
},
|
|
295
|
+
/** Show sidebar */
|
|
296
|
+
hasSidebar: {
|
|
297
|
+
type: Boolean,
|
|
298
|
+
default: true,
|
|
299
|
+
},
|
|
300
|
+
/** Show reviews section */
|
|
301
|
+
hasReviews: {
|
|
302
|
+
type: Boolean,
|
|
303
|
+
default: false,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
</script>
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<!-- Search + Filters -->
|
|
4
|
+
<div v-if="hasSearch || hasFilters" class="flex flex-wrap gap-4 mb-8">
|
|
5
|
+
<UInput
|
|
6
|
+
v-if="hasSearch"
|
|
7
|
+
v-model="search"
|
|
8
|
+
placeholder="Search listings..."
|
|
9
|
+
icon="i-lucide-search"
|
|
10
|
+
class="w-full sm:w-72"
|
|
11
|
+
/>
|
|
12
|
+
<USelect
|
|
13
|
+
v-if="hasFilters && categories.length"
|
|
14
|
+
v-model="selectedCategory"
|
|
15
|
+
:options="categoryOptions"
|
|
16
|
+
placeholder="All Categories"
|
|
17
|
+
class="w-48"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Grid / List -->
|
|
22
|
+
<div :class="containerClasses">
|
|
23
|
+
<XMarkDirectoryListingCard
|
|
24
|
+
v-for="(listing, index) in paginatedListings"
|
|
25
|
+
:key="listing.name + index"
|
|
26
|
+
:listing="listing"
|
|
27
|
+
:layout="layout === 'list' ? 'row' : 'card'"
|
|
28
|
+
class="xFadeUp"
|
|
29
|
+
:style="{ transitionDelay: `${index * 50}ms` }"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<!-- Empty State -->
|
|
34
|
+
<div v-if="!paginatedListings.length" class="text-center py-20">
|
|
35
|
+
<UIcon
|
|
36
|
+
name="i-lucide-search-x"
|
|
37
|
+
class="w-12 h-12 text-neutral-300 dark:text-neutral-700 mx-auto mb-4"
|
|
38
|
+
/>
|
|
39
|
+
<p
|
|
40
|
+
class="text-lg font-medium text-neutral-700 dark:text-neutral-300 mb-1"
|
|
41
|
+
>
|
|
42
|
+
No listings found
|
|
43
|
+
</p>
|
|
44
|
+
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
|
45
|
+
Try adjusting your search or filter criteria.
|
|
46
|
+
</p>
|
|
47
|
+
<UButton
|
|
48
|
+
v-if="search || selectedCategory"
|
|
49
|
+
label="Clear filters"
|
|
50
|
+
variant="ghost"
|
|
51
|
+
color="primary"
|
|
52
|
+
size="sm"
|
|
53
|
+
class="mt-4"
|
|
54
|
+
@click="clearFilters"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Pagination -->
|
|
59
|
+
<div
|
|
60
|
+
v-if="hasPagination && totalPages > 1"
|
|
61
|
+
class="mt-12 flex justify-center"
|
|
62
|
+
>
|
|
63
|
+
<UPagination
|
|
64
|
+
v-model="currentPage"
|
|
65
|
+
:total="filteredListings.length"
|
|
66
|
+
:page-count="perPage"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<script setup>
|
|
73
|
+
// Initialize scroll reveal animations
|
|
74
|
+
useScrollReveal();
|
|
75
|
+
|
|
76
|
+
const props = defineProps({
|
|
77
|
+
/** Array of listing objects */
|
|
78
|
+
listings: {
|
|
79
|
+
type: Array,
|
|
80
|
+
default: () => [],
|
|
81
|
+
},
|
|
82
|
+
/** Available category strings for filter */
|
|
83
|
+
categories: {
|
|
84
|
+
type: Array,
|
|
85
|
+
default: () => [],
|
|
86
|
+
},
|
|
87
|
+
/** Display mode */
|
|
88
|
+
layout: {
|
|
89
|
+
type: String,
|
|
90
|
+
default: "grid",
|
|
91
|
+
validator: (v) => ["grid", "list"].includes(v),
|
|
92
|
+
},
|
|
93
|
+
/** Number of columns in grid mode */
|
|
94
|
+
columns: {
|
|
95
|
+
type: Number,
|
|
96
|
+
default: 3,
|
|
97
|
+
validator: (v) => [2, 3, 4].includes(v),
|
|
98
|
+
},
|
|
99
|
+
/** Show search input */
|
|
100
|
+
hasSearch: {
|
|
101
|
+
type: Boolean,
|
|
102
|
+
default: true,
|
|
103
|
+
},
|
|
104
|
+
/** Show category filter */
|
|
105
|
+
hasFilters: {
|
|
106
|
+
type: Boolean,
|
|
107
|
+
default: true,
|
|
108
|
+
},
|
|
109
|
+
/** Show pagination */
|
|
110
|
+
hasPagination: {
|
|
111
|
+
type: Boolean,
|
|
112
|
+
default: true,
|
|
113
|
+
},
|
|
114
|
+
/** Items per page */
|
|
115
|
+
perPage: {
|
|
116
|
+
type: Number,
|
|
117
|
+
default: 12,
|
|
118
|
+
},
|
|
119
|
+
/** Optional button config passed through to each ListingCard */
|
|
120
|
+
cardButton: {
|
|
121
|
+
type: Object,
|
|
122
|
+
default: null,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const search = ref("");
|
|
127
|
+
const selectedCategory = ref("");
|
|
128
|
+
const currentPage = ref(1);
|
|
129
|
+
|
|
130
|
+
const categoryOptions = computed(() => {
|
|
131
|
+
return [
|
|
132
|
+
{ label: "All Categories", value: "" },
|
|
133
|
+
...props.categories.map((cat) => ({
|
|
134
|
+
label: cat,
|
|
135
|
+
value: cat,
|
|
136
|
+
})),
|
|
137
|
+
];
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const filteredListings = computed(() => {
|
|
141
|
+
let result = props.listings;
|
|
142
|
+
|
|
143
|
+
if (search.value) {
|
|
144
|
+
const q = search.value.toLowerCase();
|
|
145
|
+
result = result.filter(
|
|
146
|
+
(item) =>
|
|
147
|
+
item.name?.toLowerCase().includes(q) ||
|
|
148
|
+
item.description?.toLowerCase().includes(q)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (selectedCategory.value) {
|
|
153
|
+
result = result.filter((item) => item.category === selectedCategory.value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const totalPages = computed(() => {
|
|
160
|
+
return Math.ceil(filteredListings.value.length / props.perPage);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const paginatedListings = computed(() => {
|
|
164
|
+
const start = (currentPage.value - 1) * props.perPage;
|
|
165
|
+
const end = start + props.perPage;
|
|
166
|
+
return filteredListings.value.slice(start, end);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const containerClasses = computed(() => {
|
|
170
|
+
if (props.layout === "list") {
|
|
171
|
+
return "flex flex-col gap-3";
|
|
172
|
+
}
|
|
173
|
+
const cols = {
|
|
174
|
+
2: "grid sm:grid-cols-2 gap-6",
|
|
175
|
+
3: "grid sm:grid-cols-2 lg:grid-cols-3 gap-6",
|
|
176
|
+
4: "grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6",
|
|
177
|
+
};
|
|
178
|
+
return cols[props.columns] || cols[3];
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const clearFilters = () => {
|
|
182
|
+
search.value = "";
|
|
183
|
+
selectedCategory.value = "";
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Reset pagination when filters change
|
|
187
|
+
watch([search, selectedCategory], () => {
|
|
188
|
+
currentPage.value = 1;
|
|
189
|
+
});
|
|
190
|
+
</script>
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section :class="['relative overflow-hidden rounded-2xl', bgClasses]">
|
|
3
|
+
<!-- Background Pattern -->
|
|
4
|
+
<XMarkPatternBg
|
|
5
|
+
v-if="bg !== 'bold' && bg !== 'gradient'"
|
|
6
|
+
pattern="dots"
|
|
7
|
+
:opacity="0.04"
|
|
8
|
+
class="absolute inset-0 pointer-events-none"
|
|
9
|
+
/>
|
|
10
|
+
|
|
11
|
+
<div class="relative z-10 p-8 md:p-12 lg:p-16">
|
|
12
|
+
<div class="grid lg:grid-cols-2 gap-10 lg:gap-16 items-center">
|
|
13
|
+
<!-- Left: Text + Features -->
|
|
14
|
+
<div class="xFadeUp">
|
|
15
|
+
<!-- Title -->
|
|
16
|
+
<h2
|
|
17
|
+
:class="[
|
|
18
|
+
'xText-headline',
|
|
19
|
+
isBold ? 'text-white' : 'text-neutral-900 dark:text-white',
|
|
20
|
+
]"
|
|
21
|
+
>
|
|
22
|
+
{{ title }}
|
|
23
|
+
</h2>
|
|
24
|
+
|
|
25
|
+
<!-- Description -->
|
|
26
|
+
<p
|
|
27
|
+
v-if="description"
|
|
28
|
+
:class="[
|
|
29
|
+
'mt-4 text-lg leading-relaxed',
|
|
30
|
+
isBold
|
|
31
|
+
? 'text-white/80'
|
|
32
|
+
: 'text-neutral-600 dark:text-neutral-400',
|
|
33
|
+
]"
|
|
34
|
+
>
|
|
35
|
+
{{ description }}
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<!-- Features List -->
|
|
39
|
+
<ul v-if="features.length" class="mt-6 space-y-3">
|
|
40
|
+
<li
|
|
41
|
+
v-for="(feature, index) in features"
|
|
42
|
+
:key="index"
|
|
43
|
+
class="flex items-center gap-3"
|
|
44
|
+
>
|
|
45
|
+
<span
|
|
46
|
+
:class="[
|
|
47
|
+
'flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center',
|
|
48
|
+
isBold
|
|
49
|
+
? 'bg-white/20'
|
|
50
|
+
: 'bg-primary-100 dark:bg-primary-900/40',
|
|
51
|
+
]"
|
|
52
|
+
>
|
|
53
|
+
<UIcon
|
|
54
|
+
name="i-lucide-check"
|
|
55
|
+
:class="[
|
|
56
|
+
'w-3 h-3',
|
|
57
|
+
isBold
|
|
58
|
+
? 'text-white'
|
|
59
|
+
: 'text-primary-600 dark:text-primary-400',
|
|
60
|
+
]"
|
|
61
|
+
/>
|
|
62
|
+
</span>
|
|
63
|
+
<span
|
|
64
|
+
:class="[
|
|
65
|
+
'text-sm font-medium',
|
|
66
|
+
isBold
|
|
67
|
+
? 'text-white/90'
|
|
68
|
+
: 'text-neutral-700 dark:text-neutral-300',
|
|
69
|
+
]"
|
|
70
|
+
>
|
|
71
|
+
{{ feature }}
|
|
72
|
+
</span>
|
|
73
|
+
</li>
|
|
74
|
+
</ul>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Right: CTA Card -->
|
|
78
|
+
<div class="xFadeUp" style="transition-delay: 100ms">
|
|
79
|
+
<div
|
|
80
|
+
:class="[
|
|
81
|
+
'rounded-2xl p-8 text-center',
|
|
82
|
+
isBold
|
|
83
|
+
? 'bg-white/10 backdrop-blur-sm border border-white/20'
|
|
84
|
+
: 'bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 shadow-lg',
|
|
85
|
+
]"
|
|
86
|
+
>
|
|
87
|
+
<!-- Icon -->
|
|
88
|
+
<div
|
|
89
|
+
:class="[
|
|
90
|
+
'inline-flex items-center justify-center w-14 h-14 rounded-2xl mb-5',
|
|
91
|
+
isBold
|
|
92
|
+
? 'bg-white/20'
|
|
93
|
+
: 'bg-primary-100 dark:bg-primary-900/40',
|
|
94
|
+
]"
|
|
95
|
+
>
|
|
96
|
+
<UIcon
|
|
97
|
+
name="i-lucide-plus-circle"
|
|
98
|
+
:class="[
|
|
99
|
+
'w-7 h-7',
|
|
100
|
+
isBold
|
|
101
|
+
? 'text-white'
|
|
102
|
+
: 'text-primary-600 dark:text-primary-400',
|
|
103
|
+
]"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Card Title -->
|
|
108
|
+
<h3
|
|
109
|
+
:class="[
|
|
110
|
+
'text-xl font-semibold mb-2',
|
|
111
|
+
isBold ? 'text-white' : 'text-neutral-900 dark:text-white',
|
|
112
|
+
]"
|
|
113
|
+
>
|
|
114
|
+
{{ button.label || "Submit a Listing" }}
|
|
115
|
+
</h3>
|
|
116
|
+
|
|
117
|
+
<p
|
|
118
|
+
:class="[
|
|
119
|
+
'text-sm mb-6',
|
|
120
|
+
isBold
|
|
121
|
+
? 'text-white/70'
|
|
122
|
+
: 'text-neutral-500 dark:text-neutral-400',
|
|
123
|
+
]"
|
|
124
|
+
>
|
|
125
|
+
Get your product in front of thousands of buyers.
|
|
126
|
+
</p>
|
|
127
|
+
|
|
128
|
+
<!-- CTA Button -->
|
|
129
|
+
<UButton
|
|
130
|
+
:to="button.to || '/submit'"
|
|
131
|
+
:label="button.label || 'Submit a Listing'"
|
|
132
|
+
:color="isBold ? 'neutral' : 'primary'"
|
|
133
|
+
:class="
|
|
134
|
+
isBold
|
|
135
|
+
? 'bg-white text-neutral-900 hover:bg-white/90 w-full justify-center'
|
|
136
|
+
: 'w-full justify-center'
|
|
137
|
+
"
|
|
138
|
+
size="lg"
|
|
139
|
+
block
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</section>
|
|
146
|
+
</template>
|
|
147
|
+
|
|
148
|
+
<script setup>
|
|
149
|
+
// Initialize scroll reveal animations
|
|
150
|
+
useScrollReveal();
|
|
151
|
+
|
|
152
|
+
const props = defineProps({
|
|
153
|
+
/** Section title */
|
|
154
|
+
title: {
|
|
155
|
+
type: String,
|
|
156
|
+
default: "List your product",
|
|
157
|
+
},
|
|
158
|
+
/** Section description */
|
|
159
|
+
description: {
|
|
160
|
+
type: String,
|
|
161
|
+
default:
|
|
162
|
+
"Reach thousands of buyers actively searching for solutions like yours.",
|
|
163
|
+
},
|
|
164
|
+
/** CTA button config { label, to } */
|
|
165
|
+
button: {
|
|
166
|
+
type: Object,
|
|
167
|
+
default: () => ({
|
|
168
|
+
label: "Submit a Listing",
|
|
169
|
+
to: "/submit",
|
|
170
|
+
}),
|
|
171
|
+
},
|
|
172
|
+
/** Feature bullet points */
|
|
173
|
+
features: {
|
|
174
|
+
type: Array,
|
|
175
|
+
default: () => [
|
|
176
|
+
"Free basic listing",
|
|
177
|
+
"Featured placement available",
|
|
178
|
+
"Analytics dashboard",
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
/** Background style */
|
|
182
|
+
bg: {
|
|
183
|
+
type: String,
|
|
184
|
+
default: "subtle",
|
|
185
|
+
validator: (v) => ["subtle", "bold", "gradient"].includes(v),
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const isBold = computed(() => {
|
|
190
|
+
return props.bg === "bold" || props.bg === "gradient";
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const bgClasses = computed(() => {
|
|
194
|
+
const variants = {
|
|
195
|
+
subtle: "bg-neutral-100 dark:bg-neutral-900",
|
|
196
|
+
bold: "bg-primary-600 dark:bg-primary-700",
|
|
197
|
+
gradient: "bg-gradient-to-br from-primary-600 to-primary-800",
|
|
198
|
+
};
|
|
199
|
+
return variants[props.bg] || variants.subtle;
|
|
200
|
+
});
|
|
201
|
+
</script>
|