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:
1 parent
870a8c409c
commit
53be600df4
6 files changed
+195
-71
No files matched your search
@@ -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,13 +36,33 @@ const sidebarItems = [
|
||||
key: 'new',
|
||||
label: 'Create with AI',
|
||||
href: '/ai',
|
||||
icon: Plus,
|
||||
},
|
||||
{
|
||||
key: 'quiz',
|
||||
label: 'Test my Skills',
|
||||
href: '/ai/quiz',
|
||||
icon: Swords,
|
||||
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: 'Quiz',
|
||||
href: '/ai/quiz',
|
||||
icon: Swords,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'chat',
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user