From 9be2ba5fff2f04a143b57ecdb9df9658455eef85 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Tue, 12 Nov 2024 20:39:30 +0800 Subject: [PATCH 1/3] fix: allow user to instantly queue after refreshing while in queue --- backend/matching-service/src/services/ws.service.ts | 13 +++++-------- frontend/components/dashboard/new-session.tsx | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/matching-service/src/services/ws.service.ts b/backend/matching-service/src/services/ws.service.ts index f2b0edf372..be1a947521 100644 --- a/backend/matching-service/src/services/ws.service.ts +++ b/backend/matching-service/src/services/ws.service.ts @@ -4,6 +4,7 @@ import url from 'url' import WebSocket, { Server as WebSocketServer } from 'ws' import loggerUtil from '../common/logger.util' import { addUserToMatchmaking, removeUserFromMatchingQueue } from '../controllers/matching.controller' +import mqConnection from './rabbitmq.service' export class WebSocketConnection { private wss: WebSocketServer @@ -14,6 +15,7 @@ export class WebSocketConnection { this.wss.on('connection', (ws: WebSocket, req: IncomingMessage) => { const query = url.parse(req.url, true).query const websocketId = query.id as string + const userId = query.userId as string if (!websocketId) { ws.close(1008, 'Missing userId') @@ -22,15 +24,9 @@ export class WebSocketConnection { this.clients.set(websocketId, ws) - // // Close connection after 2 minutes automatically - // const closeAfterTimeout = setTimeout(() => { - // ws.close(1000, 'Connection closed by server after 2 minutes') - // }, 120000) - ws.on('message', (message: string) => this.handleMessage(message, websocketId)) ws.on('close', () => { - // clearTimeout(closeAfterTimeout) - this.handleClose(websocketId) + this.handleClose(websocketId, userId) }) }) } @@ -61,9 +57,10 @@ export class WebSocketConnection { } // Handle WebSocket close event - private handleClose(websocketId: string): void { + private handleClose(websocketId: string, userId: string): void { loggerUtil.info(`User ${websocketId} disconnected`) this.clients.delete(websocketId) + mqConnection.cancelUser(websocketId, userId) } // Send a message to a specific user by websocketId diff --git a/frontend/components/dashboard/new-session.tsx b/frontend/components/dashboard/new-session.tsx index 15b8b6f580..c998735027 100644 --- a/frontend/components/dashboard/new-session.tsx +++ b/frontend/components/dashboard/new-session.tsx @@ -82,7 +82,7 @@ export const NewSession = () => { // Refactor this const wsUrl = (process.env.NEXT_PUBLIC_API_URL || 'ws://localhost:3006')?.concat( - `/api/matching/ws/?id=${websocketId}` + `/api/matching/ws/?id=${websocketId}&userId=${session?.user.id ?? ''}` ) const socket = new WebSocket(wsUrl) setTimeout(() => { From 29a720b1322cea24b516cb682536f5531442cc10 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Tue, 12 Nov 2024 23:39:09 +0800 Subject: [PATCH 2/3] feat: improve loading --- frontend/components/code/actions.tsx | 85 ++++++++ frontend/components/code/chat.tsx | 119 +++++++++++ .../{pages => components}/code/editor.tsx | 15 +- .../code/language-mode-select.tsx | 0 frontend/components/code/question.tsx | 59 ++++++ .../code/read-only-editor.tsx | 15 +- .../code/test-result.tsx | 7 +- .../code/testcase-tab.tsx | 5 + frontend/components/customs/custom-loader.tsx | 69 +++++++ frontend/components/dashboard/new-session.tsx | 7 +- .../components/dashboard/recent-sessions.tsx | 7 +- .../components/dashboard/resume-session.tsx | 8 +- frontend/components/ui/scroll-area.tsx | 42 ++++ frontend/components/ui/skeleton.tsx | 7 + frontend/package.json | 9 +- frontend/pages/code/[id]/chat.tsx | 93 --------- frontend/pages/code/[id]/index.tsx | 185 +++++------------- frontend/pages/index.tsx | 24 +-- frontend/pages/questions/index.tsx | 14 +- frontend/pages/sessions/index.tsx | 15 +- package-lock.json | 68 +++++++ 21 files changed, 569 insertions(+), 284 deletions(-) create mode 100644 frontend/components/code/actions.tsx create mode 100644 frontend/components/code/chat.tsx rename frontend/{pages => components}/code/editor.tsx (92%) rename frontend/{pages => components}/code/language-mode-select.tsx (100%) create mode 100644 frontend/components/code/question.tsx rename frontend/{pages => components}/code/read-only-editor.tsx (88%) rename frontend/{pages => components}/code/test-result.tsx (94%) rename frontend/{pages => components}/code/testcase-tab.tsx (88%) create mode 100644 frontend/components/customs/custom-loader.tsx create mode 100644 frontend/components/ui/scroll-area.tsx create mode 100644 frontend/components/ui/skeleton.tsx delete mode 100644 frontend/pages/code/[id]/chat.tsx diff --git a/frontend/components/code/actions.tsx b/frontend/components/code/actions.tsx new file mode 100644 index 0000000000..59020fee81 --- /dev/null +++ b/frontend/components/code/actions.tsx @@ -0,0 +1,85 @@ +import ConfirmDialog from '../customs/confirm-dialog' +import UserAvatar from '../customs/custom-avatar' +import { LongTextSkeleton } from '../customs/custom-loader' +import { Button } from '../ui/button' +import { EndIcon, PlayIcon } from '@/assets/icons' +import { Cross1Icon } from '@radix-ui/react-icons' + +export const CodeActions = ({ + isLoading, + isViewOnly, + handleRunTests, + isCodeRunning, + isOtherUserOnline, + handleEndSession, + username, + isDialogOpen, + setIsDialogOpen, + handleEndSessionConfirmation, +}: { + isLoading: boolean + isViewOnly: boolean + handleEndSession: () => void + handleRunTests: () => void + isCodeRunning: boolean + isOtherUserOnline: boolean + username: string | undefined + isDialogOpen: boolean + setIsDialogOpen: (isOpen: boolean) => void + handleEndSessionConfirmation: () => void +}) => { + const renderCloseButton = () => { + return isViewOnly ? ( + <> + + Close + + ) : ( + <> + + End Session + + ) + } + + if (isLoading) { + return + } + + return ( +
+
+ {!isViewOnly && ( + + )} +
+
+ {!isViewOnly && } + + setIsDialogOpen(false)} + confirmHandler={handleEndSessionConfirmation} + /> +
+
+ ) +} diff --git a/frontend/components/code/chat.tsx b/frontend/components/code/chat.tsx new file mode 100644 index 0000000000..bc8a567d23 --- /dev/null +++ b/frontend/components/code/chat.tsx @@ -0,0 +1,119 @@ +'use client' + +import { FC, useEffect, useRef, useState } from 'react' +import { useSession } from 'next-auth/react' +import { ChatModel } from '@repo/collaboration-types' +import { Button } from '@/components/ui/button' +import Image from 'next/image' +import { ScrollArea } from '@/components/ui/scroll-area' + +const formatTimestamp = (timestamp: string) => { + const date = new Date(timestamp) + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toUpperCase() +} + +const Chat: FC<{ + chatData: ChatModel[] + isViewOnly: boolean + handleSendMessage: (msg: string) => void +}> = ({ chatData, isViewOnly, handleSendMessage }) => { + const chatEndRef = useRef(null) + const { data: session } = useSession() + const [value, setValue] = useState('') + const [isChatOpen, setIsChatOpen] = useState(true) + + const getChatBubbleFormat = (currUser: string, type: 'label' | 'text') => { + let format = '' + if (currUser === session?.user.username) { + format = 'items-end ml-5' + if (type === 'text') { + format += ' bg-theme-600 rounded-xl text-white' + } + } else { + format = 'items-start text-left mr-5' + if (type === 'text') { + format += ' bg-slate-100 rounded-xl p-2 text-slate-900' + } + } + return format + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && e.currentTarget.value.trim() !== '') { + handleSendMessage(e.currentTarget.value) + setValue('') + e.currentTarget.value = '' + } + } + + const toggleChat = () => { + setIsChatOpen(!isChatOpen) + } + + useEffect(() => { + if (chatEndRef.current) { + chatEndRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [chatData]) + + return ( + <> +
+
+

Chat

+ +
+ + {isChatOpen && ( + + {!!chatData?.length && + Object.values(chatData).map((chat, index) => ( +
+
+

{chat.senderId}

+ + {formatTimestamp(chat.createdAt.toString())} + +
+
+ {chat.message} +
+
+ ))} + {(!chatData || !chatData?.length) && ( +

No chat history

+ )} +
+ {!isViewOnly && ( +
+ setValue(e.target.value)} + readOnly={isViewOnly} + /> +
+ )} +
+ )} +
+ + ) +} + +export default Chat diff --git a/frontend/pages/code/editor.tsx b/frontend/components/code/editor.tsx similarity index 92% rename from frontend/pages/code/editor.tsx rename to frontend/components/code/editor.tsx index 7e73465ffa..a4e027cb8d 100644 --- a/frontend/pages/code/editor.tsx +++ b/frontend/components/code/editor.tsx @@ -11,6 +11,7 @@ import { userColor } from '@/util/cursor-colors' import { oneDark } from '@codemirror/theme-one-dark' import { javascript } from '@codemirror/lang-javascript' import { indentWithTab } from '@codemirror/commands' +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' interface IProps { roomId: string @@ -93,15 +94,11 @@ const CodeMirrorEditor = forwardRef(({ roomId, language }: IProps, ref) => { }, [editorContainerRef, ydoc, ytext, session]) return ( -
+ +
+ + + ) }) diff --git a/frontend/pages/code/language-mode-select.tsx b/frontend/components/code/language-mode-select.tsx similarity index 100% rename from frontend/pages/code/language-mode-select.tsx rename to frontend/components/code/language-mode-select.tsx diff --git a/frontend/components/code/question.tsx b/frontend/components/code/question.tsx new file mode 100644 index 0000000000..c8d34d0c70 --- /dev/null +++ b/frontend/components/code/question.tsx @@ -0,0 +1,59 @@ +import { LargeTextSkeleton, TextSkeleton } from '@/components/customs/custom-loader' +import { DifficultyLabel } from '@/components/customs/difficulty-label' +import CustomLabel from '@/components/ui/label' +import { ScrollArea } from '@/components/ui/scroll-area' +import { capitalizeFirstLowerRest } from '@/util/string-modification' +import { convertSortedComplexityToComplexity } from '@repo/question-types' +import { Category, Complexity } from '@repo/user-types' + +const formatQuestionCategories = (cat: Category[]) => { + return cat.map((c) => capitalizeFirstLowerRest(c)).join(', ') +} + +export const CodeQuestion = ({ + loading, + title, + complexity, + categories, + description, +}: { + loading: boolean + title: string + complexity: Complexity + categories: Category[] + description: string +}) => { + if (loading) { + return ( +
+

+ +

+
+ + +
+
+ +
+
+ ) + } + return ( + +

{title}

+
+ + +
+
{description}
+
+ ) +} diff --git a/frontend/pages/code/read-only-editor.tsx b/frontend/components/code/read-only-editor.tsx similarity index 88% rename from frontend/pages/code/read-only-editor.tsx rename to frontend/components/code/read-only-editor.tsx index c68bd2ce91..3952ee65d6 100644 --- a/frontend/pages/code/read-only-editor.tsx +++ b/frontend/components/code/read-only-editor.tsx @@ -7,6 +7,7 @@ import { languages } from '@codemirror/language-data' import { oneDark } from '@codemirror/theme-one-dark' import { javascript } from '@codemirror/lang-javascript' import { indentWithTab } from '@codemirror/commands' +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' interface IProps { language: string @@ -60,15 +61,11 @@ const ReadOnlyCodeMirrorEditor = ({ language, code }: IProps) => { }, [editorContainerRef, code]) return ( -
+ +
+ + + ) } diff --git a/frontend/pages/code/test-result.tsx b/frontend/components/code/test-result.tsx similarity index 94% rename from frontend/pages/code/test-result.tsx rename to frontend/components/code/test-result.tsx index 51e37e0e32..1741255c08 100644 --- a/frontend/pages/code/test-result.tsx +++ b/frontend/components/code/test-result.tsx @@ -1,12 +1,17 @@ import { ResultModel } from '@repo/collaboration-types' +import { LargeTextSkeleton } from '../customs/custom-loader' export default function TestResult({ result, expectedOutput, + isLoading, }: { result?: ResultModel | undefined expectedOutput: string + isLoading: boolean }) { + if (isLoading) return + if (!result) return
No test results yet
if (!result.status) { @@ -18,8 +23,6 @@ export default function TestResult({ ) } - const { id, description } = result.status - return (
diff --git a/frontend/pages/code/testcase-tab.tsx b/frontend/components/code/testcase-tab.tsx similarity index 88% rename from frontend/pages/code/testcase-tab.tsx rename to frontend/components/code/testcase-tab.tsx index 0dd55f9d92..60fafc9070 100644 --- a/frontend/pages/code/testcase-tab.tsx +++ b/frontend/components/code/testcase-tab.tsx @@ -1,11 +1,14 @@ import CustomTabs from '@/components/customs/custom-tabs' +import { LargeTextSkeleton } from '../customs/custom-loader' export default function TestcasesTab({ + isLoading, activeTestcaseIdx, setActiveTestcaseIdx, testInputs, testOutputs, }: { + isLoading: boolean activeTestcaseIdx: number setActiveTestcaseIdx: (tab: number) => void testInputs: string[] @@ -13,6 +16,8 @@ export default function TestcasesTab({ }) { const testcaseTabs = testInputs?.map((_, idx) => `Case ${idx + 1}`) + if (isLoading) return + if (!testInputs || testInputs.length === 0) return null return (
diff --git a/frontend/components/customs/custom-loader.tsx b/frontend/components/customs/custom-loader.tsx new file mode 100644 index 0000000000..e67f5c3c1a --- /dev/null +++ b/frontend/components/customs/custom-loader.tsx @@ -0,0 +1,69 @@ +import { Skeleton } from '@/components/ui/skeleton' + +function TableSkeleton() { + return ( +
+ + +
+ ) +} + +function TextSkeleton() { + return +} + +function LargeTextSkeleton() { + return +} + +const NewSessionSkeleton = () => { + return ( +
+
+ + + +
+ +
+ ) +} + +const RecentSessionSkeleton = () => { + return ( +
+
+ + +
+ +
+ ) +} + +function LongTextSkeleton() { + return +} + +function DataSkeleton() { + return ( +
+ +
+ + +
+
+ ) +} + +export { + TableSkeleton, + DataSkeleton, + TextSkeleton, + LargeTextSkeleton, + LongTextSkeleton, + NewSessionSkeleton, + RecentSessionSkeleton, +} diff --git a/frontend/components/dashboard/new-session.tsx b/frontend/components/dashboard/new-session.tsx index c998735027..6198ed5c92 100644 --- a/frontend/components/dashboard/new-session.tsx +++ b/frontend/components/dashboard/new-session.tsx @@ -14,8 +14,9 @@ import { addUserToMatchmaking } from '../../services/matching-service-api' import CustomModal from '../customs/custom-modal' import Loading from '../customs/loading' import { capitalizeFirstLowerRest } from '@/util/string-modification' +import { LargeTextSkeleton, NewSessionSkeleton } from '../customs/custom-loader' -export const NewSession = () => { +export const NewSession = ({ isLoading }: { isLoading: boolean }) => { const router = useRouter() const socketRef = useRef(undefined) @@ -166,6 +167,10 @@ export const NewSession = () => { })) } + if (isLoading) { + return + } + return (
diff --git a/frontend/components/dashboard/recent-sessions.tsx b/frontend/components/dashboard/recent-sessions.tsx index 8e09f1c0af..1158a70345 100644 --- a/frontend/components/dashboard/recent-sessions.tsx +++ b/frontend/components/dashboard/recent-sessions.tsx @@ -7,6 +7,7 @@ import { MoveRight } from 'lucide-react' import { IPartialSessions } from '@/types' import CustomLabel from '../ui/label' import { capitalizeFirstLowerRest } from '@/util/string-modification' +import { RecentSessionSkeleton } from '../customs/custom-loader' const cols: { key: keyof IPartialSessions; label: string }[] = [ { @@ -23,7 +24,11 @@ const cols: { key: keyof IPartialSessions; label: string }[] = [ }, ] -export const RecentSessions = ({ data }: { data: IPartialSessions[] }) => { +export const RecentSessions = ({ data, isLoading }: { data: IPartialSessions[]; isLoading: boolean }) => { + if (isLoading) { + return + } + return (
diff --git a/frontend/components/dashboard/resume-session.tsx b/frontend/components/dashboard/resume-session.tsx index 4f6ec06293..0c3f953a1c 100644 --- a/frontend/components/dashboard/resume-session.tsx +++ b/frontend/components/dashboard/resume-session.tsx @@ -3,13 +3,15 @@ import { Button } from '../ui/button' import { IMatch } from '@repo/user-types' import { convertSortedComplexityToComplexity } from '@repo/question-types' import { capitalizeFirstLowerRest } from '@/util/string-modification' +import { LargeTextSkeleton } from '../customs/custom-loader' interface IResumeSessionProps { match: IMatch isOngoing: () => Promise + isLoading: boolean } -export default function ResumeSession({ match, isOngoing }: IResumeSessionProps) { +export default function ResumeSession({ match, isOngoing, isLoading }: IResumeSessionProps) { const router = useRouter() const { category, complexity } = match @@ -22,6 +24,10 @@ export default function ResumeSession({ match, isOngoing }: IResumeSessionProps) } catch (error) {} } + if (isLoading) { + return + } + return (
diff --git a/frontend/components/ui/scroll-area.tsx b/frontend/components/ui/scroll-area.tsx new file mode 100644 index 0000000000..41b5fb17c4 --- /dev/null +++ b/frontend/components/ui/scroll-area.tsx @@ -0,0 +1,42 @@ +'use client' + +import * as React from 'react' +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area' + +import { cn } from '@/lib/utils' + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = 'vertical', ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/frontend/components/ui/skeleton.tsx b/frontend/components/ui/skeleton.tsx new file mode 100644 index 0000000000..15970f2fb5 --- /dev/null +++ b/frontend/components/ui/skeleton.tsx @@ -0,0 +1,7 @@ +import { cn } from '@/lib/utils' + +function Skeleton({ className, ...props }: React.HTMLAttributes) { + return
+} + +export { Skeleton } diff --git a/frontend/package.json b/frontend/package.json index fb7f1d8d14..d4ed431a29 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", @@ -45,12 +46,13 @@ "y-websocket": "^2.0.4" }, "devDependencies": { + "@netlify/plugin-nextjs": "^5.8.1", + "@repo/collaboration-types": "*", "@repo/eslint-config": "*", + "@repo/submission-types": "*", "@repo/typescript-config": "*", - "@repo/ws-types": "*", - "@repo/collaboration-types": "*", "@repo/user-types": "*", - "@repo/submission-types": "*", + "@repo/ws-types": "*", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", @@ -58,7 +60,6 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", - "@netlify/plugin-nextjs": "^5.8.1", "autoprefixer": "^10.4.20", "eslint": "^8", "eslint-config-next": "14.2.11", diff --git a/frontend/pages/code/[id]/chat.tsx b/frontend/pages/code/[id]/chat.tsx deleted file mode 100644 index 16cbf18b61..0000000000 --- a/frontend/pages/code/[id]/chat.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { FC, useEffect, useRef, useState } from 'react' -import { useSession } from 'next-auth/react' -import { ChatModel } from '@repo/collaboration-types' - -const formatTimestamp = (timestamp: string) => { - const date = new Date(timestamp) - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }).toUpperCase() -} - -const Chat: FC<{ chatData: ChatModel[]; isViewOnly: boolean; handleSendMessage: (msg: string) => void }> = ({ - chatData, - isViewOnly, - handleSendMessage, -}) => { - const chatEndRef = useRef(null) - const { data: session } = useSession() - const [value, setValue] = useState('') - - const getChatBubbleFormat = (currUser: string, type: 'label' | 'text') => { - let format = '' - if (currUser === session?.user.username) { - format = 'items-end ml-5' - if (type === 'text') { - format += ' bg-theme-600 rounded-xl text-white' - } - } else { - format = 'items-start text-left mr-5' - if (type === 'text') { - format += ' bg-slate-100 rounded-xl p-2 text-slate-900' - } - } - return format - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && e.currentTarget.value.trim() !== '') { - handleSendMessage(e.currentTarget.value) - setValue('') - e.currentTarget.value = '' - } - } - - useEffect(() => { - if (chatEndRef.current) { - chatEndRef.current.scrollIntoView({ behavior: 'smooth' }) - } - }, [chatData]) - - return ( - <> -
- {!!chatData?.length && - Object.values(chatData).map((chat, index) => ( -
-
-

{chat.senderId}

- - {formatTimestamp(chat.createdAt.toString())} - -
-
- {chat.message} -
-
- ))} - {(!chatData || !chatData?.length) && ( -

No chat history

- )} -
-
- {!isViewOnly && ( -
- setValue(e.target.value)} - readOnly={isViewOnly} - /> -
- )} - - ) -} - -export default Chat diff --git a/frontend/pages/code/[id]/index.tsx b/frontend/pages/code/[id]/index.tsx index 8143c6c0ba..548ea88e88 100644 --- a/frontend/pages/code/[id]/index.tsx +++ b/frontend/pages/code/[id]/index.tsx @@ -1,47 +1,38 @@ 'use client' -import { EndIcon, PlayIcon } from '@/assets/icons' import { ChatModel, ICollabDto, LanguageMode, getCodeMirrorLanguage } from '@repo/collaboration-types' import { useEffect, useRef, useState } from 'react' -import { Button } from '@/components/ui/button' -import CustomLabel from '@/components/ui/label' import CustomTabs from '@/components/customs/custom-tabs' -import { DifficultyLabel } from '@/components/customs/difficulty-label' import Image from 'next/image' -import LanguageModeSelect from '../language-mode-select' +import LanguageModeSelect from '../../../components/code/language-mode-select' import React from 'react' -import TestcasesTab from '../testcase-tab' +import TestcasesTab from '../../../components/code/testcase-tab' import useProtectedRoute from '@/hooks/UseProtectedRoute' import { useRouter } from 'next/router' -import CodeMirrorEditor from '../editor' -import { Category, IMatch, SortedComplexity } from '@repo/user-types' +import CodeMirrorEditor from '../../../components/code/editor' +import { Complexity, IMatch } from '@repo/user-types' import { useSession } from 'next-auth/react' import { getMatchDetails } from '@/services/matching-service-api' -import { convertSortedComplexityToComplexity } from '@repo/question-types' -import Chat from './chat' +import Chat from '../../../components/code/chat' import io, { Socket } from 'socket.io-client' -import UserAvatar from '@/components/customs/custom-avatar' import { toast } from 'sonner' import { ISubmission } from '@repo/submission-types' import { mapLanguageToJudge0 } from '@/util/language-mapper' -import TestResult from '../test-result' -import { Cross1Icon } from '@radix-ui/react-icons' -import ConfirmDialog from '@/components/customs/confirm-dialog' +import TestResult from '../../../components/code/test-result' import { getChatHistory, getCollabHistory } from '@/services/collaboration-service-api' -import ReadOnlyCodeMirrorEditor from '../read-only-editor' +import ReadOnlyCodeMirrorEditor from '../../../components/code/read-only-editor' import { ResultModel } from '@repo/collaboration-types' -import { capitalizeFirstLowerRest } from '@/util/string-modification' - -const formatQuestionCategories = (cat: Category[]) => { - return cat.map((c) => capitalizeFirstLowerRest(c)).join(', ') -} +import { TextSkeleton } from '@/components/customs/custom-loader' +import { CodeQuestion } from '../../../components/code/question' +import { ScrollArea } from '@/components/ui/scroll-area' +import { CodeActions } from '@/components/code/actions' export default function Code() { const router = useRouter() - const [isChatOpen, setIsChatOpen] = useState(true) const { id } = router.query const editorRef = useRef<{ getText: () => string } | null>(null) + const [loading, setLoading] = useState(true) const [editorLanguage, setEditorLanguage] = useState(LanguageMode.Javascript) const testTabs = ['Testcases', 'Test Results'] const [chatData, setChatData] = useState([]) @@ -59,6 +50,8 @@ export default function Code() { undefined ) + useProtectedRoute() + const retrieveMatchDetails = async (matchId: string) => { const response = await getMatchDetails(matchId).catch((err) => { if (retry >= 3) { @@ -75,6 +68,7 @@ export default function Code() { setEditorLanguage(collabResponse?.language ?? LanguageMode.Javascript) setCollabData(collabResponse) } + setLoading(false) } } @@ -150,10 +144,6 @@ export default function Code() { } }, [isViewOnly]) - const toggleChat = () => { - setIsChatOpen(!isChatOpen) - } - const handleSendMessage = (message: string) => { if (message.trim()) { const msg: ChatModel = { @@ -207,24 +197,6 @@ export default function Code() { setIsDialogOpen(false) } - const renderCloseButton = () => { - return isViewOnly ? ( - <> - - Close - - ) : ( - <> - - End Session - - ) - } - - const { loading } = useProtectedRoute() - - if (loading) return null - return (
@@ -232,98 +204,43 @@ export default function Code() { Logo

Session with:{' '} - {sessionData?.user.username !== matchData?.user1Name - ? matchData?.user1Name - : matchData?.user2Name} + {loading ? ( + + ) : sessionData?.user.username !== matchData?.user1Name ? ( + matchData?.user1Name + ) : ( + matchData?.user2Name + )}

-
-

{matchData?.question.title}

-
- - -
-
{matchData?.question.description}
-
- -
-
-

Chat

- -
- {isChatOpen && ( - - )} -
+ +
-
-
- {!isViewOnly && ( - - )} -
-
- {!isViewOnly && ( - - )} - - setIsDialogOpen(false)} - confirmHandler={handleEndSessionConfirmation} - /> -
-
+
-
+
-
{activeTestTab === 0 ? ( ) : ( )}
-
+
) diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 82e82f48b0..60200925d7 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,7 +1,6 @@ 'use client' import ConfirmDialog, { ConfirmDialogProps } from '@/components/customs/confirm-dialog' -import Loading from '@/components/customs/loading' import { NewSession } from '@/components/dashboard/new-session' import { ProgressCard } from '@/components/dashboard/progress-card' import { RecentSessions } from '@/components/dashboard/recent-sessions' @@ -38,7 +37,7 @@ export default function Home() { }, ] - const { session, loading } = useProtectedRoute() + const { session } = useProtectedRoute() const [ongoingMatchData, setOngoingMatchData] = useState() const [dialog, setDialog] = useState({ dialogData: { @@ -55,6 +54,8 @@ export default function Home() { showCancelButton: false, }) const [recentSessions, setRecentSessions] = useState([]) + const [loadingNewSession, setLoadingNewSession] = useState(true) + const [loadingRecentSessions, setLoadingRecentSessions] = useState(true) const checkOngoingSession = async () => { if (!session?.user?.id) return @@ -102,17 +103,12 @@ export default function Home() { } useEffect(() => { - checkOngoingSession() - getRecentSessions() + setLoadingNewSession(true) + setLoadingRecentSessions(true) + checkOngoingSession().then(() => setLoadingNewSession(false)) + getRecentSessions().then(() => setLoadingRecentSessions(false)) }, []) - if (loading) - return ( -
- -
- ) - return (

Welcome Back, {session?.user.username}

@@ -130,11 +126,11 @@ export default function Home() {
{ongoingMatchData ? ( - + ) : ( - + )} - +
diff --git a/frontend/pages/questions/index.tsx b/frontend/pages/questions/index.tsx index 2b556e3f33..4d241657a2 100644 --- a/frontend/pages/questions/index.tsx +++ b/frontend/pages/questions/index.tsx @@ -21,8 +21,10 @@ import { useSession } from 'next-auth/react' import { useEffect, useState } from 'react' import { toast } from 'sonner' import { formFields, getColumns } from './props' +import { TableSkeleton } from '@/components/customs/custom-loader' export default function Questions() { + const [loading, setLoading] = useState(true) const { data: session } = useSession() const isAdmin = session?.user.role === Role.ADMIN @@ -225,19 +227,15 @@ export default function Questions() { loadData() } - const { loading } = useProtectedRoute() + useProtectedRoute() useEffect(() => { if (!session) return - loadData() + setLoading(true) + loadData().then(() => setLoading(false)) }, []) - if (loading) - return ( -
- -
- ) + if (loading) return return (
diff --git a/frontend/pages/sessions/index.tsx b/frontend/pages/sessions/index.tsx index 913bb4f212..34bb54ea40 100644 --- a/frontend/pages/sessions/index.tsx +++ b/frontend/pages/sessions/index.tsx @@ -2,7 +2,6 @@ import { IGetSessions, IPagination, ISession, ISortBy, SessionManager, SortDirec import { useEffect, useState } from 'react' import Datatable from '@/components/customs/datatable' -import Loading from '@/components/customs/loading' import useProtectedRoute from '@/hooks/UseProtectedRoute' import { columns } from './columns' import { toast } from 'sonner' @@ -11,9 +10,11 @@ import { Button } from '@/components/ui/button' import ConfirmDialog, { ConfirmDialogProps } from '@/components/customs/confirm-dialog' import { getOngoingMatch } from '@/services/matching-service-api' import { useRouter } from 'next/router' +import { TableSkeleton } from '@/components/customs/custom-loader' export default function Sessions() { const router = useRouter() + const [loading, setLoading] = useState(true) const [data, setData] = useState([]) const [pagination, setPagination] = useState({ totalPages: 1, @@ -51,7 +52,7 @@ export default function Sessions() { checkOngoingSession() }, []) - const { session, loading } = useProtectedRoute() + const { session } = useProtectedRoute() const paginationHandler = (page: number, limit: number) => { const body = { @@ -91,12 +92,13 @@ export default function Sessions() { } const loadData = async () => { + setLoading(true) const body: IGetSessions = { page: pagination.currentPage, limit: pagination.limit, sortBy: sortBy, } - await load(body) + await load(body).then(() => setLoading(false)) } const checkOngoingSession = async () => { @@ -117,12 +119,7 @@ export default function Sessions() { } } - if (loading) - return ( -
- -
- ) + if (loading) return return (
diff --git a/package-lock.json b/package-lock.json index 5fee98189d..41c8b339f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -278,6 +278,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.2.0", "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", @@ -3685,6 +3686,73 @@ } } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.0.tgz", + "integrity": "sha512-q2jMBdsJ9zB7QG6ngQNzNwlvxLQqONyL58QbEGwuyRZZb/ARQwk3uQVbCF7GvQVOtV6EU/pDxAw3zRzJZI3rpQ==", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.1.1", "license": "MIT", From b56494b6e3efbdbf2a7b2ce276b2f64ea6c2bbc0 Mon Sep 17 00:00:00 2001 From: shishirbychapur Date: Wed, 13 Nov 2024 00:56:32 +0800 Subject: [PATCH 3/3] fix: resolve bug in landing page --- frontend/components/customs/custom-loader.tsx | 35 +------------------ frontend/components/dashboard/new-session.tsx | 7 +--- .../components/dashboard/recent-sessions.tsx | 7 +--- .../components/dashboard/resume-session.tsx | 8 +---- frontend/pages/index.tsx | 24 +++++++------ 5 files changed, 18 insertions(+), 63 deletions(-) diff --git a/frontend/components/customs/custom-loader.tsx b/frontend/components/customs/custom-loader.tsx index e67f5c3c1a..934de5e19a 100644 --- a/frontend/components/customs/custom-loader.tsx +++ b/frontend/components/customs/custom-loader.tsx @@ -17,31 +17,6 @@ function LargeTextSkeleton() { return } -const NewSessionSkeleton = () => { - return ( -
-
- - - -
- -
- ) -} - -const RecentSessionSkeleton = () => { - return ( -
-
- - -
- -
- ) -} - function LongTextSkeleton() { return } @@ -58,12 +33,4 @@ function DataSkeleton() { ) } -export { - TableSkeleton, - DataSkeleton, - TextSkeleton, - LargeTextSkeleton, - LongTextSkeleton, - NewSessionSkeleton, - RecentSessionSkeleton, -} +export { TableSkeleton, DataSkeleton, TextSkeleton, LargeTextSkeleton, LongTextSkeleton } diff --git a/frontend/components/dashboard/new-session.tsx b/frontend/components/dashboard/new-session.tsx index 6198ed5c92..c998735027 100644 --- a/frontend/components/dashboard/new-session.tsx +++ b/frontend/components/dashboard/new-session.tsx @@ -14,9 +14,8 @@ import { addUserToMatchmaking } from '../../services/matching-service-api' import CustomModal from '../customs/custom-modal' import Loading from '../customs/loading' import { capitalizeFirstLowerRest } from '@/util/string-modification' -import { LargeTextSkeleton, NewSessionSkeleton } from '../customs/custom-loader' -export const NewSession = ({ isLoading }: { isLoading: boolean }) => { +export const NewSession = () => { const router = useRouter() const socketRef = useRef(undefined) @@ -167,10 +166,6 @@ export const NewSession = ({ isLoading }: { isLoading: boolean }) => { })) } - if (isLoading) { - return - } - return (
diff --git a/frontend/components/dashboard/recent-sessions.tsx b/frontend/components/dashboard/recent-sessions.tsx index 1158a70345..8e09f1c0af 100644 --- a/frontend/components/dashboard/recent-sessions.tsx +++ b/frontend/components/dashboard/recent-sessions.tsx @@ -7,7 +7,6 @@ import { MoveRight } from 'lucide-react' import { IPartialSessions } from '@/types' import CustomLabel from '../ui/label' import { capitalizeFirstLowerRest } from '@/util/string-modification' -import { RecentSessionSkeleton } from '../customs/custom-loader' const cols: { key: keyof IPartialSessions; label: string }[] = [ { @@ -24,11 +23,7 @@ const cols: { key: keyof IPartialSessions; label: string }[] = [ }, ] -export const RecentSessions = ({ data, isLoading }: { data: IPartialSessions[]; isLoading: boolean }) => { - if (isLoading) { - return - } - +export const RecentSessions = ({ data }: { data: IPartialSessions[] }) => { return (
diff --git a/frontend/components/dashboard/resume-session.tsx b/frontend/components/dashboard/resume-session.tsx index 0c3f953a1c..4f6ec06293 100644 --- a/frontend/components/dashboard/resume-session.tsx +++ b/frontend/components/dashboard/resume-session.tsx @@ -3,15 +3,13 @@ import { Button } from '../ui/button' import { IMatch } from '@repo/user-types' import { convertSortedComplexityToComplexity } from '@repo/question-types' import { capitalizeFirstLowerRest } from '@/util/string-modification' -import { LargeTextSkeleton } from '../customs/custom-loader' interface IResumeSessionProps { match: IMatch isOngoing: () => Promise - isLoading: boolean } -export default function ResumeSession({ match, isOngoing, isLoading }: IResumeSessionProps) { +export default function ResumeSession({ match, isOngoing }: IResumeSessionProps) { const router = useRouter() const { category, complexity } = match @@ -24,10 +22,6 @@ export default function ResumeSession({ match, isOngoing, isLoading }: IResumeSe } catch (error) {} } - if (isLoading) { - return - } - return (
diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx index 60200925d7..82e82f48b0 100644 --- a/frontend/pages/index.tsx +++ b/frontend/pages/index.tsx @@ -1,6 +1,7 @@ 'use client' import ConfirmDialog, { ConfirmDialogProps } from '@/components/customs/confirm-dialog' +import Loading from '@/components/customs/loading' import { NewSession } from '@/components/dashboard/new-session' import { ProgressCard } from '@/components/dashboard/progress-card' import { RecentSessions } from '@/components/dashboard/recent-sessions' @@ -37,7 +38,7 @@ export default function Home() { }, ] - const { session } = useProtectedRoute() + const { session, loading } = useProtectedRoute() const [ongoingMatchData, setOngoingMatchData] = useState() const [dialog, setDialog] = useState({ dialogData: { @@ -54,8 +55,6 @@ export default function Home() { showCancelButton: false, }) const [recentSessions, setRecentSessions] = useState([]) - const [loadingNewSession, setLoadingNewSession] = useState(true) - const [loadingRecentSessions, setLoadingRecentSessions] = useState(true) const checkOngoingSession = async () => { if (!session?.user?.id) return @@ -103,12 +102,17 @@ export default function Home() { } useEffect(() => { - setLoadingNewSession(true) - setLoadingRecentSessions(true) - checkOngoingSession().then(() => setLoadingNewSession(false)) - getRecentSessions().then(() => setLoadingRecentSessions(false)) + checkOngoingSession() + getRecentSessions() }, []) + if (loading) + return ( +
+ +
+ ) + return (

Welcome Back, {session?.user.username}

@@ -126,11 +130,11 @@ export default function Home() {
{ongoingMatchData ? ( - + ) : ( - + )} - +