@xenterprises/nuxt-x-marketing 1.2.2 → 1.4.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/CHANGELOG.md +64 -0
- package/README.md +107 -284
- package/app/app.config.ts +29 -123
- package/app/app.vue +31 -100
- package/app/assets/css/x-marketing.css +10 -0
- package/app/components/X/Footer/index.vue +1 -1
- package/app/components/X/Mark/Button/BackToTop.vue +2 -2
- package/app/components/X/Mark/CTA/index.vue +19 -6
- package/app/components/X/Mark/Privacy/CookieConsent.vue +15 -16
- package/app/components/X/Mark/Privacy/GDPR.vue +1 -1
- package/app/composables/useXBlog.js +168 -8
- package/app/pages/blog/[...slug].vue +81 -69
- package/app/pages/blog/index.vue +46 -50
- package/app/pages/index.vue +7 -209
- package/app/types/config.ts +134 -0
- package/content.config.ts +35 -0
- package/nuxt.config.ts +18 -5
- package/package.json +33 -11
- package/playwright-report-smoke/index.html +90 -0
- package/app/components/X/Mark/Affiliate/ComparisonTable.vue +0 -382
- package/app/components/X/Mark/Affiliate/Disclosure.vue +0 -138
- package/app/components/X/Mark/Affiliate/ProductCard.vue +0 -431
- package/app/components/X/Mark/Affiliate/ProductDetail.vue +0 -410
- package/app/components/X/Mark/Affiliate/ProductGrid.vue +0 -101
|
@@ -1,431 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!-- Card Layout (vertical) -->
|
|
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
|
-
<!-- Rank Badge -->
|
|
8
|
-
<div v-if="product.badge" class="absolute top-3 left-3 z-10">
|
|
9
|
-
<UBadge
|
|
10
|
-
:label="product.badge"
|
|
11
|
-
color="primary"
|
|
12
|
-
variant="solid"
|
|
13
|
-
size="sm"
|
|
14
|
-
/>
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
<!-- Product Image -->
|
|
18
|
-
<figure
|
|
19
|
-
class="aspect-4/3 overflow-hidden bg-neutral-50 dark:bg-neutral-800"
|
|
20
|
-
>
|
|
21
|
-
<img
|
|
22
|
-
v-if="product.image"
|
|
23
|
-
:src="product.image"
|
|
24
|
-
:alt="product.name"
|
|
25
|
-
class="w-full h-full object-contain p-4 transition-transform duration-300 group-hover:scale-105"
|
|
26
|
-
/>
|
|
27
|
-
<div v-else class="w-full h-full flex items-center justify-center">
|
|
28
|
-
<UIcon
|
|
29
|
-
name="i-lucide-package"
|
|
30
|
-
class="w-16 h-16 text-neutral-300 dark:text-neutral-600"
|
|
31
|
-
/>
|
|
32
|
-
</div>
|
|
33
|
-
</figure>
|
|
34
|
-
|
|
35
|
-
<!-- Content -->
|
|
36
|
-
<div class="flex flex-col flex-1 p-5">
|
|
37
|
-
<!-- Name -->
|
|
38
|
-
<h3
|
|
39
|
-
class="font-semibold text-neutral-900 dark:text-white text-base leading-snug mb-2"
|
|
40
|
-
>
|
|
41
|
-
{{ product.name }}
|
|
42
|
-
</h3>
|
|
43
|
-
|
|
44
|
-
<!-- Rating -->
|
|
45
|
-
<div
|
|
46
|
-
v-if="showRating && product.rating"
|
|
47
|
-
class="flex items-center gap-2 mb-3"
|
|
48
|
-
>
|
|
49
|
-
<div class="flex items-center gap-0.5">
|
|
50
|
-
<UIcon
|
|
51
|
-
v-for="i in 5"
|
|
52
|
-
:key="i"
|
|
53
|
-
name="i-lucide-star"
|
|
54
|
-
class="w-3.5 h-3.5"
|
|
55
|
-
:class="
|
|
56
|
-
i <= Math.round(product.rating)
|
|
57
|
-
? 'text-yellow-400'
|
|
58
|
-
: 'text-neutral-300 dark:text-neutral-600'
|
|
59
|
-
"
|
|
60
|
-
/>
|
|
61
|
-
</div>
|
|
62
|
-
<span
|
|
63
|
-
class="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
|
64
|
-
>
|
|
65
|
-
{{ product.rating.toFixed(1) }}
|
|
66
|
-
</span>
|
|
67
|
-
<span
|
|
68
|
-
v-if="product.reviewCount"
|
|
69
|
-
class="text-xs text-neutral-500 dark:text-neutral-400"
|
|
70
|
-
>
|
|
71
|
-
({{ product.reviewCount.toLocaleString() }})
|
|
72
|
-
</span>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<!-- Price -->
|
|
76
|
-
<div v-if="showPrice" class="flex items-baseline gap-2 mb-4">
|
|
77
|
-
<span class="text-xl font-bold text-neutral-900 dark:text-white">
|
|
78
|
-
{{ product.price }}
|
|
79
|
-
</span>
|
|
80
|
-
<span
|
|
81
|
-
v-if="product.originalPrice"
|
|
82
|
-
class="text-sm text-neutral-400 dark:text-neutral-500 line-through"
|
|
83
|
-
>
|
|
84
|
-
{{ product.originalPrice }}
|
|
85
|
-
</span>
|
|
86
|
-
</div>
|
|
87
|
-
|
|
88
|
-
<!-- Pros -->
|
|
89
|
-
<ul v-if="showPros && product.pros?.length" class="space-y-1.5 mb-4">
|
|
90
|
-
<li
|
|
91
|
-
v-for="pro in product.pros"
|
|
92
|
-
:key="pro"
|
|
93
|
-
class="flex items-start gap-2 text-sm text-neutral-700 dark:text-neutral-300"
|
|
94
|
-
>
|
|
95
|
-
<UIcon
|
|
96
|
-
name="i-lucide-check"
|
|
97
|
-
class="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5"
|
|
98
|
-
/>
|
|
99
|
-
<span>{{ pro }}</span>
|
|
100
|
-
</li>
|
|
101
|
-
</ul>
|
|
102
|
-
|
|
103
|
-
<!-- Cons -->
|
|
104
|
-
<ul v-if="showCons && product.cons?.length" class="space-y-1.5 mb-4">
|
|
105
|
-
<li
|
|
106
|
-
v-for="con in product.cons"
|
|
107
|
-
:key="con"
|
|
108
|
-
class="flex items-start gap-2 text-sm text-neutral-700 dark:text-neutral-300"
|
|
109
|
-
>
|
|
110
|
-
<UIcon
|
|
111
|
-
name="i-lucide-x"
|
|
112
|
-
class="w-4 h-4 text-red-500 flex-shrink-0 mt-0.5"
|
|
113
|
-
/>
|
|
114
|
-
<span>{{ con }}</span>
|
|
115
|
-
</li>
|
|
116
|
-
</ul>
|
|
117
|
-
|
|
118
|
-
<!-- Spacer -->
|
|
119
|
-
<div class="flex-1" />
|
|
120
|
-
|
|
121
|
-
<!-- CTA Button -->
|
|
122
|
-
<a
|
|
123
|
-
:href="affiliateHref"
|
|
124
|
-
target="_blank"
|
|
125
|
-
rel="noopener noreferrer"
|
|
126
|
-
class="mt-auto"
|
|
127
|
-
>
|
|
128
|
-
<UButton
|
|
129
|
-
:label="buttonLabel"
|
|
130
|
-
:trailing-icon="buttonIcon"
|
|
131
|
-
color="primary"
|
|
132
|
-
variant="solid"
|
|
133
|
-
size="md"
|
|
134
|
-
block
|
|
135
|
-
class="justify-center"
|
|
136
|
-
/>
|
|
137
|
-
</a>
|
|
138
|
-
|
|
139
|
-
<!-- Disclosure -->
|
|
140
|
-
<p
|
|
141
|
-
v-if="disclosure"
|
|
142
|
-
class="mt-2 text-xs text-neutral-400 dark:text-neutral-500 text-center italic"
|
|
143
|
-
>
|
|
144
|
-
{{ disclosure }}
|
|
145
|
-
</p>
|
|
146
|
-
</div>
|
|
147
|
-
</article>
|
|
148
|
-
|
|
149
|
-
<!-- Row Layout (horizontal) -->
|
|
150
|
-
<article
|
|
151
|
-
v-else-if="layout === 'row'"
|
|
152
|
-
class="group relative flex 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)]"
|
|
153
|
-
>
|
|
154
|
-
<!-- Rank Badge -->
|
|
155
|
-
<div v-if="product.badge" class="absolute top-3 right-3 z-10">
|
|
156
|
-
<UBadge
|
|
157
|
-
:label="product.badge"
|
|
158
|
-
color="primary"
|
|
159
|
-
variant="solid"
|
|
160
|
-
size="xs"
|
|
161
|
-
/>
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
<!-- Product Image -->
|
|
165
|
-
<figure
|
|
166
|
-
class="w-32 h-32 flex-shrink-0 rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-800 border border-neutral-100 dark:border-neutral-700"
|
|
167
|
-
>
|
|
168
|
-
<img
|
|
169
|
-
v-if="product.image"
|
|
170
|
-
:src="product.image"
|
|
171
|
-
:alt="product.name"
|
|
172
|
-
class="w-full h-full object-contain p-2"
|
|
173
|
-
/>
|
|
174
|
-
<div v-else class="w-full h-full flex items-center justify-center">
|
|
175
|
-
<UIcon
|
|
176
|
-
name="i-lucide-package"
|
|
177
|
-
class="w-10 h-10 text-neutral-300 dark:text-neutral-600"
|
|
178
|
-
/>
|
|
179
|
-
</div>
|
|
180
|
-
</figure>
|
|
181
|
-
|
|
182
|
-
<!-- Content -->
|
|
183
|
-
<div class="flex flex-col flex-1 min-w-0">
|
|
184
|
-
<h3
|
|
185
|
-
class="font-semibold text-neutral-900 dark:text-white text-base leading-snug mb-1.5"
|
|
186
|
-
>
|
|
187
|
-
{{ product.name }}
|
|
188
|
-
</h3>
|
|
189
|
-
|
|
190
|
-
<!-- Rating -->
|
|
191
|
-
<div
|
|
192
|
-
v-if="showRating && product.rating"
|
|
193
|
-
class="flex items-center gap-1.5 mb-2"
|
|
194
|
-
>
|
|
195
|
-
<div class="flex items-center gap-0.5">
|
|
196
|
-
<UIcon
|
|
197
|
-
v-for="i in 5"
|
|
198
|
-
:key="i"
|
|
199
|
-
name="i-lucide-star"
|
|
200
|
-
class="w-3 h-3"
|
|
201
|
-
:class="
|
|
202
|
-
i <= Math.round(product.rating)
|
|
203
|
-
? 'text-yellow-400'
|
|
204
|
-
: 'text-neutral-300 dark:text-neutral-600'
|
|
205
|
-
"
|
|
206
|
-
/>
|
|
207
|
-
</div>
|
|
208
|
-
<span class="text-xs text-neutral-500 dark:text-neutral-400">
|
|
209
|
-
{{ product.rating.toFixed(1) }}
|
|
210
|
-
<span v-if="product.reviewCount"
|
|
211
|
-
>({{ product.reviewCount.toLocaleString() }})</span
|
|
212
|
-
>
|
|
213
|
-
</span>
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
<!-- Price -->
|
|
217
|
-
<div v-if="showPrice" class="flex items-baseline gap-2 mb-3">
|
|
218
|
-
<span class="text-lg font-bold text-neutral-900 dark:text-white">
|
|
219
|
-
{{ product.price }}
|
|
220
|
-
</span>
|
|
221
|
-
<span
|
|
222
|
-
v-if="product.originalPrice"
|
|
223
|
-
class="text-sm text-neutral-400 dark:text-neutral-500 line-through"
|
|
224
|
-
>
|
|
225
|
-
{{ product.originalPrice }}
|
|
226
|
-
</span>
|
|
227
|
-
</div>
|
|
228
|
-
|
|
229
|
-
<!-- Pros -->
|
|
230
|
-
<ul v-if="showPros && product.pros?.length" class="space-y-1 mb-3">
|
|
231
|
-
<li
|
|
232
|
-
v-for="pro in product.pros.slice(0, 3)"
|
|
233
|
-
:key="pro"
|
|
234
|
-
class="flex items-start gap-1.5 text-sm text-neutral-700 dark:text-neutral-300"
|
|
235
|
-
>
|
|
236
|
-
<UIcon
|
|
237
|
-
name="i-lucide-check"
|
|
238
|
-
class="w-3.5 h-3.5 text-green-500 flex-shrink-0 mt-0.5"
|
|
239
|
-
/>
|
|
240
|
-
<span>{{ pro }}</span>
|
|
241
|
-
</li>
|
|
242
|
-
</ul>
|
|
243
|
-
|
|
244
|
-
<!-- Cons -->
|
|
245
|
-
<ul v-if="showCons && product.cons?.length" class="space-y-1 mb-3">
|
|
246
|
-
<li
|
|
247
|
-
v-for="con in product.cons.slice(0, 2)"
|
|
248
|
-
:key="con"
|
|
249
|
-
class="flex items-start gap-1.5 text-sm text-neutral-700 dark:text-neutral-300"
|
|
250
|
-
>
|
|
251
|
-
<UIcon
|
|
252
|
-
name="i-lucide-x"
|
|
253
|
-
class="w-3.5 h-3.5 text-red-500 flex-shrink-0 mt-0.5"
|
|
254
|
-
/>
|
|
255
|
-
<span>{{ con }}</span>
|
|
256
|
-
</li>
|
|
257
|
-
</ul>
|
|
258
|
-
|
|
259
|
-
<!-- CTA Button -->
|
|
260
|
-
<a
|
|
261
|
-
:href="affiliateHref"
|
|
262
|
-
target="_blank"
|
|
263
|
-
rel="noopener noreferrer"
|
|
264
|
-
class="mt-auto"
|
|
265
|
-
>
|
|
266
|
-
<UButton
|
|
267
|
-
:label="buttonLabel"
|
|
268
|
-
:trailing-icon="buttonIcon"
|
|
269
|
-
color="primary"
|
|
270
|
-
variant="solid"
|
|
271
|
-
size="sm"
|
|
272
|
-
/>
|
|
273
|
-
</a>
|
|
274
|
-
|
|
275
|
-
<!-- Disclosure -->
|
|
276
|
-
<p
|
|
277
|
-
v-if="disclosure"
|
|
278
|
-
class="mt-1.5 text-xs text-neutral-400 dark:text-neutral-500 italic"
|
|
279
|
-
>
|
|
280
|
-
{{ disclosure }}
|
|
281
|
-
</p>
|
|
282
|
-
</div>
|
|
283
|
-
</article>
|
|
284
|
-
|
|
285
|
-
<!-- Compact Layout (minimal one-row) -->
|
|
286
|
-
<article
|
|
287
|
-
v-else-if="layout === 'compact'"
|
|
288
|
-
class="group flex items-center gap-3 py-3 px-3 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 transition-all duration-200 hover:bg-neutral-50 dark:hover:bg-neutral-800/50"
|
|
289
|
-
>
|
|
290
|
-
<!-- Product Image -->
|
|
291
|
-
<figure
|
|
292
|
-
class="w-16 h-16 flex-shrink-0 rounded-lg overflow-hidden bg-neutral-50 dark:bg-neutral-800 border border-neutral-100 dark:border-neutral-700"
|
|
293
|
-
>
|
|
294
|
-
<img
|
|
295
|
-
v-if="product.image"
|
|
296
|
-
:src="product.image"
|
|
297
|
-
:alt="product.name"
|
|
298
|
-
class="w-full h-full object-contain p-1"
|
|
299
|
-
/>
|
|
300
|
-
<div v-else class="w-full h-full flex items-center justify-center">
|
|
301
|
-
<UIcon
|
|
302
|
-
name="i-lucide-package"
|
|
303
|
-
class="w-7 h-7 text-neutral-300 dark:text-neutral-600"
|
|
304
|
-
/>
|
|
305
|
-
</div>
|
|
306
|
-
</figure>
|
|
307
|
-
|
|
308
|
-
<!-- Name + Rating -->
|
|
309
|
-
<div class="flex-1 min-w-0">
|
|
310
|
-
<div class="flex items-center gap-2 flex-wrap">
|
|
311
|
-
<h3
|
|
312
|
-
class="font-medium text-sm text-neutral-900 dark:text-white truncate"
|
|
313
|
-
>
|
|
314
|
-
{{ product.name }}
|
|
315
|
-
</h3>
|
|
316
|
-
<UBadge
|
|
317
|
-
v-if="product.badge"
|
|
318
|
-
:label="product.badge"
|
|
319
|
-
color="primary"
|
|
320
|
-
variant="subtle"
|
|
321
|
-
size="xs"
|
|
322
|
-
/>
|
|
323
|
-
</div>
|
|
324
|
-
<div
|
|
325
|
-
v-if="showRating && product.rating"
|
|
326
|
-
class="flex items-center gap-1 mt-0.5"
|
|
327
|
-
>
|
|
328
|
-
<UIcon name="i-lucide-star" class="w-3 h-3 text-yellow-400" />
|
|
329
|
-
<span class="text-xs text-neutral-500 dark:text-neutral-400">{{
|
|
330
|
-
product.rating.toFixed(1)
|
|
331
|
-
}}</span>
|
|
332
|
-
</div>
|
|
333
|
-
</div>
|
|
334
|
-
|
|
335
|
-
<!-- Price -->
|
|
336
|
-
<div v-if="showPrice" class="flex-shrink-0 text-right">
|
|
337
|
-
<div class="font-bold text-sm text-neutral-900 dark:text-white">
|
|
338
|
-
{{ product.price }}
|
|
339
|
-
</div>
|
|
340
|
-
<div
|
|
341
|
-
v-if="product.originalPrice"
|
|
342
|
-
class="text-xs text-neutral-400 line-through"
|
|
343
|
-
>
|
|
344
|
-
{{ product.originalPrice }}
|
|
345
|
-
</div>
|
|
346
|
-
</div>
|
|
347
|
-
|
|
348
|
-
<!-- CTA Button -->
|
|
349
|
-
<a
|
|
350
|
-
:href="affiliateHref"
|
|
351
|
-
target="_blank"
|
|
352
|
-
rel="noopener noreferrer"
|
|
353
|
-
class="flex-shrink-0"
|
|
354
|
-
>
|
|
355
|
-
<UButton
|
|
356
|
-
:label="buttonLabel"
|
|
357
|
-
:trailing-icon="buttonIcon"
|
|
358
|
-
color="primary"
|
|
359
|
-
variant="solid"
|
|
360
|
-
size="xs"
|
|
361
|
-
/>
|
|
362
|
-
</a>
|
|
363
|
-
</article>
|
|
364
|
-
</template>
|
|
365
|
-
|
|
366
|
-
<script setup>
|
|
367
|
-
const props = defineProps({
|
|
368
|
-
/** Product data object */
|
|
369
|
-
product: {
|
|
370
|
-
type: Object,
|
|
371
|
-
default: () => ({
|
|
372
|
-
name: "Product Name",
|
|
373
|
-
price: "$99.99",
|
|
374
|
-
rating: 4.5,
|
|
375
|
-
reviewCount: 1200,
|
|
376
|
-
pros: ["Great quality", "Easy to use", "Good value"],
|
|
377
|
-
affiliateUrl: "#",
|
|
378
|
-
}),
|
|
379
|
-
},
|
|
380
|
-
/** Card display layout */
|
|
381
|
-
layout: {
|
|
382
|
-
type: String,
|
|
383
|
-
default: "card",
|
|
384
|
-
validator: (v) => ["card", "row", "compact"].includes(v),
|
|
385
|
-
},
|
|
386
|
-
/** Show pros list */
|
|
387
|
-
showPros: {
|
|
388
|
-
type: Boolean,
|
|
389
|
-
default: true,
|
|
390
|
-
},
|
|
391
|
-
/** Show cons list */
|
|
392
|
-
showCons: {
|
|
393
|
-
type: Boolean,
|
|
394
|
-
default: false,
|
|
395
|
-
},
|
|
396
|
-
/** Show star rating */
|
|
397
|
-
showRating: {
|
|
398
|
-
type: Boolean,
|
|
399
|
-
default: true,
|
|
400
|
-
},
|
|
401
|
-
/** Show price */
|
|
402
|
-
showPrice: {
|
|
403
|
-
type: Boolean,
|
|
404
|
-
default: true,
|
|
405
|
-
},
|
|
406
|
-
/** CTA button label */
|
|
407
|
-
buttonLabel: {
|
|
408
|
-
type: String,
|
|
409
|
-
default: "Check Price on Amazon",
|
|
410
|
-
},
|
|
411
|
-
/** CTA button trailing icon */
|
|
412
|
-
buttonIcon: {
|
|
413
|
-
type: String,
|
|
414
|
-
default: "i-lucide-external-link",
|
|
415
|
-
},
|
|
416
|
-
/** Small disclosure text shown below the button */
|
|
417
|
-
disclosure: {
|
|
418
|
-
type: String,
|
|
419
|
-
default: "",
|
|
420
|
-
},
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
/** Build the affiliate URL, appending tag param if affiliateTag is set */
|
|
424
|
-
const affiliateHref = computed(() => {
|
|
425
|
-
const url = props.product.affiliateUrl || "#";
|
|
426
|
-
const tag = props.product.affiliateTag;
|
|
427
|
-
if (!tag || url === "#") return url;
|
|
428
|
-
const separator = url.includes("?") ? "&" : "?";
|
|
429
|
-
return `${url}${separator}tag=${tag}`;
|
|
430
|
-
});
|
|
431
|
-
</script>
|