@codaijs/keel 0.1.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/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/cli.test.js +173 -0
- package/dist/__tests__/cli.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.js +86 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/sail-installer.test.d.ts +2 -0
- package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
- package/dist/__tests__/sail-installer.test.js +158 -0
- package/dist/__tests__/sail-installer.test.js.map +1 -0
- package/dist/create-runner.d.ts +11 -0
- package/dist/create-runner.d.ts.map +1 -0
- package/dist/create-runner.js +63 -0
- package/dist/create-runner.js.map +1 -0
- package/dist/create.d.ts +10 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +15 -0
- package/dist/create.js.map +1 -0
- package/dist/manage.d.ts +24 -0
- package/dist/manage.d.ts.map +1 -0
- package/dist/manage.js +1461 -0
- package/dist/manage.js.map +1 -0
- package/dist/prompts.d.ts +36 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +208 -0
- package/dist/prompts.js.map +1 -0
- package/dist/sail-installer.d.ts +37 -0
- package/dist/sail-installer.d.ts.map +1 -0
- package/dist/sail-installer.js +935 -0
- package/dist/sail-installer.js.map +1 -0
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +297 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +57 -0
- package/sails/_template/addon.json +20 -0
- package/sails/_template/install.ts +402 -0
- package/sails/admin-dashboard/README.md +117 -0
- package/sails/admin-dashboard/addon.json +28 -0
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
- package/sails/admin-dashboard/install.ts +305 -0
- package/sails/analytics/README.md +178 -0
- package/sails/analytics/addon.json +27 -0
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
- package/sails/analytics/install.ts +297 -0
- package/sails/file-uploads/README.md +191 -0
- package/sails/file-uploads/addon.json +30 -0
- package/sails/file-uploads/files/backend/routes/files.ts +198 -0
- package/sails/file-uploads/files/backend/schema/files.ts +36 -0
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
- package/sails/file-uploads/install.ts +466 -0
- package/sails/gdpr/README.md +174 -0
- package/sails/gdpr/addon.json +27 -0
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
- package/sails/gdpr/install.ts +756 -0
- package/sails/google-oauth/README.md +121 -0
- package/sails/google-oauth/addon.json +22 -0
- package/sails/google-oauth/files/GoogleButton.tsx +50 -0
- package/sails/google-oauth/install.ts +252 -0
- package/sails/i18n/README.md +193 -0
- package/sails/i18n/addon.json +30 -0
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
- package/sails/i18n/files/frontend/locales/de/common.json +44 -0
- package/sails/i18n/files/frontend/locales/en/common.json +44 -0
- package/sails/i18n/install.ts +407 -0
- package/sails/push-notifications/README.md +163 -0
- package/sails/push-notifications/addon.json +31 -0
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
- package/sails/push-notifications/install.ts +384 -0
- package/sails/r2-storage/README.md +101 -0
- package/sails/r2-storage/addon.json +29 -0
- package/sails/r2-storage/files/backend/services/storage.ts +71 -0
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
- package/sails/r2-storage/install.ts +412 -0
- package/sails/rate-limiting/README.md +145 -0
- package/sails/rate-limiting/addon.json +20 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
- package/sails/rate-limiting/install.ts +300 -0
- package/sails/registry.json +107 -0
- package/sails/stripe/README.md +214 -0
- package/sails/stripe/addon.json +24 -0
- package/sails/stripe/files/backend/routes/stripe.ts +154 -0
- package/sails/stripe/files/backend/schema/stripe.ts +74 -0
- package/sails/stripe/files/backend/services/stripe.ts +224 -0
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
- package/sails/stripe/install.ts +378 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { Link, useSearchParams } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
interface CheckoutPageProps {
|
|
5
|
+
status: "success" | "cancel";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Post-checkout page shown after Stripe redirects back to the app.
|
|
10
|
+
*
|
|
11
|
+
* Rendered for both success and cancellation outcomes.
|
|
12
|
+
*/
|
|
13
|
+
export function CheckoutPage({ status }: CheckoutPageProps) {
|
|
14
|
+
const [searchParams] = useSearchParams();
|
|
15
|
+
const sessionId = searchParams.get("session_id");
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (status === "success" && sessionId) {
|
|
19
|
+
// Optionally verify the session on the server or trigger analytics
|
|
20
|
+
console.log("Checkout completed:", sessionId);
|
|
21
|
+
}
|
|
22
|
+
}, [status, sessionId]);
|
|
23
|
+
|
|
24
|
+
if (status === "success") {
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex min-h-[60vh] items-center justify-center px-4">
|
|
27
|
+
<div className="max-w-md text-center">
|
|
28
|
+
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
|
|
29
|
+
<svg
|
|
30
|
+
className="h-8 w-8 text-green-600 dark:text-green-400"
|
|
31
|
+
fill="none"
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
stroke="currentColor"
|
|
34
|
+
strokeWidth={2}
|
|
35
|
+
>
|
|
36
|
+
<path
|
|
37
|
+
strokeLinecap="round"
|
|
38
|
+
strokeLinejoin="round"
|
|
39
|
+
d="M5 13l4 4L19 7"
|
|
40
|
+
/>
|
|
41
|
+
</svg>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
45
|
+
Payment successful!
|
|
46
|
+
</h1>
|
|
47
|
+
<p className="mt-3 text-gray-500 dark:text-gray-400">
|
|
48
|
+
Thank you for subscribing. Your account has been upgraded and all
|
|
49
|
+
premium features are now available.
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:justify-center">
|
|
53
|
+
<Link
|
|
54
|
+
to="/dashboard"
|
|
55
|
+
className="inline-flex items-center justify-center rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-blue-700"
|
|
56
|
+
>
|
|
57
|
+
Go to Dashboard
|
|
58
|
+
</Link>
|
|
59
|
+
<Link
|
|
60
|
+
to="/settings"
|
|
61
|
+
className="inline-flex items-center justify-center rounded-lg border border-gray-300 px-5 py-2.5 text-sm font-semibold text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
62
|
+
>
|
|
63
|
+
Manage Subscription
|
|
64
|
+
</Link>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Cancellation
|
|
72
|
+
return (
|
|
73
|
+
<div className="flex min-h-[60vh] items-center justify-center px-4">
|
|
74
|
+
<div className="max-w-md text-center">
|
|
75
|
+
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-900/30">
|
|
76
|
+
<svg
|
|
77
|
+
className="h-8 w-8 text-yellow-600 dark:text-yellow-400"
|
|
78
|
+
fill="none"
|
|
79
|
+
viewBox="0 0 24 24"
|
|
80
|
+
stroke="currentColor"
|
|
81
|
+
strokeWidth={2}
|
|
82
|
+
>
|
|
83
|
+
<path
|
|
84
|
+
strokeLinecap="round"
|
|
85
|
+
strokeLinejoin="round"
|
|
86
|
+
d="M6 18L18 6M6 6l12 12"
|
|
87
|
+
/>
|
|
88
|
+
</svg>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
92
|
+
Checkout cancelled
|
|
93
|
+
</h1>
|
|
94
|
+
<p className="mt-3 text-gray-500 dark:text-gray-400">
|
|
95
|
+
No worries! Your account has not been charged. You can subscribe
|
|
96
|
+
anytime from the pricing page.
|
|
97
|
+
</p>
|
|
98
|
+
|
|
99
|
+
<div className="mt-8 flex flex-col gap-3 sm:flex-row sm:justify-center">
|
|
100
|
+
<Link
|
|
101
|
+
to="/pricing"
|
|
102
|
+
className="inline-flex items-center justify-center rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-blue-700"
|
|
103
|
+
>
|
|
104
|
+
Back to Pricing
|
|
105
|
+
</Link>
|
|
106
|
+
<Link
|
|
107
|
+
to="/dashboard"
|
|
108
|
+
className="inline-flex items-center justify-center rounded-lg border border-gray-300 px-5 py-2.5 text-sm font-semibold text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
109
|
+
>
|
|
110
|
+
Go to Dashboard
|
|
111
|
+
</Link>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { useSubscription } from "@/hooks/useSubscription";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pricing plans configuration.
|
|
7
|
+
*
|
|
8
|
+
* Replace the priceId values with your actual Stripe Price IDs from the
|
|
9
|
+
* Stripe Dashboard (https://dashboard.stripe.com/test/products).
|
|
10
|
+
*/
|
|
11
|
+
const PLANS = [
|
|
12
|
+
{
|
|
13
|
+
name: "Free",
|
|
14
|
+
description: "Get started with the basics",
|
|
15
|
+
price: "$0",
|
|
16
|
+
interval: "forever",
|
|
17
|
+
priceId: null,
|
|
18
|
+
features: [
|
|
19
|
+
"Up to 3 projects",
|
|
20
|
+
"Basic analytics",
|
|
21
|
+
"Community support",
|
|
22
|
+
"1 GB storage",
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "Pro",
|
|
27
|
+
description: "Everything you need to grow",
|
|
28
|
+
price: "$19",
|
|
29
|
+
interval: "month",
|
|
30
|
+
priceId: "price_REPLACE_WITH_PRO_PRICE_ID",
|
|
31
|
+
popular: true,
|
|
32
|
+
features: [
|
|
33
|
+
"Unlimited projects",
|
|
34
|
+
"Advanced analytics",
|
|
35
|
+
"Priority support",
|
|
36
|
+
"50 GB storage",
|
|
37
|
+
"Custom domain",
|
|
38
|
+
"Team collaboration",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "Enterprise",
|
|
43
|
+
description: "For large teams and organizations",
|
|
44
|
+
price: "$49",
|
|
45
|
+
interval: "month",
|
|
46
|
+
priceId: "price_REPLACE_WITH_ENTERPRISE_PRICE_ID",
|
|
47
|
+
features: [
|
|
48
|
+
"Everything in Pro",
|
|
49
|
+
"Unlimited storage",
|
|
50
|
+
"SSO / SAML",
|
|
51
|
+
"Dedicated support",
|
|
52
|
+
"SLA guarantee",
|
|
53
|
+
"Custom integrations",
|
|
54
|
+
"Audit logs",
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export function PricingPage() {
|
|
60
|
+
const navigate = useNavigate();
|
|
61
|
+
const { subscription, isLoading: subLoading } = useSubscription();
|
|
62
|
+
const [loadingPriceId, setLoadingPriceId] = useState<string | null>(null);
|
|
63
|
+
|
|
64
|
+
const handleSubscribe = async (priceId: string | null) => {
|
|
65
|
+
if (!priceId) return;
|
|
66
|
+
|
|
67
|
+
setLoadingPriceId(priceId);
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch("/api/stripe/create-checkout-session", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
credentials: "include",
|
|
73
|
+
body: JSON.stringify({ priceId }),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
|
|
78
|
+
if (data.url) {
|
|
79
|
+
window.location.href = data.url;
|
|
80
|
+
} else {
|
|
81
|
+
console.error("No checkout URL returned");
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error("Failed to create checkout session:", error);
|
|
85
|
+
} finally {
|
|
86
|
+
setLoadingPriceId(null);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleManage = async () => {
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch("/api/stripe/create-portal-session", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
credentials: "include",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
|
|
100
|
+
if (data.url) {
|
|
101
|
+
window.location.href = data.url;
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error("Failed to create portal session:", error);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
|
110
|
+
{/* Header */}
|
|
111
|
+
<div className="text-center">
|
|
112
|
+
<h1 className="text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-5xl">
|
|
113
|
+
Simple, transparent pricing
|
|
114
|
+
</h1>
|
|
115
|
+
<p className="mt-4 text-lg text-gray-500 dark:text-gray-400">
|
|
116
|
+
Choose the plan that works best for you. Upgrade or downgrade anytime.
|
|
117
|
+
</p>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Plan Cards */}
|
|
121
|
+
<div className="mt-16 grid gap-8 lg:grid-cols-3">
|
|
122
|
+
{PLANS.map((plan) => {
|
|
123
|
+
const isCurrentPlan =
|
|
124
|
+
subscription?.priceId === plan.priceId && subscription?.status === "active";
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
key={plan.name}
|
|
129
|
+
className={`relative flex flex-col rounded-2xl border p-8 shadow-sm ${
|
|
130
|
+
plan.popular
|
|
131
|
+
? "border-blue-500 ring-2 ring-blue-500"
|
|
132
|
+
: "border-gray-200 dark:border-gray-700"
|
|
133
|
+
}`}
|
|
134
|
+
>
|
|
135
|
+
{plan.popular && (
|
|
136
|
+
<span className="absolute -top-3 left-1/2 -translate-x-1/2 rounded-full bg-blue-500 px-4 py-1 text-xs font-semibold text-white">
|
|
137
|
+
Most Popular
|
|
138
|
+
</span>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
142
|
+
{plan.name}
|
|
143
|
+
</h2>
|
|
144
|
+
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
145
|
+
{plan.description}
|
|
146
|
+
</p>
|
|
147
|
+
|
|
148
|
+
<div className="mt-6 flex items-baseline gap-1">
|
|
149
|
+
<span className="text-4xl font-bold tracking-tight text-gray-900 dark:text-white">
|
|
150
|
+
{plan.price}
|
|
151
|
+
</span>
|
|
152
|
+
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
153
|
+
/{plan.interval}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<ul className="mt-8 flex-1 space-y-3">
|
|
158
|
+
{plan.features.map((feature) => (
|
|
159
|
+
<li key={feature} className="flex items-start gap-2">
|
|
160
|
+
<svg
|
|
161
|
+
className="mt-0.5 h-5 w-5 flex-shrink-0 text-green-500"
|
|
162
|
+
fill="none"
|
|
163
|
+
viewBox="0 0 24 24"
|
|
164
|
+
stroke="currentColor"
|
|
165
|
+
strokeWidth={2}
|
|
166
|
+
>
|
|
167
|
+
<path
|
|
168
|
+
strokeLinecap="round"
|
|
169
|
+
strokeLinejoin="round"
|
|
170
|
+
d="M5 13l4 4L19 7"
|
|
171
|
+
/>
|
|
172
|
+
</svg>
|
|
173
|
+
<span className="text-sm text-gray-700 dark:text-gray-300">
|
|
174
|
+
{feature}
|
|
175
|
+
</span>
|
|
176
|
+
</li>
|
|
177
|
+
))}
|
|
178
|
+
</ul>
|
|
179
|
+
|
|
180
|
+
<div className="mt-8">
|
|
181
|
+
{isCurrentPlan ? (
|
|
182
|
+
<button
|
|
183
|
+
onClick={handleManage}
|
|
184
|
+
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-semibold text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
185
|
+
>
|
|
186
|
+
Manage Subscription
|
|
187
|
+
</button>
|
|
188
|
+
) : plan.priceId ? (
|
|
189
|
+
<button
|
|
190
|
+
onClick={() => handleSubscribe(plan.priceId)}
|
|
191
|
+
disabled={loadingPriceId === plan.priceId}
|
|
192
|
+
className={`w-full rounded-lg px-4 py-2.5 text-sm font-semibold text-white transition-colors ${
|
|
193
|
+
plan.popular
|
|
194
|
+
? "bg-blue-600 hover:bg-blue-700"
|
|
195
|
+
: "bg-gray-900 hover:bg-gray-800 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200"
|
|
196
|
+
} disabled:cursor-not-allowed disabled:opacity-50`}
|
|
197
|
+
>
|
|
198
|
+
{loadingPriceId === plan.priceId
|
|
199
|
+
? "Redirecting..."
|
|
200
|
+
: "Subscribe"}
|
|
201
|
+
</button>
|
|
202
|
+
) : (
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => navigate("/signup")}
|
|
205
|
+
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-semibold text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
206
|
+
>
|
|
207
|
+
Get Started
|
|
208
|
+
</button>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
})}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* FAQ or trust badges can go here */}
|
|
217
|
+
<div className="mt-16 text-center">
|
|
218
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
219
|
+
All plans include a 14-day free trial. No credit card required to start.
|
|
220
|
+
<br />
|
|
221
|
+
Secure payment processing by Stripe.
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|