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

Improve AI Tutor landing page, and Sidebar (#8969)

* Add tree to AI tutor sidebar

* Update featured card design

* Featured card changes

* Improve feature cards

* Active item bug in ai tutor sidebar

* Add demo button on premium page
This commit is contained in:
Kamran Ahmed
2025-07-28 21:34:09 +01:00
committed by GitHub
parent 870a8c409c
commit 53be600df4
6 changed files with 195 additions and 71 deletions

1
.astro/types.d.ts vendored
View File

@@ -1,2 +1 @@
/// <reference types="astro/client" /> /// <reference types="astro/client" />
/// <reference path="content.d.ts" />

View File

@@ -1,26 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { import {
BookOpen, BookOpen,
ChevronDown,
ChevronRight,
Compass, Compass,
FileText,
Map, Map,
MessageCircle, MessageCircle,
Plus,
Star, Star,
Swords, Swords,
X, X,
Zap
} from 'lucide-react'; } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { isLoggedIn } from '../../lib/jwt'; import { getUrlParams } from '../../lib/browser';
import { useIsPaidUser } from '../../queries/billing';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AITutorLogo } from '../ReactIcons/AITutorLogo';
import { queryClient } from '../../stores/query-client';
import { aiLimitOptions } from '../../queries/ai-course';
import { useQuery } from '@tanstack/react-query';
import { getPercentage } from '../../lib/number';
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { UserDropdown } from './UserDropdown'; import { isLoggedIn } from '../../lib/jwt';
import { aiLimitOptions } from '../../queries/ai-course';
import { useIsPaidUser } from '../../queries/billing';
import { queryClient } from '../../stores/query-client';
import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal';
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
import { AITutorLogo } from '../ReactIcons/AITutorLogo';
import { UpgradeSidebarCard } from './UpgradeSidebarCard'; import { UpgradeSidebarCard } from './UpgradeSidebarCard';
import { UserDropdown } from './UserDropdown';
type AITutorSidebarProps = { type AITutorSidebarProps = {
isFloating: boolean; isFloating: boolean;
@@ -33,13 +36,33 @@ const sidebarItems = [
key: 'new', key: 'new',
label: 'Create with AI', label: 'Create with AI',
href: '/ai', href: '/ai',
icon: Plus, icon: Zap,
}, children: [
{ {
key: 'quiz', key: 'create-course',
label: 'Test my Skills', label: 'Course',
href: '/ai/quiz', href: '/ai?format=course',
icon: Swords, icon: BookOpen,
},
{
key: 'create-guide',
label: 'Guide',
href: '/ai?format=guide',
icon: FileText,
},
{
key: 'create-roadmap',
label: 'Roadmap',
href: '/ai?format=roadmap',
icon: Map,
},
{
key: 'quiz',
label: 'Quiz',
href: '/ai/quiz',
icon: Swords,
},
],
}, },
{ {
key: 'chat', key: 'chat',
@@ -78,7 +101,11 @@ export type AITutorTab = (typeof sidebarItems)[number]['key'];
export function AITutorSidebar(props: AITutorSidebarProps) { export function AITutorSidebar(props: AITutorSidebarProps) {
const { activeTab, isFloating, onClose } = props; const { activeTab, isFloating, onClose } = props;
const [format, setFormat] = useState('');
const [isInitialLoad, setIsInitialLoad] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true);
const [expandedItems, setExpandedItems] = useState<Record<string, boolean>>({
new: true, // Keep "Create with AI" expanded by default
});
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false); const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false); const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
@@ -89,15 +116,21 @@ export function AITutorSidebar(props: AITutorSidebarProps) {
queryClient, queryClient,
); );
const { used, limit } = limits ?? { used: 0, limit: 0 };
const totalPercentage = getPercentage(used, limit);
useEffect(() => { useEffect(() => {
const { format } = getUrlParams();
setFormat(format || 'course');
setIsInitialLoad(false); setIsInitialLoad(false);
}, []); }, []);
const isLoading = isPaidUserLoading || isLimitsLoading; const isLoading = isPaidUserLoading || isLimitsLoading;
const toggleExpanded = (key: string) => {
setExpandedItems((prev) => ({
...prev,
[key]: !prev[key],
}));
};
return ( return (
<> <>
{isUpgradeModalOpen && ( {isUpgradeModalOpen && (
@@ -156,7 +189,27 @@ export function AITutorSidebar(props: AITutorSidebarProps) {
<AITutorSidebarItem <AITutorSidebarItem
item={item} item={item}
isActive={activeTab === item.key} isActive={activeTab === item.key}
isExpanded={expandedItems[item.key] || false}
onToggleExpanded={() => toggleExpanded(item.key)}
/> />
{item.children && expandedItems[item.key] && (
<ul className="relative list-none">
<div className="absolute top-0 bottom-0 left-7 w-px bg-gray-200" />
{item.children.map((child) => (
<li key={child.key}>
<AITutorSidebarItem
item={child}
isActive={
(activeTab === item.key &&
`create-${format}` === child.key) ||
activeTab === child.key
}
isChild={true}
/>
</li>
))}
</ul>
)}
</li> </li>
))} ))}
@@ -179,35 +232,83 @@ export function AITutorSidebar(props: AITutorSidebarProps) {
); );
} }
type SidebarItem = {
key: string;
label: string;
href: string;
icon: any;
children?: {
key: string;
label: string;
href: string;
icon: any;
}[];
};
type ChildItem = {
key: string;
label: string;
href: string;
icon: any;
};
type AITutorSidebarItemProps = { type AITutorSidebarItemProps = {
item: (typeof sidebarItems)[number]; item: SidebarItem | ChildItem;
as?: 'a' | 'button'; as?: 'a' | 'button';
onClick?: () => void; onClick?: () => void;
className?: string; className?: string;
isActive?: boolean; isActive?: boolean;
isExpanded?: boolean;
onToggleExpanded?: () => void;
isChild?: boolean;
}; };
function AITutorSidebarItem(props: AITutorSidebarItemProps) { function AITutorSidebarItem(props: AITutorSidebarItemProps) {
const { item, as = 'a', onClick, className, isActive } = props; const {
item,
as = 'a',
onClick,
className,
isActive,
isExpanded,
onToggleExpanded,
isChild,
} = props;
const Component = as; const hasChildren = 'children' in item && item.children;
const Component = hasChildren ? 'button' : as;
return ( return (
<Component <Component
{...(as === 'a' ? { href: item.href } : {})} {...(Component === 'a' && !hasChildren ? { href: item.href } : {})}
{...(as === 'button' ? { onClick } : {})} {...(Component === 'button'
? { onClick: hasChildren ? onToggleExpanded : onClick }
: {})}
className={cn( className={cn(
'font-regular flex w-full items-center border-r-2 px-5 py-2 text-sm transition-all', 'font-regular flex w-full items-center border-r-2 px-5 py-2 text-sm transition-all',
isActive isActive && !hasChildren
? 'border-r-black bg-gray-100 text-black' ? 'border-r-black bg-gray-100 text-black'
: 'border-r-transparent text-gray-500 hover:border-r-gray-300', : 'border-r-transparent text-gray-500',
!isActive && !hasChildren && 'hover:bg-gray-50 hover:text-gray-700',
!isActive && hasChildren && 'hover:text-gray-700',
isChild && 'border-r-transparent py-1.5 pl-11',
isChild && isActive && 'border-r-black border-r-2 bg-gray-100 text-black',
className, className,
)} )}
> >
<span className="flex grow items-center"> <span className="flex grow items-center">
<item.icon className="mr-2 size-4" /> {!isChild && <item.icon className="mr-2 size-4" />}
{item.label} {item.label}
</span> </span>
{hasChildren && (
<span className="ml-auto">
{isExpanded ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
</span>
)}
</Component> </Component>
); );
} }

View File

@@ -21,7 +21,7 @@ import {
} from './QuestionAnswerChat'; } from './QuestionAnswerChat';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { getUrlParams } from '../../lib/browser'; import { getUrlParams, setUrlParams } from '../../lib/browser';
import { useParams } from '../../hooks/use-params'; import { useParams } from '../../hooks/use-params';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { aiLimitOptions } from '../../queries/ai-course'; import { aiLimitOptions } from '../../queries/ai-course';
@@ -205,7 +205,10 @@ export function ContentGenerator() {
<FormatItem <FormatItem
key={format.value} key={format.value}
label={format.label} label={format.label}
onClick={() => setSelectedFormat(format.value)} onClick={() => {
setSelectedFormat(format.value);
setUrlParams({ format: format.value });
}}
icon={format.icon} icon={format.icon}
isSelected={isSelected} isSelected={isSelected}
/> />

View File

@@ -1,4 +1,5 @@
import { Lock, Play } from 'lucide-react'; import { Lock, Play } from 'lucide-react';
import { markdownToHtml } from '../../lib/markdown';
interface FeatureCardProps { interface FeatureCardProps {
title: string; title: string;
@@ -42,29 +43,32 @@ export function FeatureCard(props: FeatureCardProps) {
} }
return ( return (
<button <div className="group relative overflow-hidden rounded-lg border border-slate-700 bg-[#151F33] p-4 hover:border-blue-400">
onClick={onClick} <button
className="group relative overflow-hidden rounded-lg border border-slate-700 bg-[#151F33] p-4 text-left hover:border-blue-400" onClick={onClick}
> className="relative block aspect-video w-full cursor-pointer overflow-hidden rounded-lg bg-slate-900/50"
<span className="group relative block aspect-video w-full cursor-pointer overflow-hidden rounded-lg bg-slate-900/50"> >
<img <img
src={thumbnail} src={thumbnail}
alt={title} alt={title}
className="absolute inset-0 h-full w-full object-cover" className="absolute inset-0 h-full w-full object-cover"
/> />
</span> <span className="absolute inset-0 -m-4 flex items-center justify-center bg-black/30 opacity-0 group-hover:opacity-100">
<span className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 group-hover:opacity-100"> <span className="flex h-12 w-12 items-center justify-center rounded-full bg-white/10 backdrop-blur-sm">
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-white/10 backdrop-blur-sm"> <Play className="h-6 w-6 fill-current text-white" strokeWidth={2} />
<Play className="h-6 w-6 fill-current text-white" strokeWidth={2} /> </span>
</span> </span>
</span> </button>
<span className="absolute top-1 right-1 rounded bg-black/60 px-2 py-1 text-xs text-white backdrop-blur-sm"> <span className="absolute top-1 right-1 rounded bg-black/60 px-2 py-1 text-xs text-white backdrop-blur-sm">
{duration} {duration}
</span> </span>
<div className="mt-4"> <div className="mt-4">
<h3 className="mb-1 text-sm font-medium text-white">{title}</h3> <h3 className="mb-1 text-sm font-medium text-white">{title}</h3>
<p className="text-xs leading-relaxed text-slate-400">{description}</p> <p
className="text-xs [&_a]:text-blue-400 [&_a]:underline-offset-2 [&_a]:underline leading-relaxed text-slate-400"
dangerouslySetInnerHTML={{ __html: markdownToHtml(description) }}
/>
</div> </div>
</button> </div>
); );
} }

