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 path="content.d.ts" />

View File

@@ -1,26 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import {
BookOpen,
ChevronDown,
ChevronRight,
Compass,
FileText,
Map,
MessageCircle,
Plus,
Star,
Swords,
X,
Zap
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { isLoggedIn } from '../../lib/jwt';
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 { getUrlParams } from '../../lib/browser';
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 { UserDropdown } from './UserDropdown';
type AITutorSidebarProps = {
isFloating: boolean;
@@ -33,14 +36,34 @@ const sidebarItems = [
key: 'new',
label: 'Create with AI',
href: '/ai',
icon: Plus,
icon: Zap,
children: [
{
key: 'create-course',
label: 'Course',
href: '/ai?format=course',
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: 'Test my Skills',
label: 'Quiz',
href: '/ai/quiz',
icon: Swords,
},
],
},
{
key: 'chat',
label: 'Ask AI Tutor',
@@ -78,7 +101,11 @@ export type AITutorTab = (typeof sidebarItems)[number]['key'];
export function AITutorSidebar(props: AITutorSidebarProps) {
const { activeTab, isFloating, onClose } = props;
const [format, setFormat] = useState('');
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 [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
@@ -89,15 +116,21 @@ export function AITutorSidebar(props: AITutorSidebarProps) {
queryClient,
);
const { used, limit } = limits ?? { used: 0, limit: 0 };
const totalPercentage = getPercentage(used, limit);
useEffect(() => {
const { format } = getUrlParams();
setFormat(format || 'course');
setIsInitialLoad(false);
}, []);
const isLoading = isPaidUserLoading || isLimitsLoading;
const toggleExpanded = (key: string) => {
setExpandedItems((prev) => ({
...prev,
[key]: !prev[key],
}));
};
return (
<>
{isUpgradeModalOpen && (
@@ -156,7 +189,27 @@ export function AITutorSidebar(props: AITutorSidebarProps) {
<AITutorSidebarItem
item={item}
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>
))}
@@ -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 = {
item: (typeof sidebarItems)[number];
item: SidebarItem | ChildItem;
as?: 'a' | 'button';
onClick?: () => void;
className?: string;
isActive?: boolean;
isExpanded?: boolean;
onToggleExpanded?: () => void;
isChild?: boolean;
};
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 (
<Component
{...(as === 'a' ? { href: item.href } : {})}
{...(as === 'button' ? { onClick } : {})}
{...(Component === 'a' && !hasChildren ? { href: item.href } : {})}
{...(Component === 'button'
? { onClick: hasChildren ? onToggleExpanded : onClick }
: {})}
className={cn(
'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-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,
)}
>
<span className="flex grow items-center">
<item.icon className="mr-2 size-4" />
{!isChild && <item.icon className="mr-2 size-4" />}
{item.label}
</span>
{hasChildren && (
<span className="ml-auto">
{isExpanded ? (
<ChevronDown className="size-4" />
) : (
<ChevronRight className="size-4" />
)}
</span>
)}
</Component>
);
}

View File

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

View File

@@ -1,4 +1,5 @@
import { Lock, Play } from 'lucide-react';
import { markdownToHtml } from '../../lib/markdown';
interface FeatureCardProps {
title: string;
@@ -42,29 +43,32 @@ export function FeatureCard(props: FeatureCardProps) {
}
return (
<div className="group relative overflow-hidden rounded-lg border border-slate-700 bg-[#151F33] p-4 hover:border-blue-400">
<button
onClick={onClick}
className="group relative overflow-hidden rounded-lg border border-slate-700 bg-[#151F33] p-4 text-left hover:border-blue-400"
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
src={thumbnail}
alt={title}
className="absolute inset-0 h-full w-full object-cover"
/>
</span>
<span className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 group-hover:opacity-100">
<span className="absolute inset-0 -m-4 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">
<Play className="h-6 w-6 fill-current text-white" strokeWidth={2} />
</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">
{duration}
</span>
<div className="mt-4">
<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>
);
}

View File

@@ -3,6 +3,7 @@ import {
Bot,
CheckCircle,
Clock,
MousePointerClick,
PartyPopper,
Users2,
Wand2,
@@ -161,10 +162,10 @@ export function PremiumPage() {
Generate unlimited courses about any topic, get career guidance
and instant answers from AI, test your skills and more
</p>
<div className="flex justify-center">
<div className="flex flex-col items-center justify-center gap-4 sm:flex-row">
<a
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">
Upgrade now
@@ -179,6 +180,21 @@ export function PremiumPage() {
</span>
</span>
</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>
<p className="mt-5 flex items-center justify-center gap-2 text-sm">
<Clock className="h-3 w-3 text-white" />

View File

@@ -10,18 +10,10 @@ export const paidFeaturesList = [
];
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',
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',
thumbnail: 'https://assets.roadmap.sh/guest/ai-courses-m07ra.png',
duration: '3:07',
@@ -29,15 +21,32 @@ export const features = [
{
title: 'In-depth Guides',
description:
'Create unlimited personalized in-depth learning guides with AI',
'Create unlimited personalized [in-depth learning guides](/ai?format=guide) with AI',
videoId: '5kwYjCg2Lu4',
thumbnail: 'https://assets.roadmap.sh/guest/ai-guides-s4bjj.png',
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',
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',
thumbnail: 'https://assets.roadmap.sh/guest/roadmap-ai-tools-adhqq.png',
duration: '2:45',
@@ -46,24 +55,16 @@ export const features = [
{
title: 'Your Personal Coach',
description:
'Get career guidance, roadmap and personalized growth suggestions',
'Get career guidance, personalized growth suggestions using [AI Tutor Chat](/ai/chat)',
videoId: '77VwAeFmoIw',
thumbnail: 'https://assets.roadmap.sh/guest/career-guidance-t2mpu.png',
duration: '3:45',
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',
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',
thumbnail: 'https://assets.roadmap.sh/guest/test-yourself-uwzqo.png',
duration: '2:15',
@@ -71,7 +72,7 @@ export const features = [
{
title: 'Powerful Roadmap Editor',
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',
thumbnail:
'https://assets.roadmap.sh/guest/ai-based-roadmap-editor-ochm8.png',