@edgedev/create-edge-site 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/.nvmrc +1 -0
- package/.vscode/settings.json +21 -0
- package/README.md +9 -0
- package/app.vue +5 -0
- package/assets/css/global.css +3 -0
- package/bin/cli.js +74 -0
- package/components/Input.vue +43 -0
- package/components/Select.vue +34 -0
- package/components/Textarea.vue +29 -0
- package/components/edgeFooter.vue +52 -0
- package/components/edgeFormFling.vue +124 -0
- package/components/edgeNavbar.vue +167 -0
- package/components/titleSection.vue +63 -0
- package/eslint.config.js +13 -0
- package/nuxt.config.ts +37 -0
- package/package.json +31 -0
- package/pages/contact.vue +93 -0
- package/pages/index.vue +16 -0
- package/plugins/vee-validate-rules.js +11 -0
- package/public/favicon.ico +0 -0
- package/public/images/edge_logo.png +0 -0
- package/public/images/edgemarketing.jpg +0 -0
- package/public/images/hero/titleShape.png +0 -0
- package/public/images/logo.png +0 -0
- package/public/robots.txt +1 -0
- package/server/tsconfig.json +3 -0
- package/tailwind.config.js +27 -0
- package/tsconfig.json +3 -0
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
22.13.1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files.associations": {
|
|
3
|
+
"*.json": "json"
|
|
4
|
+
},
|
|
5
|
+
"[json]": {
|
|
6
|
+
"editor.formatOnSave": true
|
|
7
|
+
},
|
|
8
|
+
"editor.formatOnSave": false,
|
|
9
|
+
"vetur.validation.template": false,
|
|
10
|
+
"editor.codeActionsOnSave": {
|
|
11
|
+
"source.fixAll.eslint": "explicit"
|
|
12
|
+
},
|
|
13
|
+
"editor.formatOnSaveMode": "modifications",
|
|
14
|
+
"editor.trimFinalNewlines": false,
|
|
15
|
+
"vue.features.codeActions": false,
|
|
16
|
+
"editor.tabSize": 2,
|
|
17
|
+
"editor.insertSpaces": true,
|
|
18
|
+
"editor.detectIndentation": false,
|
|
19
|
+
"vue3snippets.enable-compile-vue-file-on-did-save-code": false
|
|
20
|
+
}
|
|
21
|
+
|
package/README.md
ADDED
package/app.vue
ADDED
package/bin/cli.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
console.log('Script started')
|
|
3
|
+
const { execSync } = require('node:child_process')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
|
|
7
|
+
const runCommand = (command) => {
|
|
8
|
+
try {
|
|
9
|
+
console.log(`Running command: ${command}`)
|
|
10
|
+
execSync(`${command}`, { stdio: 'inherit' })
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
console.error(`Failed to run command: ${command}`, err)
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
return true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const modifyPackageJson = (repoName) => {
|
|
20
|
+
try {
|
|
21
|
+
const packageJsonPath = path.join(repoName, 'package.json')
|
|
22
|
+
const packageJsonData = fs.readFileSync(packageJsonPath, 'utf-8')
|
|
23
|
+
const packageJsonObj = JSON.parse(packageJsonData)
|
|
24
|
+
|
|
25
|
+
// Modify the properties
|
|
26
|
+
delete packageJsonObj.version
|
|
27
|
+
delete packageJsonObj.bin
|
|
28
|
+
packageJsonObj.name = repoName
|
|
29
|
+
packageJsonObj.description = `A really cool Edge Site for ${repoName}`
|
|
30
|
+
|
|
31
|
+
// Write the file back
|
|
32
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonObj, null, 2))
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.error('Failed to modify package.json', err)
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const process = require('node:process')
|
|
42
|
+
|
|
43
|
+
const repoName = process.argv[2]
|
|
44
|
+
|
|
45
|
+
const gitCheckoutCommand = `git clone --depth 1 https://github.com/Edge-Marketing-and-Design/edge-site-starter.git ${repoName}`
|
|
46
|
+
const removeGitDirCommand = `rm -rf ${repoName}/.git`
|
|
47
|
+
const installDependenciesCommand = `cd ${repoName} && pnpm store prune && pnpm install --force --ignore-scripts=false`
|
|
48
|
+
|
|
49
|
+
console.log(`Cloning with name ${repoName}...`)
|
|
50
|
+
const checkedOut = runCommand(gitCheckoutCommand)
|
|
51
|
+
if (!checkedOut) {
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(`Removing .git directory from ${repoName}...`)
|
|
56
|
+
const removedGitDir = runCommand(removeGitDirCommand)
|
|
57
|
+
if (!removedGitDir) {
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`Modifying package.json for ${repoName}...`)
|
|
62
|
+
const modifiedPackageJson = modifyPackageJson(repoName)
|
|
63
|
+
if (!modifiedPackageJson) {
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`Installing dependencies for ${repoName}...`)
|
|
68
|
+
const installedDeps = runCommand(installDependenciesCommand)
|
|
69
|
+
if (!installedDeps) {
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`Successfully created ${repoName}!`)
|
|
74
|
+
console.log(`cd into ${repoName} and run 'sh firebase_init.sh' to initialize your firebase project.`)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { MaskInput } from 'vue-3-mask'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
inheritAttrs: false,
|
|
6
|
+
}
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<script setup>
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
errorClass: {
|
|
12
|
+
type: String,
|
|
13
|
+
required: false,
|
|
14
|
+
default: '',
|
|
15
|
+
},
|
|
16
|
+
name: {
|
|
17
|
+
type: String,
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
type: {
|
|
21
|
+
type: String,
|
|
22
|
+
default: 'text',
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
defineOptions({ inheritAttrs: false })
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<Field v-slot="{ field }" :name="props.name">
|
|
31
|
+
<MaskInput
|
|
32
|
+
v-if="props.type === 'phone'"
|
|
33
|
+
mask="(###) ###-####"
|
|
34
|
+
type="props.type"
|
|
35
|
+
v-bind="{ ...field, ...$attrs }"
|
|
36
|
+
/>
|
|
37
|
+
<input
|
|
38
|
+
v-else
|
|
39
|
+
v-bind="{ ...field, ...$attrs }"
|
|
40
|
+
>
|
|
41
|
+
</Field>
|
|
42
|
+
<ErrorMessage :class="props.errorClass" :name="props.name" />
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
export default {
|
|
3
|
+
inheritAttrs: false,
|
|
4
|
+
}
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
errorClass: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: '',
|
|
12
|
+
},
|
|
13
|
+
name: {
|
|
14
|
+
type: String,
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
options: {
|
|
18
|
+
type: Array,
|
|
19
|
+
required: true,
|
|
20
|
+
// Example format: [{ label: 'Option 1', value: '1' }]
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<Field v-slot="{ field }" :name="props.name">
|
|
27
|
+
<select v-bind="{ ...field, ...$attrs }">
|
|
28
|
+
<option v-for="option in props.options" :key="option.value" :value="option.value">
|
|
29
|
+
{{ option.label }}
|
|
30
|
+
</option>
|
|
31
|
+
</select>
|
|
32
|
+
</Field>
|
|
33
|
+
<ErrorMessage :class="props.errorClass" :name="props.name" />
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
export default {
|
|
3
|
+
inheritAttrs: false,
|
|
4
|
+
}
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
// or any key you expect
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
errorClass: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: false,
|
|
13
|
+
default: '',
|
|
14
|
+
},
|
|
15
|
+
name: {
|
|
16
|
+
type: String,
|
|
17
|
+
required: true,
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<Field v-slot="{ field }" :name="props.name">
|
|
24
|
+
<textarea
|
|
25
|
+
v-bind="{ ...field, ...$attrs }"
|
|
26
|
+
/>
|
|
27
|
+
</Field>
|
|
28
|
+
<ErrorMessage :class="props.errorClass" :name="props.name" />
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<footer class="bg-black items-center justify-center flex flex-col">
|
|
3
|
+
<div class="text-center">
|
|
4
|
+
<!-- Social Icons -->
|
|
5
|
+
<div class="flex justify-center mb-6 space-x-4">
|
|
6
|
+
<a
|
|
7
|
+
href="#"
|
|
8
|
+
class="flex items-center justify-center w-12 h-12 transition rounded-full bg-lblue text-dblue hover:bg-opacity-80"
|
|
9
|
+
>
|
|
10
|
+
<i class="fab fa-facebook-f fa-lg" />
|
|
11
|
+
</a>
|
|
12
|
+
<a
|
|
13
|
+
href="#"
|
|
14
|
+
class="flex items-center justify-center w-12 h-12 transition rounded-full bg-lblue text-dblue hover:bg-opacity-80"
|
|
15
|
+
>
|
|
16
|
+
<i class="fab fa-instagram fa-lg" />
|
|
17
|
+
</a>
|
|
18
|
+
<a
|
|
19
|
+
href="#"
|
|
20
|
+
class="flex items-center justify-center w-12 h-12 transition rounded-full bg-lblue text-dblue hover:bg-opacity-80"
|
|
21
|
+
>
|
|
22
|
+
<i class="fab fa-houzz fa-lg" /> <!-- Houzz or custom icon -->
|
|
23
|
+
</a>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- Contact Information -->
|
|
27
|
+
<div
|
|
28
|
+
class="mb-8 text-sm font-medium tracking-wide text-center uppercase text-lblue md:flex md:justify-center md:space-x-8 md:space-y-0"
|
|
29
|
+
>
|
|
30
|
+
<div>
|
|
31
|
+
<a href="tel:+19999999"><span
|
|
32
|
+
class="font-serif font-bold tracking-wider text-white"
|
|
33
|
+
>Office</span> 999.999.9999</a>
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<span class="font-serif font-bold tracking-wider text-white">Mail</span> Mailing Address
|
|
37
|
+
</div>
|
|
38
|
+
<div>
|
|
39
|
+
<span class="font-serif font-bold tracking-wider text-white">Other Info</span> Other Info
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- Logo -->
|
|
44
|
+
<div class="flex justify-center">
|
|
45
|
+
<img src="/images/logo.png" alt="JDL Construction Logo" class="h-auto opacity-75 w-46">
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</footer>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
</style>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import VueTurnstile from 'vue-turnstile'
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
validationSchema: {
|
|
6
|
+
type: Object,
|
|
7
|
+
required: true,
|
|
8
|
+
},
|
|
9
|
+
formFlingEndpoint: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: true,
|
|
12
|
+
},
|
|
13
|
+
turnstileSiteSecret: {
|
|
14
|
+
type: String,
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
successMessage: {
|
|
18
|
+
type: String,
|
|
19
|
+
required: false,
|
|
20
|
+
default: 'Form submitted successfully!',
|
|
21
|
+
},
|
|
22
|
+
errorMessage: {
|
|
23
|
+
type: String,
|
|
24
|
+
required: false,
|
|
25
|
+
default: 'Submission failed, please try again.',
|
|
26
|
+
},
|
|
27
|
+
successClass: {
|
|
28
|
+
type: String,
|
|
29
|
+
required: false,
|
|
30
|
+
default: 'text-green-500 mt-2',
|
|
31
|
+
},
|
|
32
|
+
errorClass: {
|
|
33
|
+
type: String,
|
|
34
|
+
required: false,
|
|
35
|
+
default: 'text-red-500 mt-2',
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const state = reactive({
|
|
40
|
+
submitting: false,
|
|
41
|
+
turnstileToken: '',
|
|
42
|
+
submitResponse: {
|
|
43
|
+
sent: false,
|
|
44
|
+
success: false,
|
|
45
|
+
message: '',
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const onSubmit = async (values, { resetForm }) => {
|
|
50
|
+
state.submitting = true
|
|
51
|
+
state.submitResponse.sent = false
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const formData = new FormData()
|
|
55
|
+
|
|
56
|
+
for (const key in values) {
|
|
57
|
+
formData.append(key, values[key])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (state.turnstileToken) {
|
|
61
|
+
formData.append('cf-turnstile-response', state.turnstileToken)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const response = await fetch(props.formFlingEndpoint, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
body: formData,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
state.submitResponse.sent = true
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
state.submitResponse.success = false
|
|
73
|
+
state.submitResponse.message = props.errorMessage
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const data = await response.json()
|
|
78
|
+
|
|
79
|
+
let success = true
|
|
80
|
+
let errorMessage = props.successMessage
|
|
81
|
+
|
|
82
|
+
if (Array.isArray(data)) {
|
|
83
|
+
for (const step of data) {
|
|
84
|
+
if (!step.response.success) {
|
|
85
|
+
success = false
|
|
86
|
+
errorMessage = props.errorMessage
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
state.submitResponse.success = success
|
|
93
|
+
state.submitResponse.message = errorMessage
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.log(err)
|
|
97
|
+
state.submitResponse.success = false
|
|
98
|
+
state.submitResponse.message = props.errorMessage
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
state.submitting = false
|
|
102
|
+
}
|
|
103
|
+
if (state.submitResponse.success) {
|
|
104
|
+
resetForm()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<Form
|
|
111
|
+
:validation-schema="props.validationSchema"
|
|
112
|
+
@submit="onSubmit"
|
|
113
|
+
>
|
|
114
|
+
<slot :submitting="state.submitting" :submit-response="state.submitResponse" />
|
|
115
|
+
<VueTurnstile
|
|
116
|
+
v-if="props.turnstileSiteSecret"
|
|
117
|
+
v-model="state.turnstileToken"
|
|
118
|
+
:site-key="props.turnstileSiteSecret"
|
|
119
|
+
/>
|
|
120
|
+
<div v-if="state.submitResponse.sent" :class="state.submitResponse.success ? props.successClass : props.errorClass">
|
|
121
|
+
{{ state.submitResponse.message }}
|
|
122
|
+
</div>
|
|
123
|
+
</Form>
|
|
124
|
+
</template>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { onMounted, onUnmounted, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
const menuOpen = ref(false)
|
|
5
|
+
|
|
6
|
+
const toggleMenu = () => {
|
|
7
|
+
menuOpen.value = !menuOpen.value
|
|
8
|
+
if (menuOpen.value) {
|
|
9
|
+
document.body.classList.add('overflow-hidden') // Prevent scrolling when open
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
document.body.classList.remove('overflow-hidden')
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Automatically close the menu when clicking a link
|
|
17
|
+
const closeMenu = () => {
|
|
18
|
+
menuOpen.value = false
|
|
19
|
+
document.body.classList.remove('overflow-hidden')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Ensure the menu closes when resizing back to desktop
|
|
23
|
+
const handleResize = () => {
|
|
24
|
+
if (window.innerWidth >= 1024) {
|
|
25
|
+
closeMenu()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Listen for window resize events
|
|
30
|
+
onMounted(() => {
|
|
31
|
+
window.addEventListener('resize', handleResize)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
onUnmounted(() => {
|
|
35
|
+
window.removeEventListener('resize', handleResize)
|
|
36
|
+
})
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<nav class="relative z-30 text-white">
|
|
41
|
+
<!-- Navigation Content (Top Half) -->
|
|
42
|
+
<div class="relative px-6 md:px-12 bg-dblue">
|
|
43
|
+
<div class="container mx-auto flex items-center justify-between h-[128px]">
|
|
44
|
+
<!-- Logo -->
|
|
45
|
+
<NuxtLink to="/" class="text-xl font-bold">
|
|
46
|
+
<img src="/images/logo.png" alt="JDL Construction MT" class="w-full h-auto">
|
|
47
|
+
</NuxtLink>
|
|
48
|
+
|
|
49
|
+
<!-- Mobile Menu Button -->
|
|
50
|
+
<button class="md:hidden focus:outline-none" @click="toggleMenu">
|
|
51
|
+
<svg v-if="!menuOpen" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
52
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
53
|
+
</svg>
|
|
54
|
+
<svg v-else class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
55
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
56
|
+
</svg>
|
|
57
|
+
</button>
|
|
58
|
+
|
|
59
|
+
<!-- Desktop Menu -->
|
|
60
|
+
<ul class="hidden md:flex space-x-[30px] font-semibold tracking-widest uppercase">
|
|
61
|
+
<li>
|
|
62
|
+
<NuxtLink to="/" class="nav-item">
|
|
63
|
+
Home
|
|
64
|
+
</NuxtLink>
|
|
65
|
+
</li>
|
|
66
|
+
<li>
|
|
67
|
+
<NuxtLink to="/contact" class="nav-item">
|
|
68
|
+
Contact
|
|
69
|
+
</NuxtLink>
|
|
70
|
+
</li>
|
|
71
|
+
</ul>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- Mobile Slide-in Menu -->
|
|
76
|
+
<transition name="slide">
|
|
77
|
+
<div v-if="menuOpen" class="fixed inset-0 flex flex-col items-center justify-center px-20 py-6 text-center bg-dblue z-[9999] w-full h-full">
|
|
78
|
+
<!-- Close Button -->
|
|
79
|
+
<button class="absolute text-4xl top-6 right-6 text-lblue" @click="toggleMenu">
|
|
80
|
+
×
|
|
81
|
+
</button>
|
|
82
|
+
|
|
83
|
+
<!-- Mobile Logo -->
|
|
84
|
+
<a href="/"><img src="/images/logo.png" alt="JDL Construction Logo" class="mb-4 w-50"></a>
|
|
85
|
+
|
|
86
|
+
<!-- Social Media Icons -->
|
|
87
|
+
<div class="flex justify-center my-6 space-x-4">
|
|
88
|
+
<a href="#" class="flex items-center justify-center w-12 h-12 transition rounded-full bg-lblue text-dblue hover:bg-opacity-80">
|
|
89
|
+
<i class="fab fa-facebook-f fa-lg" />
|
|
90
|
+
</a>
|
|
91
|
+
<a href="#" class="flex items-center justify-center w-12 h-12 transition rounded-full bg-lblue text-dblue hover:bg-opacity-80">
|
|
92
|
+
<i class="fab fa-instagram fa-lg" />
|
|
93
|
+
</a>
|
|
94
|
+
<a href="#" class="flex items-center justify-center w-12 h-12 transition rounded-full bg-lblue text-dblue hover:bg-opacity-80">
|
|
95
|
+
<i class="fab fa-houzz fa-lg" /> <!-- Houzz or custom icon -->
|
|
96
|
+
</a>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Mobile Menu Items -->
|
|
100
|
+
<ul class="w-full">
|
|
101
|
+
<li class="border-t border-lblue">
|
|
102
|
+
<NuxtLink to="/" class="text-lg tracking-widest uppercase" @click="closeMenu">
|
|
103
|
+
Home
|
|
104
|
+
</NuxtLink>
|
|
105
|
+
</li>
|
|
106
|
+
<li class="border-t border-b border-lblue">
|
|
107
|
+
<NuxtLink to="/contact" class="text-lg tracking-widest uppercase" @click="closeMenu">
|
|
108
|
+
Contact
|
|
109
|
+
</NuxtLink>
|
|
110
|
+
</li>
|
|
111
|
+
</ul>
|
|
112
|
+
|
|
113
|
+
<!-- Call to Action Section -->
|
|
114
|
+
<div class="mt-8 text-center">
|
|
115
|
+
<h2 class="text-3xl font-bold tracking-widest uppercase text-lblue">
|
|
116
|
+
Stuff
|
|
117
|
+
</h2>
|
|
118
|
+
<p class="mt-2 text-white">
|
|
119
|
+
More stuff here
|
|
120
|
+
</p>
|
|
121
|
+
<NuxtLink to="/contact" class="inline-block px-10 py-4 mt-4 text-lg font-bold tracking-widest underline uppercase transition text-dblue bg-burntorange hover:bg-opacity-80" @click="closeMenu">
|
|
122
|
+
Contact Us
|
|
123
|
+
</NuxtLink>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</transition>
|
|
127
|
+
</nav>
|
|
128
|
+
</template>
|
|
129
|
+
|
|
130
|
+
<style>
|
|
131
|
+
/* Mobile slide-in transition */
|
|
132
|
+
.slide-enter-active,
|
|
133
|
+
.slide-leave-active {
|
|
134
|
+
transition: transform 0.3s ease-in-out;
|
|
135
|
+
}
|
|
136
|
+
.slide-enter-from,
|
|
137
|
+
.slide-leave-to {
|
|
138
|
+
transform: translateX(100%);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Navigation Items with Triangle */
|
|
142
|
+
.nav-item {
|
|
143
|
+
position: relative;
|
|
144
|
+
|
|
145
|
+
transition: color 0.3s ease-in-out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Hover Effect */
|
|
149
|
+
.nav-item:hover {
|
|
150
|
+
color: #A2A8AE; /* Accent color on hover */
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.nav-item:hover::after {
|
|
154
|
+
transform: scale(1.3); /* Triangle expands */
|
|
155
|
+
background-color: white; /* Changes color slightly */
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Active State */
|
|
159
|
+
.nav-item.router-link-active {
|
|
160
|
+
color: white;
|
|
161
|
+
font-weight: bold;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.nav-item.router-link-active {
|
|
165
|
+
color: #A2A8AE;
|
|
166
|
+
}
|
|
167
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { onMounted, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
defineProps({
|
|
5
|
+
bgImage: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: true,
|
|
8
|
+
},
|
|
9
|
+
headline: {
|
|
10
|
+
type: String,
|
|
11
|
+
required: false,
|
|
12
|
+
default: '',
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// Refs for animation
|
|
17
|
+
const accentImg = ref(null)
|
|
18
|
+
const heroHeading = ref(null)
|
|
19
|
+
|
|
20
|
+
// Animation on mount
|
|
21
|
+
onMounted(() => {
|
|
22
|
+
setTimeout(() => {
|
|
23
|
+
if (accentImg.value) {
|
|
24
|
+
accentImg.value.classList.add('opacity-100', 'translate-x-0')
|
|
25
|
+
}
|
|
26
|
+
if (heroHeading.value) {
|
|
27
|
+
heroHeading.value.classList.add('opacity-100', 'translate-y-0')
|
|
28
|
+
}
|
|
29
|
+
}, 300)
|
|
30
|
+
})
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<section
|
|
35
|
+
class="relative z-[10] md:pt-20 md:pb-44 pb-16 px-6 flex items-center text-white text-center bg-right-bottom bg-cover md:bg-contain bg-no-repeat overflow-hidden"
|
|
36
|
+
:style="{ backgroundImage: `url(${bgImage})` }"
|
|
37
|
+
>
|
|
38
|
+
<!-- Ensure Text is Always on Top -->
|
|
39
|
+
<div class="container relative z-[20] m-auto text-left bg-opacity-50 md:pr-[50%] md:h-80">
|
|
40
|
+
<slot>
|
|
41
|
+
<h1
|
|
42
|
+
ref="heroHeading"
|
|
43
|
+
class="text-3xl md:text-6xl font-semibold !leading-snug opacity-0 translate-y-[20px] transition-all duration-1000 ease-out"
|
|
44
|
+
>
|
|
45
|
+
{{ headline }}
|
|
46
|
+
</h1>
|
|
47
|
+
</slot>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Gradient Background (top Half) -->
|
|
51
|
+
<div class="absolute z-10 top-0 left-0 w-full h-[128px] bg-gradient-to-b from-dblue to-transparent" />
|
|
52
|
+
|
|
53
|
+
<!-- Design Accent Image -->
|
|
54
|
+
<img
|
|
55
|
+
ref="accentImg"
|
|
56
|
+
src="/images/hero/titleShape.png"
|
|
57
|
+
alt="Decorative Accent"
|
|
58
|
+
class="absolute bottom-0 right-0 z-0 object-cover"
|
|
59
|
+
>
|
|
60
|
+
<!-- Diagonal Gradient Overlay -->
|
|
61
|
+
<div class="absolute inset-0 z-0 bg-reversed-angled-gradient" />
|
|
62
|
+
</section>
|
|
63
|
+
</template>
|
package/eslint.config.js
ADDED
package/nuxt.config.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineNuxtConfig } from 'nuxt/config'
|
|
2
|
+
|
|
3
|
+
export default defineNuxtConfig({
|
|
4
|
+
ssr: false,
|
|
5
|
+
compatibilityDate: '2024-11-01',
|
|
6
|
+
app: {
|
|
7
|
+
head: {
|
|
8
|
+
link: [
|
|
9
|
+
{
|
|
10
|
+
rel: 'icon',
|
|
11
|
+
type: 'image/x-icon',
|
|
12
|
+
href: '/favicon.ico',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
rel: 'stylesheet',
|
|
16
|
+
href: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
css: [
|
|
22
|
+
'~/assets/css/global.css', // ✅ Keep global styles only
|
|
23
|
+
],
|
|
24
|
+
modules: ['@nuxtjs/tailwindcss', '@vee-validate/nuxt'],
|
|
25
|
+
vite: {
|
|
26
|
+
define: {
|
|
27
|
+
'process.env.DEBUG': false,
|
|
28
|
+
},
|
|
29
|
+
server: {
|
|
30
|
+
hmr: {
|
|
31
|
+
port: 3000, // Make sure this port matches your Nuxt server port
|
|
32
|
+
clientPort: 3000, // Ensure this matches your Nuxt server port as well
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
devtools: { enabled: false },
|
|
37
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@edgedev/create-edge-site",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create Edge Starter Site",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-edge-site": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "nuxt build",
|
|
10
|
+
"dev": "nuxt dev",
|
|
11
|
+
"generate": "nuxt generate",
|
|
12
|
+
"preview": "nuxt preview",
|
|
13
|
+
"postinstall": "nuxt prepare"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@nuxtjs/tailwindcss": "6.13.2",
|
|
17
|
+
"@vee-validate/nuxt": "^4.15.0",
|
|
18
|
+
"@vee-validate/rules": "^4.15.0",
|
|
19
|
+
"@vee-validate/zod": "^4.15.0",
|
|
20
|
+
"nuxt": "^3.16.1",
|
|
21
|
+
"vue": "^3.5.13",
|
|
22
|
+
"vue-3-mask": "0.0.1-alpha",
|
|
23
|
+
"vue-router": "^4.5.0",
|
|
24
|
+
"vue-turnstile": "^1.0.11",
|
|
25
|
+
"zod": "^3.24.2"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@antfu/eslint-config": "^4.11.0",
|
|
29
|
+
"eslint": "^9"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { toTypedSchema } from '@vee-validate/zod'
|
|
3
|
+
import * as zod from 'zod'
|
|
4
|
+
|
|
5
|
+
// Zod Documentation: https://zod.dev/?id=strings
|
|
6
|
+
|
|
7
|
+
const validationSchema = toTypedSchema(
|
|
8
|
+
zod.object({
|
|
9
|
+
email: zod.string({
|
|
10
|
+
required_error: 'Email is required',
|
|
11
|
+
}).min(1, { message: 'Email is required' }).email({ message: 'Must be a valid email' }),
|
|
12
|
+
name: zod.string({
|
|
13
|
+
required_error: 'Name is required',
|
|
14
|
+
}).min(1, { message: 'Name is required' }),
|
|
15
|
+
phone: zod.string({
|
|
16
|
+
required_error: 'Phone is required',
|
|
17
|
+
}).min(14, { message: 'Valid phone # required' }),
|
|
18
|
+
message: zod.string({
|
|
19
|
+
required_error: 'Message is required',
|
|
20
|
+
}).min(1, { message: 'Message is required' }),
|
|
21
|
+
}),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
onMounted(() => {
|
|
25
|
+
console.log('Hello world.')
|
|
26
|
+
})
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<Head>
|
|
31
|
+
<title>Edge Website - An awesome Edge website</title>
|
|
32
|
+
<meta name="description" content="This is an Edge website template">
|
|
33
|
+
<link rel="canonical" href="https://edgemarketingdesign.com/">
|
|
34
|
+
</Head>
|
|
35
|
+
<div class="min-h-[calc(100vh_-_328px)] w-full items-center justify-center flex flex-col">
|
|
36
|
+
<edge-form-fling
|
|
37
|
+
v-slot="{ submitting }"
|
|
38
|
+
form-fling-endpoint="https://formfling.com/s/KLm807Hz7BXhB8S08uuF-oFPhf8TuWOSPGEmUATyV-2t5gal"
|
|
39
|
+
turnstile-site-secret="0x4AAAAAAANxjIQsY8S7Lqur"
|
|
40
|
+
:validation-schema="validationSchema"
|
|
41
|
+
success-message="Thank you, we will be in touch soon."
|
|
42
|
+
error-message="There was an error submitting the form."
|
|
43
|
+
success-class="text-green-500"
|
|
44
|
+
error-class="text-red-500"
|
|
45
|
+
>
|
|
46
|
+
<Input
|
|
47
|
+
type="text"
|
|
48
|
+
placeholder="Name"
|
|
49
|
+
name="name"
|
|
50
|
+
class="w-full px-4 py-2 border border-gray-300 focus:outline-none"
|
|
51
|
+
error-class="text-red-500 text-sm"
|
|
52
|
+
/>
|
|
53
|
+
<!-- Email and Phone -->
|
|
54
|
+
<div class="max-w-[400px] my-2 grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
55
|
+
<div>
|
|
56
|
+
<Input
|
|
57
|
+
type="email"
|
|
58
|
+
placeholder="Email"
|
|
59
|
+
name="email"
|
|
60
|
+
class="w-full px-4 py-2 border border-gray-300 focus:outline-none"
|
|
61
|
+
error-class="text-red-500 text-sm"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<Input
|
|
66
|
+
type="phone"
|
|
67
|
+
placeholder="Phone"
|
|
68
|
+
name="phone"
|
|
69
|
+
class="w-full px-4 py-2 border border-gray-300 focus:outline-none"
|
|
70
|
+
error-class="text-red-500 text-sm"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<!-- Message -->
|
|
75
|
+
<div>
|
|
76
|
+
<Textarea
|
|
77
|
+
name="message"
|
|
78
|
+
placeholder="Message"
|
|
79
|
+
class="w-full h-32 px-4 py-2 mt-2 border border-gray-300 resize-none focus:outline-none"
|
|
80
|
+
error-class="text-red-500 text-sm"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
<div>
|
|
84
|
+
<button v-if="!submitting" type="submit" class="px-6 mt-2 py-2 transition-colors bg-lblue text-dblue hover:bg-opacity-80">
|
|
85
|
+
Send Message
|
|
86
|
+
</button>
|
|
87
|
+
<button v-else type="button" class="px-6 mt-2 py-2 transition-colors bg-gray-300 text-gray-500 cursor-not-allowed">
|
|
88
|
+
Sending...
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
</edge-form-fling>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
package/pages/index.vue
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
onMounted(() => {
|
|
3
|
+
console.log('Hello world.')
|
|
4
|
+
})
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<Head>
|
|
9
|
+
<title>Edge Website - An awesome Edge website</title>
|
|
10
|
+
<meta name="description" content="This is an Edge website template">
|
|
11
|
+
<link rel="canonical" href="https://edgemarketingdesign.com/">
|
|
12
|
+
</Head>
|
|
13
|
+
<div class="min-h-[calc(100vh_-_328px)] w-full items-center justify-center flex flex-col">
|
|
14
|
+
<img src="/images/edge_logo.png" class="w-[400px] h-auto" alt="Edge Marketing Design Logo">
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineNuxtPlugin } from '#app'
|
|
2
|
+
import * as rules from '@vee-validate/rules'
|
|
3
|
+
import { defineRule } from 'vee-validate'
|
|
4
|
+
|
|
5
|
+
export default defineNuxtPlugin(() => {
|
|
6
|
+
for (const [ruleName, ruleFn] of Object.entries(rules)) {
|
|
7
|
+
if (typeof ruleFn === 'function') {
|
|
8
|
+
defineRule(ruleName, ruleFn)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
})
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: [
|
|
4
|
+
"./components/**/*.{vue,js,ts}",
|
|
5
|
+
"./layouts/**/*.{vue,js,ts}",
|
|
6
|
+
"./pages/**/*.{vue,js,ts}",
|
|
7
|
+
"./app.vue",
|
|
8
|
+
],
|
|
9
|
+
theme: {
|
|
10
|
+
extend: {
|
|
11
|
+
colors: {
|
|
12
|
+
dblue: "#30464C", // Dark Blue
|
|
13
|
+
mblue: "#46616F", // Medium Blue
|
|
14
|
+
lgray: "#A2A8AE", // Light Gray
|
|
15
|
+
lblue: "#87B4B7", // Light Blue
|
|
16
|
+
burntorange: "#B65B33", // Burnt Orange
|
|
17
|
+
tan: "#BDA86A", // Tan
|
|
18
|
+
cream: "#D9D0C4", // Cream
|
|
19
|
+
},
|
|
20
|
+
fontFamily: {
|
|
21
|
+
sans: ["Overpass", "sans-serif"],
|
|
22
|
+
serif: ["Rokkitt", "serif"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
plugins: [],
|
|
27
|
+
};
|
package/tsconfig.json
ADDED