@indirecttek/essentials-engine 1.1.3 → 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.
@@ -0,0 +1,80 @@
1
+ ---
2
+ import type { SiteConfig } from "../types";
3
+
4
+ export interface Props {
5
+ config: SiteConfig;
6
+ inline?: boolean; // true = embedded calendar, false = popup button
7
+ }
8
+
9
+ const { config, inline = false } = Astro.props;
10
+
11
+ const scheduling = config.integrations?.scheduling;
12
+ const isEnabled = scheduling && scheduling.provider !== "none" && scheduling.url;
13
+ const buttonText = scheduling?.buttonText || "Schedule Now";
14
+
15
+ // Extract Calendly username from URL for inline embed
16
+ const getCalendlyPath = (url: string) => {
17
+ try {
18
+ const urlObj = new URL(url);
19
+ return urlObj.pathname;
20
+ } catch {
21
+ return url.replace("https://calendly.com", "");
22
+ }
23
+ };
24
+ ---
25
+
26
+ {isEnabled && scheduling.provider === "calendly" && (
27
+ inline ? (
28
+ <!-- Inline Calendly Embed -->
29
+ <div class="calendly-inline-widget" data-url={scheduling.url} style="min-width:320px;height:700px;"></div>
30
+ <script is:inline src="https://assets.calendly.com/assets/external/widget.js" async></script>
31
+ ) : (
32
+ <!-- Calendly Popup Button -->
33
+ <button
34
+ type="button"
35
+ onclick={`Calendly.initPopupWidget({url: '${scheduling.url}'});return false;`}
36
+ class="inline-flex items-center justify-center gap-2 bg-[color:var(--color-primary)] text-white px-6 py-3 rounded-lg font-semibold hover:opacity-90 transition-opacity"
37
+ >
38
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
39
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
40
+ </svg>
41
+ {buttonText}
42
+ </button>
43
+ <link href="https://assets.calendly.com/assets/external/widget.css" rel="stylesheet">
44
+ <script is:inline src="https://assets.calendly.com/assets/external/widget.js" async></script>
45
+ )
46
+ )}
47
+
48
+ {isEnabled && scheduling.provider === "acuity" && (
49
+ <!-- Acuity Scheduling Embed -->
50
+ <iframe
51
+ src={scheduling.url}
52
+ title="Schedule Appointment"
53
+ width="100%"
54
+ height="800"
55
+ frameborder="0"
56
+ class="rounded-lg"
57
+ ></iframe>
58
+ )}
59
+
60
+ {isEnabled && scheduling.provider === "square" && (
61
+ <!-- Square Appointments Button -->
62
+ <a
63
+ href={scheduling.url}
64
+ target="_blank"
65
+ rel="noopener noreferrer"
66
+ class="inline-flex items-center justify-center gap-2 bg-[color:var(--color-primary)] text-white px-6 py-3 rounded-lg font-semibold hover:opacity-90 transition-opacity"
67
+ >
68
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
69
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
70
+ </svg>
71
+ {buttonText}
72
+ </a>
73
+ )}
74
+
75
+ {!isEnabled && (
76
+ <!-- Fallback when scheduling not configured -->
77
+ <p class="text-sm text-[color:var(--color-foreground)]/60 italic">
78
+ Online scheduling coming soon. Please call or email to book.
79
+ </p>
80
+ )}
@@ -16,7 +16,20 @@ const { config } = Astro.props;
16
16
  <p class="text-center text-[color:var(--color-foreground)] opacity-80 mb-8 md:mb-12">
17
17
  Get in touch with {config.businessName} today
18
18
  </p>
19
- <form class="space-y-6">
19
+
20
+ <!-- Success Message (hidden by default) -->
21
+ <div id="contact-success" class="hidden bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded-md mb-6">
22
+ <p class="font-medium">Message sent successfully!</p>
23
+ <p class="text-sm">We'll get back to you within 24 hours.</p>
24
+ </div>
25
+
26
+ <!-- Error Message (hidden by default) -->
27
+ <div id="contact-error" class="hidden bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-md mb-6">
28
+ <p class="font-medium">Something went wrong</p>
29
+ <p class="text-sm" id="contact-error-message">Please try again or contact us directly.</p>
30
+ </div>
31
+
32
+ <form id="contact-form" class="space-y-6">
20
33
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
21
34
  <div>
22
35
  <label
@@ -84,9 +97,11 @@ const { config } = Astro.props;
84
97
  </div>
85
98
  <button
86
99
  type="submit"
87
- class="w-full bg-[color:var(--color-primary)] text-[color:var(--color-background)] py-3 md:py-4 rounded-md font-semibold text-lg hover:opacity-90 transition-opacity"
100
+ id="contact-submit"
101
+ class="w-full bg-[color:var(--color-primary)] text-[color:var(--color-background)] py-3 md:py-4 rounded-md font-semibold text-lg hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
88
102
  >
