computer-scienceangular-roadmapbackend-roadmapblockchain-roadmapdba-roadmapdeveloper-roadmapdevops-roadmapfrontend-roadmapgo-roadmaphactoberfestjava-roadmapjavascript-roadmapnodejs-roadmappython-roadmapqa-roadmapreact-roadmaproadmapstudy-planvue-roadmapweb3-roadmap
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
275 lines
9.0 KiB
275 lines
9.0 KiB
import { useMutation, useQuery } from '@tanstack/react-query'; |
|
import { ArrowRightIcon, Play } from 'lucide-react'; |
|
import { useEffect, useState } from 'react'; |
|
import { cn } from '../../lib/classname'; |
|
import { |
|
COURSE_PURCHASE_PARAM, |
|
COURSE_PURCHASE_SUCCESS_PARAM, |
|
isLoggedIn, |
|
} from '../../lib/jwt'; |
|
import { coursePriceOptions } from '../../queries/billing'; |
|
import { courseProgressOptions } from '../../queries/course-progress'; |
|
import { queryClient } from '../../stores/query-client'; |
|
import { CourseLoginPopup } from '../AuthenticationFlow/CourseLoginPopup'; |
|
import { useToast } from '../../hooks/use-toast'; |
|
import { httpPost } from '../../lib/query-http'; |
|
import { deleteUrlParam, getUrlParams } from '../../lib/browser'; |
|
import { VideoModal } from '../VideoModal'; |
|
import { showLoginPopup } from '../../lib/popup'; |
|
|
|
export const SQL_COURSE_SLUG = 'sql'; |
|
|
|
type CreateCheckoutSessionBody = { |
|
courseId: string; |
|
success?: string; |
|
cancel?: string; |
|
}; |
|
|
|
type CreateCheckoutSessionResponse = { |
|
checkoutUrl: string; |
|
}; |
|
|
|
type BuyButtonProps = { |
|
variant?: 'main' | 'floating' | 'top-nav'; |
|
}; |
|
|
|
export function BuyButton(props: BuyButtonProps) { |
|
const { variant = 'main' } = props; |
|
|
|
const [isLoginPopupOpen, setIsLoginPopupOpen] = useState(false); |
|
const [isVideoModalOpen, setIsVideoModalOpen] = useState(false); |
|
const toast = useToast(); |
|
|
|
const { data: coursePricing, isLoading: isLoadingCourse } = useQuery( |
|
coursePriceOptions({ courseSlug: SQL_COURSE_SLUG }), |
|
queryClient, |
|
); |
|
|
|
const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery( |
|
courseProgressOptions(SQL_COURSE_SLUG), |
|
queryClient, |
|
); |
|
|
|
const { |
|
mutate: createCheckoutSession, |
|
isPending: isCreatingCheckoutSession, |
|
} = useMutation( |
|
{ |
|
mutationFn: (body: CreateCheckoutSessionBody) => { |
|
return httpPost<CreateCheckoutSessionResponse>( |
|
'/v1-create-checkout-session', |
|
body, |
|
); |
|
}, |
|
onMutate: () => { |
|
toast.loading('Creating checkout session...'); |
|
}, |
|
onSuccess: (data) => { |
|
if (!window.gtag) { |
|
window.location.href = data.checkoutUrl; |
|
return; |
|
} |
|
|
|
window?.fireEvent({ |
|
action: `${SQL_COURSE_SLUG}_begin_checkout`, |
|
category: 'course', |
|
label: `${SQL_COURSE_SLUG} Course Checkout Started`, |
|
callback: () => { |
|
window.location.href = data.checkoutUrl; |
|
}, |
|
}); |
|
|
|
// Hacky way to make sure that we redirect in case |
|
// GA was blocked or not able to redirect the user. |
|
setTimeout(() => { |
|
window.location.href = data.checkoutUrl; |
|
}, 3000); |
|
}, |
|
onError: (error) => { |
|
console.error(error); |
|
toast.error(error?.message || 'Failed to create checkout session'); |
|
}, |
|
}, |
|
queryClient, |
|
); |
|
|
|
useEffect(() => { |
|
const urlParams = getUrlParams(); |
|
const shouldTriggerPurchase = urlParams[COURSE_PURCHASE_PARAM] === '1'; |
|
if (shouldTriggerPurchase) { |
|
deleteUrlParam(COURSE_PURCHASE_PARAM); |
|
initPurchase(); |
|
} |
|
}, []); |
|
|
|
useEffect(() => { |
|
const urlParams = getUrlParams(); |
|
const param = urlParams?.[COURSE_PURCHASE_SUCCESS_PARAM]; |
|
if (!param) { |
|
return; |
|
} |
|
|
|
const success = param === '1'; |
|
|
|
if (success) { |
|
window?.fireEvent({ |
|
action: `${SQL_COURSE_SLUG}_purchase_complete`, |
|
category: 'course', |
|
label: `${SQL_COURSE_SLUG} Course Purchase Completed`, |
|
}); |
|
} else { |
|
window?.fireEvent({ |
|
action: `${SQL_COURSE_SLUG}_purchase_canceled`, |
|
category: 'course', |
|
label: `${SQL_COURSE_SLUG} Course Purchase Canceled`, |
|
}); |
|
} |
|
|
|
deleteUrlParam(COURSE_PURCHASE_SUCCESS_PARAM); |
|
}, []); |
|
|
|
const isLoadingPricing = |
|
isLoadingCourse || |
|
!coursePricing || |
|
isLoadingCourseProgress || |
|
isCreatingCheckoutSession; |
|
const isAlreadyEnrolled = !!courseProgress?.enrolledAt; |
|
|
|
function initPurchase() { |
|
if (!isLoggedIn()) { |
|
setIsLoginPopupOpen(true); |
|
return; |
|
} |
|
|
|
createCheckoutSession({ |
|
courseId: SQL_COURSE_SLUG, |
|
success: `/courses/${SQL_COURSE_SLUG}?${COURSE_PURCHASE_SUCCESS_PARAM}=1`, |
|
cancel: `/courses/${SQL_COURSE_SLUG}?${COURSE_PURCHASE_SUCCESS_PARAM}=0`, |
|
}); |
|
} |
|
|
|
function onBuyClick() { |
|
if (!isLoggedIn()) { |
|
setIsLoginPopupOpen(true); |
|
return; |
|
} |
|
|
|
const hasEnrolled = !!courseProgress?.enrolledAt; |
|
if (hasEnrolled) { |
|
window.location.href = `${import.meta.env.PUBLIC_COURSE_APP_URL}/${SQL_COURSE_SLUG}`; |
|
return; |
|
} |
|
|
|
initPurchase(); |
|
} |
|
|
|
const courseLoginPopup = isLoginPopupOpen && ( |
|
<CourseLoginPopup onClose={() => setIsLoginPopupOpen(false)} /> |
|
); |
|
|
|
if (variant === 'main') { |
|
return ( |
|
<div className="relative flex w-full flex-col items-center gap-2 md:w-auto"> |
|
{courseLoginPopup} |
|
{isVideoModalOpen && ( |
|
<VideoModal |
|
videoId="6S1CcF-ngeQ" |
|
onClose={() => setIsVideoModalOpen(false)} |
|
/> |
|
)} |
|
<button |
|
onClick={onBuyClick} |
|
disabled={isLoadingPricing} |
|
className={cn( |
|
'group relative inline-flex w-full min-w-[235px] items-center justify-center overflow-hidden rounded-xl bg-gradient-to-r from-yellow-500 to-yellow-300 px-8 py-3 text-base font-semibold text-black transition-all duration-300 ease-out hover:scale-[1.02] hover:shadow-[0_0_30px_rgba(234,179,8,0.4)] focus:outline-none active:ring-0 md:w-auto md:rounded-full md:text-lg', |
|
(isLoadingPricing || isCreatingCheckoutSession) && |
|
'striped-loader-yellow pointer-events-none scale-105 bg-yellow-500', |
|
)} |
|
> |
|
{isLoadingPricing ? ( |
|
<span className="relative flex items-center gap-2"> </span> |
|
) : isAlreadyEnrolled ? ( |
|
<span className="relative flex items-center gap-2"> |
|
Start Learning |
|
</span> |
|
) : ( |
|
<span className="relative flex items-center gap-2"> |
|
Buy now for{' '} |
|
{coursePricing?.isEligibleForDiscount ? ( |
|
<span className="flex items-center gap-2"> |
|
<span className="hidden text-base line-through opacity-75 md:inline"> |
|
${coursePricing?.fullPrice} |
|
</span> |
|
<span className="text-base md:text-xl"> |
|
${coursePricing?.regionalPrice} |
|
</span> |
|
</span> |
|
) : ( |
|
<span>${coursePricing?.regionalPrice}</span> |
|
)} |
|
<ArrowRightIcon className="h-5 w-5 transition-transform duration-300 ease-out group-hover:translate-x-1" /> |
|
</span> |
|
)} |
|
</button> |
|
|
|
{!isLoadingPricing && ( |
|
<span className="absolute top-full z-50 flex w-[300px] translate-y-3 flex-row items-center justify-center text-sm text-yellow-400"> |
|
Lifetime access <span className="mx-2">·</span>{' '} |
|
<button |
|
onClick={() => setIsVideoModalOpen(true)} |
|
className="flex cursor-pointer flex-row items-center gap-1.5 underline underline-offset-4 hover:text-yellow-500" |
|
> |
|
<Play className="size-3 fill-current" /> Watch Video (3 min) |
|
</button> |
|
</span> |
|
)} |
|
</div> |
|
); |
|
} |
|
|
|
if (variant === 'top-nav') { |
|
return ( |
|
<button |
|
onClick={onBuyClick} |
|
disabled={isLoadingPricing} |
|
className={`animate-fade-in rounded-full px-5 py-2 text-base font-medium text-yellow-700 transition-colors hover:text-yellow-500`} |
|
> |
|
Purchase Course |
|
</button> |
|
); |
|
} |
|
|
|
return ( |
|
<div className="relative flex flex-col items-center gap-2"> |
|
{courseLoginPopup} |
|
<button |
|
onClick={onBuyClick} |
|
disabled={isLoadingPricing} |
|
className={cn( |
|
'group relative inline-flex min-w-[220px] items-center justify-center overflow-hidden rounded-full bg-gradient-to-r from-yellow-500 to-yellow-300 px-8 py-2 font-medium text-black transition-all duration-300 ease-out hover:scale-[1.02] hover:shadow-[0_0_30px_rgba(234,179,8,0.4)] focus:outline-none', |
|
(isLoadingPricing || isCreatingCheckoutSession) && |
|
'striped-loader-yellow pointer-events-none bg-yellow-500', |
|
)} |
|
> |
|
{isLoadingPricing ? ( |
|
<span className="relative flex items-center gap-2"> </span> |
|
) : isAlreadyEnrolled ? ( |
|
<span className="relative flex items-center gap-2"> |
|
Start Learning |
|
</span> |
|
) : ( |
|
<span className="relative flex items-center gap-2"> |
|
Buy Now ${coursePricing?.regionalPrice} |
|
<ArrowRightIcon className="h-5 w-5 transition-transform duration-300 ease-out group-hover:translate-x-1" /> |
|
</span> |
|
)} |
|
</button> |
|
|
|
{!isLoadingPricing && !isAlreadyEnrolled && ( |
|
<span className="top-full text-sm text-yellow-400"> |
|
Lifetime access <span className="mx-1">·</span> Free updates |
|
</span> |
|
)} |
|
</div> |
|
); |
|
}
|
|
|