mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-01-31 05:07:41 +01:00
Add SQL course landing page (#8127)
* wip: courses * fix: update course sidebar * wip * fix: merge lessons * wip * wip: course footer * wip * fix: refactor layout * fix: refactor * feat: course progress * fix: update current lesson store * fix: refactor props * wip * wip * feat: course certificate * wip: course rating * wip: course notes * wip * feat: implement course notes * feat: make card clickable * fix: add hover background * fix: refactor course layout * fix: resizeable * fix: go back on save * feat: delete confimation * wip * feat: chat UI * fix: lesson complete guard issue * wip: add public json files * wip: course ai * fix: loading card * Fix failing dev without internet * Light mode and UI changes * Update UI * Update course UI * Add chapter page * Improve sidebar of course * Update navigation: * Update quiz view * Improve UI for quiz attempts * Remove unnecessary console.logs * Add progress loading skeletons * Update UI * Change background color of editor * Fix line color not applied on editor * UI updates * feat: empty view * feat: course ai token limit * feat: handle auth users * wip * feat: course landing page * wip * Add first chapter of SQL * Add introduction chapter * Add quiz for introduction * Add expressions in select * Add content for DISTINCT * Add filter with where * Add lesson about limit and offset * Add lesson for handling null values * Add lesson about comments * Add challenges * Add challenge * Add challenge * Add challenge * Add challenge 7 * Add creating tables lesson * Add common data types lesson * Add data types in sqlite * Add more on data types lesson * feat: course landing page * Add more on numeric types * Update * Add lesson about temporal data types * Add constraints * Add primary keys chapter * Add modifying tables * Add dropping and truncating * Rewrite for PostgreSQL * Update numeric types to PostgreSQL * Improve temporal data type content * Improve temporal data type content * Add setup for temporal data * Improve challenges in SQL basics * Update challenge names * Add new challenges * Add temporal validation challenge * Add new constraint * Add modifying tables query * Removing table * Add insert operations lesson * Add updating data lesson * Add delete operations * Add inserting and updating challenges * Add lesson for cleaning up data * Update course title * Add relation data lesson * Add relationships and types * Add relationships and types * wip * Add joins lesson * Joins in queries * Add inner join details * Add join queries * Add inner join details * Add foreign key constraint lesson * Update composite foreign keys * Add lesson about foreign keys * Add lesson about set operation queries * Add lesson about set operation queries * Add set operator challenges * Add new challenge * Add view lesson * Add notes in views * Add inactive customer challenge * Add high value order challenge * gst * Add new challenges * Add readers like you challenge * Update inactive customer query * Update inactive customer query * Update inactive customer query * Update inactive customer query * Update inactive customer query * add challenge for same price books * Add aggregate functions introduction * Add basic aggregation lesson * Add basic aggregation lesson * Add introduction quiz * Add grouping lesson * Add grouping gotchas * Add grouping and filtering lesson * Add note for lesson * Add challenges for aggregate * Update aggregate challenge * Rearrange chapters * Add scalar functions lessons * Add numeric functions * Add date functions * Add conversion functions * Add conversion functions * Add logical functions chapter * Add exercises * Add new challenges * Add monthly sales analysis * Add subqueries and ctes * Update * Add correlated subqueries * Add common table expressions * Add common-table expressions * Add example * Add recursive CTEs * Add subquery challenge * Add latest category books challenge * Add challenges * Add bestseller rankings challenge * Add new customer analysis * Add daily sales report * Improve queries * Add introduction to window functions * Add over and partition * wip: billing page * Add ranking functions * Improve ranking functions * Add order by * Add window frames lesson * Add window frames explanation * Add challenges for window functions * Add price range analysis challenge * wip * wip: course enroll * fix: start learning * wip * wip * Enrollment changes * wip * wip * feat: mobile responsive * Changelog banner refactor * Update * Header for course * Header for what to expect * UI color * Table of contents * Icons on chapters * Change design for road to sql * Add sql course page * Add lesson content * Update UI * Expanded chapter row * Add course page * Refactor * Add spotlight * Improve features * Add course features * Add certificate note * Zoom in on the image * Update * Add floating purchase * Floating purchase indicatorg * Add about section * Update about section * Add FAQ section * Update UI * Add purchase power parity * Show purchasing power pricing * Add course login popup * Add course login popup * Add account button * Add trigger for course purchase * Course purchase param * Buy button changes * Add faqs * Add purchase trigger on reload * Landing verification * Make header responsive * Make course page upper half responsive * Full page is responsive * Fix login height bug * Responsiveness * Implement login after checkout * Remove unused code * Update dependenciesg * Update * fix: refetch mount to false * Remove unused code * Remove unused code * Remove unused code * Remove unused code * Remove unused code * Remove unused code * Remove unused * Add quizzes to chapters * Update course slug * Update dependencies * Add header for sql course --------- Co-authored-by: Arik Chakma <arikchangma@gmail.com>
This commit is contained in:
parent
4696af9c6a
commit
28af19cd1c
@ -3,6 +3,6 @@
|
||||
"enabled": false
|
||||
},
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1737069970237
|
||||
"lastUpdateCheck": 1737392387456
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
PUBLIC_API_URL=https://api.roadmap.sh
|
||||
PUBLIC_AVATAR_BASE_URL=https://dodrc8eu8m09s.cloudfront.net/avatars
|
||||
PUBLIC_EDITOR_APP_URL=https://draw.roadmap.sh
|
||||
PUBLIC_COURSE_APP_URL=http://localhost:5173
|
@ -67,10 +67,12 @@
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"roadmap-renderer": "^1.0.6",
|
||||
"sanitize-html": "^2.13.1",
|
||||
"satori": "^0.11.2",
|
||||
"satori-html": "^0.3.2",
|
||||
"sharp": "^0.33.5",
|
||||
"slugify": "^1.6.6",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"tailwind-merge": "^2.5.3",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"turndown": "^7.2.0",
|
||||
@ -86,6 +88,7 @@
|
||||
"@types/prismjs": "^1.26.4",
|
||||
"@types/react-calendar-heatmap": "^1.6.7",
|
||||
"@types/react-slick": "^0.23.13",
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"csv-parser": "^3.0.0",
|
||||
"gh-pages": "^6.2.0",
|
||||
|
2718
pnpm-lock.yaml
generated
2718
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -86,9 +86,6 @@ export function AdvertiseForm() {
|
||||
|
||||
pageProgressMessage.set('Please wait');
|
||||
|
||||
// Placeholder function to send data
|
||||
console.log('Form data:', formData);
|
||||
|
||||
const { response, error } = await httpPost(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-advertise`,
|
||||
formData,
|
||||
|
@ -1,6 +1,5 @@
|
||||
---
|
||||
import { parse } from 'node-html-parser';
|
||||
import type { Attributes } from 'node-html-parser/dist/nodes/html';
|
||||
|
||||
export interface Props {
|
||||
icon: string;
|
||||
@ -15,7 +14,6 @@ async function getSVG(name: string) {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
|
||||
if (!(filepath in files)) {
|
||||
throw new Error(`${filepath} not found`);
|
||||
}
|
||||
|
143
src/components/AuthenticationFlow/CourseLoginPopup.tsx
Normal file
143
src/components/AuthenticationFlow/CourseLoginPopup.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Modal } from '../Modal';
|
||||
import { GitHubButton } from './GitHubButton';
|
||||
import { GoogleButton } from './GoogleButton';
|
||||
import { LinkedInButton } from './LinkedInButton';
|
||||
import { EmailLoginForm } from './EmailLoginForm';
|
||||
import { EmailSignupForm } from './EmailSignupForm';
|
||||
|
||||
type CourseLoginPopupProps = {
|
||||
onClose: () => void;
|
||||
checkoutAfterLogin?: boolean;
|
||||
};
|
||||
|
||||
export const CHECKOUT_AFTER_LOGIN_KEY = 'checkoutAfterLogin';
|
||||
|
||||
export function CourseLoginPopup(props: CourseLoginPopupProps) {
|
||||
const { onClose: parentOnClose, checkoutAfterLogin = true } = props;
|
||||
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
const [isUsingEmail, setIsUsingEmail] = useState(false);
|
||||
|
||||
const [emailNature, setEmailNature] = useState<'login' | 'signup' | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
function onClose() {
|
||||
// if user didn't login and closed the popup, we remove the checkoutAfterLogin flag
|
||||
// so that login from other buttons on course page will trigger purchase
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
parentOnClose();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
CHECKOUT_AFTER_LOGIN_KEY,
|
||||
checkoutAfterLogin ? '1' : '0',
|
||||
);
|
||||
}, [checkoutAfterLogin]);
|
||||
|
||||
if (emailNature) {
|
||||
const emailHeader = (
|
||||
<div className="mb-7 text-center">
|
||||
<p className="mb-3.5 pt-2 text-2xl font-semibold leading-5 text-slate-900">
|
||||
{emailNature === 'login'
|
||||
? 'Login to your account'
|
||||
: 'Create an account'}
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-4 text-slate-600">
|
||||
Fill in the details below to continue
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} bodyClassName="p-5 h-auto">
|
||||
{emailHeader}
|
||||
{emailNature === 'login' && (
|
||||
<EmailLoginForm
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
)}
|
||||
{emailNature === 'signup' && (
|
||||
<EmailSignupForm
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="mt-2 w-full rounded-md border border-gray-400 py-2 text-center text-sm text-gray-600 hover:bg-gray-100"
|
||||
onClick={() => setEmailNature(null)}
|
||||
>
|
||||
Back to Options
|
||||
</button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} bodyClassName="p-5 h-auto">
|
||||
<div className="mb-7 text-center">
|
||||
<p className="mb-3.5 pt-2 text-2xl font-semibold leading-5 text-slate-900">
|
||||
Create or login to your account
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-4 text-slate-600">
|
||||
Login or sign up for an account to start learning
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<GitHubButton
|
||||
className="rounded-md border-gray-400 hover:bg-gray-100"
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
<GoogleButton
|
||||
className="rounded-md border-gray-400 hover:bg-gray-100"
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
<LinkedInButton
|
||||
className="rounded-md border-gray-400 hover:bg-gray-100"
|
||||
isDisabled={isDisabled}
|
||||
setIsDisabled={setIsDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center gap-4 py-6 text-sm text-gray-600">
|
||||
<div className="h-px w-full bg-gray-200" />
|
||||
OR
|
||||
<div className="h-px w-full bg-gray-200" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
{!isUsingEmail && (
|
||||
<button
|
||||
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
|
||||
onClick={() => setIsUsingEmail(true)}
|
||||
>
|
||||
Use your email address
|
||||
</button>
|
||||
)}
|
||||
{isUsingEmail && (
|
||||
<>
|
||||
<button
|
||||
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
|
||||
onClick={() => setEmailNature('login')}
|
||||
>
|
||||
Already have an account
|
||||
</button>
|
||||
<button
|
||||
className="flex-grow rounded-md border border-gray-400 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100"
|
||||
onClick={() => setEmailNature('signup')}
|
||||
>
|
||||
Create an account
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -1,21 +1,23 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { cn } from '../../../editor/utils/classname.ts';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { COURSE_PURCHASE_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
import { triggerUtmRegistration } from '../../lib/browser.ts';
|
||||
|
||||
type GitHubButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const GITHUB_REDIRECT_AT = 'githubRedirectAt';
|
||||
const GITHUB_LAST_PAGE = 'githubLastPage';
|
||||
|
||||
export function GitHubButton(props: GitHubButtonProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
const { isDisabled, setIsDisabled, className } = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@ -74,6 +76,17 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
localStorage.removeItem(GITHUB_REDIRECT_AT);
|
||||
localStorage.removeItem(GITHUB_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) {
|
||||
const tempUrl = new URL(redirectUrl, window.location.origin);
|
||||
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
redirectUrl = tempUrl.toString();
|
||||
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -120,7 +133,10 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className={cn(
|
||||
'inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none hover:border-gray-400 hover:bg-gray-50 focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
className,
|
||||
)}
|
||||
disabled={isLoading || isDisabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { COURSE_PURCHASE_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
import {
|
||||
getStoredUtmParams,
|
||||
triggerUtmRegistration,
|
||||
@ -12,13 +13,14 @@ import {
|
||||
type GoogleButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const GOOGLE_REDIRECT_AT = 'googleRedirectAt';
|
||||
const GOOGLE_LAST_PAGE = 'googleLastPage';
|
||||
|
||||
export function GoogleButton(props: GoogleButtonProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
const { isDisabled, setIsDisabled, className } = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@ -75,6 +77,16 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
redirectUrl = authRedirectUrl;
|
||||
}
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) {
|
||||
const tempUrl = new URL(redirectUrl, window.location.origin);
|
||||
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
redirectUrl = tempUrl.toString();
|
||||
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
localStorage.removeItem(GOOGLE_REDIRECT_AT);
|
||||
localStorage.removeItem(GOOGLE_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
@ -130,7 +142,10 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className={cn(
|
||||
'inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none hover:border-gray-400 hover:bg-gray-50 focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
className,
|
||||
)}
|
||||
disabled={isLoading || isDisabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
|
@ -1,21 +1,23 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME, setAuthToken } from '../../lib/jwt';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { COURSE_PURCHASE_PARAM, setAuthToken } from '../../lib/jwt';
|
||||
import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';
|
||||
import { triggerUtmRegistration } from '../../lib/browser.ts';
|
||||
|
||||
type LinkedInButtonProps = {
|
||||
isDisabled?: boolean;
|
||||
setIsDisabled?: (isDisabled: boolean) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const LINKEDIN_REDIRECT_AT = 'linkedInRedirectAt';
|
||||
const LINKEDIN_LAST_PAGE = 'linkedInLastPage';
|
||||
|
||||
export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
const { isDisabled, setIsDisabled } = props;
|
||||
const { isDisabled, setIsDisabled, className } = props;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@ -70,6 +72,16 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
redirectUrl = authRedirectUrl;
|
||||
}
|
||||
|
||||
const shouldTriggerPurchase =
|
||||
localStorage.getItem(CHECKOUT_AFTER_LOGIN_KEY) !== '0';
|
||||
if (redirectUrl.includes('/courses/sql') && shouldTriggerPurchase) {
|
||||
const tempUrl = new URL(redirectUrl, window.location.origin);
|
||||
tempUrl.searchParams.set(COURSE_PURCHASE_PARAM, '1');
|
||||
redirectUrl = tempUrl.toString();
|
||||
|
||||
localStorage.removeItem(CHECKOUT_AFTER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
localStorage.removeItem(LINKEDIN_REDIRECT_AT);
|
||||
localStorage.removeItem(LINKEDIN_LAST_PAGE);
|
||||
setAuthToken(response.token);
|
||||
@ -125,14 +137,17 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className={cn(
|
||||
'inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none hover:border-gray-400 focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60',
|
||||
className,
|
||||
)}
|
||||
disabled={isLoading || isDisabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
|
||||
) : (
|
||||
<LinkedInIcon className={'h-[18px] w-[18px]'} />
|
||||
<LinkedInIcon className={'h-[18px] w-[18px] text-blue-700'} />
|
||||
)}
|
||||
Continue with LinkedIn
|
||||
</button>
|
||||
|
@ -1,11 +1,9 @@
|
||||
---
|
||||
import { Menu } from 'lucide-react';
|
||||
import { AccountStreak } from '../AccountStreak/AccountStreak';
|
||||
import Icon from '../AstroIcon.astro';
|
||||
import { NavigationDropdown } from '../NavigationDropdown';
|
||||
import { AccountDropdown } from './AccountDropdown';
|
||||
import NewIndicator from './NewIndicator.astro';
|
||||
import { AccountStreak } from '../AccountStreak/AccountStreak';
|
||||
import { RoadmapDropdownMenu } from '../RoadmapDropdownMenu/RoadmapDropdownMenu';
|
||||
import { AccountDropdown } from './AccountDropdown';
|
||||
---
|
||||
|
||||
<div class='bg-slate-900 py-5 text-white sm:py-8'>
|
||||
@ -48,7 +46,7 @@ import { RoadmapDropdownMenu } from '../RoadmapDropdownMenu/RoadmapDropdownMenu'
|
||||
</a>
|
||||
<a
|
||||
href='/changelog'
|
||||
class='group relative text-blue-300 hover:text-white hidden md:block ml-0.5'
|
||||
class='group relative ml-0.5 hidden text-blue-300 hover:text-white md:block'
|
||||
>
|
||||
Changelog
|
||||
|
||||
|
28
src/components/ReactIcons/RoadmapLogo.tsx
Normal file
28
src/components/ReactIcons/RoadmapLogo.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import type { SVGProps } from 'react';
|
||||
|
||||
type RoadmapLogoIconProps = SVGProps<SVGSVGElement> & {
|
||||
color?: 'white' | 'black';
|
||||
};
|
||||
|
||||
export function RoadmapLogoIcon(props: RoadmapLogoIconProps) {
|
||||
const { color = 'white', ...rest } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 283 283"
|
||||
{...rest}
|
||||
>
|
||||
<path
|
||||
fill={color === 'black' ? '#000' : '#fff'}
|
||||
d="M0 39C0 17.46 17.46 0 39 0h205c21.539 0 39 17.46 39 39v205c0 21.539-17.461 39-39 39H39c-21.54 0-39-17.461-39-39V39Z"
|
||||
/>
|
||||
<path
|
||||
fill={color === 'black' ? '#fff' : '#000'}
|
||||
d="M121.215 210.72c-1.867.56-4.854 1.12-8.96 1.68-3.92.56-8.027.84-12.32.84-4.107 0-7.84-.28-11.2-.84-3.174-.56-5.88-1.68-8.12-3.36s-4.014-3.92-5.32-6.72c-1.12-2.987-1.68-6.813-1.68-11.48v-84c0-4.293.746-7.933 2.24-10.92 1.68-3.173 4.013-5.973 7-8.4s6.626-4.573 10.92-6.44c4.48-2.053 9.24-3.827 14.28-5.32a106.176 106.176 0 0 1 15.68-3.36 95.412 95.412 0 0 1 16.24-1.4c8.96 0 16.053 1.773 21.28 5.32 5.226 3.36 7.84 8.96 7.84 16.8 0 2.613-.374 5.227-1.12 7.84-.747 2.427-1.68 4.667-2.8 6.72a133.1 133.1 0 0 0-12.04.56c-4.107.373-8.12.933-12.04 1.68s-7.654 1.587-11.2 2.52c-3.36.747-6.254 1.68-8.68 2.8v95.48zm45.172-22.4c0-7.84 2.426-14.373 7.28-19.6s11.48-7.84 19.88-7.84 15.026 2.613 19.88 7.84 7.28 11.76 7.28 19.6-2.427 14.373-7.28 19.6-11.48 7.84-19.88 7.84-15.027-2.613-19.88-7.84-7.28-11.76-7.28-19.6z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
67
src/components/SQLCourse/AccountButton.tsx
Normal file
67
src/components/SQLCourse/AccountButton.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import {
|
||||
courseProgressOptions
|
||||
} from '../../queries/course-progress';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { CourseLoginPopup } from '../AuthenticationFlow/CourseLoginPopup';
|
||||
import { BuyButton, COURSE_SLUG } from './BuyButton';
|
||||
|
||||
export function AccountButton() {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
|
||||
const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery(
|
||||
courseProgressOptions(COURSE_SLUG),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(true);
|
||||
}, []);
|
||||
|
||||
const buttonClasses =
|
||||
'rounded-full px-5 py-2 text-base font-medium text-yellow-700 hover:text-yellow-500 transition-colors';
|
||||
|
||||
const hasEnrolled = !!courseProgress?.enrolledAt;
|
||||
const loginModal = (
|
||||
<CourseLoginPopup
|
||||
checkoutAfterLogin={false}
|
||||
onClose={() => {
|
||||
setShowLoginModal(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isVisible || isLoadingCourseProgress) {
|
||||
return <button className={`${buttonClasses} opacity-0`}>...</button>;
|
||||
}
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setShowLoginModal(true)}
|
||||
className={`${buttonClasses} animate-fade-in`}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
{showLoginModal && loginModal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasEnrolled) {
|
||||
return <BuyButton variant="top-nav" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`${import.meta.env.PUBLIC_COURSE_APP_URL}/sql`}
|
||||
className={`${buttonClasses} animate-fade-in`}
|
||||
>
|
||||
Start Learning
|
||||
</a>
|
||||
);
|
||||
}
|
210
src/components/SQLCourse/BuyButton.tsx
Normal file
210
src/components/SQLCourse/BuyButton.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { COURSE_PURCHASE_PARAM, isLoggedIn } from '../../lib/jwt';
|
||||
import { coursePriceOptions } from '../../queries/billing';
|
||||
import { courseProgressOptions } from '../../queries/course-progress';
|
||||
import { queryClient } from '../../stores/query-client';
|
||||
import { CourseLoginPopup } from '../AuthenticationFlow/CourseLoginPopup';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { httpPost } from '../../lib/query-http';
|
||||
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
|
||||
|
||||
export const COURSE_SLUG = 'master-sql';
|
||||
|
||||
type CreateCheckoutSessionBody = {
|
||||
courseId: string;
|
||||
success?: string;
|
||||
cancel?: string;
|
||||
};
|
||||
|
||||
type CreateCheckoutSessionResponse = {
|
||||
checkoutUrl: string;
|
||||
};
|
||||
|
||||
type BuyButtonProps = {
|
||||
variant?: 'main' | 'floating' | 'top-nav';
|
||||
};
|
||||
|
||||
export function BuyButton(props: BuyButtonProps) {
|
||||
const { variant = 'main' } = props;
|
||||
|
||||
const [isLoginPopupOpen, setIsLoginPopupOpen] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
const { data: coursePricing, isLoading: isLoadingCourse } = useQuery(
|
||||
coursePriceOptions({ courseSlug: COURSE_SLUG }),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const { data: courseProgress, isLoading: isLoadingCourseProgress } = useQuery(
|
||||
courseProgressOptions(COURSE_SLUG),
|
||||
queryClient,
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: createCheckoutSession,
|
||||
isPending: isCreatingCheckoutSession,
|
||||
} = useMutation(
|
||||
{
|
||||
mutationFn: (body: CreateCheckoutSessionBody) => {
|
||||
return httpPost<CreateCheckoutSessionResponse>(
|
||||
'/v1-create-checkout-session',
|
||||
body,
|
||||
);
|
||||
},
|
||||
onMutate: () => {
|
||||
toast.loading('Creating checkout session...');
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
window.location.href = data.checkoutUrl;
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.error(error?.message || 'Failed to create checkout session');
|
||||
},
|
||||
},
|
||||
queryClient,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = getUrlParams();
|
||||
const shouldTriggerPurchase = urlParams[COURSE_PURCHASE_PARAM] === '1';
|
||||
if (shouldTriggerPurchase) {
|
||||
deleteUrlParam(COURSE_PURCHASE_PARAM);
|
||||
initPurchase();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isLoadingPricing =
|
||||
isLoadingCourse || !coursePricing || isLoadingCourseProgress;
|
||||
const isAlreadyEnrolled = !!courseProgress?.enrolledAt;
|
||||
|
||||
function initPurchase() {
|
||||
if (!isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
createCheckoutSession({
|
||||
courseId: COURSE_SLUG,
|
||||
success: `/courses/sql?e=1`,
|
||||
cancel: `/courses/sql`,
|
||||
});
|
||||
}
|
||||
|
||||
function onBuyClick() {
|
||||
if (!isLoggedIn()) {
|
||||
setIsLoginPopupOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasEnrolled = !!courseProgress?.enrolledAt;
|
||||
if (hasEnrolled) {
|
||||
window.location.href = `${import.meta.env.PUBLIC_COURSE_APP_URL}/sql`;
|
||||
return;
|
||||
}
|
||||
|
||||
initPurchase();
|
||||
}
|
||||
|
||||
const courseLoginPopup = isLoginPopupOpen && (
|
||||
<CourseLoginPopup onClose={() => setIsLoginPopupOpen(false)} />
|
||||
);
|
||||
|
||||
if (variant === 'main') {
|
||||
return (
|
||||
<div className="relative flex w-full flex-col items-center gap-2 md:w-auto">
|
||||
{courseLoginPopup}
|
||||
<button
|
||||
onClick={onBuyClick}
|
||||
disabled={isLoadingPricing}
|
||||
className={cn(
|
||||
'group relative inline-flex w-full min-w-[235px] items-center justify-center overflow-hidden rounded-xl bg-gradient-to-r from-yellow-500 to-yellow-300 px-8 py-3 text-base font-semibold text-black transition-all duration-300 ease-out hover:scale-[1.02] hover:shadow-[0_0_30px_rgba(234,179,8,0.4)] focus:outline-none active:ring-0 md:w-auto md:rounded-full md:text-lg',
|
||||
(isLoadingPricing || isCreatingCheckoutSession) &&
|
||||
'striped-loader-yellow pointer-events-none scale-105 bg-yellow-500',
|
||||
)}
|
||||
>
|
||||
{isLoadingPricing ? (
|
||||
<span className="relative flex items-center gap-2"> </span>
|
||||
) : isAlreadyEnrolled ? (
|
||||
<span className="relative flex items-center gap-2">
|
||||
Start Learning
|
||||
</span>
|
||||
) : (
|
||||
<span className="relative flex items-center gap-2">
|
||||
{coursePricing?.isEligibleForDiscount && coursePricing?.flag} Buy
|
||||
now for{' '}
|
||||
{coursePricing?.isEligibleForDiscount ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="hidden text-base line-through opacity-75 md:inline">
|
||||
${coursePricing?.fullPrice}
|
||||
</span>
|
||||
<span className="text-base md:text-xl">
|
||||
${coursePricing?.regionalPrice}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span>${coursePricing?.regionalPrice}</span>
|
||||
)}
|
||||
<ArrowRightIcon className="h-5 w-5 transition-transform duration-300 ease-out group-hover:translate-x-1" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{!isLoadingPricing &&
|
||||
!isAlreadyEnrolled &&
|
||||
coursePricing?.isEligibleForDiscount && (
|
||||
<span className="absolute top-full translate-y-2.5 text-sm text-yellow-400">
|
||||
{coursePricing.regionalDiscountPercentage}% regional discount
|
||||
applied
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'top-nav') {
|
||||
return (
|
||||
<button
|
||||
onClick={onBuyClick}
|
||||
disabled={isLoadingPricing}
|
||||
className={`animate-fade-in rounded-full px-5 py-2 text-base font-medium text-yellow-700 transition-colors hover:text-yellow-500`}
|
||||
>
|
||||
Purchase Course
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-center gap-2">
|
||||
{courseLoginPopup}
|
||||
<button
|
||||
onClick={onBuyClick}
|
||||
disabled={isLoadingPricing}
|
||||
className={cn(
|
||||
'group relative inline-flex min-w-[220px] items-center justify-center overflow-hidden rounded-full bg-gradient-to-r from-yellow-500 to-yellow-300 px-8 py-2 font-medium text-black transition-all duration-300 ease-out hover:scale-[1.02] hover:shadow-[0_0_30px_rgba(234,179,8,0.4)] focus:outline-none',
|
||||
(isLoadingPricing || isCreatingCheckoutSession) &&
|
||||
'striped-loader-yellow pointer-events-none bg-yellow-500',
|
||||
)}
|
||||
>
|
||||
{isLoadingPricing ? (
|
||||
<span className="relative flex items-center gap-2"> </span>
|
||||
) : isAlreadyEnrolled ? (
|
||||
<span className="relative flex items-center gap-2">
|
||||
Start Learning
|
||||
</span>
|
||||
) : (
|
||||
<span className="relative flex items-center gap-2">
|
||||
{coursePricing?.flag} Buy Now ${coursePricing?.regionalPrice}
|
||||
<ArrowRightIcon className="h-5 w-5 transition-transform duration-300 ease-out group-hover:translate-x-1" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{!isAlreadyEnrolled && coursePricing?.isEligibleForDiscount && (
|
||||
<span className="top-full text-sm text-yellow-400">
|
||||
{coursePricing.regionalDiscountPercentage}% regional discount applied
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
145
src/components/SQLCourse/ChapterRow.tsx
Normal file
145
src/components/SQLCourse/ChapterRow.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { ChevronDown, BookIcon, CodeIcon, FileQuestion, MessageCircleQuestionIcon, CircleDot } from 'lucide-react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type ChapterRowProps = {
|
||||
counter: number;
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
lessonCount: number;
|
||||
challengeCount: number;
|
||||
isExpandable?: boolean;
|
||||
className?: string;
|
||||
lessons?: { title: string; type: 'lesson' | 'challenge' | 'quiz' }[];
|
||||
};
|
||||
|
||||
export function ChapterRow(props: ChapterRowProps) {
|
||||
const {
|
||||
counter,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
lessonCount,
|
||||
challengeCount,
|
||||
isExpandable = true,
|
||||
className,
|
||||
lessons = [],
|
||||
} = props;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
const regularLessons = lessons.filter((l) => l.type === 'lesson');
|
||||
const challenges = lessons.filter((l) =>
|
||||
['challenge', 'quiz'].includes(l.type),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
setIsExpanded(!isMobile);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('group relative select-none overflow-hidden', className)}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => isExpandable && setIsExpanded(!isExpanded)}
|
||||
className={cn(
|
||||
'relative rounded-xl border border-zinc-800 bg-zinc-800 p-6',
|
||||
'bg-gradient-to-br from-zinc-900/90 via-zinc-900/70 to-zinc-900/50',
|
||||
!isExpanded &&
|
||||
'hover:bg-gradient-to-br hover:from-zinc-900/95 hover:via-zinc-900/80 hover:to-zinc-900/60',
|
||||
!isExpanded &&
|
||||
'hover:cursor-pointer hover:shadow-[0_0_30px_rgba(0,0,0,0.2)]',
|
||||
isExpanded && 'rounded-b-none border-b-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="hidden flex-shrink-0 md:block">
|
||||
<div className="rounded-full bg-yellow-500/10 p-3">{icon}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow">
|
||||
<h3 className="text-xl font-semibold tracking-wide text-white">
|
||||
<span className="inline text-gray-500 md:hidden">
|
||||
{counter}.{' '}
|
||||
</span>
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mt-2 text-zinc-400">{description}</p>
|
||||
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-zinc-500">
|
||||
<span>{lessonCount} Lessons</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-zinc-500">
|
||||
<span>{challengeCount} Challenges</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpandable && (
|
||||
<div className="flex-shrink-0 rounded-full bg-zinc-800/80 p-2 text-zinc-400 group-hover:bg-zinc-800 group-hover:text-yellow-500">
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 transition-transform',
|
||||
isExpanded ? 'rotate-180' : '',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="rounded-b-xl border border-t-0 border-zinc-800 bg-gradient-to-br from-zinc-900/50 via-zinc-900/30 to-zinc-900/20">
|
||||
<div className="grid grid-cols-1 divide-zinc-800 md:grid-cols-2 md:divide-x">
|
||||
{regularLessons.length > 0 && (
|
||||
<div className="p-6 pb-0 md:pb-6">
|
||||
<h4 className="mb-4 text-sm font-medium uppercase tracking-wider text-zinc-500">
|
||||
Lessons
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{regularLessons.map((lesson, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-3 text-zinc-400 hover:text-yellow-500"
|
||||
>
|
||||
<BookIcon className="h-4 w-4" />
|
||||
<span>{lesson.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{challenges.length > 0 && (
|
||||
<div className="p-6">
|
||||
<h4 className="mb-4 text-sm font-medium uppercase tracking-wider text-zinc-500">
|
||||
Exercises
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{challenges.map((challenge, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-3 text-zinc-400 hover:text-yellow-500"
|
||||
>
|
||||
{challenge.type === 'challenge' ? (
|
||||
<CodeIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<CircleDot className="h-4 w-4" />
|
||||
)}
|
||||
<span>{challenge.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
24
src/components/SQLCourse/CourseAuthor.tsx
Normal file
24
src/components/SQLCourse/CourseAuthor.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
export function CourseAuthor() {
|
||||
return (
|
||||
<div className="mt-8 w-full max-w-3xl space-y-4">
|
||||
<div className="flex flex-row items-center gap-5">
|
||||
<img
|
||||
src="https://github.com/kamranahmedse.png"
|
||||
className="size-12 rounded-full bg-yellow-500/10 md:size-16"
|
||||
/>
|
||||
<a
|
||||
href="https://twitter.com/kamrify"
|
||||
target="_blank"
|
||||
className="flex flex-col"
|
||||
>
|
||||
<span className="text-lg font-medium text-zinc-200 md:text-2xl">
|
||||
Kamran Ahmed
|
||||
</span>
|
||||
<span className="text-sm text-zinc-500 md:text-lg">
|
||||
Software Engineer
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
94
src/components/SQLCourse/CourseFeature.tsx
Normal file
94
src/components/SQLCourse/CourseFeature.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { MinusIcon, PlusIcon, type LucideIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type CourseFeatureProps = {
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
description: string;
|
||||
imgUrl?: string;
|
||||
};
|
||||
|
||||
export function CourseFeature(props: CourseFeatureProps) {
|
||||
const { title, icon: Icon, description, imgUrl } = props;
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isZoomed, setIsZoomed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
if (isZoomed) {
|
||||
setIsZoomed(false);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, [isZoomed]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isZoomed && (
|
||||
<div
|
||||
onClick={() => {
|
||||
setIsZoomed(false);
|
||||
setIsExpanded(false);
|
||||
}}
|
||||
className="fixed inset-0 z-[999] flex cursor-zoom-out items-center justify-center bg-black bg-opacity-75"
|
||||
>
|
||||
<img
|
||||
src={imgUrl}
|
||||
alt={title}
|
||||
className="max-h-[50%] max-w-[90%] rounded-xl object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 z-10 bg-black/70 opacity-100 transition-opacity duration-200 ease-out',
|
||||
{
|
||||
'pointer-events-none opacity-0': !isExpanded,
|
||||
},
|
||||
)}
|
||||
onClick={() => setIsExpanded(false)}
|
||||
></div>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={cn(
|
||||
'z-20 flex w-full items-center rounded-lg border border-zinc-800 bg-zinc-900/50 px-4 py-3 text-left transition-colors duration-200 ease-out hover:bg-zinc-800/40',
|
||||
{
|
||||
'relative bg-zinc-800 hover:bg-zinc-800': isExpanded,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-grow items-center space-x-3">
|
||||
<Icon />
|
||||
<span>{title}</span>
|
||||
</span>
|
||||
{isExpanded ? (
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="absolute left-0 top-full z-20 translate-y-2 rounded-lg border border-zinc-800 bg-zinc-800 p-4">
|
||||
<p>{description}</p>
|
||||
{imgUrl && (
|
||||
<img
|
||||
onClick={() => {
|
||||
setIsZoomed(true);
|
||||
setIsExpanded(false);
|
||||
}}
|
||||
src={imgUrl}
|
||||
alt={title}
|
||||
className="mt-4 h-auto pointer-events-none md:pointer-events-auto w-full cursor-zoom-in rounded-lg object-right-top"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
113
src/components/SQLCourse/FAQSection.tsx
Normal file
113
src/components/SQLCourse/FAQSection.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { SectionHeader } from './SectionHeader';
|
||||
|
||||
type FAQItem = {
|
||||
question: string;
|
||||
answer: string;
|
||||
};
|
||||
|
||||
function FAQRow({ question, answer }: FAQItem) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-zinc-800 bg-zinc-900">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="flex w-full items-center justify-between p-4 md:p-6 text-left gap-2"
|
||||
>
|
||||
<h3 className="text-lg md:text-xl text-balance font-normal text-white">{question}</h3>
|
||||
<ChevronDownIcon
|
||||
className={`h-5 w-5 text-zinc-400 transition-transform duration-200 ${
|
||||
isExpanded ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="border-t border-zinc-800 p-6 pt-4 text-base md:text-lg leading-relaxed">
|
||||
<p>{answer}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FAQSection() {
|
||||
const faqs: FAQItem[] = [
|
||||
{
|
||||
question: 'What is the format of the course?',
|
||||
answer:
|
||||
'The course is written in textual format. There are several chapters; each chapter has a set of lessons, followed by a set of practice problems and quizzes. You can learn at your own pace and revisit the content anytime.',
|
||||
},
|
||||
{
|
||||
question: 'What prerequisites do I need for this course?',
|
||||
answer:
|
||||
'No prior SQL knowledge is required. The course starts from the basics and gradually progresses to advanced topics.',
|
||||
},
|
||||
{
|
||||
question: 'Do I need to have a local database to follow the course?',
|
||||
answer:
|
||||
'No, we have an integrated coding playground, populated with a sample databases depending on the lesson, that you can use to follow the course. You can also use your own database if you have one.',
|
||||
},
|
||||
{
|
||||
question: 'How long do I have access to the course?',
|
||||
answer:
|
||||
'You get lifetime access to the course including all future updates. Once you purchase, you can learn at your own pace and revisit the content anytime.',
|
||||
},
|
||||
{
|
||||
question: 'What kind of support is available?',
|
||||
answer:
|
||||
'You get access to an AI tutor within the course that can help you with queries 24/7. Additionally, you can use the community forums to discuss problems and get help from other learners.',
|
||||
},
|
||||
{
|
||||
question: 'Will I get a certificate upon completion?',
|
||||
answer:
|
||||
"Yes, upon completing the course and its challenges, you'll receive a certificate of completion that you can share with employers or add to your LinkedIn profile.",
|
||||
},
|
||||
{
|
||||
question: 'Can I use this for job interviews?',
|
||||
answer:
|
||||
'Absolutely! The course covers common SQL interview topics and includes practical challenges similar to what you might face in technical interviews. The hands-on experience will prepare you well for real-world scenarios.',
|
||||
},
|
||||
{
|
||||
question: "What if I don't like the course?",
|
||||
answer:
|
||||
'I will refund your purchase within 7 days of the purchase. No questions asked. However, I would love to hear your feedback so that I can improve the course. Send me an email at kamran@roadmap.sh',
|
||||
},
|
||||
{
|
||||
question: 'I already know SQL, can I still take this course?',
|
||||
answer:
|
||||
'Yes! The course starts from the basics and gradually progresses to advanced topics. You can skip the chapters that you already know and focus on the ones that you need.',
|
||||
},
|
||||
{
|
||||
question: 'Do you offer any team licenses?',
|
||||
answer: 'Yes, please contact me at kamran@roadmap.sh',
|
||||
},
|
||||
{
|
||||
question: 'How can I gift this course to someone?',
|
||||
answer:
|
||||
'Please contact me at kamran@roadmap.sh and I will be happy to help you.',
|
||||
},
|
||||
{
|
||||
question: 'What if I have a question that is not answered here?',
|
||||
answer:
|
||||
'Please contact me at kamran@roadmap.sh and I will be happy to help you.',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title="Frequently Asked Questions"
|
||||
description="Find answers to common questions about the course below."
|
||||
className="mt-10 md:mt-24"
|
||||
/>
|
||||
|
||||
<div className="mt-6 md:mt-8 w-full max-w-3xl space-y-2 md:space-y-6">
|
||||
{faqs.map((faq, index) => (
|
||||
<FAQRow key={index} {...faq} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
56
src/components/SQLCourse/FloatingPurchase.tsx
Normal file
56
src/components/SQLCourse/FloatingPurchase.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { BuyButton } from './BuyButton';
|
||||
|
||||
export function FloatingPurchase() {
|
||||
const [isHidden, setIsHidden] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
setIsHidden(window.scrollY < 400);
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed bottom-0 left-0 right-0 z-[5] flex items-center justify-center transition-all duration-200 ease-out',
|
||||
{
|
||||
'pointer-events-none -bottom-10 opacity-0': isHidden,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{/* Desktop version */}
|
||||
<div className="hidden mb-5 md:flex w-full max-w-[800px] items-center justify-between rounded-2xl bg-yellow-950 p-5 shadow-lg ring-1 ring-yellow-500/40">
|
||||
<div className="flex flex-col">
|
||||
<h2 className="mb-1 text-xl font-medium text-white">
|
||||
Go from Zero to Hero in SQL
|
||||
</h2>
|
||||
<p className="text-sm text-zinc-400">
|
||||
Get instant access to the course and start learning today
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BuyButton variant="floating" />
|
||||
</div>
|
||||
|
||||
{/* Mobile version */}
|
||||
<div className="flex md:hidden w-full flex-col bg-yellow-950 px-4 pt-3 pb-4 shadow-lg ring-1 ring-yellow-500/40">
|
||||
<div className="flex flex-col items-center text-center mb-3">
|
||||
<h2 className="text-lg font-medium text-white">
|
||||
Master SQL Today
|
||||
</h2>
|
||||
<p className="text-xs text-zinc-400">
|
||||
Get instant lifetime access
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<BuyButton variant="floating" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
415
src/components/SQLCourse/SQLCoursePage.tsx
Normal file
415
src/components/SQLCourse/SQLCoursePage.tsx
Normal file
@ -0,0 +1,415 @@
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
ArrowUpDownIcon,
|
||||
BarChartIcon,
|
||||
BrainIcon,
|
||||
ClipboardIcon,
|
||||
CodeIcon,
|
||||
DatabaseIcon,
|
||||
Eye,
|
||||
FileCheckIcon,
|
||||
FileQuestionIcon,
|
||||
GitBranchIcon,
|
||||
GitMergeIcon,
|
||||
LayersIcon,
|
||||
TableIcon,
|
||||
WrenchIcon,
|
||||
} from 'lucide-react';
|
||||
import { ChapterRow } from './ChapterRow';
|
||||
import { CourseFeature } from './CourseFeature';
|
||||
import { SectionHeader } from './SectionHeader';
|
||||
import { Spotlight } from './Spotlight';
|
||||
import { FloatingPurchase } from './FloatingPurchase';
|
||||
import { CourseAuthor } from './CourseAuthor';
|
||||
import { FAQSection } from './FAQSection';
|
||||
import { BuyButton } from './BuyButton';
|
||||
import { AccountButton } from './AccountButton';
|
||||
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
|
||||
|
||||
type ChapterData = {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
lessonCount: number;
|
||||
challengeCount: number;
|
||||
lessons: { title: string; type: 'lesson' | 'challenge' | 'quiz' }[];
|
||||
};
|
||||
|
||||
export function SQLCoursePage() {
|
||||
const chapters: ChapterData[] = [
|
||||
{
|
||||
icon: <DatabaseIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Introduction',
|
||||
description:
|
||||
'Get comfortable with database concepts and SQL fundamentals.',
|
||||
lessonCount: 4,
|
||||
challengeCount: 1,
|
||||
lessons: [
|
||||
{ title: 'Basics of Databases', type: 'lesson' },
|
||||
{ title: 'What is SQL?', type: 'lesson' },
|
||||
{ title: 'Types of Queries', type: 'lesson' },
|
||||
{ title: 'Next Steps', type: 'lesson' },
|
||||
{ title: 'Introduction Quiz', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <TableIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'SQL Basics',
|
||||
description: 'Master the essential SQL query operations and syntax.',
|
||||
lessonCount: 9,
|
||||
challengeCount: 7,
|
||||
lessons: [
|
||||
{ title: 'SELECT Fundamentals', type: 'lesson' },
|
||||
{ title: 'Aliases and Constants', type: 'lesson' },
|
||||
{ title: 'Expressions in SELECT', type: 'lesson' },
|
||||
{ title: 'Selecting DISTINCT Values', type: 'lesson' },
|
||||
{ title: 'Filtering with WHERE', type: 'lesson' },
|
||||
{ title: 'Sorting with ORDER BY', type: 'lesson' },
|
||||
{ title: 'Limiting Results with LIMIT', type: 'lesson' },
|
||||
{ title: 'Handling NULL Values', type: 'lesson' },
|
||||
{ title: 'Comments', type: 'lesson' },
|
||||
{ title: 'Basic Queries Quiz', type: 'quiz' },
|
||||
{ title: 'Projection Challenge', type: 'challenge' },
|
||||
{ title: 'Select Expression', type: 'challenge' },
|
||||
{ title: 'Select Unique', type: 'challenge' },
|
||||
{ title: 'Logical Operators', type: 'challenge' },
|
||||
{ title: 'Sorting Challenge', type: 'challenge' },
|
||||
{ title: 'Sorting and Limiting', type: 'challenge' },
|
||||
{ title: 'Sorting and Filtering', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <CodeIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Manipulating Data',
|
||||
description: 'Learn how to modify and manipulate data in your database.',
|
||||
lessonCount: 3,
|
||||
challengeCount: 3,
|
||||
lessons: [
|
||||
{ title: 'INSERT Operations', type: 'lesson' },
|
||||
{ title: 'UPDATE Operations', type: 'lesson' },
|
||||
{ title: 'DELETE Operations', type: 'lesson' },
|
||||
{ title: 'Data Manipulation Quiz', type: 'quiz' },
|
||||
{ title: 'Inserting Customers', type: 'challenge' },
|
||||
{ title: 'Updating Bookstore', type: 'challenge' },
|
||||
{ title: 'Deleting Books', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <LayersIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Defining Tables',
|
||||
description: 'Master database schema design and table management.',
|
||||
lessonCount: 9,
|
||||
challengeCount: 7,
|
||||
lessons: [
|
||||
{ title: 'Creating Tables', type: 'lesson' },
|
||||
{ title: 'Data Types in SQLite', type: 'lesson' },
|
||||
{ title: 'Common Data Types', type: 'lesson' },
|
||||
{ title: 'More on Numeric Types', type: 'lesson' },
|
||||
{ title: 'Temporal Data Types', type: 'lesson' },
|
||||
{ title: 'CHECK Constraints', type: 'lesson' },
|
||||
{ title: 'Primary Key Constraint', type: 'lesson' },
|
||||
{ title: 'Modifying Tables', type: 'lesson' },
|
||||
{ title: 'Dropping and Truncating', type: 'lesson' },
|
||||
{ title: 'Defining Tables Quiz', type: 'quiz' },
|
||||
{ title: 'Simple Table Creation', type: 'challenge' },
|
||||
{ title: 'Data Types Challenge', type: 'challenge' },
|
||||
{ title: 'Constraints Challenge', type: 'challenge' },
|
||||
{ title: 'Temporal Validation', type: 'challenge' },
|
||||
{ title: 'Sales Data Analysis', type: 'challenge' },
|
||||
{ title: 'Modifying Tables', type: 'challenge' },
|
||||
{ title: 'Removing Table Data', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <GitMergeIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Multi-Table Queries',
|
||||
description:
|
||||
'Learn to work with multiple tables using JOINs and relationships.',
|
||||
lessonCount: 7,
|
||||
challengeCount: 10,
|
||||
lessons: [
|
||||
{ title: 'More on Relational Data', type: 'lesson' },
|
||||
{ title: 'Relationships and Types', type: 'lesson' },
|
||||
{ title: 'JOINs in Queries', type: 'lesson' },
|
||||
{ title: 'Self Joins and Usecases', type: 'lesson' },
|
||||
{ title: 'Foreign Key Constraint', type: 'lesson' },
|
||||
{ title: 'Set Operator Queries', type: 'lesson' },
|
||||
{ title: 'Views and Virtual Tables', type: 'lesson' },
|
||||
{ title: 'Multi-Table Queries Quiz', type: 'quiz' },
|
||||
{ title: 'Inactive Customers', type: 'challenge' },
|
||||
{ title: 'Recent 3 Orders', type: 'challenge' },
|
||||
{ title: 'High Value Orders', type: 'challenge' },
|
||||
{ title: 'Specific Book Customers', type: 'challenge' },
|
||||
{ title: 'Referred Customers', type: 'challenge' },
|
||||
{ title: 'Readers Like You', type: 'challenge' },
|
||||
{ title: 'Same Price Books', type: 'challenge' },
|
||||
{ title: 'Multi-Section Authors', type: 'challenge' },
|
||||
{ title: 'Expensive Books', type: 'challenge' },
|
||||
{ title: 'Trending Tech Books', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <WrenchIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Aggregate Functions',
|
||||
description:
|
||||
"Analyze and summarize data using SQL's powerful aggregation features.",
|
||||
lessonCount: 4,
|
||||
challengeCount: 10,
|
||||
lessons: [
|
||||
{ title: 'What is Aggregation?', type: 'lesson' },
|
||||
{ title: 'Basic Aggregation', type: 'lesson' },
|
||||
{ title: 'Grouping Data', type: 'lesson' },
|
||||
{ title: 'Grouping and Filtering', type: 'lesson' },
|
||||
{ title: 'Aggregate Queries Quiz', type: 'quiz' },
|
||||
{ title: 'Book Sales Summary', type: 'challenge' },
|
||||
{ title: 'Category Insights', type: 'challenge' },
|
||||
{ title: 'Author Tier Analysis', type: 'challenge' },
|
||||
{ title: 'Author Book Stats', type: 'challenge' },
|
||||
{ title: 'Daily Sales Report', type: 'challenge' },
|
||||
{ title: 'Publisher Stats', type: 'challenge' },
|
||||
{ title: 'High Value Publishers', type: 'challenge' },
|
||||
{ title: 'Premium Authors', type: 'challenge' },
|
||||
{ title: 'Sales Analysis', type: 'challenge' },
|
||||
{ title: 'Employee Performance', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <BarChartIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Scalar Functions',
|
||||
description:
|
||||
'Master built-in functions for data transformation and manipulation.',
|
||||
lessonCount: 6,
|
||||
challengeCount: 5,
|
||||
lessons: [
|
||||
{ title: 'What are they?', type: 'lesson' },
|
||||
{ title: 'String Functions', type: 'lesson' },
|
||||
{ title: 'Numeric Functions', type: 'lesson' },
|
||||
{ title: 'Date Functions', type: 'lesson' },
|
||||
{ title: 'Conversion Functions', type: 'lesson' },
|
||||
{ title: 'Logical Functions', type: 'lesson' },
|
||||
{ title: 'Scalar Functions Quiz', type: 'quiz' },
|
||||
{ title: 'Customer Contact List', type: 'challenge' },
|
||||
{ title: 'Membership Duration', type: 'challenge' },
|
||||
{ title: 'Book Performance', type: 'challenge' },
|
||||
{ title: 'Book Categories', type: 'challenge' },
|
||||
{ title: 'Monthly Sales Analysis', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <GitBranchIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Subqueries and CTEs',
|
||||
description:
|
||||
'Write complex queries using subqueries and common table expressions.',
|
||||
lessonCount: 4,
|
||||
challengeCount: 6,
|
||||
lessons: [
|
||||
{ title: 'What are Subqueries?', type: 'lesson' },
|
||||
{ title: 'Correlated Subqueries', type: 'lesson' },
|
||||
{ title: 'Common Table Expressions', type: 'lesson' },
|
||||
{ title: 'Recursive CTEs', type: 'lesson' },
|
||||
{ title: 'Subqueries Quiz', type: 'quiz' },
|
||||
{ title: 'Books Above Average', type: 'challenge' },
|
||||
{ title: 'Latest Category Books', type: 'challenge' },
|
||||
{ title: 'Low Stock by Category', type: 'challenge' },
|
||||
{ title: 'Bestseller Rankings', type: 'challenge' },
|
||||
{ title: 'New Customer Analysis', type: 'challenge' },
|
||||
{ title: 'Daily Sales Report', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <ArrowUpDownIcon className="h-6 w-6 text-yellow-500" />,
|
||||
title: 'Window Functions',
|
||||
description:
|
||||
'Advanced analytics and calculations using window functions.',
|
||||
lessonCount: 5,
|
||||
challengeCount: 7,
|
||||
lessons: [
|
||||
{ title: 'What are they?', type: 'lesson' },
|
||||
{ title: 'OVER and PARTITION BY', type: 'lesson' },
|
||||
{ title: 'Use of ORDER BY', type: 'lesson' },
|
||||
{ title: 'Ranking Functions', type: 'lesson' },
|
||||
{ title: 'Window Frames', type: 'lesson' },
|
||||
{ title: 'Window Functions Quiz', type: 'quiz' },
|
||||
{ title: 'Basic Sales Metrics', type: 'challenge' },
|
||||
{ title: 'Bestseller Comparison', type: 'challenge' },
|
||||
{ title: 'Author Category Sales', type: 'challenge' },
|
||||
{ title: 'Top Authors', type: 'challenge' },
|
||||
{ title: 'Price Tier Rankings', type: 'challenge' },
|
||||
{ title: 'Month-over-Month Sales', type: 'challenge' },
|
||||
{ title: 'Price Range Analysis', type: 'challenge' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow flex-col items-center bg-gradient-to-b from-zinc-900 to-zinc-950 px-4 pb-52 pt-3 text-zinc-400 md:px-10 md:pt-8">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<a
|
||||
href="https://roadmap.sh"
|
||||
target="_blank"
|
||||
className="opacity-20 transition-opacity hover:opacity-100"
|
||||
>
|
||||
<RoadmapLogoIcon />
|
||||
</a>
|
||||
<AccountButton />
|
||||
</div>
|
||||
<div className="relative mt-7 max-w-3xl text-left md:mt-20 md:text-center">
|
||||
<Spotlight className="left-[-170px] top-[-200px]" fill="#EAB308" />
|
||||
<div className="inline-block rounded-full bg-yellow-500/10 px-4 py-1.5 text-base text-yellow-500 md:px-6 md:py-2 md:text-lg">
|
||||
<span className="hidden sm:block">
|
||||
Complete Course to Master Practical SQL
|
||||
</span>
|
||||
<span className="block sm:hidden">Complete SQL Course</span>
|
||||
</div>
|
||||
|
||||
<h1 className="mt-5 text-4xl font-bold tracking-tight text-white md:mt-8 md:text-7xl">
|
||||
Master SQL <span className="hidden min-[384px]:inline">Queries</span>
|
||||
<div className="mt-2.5 bg-gradient-to-r from-yellow-500 to-yellow-300 bg-clip-text text-transparent md:text-6xl lg:text-7xl">
|
||||
From Basic to Advanced
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<p className="mx-auto my-5 max-w-2xl text-xl text-zinc-300 md:my-12 lg:text-2xl">
|
||||
A structured course to master database querying - perfect for
|
||||
developers, data analysts, and anyone working with data.
|
||||
</p>
|
||||
|
||||
<div className="hidden flex-row items-center justify-center gap-5 md:flex">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<ClipboardIcon className="size-6 text-yellow-600" />
|
||||
<span>55+ Lessons</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<FileQuestionIcon className="size-6 text-yellow-600" />
|
||||
<span>100+ Challenges</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CodeIcon className="size-6 text-yellow-600" />
|
||||
<span>Integrated IDE</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<BrainIcon className="size-6 text-yellow-600" />
|
||||
<span>AI Tutor</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-7 flex justify-start md:mt-12 md:justify-center">
|
||||
<BuyButton variant="main" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SectionHeader
|
||||
title="Not your average SQL course"
|
||||
description="Built around a text-based interactive approach and packed with practical challenges, this course stands out with features that make it truly unique."
|
||||
className="mt-16 md:mt-32"
|
||||
/>
|
||||
|
||||
<div className="mx-auto mt-6 w-full max-w-5xl md:mt-10">
|
||||
<div className="grid grid-cols-1 gap-2 md:grid-cols-2 md:gap-4 lg:grid-cols-3">
|
||||
<CourseFeature
|
||||
title="Textual Course"
|
||||
icon={Eye}
|
||||
imgUrl="https://assets.roadmap.sh/guest/textual-course.png"
|
||||
description="Unlike video-based courses where you have to learn at the pace of the instructor, this course is text-based, allowing you to learn at your own pace."
|
||||
/>
|
||||
<CourseFeature
|
||||
title="Coding Environment"
|
||||
icon={CodeIcon}
|
||||
imgUrl="https://assets.roadmap.sh/guest/coding-environment.png"
|
||||
description="With the integrated IDE, you can practice your SQL queries in real-time, getting instant feedback on your results."
|
||||
/>
|
||||
<CourseFeature
|
||||
title="Practical Challenges"
|
||||
icon={FileQuestionIcon}
|
||||
imgUrl="https://assets.roadmap.sh/guest/coding-challenges.png"
|
||||
description="The course is packed with practical challenges and quizzes, allowing you to test your knowledge and skills."
|
||||
/>
|
||||
<CourseFeature
|
||||
title="AI Instructor"
|
||||
icon={BrainIcon}
|
||||
description="Powerful AI tutor to help you with your queries, provide additional explanations and help if you get stuck."
|
||||
imgUrl="https://assets.roadmap.sh/guest/ai-integration.png"
|
||||
/>
|
||||
<CourseFeature
|
||||
title="Take Notes"
|
||||
icon={ClipboardIcon}
|
||||
description="The course allows you to take notes, where you can write down your thoughts and ideas. You can visit them later to review your progress."
|
||||
imgUrl="https://assets.roadmap.sh/guest/course-notes.png"
|
||||
/>
|
||||
<CourseFeature
|
||||
title="Completion Certificate"
|
||||
icon={FileCheckIcon}
|
||||
imgUrl="https://assets.roadmap.sh/guest/course-certificate.jpg"
|
||||
description="The course provides a completion certificate, which you can share with your potential employers."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-7 w-full max-w-3xl text-left md:mt-9">
|
||||
<p className="text-lg leading-normal md:text-xl">
|
||||
Oh, and you get the{' '}
|
||||
<span className="bg-gradient-to-r from-yellow-500 to-yellow-300 bg-clip-text text-transparent">
|
||||
lifetime access
|
||||
</span>{' '}
|
||||
to the course including all the future updates. Also, there is a
|
||||
certificate of completion which you can share with your potential
|
||||
employers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SectionHeader
|
||||
title="Course Overview"
|
||||
description="The course is designed to help you go from SQL beginner to expert
|
||||
through hands-on practice with real-world scenarios, mastering
|
||||
everything from basic to complex queries."
|
||||
className="mt-8 md:mt-24"
|
||||
/>
|
||||
|
||||
<div className="mt-8 w-full max-w-3xl space-y-4 md:mt-12">
|
||||
{chapters.map((chapter, index) => (
|
||||
<ChapterRow key={index} counter={index + 1} {...chapter} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<SectionHeader
|
||||
title="About the Author"
|
||||
className="mt-12 md:mt-24"
|
||||
description={
|
||||
<div className="mt-2 md:mt-4 flex flex-col gap-4 md:gap-6 text-lg md:text-xl leading-[1.52]">
|
||||
<p>
|
||||
I am Kamran Ahmed, an engineering leader with over a decade of
|
||||
experience in the tech industry. Throughout my career I have built
|
||||
and scaled software systems, architected complex data systems, and
|
||||
worked with large amounts of data to create efficient solutions.
|
||||
</p>
|
||||
<p>
|
||||
I am also the creator of{' '}
|
||||
<a
|
||||
href="https://roadmap.sh"
|
||||
target="_blank"
|
||||
className="text-yellow-400"
|
||||
>
|
||||
roadmap.sh
|
||||
</a>
|
||||
, a platform trusted by millions of developers to guide their
|
||||
learning journeys. I love to simplify complex topics and make
|
||||
learning practical and accessible.
|
||||
</p>
|
||||
<p>
|
||||
In this course, I will share everything I have learned about SQL
|
||||
from the basics to advanced concepts in a way that is easy to
|
||||
understand and apply. Whether you are just starting or looking to
|
||||
sharpen your skills, you are in the right place.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<CourseAuthor />
|
||||
|
||||
<FAQSection />
|
||||
|
||||
<FloatingPurchase />
|
||||
</div>
|
||||
);
|
||||
}
|
29
src/components/SQLCourse/SectionHeader.tsx
Normal file
29
src/components/SQLCourse/SectionHeader.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type SectionHeaderProps = {
|
||||
title: string;
|
||||
description: string | React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function SectionHeader(props: SectionHeaderProps) {
|
||||
const { title, description, className } = props;
|
||||
|
||||
return (
|
||||
<div className={cn('mx-auto w-full mt-24 max-w-3xl', className)}>
|
||||
<div className="relative w-full">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="inline-flex items-center rounded-xl ">
|
||||
<span className="text-2xl md:text-3xl font-medium text-zinc-200">{title}</span>
|
||||
</div>
|
||||
<div className="h-[1px] flex-grow bg-gradient-to-r from-yellow-500/20 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
{typeof description === 'string' ? (
|
||||
<p className="mt-2 md:mt-5 text-lg md:text-xl text-zinc-400">{description}</p>
|
||||
) : (
|
||||
description
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
57
src/components/SQLCourse/Spotlight.tsx
Normal file
57
src/components/SQLCourse/Spotlight.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type SpotlightProps = {
|
||||
className?: string;
|
||||
fill?: string;
|
||||
};
|
||||
|
||||
export function Spotlight(props: SpotlightProps) {
|
||||
const { className, fill } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={cn(
|
||||
'animate-spotlight pointer-events-none absolute z-[1] h-[169%] w-[238%] opacity-0 lg:w-[138%]',
|
||||
className,
|
||||
)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 3787 2842"
|
||||
fill="none"
|
||||
>
|
||||
<g filter="url(#filter)">
|
||||
<ellipse
|
||||
cx="1924.71"
|
||||
cy="273.501"
|
||||
rx="1924.71"
|
||||
ry="273.501"
|
||||
transform="matrix(-0.822377 -0.568943 -0.568943 0.822377 3631.88 2291.09)"
|
||||
fill={fill || 'white'}
|
||||
fillOpacity="0.21"
|
||||
></ellipse>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter"
|
||||
x="0.860352"
|
||||
y="0.838989"
|
||||
width="3785.16"
|
||||
height="2840.26"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB"
|
||||
>
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix"></feFlood>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="BackgroundImageFix"
|
||||
result="shape"
|
||||
></feBlend>
|
||||
<feGaussianBlur
|
||||
stdDeviation="151"
|
||||
result="effect1_foregroundBlur_1065_8"
|
||||
></feGaussianBlur>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -108,7 +108,6 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
useKeydown(
|
||||
'r',
|
||||
() => {
|
||||
console.log(progress);
|
||||
if (progress === 'pending') {
|
||||
onClose();
|
||||
return;
|
||||
|
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
@ -7,6 +7,7 @@ interface ImportMetaEnv {
|
||||
PUBLIC_APP_URL: string;
|
||||
PUBLIC_AVATAR_BASE_URL: string;
|
||||
PUBLIC_EDITOR_APP_URL: string;
|
||||
PUBLIC_COURSE_APP_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
@ -167,7 +167,6 @@ const gaPageIdentifier = Astro.url.pathname
|
||||
|
||||
<slot name='page-footer'>
|
||||
<slot name='changelog-banner'>
|
||||
<ChangelogBanner />
|
||||
</slot>
|
||||
<slot name='open-source-banner'>
|
||||
<OpenSourceBanner />
|
||||
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
import BaseLayout, { Props as BaseLayoutProps } from './BaseLayout.astro';
|
||||
import BaseLayout, { type Props as BaseLayoutProps } from './BaseLayout.astro';
|
||||
|
||||
export interface Props extends BaseLayoutProps {}
|
||||
|
||||
|
@ -33,7 +33,6 @@ export function getUrlUtmParams(): UtmParams {
|
||||
|
||||
export function triggerUtmRegistration() {
|
||||
const utmParams = getStoredUtmParams();
|
||||
console.log(utmParams);
|
||||
if (!utmParams.utmSource) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { siteConfig } from './config.ts';
|
||||
|
||||
const formatter = Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
});
|
||||
@ -14,6 +16,16 @@ export async function getDiscordInfo(): Promise<{
|
||||
return discordStats;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
return {
|
||||
url: 'https://roadmap.sh/discord',
|
||||
total: 27000,
|
||||
totalFormatted: '27k',
|
||||
online: 49,
|
||||
onlineFormatted: '3.44k',
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
'https://discord.com/api/v9/invites/cJpEt5Qbwa?with_counts=true',
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import Cookies from 'js-cookie';
|
||||
import type { AllowedOnboardingStatus } from '../api/user';
|
||||
|
||||
export const TOKEN_COOKIE_NAME = '__roadmapsh_jt__';
|
||||
export const COURSE_PURCHASE_PARAM = 't';
|
||||
|
||||
export type TokenPayload = {
|
||||
id: string;
|
||||
|
@ -9,3 +9,15 @@ export function formatCommaNumber(number: number): string {
|
||||
export function decimalIfNeeded(number: number): string {
|
||||
return number % 1 === 0 ? number.toString() : number.toFixed(1);
|
||||
}
|
||||
|
||||
export function humanizeNumber(number: number): string {
|
||||
if (number < 1000) {
|
||||
return formatCommaNumber(number);
|
||||
}
|
||||
|
||||
if (number < 1000000) {
|
||||
return `${decimalIfNeeded(number / 1000)}k`;
|
||||
}
|
||||
|
||||
return `${decimalIfNeeded(number / 1000000)}m`;
|
||||
}
|
||||
|
16
src/pages/courses/sql.astro
Normal file
16
src/pages/courses/sql.astro
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
import { SQLCoursePage } from '../../components/SQLCourse/SQLCoursePage.tsx';
|
||||
import SkeletonLayout from '../../layouts/SkeletonLayout.astro';
|
||||
---
|
||||
|
||||
<SkeletonLayout
|
||||
title='Master SQL'
|
||||
briefTitle='Learn SQL from the ground up'
|
||||
ogImageUrl='https://assets.roadmap.sh/guest/sql-course-bjc53.png'
|
||||
description='Learn SQL from the ground up. This course covers the basics of SQL, intermediate concepts, and advanced topics.'
|
||||
keywords={['sql', 'database', 'database management', 'database administration']}
|
||||
canonicalUrl='/courses/sql'
|
||||
noIndex={true}
|
||||
>
|
||||
<SQLCoursePage client:load />
|
||||
</SkeletonLayout>
|
@ -19,8 +19,8 @@ const ogImageUrl =
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={guideData.seo.title}
|
||||
description={guideData.seo.description}
|
||||
title={replaceVariables(guideData.seo.title)}
|
||||
description={replaceVariables(guideData.seo.description)}
|
||||
permalink={guideData.excludedBySlug}
|
||||
canonicalUrl={guideData.canonicalUrl}
|
||||
ogImageUrl={ogImageUrl}
|
||||
|
@ -40,6 +40,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { SectionBadge } from '../components/GetStarted/SectionBadge';
|
||||
import { TipItem } from '../components/GetStarted/TipItem';
|
||||
import ChangelogBanner from '../components/ChangelogBanner.astro';
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@ -508,4 +509,5 @@ import { TipItem } from '../components/GetStarted/TipItem';
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChangelogBanner slot='changelog-banner' />
|
||||
</BaseLayout>
|
||||
|
@ -9,6 +9,7 @@ import { getAllGuides } from '../lib/guide';
|
||||
import { getRoadmapsByTag } from '../lib/roadmap';
|
||||
import { getAllVideos } from '../lib/video';
|
||||
import { getAllQuestionGroups } from '../lib/question-group';
|
||||
import ChangelogBanner from '../components/ChangelogBanner.astro';
|
||||
|
||||
const roleRoadmaps = await getRoadmapsByTag('role-roadmap');
|
||||
const skillRoadmaps = await getRoadmapsByTag('skill-roadmap');
|
||||
@ -23,7 +24,7 @@ const projectGroups = [
|
||||
title: 'Backend',
|
||||
id: 'backend',
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
const guides = await getAllGuides();
|
||||
const questionGuides = (await getAllQuestionGroups()).filter(
|
||||
@ -107,4 +108,5 @@ const videos = await getAllVideos();
|
||||
<FeaturedVideos heading='Videos' videos={videos.slice(0, 7)} />
|
||||
</div>
|
||||
</div>
|
||||
<ChangelogBanner slot='changelog-banner' />
|
||||
</BaseLayout>
|
||||
|
@ -5,6 +5,7 @@ import GridItem from '../components/GridItem.astro';
|
||||
import SimplePageHeader from '../components/SimplePageHeader.astro';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { getRoadmapsByTag } from '../lib/roadmap';
|
||||
import ChangelogBanner from '../components/ChangelogBanner.astro';
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@ -14,4 +15,5 @@ import { getRoadmapsByTag } from '../lib/roadmap';
|
||||
>
|
||||
<RoadmapsPageHeader client:load />
|
||||
<RoadmapsPage client:load />
|
||||
<ChangelogBanner slot='changelog-banner' />
|
||||
</BaseLayout>
|
||||
|
25
src/queries/billing.ts
Normal file
25
src/queries/billing.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import { httpGet } from '../lib/query-http';
|
||||
|
||||
type CoursePriceParams = {
|
||||
courseSlug: string;
|
||||
};
|
||||
|
||||
type CoursePriceResponse = {
|
||||
flag: string;
|
||||
fullPrice: number;
|
||||
regionalPrice: number;
|
||||
regionalDiscountPercentage: number;
|
||||
isEligibleForDiscount: boolean;
|
||||
};
|
||||
|
||||
export function coursePriceOptions(params: CoursePriceParams) {
|
||||
return queryOptions({
|
||||
queryKey: ['course-price', params],
|
||||
queryFn: async () => {
|
||||
return httpGet<CoursePriceResponse>(
|
||||
`/v1-course-price/${params.courseSlug}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
41
src/queries/course-progress.ts
Normal file
41
src/queries/course-progress.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import { isLoggedIn } from '../lib/jwt';
|
||||
import { httpGet } from '../lib/query-http';
|
||||
|
||||
export interface CourseProgressDocument {
|
||||
_id: string;
|
||||
userId: string;
|
||||
courseId: string;
|
||||
completed: {
|
||||
chapterId: string;
|
||||
lessonId: string;
|
||||
completedAt: Date;
|
||||
}[];
|
||||
review?: {
|
||||
rating: number;
|
||||
feedback?: string;
|
||||
};
|
||||
|
||||
enrolledAt?: Date;
|
||||
completedAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type CourseProgressResponse = Pick<
|
||||
CourseProgressDocument,
|
||||
'completed' | 'completedAt' | 'review' | 'enrolledAt'
|
||||
>;
|
||||
|
||||
export function courseProgressOptions(courseSlug: string) {
|
||||
return queryOptions({
|
||||
queryKey: ['course-progress', courseSlug],
|
||||
retryOnMount: false,
|
||||
queryFn: async () => {
|
||||
return httpGet<CourseProgressResponse>(
|
||||
`/v1-course-progress/${courseSlug}`,
|
||||
);
|
||||
},
|
||||
enabled: !!isLoggedIn(),
|
||||
});
|
||||
}
|
@ -56,6 +56,9 @@ h3 > code {
|
||||
.prose ul li > code:before,
|
||||
p > code:before,
|
||||
.prose ul li > code:after,
|
||||
.prose ol li > code:before,
|
||||
p > code:before,
|
||||
.prose ol li > code:after,
|
||||
p > code:after,
|
||||
a > code:after,
|
||||
a > code:before {
|
||||
@ -113,6 +116,18 @@ a > code:before {
|
||||
animation: barberpole 15s linear infinite;
|
||||
}
|
||||
|
||||
.striped-loader-yellow {
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
transparent,
|
||||
transparent 5px,
|
||||
hsla(55, 100%, 50%, 0.7) 5px,
|
||||
hsla(55, 100%, 50%, 0.7) 10px
|
||||
);
|
||||
background-size: 200% 200%;
|
||||
animation: barberpole 15s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes barberpole {
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
|
@ -37,11 +37,22 @@ module.exports = {
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
spotlight: {
|
||||
"0%": {
|
||||
opacity: 0,
|
||||
transform: "translate(-72%, -62%) scale(0.5)",
|
||||
},
|
||||
"100%": {
|
||||
opacity: 1,
|
||||
transform: "translate(-50%,-40%) scale(1)",
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'fade-slide-up':
|
||||
'fade-slide-up 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards',
|
||||
'fade-in': 'fade-in 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards',
|
||||
spotlight: "spotlight 2s ease 0.25s 1 forwards",
|
||||
},
|
||||
},
|
||||
container: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user