@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
|
@@ -1,50 +1,61 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<article>
|
|
2
|
+
<article class="blog-detail">
|
|
3
3
|
<!-- Hero -->
|
|
4
|
-
<header class="mb-
|
|
5
|
-
<div class="max-w-3xl mx-auto text-center mb-
|
|
4
|
+
<header class="mb-16 lg:mb-20">
|
|
5
|
+
<div class="max-w-3xl mx-auto text-center mb-10">
|
|
6
6
|
<UBadge
|
|
7
7
|
v-if="post.category"
|
|
8
8
|
:label="post.category"
|
|
9
|
-
|
|
9
|
+
color="primary"
|
|
10
|
+
variant="subtle"
|
|
11
|
+
class="mb-6"
|
|
10
12
|
/>
|
|
11
|
-
<h1
|
|
13
|
+
<h1
|
|
14
|
+
class="text-3xl sm:text-4xl lg:text-5xl font-bold leading-tight tracking-tight text-neutral-900 dark:text-white"
|
|
15
|
+
>
|
|
12
16
|
{{ post.title }}
|
|
13
17
|
</h1>
|
|
14
18
|
<p
|
|
15
19
|
v-if="post.description"
|
|
16
|
-
class="mt-
|
|
20
|
+
class="mt-6 text-lg sm:text-xl text-neutral-600 dark:text-neutral-400 leading-relaxed max-w-2xl mx-auto"
|
|
17
21
|
>
|
|
18
22
|
{{ post.description }}
|
|
19
23
|
</p>
|
|
20
24
|
|
|
21
25
|
<!-- Meta -->
|
|
22
|
-
<div
|
|
26
|
+
<div
|
|
27
|
+
class="mt-8 flex items-center justify-center gap-3 sm:gap-4 flex-wrap text-sm"
|
|
28
|
+
>
|
|
23
29
|
<!-- Author -->
|
|
24
|
-
<div v-if="post.author" class="flex items-center gap-2">
|
|
30
|
+
<div v-if="post.author" class="flex items-center gap-2.5">
|
|
25
31
|
<img
|
|
26
32
|
v-if="post.author.avatar"
|
|
27
33
|
:src="post.author.avatar"
|
|
28
34
|
:alt="post.author.name"
|
|
29
|
-
class="w-
|
|
35
|
+
class="w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-neutral-800"
|
|
30
36
|
/>
|
|
31
37
|
<span class="font-medium text-neutral-900 dark:text-white">
|
|
32
38
|
{{ post.author.name }}
|
|
33
39
|
</span>
|
|
34
40
|
</div>
|
|
35
|
-
<span class="text-neutral-300 dark:text-neutral-
|
|
36
|
-
<time
|
|
41
|
+
<span class="text-neutral-300 dark:text-neutral-600">•</span>
|
|
42
|
+
<time
|
|
43
|
+
class="text-neutral-500 dark:text-neutral-400"
|
|
44
|
+
:datetime="post.date"
|
|
45
|
+
>
|
|
37
46
|
{{ formattedDate }}
|
|
38
47
|
</time>
|
|
39
|
-
<span class="text-neutral-300 dark:text-neutral-
|
|
40
|
-
<span class="text-neutral-500">{{
|
|
48
|
+
<span class="text-neutral-300 dark:text-neutral-600">•</span>
|
|
49
|
+
<span class="text-neutral-500 dark:text-neutral-400">{{
|
|
50
|
+
post.readingTime
|
|
51
|
+
}}</span>
|
|
41
52
|
</div>
|
|
42
53
|
</div>
|
|
43
54
|
|
|
44
55
|
<!-- Featured Image -->
|
|
45
56
|
<figure
|
|
46
57
|
v-if="post.img?.src"
|
|
47
|
-
class="aspect-
|
|
58
|
+
class="aspect-2/1 sm:aspect-21/9 rounded-2xl lg:rounded-3xl overflow-hidden shadow-xl"
|
|
48
59
|
>
|
|
49
60
|
<img
|
|
50
61
|
:src="post.img.src"
|
|
@@ -55,120 +66,75 @@
|
|
|
55
66
|
</header>
|
|
56
67
|
|
|
57
68
|
<!-- Content Grid -->
|
|
58
|
-
<div class="grid lg:grid-cols-[
|
|
69
|
+
<div class="grid lg:grid-cols-[1fr_300px] gap-12 lg:gap-16">
|
|
59
70
|
<!-- Main Content -->
|
|
60
|
-
<div class="
|
|
61
|
-
<!-- Prose -->
|
|
62
|
-
<div
|
|
71
|
+
<div class="min-w-0">
|
|
72
|
+
<!-- Prose Content -->
|
|
73
|
+
<div
|
|
74
|
+
class="prose prose-lg prose-neutral dark:prose-invert max-w-none prose-headings:scroll-mt-20 prose-a:text-primary-600 dark:prose-a:text-primary-400 prose-a:no-underline hover:prose-a:underline prose-pre:bg-neutral-900 dark:prose-pre:bg-neutral-950 prose-pre:border prose-pre:border-neutral-800 prose-img:rounded-xl"
|
|
75
|
+
>
|
|
63
76
|
<slot />
|
|
64
77
|
</div>
|
|
65
78
|
|
|
66
79
|
<!-- Tags -->
|
|
67
|
-
<div
|
|
80
|
+
<div
|
|
81
|
+
v-if="post.tags?.length"
|
|
82
|
+
class="mt-16 pt-10 border-t border-neutral-200 dark:border-neutral-800"
|
|
83
|
+
>
|
|
84
|
+
<h4
|
|
85
|
+
class="text-xs font-semibold uppercase tracking-wider text-neutral-500 mb-4"
|
|
86
|
+
>
|
|
87
|
+
Tags
|
|
88
|
+
</h4>
|
|
68
89
|
<div class="flex flex-wrap gap-2">
|
|
69
90
|
<UBadge
|
|
70
91
|
v-for="tag in post.tags"
|
|
71
92
|
:key="tag"
|
|
72
93
|
:label="tag"
|
|
73
|
-
variant="
|
|
94
|
+
variant="subtle"
|
|
95
|
+
color="neutral"
|
|
96
|
+
size="md"
|
|
74
97
|
/>
|
|
75
98
|
</div>
|
|
76
99
|
</div>
|
|
77
100
|
|
|
78
101
|
<!-- Author Bio -->
|
|
79
|
-
<
|
|
80
|
-
v-if="post.author
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
:src="post.author.avatar"
|
|
86
|
-
:alt="post.author.name"
|
|
87
|
-
class="w-20 h-20 rounded-full object-cover flex-shrink-0"
|
|
88
|
-
/>
|
|
89
|
-
<div>
|
|
90
|
-
<p class="font-semibold text-lg text-neutral-900 dark:text-white">
|
|
91
|
-
{{ post.author.name }}
|
|
92
|
-
</p>
|
|
93
|
-
<p v-if="post.author.title" class="text-sm text-neutral-500 mb-3">
|
|
94
|
-
{{ post.author.title }}
|
|
95
|
-
</p>
|
|
96
|
-
<p class="text-neutral-600 dark:text-neutral-400">
|
|
97
|
-
{{ post.author.bio }}
|
|
98
|
-
</p>
|
|
99
|
-
<div v-if="post.author.social" class="flex gap-2 mt-4">
|
|
100
|
-
<UButton
|
|
101
|
-
v-for="(url, platform) in post.author.social"
|
|
102
|
-
:key="platform"
|
|
103
|
-
:to="url"
|
|
104
|
-
:icon="getSocialIcon(platform)"
|
|
105
|
-
variant="ghost"
|
|
106
|
-
color="neutral"
|
|
107
|
-
size="sm"
|
|
108
|
-
target="_blank"
|
|
109
|
-
:aria-label="platform"
|
|
110
|
-
/>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
</div>
|
|
102
|
+
<XMarkBlogAuthor
|
|
103
|
+
v-if="post.author"
|
|
104
|
+
:author="post.author"
|
|
105
|
+
variant="full"
|
|
106
|
+
class="mt-12"
|
|
107
|
+
/>
|
|
114
108
|
|
|
115
109
|
<!-- Navigation -->
|
|
116
|
-
<
|
|
110
|
+
<XMarkBlogNavigation
|
|
117
111
|
v-if="previousPost || nextPost"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
v-if="previousPost"
|
|
123
|
-
:to="`/blog/${previousPost.slug}`"
|
|
124
|
-
class="group flex items-center gap-4 p-4 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-900 transition-colors"
|
|
125
|
-
>
|
|
126
|
-
<UIcon
|
|
127
|
-
name="i-lucide-arrow-left"
|
|
128
|
-
class="w-5 h-5 text-neutral-400 group-hover:text-primary-500 transition-colors"
|
|
129
|
-
/>
|
|
130
|
-
<div>
|
|
131
|
-
<span class="text-xs text-neutral-500 uppercase tracking-wide">Previous</span>
|
|
132
|
-
<p class="font-medium text-neutral-900 dark:text-white group-hover:text-primary-500 transition-colors line-clamp-1">
|
|
133
|
-
{{ previousPost.title }}
|
|
134
|
-
</p>
|
|
135
|
-
</div>
|
|
136
|
-
</NuxtLink>
|
|
137
|
-
<div v-else />
|
|
138
|
-
|
|
139
|
-
<!-- Next -->
|
|
140
|
-
<NuxtLink
|
|
141
|
-
v-if="nextPost"
|
|
142
|
-
:to="`/blog/${nextPost.slug}`"
|
|
143
|
-
class="group flex items-center justify-end gap-4 p-4 rounded-xl hover:bg-neutral-100 dark:hover:bg-neutral-900 transition-colors text-right"
|
|
144
|
-
>
|
|
145
|
-
<div>
|
|
146
|
-
<span class="text-xs text-neutral-500 uppercase tracking-wide">Next</span>
|
|
147
|
-
<p class="font-medium text-neutral-900 dark:text-white group-hover:text-primary-500 transition-colors line-clamp-1">
|
|
148
|
-
{{ nextPost.title }}
|
|
149
|
-
</p>
|
|
150
|
-
</div>
|
|
151
|
-
<UIcon
|
|
152
|
-
name="i-lucide-arrow-right"
|
|
153
|
-
class="w-5 h-5 text-neutral-400 group-hover:text-primary-500 transition-colors"
|
|
154
|
-
/>
|
|
155
|
-
</NuxtLink>
|
|
156
|
-
</nav>
|
|
112
|
+
:previous="previousPost"
|
|
113
|
+
:next="nextPost"
|
|
114
|
+
class="mt-12"
|
|
115
|
+
/>
|
|
157
116
|
</div>
|
|
158
117
|
|
|
159
118
|
<!-- Sidebar -->
|
|
160
119
|
<aside class="hidden lg:block">
|
|
161
120
|
<div class="sticky top-24 space-y-8">
|
|
162
121
|
<!-- Table of Contents -->
|
|
163
|
-
<div
|
|
164
|
-
|
|
165
|
-
|
|
122
|
+
<div
|
|
123
|
+
v-if="toc?.length"
|
|
124
|
+
class="p-5 rounded-xl bg-neutral-50 dark:bg-neutral-900/50 border border-neutral-200/60 dark:border-neutral-800"
|
|
125
|
+
>
|
|
126
|
+
<h4
|
|
127
|
+
class="font-semibold text-neutral-900 dark:text-white mb-4 text-sm"
|
|
128
|
+
>
|
|
129
|
+
On this page
|
|
130
|
+
</h4>
|
|
131
|
+
<nav class="space-y-1">
|
|
166
132
|
<a
|
|
167
133
|
v-for="item in toc"
|
|
168
134
|
:key="item.id"
|
|
169
135
|
:href="`#${item.id}`"
|
|
170
|
-
class="block text-sm text-neutral-
|
|
171
|
-
:class="{ 'pl-4': item.depth > 2 }"
|
|
136
|
+
class="block py-1.5 text-sm text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors leading-snug"
|
|
137
|
+
:class="{ 'pl-4 text-[13px]': item.depth > 2 }"
|
|
172
138
|
>
|
|
173
139
|
{{ item.text }}
|
|
174
140
|
</a>
|
|
@@ -179,8 +145,8 @@
|
|
|
179
145
|
<slot name="sidebar">
|
|
180
146
|
<XMarkNewsletterForm
|
|
181
147
|
variant="compact"
|
|
182
|
-
title="Subscribe"
|
|
183
|
-
description="Get
|
|
148
|
+
title="Subscribe to our newsletter"
|
|
149
|
+
description="Get the latest posts and insights directly to your inbox."
|
|
184
150
|
/>
|
|
185
151
|
</slot>
|
|
186
152
|
</div>
|
|
@@ -191,22 +157,26 @@
|
|
|
191
157
|
|
|
192
158
|
<script setup>
|
|
193
159
|
const props = defineProps({
|
|
194
|
-
/** Blog post object
|
|
160
|
+
/** Blog post object { title, slug, img?, excerpt?, date?, category?, author?, body? } */
|
|
195
161
|
post: {
|
|
196
162
|
type: Object,
|
|
197
|
-
|
|
163
|
+
default: () => ({
|
|
164
|
+
title: "Blog Post Title",
|
|
165
|
+
slug: "#",
|
|
166
|
+
excerpt: "A short description of this blog post.",
|
|
167
|
+
date: new Date().toISOString(),
|
|
168
|
+
category: "General",
|
|
169
|
+
body: "",
|
|
170
|
+
}),
|
|
198
171
|
},
|
|
199
|
-
/** Previous post for navigation */
|
|
200
172
|
previousPost: {
|
|
201
173
|
type: Object,
|
|
202
174
|
default: null,
|
|
203
175
|
},
|
|
204
|
-
/** Next post for navigation */
|
|
205
176
|
nextPost: {
|
|
206
177
|
type: Object,
|
|
207
178
|
default: null,
|
|
208
179
|
},
|
|
209
|
-
/** Table of contents items */
|
|
210
180
|
toc: {
|
|
211
181
|
type: Array,
|
|
212
182
|
default: () => [],
|
|
@@ -214,27 +184,12 @@ const props = defineProps({
|
|
|
214
184
|
});
|
|
215
185
|
|
|
216
186
|
const formattedDate = computed(() => {
|
|
217
|
-
if (!props.post.date) return
|
|
187
|
+
if (!props.post.date) return "";
|
|
218
188
|
const date = new Date(props.post.date);
|
|
219
|
-
return date.toLocaleDateString(
|
|
220
|
-
year:
|
|
221
|
-
month:
|
|
222
|
-
day:
|
|
189
|
+
return date.toLocaleDateString("en-US", {
|
|
190
|
+
year: "numeric",
|
|
191
|
+
month: "long",
|
|
192
|
+
day: "numeric",
|
|
223
193
|
});
|
|
224
194
|
});
|
|
225
|
-
|
|
226
|
-
const getSocialIcon = (platform) => {
|
|
227
|
-
const icons = {
|
|
228
|
-
twitter: 'i-lucide-twitter',
|
|
229
|
-
x: 'i-lucide-twitter',
|
|
230
|
-
linkedin: 'i-lucide-linkedin',
|
|
231
|
-
github: 'i-lucide-github',
|
|
232
|
-
website: 'i-lucide-globe',
|
|
233
|
-
instagram: 'i-lucide-instagram',
|
|
234
|
-
facebook: 'i-lucide-facebook',
|
|
235
|
-
youtube: 'i-lucide-youtube',
|
|
236
|
-
email: 'i-lucide-mail',
|
|
237
|
-
};
|
|
238
|
-
return icons[platform.toLowerCase()] || 'i-lucide-link';
|
|
239
|
-
};
|
|
240
195
|
</script>
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
34
|
<!-- Empty State -->
|
|
35
|
-
<div
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
<div v-if="!paginatedPosts.length" class="text-center py-16">
|
|
36
|
+
<UIcon
|
|
37
|
+
name="i-lucide-file-text"
|
|
38
|
+
class="w-12 h-12 text-neutral-300 dark:text-neutral-700 mx-auto mb-4"
|
|
39
|
+
/>
|
|
40
40
|
<p class="text-neutral-600 dark:text-neutral-400">No posts found</p>
|
|
41
41
|
</div>
|
|
42
42
|
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
|
|
54
54
|
<script setup>
|
|
55
55
|
const props = defineProps({
|
|
56
|
-
/** Array of blog posts
|
|
56
|
+
/** Array of blog posts [{ title, slug, img?, excerpt?, date?, category?, author?, tags? }] */
|
|
57
57
|
posts: {
|
|
58
58
|
type: Array,
|
|
59
|
-
|
|
59
|
+
default: () => [],
|
|
60
60
|
},
|
|
61
61
|
/** Grid columns */
|
|
62
62
|
columns: {
|
|
@@ -96,13 +96,13 @@ const props = defineProps({
|
|
|
96
96
|
},
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
const search = ref(
|
|
100
|
-
const selectedCategory = ref(
|
|
99
|
+
const search = ref("");
|
|
100
|
+
const selectedCategory = ref("");
|
|
101
101
|
const currentPage = ref(1);
|
|
102
102
|
|
|
103
103
|
const categoryOptions = computed(() => {
|
|
104
104
|
return [
|
|
105
|
-
{ label:
|
|
105
|
+
{ label: "All Categories", value: "" },
|
|
106
106
|
...props.categories.map((cat) => ({
|
|
107
107
|
label: cat,
|
|
108
108
|
value: cat,
|
|
@@ -116,9 +116,10 @@ const filteredPosts = computed(() => {
|
|
|
116
116
|
// Filter by search
|
|
117
117
|
if (search.value) {
|
|
118
118
|
const searchLower = search.value.toLowerCase();
|
|
119
|
-
result = result.filter(
|
|
120
|
-
post
|
|
121
|
-
|
|
119
|
+
result = result.filter(
|
|
120
|
+
(post) =>
|
|
121
|
+
post.title.toLowerCase().includes(searchLower) ||
|
|
122
|
+
post.description?.toLowerCase().includes(searchLower)
|
|
122
123
|
);
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -142,9 +143,9 @@ const paginatedPosts = computed(() => {
|
|
|
142
143
|
|
|
143
144
|
const gridClasses = computed(() => {
|
|
144
145
|
const cols = {
|
|
145
|
-
1:
|
|
146
|
-
2:
|
|
147
|
-
3:
|
|
146
|
+
1: "grid gap-8",
|
|
147
|
+
2: "grid md:grid-cols-2 gap-8",
|
|
148
|
+
3: "grid md:grid-cols-2 lg:grid-cols-3 gap-8",
|
|
148
149
|
};
|
|
149
150
|
return cols[props.columns];
|
|
150
151
|
});
|