Skip to content

Commit 0a386ac

Browse files
committed
PEER-234: Add question details pane
Signed-off-by: SeeuSim <[email protected]>
1 parent 5daa9f5 commit 0a386ac

File tree

10 files changed

+95
-78
lines changed

10 files changed

+95
-78
lines changed

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"preview": "vite preview --port 5173 --host 0.0.0.0"
1010
},
1111
"dependencies": {
12+
"@codemirror/view": "^6.34.1",
1213
"@hookform/resolvers": "^3.9.0",
1314
"@radix-ui/react-alert-dialog": "^1.1.1",
1415
"@radix-ui/react-checkbox": "^1.1.1",

frontend/src/components/blocks/interview/editor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type EditorProps = {
2323

2424
export const Editor = ({ room }: EditorProps) => {
2525
const { height } = useWindowSize();
26-
const [theme, setTheme] = useState<IEditorTheme>('darcula');
26+
const [theme, setTheme] = useState<IEditorTheme>('vscodeDark');
2727
const { editorRef, extensions, language, setLanguage, code, setCode } = useCollab(room);
2828
const themePreset = useMemo(() => {
2929
return getTheme(theme);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Markdown from 'react-markdown';
2+
import rehypeKatex from 'rehype-katex';
3+
import remarkGfm from 'remark-gfm';
4+
import remarkMath from 'remark-math';
5+
6+
import { Badge } from '@/components/ui/badge';
7+
import { CardHeader, CardTitle, CardContent } from '@/components/ui/card';
8+
import { ScrollArea } from '@/components/ui/scroll-area';
9+
import { Separator } from '@/components/ui/separator';
10+
import type { IGetQuestionDetailsResponse } from '@/services/question-service';
11+
12+
export const QuestionDetails = ({
13+
questionDetails,
14+
}: {
15+
questionDetails: IGetQuestionDetailsResponse['question'];
16+
}) => {
17+
return (
18+
<ScrollArea className='h-full'>
19+
<CardHeader>
20+
<div className='flex flex-col gap-4'>
21+
<div className='flex w-full items-center gap-4'>
22+
<CardTitle className='text-2xl'>
23+
{questionDetails.id}.&nbsp;{questionDetails.title}
24+
</CardTitle>
25+
</div>
26+
<div className='flex flex-wrap items-center gap-1'>
27+
<Badge
28+
variant={questionDetails.difficulty.toLowerCase() as 'easy' | 'medium' | 'hard'}
29+
className='flex w-min grow-0'
30+
>
31+
{questionDetails.difficulty}
32+
</Badge>
33+
<Separator orientation='vertical' className='mx-2 h-4' />
34+
<span className='text-sm font-medium'>Topics:</span>
35+
{questionDetails.topic.map((v, i) => (
36+
<Badge variant='secondary' className='flex w-min grow-0 whitespace-nowrap' key={i}>
37+
{v}
38+
</Badge>
39+
))}
40+
</div>
41+
</div>
42+
</CardHeader>
43+
<CardContent>
44+
<Markdown
45+
rehypePlugins={[rehypeKatex]}
46+
remarkPlugins={[remarkMath, remarkGfm]}
47+
className='prose prose-neutral text-card-foreground prose-strong:text-card-foreground leading-normal'
48+
components={{
49+
code: ({ children, className, ...rest }) => {
50+
// const isCodeBlock = /language-(\w+)/.exec(className || '');
51+
52+
return (
53+
<code
54+
{...rest}
55+
className='bg-secondary text-secondary-foreground rounded px-1.5 py-1 font-mono'
56+
>
57+
{children}
58+
</code>
59+
);
60+
},
61+
}}
62+
>
63+
{questionDetails.description}
64+
</Markdown>
65+
</CardContent>
66+
</ScrollArea>
67+
);
68+
};

frontend/src/lib/editor/extensions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { langs, LanguageName, loadLanguage } from '@uiw/codemirror-extensions-langs';
22
import * as themes from '@uiw/codemirror-themes-all';
33
import { Extension } from '@uiw/react-codemirror';
4-
import { keymap } from '@codemirror/view';
4+
import { EditorView, keymap } from '@codemirror/view';
55
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
66

77
export const languages = [
@@ -72,4 +72,4 @@ export const getTheme = (theme: IEditorTheme) => {
7272
return themes[theme as keyof typeof themes] as Extension;
7373
};
7474

75-
export const extensions = [keymap.of(vscodeKeymap)];
75+
export const extensions = [keymap.of(vscodeKeymap), EditorView.lineWrapping];

frontend/src/lib/hooks/use-collab.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { Extension, type ReactCodeMirrorRef } from '@uiw/react-codemirror';
1+
import type { LanguageName } from '@uiw/codemirror-extensions-langs';
2+
import type { Extension, ReactCodeMirrorRef } from '@uiw/react-codemirror';
23
import { useEffect, useMemo, useRef, useState } from 'react';
34
import { yCollab } from 'y-codemirror.next';
4-
import * as Y from 'yjs';
55
import { SocketIOProvider } from 'y-socket.io';
6+
import * as Y from 'yjs';
67

7-
import { COLLAB_SERVICE } from '@/services/api-clients';
88
import { extensions as baseExtensions, getLanguage } from '@/lib/editor/extensions';
9-
import { LanguageName } from '@uiw/codemirror-extensions-langs';
9+
import { COLLAB_SERVICE } from '@/services/api-clients';
1010

1111
// credit: https://github.com/yjs/y-websocket
1212
const usercolors = [
@@ -42,7 +42,7 @@ export const useCollab = (roomId: string) => {
4242
provider = new SocketIOProvider(COLLAB_SERVICE, roomId, doc, {});
4343
} catch (err) {
4444
const { name, message } = err as Error;
45-
console.log(
45+
console.error(
4646
`An error occurred connecting to the Collab Server: ${JSON.stringify({ name, message })}`
4747
);
4848
return;
@@ -51,11 +51,14 @@ export const useCollab = (roomId: string) => {
5151
const undoManager = new Y.UndoManager(ytext);
5252
const awareness = provider.awareness;
5353
const { color, light } = getRandomColor();
54+
55+
// TODO: Get user name
5456
awareness.setLocalStateField('user', {
5557
name: `Anon`,
5658
color: color,
5759
colorLight: light,
5860
});
61+
5962
const collabExt = yCollab(ytext, awareness, { undoManager });
6063
setCode(ytext.toString());
6164
setExtensions([...extensions, collabExt]);

frontend/src/lib/router.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { ForgotPassword } from '@/routes/forgot-password';
88
import { HomePage } from '@/routes/home';
99
import { InterviewRoom, loader as interviewRoomLoader } from '@/routes/interview/[room]';
1010
import { Login } from '@/routes/login';
11-
import { QuestionDetails, loader as questionDetailsLoader } from '@/routes/questions/details';
11+
import { QuestionDetailsPage, loader as questionDetailsLoader } from '@/routes/questions/details';
1212
import { Questions, loader as questionsLoader } from '@/routes/questions/main';
1313
import { SignUp } from '@/routes/signup';
1414

@@ -38,7 +38,7 @@ export const router = createBrowserRouter([
3838
{
3939
path: ROUTES.QUESTION_DETAILS,
4040
loader: questionDetailsLoader(queryClient),
41-
element: <QuestionDetails />,
41+
element: <QuestionDetailsPage />,
4242
},
4343
{
4444
path: ROUTES.INTERVIEW,

frontend/src/routes/interview/[room]/main.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1+
import { LoaderFunctionArgs, Navigate, useLoaderData } from 'react-router-dom';
2+
13
import { Editor } from '@/components/blocks/interview/editor';
24
import { Card } from '@/components/ui/card';
3-
import { LoaderFunctionArgs, redirect, useLoaderData } from 'react-router-dom';
5+
import { ROUTES } from '@/lib/routes';
46

57
export const loader = ({ params, request }: LoaderFunctionArgs) => {
68
const roomId = params.roomId;
79
const url = new URL(request.url);
810
const questionId = url.searchParams.get('questionId');
911

10-
if (!roomId || !questionId) {
11-
redirect('/');
12-
}
12+
// TODO: Load question data (copy from question loader)
1313

1414
return {
1515
roomId,
@@ -19,7 +19,9 @@ export const loader = ({ params, request }: LoaderFunctionArgs) => {
1919

2020
export const InterviewRoom = () => {
2121
const { roomId, questionId } = useLoaderData() as ReturnType<typeof loader>;
22-
return (
22+
return !questionId || !roomId ? (
23+
<Navigate to={ROUTES.HOME} />
24+
) : (
2325
<div className='flex flex-1 overflow-hidden'>
2426
<Card className='border-border m-4 w-1/3 max-w-[500px] overflow-hidden p-4 md:w-2/5'>
2527
{/* Question Deets */}

frontend/src/routes/questions/details/main.tsx

Lines changed: 4 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
import { QueryClient, queryOptions, useSuspenseQuery } from '@tanstack/react-query';
22
import { useMemo } from 'react';
3-
import Markdown from 'react-markdown';
43
import { LoaderFunctionArgs, useLoaderData } from 'react-router-dom';
5-
import rehypeKatex from 'rehype-katex';
6-
import remarkGfm from 'remark-gfm';
7-
import remarkMath from 'remark-math';
84

95
import { WithNavBanner } from '@/components/blocks/authed/with-nav-banner';
10-
import { Badge } from '@/components/ui/badge';
11-
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
12-
import { ScrollArea } from '@/components/ui/scroll-area';
13-
import { Separator } from '@/components/ui/separator';
6+
import { Card } from '@/components/ui/card';
147

158
import { useCrumbs } from '@/lib/hooks/use-crumbs';
169
import { usePageTitle } from '@/lib/hooks/use-page-title';
1710
import { getQuestionDetails } from '@/services/question-service';
11+
import { QuestionDetails } from '@/components/blocks/questions/details';
1812

1913
const questionDetailsQuery = (id: number) =>
2014
queryOptions({
@@ -30,7 +24,7 @@ export const loader =
3024
return { questionId };
3125
};
3226

33-
export const QuestionDetails = () => {
27+
export const QuestionDetailsPage = () => {
3428
const { questionId } = useLoaderData() as Awaited<ReturnType<ReturnType<typeof loader>>>;
3529
const { data: details } = useSuspenseQuery(questionDetailsQuery(questionId));
3630
const questionDetails = useMemo(() => {
@@ -46,59 +40,7 @@ export const QuestionDetails = () => {
4640
<WithNavBanner crumbs={[...crumbs]}>
4741
<div className='flex flex-1 overflow-hidden'>
4842
<Card className='border-border m-4 w-1/3 max-w-[500px] overflow-hidden p-4 md:w-2/5'>
49-
<ScrollArea className='h-full'>
50-
<CardHeader>
51-
<div className='flex flex-col gap-4'>
52-
<div className='flex w-full items-center gap-4'>
53-
<CardTitle className='text-2xl'>
54-
{questionDetails.id}.&nbsp;{questionDetails.title}
55-
</CardTitle>
56-
</div>
57-
<div className='flex flex-wrap items-center gap-1'>
58-
<Badge
59-
variant={questionDetails.difficulty.toLowerCase() as 'easy' | 'medium' | 'hard'}
60-
className='flex w-min grow-0'
61-
>
62-
{questionDetails.difficulty}
63-
</Badge>
64-
<Separator orientation='vertical' className='mx-2 h-4' />
65-
<span className='text-sm font-medium'>Topics:</span>
66-
{questionDetails.topic.map((v, i) => (
67-
<Badge
68-
variant='secondary'
69-
className='flex w-min grow-0 whitespace-nowrap'
70-
key={i}
71-
>
72-
{v}
73-
</Badge>
74-
))}
75-
</div>
76-
</div>
77-
</CardHeader>
78-
<CardContent>
79-
<Markdown
80-
rehypePlugins={[rehypeKatex]}
81-
remarkPlugins={[remarkMath, remarkGfm]}
82-
className='prose prose-neutral text-card-foreground prose-strong:text-card-foreground leading-normal'
83-
components={{
84-
code: ({ children, className, ...rest }) => {
85-
// const isCodeBlock = /language-(\w+)/.exec(className || '');
86-
87-
return (
88-
<code
89-
{...rest}
90-
className='bg-secondary text-secondary-foreground rounded px-1.5 py-1 font-mono'
91-
>
92-
{children}
93-
</code>
94-
);
95-
},
96-
}}
97-
>
98-
{questionDetails.description}
99-
</Markdown>
100-
</CardContent>
101-
</ScrollArea>
43+
<QuestionDetails questionDetails={questionDetails} />
10244
</Card>
10345
<div className='flex flex-1 flex-col' />
10446
</div>

frontend/src/services/question-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const QUESTION_SERVICE_ROUTES = {
77
GET_QUESTION_DETAILS: '/questions/<questionId>',
88
};
99

10-
type IGetQuestionDetailsResponse = {
10+
export type IGetQuestionDetailsResponse = {
1111
question: {
1212
title: string;
1313
description: string;

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)