@codecavepro/brand 1.0.0 → 1.2.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/README.md +85 -7
- package/dist/src/assets/icons/asterisk-icon.vue +12 -0
- package/dist/src/assets/icons/back-icon.vue +13 -0
- package/dist/src/assets/icons/cart-icon.vue +16 -0
- package/dist/src/assets/icons/close-icon.vue +10 -0
- package/dist/src/assets/icons/cloud-icon.vue +13 -0
- package/dist/src/assets/icons/database-icon.vue +17 -0
- package/dist/src/assets/icons/lightning-icon.vue +13 -0
- package/dist/src/assets/icons/linkedin-icon.vue +17 -0
- package/dist/src/assets/icons/panorama-icon.vue +16 -0
- package/dist/src/assets/icons/shevron.vue +8 -0
- package/dist/src/assets/icons/success-icon.vue +43 -0
- package/dist/src/assets/icons/verified-icon.vue +10 -0
- package/dist/src/assets/icons/widget-icon.vue +18 -0
- package/dist/src/assets/images/logo.svg +13 -0
- package/dist/src/components/common/Button.vue +46 -0
- package/dist/src/components/common/Checkbox.vue +118 -0
- package/dist/src/components/common/GlowButton.vue +122 -0
- package/dist/src/components/common/InputText.vue +58 -0
- package/dist/src/components/common/Radio.vue +68 -0
- package/dist/src/components/common/TextField.vue +55 -0
- package/dist/src/components/common/effects/TypingEffect.vue +38 -0
- package/dist/src/components/common/forms/ContactUsForm.vue +243 -0
- package/dist/src/components/common/forms/ResearchForm.vue +56 -0
- package/dist/src/components/common/images/LazyImage.vue +72 -0
- package/dist/src/components/footer/link-group.vue +27 -0
- package/dist/src/components/footer/links.ts +52 -0
- package/dist/src/components/header/desktop-menu.vue +95 -0
- package/dist/src/components/header/menu.ts +41 -0
- package/dist/src/components/header/mobile-menu.vue +117 -0
- package/dist/src/components/header/services-list.vue +26 -0
- package/dist/src/components/homepage/technology-card.vue +95 -0
- package/dist/src/helpers/form-validator.ts +9 -0
- package/dist/src/helpers/paths.ts +25 -0
- package/dist/src/lib/crm/types.ts +62 -0
- package/dist/tokens.css +229 -0
- package/package.json +15 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, onUnmounted, ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
src: string;
|
|
6
|
+
alt?: string;
|
|
7
|
+
threshold?: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
13
|
+
alt: '',
|
|
14
|
+
threshold:300,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const image = ref<HTMLImageElement | null>(null);
|
|
18
|
+
|
|
19
|
+
let observer: IntersectionObserver | null = null;
|
|
20
|
+
|
|
21
|
+
const loadImage = () => {
|
|
22
|
+
if (!image.value) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const src = image.value.dataset.src;
|
|
27
|
+
|
|
28
|
+
if (!src) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
image.value.src = src;
|
|
33
|
+
image.value.removeAttribute('data-src');
|
|
34
|
+
|
|
35
|
+
observer?.disconnect();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
onMounted(() => {
|
|
39
|
+
if (!image.value) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
observer = new IntersectionObserver(
|
|
44
|
+
(entries) => {
|
|
45
|
+
const entry = entries[0];
|
|
46
|
+
|
|
47
|
+
if (entry?.isIntersecting) {
|
|
48
|
+
loadImage();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
rootMargin: `${props.threshold}px`,
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
observer.observe(image.value);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
onUnmounted(() => {
|
|
60
|
+
observer?.disconnect();
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<img
|
|
66
|
+
ref="image"
|
|
67
|
+
:data-src="src"
|
|
68
|
+
:alt="alt"
|
|
69
|
+
:width="width"
|
|
70
|
+
:height="height"
|
|
71
|
+
/>
|
|
72
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Button from "../common/Button.vue";
|
|
3
|
+
import type { Link } from "./links.ts";
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
groupName: string
|
|
7
|
+
items: Link[]
|
|
8
|
+
}>()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div class="space-y-4">
|
|
13
|
+
<p class="text-body-secondary uppercase font-bold text-xs">
|
|
14
|
+
{{ groupName }}
|
|
15
|
+
</p>
|
|
16
|
+
<div class="space-y-3 xl:space-y-2 text-sm">
|
|
17
|
+
<div :key="index" v-for="(item, index) in items">
|
|
18
|
+
<Button
|
|
19
|
+
:title="item.name"
|
|
20
|
+
as="link"
|
|
21
|
+
:href="item.href"
|
|
22
|
+
variant="text"
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { paths } from "../../helpers/paths.ts";
|
|
2
|
+
|
|
3
|
+
export interface Link {
|
|
4
|
+
name: string
|
|
5
|
+
href: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const services: Link[] = [
|
|
9
|
+
{name: 'AR & VR', href: paths.arVr},
|
|
10
|
+
{name: 'Cloud services', href: paths.devops},
|
|
11
|
+
{name: 'DevOps', href: paths.devops},
|
|
12
|
+
{name: 'E-commerce', href: paths.ecommerce},
|
|
13
|
+
{name: 'Web development', href: paths.ecommerce},
|
|
14
|
+
{name: 'UX\\UI design', href: paths.ecommerce},
|
|
15
|
+
{name: 'QA & testing', href: paths.ecommerce},
|
|
16
|
+
{name: 'Automation & AI', href: paths.automation},
|
|
17
|
+
{name: 'Autodesk plugins', href: paths.autodesk},
|
|
18
|
+
{name: 'Legacy modernization', href: paths.ecommerce},
|
|
19
|
+
{name: 'Data migration', href: paths.hubspot},
|
|
20
|
+
{name: 'HubSpot', href: paths.hubspot},
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
export const industries: Link[] = [
|
|
24
|
+
{name: 'eCommerce', href: '/'},
|
|
25
|
+
{name: 'Logistics', href: '/'},
|
|
26
|
+
{name: 'Healthcare', href: '/'},
|
|
27
|
+
{name: 'Manufacturing', href: '/'},
|
|
28
|
+
{name: 'Education', href: '/'},
|
|
29
|
+
{name: 'Finance', href: '/'},
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
export const other: Link[] = [
|
|
33
|
+
{name: 'Our projects', href: paths.projects},
|
|
34
|
+
{name: 'Feedback', href: paths.testimonials},
|
|
35
|
+
{name: 'How we work', href: paths.workflow},
|
|
36
|
+
{name: 'Our strong points', href: paths.strongPoints},
|
|
37
|
+
{name: 'Our tech stack', href: paths.toolsAndTechnologies},
|
|
38
|
+
{name: 'All Insights', href: paths.insights},
|
|
39
|
+
{name: 'F.A.Q.', href: '/#questions'},
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
export const reviews: Link[] = [
|
|
43
|
+
{name: 'Trustpilot', href: 'https://www.trustpilot.com/review/codecave.pro'},
|
|
44
|
+
{name: 'Glassdoor', href: 'https://www.glassdoor.com/Reviews/Employee-Review-CodeCave-E8324784-RVW95188942.htm'},
|
|
45
|
+
{name: 'Clutch', href: 'https://clutch.co/profile/codecave-0-1'},
|
|
46
|
+
{name: 'G2', href: 'https://www.g2.com/products/codecave/expertises'},
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
export const legal: Link[] = [
|
|
50
|
+
{name: 'Cookie policy', href: '/cookie-policy'},
|
|
51
|
+
{name: 'Privacy policy', href: '/privacy-policy'},
|
|
52
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { menu } from "./menu.ts";
|
|
3
|
+
import Button from "../common/Button.vue";
|
|
4
|
+
import Shevron from "../../assets/icons/shevron.vue";
|
|
5
|
+
import { paths } from "../../helpers/paths.ts";
|
|
6
|
+
import Logo from "../../assets/images/logo.svg";
|
|
7
|
+
import ServicesList from "./services-list.vue";
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="page-container py-3">
|
|
12
|
+
<nav class="flex justify-between items-center relative text-sm">
|
|
13
|
+
<ul class="flex">
|
|
14
|
+
<li v-for="(item, index) in menu.slice(0, 3)" :key="index">
|
|
15
|
+
<div v-if="item.name === 'Services'" class="dropdown">
|
|
16
|
+
<Button variant="ghost" class="cursor-pointer dropbtn" :title="item.name">
|
|
17
|
+
<Shevron class="ml-1" />
|
|
18
|
+
</Button>
|
|
19
|
+
<div class="dropdown-content bg-surface-primary-transparent backdrop-blur-3xl rounded-2xl p-5">
|
|
20
|
+
<ServicesList />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<Button v-else as="link" variant="ghost" :title="item.name" :href="item.link" />
|
|
24
|
+
</li>
|
|
25
|
+
</ul>
|
|
26
|
+
<a :href="paths.home" class="absolute left-1/2 -translate-x-1/2 hover:opacity-80 transition-opacity">
|
|
27
|
+
<img :src="Logo.src" alt="CODECAVE" />
|
|
28
|
+
</a>
|
|
29
|
+
<ul class="flex">
|
|
30
|
+
<li v-for="(item, index) in menu.slice(3, 5)" :key="index">
|
|
31
|
+
<Button as="link" variant="ghost" :title="item.name" :href="item.link" />
|
|
32
|
+
</li>
|
|
33
|
+
</ul>
|
|
34
|
+
</nav>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<style scoped>
|
|
39
|
+
ul:last-of-type li:last-child {
|
|
40
|
+
position: relative;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ul:last-of-type li:last-child::after {
|
|
44
|
+
position: absolute;
|
|
45
|
+
content: '';
|
|
46
|
+
width: 10px;
|
|
47
|
+
height: 10px;
|
|
48
|
+
right: 8px;
|
|
49
|
+
top: 11px;
|
|
50
|
+
background: red;
|
|
51
|
+
border-radius: 50%;
|
|
52
|
+
box-shadow: 0 0 12px 0 var(--color-error-400),
|
|
53
|
+
0 0 4px 0 hsl(from var(--color-error-400) h s l / 0.5);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.dropdown {
|
|
57
|
+
position: relative;
|
|
58
|
+
display: inline-block;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dropdown-content {
|
|
62
|
+
position: absolute;
|
|
63
|
+
top: 100%;
|
|
64
|
+
left: 0;
|
|
65
|
+
margin-top: 12px;
|
|
66
|
+
width: max-content;
|
|
67
|
+
|
|
68
|
+
opacity: 0;
|
|
69
|
+
visibility: hidden;
|
|
70
|
+
|
|
71
|
+
transition:
|
|
72
|
+
opacity 0.2s ease,
|
|
73
|
+
transform 0.2s ease,
|
|
74
|
+
visibility 0.2s;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.dropdown-content::before {
|
|
78
|
+
content: '';
|
|
79
|
+
position: absolute;
|
|
80
|
+
top: -20px;
|
|
81
|
+
left: 0;
|
|
82
|
+
width: 100px;
|
|
83
|
+
height: 20px;
|
|
84
|
+
background-color: transparent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.dropdown:hover .dropdown-content {
|
|
88
|
+
opacity: 1;
|
|
89
|
+
visibility: visible;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.dropdown:hover .dropbtn {
|
|
93
|
+
color: var(--color-hovered);
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { paths } from "../../helpers/paths.ts";
|
|
2
|
+
import type { Component } from "vue";
|
|
3
|
+
import CloudIcon from "../../assets/icons/cloud-icon.vue";
|
|
4
|
+
import CartIcon from "../../assets/icons/cart-icon.vue";
|
|
5
|
+
import WidgetIcon from "../../assets/icons/widget-icon.vue";
|
|
6
|
+
import LightningIcon from "../../assets/icons/lightning-icon.vue";
|
|
7
|
+
import DatabaseIcon from "../../assets/icons/database-icon.vue";
|
|
8
|
+
import PanoramaIcon from "../../assets/icons/panorama-icon.vue";
|
|
9
|
+
|
|
10
|
+
interface Submenu {
|
|
11
|
+
icon: Component
|
|
12
|
+
name: string
|
|
13
|
+
description: string
|
|
14
|
+
link: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Menu {
|
|
18
|
+
name: string
|
|
19
|
+
link?: string
|
|
20
|
+
submenuTitle?: string
|
|
21
|
+
submenu?: Submenu[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const menu: Menu[] = [
|
|
25
|
+
{
|
|
26
|
+
name: 'Services',
|
|
27
|
+
submenuTitle: 'Services',
|
|
28
|
+
submenu: [
|
|
29
|
+
{icon: CloudIcon, name: 'Cloud & DevOps', description: 'Optimize costs. Protect your Data', link: paths.devops},
|
|
30
|
+
{icon: CartIcon, name: 'E-Commerce', description: 'Maximize revenue, dominate Markets', link: paths.ecommerce},
|
|
31
|
+
{icon: WidgetIcon, name: 'Autodesk plugins', description: '10x efficiency with custom Plugins', link: paths.autodesk},
|
|
32
|
+
{icon: LightningIcon, name: 'Automation & AI', description: 'Leverage virtual Workforce', link: paths.automation},
|
|
33
|
+
{icon: DatabaseIcon, name: 'HubSpot', description: 'Aggregate data from all your Tools', link: paths.hubspot},
|
|
34
|
+
{icon: PanoramaIcon, name: 'AR & VR', description: 'Stunning visualisations for your business', link: paths.arVr},
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{name: 'Workflow', link: paths.workflow},
|
|
38
|
+
{name: 'Projects', link: paths.projects},
|
|
39
|
+
{name: 'Insights', link: paths.insights},
|
|
40
|
+
{name: 'Contact us', link: paths.contactUs},
|
|
41
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { menu } from "./menu.ts";
|
|
3
|
+
import Button from "../common/Button.vue";
|
|
4
|
+
import Shevron from "../../assets/icons/shevron.vue";
|
|
5
|
+
import { onUnmounted, ref, watch } from "vue";
|
|
6
|
+
import { paths } from "../../helpers/paths.ts";
|
|
7
|
+
import Logo from "../../assets/images/logo.svg";
|
|
8
|
+
import BackIcon from "../../assets/icons/back-icon.vue";
|
|
9
|
+
import ServicesList from "./services-list.vue";
|
|
10
|
+
|
|
11
|
+
const isMenuOpen = ref(false)
|
|
12
|
+
const isServicesOpen = ref(false)
|
|
13
|
+
let scrollPosition = 0
|
|
14
|
+
|
|
15
|
+
const lockScroll = () => {
|
|
16
|
+
scrollPosition = window.scrollY || window.pageYOffset
|
|
17
|
+
|
|
18
|
+
document.body.style.overflow = 'hidden'
|
|
19
|
+
document.body.style.position = 'fixed'
|
|
20
|
+
document.body.style.top = `-${scrollPosition}px`
|
|
21
|
+
document.body.style.width = '100%'
|
|
22
|
+
|
|
23
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
|
|
24
|
+
if (scrollbarWidth > 0) {
|
|
25
|
+
document.body.style.paddingRight = `${scrollbarWidth}px`
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const unlockScroll = () => {
|
|
30
|
+
document.body.style.position = ''
|
|
31
|
+
document.body.style.top = ''
|
|
32
|
+
document.body.style.width = ''
|
|
33
|
+
document.body.style.paddingRight = ''
|
|
34
|
+
document.body.style.overflow = ''
|
|
35
|
+
window.scrollTo(0, scrollPosition)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
watch(isMenuOpen, (newVal) => {
|
|
39
|
+
if (newVal) {
|
|
40
|
+
lockScroll()
|
|
41
|
+
} else {
|
|
42
|
+
unlockScroll()
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
onUnmounted(() => {
|
|
47
|
+
if (isMenuOpen.value) {
|
|
48
|
+
unlockScroll()
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
const handleCloseMenu = () => {
|
|
52
|
+
isMenuOpen.value = false
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<div>
|
|
58
|
+
<div :class="`fixed z-50 top-0 w-full bg-surface-primary-transparent backdrop-blur-3xl ${isMenuOpen ? 'rounded-b-3xl' : 'rounded-none'}`">
|
|
59
|
+
<div :class="`h-full px-5 ${isMenuOpen ? 'pb-5' : 'pb-0'} flex flex-col gap-2 transition-all`">
|
|
60
|
+
<div class="flex justify-between items-center py-1.5">
|
|
61
|
+
<a :href="paths.home" class="px-1.5 hover:opacity-80 transition-opacity">
|
|
62
|
+
<img :src="Logo.src" alt="CODECAVE" />
|
|
63
|
+
</a>
|
|
64
|
+
<button @click="isMenuOpen = !isMenuOpen" :class="`burger-menu ${isMenuOpen ? 'text-action' : 'text-heading'} transition-colors`">
|
|
65
|
+
<span></span>
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
<nav v-if="isMenuOpen" class="h-full text-sm">
|
|
69
|
+
<div v-if="isServicesOpen">
|
|
70
|
+
<h2 class="text-center font-bold text-body-primary py-2.5">
|
|
71
|
+
{{ menu[0].submenuTitle }}
|
|
72
|
+
</h2>
|
|
73
|
+
<ServicesList />
|
|
74
|
+
<button class="pt-4" @click="isServicesOpen = false">
|
|
75
|
+
<component class="w-11 h-11 text-action" :is="BackIcon" />
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
<ul v-else class="h-full flex flex-col items-center gap-3 justify-around px-4 pt-2">
|
|
79
|
+
<li v-for="(item, index) in menu" :key="index">
|
|
80
|
+
<div v-if="item.name === 'Services'">
|
|
81
|
+
<Button :title="item.name" variant="ghost" @click="isServicesOpen = true" class="transition-transform duration-150 pr-1">
|
|
82
|
+
<Shevron class="rotate-270 ml-1" />
|
|
83
|
+
</Button>
|
|
84
|
+
</div>
|
|
85
|
+
<Button v-else-if="item.name === 'Workflow'" as="link" variant="ghost" title="About us | Workflow" :href="item.link" />
|
|
86
|
+
<Button v-else-if="item.name === 'Contact us'" as="link" :href="item.link" title="Get a free consultation" variant="tertiary" @click="handleCloseMenu" />
|
|
87
|
+
<Button v-else as="link" variant="ghost" :title="item.name" :href="item.link" />
|
|
88
|
+
</li>
|
|
89
|
+
</ul>
|
|
90
|
+
</nav>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div v-if="isMenuOpen" class="fixed z-0 inset-0" @click="handleCloseMenu">
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<style scoped>
|
|
99
|
+
.burger-menu {
|
|
100
|
+
display: flex;
|
|
101
|
+
justify-content: space-around;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
padding: 0.625rem;
|
|
104
|
+
width: 3rem;
|
|
105
|
+
height: 3rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.burger-menu::before,
|
|
109
|
+
.burger-menu::after,
|
|
110
|
+
.burger-menu span {
|
|
111
|
+
content: '';
|
|
112
|
+
width: 100%;
|
|
113
|
+
height: 0.1rem;
|
|
114
|
+
background-color: currentColor;
|
|
115
|
+
border-radius: 3rem;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {menu} from "./menu.ts";
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<ul class="grid xl:grid-cols-2 gap-3.5 xl:gap-x-8 xl:gap-y-4">
|
|
7
|
+
<li v-for="item in menu[0].submenu" :key="item.name">
|
|
8
|
+
<a :href="item.link" class="flex items-center gap-3 group">
|
|
9
|
+
<div class="w-fit p-3 rounded-xl bg-surface-tertiary">
|
|
10
|
+
<component
|
|
11
|
+
class="w-5 h-5 text-default-transparent group-active:text-hovered group-hover:text-hovered transition-colors"
|
|
12
|
+
:is="item.icon"
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="text-sm">
|
|
16
|
+
<h3 class="font-bold text-heading group-active:text-hovered group-hover:text-hovered transition-colors">
|
|
17
|
+
{{ item.name }}
|
|
18
|
+
</h3>
|
|
19
|
+
<p class="text-body-secondary-lighter group-active:text-hovered group-hover:text-hovered transition-colors">
|
|
20
|
+
{{ item.description }}
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
</a>
|
|
24
|
+
</li>
|
|
25
|
+
</ul>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { paths } from "../../helpers/paths.ts";
|
|
3
|
+
import Button from "../common/Button.vue";
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
active: boolean
|
|
7
|
+
name: string
|
|
8
|
+
className?: string
|
|
9
|
+
index?: number
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const rotate = [
|
|
13
|
+
'-rotate-4 left-2 top-10',
|
|
14
|
+
'-rotate-1 left-1/4 top-20',
|
|
15
|
+
'rotate-1 left-1/2 top-20',
|
|
16
|
+
'rotate-4 right-2 top-10',
|
|
17
|
+
'-rotate-2 right-2/3 top-3/5',
|
|
18
|
+
'rotate-2 left-2/3 top-3/5',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const translate = [
|
|
22
|
+
'xl:-translate-y-1/3',
|
|
23
|
+
'xl:-translate-y-1/3',
|
|
24
|
+
'xl:-translate-y-1/3',
|
|
25
|
+
'xl:-translate-y-1/3',
|
|
26
|
+
'xl:-translate-y-1/2',
|
|
27
|
+
'xl:-translate-y-1/2',
|
|
28
|
+
]
|
|
29
|
+
const getTechnologyUrl = (name: string) => {
|
|
30
|
+
switch (name) {
|
|
31
|
+
case 'AR & VR':
|
|
32
|
+
return paths.arVr
|
|
33
|
+
case 'Autodesk plugins':
|
|
34
|
+
return paths.autodesk
|
|
35
|
+
case 'Automation & AI':
|
|
36
|
+
return paths.automation
|
|
37
|
+
case 'Cloud & DevOps':
|
|
38
|
+
return paths.devops
|
|
39
|
+
case 'E-Commerce':
|
|
40
|
+
return paths.ecommerce
|
|
41
|
+
case 'HubSpot':
|
|
42
|
+
return paths.hubspot
|
|
43
|
+
default:
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<div :class="`rounded-3xl card-wrapper cursor-pointer select-none absolute transform transition-transform duration-500
|
|
51
|
+
${rotate[index]} ${active ? `${translate[index]}` : ''} ${className || ''}`">
|
|
52
|
+
<div class="card flex flex-col items-center justify-around">
|
|
53
|
+
<h3 class="max-w-[8rem] text-center text-xl font-bold text-heading text-balance">
|
|
54
|
+
{{ name }}
|
|
55
|
+
</h3>
|
|
56
|
+
<Button as="link" :href="getTechnologyUrl(name)" title="Explore service" variant="tertiary" :class="`${active ? 'block' : 'hidden xl:block xl:opacity-0'}`" />
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<style scoped>
|
|
62
|
+
.card-wrapper {
|
|
63
|
+
background:
|
|
64
|
+
linear-gradient(hsl(from var(--color-technology-gradient-25) h s l / 0.1),
|
|
65
|
+
hsl(from var(--color-technology-gradient-0) h s l / 0.1),
|
|
66
|
+
hsl(from var(--color-technology-gradient-50) h s l / 0.1)) border-box;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.card {
|
|
70
|
+
width: 100%;
|
|
71
|
+
height: 100%;
|
|
72
|
+
border-radius: inherit;
|
|
73
|
+
position: relative;
|
|
74
|
+
background: transparent;
|
|
75
|
+
backdrop-filter: blur(14px);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.card::before {
|
|
79
|
+
content: '';
|
|
80
|
+
position: absolute;
|
|
81
|
+
z-index: -1;
|
|
82
|
+
inset: 0;
|
|
83
|
+
border-radius: inherit;
|
|
84
|
+
border: 1px solid transparent;
|
|
85
|
+
outline: 1px solid transparent;
|
|
86
|
+
-webkit-backface-visibility: hidden;
|
|
87
|
+
transform: translate3d(0, 0, 0);
|
|
88
|
+
background: linear-gradient(hsl(from var(--color-brand-500) h s l / 0.35),
|
|
89
|
+
hsl(from var(--color-brand-400) h s l / 0.675),
|
|
90
|
+
var(--color-brand-500)) border-box;
|
|
91
|
+
mask: linear-gradient(black, black) border-box,
|
|
92
|
+
linear-gradient(black, black) padding-box;
|
|
93
|
+
mask-composite: subtract;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
2
|
+
const linkedInRegex = /^https?:\/\/([a-z]{2,3}\.)?linkedin\.com\/.*$/gim
|
|
3
|
+
|
|
4
|
+
export const isCorrectEmailFormat = (email: string | null): boolean => {
|
|
5
|
+
return !!email && emailRegex.test(email)
|
|
6
|
+
}
|
|
7
|
+
export const isCorrectLinkedInFormat = (linkedIn: string | null) : boolean => {
|
|
8
|
+
return !!linkedIn && linkedInRegex.test(linkedIn);
|
|
9
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const services = '/services'
|
|
2
|
+
|
|
3
|
+
export const paths = {
|
|
4
|
+
home: '/',
|
|
5
|
+
|
|
6
|
+
devops: `${services}/devops`,
|
|
7
|
+
autodesk: `${services}/autodesk`,
|
|
8
|
+
hubspot: `${services}/hubspot`,
|
|
9
|
+
ecommerce: `${services}/ecommerce`,
|
|
10
|
+
automation: `${services}/automation`,
|
|
11
|
+
arVr: `${services}/ar-vr`,
|
|
12
|
+
|
|
13
|
+
workflow: '/workflow',
|
|
14
|
+
projects: '/projects',
|
|
15
|
+
insights: '/insights',
|
|
16
|
+
contactUs: '/#contact-us',
|
|
17
|
+
cookiePolicy: '/cookie-policy',
|
|
18
|
+
privacyPolicy: '/privacy-policy',
|
|
19
|
+
|
|
20
|
+
testimonials: '/#testimonials',
|
|
21
|
+
strongPoints: '/workflow/#our-strong-point',
|
|
22
|
+
toolsAndTechnologies: '/workflow/#tools-and-technologies'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const websiteUrl = 'https://www.codecave.it'
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* The CRM boundary for form components.
|
|
2
|
+
*
|
|
3
|
+
* A form component's job is to collect and validate what a person typed. Which
|
|
4
|
+
* CRM receives it, what that CRM calls its fields and what its wire format
|
|
5
|
+
* looks like are not the component's business -- they are the site's. This file
|
|
6
|
+
* is the line between the two.
|
|
7
|
+
*
|
|
8
|
+
* Nothing here mentions HubSpot. That is the point: see hubspot-form-client.ts
|
|
9
|
+
* for the implementation, which is where every HubSpot-shaped thing lives now.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface CrmFormFieldOption {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** What a component needs in order to render one field. */
|
|
18
|
+
export interface CrmFormFieldSpec {
|
|
19
|
+
label: string;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
required: boolean;
|
|
22
|
+
maxLength?: number;
|
|
23
|
+
options?: CrmFormFieldOption[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ContactFormFieldName =
|
|
27
|
+
| 'email'
|
|
28
|
+
| 'firstName'
|
|
29
|
+
| 'lastName'
|
|
30
|
+
| 'companyName'
|
|
31
|
+
| 'linkedinCompanyPage'
|
|
32
|
+
| 'services'
|
|
33
|
+
| 'description';
|
|
34
|
+
|
|
35
|
+
/** Labels, placeholders and options for the contact form, CRM-neutral. */
|
|
36
|
+
export type ContactFormDefinition = Record<ContactFormFieldName, CrmFormFieldSpec>;
|
|
37
|
+
|
|
38
|
+
/** What the visitor typed, by logical name. */
|
|
39
|
+
export interface ContactFormValues {
|
|
40
|
+
email: string;
|
|
41
|
+
firstName: string;
|
|
42
|
+
lastName: string;
|
|
43
|
+
companyName: string;
|
|
44
|
+
linkedinCompanyPage: string;
|
|
45
|
+
services: string;
|
|
46
|
+
description: string;
|
|
47
|
+
privacyPolicyAccepted: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* A result rather than a thrown error, deliberately.
|
|
51
|
+
*
|
|
52
|
+
* The previous code did `catch (error) { console.log('request err>>', error) }`
|
|
53
|
+
* -- the form went quiet and the visitor was never told their enquiry had not
|
|
54
|
+
* been sent. A union the caller has to narrow makes handling the failure the
|
|
55
|
+
* obvious thing to write, rather than something easy to leave out. */
|
|
56
|
+
export type CrmSubmitResult =
|
|
57
|
+
| { ok: true }
|
|
58
|
+
| { ok: false; reason: 'network' | 'rejected'; message?: string };
|
|
59
|
+
|
|
60
|
+
export interface ICrmFormClient {
|
|
61
|
+
submit(values: ContactFormValues): Promise<CrmSubmitResult>;
|
|
62
|
+
}
|