@codecavepro/brand 3.1.0 → 3.2.1

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.
@@ -5,49 +5,68 @@ import { gsap } from "gsap";
5
5
  const props = defineProps<{
6
6
  title: string
7
7
  class?: string
8
- href?: string
8
+ href?: string | null
9
9
  }>()
10
+
10
11
  const isTouchDevice = ref(false)
11
- const link = ref(null)
12
- const glow = ref(null)
13
- const edgeLeft = ref(null)
14
- const edgeRight = ref(null)
15
- let leaveTween = null
12
+ const link = ref<HTMLElement | null>(null)
13
+ const glow = ref<HTMLElement | null>(null)
14
+ const edgeLeft = ref<HTMLElement | null>(null)
15
+ const edgeRight = ref<HTMLElement | null>(null)
16
+
17
+ let leaveTween: gsap.core.Tween | null = null
18
+
16
19
  const handleMouseMove = (e: MouseEvent) => {
17
- if (isTouchDevice.value) return
20
+ if (isTouchDevice.value || !link.value || !glow.value) return
18
21
 
19
22
  const rect = link.value.getBoundingClientRect()
20
23
  const x = e.clientX - rect.left
21
24
  const y = e.clientY - rect.top
22
25
  const percentX = x / rect.width
26
+
23
27
  gsap.to(glow.value, {
24
28
  left: x,
25
29
  top: y,
26
30
  duration: 0.5,
27
31
  ease: 'power2.out',
28
32
  })
33
+
29
34
  const edgeZone = 0.1
30
35
  let leftOpacity = 0
31
36
  let rightOpacity = 0
37
+
32
38
  if (percentX < edgeZone) {
33
39
  leftOpacity = 1 - percentX / edgeZone
34
40
  } else if (percentX > 1 - edgeZone) {
35
41
  rightOpacity = (percentX - (1 - edgeZone)) / edgeZone
36
42
  }
37
- edgeLeft.value.style.opacity = leftOpacity
38
- edgeRight.value.style.opacity = rightOpacity
43
+
44
+ if (edgeLeft.value) {
45
+ edgeLeft.value.style.opacity = String(leftOpacity)
46
+ }
47
+
48
+ if (edgeRight.value) {
49
+ edgeRight.value.style.opacity = String(rightOpacity)
50
+ }
39
51
  }
52
+
40
53
  const handleMouseEnter = () => {
41
54
  if (isTouchDevice.value) return
55
+
42
56
  if (leaveTween) {
43
57
  leaveTween.kill()
44
58
  leaveTween = null
45
59
  }
46
60
  }
