Skip to content

Commit 0af2d3e

Browse files
authored
feat: add Cursor workshop checkout flow and confirmation page (#1510)
* feat: add Cursor workshop checkout flow and confirmation page * fix: add padding * refactor: use stripe payment links for simplified set up
1 parent 7af38b0 commit 0af2d3e

File tree

7 files changed

+403
-29
lines changed

7 files changed

+403
-29
lines changed

src/components/workshop/cursor/Hero.tsx

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ import {fadeInUp, scaleIn} from './animations'
55
import {useState, useEffect} from 'react'
66
import './styles.css'
77
import {ArrowCircleDownIcon} from '@heroicons/react/solid'
8-
import type {SignUpFormRef} from './SignUpForm'
98
import Image from 'next/image'
109
import {Button} from '@/ui'
10+
import {Calendar, Clock, MapPin} from 'lucide-react'
11+
12+
export interface SignUpFormRef {
13+
focus: () => void
14+
}
1115

1216
const phrases = [
1317
'Conquer the Complexity of',
@@ -41,9 +45,10 @@ function scrollToSignup(
4145

4246
interface HeroProps {
4347
formRef: React.RefObject<SignUpFormRef>
48+
saleisActive: boolean
4449
}
4550

46-
export default function Hero({formRef}: HeroProps) {
51+
export default function Hero({formRef, saleisActive}: HeroProps) {
4752
const [phraseIndex, setPhraseIndex] = useState(0)
4853

4954
useEffect(() => {
@@ -114,16 +119,34 @@ export default function Hero({formRef}: HeroProps) {
114119
transition={{delay: 0.3, type: 'spring', stiffness: 200}}
115120
className="relative"
116121
>
117-
<Button
118-
asChild
119-
size="lg"
120-
className=" sm:mt-5 dark:bg-white dark:text-black bg-black text-white font-semibold"
121-
>
122-
<Link href="#signup" onClick={(e) => scrollToSignup(e, formRef)}>
123-
Join the Waitlist
124-
{/* <ArrowCircleDownIcon className="group-hover:scale-105 w-8 h-8 transition-all duration-200" /> */}
125-
</Link>
126-
</Button>
122+
<div className="flex flex-col gap-4 justify-center items-center">
123+
<Button
124+
asChild
125+
size="lg"
126+
className=" sm:mt-5 dark:bg-white dark:text-black bg-black text-white font-semibold w-fit"
127+
>
128+
<Link href="#signup" onClick={(e) => scrollToSignup(e, formRef)}>
129+
Register Now
130+
{/* <ArrowCircleDownIcon className="group-hover:scale-105 w-8 h-8 transition-all duration-200" /> */}
131+
</Link>
132+
</Button>
133+
{saleisActive && (
134+
<div className="flex flex-col gap-2 text-sm text-muted-foreground md:flex-row md:gap-6">
135+
<div className="flex items-center gap-1">
136+
<Calendar className="h-4 w-4" />
137+
<span>March 11, 2025</span>
138+
</div>
139+
<div className="flex items-center gap-1">
140+
<Clock className="h-4 w-4" />
141+
<span>9:00 AM - 2:00 PM (PST)</span>
142+
</div>
143+
<div className="flex items-center gap-1">
144+
<MapPin className="h-4 w-4" />
145+
<span>Zoom</span>
146+
</div>
147+
</div>
148+
)}
149+
</div>
127150
</motion.div>
128151
</motion.div>
129152
</section>

src/components/workshop/cursor/SignUpForm.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import {forwardRef, useEffect} from 'react'
44
import {motion} from 'framer-motion'
55
import {fadeInUp} from './animations'
66

7-
export interface SignUpFormRef {
8-
focus: () => void
9-
}
10-
11-
const SignUpForm = forwardRef<SignUpFormRef>((props, ref) => {
7+
const SignUpForm = () => {
128
useEffect(() => {
139
const script = document.createElement('script')
1410
script.src = '//embed.typeform.com/next/embed.js'
@@ -53,8 +49,6 @@ const SignUpForm = forwardRef<SignUpFormRef>((props, ref) => {
5349
</div>
5450
</section>
5551
)
56-
})
57-
58-
SignUpForm.displayName = 'SignUpForm'
52+
}
5953

6054
export default SignUpForm
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {Button} from '@/ui/button'
2+
import Link from 'next/link'
3+
4+
const CheckIcon = () => {
5+
return (
6+
<svg
7+
xmlns="http://www.w3.org/2000/svg"
8+
width="24"
9+
height="24"
10+
viewBox="0 0 24 24"
11+
fill="none"
12+
stroke="currentColor"
13+
strokeWidth="2"
14+
strokeLinecap="round"
15+
strokeLinejoin="round"
16+
className="h-4 w-4 text-primary"
17+
>
18+
<polyline points="20 6 9 17 4 12"></polyline>
19+
</svg>
20+
)
21+
}
22+
23+
const ActiveSale = ({
24+
isPro,
25+
workshopFeatures,
26+
}: {
27+
isPro: boolean
28+
workshopFeatures: string[]
29+
}) => {
30+
const paymentLink = `${
31+
process.env.NEXT_PUBLIC_LIVE_WORKSHOP_STRIPE_PAYMENT_LINK
32+
}${
33+
isPro
34+
? `?prefilled_promo_code=${process.env.NEXT_PUBLIC_LIVE_WORKSHOP_PROMO_CODE}`
35+
: ''
36+
}`
37+
38+
console.log(paymentLink)
39+
return (
40+
<div>
41+
<section
42+
id="pricing"
43+
className="w-full py-12 md:py-24 lg:py-32 bg-muted/50"
44+
>
45+
<div className="container px-4 md:px-6">
46+
<div className="mx-auto flex max-w-[58rem] flex-col items-center justify-center gap-4 text-center">
47+
<h2 className="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">
48+
Workshop Pricing
49+
</h2>
50+
<p className="max-w-[85%] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
51+
Streamline your development cycle with Cursor
52+
</p>
53+
</div>
54+
<div className="mx-auto max-w-lg py-12">
55+
<div className="bg-white dark:bg-gray-800 border-2 border-gray-200 dark:border-gray-800 rounded-lg text-card-foreground shadow-sm">
56+
<div className="flex flex-col pt-6 pb-4 px-6 space-y-2 text-center">
57+
<h3 className="text-2xl font-bold text-balance">
58+
Become More Productive with Cursor
59+
</h3>
60+
<div className="space-y-1">
61+
{isPro ? (
62+
<div className="flex items-center justify-center gap-4">
63+
<p className="text-5xl font-bold">$119</p>
64+
<div>
65+
<p className="text-sm font-semibold text-yellow-500">
66+
SAVE 20%
67+
</p>
68+
<p className="text-2xl text-muted-foreground line-through opacity-70">
69+
$149
70+
</p>
71+
</div>
72+
</div>
73+
) : (
74+
<p className="text-5xl font-bold">$149</p>
75+
)}
76+
</div>
77+
</div>
78+
<div className="p-6 pt-0 grid gap-4">
79+
<ul className="flex flex-col gap-2 w-fit mx-auto">
80+
{workshopFeatures.map((feature) => (
81+
<li className="flex items-center gap-2">
82+
<CheckIcon />
83+
<span>{feature}</span>
84+
</li>
85+
))}
86+
</ul>
87+
<a
88+
href={paymentLink}
89+
target="_blank"
90+
rel="noopener noreferrer"
91+
className="relative inline-flex items-center justify-center rounded-md text-base transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background h-10 py-2 px-4 w-full bg-blue-500 text-white font-semibold"
92+
>
93+
Register Now
94+
</a>
95+
<p className="text-xs text-center text-muted-foreground">
96+
Limited spots available. Secure yours today!
97+
</p>
98+
</div>
99+
</div>
100+
</div>
101+
</div>
102+
</section>
103+
</div>
104+
)
105+
}
106+
107+
export default ActiveSale
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {forwardRef} from 'react'
2+
3+
export interface SignUpFormRef {
4+
focus: () => void
5+
}
6+
7+
interface CtaSectionProps {
8+
ActiveSaleUi: React.ReactNode
9+
SaleClosedUi: React.ReactNode
10+
saleisActive: boolean
11+
}
12+
13+
const CtaSection = forwardRef<SignUpFormRef, CtaSectionProps>(
14+
({ActiveSaleUi, SaleClosedUi, saleisActive}, ref) => {
15+
return (
16+
<div id="signup">
17+
{saleisActive === true ? ActiveSaleUi : SaleClosedUi}
18+
</div>
19+
)
20+
},
21+
)
22+
23+
CtaSection.displayName = 'CtaSection'
24+
25+
export default CtaSection

src/hooks/use-commerce-machine.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ const extractPricesFromPricingData = (pricingData: PricingData): Prices => {
2222
interval_count: 3,
2323
})
2424

25-
if (!annualPrice?.stripe_price_id)
26-
throw new Error('no annual price to load 😭')
25+
// if (!annualPrice?.stripe_price_id)
26+
// throw new Error('no annual price to load 😭')
2727

2828
return pickBy({annualPrice, quarterlyPrice, monthlyPrice})
2929
}

src/pages/workshop/cursor/index.tsx

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,54 @@ import Features from '@/components/workshop/cursor/Features'
55
import WorkshopStructure from '@/components/workshop/cursor/WorkshopStructure'
66
import Instructor from '@/components/workshop/cursor/Instructor'
77
import SignUpForm from '@/components/workshop/cursor/SignUpForm'
8-
import type {SignUpFormRef} from '@/components/workshop/cursor/SignUpForm'
9-
import type {NextPage} from 'next'
8+
import type {SignUpFormRef} from '@/components/workshop/cursor/Hero'
9+
import type {GetServerSideProps, NextPage} from 'next'
1010
import {useRef} from 'react'
1111
import {NextSeo} from 'next-seo'
12+
import {getLastChargeForActiveSubscription} from '@/lib/subscriptions'
13+
import ActiveSale from '@/components/workshop/cursor/active-sale'
1214

13-
const WorkshopPage = ({getLayout}: {getLayout: any}) => {
15+
import CtaSection from '@/components/workshop/cursor/cta-section'
16+
import {useViewer} from '@/context/viewer-context'
17+
import {Scale} from 'lucide-react'
18+
19+
const WorkshopPage = () => {
1420
const formRef = useRef<SignUpFormRef>(null)
21+
const {viewer} = useViewer()
22+
const isPro = viewer?.is_pro
23+
24+
const saleisActive = true
25+
26+
const LIVE_WORKSHOP_FEATURES = [
27+
'Live Q&A with John Lindquist',
28+
'Learn to Prompt for developers',
29+
'Effective .cursorrules used',
30+
'Build context for Agents',
31+
'Full day of intensive training',
32+
'Hour long break for lunch',
33+
'Access to the workshop recording',
34+
]
1535

1636
return (
1737
<main className="min-h-screen relative bg-white dark:bg-gray-900">
1838
<div className="relative">
1939
<div className="relative">
20-
<Hero formRef={formRef} />
40+
<Hero formRef={formRef} saleisActive={saleisActive} />
2141
<Features />
2242
</div>
2343
{/* <WorkshopStructure /> */}
24-
<div className="relative">
25-
<SignUpForm ref={formRef} />
26-
</div>
44+
45+
<CtaSection
46+
saleisActive={saleisActive}
47+
ref={formRef}
48+
SaleClosedUi={<SignUpForm />}
49+
ActiveSaleUi={
50+
<ActiveSale
51+
isPro={isPro}
52+
workshopFeatures={LIVE_WORKSHOP_FEATURES}
53+
/>
54+
}
55+
/>
2756
<div className="relative">
2857
<Instructor />
2958
</div>
@@ -54,4 +83,23 @@ WorkshopPage.getLayout = (Page: any, pageProps: any) => {
5483
)
5584
}
5685

86+
export const getServerSideProps: GetServerSideProps = async (ctx) => {
87+
const ehUser = JSON.parse(ctx.req.cookies.eh_user || '{}')
88+
const authToken = ctx.req.cookies['eh_token_2020_11_22']
89+
console.log('ehUser', ehUser)
90+
91+
if (!authToken) return {props: {}}
92+
93+
const lastCharge = await getLastChargeForActiveSubscription(
94+
ehUser.email,
95+
authToken,
96+
)
97+
98+
return {
99+
props: {
100+
lastCharge: lastCharge || null,
101+
},
102+
}
103+
}
104+
57105
export default WorkshopPage

0 commit comments

Comments
 (0)