@xenterprises/nuxt-x-marketing 1.0.0 → 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 +2 -1
- package/.playground/app.vue +12 -6
- package/.playground/pages/blog/detail.vue +341 -249
- package/.playground/pages/index.vue +2 -2
- package/.playground/pages/layout/x-legal.vue +12 -12
- package/.playground/pages/notifications/social-proof-toast.vue +330 -70
- package/LICENSE +60 -0
- package/README.md +210 -64
- package/app/app.config.ts +2 -72
- package/app/assets/css/x-marketing.css +1 -0
- 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 +82 -127
- 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/CodeBlock/index.vue +69 -0
- 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 +155 -102
- 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/nuxt.config.ts +12 -3
- package/package.json +5 -2
- package/screenshots/playground-home.png +0 -0
|
@@ -4,15 +4,20 @@
|
|
|
4
4
|
* From spec-prompt.md specification
|
|
5
5
|
*/
|
|
6
6
|
export const useParallax = (options = {}) => {
|
|
7
|
+
// Only register lifecycle hooks when inside a component instance
|
|
8
|
+
const instance = getCurrentInstance();
|
|
9
|
+
if (!instance) return { initParallax: () => {}, cleanup: () => {} };
|
|
10
|
+
|
|
7
11
|
const defaultOptions = {
|
|
8
12
|
defaultSpeed: 0.5,
|
|
9
13
|
...options,
|
|
10
14
|
};
|
|
11
15
|
|
|
12
16
|
const elements = ref<Element[]>([]);
|
|
17
|
+
let handleScroll: (() => void) | null = null;
|
|
13
18
|
|
|
14
19
|
const initParallax = () => {
|
|
15
|
-
if (
|
|
20
|
+
if (import.meta.client) {
|
|
16
21
|
// Find all elements with xParallax class and data-parallax-speed attribute
|
|
17
22
|
const parallaxElements = document.querySelectorAll('.xParallax[data-parallax-speed]');
|
|
18
23
|
elements.value = Array.from(parallaxElements);
|
|
@@ -37,7 +42,7 @@ export const useParallax = (options = {}) => {
|
|
|
37
42
|
|
|
38
43
|
// Use requestAnimationFrame for smoother updates
|
|
39
44
|
let ticking = false;
|
|
40
|
-
|
|
45
|
+
handleScroll = () => {
|
|
41
46
|
if (!ticking) {
|
|
42
47
|
window.requestAnimationFrame(() => {
|
|
43
48
|
updateParallax();
|
|
@@ -51,25 +56,27 @@ export const useParallax = (options = {}) => {
|
|
|
51
56
|
|
|
52
57
|
// Initial update
|
|
53
58
|
updateParallax();
|
|
54
|
-
|
|
55
|
-
onUnmounted(() => {
|
|
56
|
-
window.removeEventListener('scroll', handleScroll);
|
|
57
|
-
});
|
|
58
59
|
}
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
const cleanup = () => {
|
|
63
|
+
if (handleScroll) {
|
|
64
|
+
window.removeEventListener('scroll', handleScroll);
|
|
65
|
+
handleScroll = null;
|
|
66
|
+
}
|
|
62
67
|
elements.value = [];
|
|
63
68
|
};
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
if (import.meta.client) {
|
|
71
|
+
onMounted(() => {
|
|
72
|
+
// Small delay to ensure DOM is ready
|
|
73
|
+
setTimeout(initParallax, 100);
|
|
74
|
+
});
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
onUnmounted(() => {
|
|
77
|
+
cleanup();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
73
80
|
|
|
74
81
|
return {
|
|
75
82
|
initParallax,
|
|
@@ -83,9 +90,10 @@ export const useParallax = (options = {}) => {
|
|
|
83
90
|
*/
|
|
84
91
|
export const useElementParallax = (speed = 0.5) => {
|
|
85
92
|
const elementRef = ref<HTMLElement | null>(null);
|
|
93
|
+
let handleScroll: (() => void) | null = null;
|
|
86
94
|
|
|
87
95
|
onMounted(() => {
|
|
88
|
-
if (
|
|
96
|
+
if (import.meta.client && elementRef.value) {
|
|
89
97
|
let ticking = false;
|
|
90
98
|
|
|
91
99
|
const updateParallax = () => {
|
|
@@ -99,7 +107,7 @@ export const useElementParallax = (speed = 0.5) => {
|
|
|
99
107
|
elementRef.value.style.transform = `translateY(${offset}px)`;
|
|
100
108
|
};
|
|
101
109
|
|
|
102
|
-
|
|
110
|
+
handleScroll = () => {
|
|
103
111
|
if (!ticking) {
|
|
104
112
|
window.requestAnimationFrame(() => {
|
|
105
113
|
updateParallax();
|
|
@@ -111,13 +119,18 @@ export const useElementParallax = (speed = 0.5) => {
|
|
|
111
119
|
|
|
112
120
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
113
121
|
updateParallax();
|
|
114
|
-
|
|
115
|
-
onUnmounted(() => {
|
|
116
|
-
window.removeEventListener('scroll', handleScroll);
|
|
117
|
-
});
|
|
118
122
|
}
|
|
119
123
|
});
|
|
120
124
|
|
|
125
|
+
if (import.meta.client) {
|
|
126
|
+
onUnmounted(() => {
|
|
127
|
+
if (handleScroll) {
|
|
128
|
+
window.removeEventListener('scroll', handleScroll);
|
|
129
|
+
handleScroll = null;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
121
134
|
return {
|
|
122
135
|
elementRef,
|
|
123
136
|
};
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
* - .xFadeUp, .xFadeIn, .xFadeLeft, .xFadeRight, .xScale classes
|
|
9
9
|
* - .xFadeUp-stagger for staggered child animations
|
|
10
10
|
*/
|
|
11
|
-
export const useScrollReveal = (options = {}) => {
|
|
11
|
+
export const useScrollReveal = (options: Record<string, unknown> = {}) => {
|
|
12
|
+
// Only register lifecycle hooks when inside a component instance
|
|
13
|
+
const instance = getCurrentInstance();
|
|
14
|
+
if (!instance) return { initScrollReveal: () => {}, cleanup: () => {} };
|
|
15
|
+
|
|
12
16
|
const defaultOptions = {
|
|
13
17
|
threshold: 0.15,
|
|
14
18
|
rootMargin: '0px 0px -80px 0px',
|
|
@@ -29,7 +33,7 @@ export const useScrollReveal = (options = {}) => {
|
|
|
29
33
|
].join(', ');
|
|
30
34
|
|
|
31
35
|
const initScrollReveal = () => {
|
|
32
|
-
if (
|
|
36
|
+
if (import.meta.client) {
|
|
33
37
|
observer.value = new IntersectionObserver((entries) => {
|
|
34
38
|
entries.forEach((entry) => {
|
|
35
39
|
if (entry.isIntersecting) {
|
|
@@ -54,17 +58,20 @@ export const useScrollReveal = (options = {}) => {
|
|
|
54
58
|
const cleanup = () => {
|
|
55
59
|
if (observer.value) {
|
|
56
60
|
observer.value.disconnect();
|
|
61
|
+
observer.value = null;
|
|
57
62
|
}
|
|
58
63
|
};
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
if (import.meta.client) {
|
|
66
|
+
onMounted(() => {
|
|
67
|
+
// Small delay to ensure DOM is ready
|
|
68
|
+
setTimeout(initScrollReveal, 100);
|
|
69
|
+
});
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
onUnmounted(() => {
|
|
72
|
+
cleanup();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
68
75
|
|
|
69
76
|
return {
|
|
70
77
|
initScrollReveal,
|
|
@@ -76,8 +83,8 @@ export const useScrollReveal = (options = {}) => {
|
|
|
76
83
|
* Helper to add staggered reveal delays to child elements
|
|
77
84
|
*/
|
|
78
85
|
export const useStaggerReveal = (containerSelector: string, staggerDelay = 100) => {
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
if (import.meta.client) {
|
|
87
|
+
onMounted(() => {
|
|
81
88
|
setTimeout(() => {
|
|
82
89
|
const container = document.querySelector(containerSelector);
|
|
83
90
|
if (container) {
|
|
@@ -87,6 +94,6 @@ export const useStaggerReveal = (containerSelector: string, staggerDelay = 100)
|
|
|
87
94
|
});
|
|
88
95
|
}
|
|
89
96
|
}, 100);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
92
99
|
};
|
package/app/pages/index.vue
CHANGED
|
@@ -10,9 +10,7 @@
|
|
|
10
10
|
overlay="gradient"
|
|
11
11
|
>
|
|
12
12
|
<template #actions>
|
|
13
|
-
<UButton size="xl" color="white">
|
|
14
|
-
Get Started Free
|
|
15
|
-
</UButton>
|
|
13
|
+
<UButton size="xl" color="white"> Get Started Free </UButton>
|
|
16
14
|
<UButton size="xl" variant="outline" color="white">
|
|
17
15
|
Watch Demo
|
|
18
16
|
</UButton>
|
|
@@ -23,41 +21,41 @@
|
|
|
23
21
|
<XMarkSection id="features" bg="default" padding="xl">
|
|
24
22
|
<header class="text-center max-w-3xl mx-auto mb-16">
|
|
25
23
|
<p class="xText-eyebrow text-primary-500 mb-4">Features</p>
|
|
26
|
-
<h2
|
|
24
|
+
<h2
|
|
25
|
+
class="xText-headline text-neutral-900 dark:text-white xText-balance"
|
|
26
|
+
>
|
|
27
27
|
Everything you need to succeed
|
|
28
28
|
</h2>
|
|
29
29
|
<p class="mt-4 text-lg text-neutral-600 dark:text-neutral-400">
|
|
30
|
-
Powerful tools designed to help you build, launch, and grow your
|
|
30
|
+
Powerful tools designed to help you build, launch, and grow your
|
|
31
|
+
product.
|
|
31
32
|
</p>
|
|
32
33
|
</header>
|
|
33
34
|
|
|
34
|
-
<XMarkFeatures
|
|
35
|
-
:features="features"
|
|
36
|
-
layout="grid"
|
|
37
|
-
:columns="3"
|
|
38
|
-
/>
|
|
35
|
+
<XMarkFeatures :features="features" layout="grid" :columns="3" />
|
|
39
36
|
</XMarkSection>
|
|
40
37
|
|
|
41
38
|
<!-- Testimonials Section -->
|
|
42
39
|
<XMarkSection bg="subtle" padding="xl">
|
|
43
40
|
<header class="text-center max-w-3xl mx-auto mb-16">
|
|
44
41
|
<p class="xText-eyebrow text-primary-500 mb-4">Testimonials</p>
|
|
45
|
-
<h2
|
|
42
|
+
<h2
|
|
43
|
+
class="xText-headline text-neutral-900 dark:text-white xText-balance"
|
|
44
|
+
>
|
|
46
45
|
Loved by teams worldwide
|
|
47
46
|
</h2>
|
|
48
47
|
</header>
|
|
49
48
|
|
|
50
|
-
<XMarkTestimonials
|
|
51
|
-
:testimonials="testimonials"
|
|
52
|
-
layout="grid"
|
|
53
|
-
/>
|
|
49
|
+
<XMarkTestimonials :testimonials="testimonials" layout="grid" />
|
|
54
50
|
</XMarkSection>
|
|
55
51
|
|
|
56
52
|
<!-- Pricing Section -->
|
|
57
53
|
<XMarkSection bg="default" padding="xl">
|
|
58
54
|
<header class="text-center max-w-3xl mx-auto mb-16">
|
|
59
55
|
<p class="xText-eyebrow text-primary-500 mb-4">Pricing</p>
|
|
60
|
-
<h2
|
|
56
|
+
<h2
|
|
57
|
+
class="xText-headline text-neutral-900 dark:text-white xText-balance"
|
|
58
|
+
>
|
|
61
59
|
Simple, transparent pricing
|
|
62
60
|
</h2>
|
|
63
61
|
<p class="mt-4 text-lg text-neutral-600 dark:text-neutral-400">
|
|
@@ -65,24 +63,18 @@
|
|
|
65
63
|
</p>
|
|
66
64
|
</header>
|
|
67
65
|
|
|
68
|
-
<XMarkPricing
|
|
69
|
-
:plans="pricingPlans"
|
|
70
|
-
/>
|
|
66
|
+
<XMarkPricing :plans="pricingPlans" />
|
|
71
67
|
</XMarkSection>
|
|
72
68
|
|
|
73
69
|
<!-- CTA Section -->
|
|
74
70
|
<XMarkSection bg="bold" padding="xl">
|
|
75
71
|
<div class="text-center max-w-3xl mx-auto">
|
|
76
|
-
<h2 class="xText-headline xText-balance">
|
|
77
|
-
Ready to get started?
|
|
78
|
-
</h2>
|
|
72
|
+
<h2 class="xText-headline xText-balance">Ready to get started?</h2>
|
|
79
73
|
<p class="mt-4 text-lg opacity-80">
|
|
80
74
|
Join thousands of teams already using our platform.
|
|
81
75
|
</p>
|
|
82
76
|
<div class="mt-8 flex flex-wrap justify-center gap-4">
|
|
83
|
-
<UButton size="xl" color="white">
|
|
84
|
-
Start Free Trial
|
|
85
|
-
</UButton>
|
|
77
|
+
<UButton size="xl" color="white"> Start Free Trial </UButton>
|
|
86
78
|
<UButton size="xl" variant="outline" color="white">
|
|
87
79
|
Contact Sales
|
|
88
80
|
</UButton>
|
|
@@ -93,13 +85,123 @@
|
|
|
93
85
|
</div>
|
|
94
86
|
</div>
|
|
95
87
|
</XMarkSection>
|
|
88
|
+
|
|
89
|
+
<!-- ─── Affiliate Components Demo ─── -->
|
|
90
|
+
|
|
91
|
+
<!-- Affiliate Disclosure variants -->
|
|
92
|
+
<XMarkSection bg="subtle" padding="xl">
|
|
93
|
+
<header class="text-center max-w-3xl mx-auto mb-12">
|
|
94
|
+
<p class="xText-eyebrow text-primary-500 mb-4">Affiliate Components</p>
|
|
95
|
+
<h2
|
|
96
|
+
class="xText-headline text-neutral-900 dark:text-white xText-balance"
|
|
97
|
+
>
|
|
98
|
+
Affiliate & Review Components
|
|
99
|
+
</h2>
|
|
100
|
+
<p class="mt-4 text-lg text-neutral-600 dark:text-neutral-400">
|
|
101
|
+
FTC-compliant disclosures, product cards, grids, detail pages, and
|
|
102
|
+
comparison tables.
|
|
103
|
+
</p>
|
|
104
|
+
</header>
|
|
105
|
+
|
|
106
|
+
<!-- Disclosure variants -->
|
|
107
|
+
<div class="max-w-3xl mx-auto space-y-4 mb-16">
|
|
108
|
+
<h3
|
|
109
|
+
class="font-semibold text-neutral-700 dark:text-neutral-300 text-sm uppercase tracking-wider mb-4"
|
|
110
|
+
>
|
|
111
|
+
Disclosure Variants
|
|
112
|
+
</h3>
|
|
113
|
+
<XMarkAffiliateDisclosure variant="subtle" />
|
|
114
|
+
<XMarkAffiliateDisclosure variant="banner" />
|
|
115
|
+
<XMarkAffiliateDisclosure variant="inline" />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<!-- Product Cards — all 3 layouts -->
|
|
119
|
+
<div class="mb-16">
|
|
120
|
+
<h3
|
|
121
|
+
class="font-semibold text-neutral-700 dark:text-neutral-300 text-sm uppercase tracking-wider mb-6"
|
|
122
|
+
>
|
|
123
|
+
Product Card Layouts
|
|
124
|
+
</h3>
|
|
125
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
|
126
|
+
<XMarkAffiliateProductCard
|
|
127
|
+
:product="sampleProducts[0]"
|
|
128
|
+
layout="card"
|
|
129
|
+
:show-pros="true"
|
|
130
|
+
/>
|
|
131
|
+
<XMarkAffiliateProductCard
|
|
132
|
+
:product="sampleProducts[1]"
|
|
133
|
+
layout="card"
|
|
134
|
+
:show-pros="true"
|
|
135
|
+
:show-cons="true"
|
|
136
|
+
/>
|
|
137
|
+
<XMarkAffiliateProductCard
|
|
138
|
+
:product="sampleProducts[2]"
|
|
139
|
+
layout="card"
|
|
140
|
+
:show-pros="true"
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="space-y-4 mb-6">
|
|
144
|
+
<XMarkAffiliateProductCard
|
|
145
|
+
:product="sampleProducts[0]"
|
|
146
|
+
layout="row"
|
|
147
|
+
:show-pros="true"
|
|
148
|
+
/>
|
|
149
|
+
<XMarkAffiliateProductCard
|
|
150
|
+
:product="sampleProducts[1]"
|
|
151
|
+
layout="row"
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="space-y-2">
|
|
155
|
+
<XMarkAffiliateProductCard
|
|
156
|
+
v-for="p in sampleProducts"
|
|
157
|
+
:key="p.name"
|
|
158
|
+
:product="p"
|
|
159
|
+
layout="compact"
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<!-- Product Grid -->
|
|
165
|
+
<div class="mb-16">
|
|
166
|
+
<h3
|
|
167
|
+
class="font-semibold text-neutral-700 dark:text-neutral-300 text-sm uppercase tracking-wider mb-6"
|
|
168
|
+
>
|
|
169
|
+
Product Grid (Roundup Post)
|
|
170
|
+
</h3>
|
|
171
|
+
<XMarkAffiliateProductGrid
|
|
172
|
+
:products="sampleProducts"
|
|
173
|
+
layout="grid"
|
|
174
|
+
:columns="3"
|
|
175
|
+
:show-ranking="true"
|
|
176
|
+
disclosure="This page contains affiliate links. As an Amazon Associate, we earn from qualifying purchases."
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- Comparison Table -->
|
|
181
|
+
<div class="mb-16">
|
|
182
|
+
<h3
|
|
183
|
+
class="font-semibold text-neutral-700 dark:text-neutral-300 text-sm uppercase tracking-wider mb-6"
|
|
184
|
+
>
|
|
185
|
+
Comparison Table (X vs Y)
|
|
186
|
+
</h3>
|
|
187
|
+
<XMarkAffiliateComparisonTable
|
|
188
|
+
:products="comparisonProducts"
|
|
189
|
+
:specs="comparisonSpecs"
|
|
190
|
+
:show-buy-buttons="true"
|
|
191
|
+
:highlight-best="true"
|
|
192
|
+
button-label="Buy on Amazon"
|
|
193
|
+
disclosure="This page contains affiliate links."
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
</XMarkSection>
|
|
96
197
|
</div>
|
|
97
198
|
</template>
|
|
98
199
|
|
|
99
200
|
<script setup>
|
|
100
201
|
useSeoMeta({
|
|
101
202
|
title: "Modern Platform for Teams",
|
|
102
|
-
description:
|
|
203
|
+
description:
|
|
204
|
+
"Build, launch, and grow your product with our powerful platform.",
|
|
103
205
|
});
|
|
104
206
|
|
|
105
207
|
// Features data
|
|
@@ -139,7 +241,8 @@ const features = [
|
|
|
139
241
|
// Testimonials data
|
|
140
242
|
const testimonials = [
|
|
141
243
|
{
|
|
142
|
-
quote:
|
|
244
|
+
quote:
|
|
245
|
+
"This platform transformed how our team works. We shipped 3x faster within the first month.",
|
|
143
246
|
name: "Sarah Chen",
|
|
144
247
|
title: "CTO",
|
|
145
248
|
company: "TechStart",
|
|
@@ -153,7 +256,8 @@ const testimonials = [
|
|
|
153
256
|
avatar: "https://randomuser.me/api/portraits/men/32.jpg",
|
|
154
257
|
},
|
|
155
258
|
{
|
|
156
|
-
quote:
|
|
259
|
+
quote:
|
|
260
|
+
"Finally, a platform that understands what modern teams need. Highly recommended.",
|
|
157
261
|
name: "Emily Park",
|
|
158
262
|
title: "Product Lead",
|
|
159
263
|
company: "InnovateCo",
|
|
@@ -161,6 +265,104 @@ const testimonials = [
|
|
|
161
265
|
},
|
|
162
266
|
];
|
|
163
267
|
|
|
268
|
+
// Affiliate sample products
|
|
269
|
+
const sampleProducts = [
|
|
270
|
+
{
|
|
271
|
+
name: "Sony WH-1000XM5",
|
|
272
|
+
image:
|
|
273
|
+
"https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=400&q=80",
|
|
274
|
+
price: "$279.99",
|
|
275
|
+
originalPrice: "$349.99",
|
|
276
|
+
rating: 4.8,
|
|
277
|
+
reviewCount: 12400,
|
|
278
|
+
pros: ["Best-in-class ANC", "30hr battery life", "Foldable design"],
|
|
279
|
+
cons: ["No IP rating", "Expensive"],
|
|
280
|
+
affiliateUrl: "https://amazon.com",
|
|
281
|
+
affiliateTag: "mysite-20",
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "Bose QuietComfort 45",
|
|
285
|
+
image:
|
|
286
|
+
"https://images.unsplash.com/photo-1484704849700-f032a568e944?w=400&q=80",
|
|
287
|
+
price: "$249.99",
|
|
288
|
+
rating: 4.6,
|
|
289
|
+
reviewCount: 8900,
|
|
290
|
+
pros: ["Comfortable fit", "Great sound quality", "Easy controls"],
|
|
291
|
+
cons: ["No multipoint", "Older design"],
|
|
292
|
+
affiliateUrl: "https://amazon.com",
|
|
293
|
+
affiliateTag: "mysite-20",
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "Apple AirPods Max",
|
|
297
|
+
image:
|
|
298
|
+
"https://images.unsplash.com/photo-1585386959984-a4155224a1ad?w=400&q=80",
|
|
299
|
+
price: "$449.00",
|
|
300
|
+
rating: 4.7,
|
|
301
|
+
reviewCount: 6200,
|
|
302
|
+
pros: ["Premium build quality", "Excellent ANC", "Spatial audio"],
|
|
303
|
+
cons: ["Very expensive", "Heavy", "No USB-C charging"],
|
|
304
|
+
affiliateUrl: "https://amazon.com",
|
|
305
|
+
affiliateTag: "mysite-20",
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
// Comparison table data
|
|
310
|
+
const comparisonProducts = [
|
|
311
|
+
{
|
|
312
|
+
name: "Sony WH-1000XM5",
|
|
313
|
+
image:
|
|
314
|
+
"https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=200&q=80",
|
|
315
|
+
price: "$279.99",
|
|
316
|
+
affiliateUrl: "https://amazon.com",
|
|
317
|
+
affiliateTag: "mysite-20",
|
|
318
|
+
specs: {
|
|
319
|
+
price: "$279.99",
|
|
320
|
+
rating: "4.8/5",
|
|
321
|
+
battery: "30 hours",
|
|
322
|
+
weight: "250g",
|
|
323
|
+
anc: "Yes",
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "Bose QC45",
|
|
328
|
+
image:
|
|
329
|
+
"https://images.unsplash.com/photo-1484704849700-f032a568e944?w=200&q=80",
|
|
330
|
+
price: "$249.99",
|
|
331
|
+
affiliateUrl: "https://amazon.com",
|
|
332
|
+
affiliateTag: "mysite-20",
|
|
333
|
+
specs: {
|
|
334
|
+
price: "$249.99",
|
|
335
|
+
rating: "4.6/5",
|
|
336
|
+
battery: "24 hours",
|
|
337
|
+
weight: "238g",
|
|
338
|
+
anc: "Yes",
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "AirPods Max",
|
|
343
|
+
image:
|
|
344
|
+
"https://images.unsplash.com/photo-1585386959984-a4155224a1ad?w=200&q=80",
|
|
345
|
+
price: "$449.00",
|
|
346
|
+
affiliateUrl: "https://amazon.com",
|
|
347
|
+
affiliateTag: "mysite-20",
|
|
348
|
+
specs: {
|
|
349
|
+
price: "$449.00",
|
|
350
|
+
rating: "4.7/5",
|
|
351
|
+
battery: "20 hours",
|
|
352
|
+
weight: "385g",
|
|
353
|
+
anc: "Yes",
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
const comparisonSpecs = [
|
|
359
|
+
{ label: "Price", key: "price" },
|
|
360
|
+
{ label: "Rating", key: "rating" },
|
|
361
|
+
{ label: "Battery Life", key: "battery" },
|
|
362
|
+
{ label: "Weight", key: "weight" },
|
|
363
|
+
{ label: "ANC", key: "anc" },
|
|
364
|
+
];
|
|
365
|
+
|
|
164
366
|
// Pricing plans data
|
|
165
367
|
const pricingPlans = [
|
|
166
368
|
{
|