feat: integrate Cal.com popup booking via @calcom/embed-react
All checks were successful
Build & Push / build-and-push (push) Successful in 4m52s
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:
31
package-lock.json
generated
31
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<div className="rounded-xl overflow-hidden border border-outline-variant/40 bg-surface-high">
|
|
||||||
<iframe
|
|
||||||
src={calcomUrl}
|
|
||||||
width="100%"
|
|
||||||
height="480"
|
|
||||||
frameBorder="0"
|
|
||||||
title="Book a consultation"
|
|
||||||
className="block"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-outline-variant/40 bg-surface-low px-5 py-5 text-center">
|
<div className="rounded-xl 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
44
src/components/ui/CalButton.tsx
Normal file
44
src/components/ui/CalButton.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user