61
+
47
62
  const handleMouseLeave = () => {
48
- edgeLeft.value.style.opacity = 1
49
- edgeRight.value.style.opacity = 0
63
+ if (edgeLeft.value) edgeLeft.value.style.opacity = '1'
64
+ if (edgeRight.value) edgeRight.value.style.opacity = '0'
65
+
50
66
  if (leaveTween) leaveTween.kill()
67
+
68
+ if (!glow.value) return
69
+
51
70
  leaveTween = gsap.to(glow.value, {
52
71
  left: '20%',
53
72
  top: '50%',
@@ -56,35 +75,49 @@ const handleMouseLeave = () => {
56
75
  ease: 'power2.out',
57
76
  })
58
77
  }
78
+
59
79
  onMounted(() => {
60
- isTouchDevice.value = window.matchMedia("(pointer: coarse)").matches
61
- || "ontouchstart" in window
62
- || navigator.maxTouchPoints > 0
80
+ isTouchDevice.value =
81
+ window.matchMedia('(pointer: coarse)').matches ||
82
+ 'ontouchstart' in window ||
83
+ navigator.maxTouchPoints > 0
63
84
  })
64
85
  </script>
65
86
 
66
87
  <template>
67
88
  <div :class="`relative inline-block ${props.class ?? ''}`">
68
89
  <div ref="edgeLeft" class="edge-glow -left-2"></div>
69
- <a ref="link" :href="href" @mousemove="handleMouseMove" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" :class="`cursor-pointer flex items-center justify-center relative z-[1] overflow-hidden w-full h-12 px-6 py-1 rounded-full bg-glow-25 ${props.class ?? ''}`">
70
- <span class="relative z-[2] text-primary-800 font-bold select-none">
90
+
91
+ <component
92
+ :is="href ? 'a' : 'span'"
93
+ ref="link"
94
+ :href="href || undefined"
95
+ @mousemove="handleMouseMove"
96
+ @mouseenter="handleMouseEnter"
97
+ @mouseleave="handleMouseLeave"
98
+ class="cursor-pointer flex items-center justify-center relative z-[1] overflow-hidden w-full h-12 px-6 py-1 rounded-full bg-glow-25"
99
+ >
100
+ <p class="relative z-[2] text-primary-800 font-bold select-none">
71
101
  {{ title }}
72
- </span>
102
+ </p>
103
+
73
104
  <div ref="glow" class="glow"></div>
74
- </a>
105
+ </component>
106
+
75
107
  <div ref="edgeRight" class="edge-glow -right-2 opacity-0"></div>
76
108
  </div>
77
109
  </template>
78
110
 
79
111
  <style scoped>
80
- a {
112
+ span, a {
81
113
  box-shadow: 0 0 64px 0 #7A58FFA8,
82
114
  0 0 16px 0 #4F22FFA6,
83
115
  0 0 4px 2px #5B34FA;
84
116
  transition: transform 0.5s ease;
85
117
  }
86
118
 
87
- a:active {
119
+
120
+ span, a:active {
88
121
  transform: scale(0.98);
89
122
  }
90
123
 
@@ -2,6 +2,7 @@
2
2
  import LinkedinIcon from "../assets/icons/linkedin-icon.vue";
3
3
  import VerifiedIcon from "../assets/icons/verified-icon.vue";
4
4
  import LazyImage from "./images/LazyImage.vue"
5
+ import { isCorrectLinkedInFormat } from "../helpers/form-validator.ts";
5
6
 
6
7
  /* See ArticlePreview.vue: the fields read here, not the generated Strapi
7
8
  * `Testimonial`, which a caller may still pass unchanged. */
@@ -27,14 +28,14 @@ const imageUrl = (url: string) => props.resolveImage?.(url) ?? url
27
28
  <template>
28
29
  <div :class="`mx-1 lg:mx-2 testimonial space-y-2 lg:space-y-3 py-10 px-6 lg:px-11 ${className}`">
29
30
  <div class="flex flex-col lg:flex-row gap-5 lg:items-center">
30
- <LazyImage v-if="item.photo.name !== 'no-image.svg'"
31
+ <img v-if="item.photo.name !== ''"
31
32
  class="w-12 lg:w-16 h-12 lg:h-16" :src="imageUrl(item.photo.url)"
32
33
  :alt=item.photo.name />
33
34
  <div>
34
35
  <div class="flex items-center gap-2">
35
36
  <p class="text-heading text-lg lg:text-xl">{{ item.name }}</p>
36
37
  <a
37
- v-if="item.linkedinurl.length > 1"
38
+ v-if="isCorrectLinkedInFormat(item.linkedinurl)"
38
39
  target="_blank"
39
40
  rel='noopener noreferrer'
40
41
  class="text-default-transparent hover:text-action transition-colors"
@@ -54,25 +54,24 @@ const createFormField = <T,>(name: keyof ContactFormDefinition, value: T): FormF
54
54
  const formData = ref({
55
55
  email: createFormField('email', ''),
56
56
  firstName: createFormField('firstName', ''),
57
- lastName: createFormField('lastName', ''),
58
57
  companyName: createFormField('companyName', ''),
59
58
  linkedinCompanyPage: createFormField('linkedinCompanyPage', ''),
60
59
  services: createFormField('services', ''),
61
60
  description: createFormField('description', ''),
62
-
63
- consentToProcess: {
64
- value: false,
65
- error: '',
66
- label: props.definition.consentToProcess.label,
67
- required: props.definition.consentToProcess.required,
68
- },
69
-
70
- communicationConsent: {
71
- value: false,
72
- error: '',
73
- label: props.definition.communicationConsent.label,
74
- required: props.definition.communicationConsent.required,
75
- },
61
+
62
+ consentToProcess: {
63
+ value: false,
64
+ error: '',
65
+ label: props.definition.consentToProcess.label,
66
+ required: props.definition.consentToProcess.required,
67
+ },
68
+
69
+ communicationConsent: {
70
+ value: false,
71
+ error: '',
72
+ label: props.definition.communicationConsent.label,
73
+ required: props.definition.communicationConsent.required,
74
+ },
76
75
  })
77
76
 
78
77
  const services = computed(() => props.definition.services.options ?? [])
@@ -123,7 +122,6 @@ const validateRequiredFields = () => {
123
122
  const resetForm = () => {
124
123
  formData.value.email.value = '';
125
124
  formData.value.firstName.value = '';
126
- formData.value.lastName.value = '';
127
125
  formData.value.companyName.value = '';
128
126
  formData.value.linkedinCompanyPage.value = '';
129
127
  formData.value.description.value = '';
@@ -136,7 +134,6 @@ const resetForm = () => {
136
134
  const currentValues = (): ContactFormValues => ({
137
135
  email: formData.value.email.value,
138
136
  firstName: formData.value.firstName.value,
139
- lastName: formData.value.lastName.value,
140
137
  companyName: formData.value.companyName.value,
141
138
  linkedinCompanyPage: formData.value.linkedinCompanyPage.value,
142
139
  services: formData.value.services.value,
@@ -233,6 +230,22 @@ fields.forEach((key) => {
233
230
  )
234
231
  })
235
232
 
233
+ const companyNamePattern = /[^\p{L}\p{N} &]/u;
234
+ const firstNamePattern = /[^\p{L} ]/u;
235
+
236
+ const createSanitizer = (pattern) => {
237
+ return (value) => value.replace(pattern, '');
238
+ };
239
+
240
+ const sanitizeCompanyName = createSanitizer(companyNamePattern);
241
+ const sanitizeFirstName = createSanitizer(firstNamePattern);
242
+
243
+ const preventInvalidInput = (event, pattern) => {
244
+ if (event.data && pattern.test(event.data)) {
245
+ event.preventDefault();
246
+ }
247
+ };
248
+
236
249
  </script>
237
250
 
238
251
  <template>
@@ -241,11 +254,10 @@ fields.forEach((key) => {
241
254
  <fieldset class="space-y-2">
242
255
  <InputText id="email" v-model="formData.email.value" :isRequired=formData.email.required :label=formData.email.label type="email" :isError="!!formData.email.error" :errorMessage="formData.email.error" :placeholder="formData.email.placeholder ?? ''" autocomplete="email" />
243
256
  <div class="flex flex-col xl:flex-row gap-2">
244
- <InputText id="firstname" v-model="formData.firstName.value" :isRequired=formData.firstName.required :label=formData.firstName.label type="text" :isError="!!formData.firstName.error" :errorMessage="formData.firstName.error" :placeholder="formData.firstName.placeholder ?? ''" autocomplete="given-name" />
245
- <InputText id="lastname" v-model="formData.lastName.value" :isRequired=formData.lastName.required :label=formData.lastName.label type="text" :isError="!!formData.lastName.error" :errorMessage="formData.lastName.error" :placeholder="formData.lastName.placeholder ?? ''" autocomplete="family-name" />
257
+ <InputText id="firstname" v-model="formData.firstName.value" @beforeinput="event => preventInvalidInput(event, firstNamePattern)" @update:model-value="value => { formData.firstName.value = sanitizeFirstName(value) }" :isRequired="formData.firstName.required" :label="formData.firstName.label" type="text" :isError="!!formData.firstName.error" :errorMessage="formData.firstName.error" :placeholder="formData.firstName.placeholder ?? ''" autocomplete="given-name" />
258
+ <InputText id="companyName" v-model="formData.companyName.value" @beforeinput="event => preventInvalidInput(event, companyNamePattern)" @update:model-value="value => { formData.companyName.value = sanitizeCompanyName(value) }" :isRequired="formData.companyName.required" :label="formData.companyName.label" type="text" :isError="!!formData.companyName.error" :errorMessage="formData.companyName.error" :placeholder="formData.companyName.placeholder ?? ''" />
246
259
  </div>
247
260
  <div class="flex flex-col xl:flex-row gap-2">
248
- <InputText id="companyName" v-model="formData.companyName.value" :isRequired=formData.companyName.required :label=formData.companyName.label type="text" :isError="!!formData.companyName.error" :errorMessage="formData.companyName.error" :placeholder="formData.companyName.placeholder ?? ''" />
249
261
  <InputText id="linkedinCompanyPage" v-model="formData.linkedinCompanyPage.value" :isRequired=formData.linkedinCompanyPage.required :label=formData.linkedinCompanyPage.label type="text" :isError="!!formData.linkedinCompanyPage.error" :errorMessage="formData.linkedinCompanyPage.error" :placeholder="formData.linkedinCompanyPage.placeholder ?? ''" />
250
262
  </div>
251
263
  </fieldset>
@@ -260,22 +272,22 @@ fields.forEach((key) => {
260
272
  </fieldset>
261
273
  <div class="space-y-2">
262
274
  <Checkbox id="privacy" v-model="formData.consentToProcess.value" :label="formData.consentToProcess.label" :isRequired="formData.consentToProcess.required" :isError="!!formData.consentToProcess.error" />
263
- <Checkbox id="promotions" v-model="formData.communicationConsent.value" :label="formData.communicationConsent.label" :isRequired="formData.communicationConsent.required" :isError="!!formData.communicationConsent.error" />
275
+ <Checkbox id="promotions" v-model="formData.communicationConsent.value" :label="formData.communicationConsent.label" :isRequired="formData.communicationConsent.required" :isError="!!formData.communicationConsent.error" />
264
276
  </div>
265
277
  </div>
266
278
  <GlowButton @click="submitContactsForm" title="Leave consultation request" class="self-center lg:self-start" />
267
279
  </form>
268
280
  <div v-show="isAlertVisible" class="border border-action group absolute bottom-19.25 md:fixed z-350 left-1/2 -translate-x-1/2 md:top-40 form-alert flex gap-2 md:gap-8 items-center justify-between w-full h-fit max-w-139 bg-surface-secondary p-4 md:px-6 md:py-2.5 rounded-3xl">
269
- <div v-if="alertKind === 'success'" class="w-8 h-8">
270
- <component :is="SuccessIcon" class="w-8 h-8" />
271
- </div>
272
- <p class="text-sm md:text-lg font-bold text-heading md:max-w-sm ">
273
- {{ alertKind ? ALERT_TEXT[alertKind] : '' }}
274
- </p>
275
- <button @click="alertKind = null" class="group-hover:text-action text-heading transition-colors cursor-pointer">
276
- <component :is="CloseIcon" />
277
- </button>
281
+ <div v-if="alertKind === 'success'" class="w-8 h-8">
282
+ <component :is="SuccessIcon" class="w-8 h-8" />
278
283
  </div>
284
+ <p class="text-sm md:text-lg font-bold text-heading md:max-w-sm ">
285
+ {{ alertKind ? ALERT_TEXT[alertKind] : '' }}
286
+ </p>
287
+ <button @click="alertKind = null" class="group-hover:text-action text-heading transition-colors cursor-pointer">
288
+ <component :is="CloseIcon" />
289
+ </button>
290
+ </div>
279
291
  </template>
280
292
 
281
293
  <style scoped>
@@ -1,9 +1,10 @@
1
1
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
2
- const linkedInRegex = /^https?:\/\/([a-z]{2,3}\.)?linkedin\.com\/.*$/gim
2
+ const linkedInRegex = /^https?:\/\/([a-z]{2,3}\.)?linkedin\.com\/.*$/im
3
3
 
4
4
  export const isCorrectEmailFormat = (email: string | null): boolean => {
5
5
  return !!email && emailRegex.test(email)
6
6
  }
7
- export const isCorrectLinkedInFormat = (linkedIn: string | null) : boolean => {
8
- return !!linkedIn && linkedInRegex.test(linkedIn);
7
+
8
+ export const isCorrectLinkedInFormat = (linkedIn: string | null): boolean => {
9
+ return !!linkedIn && linkedInRegex.test(linkedIn)
9
10
  }
@@ -26,7 +26,6 @@ export interface CrmFormFieldSpec {
26
26
  export type ContactFormFieldName =
27
27
  | 'email'
28
28
  | 'firstName'
29
- | 'lastName'
30
29
  | 'companyName'
31
30
  | 'linkedinCompanyPage'
32
31
  | 'services'
@@ -41,7 +40,6 @@ export type ContactFormDefinition = Record<ContactFormFieldName, CrmFormFieldSpe
41
40
  export interface ContactFormValues {
42
41
  email: string;
43
42
  firstName: string;
44
- lastName: string;
45
43
  companyName: string;
46
44
  linkedinCompanyPage: string;
47
45
  services: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codecavepro/brand",
3
- "version": "3.1.0",
3
+ "version": "3.2.1",
4
4
  "description": "CODECAVE design system — colour, typography and layout tokens as CSS custom properties and as a typed module.",
5
5
  "license": "Unlicense",
6
6
  "type": "module",