1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-08-29 12:10:22 +02:00

feat: course landing page redesign (#8864)

* Add alt course page

* wip

* wi

* wip

* wip

* wip

* wip

* wip

* feat: review carousel

* wip: update meet instructor ui

* wip

* wip

* wip

* wip

* wip

* Improve upper banner from course page

* Improve upper banner from course page

* Fix height issue of image

* Improve testimonial design

* Improve review carousel

* Update

* Improve meet your instructor

* Improve overall page design

* Reuse lessons list

* Improve variant page

* Responsive banner

* Purchase banner

* Add new page

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
This commit is contained in:
Arik Chakma
2025-07-01 17:05:23 +06:00
committed by GitHub
parent 50c4d41fde
commit 19b480402c
16 changed files with 1947 additions and 207 deletions

View File

@@ -39,212 +39,210 @@ type ChapterData = {
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' },
],
},
];
export const sqlCourseChapters: 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' },
],
},
];
export function SQLCoursePage() {
return (
<>
<CourseDiscountBanner />
@@ -379,7 +377,7 @@ export function SQLCoursePage() {
/>
<div className="mt-8 w-full max-w-3xl space-y-4 md:mt-12">
{chapters.map((chapter, index) => (
{sqlCourseChapters.map((chapter, index) => (
<ChapterRow key={index} counter={index + 1} {...chapter} />
))}
</div>

View File

@@ -0,0 +1,68 @@
import { AwardIcon, InfoIcon } from 'lucide-react';
import { GitHubIcon } from '../ReactIcons/GitHubIcon';
import { Popover, PopoverContent, PopoverTrigger } from '../Popover';
export function AuthorCredentials() {
return (
<div className="flex items-center gap-3 text-white lg:mt-auto">
<img
src="https://assets.roadmap.sh/guest/kamran-course-pf-agibf.jpg"
className="aspect-[4/5] h-[110px] w-[88px] rounded-xl object-cover shadow-md"
alt="Kamran Ahmed"
/>
<div className="flex flex-col gap-2">
<div>
<p className="text-xl font-medium transition-colors duration-200">
by Kamran Ahmed
</p>
<p className="mt-0.5 text-sm text-gray-400">
Your teacher for this course
</p>
</div>
<div className="flex flex-col gap-1">
<a
href="https://github.com/kamranahmedse"
target="_blank"
rel="noopener noreferrer"
className="inline-flex cursor-pointer items-center gap-1 rounded-md bg-gradient-to-r from-yellow-500/15 to-orange-500/15 px-2 py-1.5 backdrop-blur-sm transition-all duration-200 hover:border-yellow-400/40 hover:from-yellow-500/25 hover:to-orange-500/25"
>
<GitHubIcon className="size-3 text-yellow-400" />
<span className="text-xs font-medium text-yellow-200">
#2 Most Starred Developer
</span>
</a>
<Popover>
<PopoverTrigger asChild>
<div className="inline-flex cursor-pointer items-center gap-1 rounded-md bg-gradient-to-r from-yellow-500/15 to-orange-500/15 px-2 py-1.5 backdrop-blur-sm transition-all duration-200 hover:border-yellow-400/40 hover:from-yellow-500/25 hover:to-orange-500/25">
<AwardIcon className="size-3 text-yellow-400" />
<span className="text-xs font-medium text-yellow-200">
Founder roadmap.sh
</span>
<InfoIcon className="ml-auto size-3 text-yellow-400/70 hover:text-yellow-300" />
</div>
</PopoverTrigger>
<PopoverContent
className="border-zinc-700 bg-zinc-900 px-2.5 text-sm text-zinc-200"
side="top"
align="start"
>
<a
href="/"
className="text-blue-400 underline hover:text-blue-500 focus:text-blue-500"
>
roadmap.sh
</a>{' '}
provides community-curated roadmaps, study plans, paths, and
resources for developers and IT professionals. Serving 2M+
registered users, it is the 6th most-starred open source project
on GitHub
</PopoverContent>
</Popover>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,381 @@
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';
floatingClassName?: string;
};
export function BuyButton(props: BuyButtonProps) {
const { variant = 'main', floatingClassName } = 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<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';
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 && (
<CourseLoginPopup onClose={() => setIsLoginPopupOpen(false)} />
);
const mainCouponAlert = (
<div
data-coupon-alert
className="absolute top-1/2 -left-59 z-50 hidden -translate-y-1/2 md:block"
>
<div className="relative flex items-center rounded-xl bg-yellow-50 px-3 py-1.5 shadow-lg">
<div className="absolute top-1/2 -right-0.5 h-1.5 w-1.5 -translate-y-1/2 rotate-45 bg-yellow-50"></div>
<span className="text-xs font-bold text-black">
🎁 30% OFF with code{' '}
<button
onClick={() => {
copyText(sqlCouponCode);
}}
className="inline-block rounded bg-black px-1.5 py-0.5 font-mono text-white hover:opacity-75"
>
{sqlCouponCode}
{isCopied && (
<CheckIcon className="relative -top-[2px] ml-1.5 inline-block size-3" />
)}
{!isCopied && (
<CopyIcon className="relative -top-px ml-1.5 inline-block size-3 text-gray-500" />
)}
</button>
</span>
</div>
</div>
);
if (variant === 'main') {
return (
<>
{courseLoginPopup}
<div className="relative flex w-full flex-col items-center gap-2 md:w-auto">
{isVideoModalOpen && (
<VideoModal
videoId="6S1CcF-ngeQ"
onClose={() => setIsVideoModalOpen(false)}
/>
)}
<div className="relative flex flex-col gap-2 md:flex-row md:gap-0">
{!isLoadingPricing && !isAlreadyEnrolled && mainCouponAlert}
<button
onClick={onBuyClick}
disabled={isLoadingPricing}
className={cn(
'group relative mr-2 inline-flex w-full min-w-[235px] items-center justify-center overflow-hidden rounded-xl bg-linear-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-hidden active:ring-0 md:w-auto md:rounded-full md:text-lg',
(isLoadingPricing || isCreatingCheckoutSession) &&
'striped-loader-yellow pointer-events-none mr-4 scale-105 bg-yellow-500',
)}
>
{isLoadingPricing ? (
<span className="relative flex items-center gap-2">&nbsp;</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>
<button
onClick={onReadSampleClick}
data-demo-button
className={cn(
'group relative hidden items-center justify-center overflow-hidden rounded-xl border border-yellow-500/30 bg-transparent px-6 py-3 text-base font-medium text-yellow-500 transition-all duration-300 ease-out hover:bg-yellow-500/10 focus:outline-hidden active:ring-0 md:rounded-full',
{
'hidden lg:inline-flex':
!isLoadingPricing && !isAlreadyEnrolled,
},
)}
>
<span className="relative flex items-center gap-2">
<MousePointerClick className="h-5 w-5" />
Access Demo
</span>
</button>
</div>
{!isLoadingPricing && (
<span className="absolute top-full z-50 flex w-max translate-y-4 flex-row items-center justify-center text-sm text-yellow-400">
Lifetime access <span className="mx-2">&middot;</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 (
<>
{courseLoginPopup}
<div
className={cn(
'relative flex flex-col items-center gap-2',
floatingClassName,
)}
>
<button
onClick={onBuyClick}
disabled={isLoadingPricing}
className={cn(
'group relative inline-flex min-w-[220px] items-center justify-center overflow-hidden rounded-full bg-[rgb(168,85,247)] px-8 py-3 font-medium text-white transition-all duration-300 ease-out hover:scale-[1.02] hover:shadow-[0_0_30px_rgba(168,85,247,0.4)] focus:outline-hidden',
(isLoadingPricing || isCreatingCheckoutSession) &&
'striped-loader pointer-events-none bg-[rgb(168,85,247)]',
)}
>
{isLoadingPricing ? (
<span className="relative flex items-center gap-2">&nbsp;</span>
) : isAlreadyEnrolled ? (
<span className="relative flex items-center gap-2">
Start Learning
</span>
) : (
<span className="relative flex items-center gap-2">
<span className="hidden md:inline">Start learning now</span>
<span className="inline md:hidden">Start now</span>- $
{coursePricing?.regionalPrice}
<ArrowRightIcon className="h-5 w-5 transition-transform duration-300 ease-out group-hover:translate-x-1" />
</span>
)}
</button>
</div>
</>
);
}

View File

@@ -0,0 +1,140 @@
import { ChevronDown, BookIcon, CodeIcon, CircleDot } from 'lucide-react';
import { cn } from '../../lib/classname';
import { 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(false);
const regularLessons = lessons.filter((l) => l.type === 'lesson');
const challenges = lessons.filter((l) =>
['challenge', 'quiz'].includes(l.type),
);
return (
<div
className={cn('group relative overflow-hidden select-none', className)}
>
<div
role="button"
onClick={() => isExpandable && setIsExpanded(!isExpanded)}
className={cn(
'relative rounded-xl border border-zinc-800 bg-zinc-800 p-6',
'bg-linear-to-br from-zinc-900/90 via-zinc-900/70 to-zinc-900/50',
!isExpanded &&
'hover:bg-linear-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 && 'cursor-pointer rounded-b-none border-b-0',
)}
>
<div className="flex items-start gap-4">
<div className="hidden shrink-0 md:block">
<div className="rounded-full bg-yellow-500/10 p-3">{icon}</div>
</div>
<div className="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="shrink-0 rounded-full bg-zinc-800/80 p-2 text-zinc-400 group-hover:bg-zinc-800 group-hover:text-zinc-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-linear-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 tracking-wider text-zinc-500 uppercase">
Lessons
</h4>
<div className="space-y-3">
{regularLessons.map((lesson, index) => (
<div
key={index}
className="flex items-center gap-3 text-zinc-400 cursor-text"
>
<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 tracking-wider text-zinc-500 uppercase">
Exercises
</h4>
<div className="space-y-3">
{challenges.map((challenge, index) => (
<div
key={index}
className="flex items-center gap-3 text-zinc-400 cursor-text"
>
{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>
);
}

View File

@@ -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 (
<div
data-coupon-alert
className={cn(
'sticky top-0 z-[999] flex w-full items-center justify-center overflow-hidden bg-yellow-500 text-center text-sm font-medium transition-[height] duration-300',
isVisible ? 'h-[34px] sm:h-[35px]' : 'h-0',
)}
>
<span className="mr-1 hidden font-bold sm:block">
🎁 Limited time offer :
</span>
Get 30% off using{' '}
<button
onClick={() => {
copyText(sqlCouponCode);
}}
className={cn(
'animate-wiggle ml-1 inline-block cursor-pointer rounded-md border border-dashed border-black bg-gray-900 px-1.5 py-[3px] text-xs text-white [animation-delay:0.25s]',
isCopied && 'bg-gray-900 text-white',
)}
>
<span className="mr-1">Coupon code :</span>
{sqlCouponCode}
{isCopied && (
<CheckIcon
className="relative -top-[2px] ml-1.5 inline-block size-3"
strokeWidth={2.5}
/>
)}
{!isCopied && (
<CopyIcon
className="relative -top-[2px] ml-1.5 inline-block size-3"
strokeWidth={2.5}
/>
)}
</button>
<button
onClick={() => {
setIsVisible(false);
}}
className="absolute top-1/2 right-4 hidden -translate-y-1/2 rounded-md px-1.5 py-1.5 hover:bg-yellow-600 hover:text-black sm:block"
>
<XIcon className="size-4" />
</button>
</div>
);
}

View File

@@ -0,0 +1,149 @@
import {
BookIcon,
BrainIcon,
ClipboardIcon,
CodeIcon,
FileCheckIcon,
FileQuestionIcon,
MinusIcon,
PlusIcon,
} from 'lucide-react';
import { SectionHeader } from './SectionHeader';
import { useState } from 'react';
import { cn } from '../../lib/classname';
type Feature = {
title: string;
description: string;
icon: React.ElementType;
imgUrl: string;
};
export function CourseFeatures() {
const features: Feature[] = [
{
title: 'AI Tutor',
description:
'Powerful AI tutor to help you with your queries, provide additional explanations and help if you get stuck.',
icon: BrainIcon,
imgUrl: 'https://assets.roadmap.sh/guest/ai-integration.png',
},
{
title: 'Real-world Challenges',
description:
'The course is packed with practical challenges and quizzes, allowing you to test your knowledge and skills.',
icon: FileQuestionIcon,
imgUrl: 'https://assets.roadmap.sh/guest/coding-challenges.png',
},
{
title: 'Coding Environment',
description:
'With the integrated IDE, you can practice your SQL queries in real-time, getting instant feedback on your results.',
icon: CodeIcon,
imgUrl: 'https://assets.roadmap.sh/guest/coding-environment.png',
},
{
title: 'Textual Course',
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.',
icon: BookIcon,
imgUrl: 'https://assets.roadmap.sh/guest/textual-course.png',
},
{
title: 'Take Notes',
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.',
icon: ClipboardIcon,
imgUrl: 'https://assets.roadmap.sh/guest/course-notes.png',
},
{
title: 'Completion Certificate',
description:
'The course provides a completion certificate, which you can share with your potential employers.',
icon: FileCheckIcon,
imgUrl: 'https://assets.roadmap.sh/guest/course-certificate.jpg',
},
];
const [expandedFeatureIndex, setExpandedFeatureIndex] = useState<
number | null
>(0);
return (
<div>
<SectionHeader
title="Not your average SQL course"
description="Built around a text-based interactive approach and packed with practical challenges, this comprehensive SQL bootcamp stands out with features that make it truly unique."
/>
<div className="mx-auto mt-10 w-full max-w-2xl divide-y divide-zinc-800 overflow-hidden rounded-xl border border-zinc-800">
{features.map((feature, index) => (
<CourseFeature
key={feature.title}
{...feature}
isExpanded={expandedFeatureIndex === index}
onExpand={() =>
setExpandedFeatureIndex(
expandedFeatureIndex === index ? null : index,
)
}
/>
))}
</div>
</div>
);
}
type CourseFeatureProps = Feature & {
isExpanded?: boolean;
onExpand?: () => void;
};
function CourseFeature(props: CourseFeatureProps) {
const {
title,
description,
icon: Icon,
imgUrl,
isExpanded,
onExpand,
} = props;
return (
<div>
<button
className={cn(
'flex w-full items-center justify-between gap-2 px-5 py-3 hover:bg-transparent',
!isExpanded && 'bg-zinc-900',
)}
onClick={onExpand}
>
<div className="flex items-center gap-2">
<Icon className="h-5 w-5 shrink-0 text-yellow-600" />
<h3 className={cn('text-lg', isExpanded && 'text-zinc-200')}>
{title}
</h3>
</div>
<div className="text-zinc-400 hover:text-zinc-300">
{isExpanded ? (
<MinusIcon className="h-5 w-5" />
) : (
<PlusIcon className="h-5 w-5" />
)}
</div>
</button>
{isExpanded && (
<div className="grid gap-4 px-5 py-3 sm:grid-cols-2">
<p className="text-lg text-balance text-white">{description}</p>
<img
src={imgUrl}
alt={title}
className="h-full w-full rounded-lg sm:order-2"
/>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,115 @@
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 gap-2 p-4 text-left md:p-6"
>
<h3 className="text-lg font-normal text-balance text-white md:text-xl">
{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 leading-relaxed md:text-lg">
<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:
"You can request a refund within 30 days of purchase by emailing info@roadmap.sh. The refund amount will be prorated based on when you request it. For example, if you request a refund 15 days after purchase, you'll receive 50% back. I'd also love to hear your feedback to improve the course.",
},
{
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={null}
className="mt-10 md:mt-24"
/>
<div className="mx-auto mt-6 w-full max-w-3xl space-y-2 md:mt-8 md:space-y-6">
{faqs.map((faq, index) => (
<FAQRow key={index} {...faq} />
))}
</div>
</>
);
}

View File

@@ -0,0 +1,89 @@
import { AwardIcon, TrophyIcon } from 'lucide-react';
import { GitHubIcon } from '../ReactIcons/GitHubIcon';
export function MeetYourInstructor() {
const features = [
{
icon: TrophyIcon,
text: 'Multiple GitHub Star Awards'
},
{
icon: GitHubIcon,
text: '#2 Most Starred Developer'
},
{
icon: AwardIcon,
text: 'Google Developer Expert'
},
{
icon: AwardIcon,
text: '2M+ roadmap.sh users'
}
];
return (
<div className="mx-auto mt-14 max-w-4xl">
<div className="rounded-3xl bg-gradient-to-br from-yellow-500/20 via-yellow-500/10 to-transparent p-8 md:p-12">
<h4 className="mb-2 text-center text-xl font-medium text-zinc-200 md:text-2xl">
Meet your instructor
</h4>
<div className="mb-12 text-center text-3xl font-bold text-yellow-400 md:text-4xl">
Kamran Ahmed
</div>
<div className="flex flex-col gap-12 lg:flex-row lg:gap-16">
<div className="flex shrink-0 flex-col items-center">
<div className="relative">
<img
src="https://assets.roadmap.sh/guest/kamran-lqjta.jpeg"
alt="Kamran Ahmed"
className="h-40 w-40 rounded-full object-cover ring-4 ring-yellow-500/40 transition-all duration-300 hover:ring-yellow-500/60"
/>
</div>
<h5 className="mt-6 text-xl font-semibold text-zinc-100">
Kamran Ahmed
</h5>
<span className="text-yellow-400">
Founder of roadmap.sh
</span>
</div>
<div className="flex-1 space-y-8">
<div className="space-y-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{features.map((feature, index) => {
const IconComponent = feature.icon;
return (
<div key={index} className="flex items-center gap-3 rounded-lg border border-yellow-500/20 bg-gradient-to-r from-yellow-500/10 to-yellow-500/5 p-3">
<IconComponent className="size-4 shrink-0 text-yellow-400" />
<span className="text-sm font-medium text-zinc-300">
{feature.text}
</span>
</div>
);
})}
</div>
</div>
<div className="space-y-6">
<div className="prose prose-zinc max-w-none">
<p className="m-0 text-xl leading-relaxed text-zinc-300">
Kamran is the creator of roadmap.sh (2M+ registered users!)
and an engineering leader with over a decade of experience in
the tech industry. Throughout his career he's built and scaled
software systems, designed complex data systems, and worked
with large amounts of data to create efficient solutions.
</p>
<p className="m-0 text-xl leading-relaxed text-zinc-300">
This hands-on, AI-assisted course is his distilled blueprint
on how to master SQL queries, from beginner to advanced.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import { useState } from 'react';
import { Play } from 'lucide-react';
import { VideoModal } from '../VideoModal';
export function PlatformDemo() {
const [isVideoModalOpen, setIsVideoModalOpen] = useState(false);
return (
<>
{isVideoModalOpen && (
<VideoModal
videoId="6S1CcF-ngeQ"
onClose={() => setIsVideoModalOpen(false)}
/>
)}
<div className="relative aspect-video w-full grow overflow-hidden rounded-lg">
<img
src="https://assets.roadmap.sh/guest/course-environment-87jg8.png"
alt="Course Environment"
className="absolute inset-0 h-full w-full object-cover"
/>
<div
onClick={() => setIsVideoModalOpen(true)}
className="group absolute inset-0 flex cursor-pointer items-center justify-center bg-black/40 transition-all hover:bg-black/50"
>
<div className="flex size-12 items-center justify-center rounded-full bg-white/90 transition-transform group-hover:scale-105 lg:size-16">
<Play className="ml-1 fill-current text-black lg:size-8" />
</div>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,85 @@
import { CheckIcon, Star } from 'lucide-react';
import { BuyButton } from './BuyButton';
import { Rating } from '../Rating/Rating';
import { cn } from '../../lib/classname';
import { useEffect, useRef, useState } from 'react';
export function PurchaseBanner() {
const bannerRef = useRef<HTMLDivElement>(null);
const [isOutOfView, setIsOutOfView] = useState(false);
useEffect(() => {
const handleScroll = () => {
if (!bannerRef.current) return;
const bannerRect = bannerRef.current.getBoundingClientRect();
const bannerBottom = bannerRect.bottom;
// Banner is out of view when its bottom is above the viewport
setIsOutOfView(bannerBottom < 0);
};
window.addEventListener('scroll', handleScroll);
handleScroll(); // Check initial state
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
const Banner = (props: {
className?: string;
ref?: React.RefObject<HTMLDivElement | null>;
}) => {
return (
<div
ref={props.ref}
className={cn(
'top-4 z-50 mt-16.5 flex w-full flex-col gap-4 rounded-2xl bg-yellow-950 p-5 shadow-lg ring-1 ring-yellow-500/40 lg:sticky lg:flex-row lg:items-center lg:justify-between',
props.className,
)}
>
<div className="order-3 flex w-full flex-col items-center gap-2 lg:order-0 lg:w-fit lg:items-start">
{['7-Day Money-Back Guarantee', 'Lifetime access & updates'].map(
(text, index) => (
<span
key={index}
className="inline-flex items-center gap-1.5 text-yellow-500"
>
<CheckIcon className="size-5 stroke-[2.5]" />
{text}
</span>
),
)}
</div>
<div className="order-2 lg:order-0">
<BuyButton
variant="floating"
floatingClassName="translate-x-0 lg:-translate-x-5"
/>
</div>
<div className="flex flex-col items-center gap-2">
<Rating rating={4.9} className="hidden lg:flex" />
<span className="flex items-center gap-1 text-base font-semibold text-yellow-500">
<Star className="block size-4 fill-current lg:hidden" />
4.9 avg. Review
</span>
</div>
</div>
);
};
return (
<>
<Banner ref={bannerRef} />
<Banner
className={cn(
'fixed top-[unset] right-0 bottom-0 left-0 rounded-none lg:hidden',
isOutOfView ? 'flex' : 'hidden',
)}
/>
</>
);
}

View File

@@ -0,0 +1,312 @@
import {
ChevronLeftIcon,
ChevronRightIcon,
StarIcon,
User2Icon,
} from 'lucide-react';
import { useLayoutEffect, useMemo, useState } from 'react';
import { markdownToHtml } from '../../lib/markdown';
import { getTailwindScreenDimension } from '../../lib/is-mobile';
import { cn } from '../../lib/classname';
type Review = {
name: string;
role: string;
rating: number;
text: string | string[];
avatarUrl?: string;
isProminent?: boolean;
isSecondaryProminent?: boolean;
companyName?: string;
companyLogo?: string;
};
export function ReviewCarousel() {
const reviews: Review[] = [
{
companyName: 'Cloud Camping',
companyLogo: 'https://assets.roadmap.sh/guest/cloudcamping-mcpl6.jpeg',
name: 'Robin Wieruch',
role: 'Author - Multiple Best-Sellers',
rating: 5,
text: [
'Kamran has been in the **educative space for a long time**, and it shows in the way he teaches SQL: clear, structured, and straight to the point.',
"Even if you've used SQL before, this **course will fill in gaps you didn't even realize you had**. Get ready to level up your database skills!",
],
avatarUrl: 'https://assets.roadmap.sh/guest/robin.jpeg',
isProminent: true,
},
{
companyName: 'Hack Mamba',
companyLogo: 'https://assets.roadmap.sh/guest/hackmbamba-h0ivr.jpeg',
name: 'William Imoh',
role: 'Founder and Data Enthusiast',
rating: 5,
text: [
'I bought this course for the advanced chapters but ended up completing the entire course. I learned a lot of new things and it was **well worth the investment**.',
'No matter your SQL experience, this course is **a must-have** if you want to level up your SQL and data analysis skills. Highly recommended!',
],
avatarUrl: 'https://assets.roadmap.sh/guest/william-imoh-sd2dk.jpg',
isProminent: true,
},
{
companyName: 'GlobalLogic',
companyLogo:
'https://assets.roadmap.sh/guest/globallogic_logo-3m3ho.jpeg',
name: 'Martina Milagros',
role: 'Software Engineer',
rating: 5,
text: [
'Thanks to Kamran Ahmed for the **incredible Master SQL course!** I truly appreciate the way you break down complex topics with such clarity and ease.',
'**Highly recommend this course** to anyone looking to level up their SQL game!',
],
avatarUrl: 'https://assets.roadmap.sh/guest/martina-awc4x.jpeg',
},
{
companyName: 'Cisco',
companyLogo: 'https://assets.roadmap.sh/guest/cisco-gyw5b.jpeg',
name: 'Tomáš Janků',
role: 'Sr. Software Engineer',
rating: 5,
text: "The course and it's interactivity is excellent and I'd honestly say it's **one of the best** on the SQL theme I've seen out there.",
avatarUrl: 'https://assets.roadmap.sh/guest/tomas-janku-6bg89.jpeg',
},
{
companyName: 'Beyond Works',
companyLogo:
'https://assets.roadmap.sh/guest/beyondwordsio_logo-xia4m.jpeg',
name: 'Gourav Khunger',
role: 'Software Engineer',
rating: 5,
text: [
'This course was **absolutely brilliant!** The integrated database environment to practice what I learned was the best part. Being able to **run queries immediately** and see results in real-time made everything click so much faster than traditional learning methods.',
],
avatarUrl: 'https://assets.roadmap.sh/guest/gourav-h2f3a.png',
},
{
companyName: 'xpertSea',
companyLogo: 'https://assets.roadmap.sh/guest/xpertsea-y24hu.jpeg',
name: 'Meabed',
role: 'CTO',
rating: 5,
text: 'Kamran has **clearly put a lot of thought** into this course. The content, structure and exercises were all great.',
avatarUrl: 'https://assets.roadmap.sh/guest/meabed-fu83q.jpeg',
},
{
companyName: 'Powersoft19',
companyLogo: 'https://assets.roadmap.sh/guest/powersoft19-sk4t1.jpeg',
name: 'Mohsin Aheer',
role: 'Sr. Software Engineer',
rating: 5,
text: 'I already knew SQL but this course **taught me a bunch of new things.** Practical examples and challenges were great. Highly recommended!',
avatarUrl: 'https://assets.roadmap.sh/guest/mohsinaheer-szchu.jpeg',
},
{
companyName: 'xpertSea',
companyLogo: 'https://assets.roadmap.sh/guest/xpertsea-y24hu.jpeg',
name: 'Zeeshan',
role: 'Sr. Software Engineer',
rating: 5,
text: 'Loved the teaching style and the way the course was structured. The **AI tutor was a great help** when I wanted some extra help.',
avatarUrl: 'https://assets.roadmap.sh/guest/ziishaned-qjepj.png',
},
{
companyName: 'University of Regensburg',
companyLogo:
'https://assets.roadmap.sh/guest/university_of_regensburg_logo-01784.jpeg',
name: 'Faisal Ahsan',
role: 'Software Engineer',
rating: 5,
text: 'The course and the learning experience was great. What I really liked was the **no-fluff explanations** and **practical examples**.',
avatarUrl: 'https://assets.roadmap.sh/guest/faisal-q78p2.jpeg',
},
{
companyName: 'xpertSea',
companyLogo: 'https://assets.roadmap.sh/guest/xpertsea-y24hu.jpeg',
name: 'Adnan Ahmed',
role: 'Engineering Manager',
rating: 5,
text: 'Having the integrated IDE made a huge difference. Being able to **immediately practice** what I learned was **invaluable**.',
avatarUrl: 'https://assets.roadmap.sh/guest/idnan-fzps5.jpeg',
},
{
name: 'Kalvin Chakma',
role: 'Jr. Software Engineer',
rating: 5,
text: "Best SQL course I've taken. The progression from basic to advanced concepts is **well thought out**, and the challenges are **excellent**.",
avatarUrl: 'https://assets.roadmap.sh/guest/kalvin-d65ol.jpeg',
},
];
const [batchSize, setBatchSize] = useState(3);
const maxBatchNumber = Math.ceil(reviews.length / batchSize);
const [currentBatchNumber, setCurrentBatchNumber] = useState(0);
const currentBatch = useMemo(() => {
const result = reviews.slice(
currentBatchNumber * batchSize,
(currentBatchNumber + 1) * batchSize,
);
if (result.length < batchSize) {
const remaining = batchSize - result.length;
return [...result, ...reviews.slice(0, remaining)];
}
return result;
}, [currentBatchNumber, batchSize]);
const handleNextBatch = () => {
setCurrentBatchNumber((prev) => (prev + 1) % maxBatchNumber);
};
const handlePreviousBatch = () => {
setCurrentBatchNumber(
(prev) => (prev - 1 + maxBatchNumber) % maxBatchNumber,
);
};
useLayoutEffect(() => {
const size = getTailwindScreenDimension();
if (size === '2xl') {
setBatchSize(3);
} else if (size === 'xl' || size === 'lg') {
setBatchSize(2);
} else {
setBatchSize(1);
}
}, []);
return (
<div className="mx-auto mt-24 w-full max-w-5xl">
<h3 className="text-center text-2xl font-medium text-zinc-200 md:text-3xl">
What other learners said
</h3>
<div className="mt-10 mb-6 flex items-center justify-end gap-2 xl:hidden">
<NavigateButton
onClick={handlePreviousBatch}
icon={<ChevronLeftIcon className="h-4 w-4 stroke-[2.5] text-white" />}
/>
<NavigateButton
onClick={handleNextBatch}
icon={
<ChevronRightIcon className="h-4 w-4 stroke-[2.5] text-white" />
}
/>
</div>
<div className="relative mt-0 flex gap-4 xl:mt-10">
<div className="absolute inset-y-0 -left-2 hidden shrink-0 -translate-x-full flex-col items-center justify-center xl:flex">
<NavigateButton
onClick={handlePreviousBatch}
icon={
<ChevronLeftIcon className="h-5 w-5 stroke-[2.5] text-white" />
}
/>
</div>
<div className="grid auto-rows-fr grid-cols-1 items-stretch gap-2 sm:grid-cols-2 xl:grid-cols-3">
{currentBatch.map((review, index) => (
<div
key={index}
className={cn(
'review-testimonial relative flex h-full flex-col overflow-hidden rounded-2xl bg-linear-to-br from-yellow-500/10 via-yellow-500/5 to-transparent p-8 backdrop-blur-sm lg:min-h-[456px] [&_strong]:font-normal [&_strong]:text-yellow-300/70',
index === 2 && batchSize === 3 && 'hidden xl:flex',
index === 1 && batchSize === 3 && 'hidden lg:flex',
)}
>
<div className="absolute -top-8 -right-8 h-32 w-32 rounded-full bg-yellow-500/5" />
<div className="mb-4 flex items-center gap-4">
{review.avatarUrl && (
<img
src={review.avatarUrl}
alt={review.name}
className="h-16 w-16 rounded-full border-2 border-yellow-500/20 object-cover"
/>
)}
{!review.avatarUrl && (
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-zinc-800">
<User2Icon className="h-8 w-8 text-zinc-400" />
</div>
)}
<div>
<h3 className="text-lg font-semibold text-zinc-100">
{review.name}
</h3>
<p className="text-sm text-yellow-500/70">{review.role}</p>
<div className="mt-1 flex">
{Array.from({ length: review.rating }).map((_, i) => (
<StarIcon
key={i}
className="h-4 w-4 fill-yellow-500 text-yellow-500"
/>
))}
</div>
</div>
</div>
<div className="flex flex-1 flex-col gap-3">
{(typeof review.text === 'string'
? [review.text]
: review.text
).map((text, index) => (
<p
key={index}
className="text-zinc-400 [&_strong]:font-semibold! [&_strong]:text-white!"
dangerouslySetInnerHTML={{
__html: markdownToHtml(text),
}}
/>
))}
</div>
{review?.companyName && (
<div className="mt-10 flex items-center gap-3">
{review?.companyLogo && (
<img
src={review?.companyLogo}
alt={review?.companyName}
className="h-10 w-10 rounded-lg border border-yellow-500/20 object-cover"
/>
)}
<div className="text-lg font-medium text-zinc-200">
{review?.companyName}
</div>
</div>
)}
</div>
))}
</div>
<div className="absolute inset-y-0 -right-2 hidden shrink-0 translate-x-full flex-col items-center justify-center xl:flex">
<NavigateButton
onClick={handleNextBatch}
icon={
<ChevronRightIcon className="h-5 w-5 stroke-[2.5] text-white" />
}
/>
</div>
</div>
</div>
);
}
type NavigateButtonProps = {
onClick: () => void;
icon: React.ReactNode;
};
function NavigateButton(props: NavigateButtonProps) {
const { onClick, icon } = props;
return (
<button
onClick={onClick}
className="flex items-center justify-center rounded-full bg-zinc-800 p-2 hover:bg-zinc-700"
>
{icon}
</button>
);
}

View File

@@ -0,0 +1,27 @@
import { InfoIcon } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '../Popover';
export function RoadmapDetailsPopover() {
return (
<Popover>
<PopoverTrigger>
<InfoIcon className="size-4 text-yellow-500/80 hover:text-yellow-500" />
</PopoverTrigger>
<PopoverContent
className="border-zinc-700 bg-zinc-900 px-2.5 text-sm text-zinc-200"
side="top"
align="start"
>
<a
href="/"
className="text-blue-400 underline hover:text-blue-500 focus:text-blue-500"
>
roadmap.sh
</a>{' '}
provides community-curated roadmaps, study plans, paths, and resources
for developers and IT professionals. Serving 2M+ registered users, it is
the 6th most-starred open source project on GitHub
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,125 @@
import {
BrainIcon,
CodeIcon,
FileQuestionIcon,
NotebookTextIcon,
} from 'lucide-react';
import { Spotlight } from '../SQLCourse/Spotlight';
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
import { AuthorCredentials } from './AuthorCredentials';
import { PlatformDemo } from './PlatformDemo';
import { PurchaseBanner } from './PurchaseBanner';
import { ReviewCarousel } from './ReviewCarousel';
import { CourseFeatures } from './CourseFeatures';
import { MeetYourInstructor } from './MeetYourInstructor';
import { SectionHeader } from './SectionHeader';
import { ChapterRow } from './ChapterRow';
import { BuyButton } from './BuyButton';
import { FAQSection } from './FAQSection';
import { sqlCourseChapters } from '../SQLCourse/SQLCoursePage';
export function SQLCourseVariantPage() {
return (
<div className="relative flex grow flex-col items-center bg-linear-to-b from-zinc-900 to-zinc-950 px-4 pt-3 pb-52 text-zinc-400 md:px-10 md:pt-8">
<div className="mx-auto mt-7 w-full max-w-5xl md:mt-20">
<div className="relative">
<Spotlight className="top-[-200px] left-[-170px]" fill="#EAB308" />
<div className="flex flex-col gap-7 sm:flex-row sm:items-center">
<a
href="https://roadmap.sh"
target="_blank"
className="transition-opacity hover:opacity-100"
>
<RoadmapLogoIcon className="size-12 sm:size-22" />
</a>
<div className="flex flex-col items-start gap-2.5">
<h1 className="text-3xl font-bold tracking-tight text-white sm:text-4xl md:text-6xl">
Master SQL Queries
</h1>
<p className="text-left text-xl text-balance text-zinc-300 md:text-2xl">
Complete course with AI Tutor, real-world challenges and more
</p>
</div>
</div>
<p className="my-5 text-xl leading-relaxed text-zinc-300 md:my-14 lg:text-xl">
Get certified for SQL queries and ready to deploy your newly-gained
skill in 30 days. Perfect for developers, data analysts, and anyone
working with data. Level up risk-free with a 7-day money-back
guarantee.
</p>
<div className="flex flex-col-reverse gap-7 lg:flex-row lg:gap-14">
<div className="w-full shrink-0 flex-row-reverse items-start justify-between gap-3 text-lg md:flex lg:w-auto lg:flex-col">
<div className="mb-7 flex flex-col gap-2 lg:mb-0 lg:gap-4">
{[
{ Icon: NotebookTextIcon, text: '55+ Lessons' },
{ Icon: FileQuestionIcon, text: '100+ Challenges' },
{ Icon: BrainIcon, text: 'AI Tutor' },
{ Icon: CodeIcon, text: 'Integrated IDE' },
].map(({ Icon, text }, index) => (
<div
key={index}
className="flex flex-row items-center gap-2 text-base text-zinc-300 lg:text-xl"
>
<Icon className="size-5 text-yellow-400 lg:size-6" />
<span>{text}</span>
</div>
))}
</div>
<AuthorCredentials />
</div>
<PlatformDemo />
</div>
</div>
<PurchaseBanner />
<ReviewCarousel />
<CourseFeatures />
<MeetYourInstructor />
<SectionHeader
title="Course Overview"
description="This SQL programming class is designed to help you go from 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="mx-auto mt-8 w-full max-w-3xl space-y-4 md:mt-12">
{sqlCourseChapters.map((chapter, index) => (
<ChapterRow key={index} counter={index + 1} {...chapter} />
))}
</div>
<SectionHeader
title="Ready to master SQL?"
description="Start learning SQL queries risk-free with a 7-day money-back guarantee."
className="mt-8 md:mt-24"
/>
<div className="mx-auto mt-8 w-full">
<BuyButton variant="floating" />
</div>
<FAQSection />
<div className="mx-auto mt-12 w-full max-w-3xl text-left md:mt-9">
<p className="flex flex-col items-center justify-center gap-2 text-sm md:flex-row md:gap-0">
<a href="/terms" target="_blank" className="text-zinc-500">
Terms of Use
</a>
<span className="mx-4 hidden md:block">&middot;</span>
<a href="/privacy" target="_blank" className="text-zinc-500">
Privacy Policy
</a>
</p>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,28 @@
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 mt-24 w-full text-center', className)}>
<div className="relative w-full">
<h4 className="text-2xl font-medium text-zinc-200 md:text-3xl">
{title}
</h4>
</div>
{typeof description === 'string' ? (
<p className="mt-2 text-center text-lg text-balance text-zinc-400 md:mt-5 md:text-xl">
{description}
</p>
) : (
description
)}
</div>
);
}

View File

@@ -156,7 +156,7 @@ const gaPageIdentifier = Astro.url.pathname
<link rel='preconnect' href='https://api.roadmap.sh/' />
{
hasVarify && (
hasVarify && !import.meta.env.DEV && (
<Fragment>
<script is:inline>
window.varify = window.varify || {}; window.varify.iid = 4013;

View File

@@ -0,0 +1,111 @@
---
import { SQLCourseVariantPage } from '../../components/SQLCourseVariant/SQLCourseVariantPage.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 SQL programming class is designed to help you go from beginner to expert through hands-on practice with real-world scenarios, mastering everything from basic to complex queries.'
hasVarify={true}
keywords={[
'sql',
'database',
'database management',
'database administration',
]}
canonicalUrl='/courses/master-sql'
noIndex={true}
jsonLd={[
{
"@context": "https://schema.org",
"@type": "Course",
"@id": "https://roadmap.sh/courses/sql",
"name": "Master SQL",
"description": "A comprehensive SQL course designed to take you from beginner to advanced levels, featuring 55+ lessons, 100+ challenges, an integrated IDE, and an AI tutor. Ideal for developers, data analysts, and anyone working with data.",
"provider": {
"@type": "Organization",
"name": "roadmap.sh",
"url": "https://roadmap.sh"
},
"publisher": {
"@type": "Organization",
"name": "roadmap.sh",
"url": "https://roadmap.sh"
},
"timeRequired": "PT60H",
"isAccessibleForFree": false,
"offers": {
"@type": "Offer",
"url": "https://roadmap.sh/courses/sql",
"price": "59.99",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"category": "paid"
},
"image": [
"https://assets.roadmap.sh/guest/sql-course-bjc53.png"
],
"coursePrerequisites": [],
"teaches": [
"SQL syntax and queries",
"Data filtering and sorting",
"Joins and subqueries",
"Aggregate functions",
"Stored procedures",
"Views and indexes",
"Transactions and ACID properties",
"Query optimization techniques"
],
"educationalLevel": "Beginner to Advanced",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.8",
"ratingCount": 500
},
"inLanguage": "en",
"review": [
{
"@type": "Review",
"reviewBody": "This course was absolutely brilliant! The integrated database environment to practice what I learned was the best part.",
"author": {
"@type": "Person",
"name": "Gourav Khunger"
}
},
{
"@type": "Review",
"reviewBody": "Kamran has clearly put a lot of thought into this course. The content, structure and exercises were all great.",
"author": {
"@type": "Person",
"name": "Meabed"
}
},
{
"@type": "Review",
"reviewBody": "I already knew SQL but this course taught me a bunch of new things. Practical examples and challenges were great. Highly recommended!",
"author": {
"@type": "Person",
"name": "Mohsin Aheer"
}
}
],
"educationalCredentialAwarded": {
"@type": "EducationalOccupationalCredential",
"name": "Certificate of Completion",
"credentialCategory": "Certificate",
"url": "https://roadmap.sh/courses/sql"
},
"hasCourseInstance": [
{
"@type": "CourseInstance",
"courseMode": "Online",
"courseWorkload": "PT60H",
}
]
}
]}
>
<SQLCourseVariantPage client:load />
</SkeletonLayout>