Skip to content

Commit 22b48de

Browse files
committed
feat: add footer navigation
1 parent 5df8a7c commit 22b48de

File tree

8 files changed

+577
-0
lines changed

8 files changed

+577
-0
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"cSpell.words": [
33
"defu",
4+
"mailerlite",
45
"nuxtify",
56
"nuxtifydev",
67
"unslug",

playground/nuxt.config.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,36 @@ export default defineNuxtConfig({
1717
{ text: 'Sign In', to: '/signin' },
1818
{ text: 'Help', to: '/help' },
1919
],
20+
footerPrimary: [
21+
{
22+
title: 'Product',
23+
links: [
24+
{ text: 'Features', to: '/features' },
25+
{ text: 'Pricing', to: '/pricing' },
26+
{ text: 'Changelog', to: '/changelog' },
27+
],
28+
},
29+
{
30+
title: 'Company',
31+
links: [
32+
{ text: 'About', to: '/about' },
33+
{ text: 'Careers', to: '/careers' },
34+
{ text: 'Contact', to: '/contact' },
35+
],
36+
},
37+
{
38+
title: 'Resources',
39+
links: [
40+
{ text: 'Blog', to: '/blog' },
41+
{ text: 'Documentation', to: '/docs' },
42+
{ text: 'Help', to: '/help' },
43+
],
44+
},
45+
],
46+
footerSecondary: [
47+
{ text: 'Privacy', to: '/privacy' },
48+
{ text: 'Terms', to: '/terms' },
49+
],
2050
},
2151
},
2252
})

