Add SQL course landing page (#8127)
* wip: courses * fix: update course sidebar * wip * fix: merge lessons * wip * wip: course footer * wip * fix: refactor layout * fix: refactor * feat: course progress * fix: update current lesson store * fix: refactor props * wip * wip * feat: course certificate * wip: course rating * wip: course notes * wip * feat: implement course notes * feat: make card clickable * fix: add hover background * fix: refactor course layout * fix: resizeable * fix: go back on save * feat: delete confimation * wip * feat: chat UI * fix: lesson complete guard issue * wip: add public json files * wip: course ai * fix: loading card * Fix failing dev without internet * Light mode and UI changes * Update UI * Update course UI * Add chapter page * Improve sidebar of course * Update navigation: * Update quiz view * Improve UI for quiz attempts * Remove unnecessary console.logs * Add progress loading skeletons * Update UI * Change background color of editor * Fix line color not applied on editor * UI updates * feat: empty view * feat: course ai token limit * feat: handle auth users * wip * feat: course landing page * wip * Add first chapter of SQL * Add introduction chapter * Add quiz for introduction * Add expressions in select * Add content for DISTINCT * Add filter with where * Add lesson about limit and offset * Add lesson for handling null values * Add lesson about comments * Add challenges * Add challenge * Add challenge * Add challenge * Add challenge 7 * Add creating tables lesson * Add common data types lesson * Add data types in sqlite * Add more on data types lesson * feat: course landing page * Add more on numeric types * Update * Add lesson about temporal data types * Add constraints * Add primary keys chapter * Add modifying tables * Add dropping and truncating * Rewrite for PostgreSQL * Update numeric types to PostgreSQL * Improve temporal data type content * Improve temporal data type content * Add setup for temporal data * Improve challenges in SQL basics * Update challenge names * Add new challenges * Add temporal validation challenge * Add new constraint * Add modifying tables query * Removing table * Add insert operations lesson * Add updating data lesson * Add delete operations * Add inserting and updating challenges * Add lesson for cleaning up data * Update course title * Add relation data lesson * Add relationships and types * Add relationships and types * wip * Add joins lesson * Joins in queries * Add inner join details * Add join queries * Add inner join details * Add foreign key constraint lesson * Update composite foreign keys * Add lesson about foreign keys * Add lesson about set operation queries * Add lesson about set operation queries * Add set operator challenges * Add new challenge * Add view lesson * Add notes in views * Add inactive customer challenge * Add high value order challenge * gst * Add new challenges * Add readers like you challenge * Update inactive customer query * Update inactive customer query * Update inactive customer query * Update inactive customer query * Update inactive customer query * add challenge for same price books * Add aggregate functions introduction * Add basic aggregation lesson * Add basic aggregation lesson * Add introduction quiz * Add grouping lesson * Add grouping gotchas * Add grouping and filtering lesson * Add note for lesson * Add challenges for aggregate * Update aggregate challenge * Rearrange chapters * Add scalar functions lessons * Add numeric functions * Add date functions * Add conversion functions * Add conversion functions * Add logical functions chapter * Add exercises * Add new challenges * Add monthly sales analysis * Add subqueries and ctes * Update * Add correlated subqueries * Add common table expressions * Add common-table expressions * Add example * Add recursive CTEs * Add subquery challenge * Add latest category books challenge * Add challenges * Add bestseller rankings challenge * Add new customer analysis * Add daily sales report * Improve queries * Add introduction to window functions * Add over and partition * wip: billing page * Add ranking functions * Improve ranking functions * Add order by * Add window frames lesson * Add window frames explanation * Add challenges for window functions * Add price range analysis challenge * wip * wip: course enroll * fix: start learning * wip * wip * Enrollment changes * wip * wip * feat: mobile responsive * Changelog banner refactor * Update * Header for course * Header for what to expect * UI color * Table of contents * Icons on chapters * Change design for road to sql * Add sql course page * Add lesson content * Update UI * Expanded chapter row * Add course page * Refactor * Add spotlight * Improve features * Add course features * Add certificate note * Zoom in on the image * Update * Add floating purchase * Floating purchase indicatorg * Add about section * Update about section * Add FAQ section * Update UI * Add purchase power parity * Show purchasing power pricing * Add course login popup * Add course login popup * Add account button * Add trigger for course purchase * Course purchase param * Buy button changes * Add faqs * Add purchase trigger on reload * Landing verification * Make header responsive * Make course page upper half responsive * Full page is responsive * Fix login height bug * Responsiveness * Implement login after checkout * Remove unused code * Update dependenciesg * Update * fix: refetch mount to false * Remove unused code * Remove unused code * Remove unused code * Remove unused code * Remove unused code * Remove unused code * Remove unused * Add quizzes to chapters * Update course slug * Update dependencies * Add header for sql course --------- Co-authored-by: Arik Chakma <arikchangma@gmail.com>pull/8129/head
parent
4696af9c6a
commit
28af19cd1c
39 changed files with 2994 additions and 1329 deletions
@ -1,3 +1,4 @@ |
||||
PUBLIC_API_URL=https://api.roadmap.sh |
||||
PUBLIC_AVATAR_BASE_URL=https://dodrc8eu8m09s.cloudfront.net/avatars |
||||
PUBLIC_EDITOR_APP_URL=https://draw.roadmap.sh |
||||
PUBLIC_EDITOR_APP_URL=https://draw.roadmap.sh |
||||
PUBLIC_COURSE_APP_URL=http://localhost:5173 |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,143 @@ |
||||
import { useEffect, useState } from 'react'; |
||||
import { Modal } from '../Modal'; |
||||
import { GitHubButton } from './GitHubButton'; |
||||
import { GoogleButton } from './GoogleButton'; |
||||
import { LinkedInButton } from './LinkedInButton'; |
||||
import { EmailLoginForm } from './EmailLoginForm'; |
||||
import { EmailSignupForm } from './EmailSignupForm'; |
||||
|
||||
type CourseLoginPopupProps = { |
||||
onClose: () => void; |
||||
checkoutAfterLogin?: boolean; |
||||
}; |
||||
|
||||
export const CHECKOUT_AFTER_LOGIN_KEY = 'checkoutAfterLogin'; |
||||
|
||||
export function CourseLoginPopup(props: CourseLoginPopupProps) { |
||||
const { onClose: parentOnClose, checkoutAfterLogin = true } = props; |
||||
|
||||
const [isDisabled, setIsDisabled] = useState(false); |
||||
const [isUsingEmail, setIsUsingEmail] = useState(false); |
||||
|
||||
const [emailNature, setEmailNature] = useState<'login' | 'signup' | null>( |
||||
null, |
||||
); |
||||
|
||||
function onClose() { |
||||
// if user didn't login and closed the popup, we remove the checkoutAfterLogin flag
|
||||
// so that login from other buttons on course page will trigger purchase
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY); |
||||
parentOnClose(); |
||||
} |
||||
|
||||
useEffect(() => { |
||||
localStorage.setItem( |
||||
CHECKOUT_AFTER_LOGIN_KEY, |
||||
checkoutAfterLogin ? '1' : '0', |
||||
); |
||||
}, [checkoutAfterLogin]); |
||||
|
||||
if (emailNature) { |
||||
const emailHeader = ( |
||||
<div className="mb-7 text-center"> |
||||
<p className="mb-3.5 pt-2 text-2xl font-semibold leading-5 text-slate-900"> |
||||
{emailNature === 'login' |
||||
? 'Login to your account' |
||||
: 'Create an account'} |
||||
</p> |
||||
<p className="mt-2 text-sm leading-4 text-slate-600"> |
||||
Fill in the details below to continue |
||||
</p> |
||||
</div> |
||||
); |
||||
|
||||
return ( |
||||
<Modal onClose={onClose} bodyClassName="p-5 h-auto"> |
||||
{emailHeader} |
||||
{emailNature === 'login' && ( |
||||
<EmailLoginForm |
||||
isDisabled={isDisabled} |
||||
setIsDisabled={setIsDisabled} |
||||
/> |
||||
)} |
||||
{emailNature === 'signup' && ( |
||||
<EmailSignupForm |
||||
isDisabled={isDisabled} |
||||
setIsDisabled={setIsDisabled} |
||||
/> |
||||
)} |
||||
|
||||
<button |
||||
className="mt-2 w-full rounded-md border border-gray-400 py-2 text-center text-sm text-gray-600 hover:bg-gray-100" |
||||
onClick={() => setEmailNature(null)} |
||||
> |
||||
Back to Options |
||||
</button> |
||||
</Modal> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<Modal onClose={onClose} bodyClassName="p-5 h-auto"> |
||||
<div className="mb-7 text-center"> |
||||
<p className="mb-3.5 pt-2 text-2xl font-semibold leading-5 text-slate-900"> |
||||
Create or login to your account |
||||
</p> |
||||
<p className="mt-2 text-sm leading-4 text-slate-600"> |
||||
Login or sign up for an account to start learning |
||||
</p> |
||||
</div> |
||||
|
||||
<div className="flex w-full flex-col gap-2"> |
||||
<GitHubButton |
||||
className="rounded-md border-gray-400 hover:bg-gray-100" |
||||
isDisabled={isDisabled} |
||||
setIsDisabled={setIsDisabled} |
||||
/> |
||||
<GoogleButton |
||||
className="rounded-md border-gray-400 hover:bg-gray-100" |
||||
isDisabled={isDisabled} |
||||
setIsDisabled={setIsDisabled} |
||||
/> |
||||
<LinkedInButton |
||||
className="rounded-md border-gray-400 hover:bg-gray-100" |
||||
isDisabled={isDisabled} |
||||
setIsDisabled={setIsDisabled} |
||||
/> |
||||
</div> |
||||
|
||||
<div className="flex w-full items-center gap-4 py-6 text-sm text-gray-600"> |
||||
<div className="h-px w-full bg-gray-200" /> |
||||
OR |
||||
<div className="h-px w-full bg-gray-200" /> |
||||
</div> |
||||
|
||||
<div className="flex flex-row gap-2"> |
||||
{!isUsingEmail && ( |
||||
<button |
||||
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100" |
||||
onClick={() => setIsUsingEmail(true)} |
||||
> |
||||
Use your email address |
||||
</button> |
||||
)} |
||||
{isUsingEmail && ( |
||||
<> |
||||
<button |
||||
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100" |
||||
onClick={() => setEmailNature('login')} |
||||
> |
||||
Already have an account |
||||
</button> |
||||
<button |
||||
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100" |
||||
onClick={() => setEmailNature('signup')} |
||||
> |
||||
Create an account |
||||
</button> |
||||
</> |
||||
)} |
||||
</div> |
||||
</Modal> |
||||
); |
||||
} |
@ -0,0 +1,28 @@ |
||||
import type { SVGProps } from 'react'; |
||||
|
||||
type RoadmapLogoIconProps = SVGProps<SVGSVGElement> & { |
||||
color?: 'white' | 'black'; |
||||
}; |
||||
|
||||
export function RoadmapLogoIcon(props: RoadmapLogoIconProps) { |
||||
const { color = 'white', ...rest } = props; |
||||
|
||||
return ( |
||||
<svg |
||||
xmlns="http://www.w3.org/2000/svg" |
||||
width="30" |
||||
height="30" |
||||
viewBox="0 0 283 283" |
||||
{...rest} |
||||
> |
||||
<path |
||||
fill={color === 'black' ? '#000' : '#fff'} |
||||
d="M0 39C0 17.46 17.46 0 39 0h205c21.539 0 39 17.46 39 39v205c0 21.539-17.461 39-39 39H39c-21.54 0-39-17.461-39-39V39Z" |
||||
/> |
||||
<path |
||||
fill={color === 'black' ? '#fff' : '#000'} |
||||
d="M121.215 210.72c-1.867.56-4.854 1.12-8.96 1.68-3.92.56-8.027.84-12.32.84-4.107 0-7.84-.28-11.2-.84-3.174-.56-5.88-1.68-8.12-3.36s-4.014-3.92-5.32-6.72c-1.12-2.987-1.68-6.813-1.68-11.48v-84c0-4.293.746-7.933 2.24-10.92 1.68-3.173 4.013-5.973 7-8.4s6.626-4.573 10.92-6.44c4.48-2.053 9.24-3.827 14.28-5.32a106.176 106.176 0 0 1 15.68-3.36 95.412 95.412 0 0 1 16.24-1.4c8.96 0 16.053 1.773 21.28 5.32 5.226 3.36 7.84 8.96 7.84 16.8 0 2.613-.374 5.227-1.12 7.84-.747 2.427-1.68 4.667-2.8 6.72a133.1 133.1 0 0 0-12.04.56c-4.107.373-8.12.933-12.04 1.68s-7.654 1.587-11.2 2.52c-3.36.747-6.254 1.68-8.68 2.8v95.48zm45.172-22.4c0-7.84 2.426-14.373 7.28-19.6s11.48-7.84 19.88-7.84 15.026 2.613 19.88 7.84 7.28 11.76 7.28 19.6-2.427 14.373-7.28 19.6-11.48 7.84-19.88 7.84-15.027-2.613-19.88-7.84-7.28-11.76-7.28-19.6z" |
||||
/> |
||||
</svg> |
||||
); |
||||
} |
@ -0,0 +1,67 @@ |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import { useEffect, useState } from 'react'; |
||||
import { isLoggedIn } from '../../lib/jwt'; |
||||
import { |
||||
courseProgressOptions |
||||
} from '../../queries/course-progress'; |
||||
import { queryClient } from '../../stores/query-client'; |
||||
import { CourseLoginPopup } from '../AuthenticationFlow/CourseLoginPopup'; |
||||
import { BuyButton, COURSE_SLUG } from './BuyButton'; |
||||
|
||||
export function AccountButton() { |
||||
const [isVisible, setIsVisible] = useState(false); |
||||
const [showLoginModal, setShowLoginModal] = useState(false); |
||||
|
||||
const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery( |
||||
courseProgressOptions(COURSE_SLUG), |
||||
queryClient, |
||||
); |
||||
|
||||
useEffect(() => { |
||||
setIsVisible(true); |
||||
}, []); |
||||
|
||||
const buttonClasses = |
||||
'rounded-full px-5 py-2 text-base font-medium text-yellow-700 hover:text-yellow-500 transition-colors'; |
||||
|
||||
const hasEnrolled = !!courseProgress?.enrolledAt; |
||||
const loginModal = ( |
||||
<CourseLoginPopup |
||||
checkoutAfterLogin={false} |
||||
onClose={() => { |
||||
setShowLoginModal(false); |
||||
}} |
||||
/> |
||||
); |
||||
|
||||
if (!isVisible || isLoadingCourseProgress) { |
||||
return <button className={`${buttonClasses} opacity-0`}>...</button>; |
||||
} |
||||
|
||||
if (!isLoggedIn()) { |
||||
return ( |
||||
<> |
||||
<button |
||||
onClick={() => setShowLoginModal(true)} |
||||
className={`${buttonClasses} animate-fade-in`} |
||||
> |
||||
Login |
||||
</button> |
||||
{showLoginModal && loginModal} |
||||
</> |
||||
); |
||||
} |
||||
|
||||
if (!hasEnrolled) { |
||||
return <BuyButton variant="top-nav" />; |
||||
} |
||||
|
||||
return ( |
||||
<a |
||||
href={`${import.meta.env.PUBLIC_COURSE_APP_URL}/sql`} |
||||
className={`${buttonClasses} animate-fade-in`} |
||||
> |
||||
Start Learning |
||||
</a> |
||||
); |
||||
} |
@ -0,0 +1,210 @@ |
||||
import { useMutation, useQuery } from '@tanstack/react-query'; |
||||
import { ArrowRightIcon } from 'lucide-react'; |
||||
import { useEffect, useState } from 'react'; |
||||
import { cn } from '../../lib/classname'; |
||||
import { COURSE_PURCHASE_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'; |
||||
|
||||
export const COURSE_SLUG = 'master-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 toast = useToast(); |
||||
|
||||
const { data: coursePricing, isLoading: isLoadingCourse } = useQuery( |
||||
coursePriceOptions({ courseSlug: COURSE_SLUG }), |
||||
queryClient, |
||||
); |
||||
|
||||
const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery( |
||||
courseProgressOptions(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) => { |
||||
window.location.href = data.checkoutUrl; |
||||
}, |
||||
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(); |
||||
} |
||||
}, []); |
||||
|
||||
const isLoadingPricing = |
||||
isLoadingCourse || !coursePricing || isLoadingCourseProgress; |
||||
const isAlreadyEnrolled = !!courseProgress?.enrolledAt; |
||||
|
||||
function initPurchase() { |
||||
if (!isLoggedIn()) { |
||||
return; |
||||
} |
||||
|
||||
createCheckoutSession({ |
||||
courseId: COURSE_SLUG, |
||||
success: `/courses/sql?e=1`, |
||||
cancel: `/courses/sql`, |
||||
}); |
||||
} |
||||
|
||||
function onBuyClick() { |
||||
if (!isLoggedIn()) { |
||||
setIsLoginPopupOpen(true); |
||||
return; |
||||
} |
||||
|
||||
const hasEnrolled = !!courseProgress?.enrolledAt; |
||||
if (hasEnrolled) { |
||||
window.location.href = `${import.meta.env.PUBLIC_COURSE_APP_URL}/sql`; |
||||
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} |
||||
<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"> |
||||
{coursePricing?.isEligibleForDiscount && coursePricing?.flag} 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 && |
||||
!isAlreadyEnrolled && |
||||
coursePricing?.isEligibleForDiscount && ( |
||||
<span className="absolute top-full translate-y-2.5 text-sm text-yellow-400"> |
||||
{coursePricing.regionalDiscountPercentage}% regional discount |
||||
applied |
||||
</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"> |
||||
{coursePricing?.flag} Buy Now ${coursePricing?.regionalPrice} |
||||
<ArrowRightIcon className="h-5 w-5 transition-transform duration-300 ease-out group-hover:translate-x-1" /> |
||||
</span> |
||||
)} |
||||
</button> |
||||
{!isAlreadyEnrolled && coursePricing?.isEligibleForDiscount && ( |
||||
<span className="top-full text-sm text-yellow-400"> |
||||
{coursePricing.regionalDiscountPercentage}% regional discount applied |
||||
</span> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,145 @@ |
||||
import { ChevronDown, BookIcon, CodeIcon, FileQuestion, MessageCircleQuestionIcon, CircleDot } from 'lucide-react'; |
||||
import { cn } from '../../lib/classname'; |
||||
import { useEffect, useState } from 'react'; |
||||
|
||||
type ChapterRowProps = { |
||||
counter: number; |
||||
icon: React.ReactNode; |
||||
title: string; |
||||
description: string; |
||||
lessonCount: number; |
||||
challengeCount: number; |
||||
isExpandable?: boolean; |
||||
className?: string; |
||||
lessons?: { title: string; type: 'lesson' | 'challenge' | 'quiz' }[]; |
||||
}; |
||||
|
||||
export function ChapterRow(props: ChapterRowProps) { |
||||
const { |
||||
counter, |
||||
icon, |
||||
title, |
||||
description, |
||||
lessonCount, |
||||
challengeCount, |
||||
isExpandable = true, |
||||
className, |
||||
lessons = [], |
||||
} = props; |
||||
|
||||
const [isExpanded, setIsExpanded] = useState(true); |
||||
|
||||
const regularLessons = lessons.filter((l) => l.type === 'lesson'); |
||||
const challenges = lessons.filter((l) => |
||||
['challenge', 'quiz'].includes(l.type), |
||||
); |
||||
|
||||
useEffect(() => { |
||||
const isMobile = window.innerWidth < 768; |
||||
setIsExpanded(!isMobile); |
||||
}, []); |
||||
|
||||
return ( |
||||
<div |
||||
className={cn('group relative select-none overflow-hidden', className)} |
||||
> |
||||
<div |
||||
role="button" |
||||
onClick={() => isExpandable && setIsExpanded(!isExpanded)} |
||||
className={cn( |
||||
'relative rounded-xl border border-zinc-800 bg-zinc-800 p-6', |
||||
'bg-gradient-to-br from-zinc-900/90 via-zinc-900/70 to-zinc-900/50', |
||||
!isExpanded && |
||||
'hover:bg-gradient-to-br hover:from-zinc-900/95 hover:via-zinc-900/80 hover:to-zinc-900/60', |
||||
!isExpanded && |
||||
'hover:cursor-pointer hover:shadow-[0_0_30px_rgba(0,0,0,0.2)]', |
||||
isExpanded && 'rounded-b-none border-b-0', |
||||
)} |
||||
> |
||||
<div className="flex items-start gap-4"> |
||||
<div className="hidden flex-shrink-0 md:block"> |
||||
<div className="rounded-full bg-yellow-500/10 p-3">{icon}</div> |
||||
</div> |
||||
|
||||
<div className="flex-grow"> |
||||
<h3 className="text-xl font-semibold tracking-wide text-white"> |
||||
<span className="inline text-gray-500 md:hidden"> |
||||
{counter}.{' '} |
||||
</span> |
||||
{title} |
||||
</h3> |
||||
<p className="mt-2 text-zinc-400">{description}</p> |
||||
|
||||
<div className="mt-4 flex items-center gap-4"> |
||||
<div className="flex items-center gap-2 text-sm text-zinc-500"> |
||||
<span>{lessonCount} Lessons</span> |
||||
</div> |
||||
<div className="flex items-center gap-2 text-sm text-zinc-500"> |
||||
<span>{challengeCount} Challenges</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
{isExpandable && ( |
||||
<div className="flex-shrink-0 rounded-full bg-zinc-800/80 p-2 text-zinc-400 group-hover:bg-zinc-800 group-hover:text-yellow-500"> |
||||
<ChevronDown |
||||
className={cn( |
||||
'h-4 w-4 transition-transform', |
||||
isExpanded ? 'rotate-180' : '', |
||||
)} |
||||
/> |
||||
</div> |
||||
)} |
||||
</div> |
||||
</div> |
||||
|
||||
{isExpanded && ( |
||||
<div className="rounded-b-xl border border-t-0 border-zinc-800 bg-gradient-to-br from-zinc-900/50 via-zinc-900/30 to-zinc-900/20"> |
||||
<div className="grid grid-cols-1 divide-zinc-800 md:grid-cols-2 md:divide-x"> |
||||
{regularLessons.length > 0 && ( |
||||
<div className="p-6 pb-0 md:pb-6"> |
||||
<h4 className="mb-4 text-sm font-medium uppercase tracking-wider text-zinc-500"> |
||||
Lessons |
||||
</h4> |
||||
<div className="space-y-3"> |
||||
{regularLessons.map((lesson, index) => ( |
||||
<div |
||||
key={index} |
||||
className="flex items-center gap-3 text-zinc-400 hover:text-yellow-500" |
||||
> |
||||
<BookIcon className="h-4 w-4" /> |
||||
<span>{lesson.title}</span> |
||||
</div> |
||||
))} |
||||
</div> |
||||
</div> |
||||
)} |
||||
|
||||
{challenges.length > 0 && ( |
||||
<div className="p-6"> |
||||
<h4 className="mb-4 text-sm font-medium uppercase tracking-wider text-zinc-500"> |
||||
Exercises |
||||
</h4> |
||||
<div className="space-y-3"> |
||||
{challenges.map((challenge, index) => ( |
||||
<div |
||||
key={index} |
||||
className="flex items-center gap-3 text-zinc-400 hover:text-yellow-500" |
||||
> |
||||
{challenge.type === 'challenge' ? ( |
||||
<CodeIcon className="h-4 w-4" /> |
||||
) : ( |
||||
<CircleDot className="h-4 w-4" /> |
||||
)} |
||||
<span>{challenge.title}</span> |
||||
</div> |
||||
))} |
||||
</div> |
||||
</div> |
||||
)} |
||||
</div> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,24 @@ |
||||
export function CourseAuthor() { |
||||
return ( |
||||
<div className="mt-8 w-full max-w-3xl space-y-4"> |
||||
<div className="flex flex-row items-center gap-5"> |
||||
<img |
||||
src="https://github.com/kamranahmedse.png" |
||||
className="size-12 rounded-full bg-yellow-500/10 md:size-16" |
||||
/> |
||||
<a |
||||
href="https://twitter.com/kamrify" |
||||
target="_blank" |
||||
className="flex flex-col" |
||||
> |
||||
<span className="text-lg font-medium text-zinc-200 md:text-2xl"> |
||||
Kamran Ahmed |
||||
</span> |
||||
<span className="text-sm text-zinc-500 md:text-lg"> |
||||
Software Engineer |
||||
</span> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,94 @@ |
||||
import { MinusIcon, PlusIcon, type LucideIcon } from 'lucide-react'; |
||||
import { useEffect, useState } from 'react'; |
||||
import { cn } from '../../lib/classname'; |
||||
|
||||
type CourseFeatureProps = { |
||||
title: string; |
||||
icon: LucideIcon; |
||||
description: string; |
||||
imgUrl?: string; |
||||
}; |
||||
|
||||
export function CourseFeature(props: CourseFeatureProps) { |
||||
const { title, icon: Icon, description, imgUrl } = props; |
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false); |
||||
const [isZoomed, setIsZoomed] = useState(false); |
||||
|
||||
useEffect(() => { |
||||
function onScroll() { |
||||
if (isZoomed) { |
||||
setIsZoomed(false); |
||||
} |
||||
} |
||||
|
||||
window.addEventListener('scroll', onScroll); |
||||
return () => window.removeEventListener('scroll', onScroll); |
||||
}, [isZoomed]); |
||||
|
||||
return ( |
||||
<> |
||||
{isZoomed && ( |
||||
<div |
||||
onClick={() => { |
||||
setIsZoomed(false); |
||||
setIsExpanded(false); |
||||
}} |
||||
className="fixed inset-0 z-[999] flex cursor-zoom-out items-center justify-center bg-black bg-opacity-75" |
||||
> |
||||
<img |
||||
src={imgUrl} |
||||
alt={title} |
||||
className="max-h-[50%] max-w-[90%] rounded-xl object-contain" |
||||
/> |
||||
</div> |
||||
)} |
||||
<div |
||||
className={cn( |
||||
'fixed inset-0 z-10 bg-black/70 opacity-100 transition-opacity duration-200 ease-out', |
||||
{ |
||||
'pointer-events-none opacity-0': !isExpanded, |
||||
}, |
||||
)} |
||||
onClick={() => setIsExpanded(false)} |
||||
></div> |
||||
<div className="relative"> |
||||
<button |
||||
onClick={() => setIsExpanded(!isExpanded)} |
||||
className={cn( |
||||
'z-20 flex w-full items-center rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-3 text-left transition-colors duration-200 ease-out hover:bg-zinc-800/40', |
||||
{ |
||||
'relative bg-zinc-800 hover:bg-zinc-800': isExpanded, |
||||
}, |
||||
)} |
||||
> |
||||
<span className="flex flex-grow items-center space-x-3"> |
||||
<Icon /> |
||||
<span>{title}</span> |
||||
</span> |
||||
{isExpanded ? ( |
||||
<MinusIcon className="h-4 w-4" /> |
||||
) : ( |
||||
<PlusIcon className="h-4 w-4" /> |
||||
)} |
||||
</button> |
||||
{isExpanded && ( |
||||
<div className="absolute left-0 top-full z-20 translate-y-2 rounded-lg border border-zinc-800 bg-zinc-800 p-4"> |
||||
<p>{description}</p> |
||||
{imgUrl && ( |
||||
<img |
||||
onClick={() => { |
||||
setIsZoomed(true); |
||||
setIsExpanded(false); |
||||
}} |
||||
src={imgUrl} |
||||
alt={title} |
||||
className="mt-4 h-auto pointer-events-none md:pointer-events-auto w-full cursor-zoom-in rounded-lg object-right-top" |
||||
/> |
||||
)} |
||||
</div> |
||||
)} |
||||
</div> |
||||
</> |
||||
); |
||||
} |
@ -0,0 +1,113 @@ |
||||
import { ChevronDownIcon } from 'lucide-react'; |
||||
import { useState } from 'react'; |
||||
import { SectionHeader } from './SectionHeader'; |
||||
|
||||
type FAQItem = { |
||||
question: string; |
||||
answer: string; |
||||
}; |
||||
|
||||
function FAQRow({ question, answer }: FAQItem) { |
||||
const [isExpanded, setIsExpanded] = useState(false); |
||||
|
||||
return ( |
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900"> |
||||
<button |
||||
onClick={() => setIsExpanded(!isExpanded)} |
||||
className="flex w-full items-center justify-between p-4 md:p-6 text-left gap-2" |
||||
> |
||||
<h3 className="text-lg md:text-xl text-balance font-normal text-white">{question}</h3> |
||||
<ChevronDownIcon |
||||
className={`h-5 w-5 text-zinc-400 transition-transform duration-200 ${ |
||||
isExpanded ? 'rotate-180' : '' |
||||
}`}
|
||||
/> |
||||
</button> |
||||
{isExpanded && ( |
||||
<div className="border-t border-zinc-800 p-6 pt-4 text-base md:text-lg leading-relaxed"> |
||||
<p>{answer}</p> |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
export function FAQSection() { |
||||
const faqs: FAQItem[] = [ |
||||
{ |
||||
question: 'What is the format of the course?', |
||||
answer: |
||||
'The course is written in textual format. There are several chapters; each chapter has a set of lessons, followed by a set of practice problems and quizzes. You can learn at your own pace and revisit the content anytime.', |
||||
}, |
||||
{ |
||||
question: 'What prerequisites do I need for this course?', |
||||
answer: |
||||
'No prior SQL knowledge is required. The course starts from the basics and gradually progresses to advanced topics.', |
||||
}, |
||||
{ |
||||
question: 'Do I need to have a local database to follow the course?', |
||||
answer: |
||||
'No, we have an integrated coding playground, populated with a sample databases depending on the lesson, that you can use to follow the course. You can also use your own database if you have one.', |
||||
}, |
||||
{ |
||||
question: 'How long do I have access to the course?', |
||||
answer: |
||||
'You get lifetime access to the course including all future updates. Once you purchase, you can learn at your own pace and revisit the content anytime.', |
||||
}, |
||||
{ |
||||
question: 'What kind of support is available?', |
||||
answer: |
||||
'You get access to an AI tutor within the course that can help you with queries 24/7. Additionally, you can use the community forums to discuss problems and get help from other learners.', |
||||
}, |
||||
{ |
||||
question: 'Will I get a certificate upon completion?', |
||||
answer: |
||||
"Yes, upon completing the course and its challenges, you'll receive a certificate of completion that you can share with employers or add to your LinkedIn profile.", |
||||
}, |
||||
{ |
||||
question: 'Can I use this for job interviews?', |
||||
answer: |
||||
'Absolutely! The course covers common SQL interview topics and includes practical challenges similar to what you might face in technical interviews. The hands-on experience will prepare you well for real-world scenarios.', |
||||
}, |
||||
{ |
||||
question: "What if I don't like the course?", |
||||
answer: |
||||
'I will refund your purchase within 7 days of the purchase. No questions asked. However, I would love to hear your feedback so that I can improve the course. Send me an email at kamran@roadmap.sh', |
||||
}, |
||||
{ |
||||
question: 'I already know SQL, can I still take this course?', |
||||
answer: |
||||
'Yes! The course starts from the basics and gradually progresses to advanced topics. You can skip the chapters that you already know and focus on the ones that you need.', |
||||
}, |
||||
{ |
||||
question: 'Do you offer any team licenses?', |
||||
answer: 'Yes, please contact me at kamran@roadmap.sh', |
||||
}, |
||||
{ |
||||
question: 'How can I gift this course to someone?', |
||||
answer: |
||||
'Please contact me at kamran@roadmap.sh and I will be happy to help you.', |
||||
}, |
||||
{ |
||||
question: 'What if I have a question that is not answered here?', |
||||
answer: |
||||
'Please contact me at kamran@roadmap.sh and I will be happy to help you.', |
||||
}, |
||||
]; |
||||
|
||||
return ( |
||||
<> |
||||
<SectionHeader |
||||
title="Frequently Asked Questions" |
||||
description="Find answers to common questions about the course below." |
||||
className="mt-10 md:mt-24" |
||||
/> |
||||
|
||||
<div className="mt-6 md:mt-8 w-full max-w-3xl space-y-2 md:space-y-6"> |
||||
{faqs.map((faq, index) => ( |
||||
<FAQRow key={index} {...faq} /> |
||||
))} |
||||
</div> |
||||
</> |
||||
); |
||||
} |
@ -0,0 +1,56 @@ |
||||
import { ArrowRightIcon } from 'lucide-react'; |
||||
import { useEffect, useState } from 'react'; |
||||
import { cn } from '../../lib/classname'; |
||||
import { BuyButton } from './BuyButton'; |
||||
|
||||
export function FloatingPurchase() { |
||||
const [isHidden, setIsHidden] = useState(true); |
||||
|
||||
useEffect(() => { |
||||
function onScroll() { |
||||
setIsHidden(window.scrollY < 400); |
||||
} |
||||
|
||||
window.addEventListener('scroll', onScroll); |
||||
return () => window.removeEventListener('scroll', onScroll); |
||||
}, []); |
||||
|
||||
return ( |
||||
<div |
||||
className={cn( |
||||
'fixed bottom-0 left-0 right-0 z-[5] flex items-center justify-center transition-all duration-200 ease-out', |
||||
{ |
||||
'pointer-events-none -bottom-10 opacity-0': isHidden, |
||||
}, |
||||
)} |
||||
> |
||||
{/* Desktop version */} |
||||
<div className="hidden mb-5 md:flex w-full max-w-[800px] items-center justify-between rounded-2xl bg-yellow-950 p-5 shadow-lg ring-1 ring-yellow-500/40"> |
||||
<div className="flex flex-col"> |
||||
<h2 className="mb-1 text-xl font-medium text-white"> |
||||
Go from Zero to Hero in SQL |
||||
</h2> |
||||
<p className="text-sm text-zinc-400"> |
||||
Get instant access to the course and start learning today |
||||
</p> |
||||
</div> |
||||
|
||||
<BuyButton variant="floating" /> |
||||
</div> |
||||
|
||||
{/* Mobile version */} |
||||
<div className="flex md:hidden w-full flex-col bg-yellow-950 px-4 pt-3 pb-4 shadow-lg ring-1 ring-yellow-500/40"> |
||||
<div className="flex flex-col items-center text-center mb-3"> |
||||
<h2 className="text-lg font-medium text-white"> |
||||
Master SQL Today |
||||
</h2> |
||||
<p className="text-xs text-zinc-400"> |
||||
Get instant lifetime access |
||||
</p> |
||||
</div> |
||||
|
||||
<BuyButton variant="floating" /> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,415 @@ |
||||
import { |
||||
ArrowRightIcon, |
||||
ArrowUpDownIcon, |
||||
BarChartIcon, |
||||
BrainIcon, |
||||
ClipboardIcon, |
||||
CodeIcon, |
||||
DatabaseIcon, |
||||
Eye, |
||||
FileCheckIcon, |
||||
FileQuestionIcon, |
||||
GitBranchIcon, |
||||
GitMergeIcon, |
||||
LayersIcon, |
||||
TableIcon, |
||||
WrenchIcon, |
||||
} from 'lucide-react'; |
||||
import { ChapterRow } from './ChapterRow'; |
||||
import { CourseFeature } from './CourseFeature'; |
||||
import { SectionHeader } from './SectionHeader'; |
||||
import { Spotlight } from './Spotlight'; |
||||
import { FloatingPurchase } from './FloatingPurchase'; |
||||
import { CourseAuthor } from './CourseAuthor'; |
||||
import { FAQSection } from './FAQSection'; |
||||
import { BuyButton } from './BuyButton'; |
||||
import { AccountButton } from './AccountButton'; |
||||
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo'; |
||||
|
||||
type ChapterData = { |
||||
icon: React.ReactNode; |
||||
title: string; |
||||
description: string; |
||||
lessonCount: number; |
||||
challengeCount: number; |
||||
lessons: { title: string; type: 'lesson' | 'challenge' | 'quiz' }[]; |
||||
}; |
||||
|
||||
export function SQLCoursePage() { |
||||
const chapters: ChapterData[] = [ |
||||
{ |
||||
icon: <DatabaseIcon className="h-6 w-6 text-yellow-500" />, |
||||
title: 'Introduction', |
||||
description: |
||||
'Get comfortable with database concepts and SQL fundamentals.', |
||||
lessonCount: 4, |
||||
challengeCount: 1, |
||||
lessons: [ |
||||
{ title: 'Basics of Databases', type: 'lesson' }, |
||||
{ title: 'What is SQL?', type: 'lesson' }, |
||||
{ title: 'Types of Queries', type: 'lesson' }, |
||||
{ title: 'Next Steps', type: 'lesson' }, |
||||
{ title: 'Introduction Quiz', type: 'challenge' }, |
||||
], |
||||
}, |
||||
{ |
||||
icon: <TableIcon className="h-6 w-6 text-yellow-500" />, |
||||
title: 'SQL Basics', |
||||
description: 'Master the essential SQL query operations and syntax.', |
||||
lessonCount: 9, |
||||
challengeCount: 7, |
||||
lessons: [ |
||||
{ title: 'SELECT Fundamentals', type: 'lesson' }, |
||||
{ title: 'Aliases and Constants', type: 'lesson' }, |
||||
{ title: 'Expressions in SELECT', type: 'lesson' }, |
||||
{ title: 'Selecting DISTINCT Values', type: 'lesson' }, |
||||
{ title: 'Filtering with WHERE', type: 'lesson' }, |
||||
{ title: 'Sorting with ORDER BY', type: 'lesson' }, |
||||
{ title: 'Limiting Results with LIMIT', type: 'lesson' }, |
||||
{ title: 'Handling NULL Values', type: 'lesson' }, |
||||
{ title: 'Comments', type: 'lesson' }, |
||||
{ title: 'Basic Queries Quiz', type: 'quiz' }, |
||||
{ title: 'Projection Challenge', type: 'challenge' }, |
||||
{ title: 'Select Expression', type: 'challenge' }, |
||||
{ title: 'Select Unique', type: 'challenge' }, |
||||
{ title: 'Logical Operators', type: 'challenge' }, |
||||
{ title: 'Sorting Challenge', type: 'challenge' }, |
||||
{ title: 'Sorting and Limiting', type: 'challenge' }, |
||||
{ title: 'Sorting and Filtering', type: 'challenge' }, |
||||
], |
||||
}, |
||||
{ |
||||
icon: <CodeIcon className="h-6 w-6 text-yellow-500" />, |
||||
title: 'Manipulating Data', |
||||
description: 'Learn how to modify and manipulate data in your database.', |
||||
lessonCount: 3, |
||||
challengeCount: 3, |
||||
lessons: [ |
||||
{ title: 'INSERT Operations', type: 'lesson' }, |
||||
{ title: 'UPDATE Operations', type: 'lesson' }, |
||||
{ title: 'DELETE Operations', type: 'lesson' }, |
||||
{ title: 'Data Manipulation Quiz', type: 'quiz' }, |
||||
{ title: 'Inserting Customers', type: 'challenge' }, |
||||
{ title: 'Updating Bookstore', type: 'challenge' }, |
||||
{ title: 'Deleting Books', type: 'challenge' }, |
||||
], |
||||
}, |
||||
{ |
||||
icon: <LayersIcon className="h-6 w-6 text-yellow-500" />, |
||||
title: 'Defining Tables', |
||||
description: 'Master database schema design and table management.', |
||||
lessonCount: 9, |
||||
challengeCount: 7, |
||||
lessons: [ |
||||
{ title: 'Creating Tables', type: 'lesson' }, |
||||
{ title: 'Data Types in SQLite', type: 'lesson' }, |
||||
{ title: 'Common Data Types', type: 'lesson' }, |
||||
{ title: 'More on Numeric Types', type: 'lesson' }, |
||||
{ title: 'Temporal Data Types', type: 'lesson' }, |
||||
{ title: 'CHECK Constraints', type: 'lesson' }, |
||||
{ title: 'Primary Key Constraint', type: 'lesson' }, |
||||
{ title: 'Modifying Tables', type: 'lesson' }, |
||||
{ title: 'Dropping and Truncating', type: 'lesson' }, |
||||
{ title: 'Defining Tables Quiz', type: 'quiz' }, |
||||
{ title: 'Simple Table Creation', type: 'challenge' }, |
||||
{ title: 'Data Types Challenge', type: 'challenge' }, |
||||
{ title: 'Constraints Challenge', type: 'challenge' }, |
||||
{ title: 'Temporal Validation', type: 'challenge' }, |
||||
{ title: 'Sales Data Analysis', type: 'challenge' }, |
||||
{ title: 'Modifying Tables', type: 'challenge' }, |
||||
{ title: 'Removing Table Data', type: 'challenge' }, |
||||
], |
||||
}, |
||||
{ |
||||
icon: <GitMergeIcon className="h-6 w-6 text-yellow-500" />, |
||||
title: 'Multi-Table Queries', |
||||
description: |
||||
'Learn to work with multiple tables using JOINs and relationships.', |
||||
lessonCount: 7, |
||||
challengeCount: 10, |
||||
lessons: [ |
||||
{ title: 'More on Relational Data', type: 'lesson' }, |
||||
{ title: 'Relationships and Types', type: 'lesson' }, |
||||
{ title: 'JOINs in Queries', type: 'lesson' }, |
||||
{ title: 'Self Joins and Usecases', type: 'lesson' }, |
||||
{ title: 'Foreign Key Constraint', type: 'lesson' }, |
||||
{ title: 'Set Operator Queries', type: 'lesson' }, |
||||
{ title: 'Views and Virtual Tables', type: 'lesson' }, |
||||
{ title: 'Multi-Table Queries Quiz', type: 'quiz' }, |
||||
{ title: 'Inactive Customers', type: 'challenge' }, |
||||
{ title: 'Recent 3 Orders', type: 'challenge' }, |
||||
{ title: 'High Value Orders', type: 'challenge' }, |
||||
{ title: 'Specific Book Customers', type: 'challenge' }, |
||||
{ title: 'Referred Customers', type: 'challenge' }, |
||||
{ title: 'Readers Like You', type: 'challenge' }, |
||||
{ title: 'Same Price Books', type: 'challenge' }, |
||||
{ title: 'Multi-Section Authors', type: 'challenge' }, |
||||
{ title: 'Expensive Books', type: 'challenge' }, |
||||
{ title: 'Trending Tech Books', type: 'challenge' }, |
||||
], |
||||
}, |
||||
{ |
||||
icon: <WrenchIcon className="h-6 w-6 text-yellow-500" />, |
||||
title: 'Aggregate Functions', |
||||
description: |
||||
"Analyze and summarize data using SQL's powerful aggregation features.", |
||||
lessonCount: 4, |
||||
challengeCount: 10, |
||||
lessons: [ |
||||
{ title: 'What is Aggregation?', type: 'lesson' }, |
||||
{ title: 'Basic Aggregation', type: 'lesson' }, |
||||
{ title: 'Grouping Data', type: 'lesson' }, |
||||
{ title: 'Grouping and Filtering', type: 'lesson' }, |
||||
{ title: 'Aggregate Queries Quiz', type: 'quiz' }, |
||||
{ title: 'Book Sales Summary', type: 'challenge' }, |
||||
{ title: 'Category Insights', type: 'challenge' }, |
||||
{ title: 'Author Tier Analysis', type: 'challenge' }, |
||||
{ title: 'Author Book Stats', type: 'challenge' }, |
||||
{ title: 'Daily Sales Report', type: 'challenge' }, |
||||
{ title: 'Publisher Stats', type: 'challenge' }, |
||||
{ title: 'High Value Publishers', type: 'challenge' }, |
||||
{ title: 'Premium Authors', type: 'challenge' }, |
||||
{ title: 'Sales Analysis', type: 'challenge' }, |
||||
{ title: 'Employee Performance', type: 'challenge' }, |
||||
], |
||||
}, |
||||
{ |
||||
icon: <BarChartIcon className="h-6 w-6 text-yellow-500" />, |
||||
title: 'Scalar Functions', |
||||
description: |
||||
'Master built-in functions for data transformation and manipulation.', |
||||
lessonCount: 6, |
||||
challengeCount: 5, |
||||
lessons: [ |
||||
{ title: 'What are they?', type: 'lesson' }, |
||||
{ title: 'String Functions', type: 'lesson' }, |
||||
{ title: 'Numeric Functions', type: 'lesson' }, |
||||
{ title: 'Date Functions', type: 'lesson' }, |
||||
{ title: 'Conversion Functions', type: 'lesson' }, |
||||
{ title: 'Logical Functions', type: 'lesson' }, |
||||
{ title: 'Scalar Functions Quiz', type: 'quiz' }, |
||||
{ title: 'Customer Contact List', type: 'challenge' }, |
||||
{ title: 'Membership Duration', type: 'challenge' }, |
||||
{ title: 'Book Performance', type: 'challenge' }, |
||||
{ title: 'Book Categories', type: 'challenge' }, |
||||
{ title: 'Monthly Sales Analysis', type: 'challenge' }, |
||||
], |
||||
}, |
||||
{ |
||||
icon: <GitBranchIcon className="h-6 w-6 text-yellow-500" />, |
||||
title: 'Subqueries and CTEs', |
||||
description: |
||||
'Write complex queries using subqueries and common table expressions.', |
||||
lessonCount: 4, |
||||
challengeCount: 6, |
||||
lessons: [ |
||||
{ title: 'What are Subqueries?', type: 'lesson' }, |
||||
{ title: 'Correlated Subqueries', type: 'lesson' }, |
||||
{ title: 'Common Table Expressions', type: 'lesson' }, |
||||
{ title: 'Recursive CTEs', type: 'lesson' }, |
||||
{ title: 'Subqueries Quiz', type: 'quiz' }, |
||||
{ title: 'Books Above Average', type: 'challenge' }, |
||||
{ title: 'Latest Category Books', type: 'challenge' }, |
||||
{ title: 'Low Stock by Category', type: 'challenge' }, |
||||
{ title: 'Bestseller Rankings', type: 'challenge' }, |
||||
{ title: 'New Customer Analysis', type: 'challenge' }, |
||||
{ title: 'Daily Sales Report', type: 'challenge' }, |
||||
], |
||||
}, |
||||
{ |
||||
icon: <ArrowUpDownIcon className="h-6 w-6 text-yellow-500" />, |
||||
title: 'Window Functions', |
||||
description: |
||||
'Advanced analytics and calculations using window functions.', |
||||
lessonCount: 5, |
||||
challengeCount: 7, |
||||
lessons: [ |
||||
{ title: 'What are they?', type: 'lesson' }, |
||||
{ title: 'OVER and PARTITION BY', type: 'lesson' }, |
||||
{ title: 'Use of ORDER BY', type: 'lesson' }, |
||||
{ title: 'Ranking Functions', type: 'lesson' }, |
||||
{ title: 'Window Frames', type: 'lesson' }, |
||||
{ title: 'Window Functions Quiz', type: 'quiz' }, |
||||
{ title: 'Basic Sales Metrics', type: 'challenge' }, |
||||
{ title: 'Bestseller Comparison', type: 'challenge' }, |
||||
{ title: 'Author Category Sales', type: 'challenge' }, |
||||
{ title: 'Top Authors', type: 'challenge' }, |
||||
{ title: 'Price Tier Rankings', type: 'challenge' }, |
||||
{ title: 'Month-over-Month Sales', type: 'challenge' }, |
||||
{ title: 'Price Range Analysis', type: 'challenge' }, |
||||
], |
||||
}, |
||||
]; |
||||
|
||||
return ( |
||||
<div className="flex flex-grow flex-col items-center bg-gradient-to-b from-zinc-900 to-zinc-950 px-4 pb-52 pt-3 text-zinc-400 md:px-10 md:pt-8"> |
||||
<div className="flex w-full items-center justify-between"> |
||||
<a |
||||
href="https://roadmap.sh" |
||||
target="_blank" |
||||
className="opacity-20 transition-opacity hover:opacity-100" |
||||
> |
||||
<RoadmapLogoIcon /> |
||||
</a> |
||||
<AccountButton /> |
||||
</div> |
||||
<div className="relative mt-7 max-w-3xl text-left md:mt-20 md:text-center"> |
||||
<Spotlight className="left-[-170px] top-[-200px]" fill="#EAB308" /> |
||||
<div className="inline-block rounded-full bg-yellow-500/10 px-4 py-1.5 text-base text-yellow-500 md:px-6 md:py-2 md:text-lg"> |
||||
<span className="hidden sm:block"> |
||||
Complete Course to Master Practical SQL |
||||
</span> |
||||
<span className="block sm:hidden">Complete SQL Course</span> |
||||
</div> |
||||
|
||||
<h1 className="mt-5 text-4xl font-bold tracking-tight text-white md:mt-8 md:text-7xl"> |
||||
Master SQL <span className="hidden min-[384px]:inline">Queries</span> |
||||
<div className="mt-2.5 bg-gradient-to-r from-yellow-500 to-yellow-300 bg-clip-text text-transparent md:text-6xl lg:text-7xl"> |
||||
From Basic to Advanced |
||||
</div> |
||||
</h1> |
||||
|
||||
<p className="mx-auto my-5 max-w-2xl text-xl text-zinc-300 md:my-12 lg:text-2xl"> |
||||
A structured course to master database querying - perfect for |
||||
developers, data analysts, and anyone working with data. |
||||
</p> |
||||
|
||||
<div className="hidden flex-row items-center justify-center gap-5 md:flex"> |
||||
<div className="flex flex-row items-center gap-2"> |
||||
<ClipboardIcon className="size-6 text-yellow-600" /> |
||||
<span>55+ Lessons</span> |
||||
</div> |
||||
<div className="flex flex-row items-center gap-2"> |
||||
<FileQuestionIcon className="size-6 text-yellow-600" /> |
||||
<span>100+ Challenges</span> |
||||
</div> |
||||
<div className="flex flex-row items-center gap-2"> |
||||
<CodeIcon className="size-6 text-yellow-600" /> |
||||
<span>Integrated IDE</span> |
||||
</div> |
||||
<div className="flex flex-row items-center gap-2"> |
||||
<BrainIcon className="size-6 text-yellow-600" /> |
||||
<span>AI Tutor</span> |
||||
</div> |
||||
</div> |
||||
|
||||
<div className="mt-7 flex justify-start md:mt-12 md:justify-center"> |
||||
<BuyButton variant="main" /> |
||||
</div> |
||||
</div> |
||||
|
||||
<SectionHeader |
||||
title="Not your average SQL course" |
||||
description="Built around a text-based interactive approach and packed with practical challenges, this course stands out with features that make it truly unique." |
||||
className="mt-16 md:mt-32" |
||||
/> |
||||
|
||||
<div className="mx-auto mt-6 w-full max-w-5xl md:mt-10"> |
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 md:gap-4 lg:grid-cols-3"> |
||||
<CourseFeature |
||||
title="Textual Course" |
||||
icon={Eye} |
||||
imgUrl="https://assets.roadmap.sh/guest/textual-course.png" |
||||
description="Unlike video-based courses where you have to learn at the pace of the instructor, this course is text-based, allowing you to learn at your own pace." |
||||
/> |
||||
<CourseFeature |
||||
title="Coding Environment" |
||||
icon={CodeIcon} |
||||
imgUrl="https://assets.roadmap.sh/guest/coding-environment.png" |
||||
description="With the integrated IDE, you can practice your SQL queries in real-time, getting instant feedback on your results." |
||||
/> |
||||
<CourseFeature |
||||
title="Practical Challenges" |
||||
icon={FileQuestionIcon} |
||||
imgUrl="https://assets.roadmap.sh/guest/coding-challenges.png" |
||||
description="The course is packed with practical challenges and quizzes, allowing you to test your knowledge and skills." |
||||
/> |
||||
<CourseFeature |
||||
title="AI Instructor" |
||||
icon={BrainIcon} |
||||
description="Powerful AI tutor to help you with your queries, provide additional explanations and help if you get stuck." |
||||
imgUrl="https://assets.roadmap.sh/guest/ai-integration.png" |
||||
/> |
||||
<CourseFeature |
||||
title="Take Notes" |
||||
icon={ClipboardIcon} |
||||
description="The course allows you to take notes, where you can write down your thoughts and ideas. You can visit them later to review your progress." |
||||
imgUrl="https://assets.roadmap.sh/guest/course-notes.png" |
||||
/> |
||||
<CourseFeature |
||||
title="Completion Certificate" |
||||
icon={FileCheckIcon} |
||||
imgUrl="https://assets.roadmap.sh/guest/course-certificate.jpg" |
||||
description="The course provides a completion certificate, which you can share with your potential employers." |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
<div className="mt-7 w-full max-w-3xl text-left md:mt-9"> |
||||
<p className="text-lg leading-normal md:text-xl"> |
||||
Oh, and you get the{' '} |
||||
<span className="bg-gradient-to-r from-yellow-500 to-yellow-300 bg-clip-text text-transparent"> |
||||
lifetime access |
||||
</span>{' '} |
||||
to the course including all the future updates. Also, there is a |
||||
certificate of completion which you can share with your potential |
||||
employers. |
||||
</p> |
||||
</div> |
||||
|
||||
<SectionHeader |
||||
title="Course Overview" |
||||
description="The course is designed to help you go from SQL beginner to expert |
||||
through hands-on practice with real-world scenarios, mastering |
||||
everything from basic to complex queries." |
||||
className="mt-8 md:mt-24" |
||||
/> |
||||
|
||||
<div className="mt-8 w-full max-w-3xl space-y-4 md:mt-12"> |
||||
{chapters.map((chapter, index) => ( |
||||
<ChapterRow key={index} counter={index + 1} {...chapter} /> |
||||
))} |
||||
</div> |
||||
|
||||
<SectionHeader |
||||
title="About the Author" |
||||
className="mt-12 md:mt-24" |
||||
description={ |
||||
<div className="mt-2 md:mt-4 flex flex-col gap-4 md:gap-6 text-lg md:text-xl leading-[1.52]"> |
||||
<p> |
||||
I am Kamran Ahmed, an engineering leader with over a decade of |
||||
experience in the tech industry. Throughout my career I have built |
||||
and scaled software systems, architected complex data systems, and |
||||
worked with large amounts of data to create efficient solutions. |
||||
</p> |
||||
<p> |
||||
I am also the creator of{' '} |
||||
<a |
||||
href="https://roadmap.sh" |
||||
target="_blank" |
||||
className="text-yellow-400" |
||||
> |
||||
roadmap.sh |
||||
</a> |
||||
, a platform trusted by millions of developers to guide their |
||||
learning journeys. I love to simplify complex topics and make |
||||
learning practical and accessible. |
||||
</p> |
||||
<p> |
||||
In this course, I will share everything I have learned about SQL |
||||
from the basics to advanced concepts in a way that is easy to |
||||
understand and apply. Whether you are just starting or looking to |
||||
sharpen your skills, you are in the right place. |
||||
</p> |
||||
</div> |
||||
} |
||||
/> |
||||
|
||||
<CourseAuthor /> |
||||
|
||||
<FAQSection /> |
||||
|
||||
<FloatingPurchase /> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,29 @@ |
||||
import { cn } from '../../lib/classname'; |
||||
|
||||
type SectionHeaderProps = { |
||||
title: string; |
||||
description: string | React.ReactNode; |
||||
className?: string; |
||||
}; |
||||
|
||||
export function SectionHeader(props: SectionHeaderProps) { |
||||
const { title, description, className } = props; |
||||
|
||||
return ( |
||||
<div className={cn('mx-auto w-full mt-24 max-w-3xl', className)}> |
||||
<div className="relative w-full"> |
||||
<div className="flex items-center gap-6"> |
||||
<div className="inline-flex items-center rounded-xl "> |
||||
<span className="text-2xl md:text-3xl font-medium text-zinc-200">{title}</span> |
||||
</div> |
||||
<div className="h-[1px] flex-grow bg-gradient-to-r from-yellow-500/20 to-transparent"></div> |
||||
</div> |
||||
</div> |
||||
{typeof description === 'string' ? ( |
||||
<p className="mt-2 md:mt-5 text-lg md:text-xl text-zinc-400">{description}</p> |
||||
) : ( |
||||
description |
||||
)} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,57 @@ |
||||
import { cn } from '../../lib/classname'; |
||||
|
||||
type SpotlightProps = { |
||||
className?: string; |
||||
fill?: string; |
||||
}; |
||||
|
||||
export function Spotlight(props: SpotlightProps) { |
||||
const { className, fill } = props; |
||||
|
||||
return ( |
||||
<svg |
||||
className={cn( |
||||
'animate-spotlight pointer-events-none absolute z-[1] h-[169%] w-[238%] opacity-0 lg:w-[138%]', |
||||
className, |
||||
)} |
||||
xmlns="http://www.w3.org/2000/svg" |
||||
viewBox="0 0 3787 2842" |
||||
fill="none" |
||||
> |
||||
<g filter="url(#filter)"> |
||||
<ellipse |
||||
cx="1924.71" |
||||
cy="273.501" |
||||
rx="1924.71" |
||||
ry="273.501" |
||||
transform="matrix(-0.822377 -0.568943 -0.568943 0.822377 3631.88 2291.09)" |
||||
fill={fill || 'white'} |
||||
fillOpacity="0.21" |
||||
></ellipse> |
||||
</g> |
||||
<defs> |
||||
<filter |
||||
id="filter" |
||||
x="0.860352" |
||||
y="0.838989" |
||||
width="3785.16" |
||||
height="2840.26" |
||||
filterUnits="userSpaceOnUse" |
||||
colorInterpolationFilters="sRGB" |
||||
> |
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"></feFlood> |
||||
<feBlend |
||||
mode="normal" |
||||
in="SourceGraphic" |
||||
in2="BackgroundImageFix" |
||||
result="shape" |
||||
></feBlend> |
||||
<feGaussianBlur |
||||
stdDeviation="151" |
||||
result="effect1_foregroundBlur_1065_8" |
||||
></feGaussianBlur> |
||||
</filter> |
||||
</defs> |
||||
</svg> |
||||
); |
||||
} |
@ -0,0 +1,16 @@ |
||||
--- |
||||
import { SQLCoursePage } from '../../components/SQLCourse/SQLCoursePage.tsx'; |
||||
import SkeletonLayout from '../../layouts/SkeletonLayout.astro'; |
||||
--- |
||||
|
||||
<SkeletonLayout |
||||
title='Master SQL' |
||||
briefTitle='Learn SQL from the ground up' |
||||
ogImageUrl='https://assets.roadmap.sh/guest/sql-course-bjc53.png' |
||||
description='Learn SQL from the ground up. This course covers the basics of SQL, intermediate concepts, and advanced topics.' |
||||
keywords={['sql', 'database', 'database management', 'database administration']} |
||||
canonicalUrl='/courses/sql' |
||||
noIndex={true} |
||||
> |
||||
<SQLCoursePage client:load /> |
||||
</SkeletonLayout> |
@ -0,0 +1,25 @@ |
||||
import { queryOptions } from '@tanstack/react-query'; |
||||
import { httpGet } from '../lib/query-http'; |
||||
|
||||
type CoursePriceParams = { |
||||
courseSlug: string; |
||||
}; |
||||
|
||||
type CoursePriceResponse = { |
||||
flag: string; |
||||
fullPrice: number; |
||||
regionalPrice: number; |
||||
regionalDiscountPercentage: number; |
||||
isEligibleForDiscount: boolean; |
||||
}; |
||||
|
||||
export function coursePriceOptions(params: CoursePriceParams) { |
||||
return queryOptions({ |
||||
queryKey: ['course-price', params], |
||||
queryFn: async () => { |
||||
return httpGet<CoursePriceResponse>( |
||||
`/v1-course-price/${params.courseSlug}`, |
||||
); |
||||
}, |
||||
}); |
||||
} |
@ -0,0 +1,41 @@ |
||||
import { queryOptions } from '@tanstack/react-query'; |
||||
import { isLoggedIn } from '../lib/jwt'; |
||||
import { httpGet } from '../lib/query-http'; |
||||
|
||||
export interface CourseProgressDocument { |
||||
_id: string; |
||||
userId: string; |
||||
courseId: string; |
||||
completed: { |
||||
chapterId: string; |
||||
lessonId: string; |
||||
completedAt: Date; |
||||
}[]; |
||||
review?: { |
||||
rating: number; |
||||
feedback?: string; |
||||
}; |
||||
|
||||
enrolledAt?: Date; |
||||
completedAt?: Date; |
||||
createdAt: Date; |
||||
updatedAt: Date; |
||||
} |
||||
|
||||
export type CourseProgressResponse = Pick< |
||||
CourseProgressDocument, |
||||
'completed' | 'completedAt' | 'review' | 'enrolledAt' |
||||
>; |
||||
|
||||
export function courseProgressOptions(courseSlug: string) { |
||||
return queryOptions({ |
||||
queryKey: ['course-progress', courseSlug], |
||||
retryOnMount: false, |
||||
queryFn: async () => { |
||||
return httpGet<CourseProgressResponse>( |
||||
`/v1-course-progress/${courseSlug}`, |
||||
); |
||||
}, |
||||
enabled: !!isLoggedIn(), |
||||
}); |
||||
} |
Loading…
Reference in new issue