feat: integrate Cal.com popup booking via @calcom/embed-react
All checks were successful
Build & Push / build-and-push (push) Successful in 4m52s

- CalButton component: loads Cal.com embed script, triggers popup on click
- Configured for scheduling.letsbe.solutions (self-hosted)
- Wired into configurator completion step (Book a Call)
- Wired into footer Book a Call button
- Brand colors: #006494 light, #5BA4D9 dark

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 20:57:09 +01:00
parent cc69085320
commit a6882d517a
5 changed files with 89 additions and 39 deletions

31
package-lock.json generated
View File

@@ -7,8 +7,8 @@
"": { "": {
"name": "letsbe-agency", "name": "letsbe-agency",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC",
"dependencies": { "dependencies": {
"@calcom/embed-react": "^1.5.3",
"@payloadcms/db-postgres": "^3.80.0", "@payloadcms/db-postgres": "^3.80.0",
"@payloadcms/next": "^3.80.0", "@payloadcms/next": "^3.80.0",
"@payloadcms/richtext-lexical": "^3.80.0", "@payloadcms/richtext-lexical": "^3.80.0",
@@ -215,6 +215,35 @@
"url": "https://github.com/sponsors/Borewit" "url": "https://github.com/sponsors/Borewit"
} }
}, },
"node_modules/@calcom/embed-core": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@calcom/embed-core/-/embed-core-1.5.3.tgz",
"integrity": "sha512-GeId9gaByJ5EWiPmuvelZOvFWPOTWkcWZr5vGTCbIUTX125oE5yn0n8lDF1MJk5Xj1WO+/dk9jKIE08Ad9ytiQ==",
"license": "SEE LICENSE IN LICENSE"
},
"node_modules/@calcom/embed-react": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@calcom/embed-react/-/embed-react-1.5.3.tgz",
"integrity": "sha512-JCgge04pc8fhdvUmPNVLhW8/lCWK+AAziKecKWWPfv1nn2s+qKP2BwsEAnxhxK9yPOBgE1EIEgmYkrrNB1iajA==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@calcom/embed-core": "1.5.3",
"@calcom/embed-snippet": "1.3.3"
},
"peerDependencies": {
"react": "^18.2.0 || ^19.0.0",
"react-dom": "^18.2.0 || ^19.0.0"
}
},
"node_modules/@calcom/embed-snippet": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@calcom/embed-snippet/-/embed-snippet-1.3.3.tgz",
"integrity": "sha512-pqqKaeLB8R6BvyegcpI9gAyY6Xyx1bKYfWvIGOvIbTpguWyM1BBBVcT9DCeGe8Zw7Ujp5K56ci7isRUrT2Uadg==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@calcom/embed-core": "1.5.3"
}
},
"node_modules/@date-fns/tz": { "node_modules/@date-fns/tz": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",

View File

@@ -10,6 +10,7 @@
"generate:types": "payload generate:types" "generate:types": "payload generate:types"
}, },
"dependencies": { "dependencies": {
"@calcom/embed-react": "^1.5.3",
"@payloadcms/db-postgres": "^3.80.0", "@payloadcms/db-postgres": "^3.80.0",
"@payloadcms/next": "^3.80.0", "@payloadcms/next": "^3.80.0",
"@payloadcms/richtext-lexical": "^3.80.0", "@payloadcms/richtext-lexical": "^3.80.0",

View File

@@ -5,6 +5,7 @@ import { motion } from 'framer-motion';
import { Calendar, Mail } from 'lucide-react'; import { Calendar, Mail } from 'lucide-react';
import AnimatedCheckmark from '@/components/icons/AnimatedCheckmark'; import AnimatedCheckmark from '@/components/icons/AnimatedCheckmark';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import CalButton from '@/components/ui/CalButton';
import type { WizardFormData } from './WizardContainer'; import type { WizardFormData } from './WizardContainer';
// ─── Brief Renderer ─────────────────────────────────────────────────────────── // ─── Brief Renderer ───────────────────────────────────────────────────────────
@@ -70,26 +71,8 @@ function renderBrief(brief: string) {
// ─── Cal.com Embed / Booking ────────────────────────────────────────────────── // ─── Cal.com Embed / Booking ──────────────────────────────────────────────────
function BookingSection() { function BookingSection() {
const calcomUrl = process.env.NEXT_PUBLIC_CALCOM_URL;
if (calcomUrl) {
return ( return (
<div className="rounded-xl overflow-hidden border border-outline-variant/40 bg-surface-high"> <div className="rounded-xl bg-surface-low px-5 py-5 text-center">
<iframe
src={calcomUrl}
width="100%"
height="480"
frameBorder="0"
title="Book a consultation"
className="block"
loading="lazy"
/>
</div>
);
}
return (
<div className="rounded-xl border border-outline-variant/40 bg-surface-low px-5 py-5 text-center">
<div className="flex justify-center mb-3"> <div className="flex justify-center mb-3">
<span className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center"> <span className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
<Calendar size={18} strokeWidth={1.5} className="text-primary-dark" /> <Calendar size={18} strokeWidth={1.5} className="text-primary-dark" />
@@ -97,16 +80,13 @@ function BookingSection() {
</div> </div>
<p className="text-sm font-semibold text-on-surface mb-1">Book a Consultation</p> <p className="text-sm font-semibold text-on-surface mb-1">Book a Consultation</p>
<p className="text-xs text-outline mb-4">30 minutes to discuss your brief with our team</p> <p className="text-xs text-outline mb-4">30 minutes to discuss your brief with our team</p>
<Button <CalButton
variant="primary" className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg text-sm font-medium text-white transition-all hover:-translate-y-px active:translate-y-0"
href="https://cal.com" style={{ background: 'linear-gradient(135deg, #006494, #5BA4D9)' }}
target="_blank"
rel="noopener noreferrer"
size="sm"
arrow
> >
<Calendar size={16} />
Book a Call Book a Call
</Button> </CalButton>
</div> </div>
); );
} }

View File

@@ -1,6 +1,8 @@
import { useTranslations } from 'next-intl' 'use client'
import { useTranslations } from 'next-intl'
import { Link } from '@/i18n/navigation' import { Link } from '@/i18n/navigation'
import CalButton from '@/components/ui/CalButton'
// ── Static link data ───────────────────────────────────────────────────────── // ── Static link data ─────────────────────────────────────────────────────────
@@ -180,18 +182,12 @@ export default function Footer() {
</ul> </ul>
{/* Book a Call CTA */} {/* Book a Call CTA */}
<a <CalButton
href={CAL_LINK} className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full label-md text-white transition-all duration-300 hover:scale-[1.03] hover:shadow-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
target="_blank" style={{ background: 'linear-gradient(135deg, #006494, #5BA4D9)' }}
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full label-md transition-all duration-300 hover:scale-[1.03] hover:shadow-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
style={{
background: 'linear-gradient(135deg, #006494, #5BA4D9)',
color: '#fff',
}}
> >
{tNav('bookCall')} {tNav('bookCall')}
</a> </CalButton>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,44 @@
'use client'
import { getCalApi } from '@calcom/embed-react'
import { useEffect } from 'react'
import { cn } from '@/lib/utils'
const CAL_ORIGIN = 'https://scheduling.letsbe.solutions'
const CAL_LINK = 'matt-ciaccio/letsbe'
const CAL_NAMESPACE = 'letsbe'
interface CalButtonProps {
children: React.ReactNode
className?: string
style?: React.CSSProperties
}
export default function CalButton({ children, className, style }: CalButtonProps) {
useEffect(() => {
;(async () => {
const cal = await getCalApi({ namespace: CAL_NAMESPACE })
cal('init', CAL_NAMESPACE, { origin: CAL_ORIGIN })
cal('ui', {
hideEventTypeDetails: false,
layout: 'month_view',
cssVarsPerTheme: {
light: { 'cal-brand': '#006494' },
dark: { 'cal-brand': '#5BA4D9' },
},
})
})()
}, [])
return (
<button
data-cal-link={CAL_LINK}
data-cal-namespace={CAL_NAMESPACE}
data-cal-config='{"layout":"month_view","useSlotsViewOnSmallScreen":"true"}'
className={cn(className)}
style={style}
>
{children}
</button>
)
}