mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-09-25 00:21:28 +02:00
feat: make chat resizeable
This commit is contained in:
@@ -64,6 +64,7 @@
|
||||
"react-calendar-heatmap": "^1.9.0",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-textarea-autosize": "^8.5.7",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"reactflow": "^11.11.4",
|
||||
|
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -113,6 +113,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-resizable-panels:
|
||||
specifier: ^2.1.7
|
||||
version: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-textarea-autosize:
|
||||
specifier: ^8.5.7
|
||||
version: 8.5.7(@types/react@18.3.18)(react@18.3.1)
|
||||
@@ -2988,6 +2991,12 @@ packages:
|
||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
react-resizable-panels@2.1.7:
|
||||
resolution: {integrity: sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==}
|
||||
peerDependencies:
|
||||
react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
react-textarea-autosize@8.5.7:
|
||||
resolution: {integrity: sha512-2MqJ3p0Jh69yt9ktFIaZmORHXw4c4bxSIhCeWiFwmJ9EYKgLmuNII3e9c9b2UO+ijl4StnpZdqpxNIhTdHvqtQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6503,6 +6512,11 @@ snapshots:
|
||||
|
||||
react-refresh@0.14.2: {}
|
||||
|
||||
react-resizable-panels@2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-textarea-autosize@8.5.7(@types/react@18.3.18)(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.9
|
||||
|
@@ -39,7 +39,7 @@ export function AICourseContent(props: AICourseContentProps) {
|
||||
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||
const [showAILimitsPopup, setShowAILimitsPopup] = useState(false);
|
||||
const [isAIChatsOpen, setIsAIChatsOpen] = useState(true);
|
||||
const [isAIChatsOpen, setIsAIChatsOpen] = useState(false);
|
||||
|
||||
const [activeModuleIndex, setActiveModuleIndex] = useState(0);
|
||||
const [activeLessonIndex, setActiveLessonIndex] = useState(0);
|
||||
@@ -211,12 +211,6 @@ export function AICourseContent(props: AICourseContentProps) {
|
||||
|
||||
const isViewingLesson = viewMode === 'module';
|
||||
|
||||
useEffect(() => {
|
||||
if (window && window?.innerWidth < 1024 && isAIChatsOpen) {
|
||||
setIsAIChatsOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="flex h-screen flex-grow flex-col overflow-hidden bg-gray-50">
|
||||
{modals}
|
||||
|
@@ -31,6 +31,11 @@ import { RegenerateLesson } from './RegenerateLesson';
|
||||
import { TestMyKnowledgeAction } from './TestMyKnowledgeAction';
|
||||
import { AICourseLessonChat } from './AICourseLessonChat';
|
||||
import { AICourseFooter } from './AICourseFooter';
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from './Resizeable';
|
||||
|
||||
type AICourseLessonProps = {
|
||||
courseSlug: string;
|
||||
@@ -69,10 +74,11 @@ export function AICourseLesson(props: AICourseLessonProps) {
|
||||
|
||||
onUpgrade,
|
||||
|
||||
isAIChatsOpen,
|
||||
setIsAIChatsOpen,
|
||||
isAIChatsOpen: isAIChatsMobileOpen,
|
||||
setIsAIChatsOpen: setIsAIChatsMobileOpen,
|
||||
} = props;
|
||||
|
||||
const [isAIChatsOpen, setIsAIChatsOpen] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
@@ -218,13 +224,15 @@ export function AICourseLesson(props: AICourseLessonProps) {
|
||||
isLoading;
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-cols-5">
|
||||
<div
|
||||
className={cn(
|
||||
'relative',
|
||||
isAIChatsOpen ? 'col-span-3 max-lg:col-span-5' : 'col-span-5',
|
||||
)}
|
||||
<div className="h-full">
|
||||
<ResizablePanelGroup direction="horizontal">
|
||||
<ResizablePanel
|
||||
defaultSize={isAIChatsOpen ? 60 : 100}
|
||||
minSize={40}
|
||||
id="course-text-content"
|
||||
order={1}
|
||||
>
|
||||
<div className="relative h-full">
|
||||
<div className="absolute inset-0 overflow-y-auto bg-white p-8 pb-0 max-lg:px-4 max-lg:pt-3">
|
||||
{(isGenerating || isLoading) && (
|
||||
<div className="absolute right-6 top-6 flex items-center justify-center">
|
||||
@@ -372,7 +380,8 @@ export function AICourseLesson(props: AICourseLessonProps) {
|
||||
)}
|
||||
>
|
||||
<ChevronLeft size={16} className="mr-2" />
|
||||
Previous <span className="hidden lg:inline"> Lesson</span>
|
||||
Previous{' '}
|
||||
<span className="hidden lg:inline"> Lesson</span>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
@@ -407,7 +416,8 @@ export function AICourseLesson(props: AICourseLessonProps) {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Next <span className="hidden lg:inline"> Lesson</span>
|
||||
Next{' '}
|
||||
<span className="hidden lg:inline"> Lesson</span>
|
||||
<ChevronRight size={16} className="ml-2" />
|
||||
</>
|
||||
)}
|
||||
@@ -418,17 +428,49 @@ export function AICourseLesson(props: AICourseLessonProps) {
|
||||
<AICourseFooter />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ResizablePanel>
|
||||
{isAIChatsOpen && (
|
||||
<>
|
||||
<ResizableHandle withHandle={false} className="max-lg:hidden" />
|
||||
<ResizablePanel
|
||||
defaultSize={40}
|
||||
minSize={20}
|
||||
id="course-chat-content"
|
||||
order={2}
|
||||
className="max-lg:hidden"
|
||||
>
|
||||
<AICourseLessonChat
|
||||
courseSlug={courseSlug}
|
||||
moduleTitle={currentModuleTitle}
|
||||
lessonTitle={currentLessonTitle}
|
||||
onUpgradeClick={onUpgrade}
|
||||
isDisabled={isGenerating || isLoading || isTogglingDone}
|
||||
onClose={() => setIsAIChatsOpen(false)}
|
||||
isAIChatsOpen={isAIChatsOpen}
|
||||
setIsAIChatsOpen={setIsAIChatsOpen}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="fixed inset-0 hidden data-[state=open]:block lg:hidden data-[state=open]:lg:hidden"
|
||||
data-state={isAIChatsMobileOpen ? 'open' : 'closed'}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/50" />
|
||||
<AICourseLessonChat
|
||||
courseSlug={courseSlug}
|
||||
moduleTitle={currentModuleTitle}
|
||||
lessonTitle={currentLessonTitle}
|
||||
onUpgradeClick={onUpgrade}
|
||||
isDisabled={isGenerating || isLoading || isTogglingDone}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => setIsAIChatsMobileOpen(false)}
|
||||
className="absolute right-2 top-2 z-20 rounded-full p-1 text-gray-400 hover:text-black"
|
||||
>
|
||||
<XIcon className="size-4 stroke-[2.5]" />
|
||||
</button>
|
||||
</div>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -46,25 +46,11 @@ type AICourseLessonChatProps = {
|
||||
lessonTitle: string;
|
||||
onUpgradeClick: () => void;
|
||||
isDisabled?: boolean;
|
||||
|
||||
onClose: () => void;
|
||||
|
||||
isAIChatsOpen: boolean;
|
||||
setIsAIChatsOpen: (isAIChatsOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export function AICourseLessonChat(props: AICourseLessonChatProps) {
|
||||
const {
|
||||
courseSlug,
|
||||
moduleTitle,
|
||||
lessonTitle,
|
||||
onUpgradeClick,
|
||||
isDisabled,
|
||||
onClose,
|
||||
|
||||
isAIChatsOpen,
|
||||
setIsAIChatsOpen,
|
||||
} = props;
|
||||
const { courseSlug, moduleTitle, lessonTitle, onUpgradeClick, isDisabled } =
|
||||
props;
|
||||
|
||||
const toast = useToast();
|
||||
const scrollareaRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -211,24 +197,8 @@ export function AICourseLessonChat(props: AICourseLessonChatProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAIChatsOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-10 bg-black/50 lg:hidden"
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="relative col-span-2 h-full border-l border-gray-200 transition-all data-[state=closed]:hidden max-lg:fixed max-lg:inset-y-0 max-lg:right-0 max-lg:z-10 max-lg:w-[420px] max-lg:border-none max-lg:data-[state=closed]:translate-x-full max-lg:data-[state=open]:translate-x-0"
|
||||
data-state={isAIChatsOpen ? 'open' : 'closed'}
|
||||
>
|
||||
<div className="relative h-full border-l border-gray-200">
|
||||
<div className="absolute inset-y-0 right-0 z-20 flex w-full flex-col overflow-hidden bg-white">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute right-2 top-2 hidden rounded-full p-1 text-gray-400 hover:text-black max-lg:block"
|
||||
>
|
||||
<XIcon className="size-4 stroke-[2.5]" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 border-b border-gray-200 px-4 py-2 text-sm">
|
||||
<h4 className="text-base font-medium">Course AI</h4>
|
||||
</div>
|
||||
|
42
src/components/GenerateCourse/Resizeable.tsx
Normal file
42
src/components/GenerateCourse/Resizeable.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import * as ResizablePrimitive from 'react-resizable-panels';
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
const ResizablePanelGroup = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
|
||||
<ResizablePrimitive.PanelGroup
|
||||
className={cn(
|
||||
'flex h-full w-full data-[panel-group-direction=vertical]:flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const ResizablePanel = ResizablePrimitive.Panel;
|
||||
|
||||
const ResizableHandle = ({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
|
||||
withHandle?: boolean;
|
||||
}) => (
|
||||
<ResizablePrimitive.PanelResizeHandle
|
||||
className={cn(
|
||||
'relative flex w-px items-center justify-center bg-gray-200 after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="z-10 flex h-[30px] w-3 items-center justify-center rounded-sm bg-gray-200 text-black hover:bg-gray-300">
|
||||
<GripVertical className="size-5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizablePrimitive.PanelResizeHandle>
|
||||
);
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
Reference in New Issue
Block a user