89
- Send Message
103
+ <span id="submit-text">Send Message</span>
104
+ <span id="submit-loading" class="hidden">Sending...</span>
90
105
  </button>
91
106
  </form>
92
107
  <div class="mt-12 text-center space-y-2 text-[color:var(--color-foreground)] opacity-80">
@@ -104,3 +119,70 @@ const { config } = Astro.props;
104
119
  </div>
105
120
  </div>
106
121
  </section>
122
+
123
+ <script>
124
+ const form = document.getElementById('contact-form') as HTMLFormElement;
125
+ const submitBtn = document.getElementById('contact-submit') as HTMLButtonElement;
126
+ const submitText = document.getElementById('submit-text') as HTMLSpanElement;
127
+ const submitLoading = document.getElementById('submit-loading') as HTMLSpanElement;
128
+ const successMsg = document.getElementById('contact-success') as HTMLDivElement;
129
+ const errorMsg = document.getElementById('contact-error') as HTMLDivElement;
130
+ const errorText = document.getElementById('contact-error-message') as HTMLParagraphElement;
131
+
132
+ form?.addEventListener('submit', async (e) => {
133
+ e.preventDefault();
134
+
135
+ // Hide any previous messages
136
+ successMsg.classList.add('hidden');
137
+ errorMsg.classList.add('hidden');
138
+
139
+ // Show loading state
140
+ submitBtn.disabled = true;
141
+ submitText.classList.add('hidden');
142
+ submitLoading.classList.remove('hidden');
143
+
144
+ const formData = new FormData(form);
145
+ const data = {
146
+ name: formData.get('name'),
147
+ email: formData.get('email'),
148
+ phone: formData.get('phone') || undefined,
149
+ message: formData.get('message'),
150
+ };
151
+
152
+ try {
153
+ const response = await fetch('/api/contact', {
154
+ method: 'POST',
155
+ headers: {
156
+ 'Content-Type': 'application/json',
157
+ },
158
+ body: JSON.stringify(data),
159
+ });
160
+
161
+ const result = await response.json();
162
+
163
+ if (result.success) {
164
+ // Show success message
165
+ successMsg.classList.remove('hidden');
166
+ form.reset();
167
+
168
+ // Scroll to success message
169
+ successMsg.scrollIntoView({ behavior: 'smooth', block: 'center' });
170
+ } else {
171
+ // Show error message
172
+ errorText.textContent = result.error || 'Please try again or contact us directly.';
173
+ errorMsg.classList.remove('hidden');
174
+ errorMsg.scrollIntoView({ behavior: 'smooth', block: 'center' });
175
+ }
176
+ } catch (error) {
177
+ // Show generic error
178
+ errorText.textContent = 'Network error. Please check your connection and try again.';
179
+ errorMsg.classList.remove('hidden');
180
+ errorMsg.scrollIntoView({ behavior: 'smooth', block: 'center' });
181
+ } finally {
182
+ // Reset button state
183
+ submitBtn.disabled = false;
184
+ submitText.classList.remove('hidden');
185
+ submitLoading.classList.add('hidden');
186
+ }
187
+ });
188
+ </script>
@@ -0,0 +1,109 @@
1
+ ---
2
+ import type { SiteConfig } from "../types";
3
+
4
+ export interface Props {
5
+ config: SiteConfig;
6
+ amount?: number; // Amount in cents (overrides config)
7
+ description?: string; // Payment description
8
+ buttonStyle?: "primary" | "secondary" | "outline";
9
+ }
10
+
11
+ const { config, amount, description, buttonStyle = "primary" } = Astro.props;
12
+
13
+ const payments = config.integrations?.payments;
14
+ const isEnabled = payments && payments.provider !== "none";
15
+
16
+ // Determine button text based on mode
17
+ const getButtonText = () => {
18
+ if (payments?.buttonText) return payments.buttonText;
19
+ switch (payments?.mode) {
20
+ case "deposit": return `Pay $${((payments.depositAmount || 5000) / 100).toFixed(0)} Deposit`;
21
+ case "full": return "Pay Now";
22
+ case "quote": return "Request Quote";
23
+ default: return "Pay Now";
24
+ }
25
+ };
26
+
27
+ const buttonText = getButtonText();
28
+
29
+ // Button styling
30
+ const buttonClasses = {
31
+ primary: "bg-[color:var(--color-primary)] text-white hover:opacity-90",
32
+ secondary: "bg-[color:var(--color-secondary)] text-white hover:opacity-90",
33
+ outline: "border-2 border-[color:var(--color-primary)] text-[color:var(--color-primary)] hover:bg-[color:var(--color-primary)] hover:text-white",
34
+ };
35
+ ---
36
+
37
+ {isEnabled && payments.provider === "stripe" && (
38
+ <button
39
+ type="button"
40
+ id="stripe-payment-button"
41
+ data-amount={amount || payments.depositAmount || 5000}
42
+ data-description={description || `Payment to ${config.businessName}`}
43
+ class={`inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg font-semibold transition-all disabled:opacity-50 disabled:cursor-not-allowed ${buttonClasses[buttonStyle]}`}
44
+ >
45
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
47
+ </svg>
48
+ <span id="payment-button-text">{buttonText}</span>
49
+ </button>
50
+ )}
51
+
52
+ {isEnabled && payments.provider === "square" && (
53
+ <a
54
+ href="#"
55
+ id="square-payment-link"
56
+ class={`inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg font-semibold transition-all ${buttonClasses[buttonStyle]}`}
57
+ >
58
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
59
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
60
+ </svg>
61
+ {buttonText}
62
+ </a>
63
+ )}
64
+
65
+ {!isEnabled && (
66
+ <!-- Fallback when payments not configured -->
67
+ <p class="text-sm text-[color:var(--color-foreground)]/60 italic">
68
+ Online payments coming soon. Please contact us for payment options.
69
+ </p>
70
+ )}
71
+
72
+ <script>
73
+ const stripeButton = document.getElementById('stripe-payment-button');
74
+
75
+ if (stripeButton) {
76
+ stripeButton.addEventListener('click', async () => {
77
+ const button = stripeButton as HTMLButtonElement;
78
+ const buttonText = document.getElementById('payment-button-text');
79
+ const amount = button.dataset.amount;
80
+ const description = button.dataset.description;
81
+
82
+ // Show loading state
83
+ button.disabled = true;
84
+ if (buttonText) buttonText.textContent = 'Processing...';
85
+
86
+ try {
87
+ const response = await fetch('/api/create-checkout', {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({ amount: parseInt(amount || '5000'), description }),
91
+ });
92
+
93
+ const data = await response.json();
94
+
95
+ if (data.url) {
96
+ // Redirect to Stripe Checkout
97
+ window.location.href = data.url;
98
+ } else {
99
+ throw new Error(data.error || 'Failed to create checkout session');
100
+ }
101
+ } catch (error) {
102
+ console.error('Payment error:', error);
103
+ alert('Unable to process payment. Please try again or contact us directly.');
104
+ button.disabled = false;
105
+ if (buttonText) buttonText.textContent = button.dataset.originalText || 'Pay Now';
106
+ }
107
+ });
108
+ }
109
+ </script>
package/dist/index.ts CHANGED
@@ -9,6 +9,10 @@ export type {
9
9
  ImageSearchHints,
10
10
  SocialLinks,
11
11
  LayoutOptions,
12
+ SchedulingConfig,
13
+ PaymentsConfig,
14
+ EmailConfig,
15
+ IntegrationsConfig,
12
16
  SiteConfig,
13
17
  } from "./types";
