Skip to content

Commit fc03b15

Browse files
committed
PEER-249 Add Question Details Page
Signed-off-by: SeeuSim <[email protected]>
1 parent c232643 commit fc03b15

File tree

6 files changed

+252
-27
lines changed

6 files changed

+252
-27
lines changed

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"@radix-ui/react-icons": "^1.3.0",
1818
"@radix-ui/react-label": "^2.1.0",
1919
"@radix-ui/react-navigation-menu": "^1.2.0",
20+
"@radix-ui/react-scroll-area": "^1.1.0",
21+
"@radix-ui/react-separator": "^1.1.0",
2022
"@radix-ui/react-slot": "^1.1.0",
2123
"@radix-ui/react-tabs": "^1.1.0",
2224
"@tanstack/react-query": "^5.56.2",
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react';
2+
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
3+
4+
import { cn } from '@/lib/utils';
5+
6+
const ScrollArea = React.forwardRef<
7+
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
8+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
9+
>(({ className, children, ...props }, ref) => (
10+
<ScrollAreaPrimitive.Root
11+
ref={ref}
12+
className={cn('relative overflow-hidden', className)}
13+
{...props}
14+
>
15+
<ScrollAreaPrimitive.Viewport className='size-full rounded-[inherit]'>
16+
{children}
17+
</ScrollAreaPrimitive.Viewport>
18+
<ScrollBar />
19+
<ScrollAreaPrimitive.Corner />
20+
</ScrollAreaPrimitive.Root>
21+
));
22+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
23+
24+
const ScrollBar = React.forwardRef<
25+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
26+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
27+
>(({ className, orientation = 'vertical', ...props }, ref) => (
28+
<ScrollAreaPrimitive.ScrollAreaScrollbar
29+
ref={ref}
30+
orientation={orientation}
31+
className={cn(
32+
'flex touch-none select-none transition-colors',
33+
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
34+
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
35+
className
36+
)}
37+
{...props}
38+
>
39+
<ScrollAreaPrimitive.ScrollAreaThumb className='bg-border relative flex-1 rounded-full' />
40+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
41+
));
42+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
43+
44+
export { ScrollArea, ScrollBar };
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from 'react';
2+
import * as SeparatorPrimitive from '@radix-ui/react-separator';
3+
4+
import { cn } from '@/lib/utils';
5+
6+
const Separator = React.forwardRef<
7+
React.ElementRef<typeof SeparatorPrimitive.Root>,
8+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
9+
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
10+
<SeparatorPrimitive.Root
11+
ref={ref}
12+
decorative={decorative}
13+
orientation={orientation}
14+
className={cn(
15+
'shrink-0 bg-border',
16+
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
17+
className
18+
)}
19+
{...props}
20+
/>
21+
));
22+
Separator.displayName = SeparatorPrimitive.Root.displayName;
23+
24+
export { Separator };
Lines changed: 106 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
import { QueryClient, queryOptions, useSuspenseQuery } from '@tanstack/react-query';
22
import Markdown from 'react-markdown';
3-
import { LoaderFunctionArgs, useLoaderData } from 'react-router-dom';
3+
import { Link, LoaderFunctionArgs, useLoaderData } from 'react-router-dom';
44
import rehypeKatex from 'rehype-katex';
55
import remarkGfm from 'remark-gfm';
66
import remarkMath from 'remark-math';
77

88
import { Badge } from '@/components/ui/badge';
99
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
1010
import { getQuestionDetails } from '@/services/question-service';
11+
import { Separator } from '@/components/ui/separator';
12+
import {
13+
Breadcrumb,
14+
BreadcrumbItem,
15+
BreadcrumbLink,
16+
BreadcrumbList,
17+
BreadcrumbSeparator,
18+
} from '@/components/ui/breadcrumb';
19+
import { ScrollArea } from '@/components/ui/scroll-area';
20+
import { Fragment } from 'react/jsx-runtime';
21+
import { cn } from '@/lib/utils';
1122

