Skip to content

Commit 9d0db7f

Browse files
committed
chore/ui: Add tweaks for markdown editors
Signed-off-by: SeeuSim <[email protected]>
1 parent 3c693a1 commit 9d0db7f

File tree

10 files changed

+310
-189
lines changed

10 files changed

+310
-189
lines changed

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"type": "module",
55
"scripts": {
66
"dev": "env-cmd -f .env.local vite",
7-
"build": "tsc -b && vite build -d --emptyOutDir",
7+
"build": "tsc -b && vite build",
88
"lint": "eslint .",
99
"preview": "vite preview --port 5173 --host 0.0.0.0"
1010
},

frontend/src/components/blocks/questions/admin-edit-form.tsx

Lines changed: 163 additions & 156 deletions
Large diffs are not rendered by default.

frontend/src/components/blocks/questions/details.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { DotsVerticalIcon, Pencil1Icon, TrashIcon } from '@radix-ui/react-icons';
22
import { useState } from 'react';
33
import Markdown from 'react-markdown';
4+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5+
import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
46
import rehypeKatex from 'rehype-katex';
57
import remarkGfm from 'remark-gfm';
68
import remarkMath from 'remark-math';
@@ -99,15 +101,26 @@ export const QuestionDetails = ({
99101
<Markdown
100102
rehypePlugins={[rehypeKatex]}
101103
remarkPlugins={[remarkMath, remarkGfm]}
102-
className='prose prose-neutral text-card-foreground prose-strong:text-card-foreground prose-pre:bg-secondary prose-pre:text-secondary-foreground leading-normal'
104+
className='prose prose-neutral text-card-foreground prose-strong:text-card-foreground prose-pre:p-0 leading-normal'
103105
components={{
104106
code: ({ children, className, ...rest }) => {
105-
// const isCodeBlock = /language-(\w+)/.exec(className || '');
106-
107-
return (
107+
const match = /language-(\w+)/.exec(className || '');
108+
return match ? (
109+
<SyntaxHighlighter
110+
customStyle={{
111+
borderRadius: '0.3em',
112+
margin: 0,
113+
}}
114+
PreTag='div'
115+
style={oneLight}
116+
language={match[1]}
117+
>
118+
{String(children)}
119+
</SyntaxHighlighter>
120+
) : (
108121
<code
109122
{...rest}
110-
className='dark:bg-secondary dark:text-secondary-foreground rounded px-1.5 py-1 font-mono'
123+
className='bg-secondary text-secondary-foreground rounded px-1.5 py-1 font-mono'
111124
>
112125
{children}
113126
</code>

frontend/src/components/ui/combobox.tsx

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type Option = {
2121
value: string;
2222
};
2323

24-
type ComboboxProps = {
24+
type ComboboxMultiProps = {
2525
options: Array<Option>;
2626
placeholderText: string;
2727
noOptionsText: string;
@@ -30,7 +30,7 @@ type ComboboxProps = {
3030

3131
export const ComboboxMulti = React.forwardRef<
3232
React.ElementRef<typeof PopoverPrimitive.Content>,
33-
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & ComboboxProps
33+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & ComboboxMultiProps
3434
>(({ className, options, placeholderText, noOptionsText, setValuesCallback, ...props }, ref) => {
3535
const [open, setOpen] = React.useState(false);
3636
const [selectedValues, setSelectedValues] = React.useState<Array<string>>([]);
@@ -100,3 +100,98 @@ export const ComboboxMulti = React.forwardRef<
100100
</Popover>
101101
);
102102
});
103+
104+
type ComboboxExternalProps = {
105+
options: Array<{
106+
value: string;
107+
label: string;
108+
}>;
109+
itemName: string;
110+
chosenOptions: Array<string>;
111+
setChosenOptions: (values: Array<string>) => void;
112+
};
113+
114+
export const ComboboxExternal = React.forwardRef<
115+
React.ElementRef<typeof PopoverPrimitive.Content>,
116+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & ComboboxExternalProps
117+
>(({ className, itemName, options, chosenOptions, setChosenOptions, ...props }, ref) => {
118+
const [isOpen, setIsOpen] = React.useState(false);
119+
const [inputVal, setInputVal] = React.useState('');
120+
121+
const onAddNewOption = () => {
122+
const opt = inputVal.replace(/^[a-zA-Z]/, (c) => c.toUpperCase());
123+
124+
if (!chosenOptions.includes(opt)) {
125+
setChosenOptions([...chosenOptions, opt]);
126+
}
127+
128+
setInputVal('');
129+
setIsOpen(false);
130+
};
131+
132+
return (
133+
<Popover open={isOpen} onOpenChange={setIsOpen}>
134+
<PopoverTrigger asChild>
135+
<Button
136+
variant='outline'
137+
role='combobox'
138+
aria-expanded={isOpen}
139+
className='w-[200px] justify-between'
140+
>
141+
<span>Select {itemName}...</span>
142+
<ChevronsUpDown className='opacity-50' />
143+
</Button>
144+
</PopoverTrigger>
145+
<PopoverContent ref={ref} {...props} className={cn('w-[200px] p-0', className)}>
146+
<Command>
147+
<CommandInput
148+
value={inputVal}
149+
onValueChange={setInputVal}
150+
placeholder={`Search or add ${itemName}...`}
151+
className='h-9'
152+
onKeyDown={(event) => {
153+
if (event.key === 'Enter') {
154+
event.preventDefault();
155+
onAddNewOption();
156+
}
157+
}}
158+
/>
159+
<CommandList>
160+
<CommandEmpty
161+
className='hover:bg-secondary hover:text-secondary-foreground p-3 text-sm hover:cursor-pointer'
162+
onClick={onAddNewOption}
163+
>
164+
Add {`"${inputVal}"`}
165+
</CommandEmpty>
166+
<CommandGroup>
167+
{options.map((option) => (
168+
<CommandItem
169+
className='hover:cursor-pointer'
170+
key={option.value}
171+
value={option.value}
172+
onSelect={(currentValue) => {
173+
if (chosenOptions.includes(currentValue)) {
174+
setChosenOptions(chosenOptions.filter((v) => v !== currentValue));
175+
} else {
176+
setChosenOptions([...chosenOptions, currentValue]);
177+
}
178+
179+
setIsOpen(false);
180+
}}
181+
>
182+
{option.label}
183+
<Check
184+
className={cn(
185+
'ml-auto',
186+
chosenOptions.includes(option.value) ? 'opacity-100' : 'opacity-0'
187+
)}
188+
/>
189+
</CommandItem>
190+
))}
191+
</CommandGroup>
192+
</CommandList>
193+
</Command>
194+
</PopoverContent>
195+
</Popover>
196+
);
197+
});

frontend/src/lib/router.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@ import { ForgotPassword } from '@/routes/forgot-password';
77
import { HomePage } from '@/routes/home';
88
import { InterviewRoom, loader as interviewRoomLoader } from '@/routes/interview/[room]';
99
import { Login } from '@/routes/login';
10+
import { Match } from '@/routes/match';
1011
import { loader as topicsLoader } from '@/routes/match/logic';
11-
import { Match } from '@/routes/match/main';
12+
import { loader as questionsLoader, Questions } from '@/routes/questions';
1213
import { loader as questionDetailsLoader, QuestionDetailsPage } from '@/routes/questions/details';
13-
import {
14-
// loader as questionsLoader,
15-
Questions,
16-
} from '@/routes/questions/main';
1714
import { SignUp } from '@/routes/signup';
1815

1916
import { queryClient } from './query-client';
@@ -36,6 +33,7 @@ export const router = createBrowserRouter([
3633
},
3734
{
3835
path: ROUTES.QUESTIONS,
36+
loader: questionsLoader,
3937
// loader: questionsLoader(queryClient),
4038
element: <Questions />,
4139
},

frontend/src/routes/match/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './logic';
2+
export * from './main';

frontend/src/routes/match/logic.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ export interface MatchFormData {
1414
difficulty: string;
1515
}
1616

17-
const getTopicsQueryConfig = () =>
17+
export const getTopicsQueryConfig = () =>
1818
queryOptions({
1919
queryKey: ['topics'],
2020
queryFn: async () => fetchTopics(),
2121
});
2222

23-
const getDifficultiesQueryConfig = () => {
23+
export const getDifficultiesQueryConfig = () => {
2424
return queryOptions({
2525
queryKey: ['difficulties'],
2626
queryFn: async () => fetchDifficulties(),

frontend/src/routes/questions/main.tsx

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
useEffect,
88
useMemo,
99
} from 'react';
10+
import { LoaderFunctionArgs, useLoaderData } from 'react-router-dom';
1011

1112
// import { Await, defer, type LoaderFunctionArgs, useLoaderData } from 'react-router-dom';
1213
import { WithNavBanner } from '@/components/blocks/authed';
@@ -22,23 +23,17 @@ import type { IGetQuestionsResponse } from '@/types/question-types';
2223
import { QuestionTable } from './question-table';
2324
import { columns } from './table-columns';
2425

25-
// type IQuestionListServiceAPIResponse = Awaited<ReturnType<typeof fetchQuestions>>;
26-
// type IQuestionLoaderReturn = Awaited<ReturnType<ReturnType<typeof loader>>>['data'];
27-
// type IQuestionLoaderData = { initialPage?: IQuestionListServiceAPIResponse };
28-
// const getListQuestionsQueryConfig = (pageNumber?: number) =>
29-
// queryOptions({
30-
// queryKey: ['qn', 'list', pageNumber],
31-
// queryFn: async ({ signal: _ }) => fetchQuestions(pageNumber),
32-
// });
33-
// export const loader =
34-
// (queryClient: QueryClient) =>
35-
// async ({ params: _ }: LoaderFunctionArgs) => {
36-
// return defer({
37-
// initialPage: queryClient.ensureQueryData(getListQuestionsQueryConfig()),
38-
// });
39-
// };
26+
export const loader = (args: LoaderFunctionArgs) => {
27+
const route = new URL(args.request.url);
28+
const params = route.searchParams;
29+
const pageNum = params.get('page');
30+
return {
31+
pageNum,
32+
};
33+
};
4034

4135
export function Questions() {
36+
const { pageNum } = useLoaderData() as ReturnType<typeof loader>;
4237
const { userId } = useAuthedRoute();
4338
usePageTitle(ROUTES.QUESTIONS);
4439
const { crumbs } = useCrumbs();
@@ -47,9 +42,9 @@ export function Questions() {
4742
IGetQuestionsResponse,
4843
Error
4944
>({
50-
queryKey: ['questions', userId],
45+
queryKey: ['questions', userId, pageNum],
5146
queryFn: ({ pageParam }) => fetchQuestions(userId, pageParam as number | undefined),
52-
initialPageParam: 0,
47+
initialPageParam: +(pageNum ?? ''),
5348
getNextPageParam: (lastPage, pages) => {
5449
const nextPage = pages.length;
5550
const totalPages = Math.ceil(lastPage.totalQuestions / ROWS_PER_PAGE);

frontend/src/routes/questions/question-table.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
getPaginationRowModel,
1515
useReactTable,
1616
} from '@tanstack/react-table';
17-
import { useState } from 'react';
17+
import { useEffect, useState } from 'react';
18+
import { useSearchParams } from 'react-router-dom';
1819

1920
import { AdminEditForm } from '@/components/blocks/questions/admin-edit-form';
2021
import { Button } from '@/components/ui/button';
@@ -49,6 +50,7 @@ export function QuestionTable<TData, TValue>({
4950
isError,
5051
}: QuestionTableProps<TData, TValue>) {
5152
const { isAdmin } = useAuthedRoute();
53+
const [_searchParams, setSearchParams] = useSearchParams();
5254
const [isAdminAddFormOpen, setIsAdminAddFormOpen] = useState(false);
5355
const [pagination, setPagination] = useState({
5456
pageIndex: 0,
@@ -77,6 +79,12 @@ export function QuestionTable<TData, TValue>({
7779
setColumnFilters(value == 'all' ? [] : [{ id: 'attempted', value: value === 'attempted' }]);
7880
};
7981

82+
useEffect(() => {
83+
const newParams = new URLSearchParams();
84+
newParams.set('pageNum', String(pagination.pageIndex));
85+
setSearchParams(newParams);
86+
}, [pagination.pageIndex]);
87+
8088
return (
8189
<div className='flex size-full flex-col'>
8290
<div className='flex items-center py-4'>

frontend/tailwind.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ const config = {
6565
'code::after': {
6666
content: 'none',
6767
},
68+
'.contains-task-list': {
69+
'list-style-type': 'none',
70+
},
6871
}
6972
}
7073
}

0 commit comments

Comments
 (0)