14
18
 
@@ -21,3 +25,7 @@ export { default as Hero } from "./components/Hero.astro";
21
25
  export { default as ServicesGrid } from "./components/ServicesGrid.astro";
22
26
  export { default as ContactForm } from "./components/ContactForm.astro";
23
27
  export { default as Footer } from "./components/Footer.astro";
28
+
29
+ // Integration Components
30
+ export { default as CalendlyWidget } from "./components/CalendlyWidget.astro";
31
+ export { default as PaymentButton } from "./components/PaymentButton.astro";
package/dist/types.ts CHANGED
@@ -54,6 +54,31 @@ export interface LayoutOptions {
54
54
  stickyNav?: boolean;
55
55
  }
56
56
 
57
+ // Integration configurations
58
+ export interface SchedulingConfig {
59
+ provider: "calendly" | "acuity" | "square" | "none";
60
+ url?: string; // Calendly/Acuity booking URL
61
+ buttonText?: string; // Custom CTA text, defaults to "Schedule Now"
62
+ }
63
+
64
+ export interface PaymentsConfig {
65
+ provider: "stripe" | "square" | "none";
66
+ mode?: "deposit" | "full" | "quote"; // What kind of payment flow
67
+ depositAmount?: number; // For deposit mode, amount in cents
68
+ buttonText?: string; // Custom CTA text
69
+ }
70
+
71
+ export interface EmailConfig {
72
+ provider: "resend" | "sendgrid" | "none";
73
+ // API keys stored in environment variables, not config
74
+ }
75
+
76
+ export interface IntegrationsConfig {
77
+ scheduling?: SchedulingConfig;
78
+ payments?: PaymentsConfig;
79
+ email?: EmailConfig;
80
+ }
81
+
57
82
  export interface SiteConfig {
58
83
  businessName: string;
59
84
  theme: Theme;
@@ -65,4 +90,5 @@ export interface SiteConfig {
65
90
  imageSearchHints?: ImageSearchHints;
66
91
  socialLinks?: SocialLinks;
67
92
  layoutOptions?: LayoutOptions;
93
+ integrations?: IntegrationsConfig;
68
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indirecttek/essentials-engine",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/types.d.ts",