1
0
mirror of https://github.com/kamranahmedse/developer-roadmap.git synced 2025-09-25 08:35:42 +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,53 +393,56 @@ 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 && ( <RoadmapAIChatHeader
<> isLoading={isDataLoading}
<TopicDetail hasChatHistory={hasChatHistory}
resourceId={selectedTopicId} setAiChatHistory={setAiChatHistory}
resourceType="roadmap" onLogin={() => {
renderer="editor" showLoginPopup();
canSubmitContribution={false} }}
wrapperClassName="static mx-auto sm:pt-14 pt-14" onUpgrade={() => {
overlayClassName="hidden" setShowUpgradeModal(true);
onClose={() => setSelectedTopicId(null)} }}
shouldCloseOnBackdropClick={false} activeTab={activeTab}
shouldCloseOnEscape={false} onTabChange={(tab) => {
/> setActiveTab(tab);
<div className="absolute top-0 left-0 z-99 flex w-full items-center justify-between gap-2 bg-gray-100 p-1"> if (tab === 'topic' && selectedTopicId && selectedTopicTitle) {
<div className="flex items-center gap-2 px-4 py-2 text-sm"> handleSelectTopic(selectedTopicId, selectedTopicTitle);
AI Tutor{' '} }
<ChevronRightIcon className="size-4 shrink-0 stroke-[2.5] text-gray-500" />{' '} }}
{selectedTopicTitle} onCloseTopic={() => {
</div> setSelectedTopicId(null);
<button setSelectedTopicTitle(null);
className="flex cursor-pointer items-center gap-2 rounded-lg p-2 text-black hover:bg-gray-200" setActiveTab('chat');
onClick={() => setSelectedTopicId(null)} }}
> selectedTopicId={selectedTopicId}
<XIcon className="size-3 shrink-0" strokeWidth={2.5} /> />
</button>
</div> {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}
/>
)} )}
{!selectedTopicId && ( {activeTab === 'chat' && (
<> <>
{showUpgradeModal && ( {showUpgradeModal && (
<UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} /> <UpgradeAccountModal onClose={() => setShowUpgradeModal(false)} />
)} )}
<RoadmapAIChatHeader
isLoading={isDataLoading}
hasChatHistory={hasChatHistory}
setAiChatHistory={setAiChatHistory}
onLogin={() => {
showLoginPopup();
}}
onUpgrade={() => {
setShowUpgradeModal(true);
}}
/>
<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">
<BotIcon className="size-4 shrink-0 text-black" /> <button
<span>AI Chat</span> className={cn(
</span> '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" />
<span>AI Chat</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" />