@indirecttek/essentials-engine 1.1.4 → 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
+ )}
@@ -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.4",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/types.d.ts",