src/module.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default defineNuxtModule<ModuleOptions>({
2323
},
2424
},
2525
defaults: {
26+
// Brand
2627
brand: {
2728
name: 'nuxtify-pages',
2829
domain: '',
@@ -34,15 +35,65 @@ export default defineNuxtModule<ModuleOptions>({
3435
mobileWidth: 150,
3536
},
3637
},
38+
39+
// Pages
40+
pages: {
41+
policies: {
42+
privacyUrl: '/privacy',
43+
termsUrl: '/terms',
44+
},
45+
},
46+
47+
// Announcement
3748
announcement: {
3849
show: false,
3950
message: '',
4051
buttonText: '',
4152
buttonUrl: '',
4253
},
54+
55+
// Navigation
4356
navigation: {
4457
primary: [],
4558
secondary: [],
59+
footerPrimary: [],
60+
footerSecondary: [],
61+
},
62+
63+
// Footer
64+
footer: {
65+
copyright: '',
66+
credits: {
67+
creator: {
68+
name: '',
69+
domain: '',
70+
},
71+
prependText: '',
72+
appendText: '',
73+
showPoweredBy: true,
74+
},
75+
cta: {
76+
show: false,
77+
title: '',
78+
subtitle: '',
79+
color: 'indigo',
80+
},
81+
},
82+
83+
// Email
84+
email: {
85+
general: '',
86+
support: '',
87+
provider: {
88+
defaultSubmitUrl: '',
89+
},
90+
},
91+
92+
// Style
93+
style: {
94+
btn: {
95+
rounded: true,
96+
},
4697
},
4798
},
4899
async setup(_options, _nuxt) {
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<script setup lang="ts">
2+
// Types
3+
import type { VForm } from 'vuetify/lib/components/VForm/index.mjs'
4+
5+
import { useId, navigateTo, useDisplay, ref, useNuxtifyConfig, getUtmParams, getBaseUrl, submitFormData, formRules, isExternalUrl,
6+
} from '#imports'
7+
8+
// Props
9+
const props = defineProps({
10+
submitUrl: {
11+
type: String,
12+
required: true,
13+
},
14+
buttonText: {
15+
type: String,
16+
default: 'Subscribe',
17+
},
18+
emailPlaceholder: {
19+
type: String,
20+
default: 'Enter your best email',
21+
},
22+
marketingConsentText: {
23+
type: String,
24+
default: 'Unsubscribe any time.',
25+
},
26+
showPrivacy: {
27+
type: Boolean,
28+
default: false,
29+
},
30+
redirectUrl: {
31+
type: String,
32+
default: '',
33+
},
34+
thankYouMessage: {
35+
type: String,
36+
default: 'Thanks for signing up!',
37+
},
38+
dark: {
39+
type: Boolean,
40+
default: false,
41+
},
42+
appendButtonIcon: {
43+
type: String,
44+
default: '',
45+
},
46+
prependButtonIcon: {
47+
type: String,
48+
default: '',
49+
},
50+
})
51+
52+
// App state
53+
const id = useId()
54+
const nuxtifyConfig = useNuxtifyConfig()
55+
const { xs } = useDisplay()
56+
57+
// Form state
58+
const form = ref<VForm>()
59+
const isSubmitted = ref(false)
60+
const isError = ref(false)
61+
const errorMessage = ref('')
62+
const loading = ref(false)
63+
const formInput = ref({
64+
email: '',
65+
})
66+
67+
// Button style
68+
const rounded = () => {
69+
if (nuxtifyConfig.style?.btn?.rounded) {
70+
return xs.value ? 'lg' : '0 e-lg'
71+
}
72+
else {
73+
return 0
74+
}
75+
}
76+
77+
async function handleSubmit() {
78+
loading.value = true
79+
const res = await form.value?.validate()
80+
if (res?.valid) {
81+
const formData = new FormData()
82+
83+
// Get UTM params
84+
const { utmSource, utmMedium, utmCampaign, utmTerm, utmContent }
85+
= getUtmParams(window.location.href)
86+
87+
// Set form field keys
88+
// Default
89+
let fieldPrepend = ''
90+
let fieldAppend = ''
91+
92+
// Mailerlite
93+
if (props.submitUrl.includes('mailerlite')) {
94+
fieldPrepend = 'fields['
95+
fieldAppend = ']'
96+
}
97+
98+
// Set form field values
99+
formData.append(
100+
`${fieldPrepend}email${fieldAppend}`,
101+
formInput.value.email,
102+
)
103+
formData.append(
104+
`${fieldPrepend}referrer${fieldAppend}`,
105+
getBaseUrl(window.location.href),
106+
)
107+
108+
if (utmSource)
109+
formData.append(`${fieldPrepend}utm_source${fieldAppend}`, utmSource)
110+
if (utmMedium)
111+
formData.append(`${fieldPrepend}utm_medium${fieldAppend}`, utmMedium)
112+
if (utmCampaign)
113+
formData.append(`${fieldPrepend}utm_campaign${fieldAppend}`, utmCampaign)
114+
if (utmTerm)
115+
formData.append(`${fieldPrepend}utm_term${fieldAppend}`, utmTerm)
116+
if (utmContent)
117+
formData.append(`${fieldPrepend}utm_content${fieldAppend}`, utmContent)
118+
119+
// Send to email provider
120+
const providerResponse = await submitFormData(props.submitUrl, formData)
121+
isError.value = providerResponse.isError
122+
errorMessage.value = providerResponse.errorMessage
123+
124+
// Redirect
125+
if (!isError.value && props.redirectUrl) {
126+
await navigateTo(props.redirectUrl, {
127+
external: isExternalUrl(props.redirectUrl, nuxtifyConfig.brand?.domain ?? ''),
128+
})
129+
}
130+
else {
131+
isSubmitted.value = providerResponse.isSubmitted
132+
}
133+
}
134+
loading.value = false
135+
}
136+
</script>
137+
138+
<template>
139+
<!-- Form -->
140+
<v-form
141+
v-if="!isSubmitted"
142+
ref="form"
143+
validate-on="submit"
144+
@submit.prevent="handleSubmit"
145+
>
146+
<div class="d-sm-flex justify-center">
147+
<!-- Using useId prevents hydration mismatch warnings issue with Vuetify -->
148+
<!-- See: https://github.com/vuetifyjs/vuetify/issues/19696 -->
149+
<!-- Once the issue is resolved (and it's used internally in Vuetify), remove use of useId -->
150+
<v-text-field
151+
:id
152+
v-model="formInput.email"
153+
type="email"
154+
color="secondary"
155+
:placeholder="emailPlaceholder"
156+
:rules="[formRules.required, formRules.validEmail]"
157+
:rounded="xs ? 't-lg' : '0 ts-lg'"
158+
hide-details="auto"
159+
class="text-start"
160+
>
161+
<template #message="{ message }">
162+
<span :class="dark ? 'text-red-lighten-3' : ''">{{ message }}</span>
163+
</template>
164+
</v-text-field>
165+
<v-btn
166+
type="submit"
167+
variant="flat"
168+
color="secondary"
169+
size="x-large"
170+
class="d-flex align-center py-7 mt-2 mt-sm-0"
171+
:loading
172+
:rounded="rounded()"
173+
:append-icon="appendButtonIcon"
174+
:prepend-icon="prependButtonIcon"
175+
:block="xs"
176+
>
177+
{{ buttonText }}
178+
</v-btn>
179+
</div>
180+
181+
<!-- Supporting Text -->
182+
<div
183+
v-if="showPrivacy || marketingConsentText"
184+
:class="`text-body-2 ${
185+
dark ? 'text-grey-lighten-2' : 'text-medium-emphasis'
186+
} mt-2`"
187+
>
188+
<span v-if="marketingConsentText">
189+
{{ marketingConsentText }}
190+
</span>
191+
<span v-if="showPrivacy">
192+
By signing up you agree to the
193+
<NuxtLink
194+
:to="nuxtifyConfig.pages?.policies?.privacyUrl"
195+
:class="`text-decoration-none ${
196+
dark ? 'text-grey-lighten-2' : 'text-medium-emphasis'
197+
}`"
198+
>
199+
Privacy Policy</NuxtLink>.
200+
</span>
201+
</div>
202+
</v-form>
203+
204+
<!-- Thank You -->
205+
<div
206+
v-else
207+
class="text-body-1"
208+
>
209+
{{ thankYouMessage }}
210+
</div>
211+
</template>
212+
213+
<style scoped>
214+
/* Spacing for error message */
215+
:deep(.v-input__details) {
216+
padding-inline: 0px;
217+
}
218+
:deep(.v-text-field .v-input__details) {
219+
padding-inline: 0px;
220+
}
221+
222+
/* Hover over links */
223+
a:hover {
224+
text-decoration: underline !important;
225+
}
226+
</style>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script setup lang="ts">
2+
import { useNuxtifyConfig } from '#imports'
3+
4+
// App state
5+
const nuxtifyConfig = useNuxtifyConfig()
6+
</script>
7+
8+
<template>
9+
<v-row
10+
justify="center"
11+
class="text-center my-4"
12+
>
13+
<v-col
14+
cols="12"
15+
md="7"
16+
lg="6"
17+
xl="4"
18+
>
19+
<div
20+
v-if="nuxtifyConfig.footer?.cta?.title"
21+
:class="`text-h5 text-${nuxtifyConfig.footer.cta.color}-lighten-4`"
22+
>
23+
{{ nuxtifyConfig.footer.cta.title }}
24+
</div>
25+
<div
26+
v-if="nuxtifyConfig.footer?.cta?.subtitle"
27+
:class="`text-subtitle-1 text-${nuxtifyConfig.footer.cta.color}-lighten-3 mb-4`"
28+
>
29+
{{ nuxtifyConfig.footer.cta.subtitle }}
30+
</div>
31+
<EmailSubscribeForm
32+
:submit-url="nuxtifyConfig.email?.provider?.defaultSubmitUrl"
33+
dark
34+
/>
35+
</v-col>
36+
</v-row>
37+
</template>

0 commit comments

Comments
 (0)