1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-25 16:39:02 +02:00

feat: add tab navigation for chat and topic details

This commit is contained in:
Arik Chakma
2025-05-24 02:24:53 +06:00
parent 00a8954398
commit 2208c66e97
3 changed files with 106 additions and 48 deletions

View File

@@ -65,6 +65,8 @@ export type RoamdapAIChatHistoryType = {
jsx?: React.ReactNode; jsx?: React.ReactNode;
}; };
export type RoadmapAIChatTab = 'chat' | 'topic';
type RoadmapAIChatProps = { type RoadmapAIChatProps = {
roadmapId: string; roadmapId: string;
}; };
@@ -82,6 +84,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
const [selectedTopicTitle, setSelectedTopicTitle] = useState<string | null>( const [selectedTopicTitle, setSelectedTopicTitle] = useState<string | null>(
null, null,
); );
const [activeTab, setActiveTab] = useState<RoadmapAIChatTab>('chat');
const [aiChatHistory, setAiChatHistory] = useState< const [aiChatHistory, setAiChatHistory] = useState<
RoamdapAIChatHistoryType[] RoamdapAIChatHistoryType[]
@@ -178,6 +181,7 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
flushSync(() => { flushSync(() => {
setSelectedTopicId(topicId); setSelectedTopicId(topicId);
setSelectedTopicTitle(topicTitle); setSelectedTopicTitle(topicTitle);
setActiveTab('topic');
}); });
const topicWithSlug = slugify(topicTitle) + '@' + topicId; const topicWithSlug = slugify(topicTitle) + '@' + topicId;
@@ -389,41 +393,6 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
</div> </div>
<div className="relative flex h-full max-w-[40%] flex-grow flex-col border-l border-gray-200 bg-white"> <div className="relative flex h-full max-w-[40%] flex-grow flex-col border-l border-gray-200 bg-white">
{selectedTopicId && (
<>
<TopicDetail
resourceId={selectedTopicId}
resourceType="roadmap"
renderer="editor"
canSubmitContribution={false}
wrapperClassName="static mx-auto sm:pt-14 pt-14"
overlayClassName="hidden"
onClose={() => setSelectedTopicId(null)}
shouldCloseOnBackdropClick={false}
shouldCloseOnEscape={false}
/>
<div className="absolute top-0 left-0 z-99 flex w-full items-center justify-between gap-2 bg-gray-100 p-1">
<div className="flex items-center gap-2 px-4 py-2 text-sm">
AI Tutor{' '}
<ChevronRightIcon className="size-4 shrink-0 stroke-[2.5] text-gray-500" />{' '}
{selectedTopicTitle}
</div>
<button
className="flex cursor-pointer items-center gap-2 rounded-lg p-2 text-black hover:bg-gray-200"
onClick={() => setSelectedTopicId(null)}
>
<XIcon className="size-3 shrink-0" strokeWidth={2.5} />
</button>
</div>
</>
)}
{!selectedTopicId && (
<>
{showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
)}
<RoadmapAIChatHeader <RoadmapAIChatHeader
isLoading={isDataLoading} isLoading={isDataLoading}
hasChatHistory={hasChatHistory} hasChatHistory={hasChatHistory}
@@ -434,8 +403,46 @@ export function RoadmapAIChat(props: RoadmapAIChatProps) {
onUpgrade={() => { onUpgrade={() => {
setShowUpgradeModal(true); setShowUpgradeModal(true);
}} }}
activeTab={activeTab}
onTabChange={(tab) => {
setActiveTab(tab);
if (tab === 'topic' && selectedTopicId && selectedTopicTitle) {
handleSelectTopic(selectedTopicId, selectedTopicTitle);
}
}}
onCloseTopic={() => {
setSelectedTopicId(null);
setSelectedTopicTitle(null);
setActiveTab('chat');
}}
selectedTopicId={selectedTopicId}
/> />
{activeTab === 'topic' && selectedTopicId && (
<TopicDetail
resourceId={selectedTopicId}
resourceType="roadmap"
renderer="editor"
canSubmitContribution={false}
wrapperClassName="static mx-auto h-auto grow"
overlayClassName="hidden"
closeButtonClassName="hidden"
onClose={() => {
setSelectedTopicId(null);
setSelectedTopicTitle(null);
setActiveTab('chat');
}}
shouldCloseOnBackdropClick={false}
shouldCloseOnEscape={false}
/>
)}
{activeTab === 'chat' && (
<>
{showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
)}
<div className="relative grow overflow-y-auto" ref={scrollareaRef}> <div className="relative grow overflow-y-auto" ref={scrollareaRef}>
{isLoading && ( {isLoading && (
<div className="absolute inset-0 flex h-full w-full items-center justify-center"> <div className="absolute inset-0 flex h-full w-full items-center justify-center">

View File

@@ -3,12 +3,16 @@ import { getAiCourseLimitOptions } from '../../queries/ai-course';
import { queryClient } from '../../stores/query-client'; import { queryClient } from '../../stores/query-client';
import { billingDetailsOptions } from '../../queries/billing'; import { billingDetailsOptions } from '../../queries/billing';
import { isLoggedIn } from '../../lib/jwt'; import { isLoggedIn } from '../../lib/jwt';
import { BotIcon, GiftIcon, Trash2Icon } from 'lucide-react'; import { BookIcon, BotIcon, GiftIcon, Trash2Icon, XIcon } from 'lucide-react';
import type { RoamdapAIChatHistoryType } from './RoadmapAIChat'; import type {
RoadmapAIChatTab,
RoamdapAIChatHistoryType,
} from './RoadmapAIChat';
import { useState } from 'react'; import { useState } from 'react';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { getPercentage } from '../../lib/number'; import { getPercentage } from '../../lib/number';
import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup'; import { AILimitsPopup } from '../GenerateCourse/AILimitsPopup';
import { cn } from '../../lib/classname';
type RoadmapAIChatHeaderProps = { type RoadmapAIChatHeaderProps = {
isLoading: boolean; isLoading: boolean;
@@ -18,6 +22,11 @@ type RoadmapAIChatHeaderProps = {
onLogin: () => void; onLogin: () => void;
onUpgrade: () => void; onUpgrade: () => void;
activeTab: RoadmapAIChatTab;
onTabChange: (tab: RoadmapAIChatTab) => void;
onCloseTopic: () => void;
selectedTopicId: string | null;
}; };
export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) { export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) {
@@ -27,6 +36,11 @@ export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) {
onLogin, onLogin,
onUpgrade, onUpgrade,
isLoading: isDataLoading, isLoading: isDataLoading,
activeTab,
onTabChange,
onCloseTopic,
selectedTopicId,
} = props; } = props;
const toast = useToast(); const toast = useToast();
@@ -58,11 +72,43 @@ export function RoadmapAIChatHeader(props: RoadmapAIChatHeaderProps) {
/> />
)} )}
<div className="flex min-h-[46px] items-center justify-between gap-2 border-b border-gray-200 px-3 py-2 text-sm"> <div className="flex h-[46px] items-center justify-between gap-2 border-b border-gray-200 text-sm">
<span className="flex items-center gap-2 text-sm"> <div className="flex items-center">
<button
className={cn(
'flex items-center gap-2 px-2 py-3 text-sm',
activeTab === 'chat' && selectedTopicId && 'bg-gray-100',
)}
onClick={() => onTabChange('chat')}
>
<BotIcon className="size-4 shrink-0 text-black" /> <BotIcon className="size-4 shrink-0 text-black" />
<span>AI Chat</span> <span>AI Chat</span>
</span> </button>
{(activeTab === 'topic' || selectedTopicId) && (
<div
className={cn(
'flex items-center gap-1.5 border-l border-gray-200 px-2 py-3 text-sm',
activeTab === 'topic' && selectedTopicId && 'bg-gray-100',
)}
>
<button
className="flex items-center gap-2"
onClick={() => onTabChange('topic')}
>
<BookIcon className="size-4 shrink-0 text-black" />
<span>Topic Details</span>
</button>
<button
className="ml-auto rounded-lg p-1 text-gray-500 hover:bg-gray-200"
onClick={onCloseTopic}
>
<XIcon className="size-4 shrink-0" strokeWidth={2.5} />
</button>
</div>
)}
</div>
{!isDataLoading && ( {!isDataLoading && (
<div className="flex gap-1.5"> <div className="flex gap-1.5">

View File

@@ -93,6 +93,7 @@ type TopicDetailProps = {
wrapperClassName?: string; wrapperClassName?: string;
overlayClassName?: string; overlayClassName?: string;
closeButtonClassName?: string;
onClose?: () => void; onClose?: () => void;
shouldCloseOnBackdropClick?: boolean; shouldCloseOnBackdropClick?: boolean;
shouldCloseOnEscape?: boolean; shouldCloseOnEscape?: boolean;
@@ -106,6 +107,7 @@ export function TopicDetail(props: TopicDetailProps) {
renderer = 'balsamiq', renderer = 'balsamiq',
wrapperClassName, wrapperClassName,
overlayClassName, overlayClassName,
closeButtonClassName,
onClose, onClose,
shouldCloseOnBackdropClick = true, shouldCloseOnBackdropClick = true,
shouldCloseOnEscape = true, shouldCloseOnEscape = true,
@@ -449,7 +451,10 @@ export function TopicDetail(props: TopicDetailProps) {
<button <button
type="button" type="button"
id="close-topic" id="close-topic"
className="flex items-center gap-1.5 rounded-lg bg-gray-200 px-1.5 py-1 text-xs text-black hover:bg-gray-300 hover:text-gray-900" className={cn(
'flex items-center gap-1.5 rounded-lg bg-gray-200 px-1.5 py-1 text-xs text-black hover:bg-gray-300 hover:text-gray-900',
closeButtonClassName,
)}
onClick={handleClose} onClick={handleClose}
> >
<X className="size-4" /> <X className="size-4" />