mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-29 20:21:50 +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:
@@ -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>
|
||||
|
68
src/components/SQLCourseVariant/AuthorCredentials.tsx
Normal file
68
src/components/SQLCourseVariant/AuthorCredentials.tsx
Normal 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>
|
||||
);
|
||||
}
|
381
src/components/SQLCourseVariant/BuyButton.tsx
Normal file
381
src/components/SQLCourseVariant/BuyButton.tsx
Normal 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"> </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">·</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"> </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>
|
||||
</>
|
||||
);
|
||||
}
|
140
src/components/SQLCourseVariant/ChapterRow.tsx
Normal file
140
src/components/SQLCourseVariant/ChapterRow.tsx
Normal 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>
|
||||
);
|
||||
}
|
79
src/components/SQLCourseVariant/CourseDiscountBanner.tsx
Normal file
79
src/components/SQLCourseVariant/CourseDiscountBanner.tsx
Normal 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>
|
||||
);
|
||||
}
|
149
src/components/SQLCourseVariant/CourseFeatures.tsx
Normal file
149
src/components/SQLCourseVariant/CourseFeatures.tsx
Normal 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>
|
||||
);
|
||||
}
|
115
src/components/SQLCourseVariant/FAQSection.tsx
Normal file
115
src/components/SQLCourseVariant/FAQSection.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
89
src/components/SQLCourseVariant/MeetYourInstructor.tsx
Normal file
89
src/components/SQLCourseVariant/MeetYourInstructor.tsx
Normal 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>
|
||||
);
|
||||
}
|
33
src/components/SQLCourseVariant/PlatformDemo.tsx
Normal file
33
src/components/SQLCourseVariant/PlatformDemo.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
85
src/components/SQLCourseVariant/PurchaseBanner.tsx
Normal file
85
src/components/SQLCourseVariant/PurchaseBanner.tsx
Normal 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',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
312
src/components/SQLCourseVariant/ReviewCarousel.tsx
Normal file
312
src/components/SQLCourseVariant/ReviewCarousel.tsx
Normal 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>
|
||||
);
|
||||
}
|
27
src/components/SQLCourseVariant/RoadmapDetailsPopover.tsx
Normal file
27
src/components/SQLCourseVariant/RoadmapDetailsPopover.tsx
Normal 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>
|
||||
);
|
||||
}
|
125
src/components/SQLCourseVariant/SQLCourseVariantPage.tsx
Normal file
125
src/components/SQLCourseVariant/SQLCourseVariantPage.tsx
Normal 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">·</span>
|
||||
<a href="/privacy" target="_blank" className="text-zinc-500">
|
||||
Privacy Policy
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
28
src/components/SQLCourseVariant/SectionHeader.tsx
Normal file
28
src/components/SQLCourseVariant/SectionHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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;
|
||||
|
111
src/pages/courses/master-sql.astro
Normal file
111
src/pages/courses/master-sql.astro
Normal 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>
|
Reference in New Issue
Block a user