1223
const questionDetailsQuery = (id: number) =>
1324
queryOptions({
@@ -23,35 +34,103 @@ export const loader =
2334
return { questionId };
2435
};
2536

37+
const BreadCrumbLinks = ({ id, title }: { id: number; title: string }) => {
38+
const links = [
39+
{
40+
link: '/',
41+
label: 'Home',
42+
},
43+
{
44+
link: '/questions',
45+
label: 'Questions',
46+
},
47+
{
48+
link: '/questions/' + id,
49+
label: '1. ' + title,
50+
isCurrent: true,
51+
},
52+
];
53+
return links.map(({ link, label, isCurrent }, index) => (
54+
<Fragment key={index}>
55+
<BreadcrumbItem>
56+
<BreadcrumbLink asChild>
57+
<Link to={link} className={cn(isCurrent && 'text-secondary-foreground')}>
58+
{label}
59+
</Link>
60+
</BreadcrumbLink>
61+
</BreadcrumbItem>
62+
{index < links.length - 1 && <BreadcrumbSeparator />}
63+
</Fragment>
64+
));
65+
};
66+
2667
export const QuestionDetails = () => {
2768
const { questionId } = useLoaderData() as Awaited<ReturnType<ReturnType<typeof loader>>>;
2869
const { data: details } = useSuspenseQuery(questionDetailsQuery(questionId));
2970
return (
30-
<Card>
31-
<CardHeader>
32-
<div className='flex flex-col gap-4'>
33-
<div className='flex w-full items-center gap-4'>
34-
<CardTitle className='text-2xl'>{details.title}</CardTitle>
35-
<Badge className='flex w-min grow-0'>{details.difficulty}</Badge>
36-
</div>
37-
<div className='flex flex-wrap items-center gap-1'>
38-
{details.topics.map((v, i) => (
39-
<Badge className='flex w-min grow-0 whitespace-nowrap' key={i}>
40-
{v}
41-
</Badge>
42-
))}
43-
</div>
44-
</div>
45-
</CardHeader>
46-
<CardContent>
47-
<Markdown
48-
rehypePlugins={[rehypeKatex]}
49-
remarkPlugins={[remarkMath, remarkGfm]}
50-
className='prose prose-neutral'
51-
>
52-
{details.description}
53-
</Markdown>
54-
</CardContent>
55-
</Card>
71+
<div className='flex h-[calc(100dvh-64px)] w-full flex-col'>
72+
<div className='bg-secondary/50 flex w-full p-4 px-6'>
73+
<Breadcrumb>
74+
<BreadcrumbList>
75+
<BreadCrumbLinks id={details.id} title={details.title} />
76+
</BreadcrumbList>
77+
</Breadcrumb>
78+
</div>
79+
<div className='flex flex-1 overflow-hidden'>
80+
<Card className='border-border m-4 w-1/3 max-w-[500px] overflow-hidden p-4 md:w-2/5'>
81+
<ScrollArea className='h-full'>
82+
<CardHeader>
83+
<div className='flex flex-col gap-4'>
84+
<div className='flex w-full items-center gap-4'>
85+
<CardTitle className='text-2xl'>
86+
{details.id}.&nbsp;{details.title}
87+
</CardTitle>
88+
</div>
89+
<div className='flex flex-wrap items-center gap-1'>
90+
<Badge variant='secondary' className='flex w-min grow-0'>
91+
{details.difficulty}
92+
</Badge>
93+
<Separator orientation='vertical' className='mx-2 h-4' />
94+
<span className='text-sm font-medium'>Topics:</span>
95+
{details.topics.map((v, i) => (
96+
<Badge
97+
variant='secondary'
98+
className='flex w-min grow-0 whitespace-nowrap'
99+
key={i}
100+
>
101+
{v}
102+
</Badge>
103+
))}
104+
</div>
105+
</div>
106+
</CardHeader>
107+
<CardContent>
108+
<Markdown
109+
rehypePlugins={[rehypeKatex]}
110+
remarkPlugins={[remarkMath, remarkGfm]}
111+
className='prose prose-neutral text-card-foreground prose-strong:text-card-foreground leading-normal'
112+
components={{
113+
code: ({ children, className, ...rest }) => {
114+
// const isCodeBlock = /language-(\w+)/.exec(className || '');
115+
116+
return (
117+
<code
118+
{...rest}
119+
className='bg-secondary text-secondary-foreground rounded px-1.5 py-1 font-mono'
120+
>
121+
{children}
122+
</code>
123+
);
124+
},
125+
}}
126+
>
127+
{details.description}
128+
</Markdown>
129+
</CardContent>
130+
</ScrollArea>
131+
</Card>
132+
<div className='flex flex-1 flex-col' />
133+
</div>
134+
</div>
56135
);
57136
};

frontend/tailwind.config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ const config = {
5555
5: 'hsl(var(--chart-5))',
5656
},
5757
},
58+
typography(theme) {
59+
return {
60+
DEFAULT: {
61+
css: {
62+
'code::before': {
63+
content: 'none',
64+
},
65+
'code::after': {
66+
content: 'none',
67+
},
68+
}
69+
}
70+
}
71+
}
5872
},
5973
},
6074
plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],

package-lock.json

Lines changed: 62 additions & 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)