From 1ebc8134b17ee3ff61b7b1fd07faa41e43e0f6c1 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Fri, 27 Jun 2025 00:01:39 +0600 Subject: [PATCH] wi --- src/components/SQLCourseVariant/BuyButton.tsx | 368 ++++++++++++++++++ .../SQLCourseVariant/CourseDiscountBanner.tsx | 79 ++++ .../SQLCourseVariant/PurchaseBanner.tsx | 31 ++ .../SQLCourseVariant/SQLCourseVariantPage.tsx | 3 + 4 files changed, 481 insertions(+) create mode 100644 src/components/SQLCourseVariant/BuyButton.tsx create mode 100644 src/components/SQLCourseVariant/CourseDiscountBanner.tsx create mode 100644 src/components/SQLCourseVariant/PurchaseBanner.tsx diff --git a/src/components/SQLCourseVariant/BuyButton.tsx b/src/components/SQLCourseVariant/BuyButton.tsx new file mode 100644 index 000000000..c43063a43 --- /dev/null +++ b/src/components/SQLCourseVariant/BuyButton.tsx @@ -0,0 +1,368 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { + ArrowRightIcon, + CheckIcon, + CopyIcon, + MousePointerClick, + 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, + SAMPLE_AFTER_LOGIN_KEY, +} 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 { useCopyText } from '../../hooks/use-copy-text'; +import { sqlCouponCode } from './CourseDiscountBanner'; + +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 [isFakeLoading, setIsFakeLoading] = useState(true); + const [isLoginPopupOpen, setIsLoginPopupOpen] = useState(false); + const [isVideoModalOpen, setIsVideoModalOpen] = useState(false); + const toast = useToast(); + + const { copyText, isCopied } = useCopyText(); + + const { data: coursePricing, isLoading: isLoadingPrice } = useQuery( + coursePriceOptions({ courseSlug: SQL_COURSE_SLUG }), + queryClient, + ); + + const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery( + courseProgressOptions(SQL_COURSE_SLUG), + queryClient, + ); + + const { + mutate: createCheckoutSession, + isPending: isCreatingCheckoutSession, + isSuccess: isCheckoutSessionCreated, + } = useMutation( + { + mutationFn: (body: CreateCheckoutSessionBody) => { + return httpPost( + '/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'; + const shouldTriggerSample = + localStorage.getItem(SAMPLE_AFTER_LOGIN_KEY) === '1'; + + if (shouldTriggerSample) { + localStorage.removeItem(SAMPLE_AFTER_LOGIN_KEY); + window.location.href = `${import.meta.env.PUBLIC_COURSE_APP_URL}/${SQL_COURSE_SLUG}`; + } else 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); + }, []); + + useEffect(() => { + const timer = setTimeout(() => { + setIsFakeLoading(false); + }, 500); + + return () => clearTimeout(timer); + }, []); + + const isLoadingPricing = + isFakeLoading || + isCheckoutSessionCreated || + isLoadingPrice || + !coursePricing || + isLoadingCourseProgress || + isCreatingCheckoutSession; + const isAlreadyEnrolled = !!courseProgress?.enrolledAt; + + function initPurchase() { + if (!isLoggedIn()) { + setIsLoginPopupOpen(true); + return; + } + + const encodedCourseSlug = encodeURIComponent(`/courses/${SQL_COURSE_SLUG}`); + const successUrl = `/thank-you?next=${encodedCourseSlug}`; + + createCheckoutSession({ + courseId: SQL_COURSE_SLUG, + success: successUrl, + 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(); + } + + function onReadSampleClick() { + if (!isLoggedIn()) { + localStorage.setItem(SAMPLE_AFTER_LOGIN_KEY, '1'); + setIsLoginPopupOpen(true); + return; + } + + window?.fireEvent({ + action: `${SQL_COURSE_SLUG}_demo_started`, + category: 'course', + label: `${SQL_COURSE_SLUG} Course Demo Started`, + }); + + setTimeout(() => { + window.location.href = `${import.meta.env.PUBLIC_COURSE_APP_URL}/${SQL_COURSE_SLUG}`; + }, 200); + } + + const courseLoginPopup = isLoginPopupOpen && ( + setIsLoginPopupOpen(false)} /> + ); + + const mainCouponAlert = ( +
+
+
+ + 🎁 30% OFF with code{' '} + + +
+
+ ); + + if (variant === 'main') { + return ( +
+ {courseLoginPopup} + {isVideoModalOpen && ( + setIsVideoModalOpen(false)} + /> + )} +
+ {!isLoadingPricing && !isAlreadyEnrolled && mainCouponAlert} + + + +
+ + {!isLoadingPricing && ( + + Lifetime access ·{' '} + + + )} +
+ ); + } + + if (variant === 'top-nav') { + return ( + + ); + } + + return ( +
+ {courseLoginPopup} + +
+ ); +} diff --git a/src/components/SQLCourseVariant/CourseDiscountBanner.tsx b/src/components/SQLCourseVariant/CourseDiscountBanner.tsx new file mode 100644 index 000000000..829528ff9 --- /dev/null +++ b/src/components/SQLCourseVariant/CourseDiscountBanner.tsx @@ -0,0 +1,79 @@ +import { CheckIcon, CopyIcon, XIcon } from 'lucide-react'; +import { useState, useEffect } from 'react'; +import { useCopyText } from '../../hooks/use-copy-text'; +import { cn } from '../../lib/classname'; +import { SQL_COURSE_SLUG } from './BuyButton'; +import { queryClient } from '../../stores/query-client'; +import { courseProgressOptions } from '../../queries/course-progress'; +import { useQuery } from '@tanstack/react-query'; +import { useClientMount } from '../../hooks/use-client-mount'; + +export const sqlCouponCode = 'SQL30'; + +export function CourseDiscountBanner() { + const { copyText, isCopied } = useCopyText(); + const [isVisible, setIsVisible] = useState(false); + const isClientMounted = useClientMount(); + + useEffect(() => { + const timer = setTimeout(() => setIsVisible(true), 5000); + return () => clearTimeout(timer); + }, []); + + const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery( + courseProgressOptions(SQL_COURSE_SLUG), + queryClient, + ); + + const isAlreadyEnrolled = !!courseProgress?.enrolledAt; + if (!isClientMounted || isLoadingCourseProgress || isAlreadyEnrolled) { + return null; + } + + return ( +
+ + 🎁 Limited time offer : + + Get 30% off using{' '} + + +
+ ); +} diff --git a/src/components/SQLCourseVariant/PurchaseBanner.tsx b/src/components/SQLCourseVariant/PurchaseBanner.tsx new file mode 100644 index 000000000..1ac864793 --- /dev/null +++ b/src/components/SQLCourseVariant/PurchaseBanner.tsx @@ -0,0 +1,31 @@ +import { ArrowRightIcon, CheckIcon } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { cn } from '../../lib/classname'; +import { BuyButton } from './BuyButton'; +import { Rating } from '../Rating/Rating'; + +export function PurchaseBanner() { + return ( +
+
+ + + 7-Day Money-Back Guarantee + + + + Lifetime access & updates + +
+ + + +
+ + + 4.9 avg. Review + +
+
+ ); +} diff --git a/src/components/SQLCourseVariant/SQLCourseVariantPage.tsx b/src/components/SQLCourseVariant/SQLCourseVariantPage.tsx index 5a4fc4750..b62f2cbdc 100644 --- a/src/components/SQLCourseVariant/SQLCourseVariantPage.tsx +++ b/src/components/SQLCourseVariant/SQLCourseVariantPage.tsx @@ -10,6 +10,7 @@ import { Spotlight } from '../SQLCourse/Spotlight'; import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo'; import { AuthorCredentials } from './AuthorCredentials'; import { PlatformDemo } from './PlatformDemo'; +import { PurchaseBanner } from './PurchaseBanner'; export function SQLCourseVariantPage() { return ( @@ -69,6 +70,8 @@ export function SQLCourseVariantPage() { + +