@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.
- package/dist/components/CalendlyWidget.astro +80 -0
- package/dist/components/PaymentButton.astro +109 -0
- package/dist/index.ts +8 -0
- package/dist/types.ts +26 -0
- package/package.json +1 -1
|
@@ -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
|
}
|