- Build complete Next.js CRM for commercial real estate - Add authentication with JWT sessions and role-based access - Add GoHighLevel API integration for contacts, conversations, opportunities - Add AI-powered Control Center with tool calling - Add Setup page with onboarding checklist (/setup) - Add sidebar navigation with Setup menu item - Fix type errors in onboarding API, GHL services, and control center tools - Add Prisma schema with SQLite for local development - Add UI components with clay morphism design system Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { DFY_PRODUCTS } from '@/lib/stripe/dfy-products';
|
|
import { Check, Loader2, Clock, Sparkles } from 'lucide-react';
|
|
|
|
export default function DFYServicesPage() {
|
|
const [loading, setLoading] = useState<string | null>(null);
|
|
|
|
const handleCheckout = async (productId: string) => {
|
|
setLoading(productId);
|
|
try {
|
|
const response = await fetch('/api/v1/stripe/checkout', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ productId }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.checkoutUrl) {
|
|
window.location.href = data.checkoutUrl;
|
|
}
|
|
} catch (error) {
|
|
console.error('Checkout error:', error);
|
|
} finally {
|
|
setLoading(null);
|
|
}
|
|
};
|
|
|
|
const formatPrice = (cents: number) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
}).format(cents / 100);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<Sparkles className="text-primary-600" size={28} />
|
|
<h2 className="text-2xl font-bold text-slate-900">Done-For-You Services</h2>
|
|
</div>
|
|
<p className="text-slate-600">
|
|
Let our experts handle the setup while you focus on closing deals
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{Object.entries(DFY_PRODUCTS).map(([key, product]) => (
|
|
<div
|
|
key={key}
|
|
className="clay-card flex flex-col"
|
|
>
|
|
<div className="flex-1">
|
|
<h3 className="text-xl font-bold text-slate-900">{product.name}</h3>
|
|
<p className="text-slate-600 mt-2 text-sm">{product.description}</p>
|
|
|
|
<div className="mt-4">
|
|
<span className="text-3xl font-bold text-primary-600">
|
|
{formatPrice(product.priceInCents)}
|
|
</span>
|
|
<span className="text-slate-500 ml-2">one-time</span>
|
|
</div>
|
|
|
|
<ul className="mt-4 space-y-2">
|
|
{product.features.map((feature, i) => (
|
|
<li key={i} className="flex items-center text-sm text-slate-600">
|
|
<Check className="w-4 h-4 mr-2 text-success-500 flex-shrink-0" />
|
|
{feature}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
<div className="flex items-center text-xs text-slate-500 mt-4">
|
|
<Clock className="w-3 h-3 mr-1" />
|
|
Delivery: {product.deliveryDays} business days
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => handleCheckout(key)}
|
|
disabled={loading === key}
|
|
className="btn-primary w-full mt-6 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{loading === key ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
Processing...
|
|
</>
|
|
) : (
|
|
'Get Started'
|
|
)}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="clay-card bg-gradient-to-r from-primary-50 to-primary-100/50">
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-slate-900">Need a custom solution?</h3>
|
|
<p className="text-sm text-slate-600 mt-1">
|
|
Contact us for enterprise packages and custom integrations
|
|
</p>
|
|
</div>
|
|
<button className="btn-secondary whitespace-nowrap">
|
|
Contact Sales
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|