@xenterprises/nuxt-x-marketing 0.0.23 → 1.0.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/.claude/settings.local.json +22 -0
- package/.playground/app.config.ts +44 -45
- package/.playground/app.vue +263 -0
- package/.playground/components/AppFooter.vue +103 -0
- package/.playground/components/AppHeader.vue +41 -0
- package/.playground/components/CodeExample.vue +70 -0
- package/.playground/components/ComponentPlayground.vue +117 -0
- package/.playground/components/DocsHeader.vue +19 -0
- package/.playground/layouts/default.vue +13 -3
- package/.playground/layouts/docs-layout.vue +161 -0
- package/.playground/layouts/docs.vue +11 -0
- package/.playground/package-lock.json +6 -0
- package/.playground/pages/blog/author.vue +145 -0
- package/.playground/pages/blog/card.vue +149 -0
- package/.playground/pages/blog/cta.vue +131 -0
- package/.playground/pages/blog/detail.vue +379 -0
- package/.playground/pages/blog/list.vue +261 -0
- package/.playground/pages/blog/navigation.vue +228 -0
- package/.playground/pages/blog/sidebar.vue +279 -0
- package/.playground/pages/config.vue +289 -0
- package/.playground/pages/content/comparison.vue +253 -0
- package/.playground/pages/content/cta.vue +245 -0
- package/.playground/pages/content/features.vue +217 -0
- package/.playground/pages/content/newsletter-section.vue +260 -0
- package/.playground/pages/content/newsletter.vue +200 -0
- package/.playground/pages/content/pricing.vue +335 -0
- package/.playground/pages/content/testimonials.vue +188 -0
- package/.playground/pages/hero.vue +257 -0
- package/.playground/pages/index.vue +379 -2
- package/.playground/pages/layout/footer.vue +210 -0
- package/.playground/pages/layout/legal-footer.vue +121 -0
- package/.playground/pages/layout/navbar.vue +285 -0
- package/.playground/pages/layout/section.vue +266 -0
- package/.playground/pages/layout/x-legal.vue +199 -0
- package/.playground/pages/modals/feature-modal.vue +320 -0
- package/.playground/pages/modals/image-lightbox.vue +277 -0
- package/.playground/pages/modals/video-modal.vue +222 -0
- package/.playground/pages/notifications/announcement-bar.vue +164 -0
- package/.playground/pages/notifications/cookie-banner.vue +210 -0
- package/.playground/pages/notifications/cookie-toast.vue +143 -0
- package/.playground/pages/notifications/gdpr.vue +276 -0
- package/.playground/pages/notifications/social-proof-toast.vue +255 -0
- package/.playground/pages/ui/badge.vue +161 -0
- package/.playground/pages/ui/bg-pattern.vue +177 -0
- package/.playground/pages/ui/glass-card.vue +179 -0
- package/.playground/pages/ui/glow-divider.vue +179 -0
- package/.playground/pages/ui/promo-card.vue +259 -0
- package/.playground/pages/ui/section-stitch.vue +411 -0
- package/.playground/pages/utilities/back-to-top.vue +153 -0
- package/.playground/pages/utilities/popup-chat.vue +224 -0
- package/README.md +799 -164
- package/app/app.config.ts +88 -2
- package/app/app.vue +106 -11
- package/app/assets/css/x-marketing.css +668 -0
- package/app/components/X/Footer/XLegal/index.vue +63 -26
- package/app/components/X/Footer/index.vue +89 -89
- package/app/components/X/Header/App.vue +63 -0
- package/app/components/X/Mark/Blog/Author.vue +170 -0
- package/app/components/X/Mark/Blog/CTANewsletter.vue +197 -0
- package/app/components/X/Mark/Blog/Card.vue +117 -0
- package/app/components/X/Mark/Blog/Detail.vue +240 -0
- package/app/components/X/Mark/Blog/List.vue +156 -0
- package/app/components/X/Mark/Blog/Navigation.vue +100 -0
- package/app/components/X/Mark/Blog/Sidebar.vue +160 -0
- package/app/components/X/Mark/Button/BackToTop.vue +99 -0
- package/app/components/X/Mark/CTA/index.vue +294 -0
- package/app/components/X/Mark/Card/Glass.vue +64 -0
- package/app/components/X/Mark/Card/Promo.vue +164 -0
- package/app/components/X/Mark/Divider/Glow.vue +113 -0
- package/app/components/X/Mark/Features/index.vue +194 -0
- package/app/components/X/Mark/Hero/index.vue +217 -0
- package/app/components/X/Mark/Layout/AnnouncementBar.vue +139 -0
- package/app/components/X/Mark/Layout/Footer.vue +160 -0
- package/app/components/X/Mark/Layout/FooterLegal.vue +42 -0
- package/app/components/X/Mark/Layout/Navbar.vue +267 -0
- package/app/components/X/Mark/Modal/Chat.vue +151 -0
- package/app/components/X/Mark/Modal/Feature.vue +197 -0
- package/app/components/X/Mark/Modal/Image.vue +226 -0
- package/app/components/X/Mark/Modal/Video.vue +203 -0
- package/app/components/X/Mark/Modal/index.vue +133 -0
- package/app/components/X/Mark/Newsletter/Form.vue +174 -0
- package/app/components/X/Mark/Pattern/Bg.vue +86 -0
- package/app/components/X/Mark/Pricing/Comparison.vue +126 -0
- package/app/components/X/Mark/Pricing/Plans.vue +181 -0
- package/app/components/X/Mark/Privacy/CookieBanner.vue +112 -0
- package/app/components/X/Mark/Privacy/CookieToast.vue +43 -0
- package/app/components/X/Mark/Privacy/GDPR.vue +245 -0
- package/app/components/X/Mark/Section/Newsletter.vue +183 -0
- package/app/components/X/Mark/Section/Stitch.vue +156 -0
- package/app/components/X/Mark/Section/index.vue +151 -0
- package/app/components/X/Mark/SocialProof/Badge.vue +102 -0
- package/app/components/X/Mark/SocialProof/Testimonials.vue +326 -0
- package/app/components/X/Mark/SocialProof/Toast.vue +136 -0
- package/app/components/X/Mark/_archive/Pattern/Background.vue +83 -0
- package/app/components/X/Mark/_archive/Pattern/CheckBg.vue +35 -0
- package/app/components/X/Mark/_archive/Pattern/CircuitBg.vue +41 -0
- package/app/components/X/Mark/_archive/Pattern/DotsBg.vue +35 -0
- package/app/components/X/Mark/_archive/Pattern/GlowDivider.vue +86 -0
- package/app/components/X/Mark/_archive/Pattern/HexBg.vue +35 -0
- package/app/components/X/Mark/_archive/Pattern/PlaidBg.vue +47 -0
- package/app/components/X/Mark/_archive/Pattern/SectionStitch.vue +168 -0
- package/app/components/X/Mark/_archive/Pattern/WaveBg.vue +41 -0
- package/app/components/X/_archive/Newsletter/Form.vue +123 -0
- package/app/components/X/_archive/Newsletter/Section.vue +131 -0
- package/app/composables/useParallax.ts +124 -0
- package/app/composables/useScrollReveal.ts +92 -0
- package/app/pages/index.vue +210 -0
- package/app/plugins/marketing.client.ts +13 -0
- package/app/types/marketing.d.ts +268 -0
- package/componentList.json +511 -0
- package/nuxt.config.ts +6 -13
- package/package.json +21 -9
- package/.editorconfig +0 -12
- package/app/assets/css/main.css +0 -2
- package/app/components/X/Marketing/Sections/EmailSubscription/index.vue +0 -129
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section
|
|
3
|
+
:class="[
|
|
4
|
+
'relative overflow-hidden',
|
|
5
|
+
paddingClasses,
|
|
6
|
+
bgColorClasses
|
|
7
|
+
]"
|
|
8
|
+
>
|
|
9
|
+
<!-- Background Pattern -->
|
|
10
|
+
<XMarkPatternBg
|
|
11
|
+
v-if="resolvedBg.pattern"
|
|
12
|
+
:pattern="resolvedBg.pattern"
|
|
13
|
+
:opacity="resolvedBg.patternOpacity"
|
|
14
|
+
class="absolute inset-0 pointer-events-none"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<!-- Background Image -->
|
|
18
|
+
<div
|
|
19
|
+
v-if="resolvedBg.image"
|
|
20
|
+
class="absolute inset-0"
|
|
21
|
+
:class="{ 'xParallax': resolvedBg.parallax }"
|
|
22
|
+
:data-parallax-speed="resolvedBg.parallaxSpeed"
|
|
23
|
+
>
|
|
24
|
+
<img
|
|
25
|
+
:src="resolvedBg.image"
|
|
26
|
+
:alt="resolvedBg.imageAlt"
|
|
27
|
+
class="w-full h-full object-cover"
|
|
28
|
+
:class="resolvedBg.imagePosition"
|
|
29
|
+
/>
|
|
30
|
+
<div
|
|
31
|
+
v-if="resolvedBg.imageOverlay"
|
|
32
|
+
class="absolute inset-0"
|
|
33
|
+
:class="bgImageOverlayClass"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Custom Background Slot -->
|
|
38
|
+
<slot name="background" />
|
|
39
|
+
|
|
40
|
+
<!-- Content -->
|
|
41
|
+
<div
|
|
42
|
+
class="relative z-10"
|
|
43
|
+
:class="containerClasses"
|
|
44
|
+
>
|
|
45
|
+
<slot />
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup>
|
|
51
|
+
const props = defineProps({
|
|
52
|
+
/**
|
|
53
|
+
* Background configuration object
|
|
54
|
+
* @property {string} variant - 'default' | 'subtle' | 'elevated' | 'bold' | 'transparent'
|
|
55
|
+
* @property {string} color - Custom Tailwind classes (overrides variant)
|
|
56
|
+
* @property {string} image - Background image URL
|
|
57
|
+
* @property {string} imageAlt - Background image alt text
|
|
58
|
+
* @property {string} imageOverlay - 'light' | 'heavy' | 'gradient'
|
|
59
|
+
* @property {string} imagePosition - Tailwind object-position class
|
|
60
|
+
* @property {string} pattern - 'dots' | 'grid' | 'diagonal' | 'topography' | 'circuit' | 'waves'
|
|
61
|
+
* @property {number} patternOpacity - Pattern opacity (0-1)
|
|
62
|
+
* @property {boolean} parallax - Enable parallax on bg image
|
|
63
|
+
* @property {number} parallaxSpeed - Parallax intensity (0.1 - 1.0)
|
|
64
|
+
*/
|
|
65
|
+
bg: {
|
|
66
|
+
type: [Object, String],
|
|
67
|
+
default: () => ({}),
|
|
68
|
+
},
|
|
69
|
+
/** Vertical padding size */
|
|
70
|
+
padding: {
|
|
71
|
+
type: String,
|
|
72
|
+
default: 'lg',
|
|
73
|
+
validator: (v) => ['none', 'xs', 'sm', 'md', 'lg', 'xl'].includes(v),
|
|
74
|
+
},
|
|
75
|
+
/** Max-width container size */
|
|
76
|
+
container: {
|
|
77
|
+
type: String,
|
|
78
|
+
default: 'lg',
|
|
79
|
+
validator: (v) => ['sm', 'md', 'lg', 'xl', 'full', 'none'].includes(v),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Default bg values
|
|
84
|
+
const bgDefaults = {
|
|
85
|
+
variant: 'default',
|
|
86
|
+
color: '',
|
|
87
|
+
image: '',
|
|
88
|
+
imageAlt: '',
|
|
89
|
+
imageOverlay: '',
|
|
90
|
+
imagePosition: 'object-center',
|
|
91
|
+
pattern: '',
|
|
92
|
+
patternOpacity: 0.05,
|
|
93
|
+
parallax: false,
|
|
94
|
+
parallaxSpeed: 0.3,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Resolve bg prop (supports string shorthand for variant or full object)
|
|
98
|
+
const resolvedBg = computed(() => {
|
|
99
|
+
if (typeof props.bg === 'string') {
|
|
100
|
+
return { ...bgDefaults, variant: props.bg };
|
|
101
|
+
}
|
|
102
|
+
return { ...bgDefaults, ...props.bg };
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const paddingClasses = computed(() => {
|
|
106
|
+
const sizes = {
|
|
107
|
+
none: '',
|
|
108
|
+
xs: 'py-8 md:py-10',
|
|
109
|
+
sm: 'py-12 md:py-16',
|
|
110
|
+
md: 'py-16 md:py-20',
|
|
111
|
+
lg: 'py-20 md:py-28 lg:py-32',
|
|
112
|
+
xl: 'py-28 md:py-36 lg:py-40',
|
|
113
|
+
};
|
|
114
|
+
return sizes[props.padding];
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const bgColorClasses = computed(() => {
|
|
118
|
+
// Custom color class takes priority
|
|
119
|
+
if (resolvedBg.value.color) return resolvedBg.value.color;
|
|
120
|
+
|
|
121
|
+
const colors = {
|
|
122
|
+
default: 'bg-white dark:bg-neutral-950',
|
|
123
|
+
subtle: 'bg-neutral-50 dark:bg-neutral-900',
|
|
124
|
+
elevated: 'bg-primary-50/50 dark:bg-primary-950/20',
|
|
125
|
+
bold: 'bg-neutral-950 dark:bg-white text-white dark:text-neutral-950',
|
|
126
|
+
transparent: 'bg-transparent',
|
|
127
|
+
};
|
|
128
|
+
return colors[resolvedBg.value.variant] || colors.default;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const containerClasses = computed(() => {
|
|
132
|
+
const containers = {
|
|
133
|
+
sm: 'max-w-[42rem] mx-auto px-4 sm:px-6',
|
|
134
|
+
md: 'max-w-[64rem] mx-auto px-4 sm:px-6 lg:px-8',
|
|
135
|
+
lg: 'max-w-[80rem] mx-auto px-4 sm:px-6 lg:px-8',
|
|
136
|
+
xl: 'max-w-[96rem] mx-auto px-4 sm:px-6 lg:px-8',
|
|
137
|
+
full: 'w-full px-4 sm:px-6 lg:px-8',
|
|
138
|
+
none: '',
|
|
139
|
+
};
|
|
140
|
+
return containers[props.container];
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const bgImageOverlayClass = computed(() => {
|
|
144
|
+
const overlays = {
|
|
145
|
+
light: 'bg-black/30',
|
|
146
|
+
heavy: 'bg-black/60',
|
|
147
|
+
gradient: 'bg-gradient-to-t from-black/80 via-black/40 to-transparent',
|
|
148
|
+
};
|
|
149
|
+
return overlays[resolvedBg.value.imageOverlay] || '';
|
|
150
|
+
});
|
|
151
|
+
</script>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
:class="[
|
|
4
|
+
'inline-flex items-center gap-2 text-sm font-medium',
|
|
5
|
+
variant === 'text' ? 'px-0 py-0' : 'px-3 py-1.5 rounded-full',
|
|
6
|
+
variantClasses
|
|
7
|
+
]"
|
|
8
|
+
>
|
|
9
|
+
<UIcon :name="iconName" class="w-4 h-4" />
|
|
10
|
+
<span>{{ label }}</span>
|
|
11
|
+
</span>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script>
|
|
15
|
+
// Module-scope constant for use in defineProps validator
|
|
16
|
+
const presets = {
|
|
17
|
+
'no-credit-card': {
|
|
18
|
+
icon: 'i-lucide-credit-card',
|
|
19
|
+
label: 'No credit card required',
|
|
20
|
+
},
|
|
21
|
+
'cancel-anytime': {
|
|
22
|
+
icon: 'i-lucide-x-circle',
|
|
23
|
+
label: 'Cancel anytime',
|
|
24
|
+
},
|
|
25
|
+
'free-trial': {
|
|
26
|
+
icon: 'i-lucide-gift',
|
|
27
|
+
label: 'Free 14-day trial',
|
|
28
|
+
},
|
|
29
|
+
'secure': {
|
|
30
|
+
icon: 'i-lucide-shield-check',
|
|
31
|
+
label: 'Bank-level security',
|
|
32
|
+
},
|
|
33
|
+
'support': {
|
|
34
|
+
icon: 'i-lucide-message-circle',
|
|
35
|
+
label: '24/7 support',
|
|
36
|
+
},
|
|
37
|
+
'setup': {
|
|
38
|
+
icon: 'i-lucide-zap',
|
|
39
|
+
label: 'Setup in minutes',
|
|
40
|
+
},
|
|
41
|
+
'gdpr': {
|
|
42
|
+
icon: 'i-lucide-lock',
|
|
43
|
+
label: 'GDPR compliant',
|
|
44
|
+
},
|
|
45
|
+
'money-back': {
|
|
46
|
+
icon: 'i-lucide-undo-2',
|
|
47
|
+
label: '30-day money back',
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<script setup>
|
|
53
|
+
const props = defineProps({
|
|
54
|
+
/** Use predefined badge preset */
|
|
55
|
+
preset: {
|
|
56
|
+
type: String,
|
|
57
|
+
default: '',
|
|
58
|
+
validator: (v) => v === '' || Object.keys(presets).includes(v),
|
|
59
|
+
},
|
|
60
|
+
/** Custom icon name (if not using preset) */
|
|
61
|
+
icon: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: '',
|
|
64
|
+
},
|
|
65
|
+
/** Custom label (if not using preset) */
|
|
66
|
+
label: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: '',
|
|
69
|
+
},
|
|
70
|
+
/** Style variant */
|
|
71
|
+
variant: {
|
|
72
|
+
type: String,
|
|
73
|
+
default: 'default',
|
|
74
|
+
validator: (v) => ['default', 'subtle', 'outline', 'text', 'text-muted'].includes(v),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const iconName = computed(() => {
|
|
79
|
+
if (props.preset && presets[props.preset]) {
|
|
80
|
+
return presets[props.preset].icon;
|
|
81
|
+
}
|
|
82
|
+
return props.icon || 'i-lucide-check-circle';
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const label = computed(() => {
|
|
86
|
+
if (props.preset && presets[props.preset]) {
|
|
87
|
+
return presets[props.preset].label;
|
|
88
|
+
}
|
|
89
|
+
return props.label;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const variantClasses = computed(() => {
|
|
93
|
+
const variants = {
|
|
94
|
+
default: 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300',
|
|
95
|
+
subtle: 'bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300',
|
|
96
|
+
outline: 'border border-neutral-300 dark:border-neutral-700 text-neutral-700 dark:text-neutral-300',
|
|
97
|
+
text: 'text-neutral-600 dark:text-neutral-400',
|
|
98
|
+
'text-muted': 'text-neutral-400 dark:text-neutral-500',
|
|
99
|
+
};
|
|
100
|
+
return variants[props.variant];
|
|
101
|
+
});
|
|
102
|
+
</script>
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<!-- Single Featured -->
|
|
4
|
+
<template v-if="layout === 'featured'">
|
|
5
|
+
<div class="relative max-w-4xl mx-auto text-center">
|
|
6
|
+
<!-- Navigation Arrows -->
|
|
7
|
+
<button
|
|
8
|
+
v-if="testimonials.length > 1"
|
|
9
|
+
class="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 lg:-translate-x-16 w-11 h-11 flex items-center justify-center rounded-full bg-white dark:bg-neutral-800 shadow-lg border border-neutral-200 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-all duration-300 opacity-60 hover:opacity-100"
|
|
10
|
+
@click="prev"
|
|
11
|
+
>
|
|
12
|
+
<UIcon name="i-lucide-chevron-left" class="w-5 h-5 text-neutral-700 dark:text-neutral-300" />
|
|
13
|
+
</button>
|
|
14
|
+
<button
|
|
15
|
+
v-if="testimonials.length > 1"
|
|
16
|
+
class="absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 lg:translate-x-16 w-11 h-11 flex items-center justify-center rounded-full bg-white dark:bg-neutral-800 shadow-lg border border-neutral-200 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-all duration-300 opacity-60 hover:opacity-100"
|
|
17
|
+
@click="next"
|
|
18
|
+
>
|
|
19
|
+
<UIcon name="i-lucide-chevron-right" class="w-5 h-5 text-neutral-700 dark:text-neutral-300" />
|
|
20
|
+
</button>
|
|
21
|
+
|
|
22
|
+
<!-- Quote Icon -->
|
|
23
|
+
<!-- <div class="relative inline-block mb-8">
|
|
24
|
+
<span class="text-8xl font-serif text-primary-500/20 select-none leading-none serif">"</span>
|
|
25
|
+
</div> -->
|
|
26
|
+
|
|
27
|
+
<!-- Quote with crossfade -->
|
|
28
|
+
<Transition name="fade" mode="out-in">
|
|
29
|
+
<blockquote :key="currentIndex" class="xText-headline text-neutral-900 dark:text-white xText-balance">
|
|
30
|
+
"{{ currentTestimonial.quote }}"
|
|
31
|
+
</blockquote>
|
|
32
|
+
</Transition>
|
|
33
|
+
|
|
34
|
+
<!-- Stars -->
|
|
35
|
+
<div v-if="hasStars && currentTestimonial.rating" class="flex justify-center gap-1 mt-6">
|
|
36
|
+
<UIcon
|
|
37
|
+
v-for="i in 5"
|
|
38
|
+
:key="i"
|
|
39
|
+
name="i-lucide-star"
|
|
40
|
+
class="w-5 h-5"
|
|
41
|
+
:class="i <= currentTestimonial.rating ? 'text-yellow-400' : 'text-neutral-300 dark:text-neutral-600'"
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Attribution with crossfade -->
|
|
46
|
+
<Transition name="fade" mode="out-in">
|
|
47
|
+
<div :key="currentIndex" class="mt-8 flex items-center justify-center gap-4">
|
|
48
|
+
<div class="relative">
|
|
49
|
+
<img
|
|
50
|
+
v-if="currentTestimonial.avatar"
|
|
51
|
+
:src="currentTestimonial.avatar"
|
|
52
|
+
:alt="currentTestimonial.name"
|
|
53
|
+
class="w-14 h-14 rounded-full object-cover ring-2 ring-white dark:ring-neutral-800 shadow-lg"
|
|
54
|
+
/>
|
|
55
|
+
<div class="absolute inset-0 rounded-full ring-2 ring-primary-500/20 ring-offset-2 ring-offset-white dark:ring-offset-neutral-950" />
|
|
56
|
+
</div>
|
|
57
|
+
<div class="text-left">
|
|
58
|
+
<p class="font-semibold text-neutral-900 dark:text-white">
|
|
59
|
+
{{ currentTestimonial.name }}
|
|
60
|
+
</p>
|
|
61
|
+
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
|
62
|
+
{{ currentTestimonial.title }}
|
|
63
|
+
<span v-if="currentTestimonial.company">
|
|
64
|
+
at {{ currentTestimonial.company }}
|
|
65
|
+
</span>
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
<img
|
|
69
|
+
v-if="currentTestimonial.logo"
|
|
70
|
+
:src="currentTestimonial.logo"
|
|
71
|
+
:alt="currentTestimonial.company"
|
|
72
|
+
class="h-8 w-auto ml-4 opacity-60"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
</Transition>
|
|
76
|
+
|
|
77
|
+
<!-- Navigation Dots -->
|
|
78
|
+
<div
|
|
79
|
+
v-if="testimonials.length > 1"
|
|
80
|
+
class="mt-8 flex justify-center gap-2"
|
|
81
|
+
>
|
|
82
|
+
<button
|
|
83
|
+
v-for="(_, index) in testimonials"
|
|
84
|
+
:key="index"
|
|
85
|
+
class="w-2 h-2 rounded-full transition-colors duration-300"
|
|
86
|
+
:class="index === currentIndex ? 'bg-primary-500' : 'bg-neutral-300 dark:bg-neutral-700 hover:bg-neutral-400 dark:hover:bg-neutral-600'"
|
|
87
|
+
@click="goToIndex(index)"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<!-- Grid Layout -->
|
|
94
|
+
<template v-else-if="layout === 'grid'">
|
|
95
|
+
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
96
|
+
<article
|
|
97
|
+
v-for="(testimonial, index) in testimonials"
|
|
98
|
+
:key="testimonial.name"
|
|
99
|
+
class="xGlass rounded-2xl p-6 xFadeUp transition-all duration-300 ease-out hover:-translate-y-1 hover:shadow-[0_12px_40px_rgba(128,128,128,0.2)] dark:hover:shadow-[0_12px_40px_rgba(255,255,255,0.1)]"
|
|
100
|
+
:style="{ transitionDelay: `${index * 100}ms` }"
|
|
101
|
+
>
|
|
102
|
+
<!-- Stars -->
|
|
103
|
+
<div v-if="hasStars" class="flex gap-1 mb-4">
|
|
104
|
+
<UIcon
|
|
105
|
+
v-for="i in 5"
|
|
106
|
+
:key="i"
|
|
107
|
+
name="i-lucide-star"
|
|
108
|
+
class="w-5 h-5"
|
|
109
|
+
:class="i <= (testimonial.rating || 5) ? 'text-yellow-400' : 'text-neutral-300 dark:text-neutral-600'"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Quote -->
|
|
114
|
+
<p class="text-neutral-700 dark:text-neutral-300">
|
|
115
|
+
"{{ testimonial.quote }}"
|
|
116
|
+
</p>
|
|
117
|
+
|
|
118
|
+
<!-- Attribution -->
|
|
119
|
+
<div class="mt-6 flex items-center gap-3">
|
|
120
|
+
<img
|
|
121
|
+
v-if="testimonial.avatar"
|
|
122
|
+
:src="testimonial.avatar"
|
|
123
|
+
:alt="testimonial.name"
|
|
124
|
+
class="w-10 h-10 rounded-full object-cover"
|
|
125
|
+
/>
|
|
126
|
+
<div
|
|
127
|
+
v-else
|
|
128
|
+
class="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900 flex items-center justify-center"
|
|
129
|
+
>
|
|
130
|
+
<span class="text-primary-600 dark:text-primary-400 font-medium">
|
|
131
|
+
{{ testimonial.name.charAt(0) }}
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
<div>
|
|
135
|
+
<p class="font-medium text-neutral-900 dark:text-white text-sm">
|
|
136
|
+
{{ testimonial.name }}
|
|
137
|
+
</p>
|
|
138
|
+
<p class="text-xs text-neutral-500">
|
|
139
|
+
{{ testimonial.title }}
|
|
140
|
+
<span v-if="testimonial.company"> at {{ testimonial.company }}</span>
|
|
141
|
+
</p>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</article>
|
|
145
|
+
</div>
|
|
146
|
+
</template>
|
|
147
|
+
|
|
148
|
+
<!-- Carousel Layout -->
|
|
149
|
+
<template v-else-if="layout === 'carousel'">
|
|
150
|
+
<div class="relative overflow-hidden">
|
|
151
|
+
<div
|
|
152
|
+
class="flex transition-transform duration-500"
|
|
153
|
+
:style="{ transform: `translateX(-${currentIndex * 100}%)`, transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }"
|
|
154
|
+
>
|
|
155
|
+
<article
|
|
156
|
+
v-for="testimonial in testimonials"
|
|
157
|
+
:key="testimonial.name"
|
|
158
|
+
class="w-full flex-shrink-0 px-4"
|
|
159
|
+
>
|
|
160
|
+
<div class="max-w-2xl mx-auto text-center">
|
|
161
|
+
<!-- Stars -->
|
|
162
|
+
<div v-if="hasStars" class="flex justify-center gap-1 mb-6">
|
|
163
|
+
<UIcon
|
|
164
|
+
v-for="i in 5"
|
|
165
|
+
:key="i"
|
|
166
|
+
name="i-lucide-star"
|
|
167
|
+
class="w-5 h-5"
|
|
168
|
+
:class="i <= (testimonial.rating || 5) ? 'text-yellow-400' : 'text-neutral-300'"
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- Quote -->
|
|
173
|
+
<blockquote class="text-xl text-neutral-900 dark:text-white">
|
|
174
|
+
"{{ testimonial.quote }}"
|
|
175
|
+
</blockquote>
|
|
176
|
+
|
|
177
|
+
<!-- Attribution -->
|
|
178
|
+
<div class="mt-6 flex items-center justify-center gap-3">
|
|
179
|
+
<img
|
|
180
|
+
v-if="testimonial.avatar"
|
|
181
|
+
:src="testimonial.avatar"
|
|
182
|
+
:alt="testimonial.name"
|
|
183
|
+
class="w-12 h-12 rounded-full object-cover"
|
|
184
|
+
/>
|
|
185
|
+
<div class="text-left">
|
|
186
|
+
<p class="font-medium text-neutral-900 dark:text-white">
|
|
187
|
+
{{ testimonial.name }}
|
|
188
|
+
</p>
|
|
189
|
+
<p class="text-sm text-neutral-500">
|
|
190
|
+
{{ testimonial.title }}
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</article>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<!-- Navigation Arrows -->
|
|
199
|
+
<button
|
|
200
|
+
v-if="testimonials.length > 1"
|
|
201
|
+
class="absolute left-0 top-1/2 -translate-y-1/2 w-10 h-10 flex items-center justify-center rounded-full bg-white/80 dark:bg-neutral-800/80 shadow-lg hover:bg-white dark:hover:bg-neutral-800 transition-colors"
|
|
202
|
+
@click="prev"
|
|
203
|
+
>
|
|
204
|
+
<UIcon name="i-lucide-chevron-left" class="w-5 h-5" />
|
|
205
|
+
</button>
|
|
206
|
+
<button
|
|
207
|
+
v-if="testimonials.length > 1"
|
|
208
|
+
class="absolute right-0 top-1/2 -translate-y-1/2 w-10 h-10 flex items-center justify-center rounded-full bg-white/80 dark:bg-neutral-800/80 shadow-lg hover:bg-white dark:hover:bg-neutral-800 transition-colors"
|
|
209
|
+
@click="next"
|
|
210
|
+
>
|
|
211
|
+
<UIcon name="i-lucide-chevron-right" class="w-5 h-5" />
|
|
212
|
+
</button>
|
|
213
|
+
|
|
214
|
+
<!-- Dots -->
|
|
215
|
+
<div
|
|
216
|
+
v-if="testimonials.length > 1"
|
|
217
|
+
class="mt-8 flex justify-center gap-2"
|
|
218
|
+
>
|
|
219
|
+
<button
|
|
220
|
+
v-for="(_, index) in testimonials"
|
|
221
|
+
:key="index"
|
|
222
|
+
class="w-2 h-2 rounded-full transition-colors"
|
|
223
|
+
:class="index === currentIndex ? 'bg-primary-500' : 'bg-neutral-300 dark:bg-neutral-700'"
|
|
224
|
+
@click="goToIndex(index)"
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</template>
|
|
229
|
+
</div>
|
|
230
|
+
</template>
|
|
231
|
+
|
|
232
|
+
<script setup>
|
|
233
|
+
// Initialize scroll reveal animations
|
|
234
|
+
useScrollReveal()
|
|
235
|
+
|
|
236
|
+
const props = defineProps({
|
|
237
|
+
/** Array of testimonials */
|
|
238
|
+
testimonials: {
|
|
239
|
+
type: Array,
|
|
240
|
+
required: true,
|
|
241
|
+
},
|
|
242
|
+
/** Display layout */
|
|
243
|
+
layout: {
|
|
244
|
+
type: String,
|
|
245
|
+
default: 'featured',
|
|
246
|
+
validator: (v) => ['featured', 'grid', 'carousel'].includes(v),
|
|
247
|
+
},
|
|
248
|
+
/** Show star ratings */
|
|
249
|
+
hasStars: {
|
|
250
|
+
type: Boolean,
|
|
251
|
+
default: true,
|
|
252
|
+
},
|
|
253
|
+
/** Auto-rotate (featured/carousel) */
|
|
254
|
+
autoplay: {
|
|
255
|
+
type: Boolean,
|
|
256
|
+
default: true,
|
|
257
|
+
},
|
|
258
|
+
/** Autoplay interval (ms) */
|
|
259
|
+
interval: {
|
|
260
|
+
type: Number,
|
|
261
|
+
default: 5000,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const currentIndex = ref(0);
|
|
266
|
+
let autoplayInterval = null;
|
|
267
|
+
|
|
268
|
+
const currentTestimonial = computed(() => {
|
|
269
|
+
return props.testimonials[currentIndex.value] || props.testimonials[0];
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const goToIndex = (index) => {
|
|
273
|
+
currentIndex.value = index;
|
|
274
|
+
resetAutoplay();
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const next = () => {
|
|
278
|
+
currentIndex.value = (currentIndex.value + 1) % props.testimonials.length;
|
|
279
|
+
resetAutoplay();
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const prev = () => {
|
|
283
|
+
currentIndex.value = currentIndex.value === 0
|
|
284
|
+
? props.testimonials.length - 1
|
|
285
|
+
: currentIndex.value - 1;
|
|
286
|
+
resetAutoplay();
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const startAutoplay = () => {
|
|
290
|
+
if (props.autoplay && props.testimonials.length > 1) {
|
|
291
|
+
autoplayInterval = setInterval(() => {
|
|
292
|
+
currentIndex.value = (currentIndex.value + 1) % props.testimonials.length;
|
|
293
|
+
}, props.interval);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const resetAutoplay = () => {
|
|
298
|
+
if (autoplayInterval) {
|
|
299
|
+
clearInterval(autoplayInterval);
|
|
300
|
+
startAutoplay();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
onMounted(() => {
|
|
305
|
+
startAutoplay();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
onUnmounted(() => {
|
|
309
|
+
if (autoplayInterval) {
|
|
310
|
+
clearInterval(autoplayInterval);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
</script>
|
|
314
|
+
|
|
315
|
+
<style scoped>
|
|
316
|
+
/* Fade transition for testimonial changes */
|
|
317
|
+
.fade-enter-active,
|
|
318
|
+
.fade-leave-active {
|
|
319
|
+
transition: opacity 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.fade-enter-from,
|
|
323
|
+
.fade-leave-to {
|
|
324
|
+
opacity: 0;
|
|
325
|
+
}
|
|
326
|
+
</style>
|