View File

@@ -3,6 +3,7 @@ import {
Bot, Bot,
CheckCircle, CheckCircle,
Clock, Clock,
MousePointerClick,
PartyPopper, PartyPopper,
Users2, Users2,
Wand2, Wand2,
@@ -161,10 +162,10 @@ export function PremiumPage() {
Generate unlimited courses about any topic, get career guidance Generate unlimited courses about any topic, get career guidance
and instant answers from AI, test your skills and more and instant answers from AI, test your skills and more
</p> </p>
<div className="flex justify-center"> <div className="flex flex-col items-center justify-center gap-4 sm:flex-row">
<a <a
href="#pricing" href="#pricing"
className="group mx-auto block rounded-2xl bg-gradient-to-b from-indigo-600 to-indigo-700 px-8 py-4 shadow-lg transition-all hover:-translate-y-0.5 hover:shadow-xl hover:shadow-indigo-500/25" className="group block rounded-2xl bg-gradient-to-b from-indigo-600 to-indigo-700 px-8 py-4 shadow-sm transition-all hover:-translate-y-0.5"
> >
<span className="flex items-center justify-center gap-3 text-lg text-white sm:hidden"> <span className="flex items-center justify-center gap-3 text-lg text-white sm:hidden">
Upgrade now Upgrade now
@@ -179,6 +180,21 @@ export function PremiumPage() {
</span> </span>
</span> </span>
</a> </a>
<button
data-demo-button="true"
onClick={() => {
if (isLoggedIn()) {
window.location.href = '/ai';
} else {
showLoginPopup();
}
}}
className="group relative flex items-center justify-center gap-2 overflow-hidden rounded-2xl border border-purple-500/30 bg-transparent px-6 py-4 text-base font-medium text-purple-500 shadow-sm transition-all hover:-translate-y-0.5 hover:bg-purple-500/10 hover:text-purple-400 hover:border-purple-300/30 focus:outline-none active:ring-0"
>
<MousePointerClick className="h-5 w-5" aria-hidden="true" />
<span className="lg:hidden">Try Demo</span>
<span className="hidden lg:inline">Try for Free</span>
</button>
</div> </div>
<p className="mt-5 flex items-center justify-center gap-2 text-sm"> <p className="mt-5 flex items-center justify-center gap-2 text-sm">
<Clock className="h-3 w-3 text-white" /> <Clock className="h-3 w-3 text-white" />

View File

@@ -10,18 +10,10 @@ export const paidFeaturesList = [
]; ];
export const features = [ export const features = [
{
title: 'Chat with Roadmaps',
description:
'Ask questions and get instant answers on any roadmap through AI',
videoId: 'fq0UgNcj3Ek',
thumbnail: 'https://assets.roadmap.sh/guest/chat-with-roadmaps-ew2l9.png',
duration: '2:17',
},
{ {
title: 'Unlimited AI Courses', title: 'Unlimited AI Courses',
description: description:
'No more paying for expensive courses, create unlimited courses with AI', 'No more paying for expensive courses, [create unlimited courses](/ai) with AI',
videoId: 'uCcQNhdVUVQ', videoId: 'uCcQNhdVUVQ',
thumbnail: 'https://assets.roadmap.sh/guest/ai-courses-m07ra.png', thumbnail: 'https://assets.roadmap.sh/guest/ai-courses-m07ra.png',
duration: '3:07', duration: '3:07',
@@ -29,15 +21,32 @@ export const features = [
{ {
title: 'In-depth Guides', title: 'In-depth Guides',
description: description:
'Create unlimited personalized in-depth learning guides with AI', 'Create unlimited personalized [in-depth learning guides](/ai?format=guide) with AI',
videoId: '5kwYjCg2Lu4', videoId: '5kwYjCg2Lu4',
thumbnail: 'https://assets.roadmap.sh/guest/ai-guides-s4bjj.png', thumbnail: 'https://assets.roadmap.sh/guest/ai-guides-s4bjj.png',
duration: '1:33', duration: '1:33',
}, },
{
title: 'Learn Roadmap Topics',
description:
'Learn [roadmap topics directly](/backend) from within the nodes without leaving the roadmap',
videoId: '0jZ1Lse1Y3E',
thumbnail: 'https://assets.roadmap.sh/guest/smarter-roadmaps-f46ku.png',
duration: '3:11',
startTime: '5',
},
{
title: 'Chat with Roadmaps',
description:
'Ask questions and [get instant answers](/frontend) on any roadmap through AI',
videoId: 'fq0UgNcj3Ek',
thumbnail: 'https://assets.roadmap.sh/guest/chat-with-roadmaps-ew2l9.png',
duration: '2:17',
},
{ {
title: 'AI as Learning Companion', title: 'AI as Learning Companion',
description: description:
'Use AI-powered learning companion to accelerate your roadmap progress', 'Use [AI-powered learning companion](/ai/roadmap-chat) to accelerate your roadmap progress',
videoId: 'Jso_HRviEOE', videoId: 'Jso_HRviEOE',
thumbnail: 'https://assets.roadmap.sh/guest/roadmap-ai-tools-adhqq.png', thumbnail: 'https://assets.roadmap.sh/guest/roadmap-ai-tools-adhqq.png',
duration: '2:45', duration: '2:45',
@@ -46,24 +55,16 @@ export const features = [
{ {
title: 'Your Personal Coach', title: 'Your Personal Coach',
description: description:
'Get career guidance, roadmap and personalized growth suggestions', 'Get career guidance, personalized growth suggestions using [AI Tutor Chat](/ai/chat)',
videoId: '77VwAeFmoIw', videoId: '77VwAeFmoIw',
thumbnail: 'https://assets.roadmap.sh/guest/career-guidance-t2mpu.png', thumbnail: 'https://assets.roadmap.sh/guest/career-guidance-t2mpu.png',
duration: '3:45', duration: '3:45',
startTime: '4', startTime: '4',
}, },
{
title: 'Learn Roadmap Topics',
description:
'Learn roadmap topics directly from within the nodes without leaving the roadmap',
videoId: '0jZ1Lse1Y3E',
thumbnail: 'https://assets.roadmap.sh/guest/smarter-roadmaps-f46ku.png',
duration: '3:11',
startTime: '5'
},
{ {
title: 'Test Yourself', title: 'Test Yourself',
description: 'Test your knowledge and prepare for interviews with AI', description:
'Let AI help you [test your knowledge](/ai/quiz) and prepare for interviews',
videoId: 'ScruGC8uzjo', videoId: 'ScruGC8uzjo',
thumbnail: 'https://assets.roadmap.sh/guest/test-yourself-uwzqo.png', thumbnail: 'https://assets.roadmap.sh/guest/test-yourself-uwzqo.png',
duration: '2:15', duration: '2:15',
@@ -71,7 +72,7 @@ export const features = [
{ {
title: 'Powerful Roadmap Editor', title: 'Powerful Roadmap Editor',
description: description:
'Create and edit roadmaps with ease using our powerful roadmap editor', '[Create and edit roadmaps](/account/roadmaps) with ease using our powerful roadmap editor',
videoId: 'L2HZIHIgwOI', videoId: 'L2HZIHIgwOI',
thumbnail: thumbnail:
'https://assets.roadmap.sh/guest/ai-based-roadmap-editor-ochm8.png', 'https://assets.roadmap.sh/guest/ai-based-roadmap-editor-ochm8.png',