keystone-design-bootstrap 1.0.51 → 1.0.53

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-design-bootstrap",
3
- "version": "1.0.51",
3
+ "version": "1.0.53",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import React, { useRef, useState } from 'react';
4
+ import { Form, Button } from '../elements';
5
+ import { DynamicFormFields } from '../components/DynamicFormFields';
6
+ import type { FormDefinition } from '../../types/api/form';
7
+ import { useFormDefinitions } from '../../next/contexts/form-definitions';
8
+
9
+ export interface EmailSignupSectionProps {
10
+ title?: string;
11
+ subtitle?: string;
12
+ buttonText?: string;
13
+ successMessage?: string;
14
+ /** Override the form definition (falls back to context). */
15
+ formDefinition?: FormDefinition | null;
16
+ }
17
+
18
+ export const EmailSignupSection = ({
19
+ title = "Stay in the loop",
20
+ subtitle = "Subscribe to our newsletter for updates, tips, and exclusive offers.",
21
+ buttonText = "Subscribe",
22
+ successMessage = "You're subscribed! Thank you.",
23
+ formDefinition,
24
+ }: EmailSignupSectionProps) => {
25
+ const { marketingListSignupFormDefinition } = useFormDefinitions();
26
+ const resolvedFormDefinition = formDefinition ?? marketingListSignupFormDefinition;
27
+ const [isSubmitting, setIsSubmitting] = useState(false);
28
+ const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
29
+ const [statusMessage, setStatusMessage] = useState('');
30
+ const formRef = useRef<HTMLFormElement>(null);
31
+
32
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
33
+ e.preventDefault();
34
+ setIsSubmitting(true);
35
+ setSubmitStatus('idle');
36
+ setStatusMessage('');
37
+
38
+ const formData = new FormData(e.currentTarget);
39
+ const data: Record<string, string> = { formType: 'marketing_list_signup' };
40
+ formData.forEach((value, key) => {
41
+ if (key.endsWith('_prefix')) return;
42
+ if (typeof value === 'string') data[key] = value;
43
+ });
44
+
45
+ try {
46
+ const response = await fetch('/api/form/', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify(data),
50
+ });
51
+ const result = await response.json();
52
+
53
+ if (result.success) {
54
+ setSubmitStatus('success');
55
+ setStatusMessage(result.message || successMessage);
56
+ formRef.current?.reset();
57
+ setTimeout(() => setSubmitStatus('idle'), 6000);
58
+ } else {
59
+ setSubmitStatus('error');
60
+ setStatusMessage(result.error || 'Something went wrong. Please try again.');
61
+ }
62
+ } catch {
63
+ setSubmitStatus('error');
64
+ setStatusMessage('Network error. Please try again later.');
65
+ }
66
+
67
+ setIsSubmitting(false);
68
+ };
69
+
70
+ if (!resolvedFormDefinition) return null;
71
+
72
+ return (
73
+ <section className="bg-secondary py-16 md:py-20">
74
+ <div className="mx-auto max-w-container px-4 md:px-8">
75
+ <div className="mx-auto max-w-xl text-center">
76
+ <h2 className="font-display text-display-sm font-semibold text-primary md:text-display-md">
77
+ {title}
78
+ </h2>
79
+ {subtitle && (
80
+ <p className="mt-4 font-body text-lg text-tertiary">
81
+ {subtitle}
82
+ </p>
83
+ )}
84
+ <Form
85
+ ref={formRef}
86
+ onSubmit={handleSubmit}
87
+ className="mt-8 flex flex-col gap-6 text-left"
88
+ >
89
+ <DynamicFormFields form={resolvedFormDefinition} />
90
+ {submitStatus === 'success' && (
91
+ <div className="rounded-lg bg-success-50 p-4 text-success-700">
92
+ {statusMessage || successMessage}
93
+ </div>
94
+ )}
95
+ {submitStatus === 'error' && (
96
+ <div className="rounded-lg bg-error-50 p-4 text-error-700">
97
+ {statusMessage}
98
+ </div>
99
+ )}
100
+ <Button
101
+ type="submit"
102
+ color="primary"
103
+ size="xl"
104
+ isDisabled={isSubmitting}
105
+ isLoading={isSubmitting}
106
+ className="w-full"
107
+ >
108
+ {isSubmitting ? 'Subscribing...' : buttonText}
109
+ </Button>
110
+ </Form>
111
+ </div>
112
+ </div>
113
+ </section>
114
+ );
115
+ };
@@ -30,13 +30,15 @@ export function HeaderNavigation({
30
30
  // Timeout ref for delayed dropdown closing
31
31
  const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
32
32
 
33
- // Track scroll position for header animation
33
+ // Track scroll position for header animation; close dropdown on scroll to
34
+ // avoid a stale fixed `top` value creating a gap between the nav and dropdown.
34
35
  React.useEffect(() => {
35
36
  const handleScroll = () => {
36
37
  setIsScrolled(window.scrollY > 10);
38
+ setActiveDropdown(null);
37
39
  };
38
40
 
39
- window.addEventListener('scroll', handleScroll);
41
+ window.addEventListener('scroll', handleScroll, { passive: true });
40
42
  return () => window.removeEventListener('scroll', handleScroll);
41
43
  }, []);
42
44
 
@@ -67,7 +69,8 @@ export function HeaderNavigation({
67
69
  }
68
70
  }, []);
69
71
 
70
- // Open dropdown immediately
72
+ // Open dropdown immediately; clear it when hovering a childless item so the
73
+ // previous dropdown doesn't linger while a different top-level item is active.
71
74
  const handleMouseEnter = useCallback((item: NavItem, e: React.MouseEvent<HTMLDivElement>) => {
72
75
  cancelCloseTimeout();
73
76
  if (item.children && item.children.length > 0) {
@@ -77,6 +80,8 @@ export function HeaderNavigation({
77
80
  setDropdownTop(rect.bottom);
78
81
  }
79
82
  setActiveDropdown(item.label);
83
+ } else {
84
+ setActiveDropdown(null);
80
85
  }
81
86
  }, [cancelCloseTimeout]);
82
87
 
@@ -51,6 +51,8 @@ export function HeaderNavigation({
51
51
  cancelCloseTimeout();
52
52
  if (item.children && item.children.length > 0) {
53
53
  setActiveDropdown(item.label);
54
+ } else {
55
+ setActiveDropdown(null);
54
56
  }
55
57
  }, [cancelCloseTimeout]);
56
58
 
@@ -54,11 +54,14 @@ export function HeaderNavigation({
54
54
  }
55
55
  }, []);
56
56
 
57
- // Open dropdown immediately
57
+ // Open dropdown immediately; clear it when hovering a childless item so the
58
+ // previous dropdown doesn't linger while a different top-level item is active.
58
59
  const handleMouseEnter = useCallback((item: NavItem) => {
59
60
  cancelCloseTimeout();
60
61
  if (item.children && item.children.length > 0) {
61
62
  setActiveDropdown(item.label);
63
+ } else {
64
+ setActiveDropdown(null);
62
65
  }
63
66
  }, [cancelCloseTimeout]);
64
67
 
@@ -178,6 +178,10 @@ export const HomeHeroComponent = createThemedExport('home-hero-component', BaseH
178
178
  // Re-export application form (client component, no theme variants)
179
179
  export { JobApplicationForm } from './job-application-form';
180
180
 
181
+ // Email / newsletter signup section (client component, no theme variants needed)
182
+ export { EmailSignupSection } from './email-signup-section';
183
+ export type { EmailSignupSectionProps } from './email-signup-section';
184
+
181
185
  // Service Menu: packages + treatments; nested specials as badges / modal callouts
182
186
  export { ServiceMenuSection } from './service-menu-section';
183
187
  export type { ServiceMenuSectionProps, PackagePublic } from './service-menu-section';
@@ -6,21 +6,24 @@ import type { FormDefinition } from '../../types/api/form';
6
6
  export type FormDefinitionsContextValue = {
7
7
  leadFormDefinition: FormDefinition | null;
8
8
  jobApplicationFormDefinition: FormDefinition | null;
9
+ marketingListSignupFormDefinition: FormDefinition | null;
9
10
  };
10
11
 
11
12
  const FormDefinitionsContext = createContext<FormDefinitionsContextValue>({
12
13
  leadFormDefinition: null,
13
14
  jobApplicationFormDefinition: null,
15
+ marketingListSignupFormDefinition: null,
14
16
  });
15
17
 
16
18
  export function FormDefinitionsProvider(props: {
17
19
  leadFormDefinition: FormDefinition | null;
18
20
  jobApplicationFormDefinition: FormDefinition | null;
21
+ marketingListSignupFormDefinition: FormDefinition | null;
19
22
  children: React.ReactNode;
20
23
  }) {
21
- const { leadFormDefinition, jobApplicationFormDefinition, children } = props;
24
+ const { leadFormDefinition, jobApplicationFormDefinition, marketingListSignupFormDefinition, children } = props;
22
25
  return (
23
- <FormDefinitionsContext.Provider value={{ leadFormDefinition, jobApplicationFormDefinition }}>
26
+ <FormDefinitionsContext.Provider value={{ leadFormDefinition, jobApplicationFormDefinition, marketingListSignupFormDefinition }}>
24
27
  {children}
25
28
  </FormDefinitionsContext.Provider>
26
29
  );
@@ -111,6 +111,7 @@ export async function KeystoneRootLayout(props: {
111
111
  teamMembersData,
112
112
  leadFormDefinition,
113
113
  jobApplicationFormDefinition,
114
+ marketingListSignupFormDefinition,
114
115
  adsConfig,
115
116
  ] =
116
117
  await Promise.all([
@@ -121,6 +122,7 @@ export async function KeystoneRootLayout(props: {
121
122
  getTeamMembers(),
122
123
  getForm('lead'),
123
124
  getForm('job_application'),
125
+ getForm('marketing_list_signup'),
124
126
  getAdsConfig(),
125
127
  ]);
126
128
 
@@ -162,6 +164,7 @@ export async function KeystoneRootLayout(props: {
162
164
  <FormDefinitionsProvider
163
165
  leadFormDefinition={leadFormDefinition ?? null}
164
166
  jobApplicationFormDefinition={jobApplicationFormDefinition ?? null}
167
+ marketingListSignupFormDefinition={marketingListSignupFormDefinition ?? null}
165
168
  >
166
169
  <HeaderNavigation
167
170
  config={dynamicConfig}
@@ -28,4 +28,4 @@ export interface FormDefinition {
28
28
  updated_at?: string;
29
29
  }
30
30
 
31
- export type FormType = 'lead' | 'job_application';
31
+ export type FormType = 'lead' | 'job_application' | 'marketing_list_signup';