-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
351 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...lopment/platform-services/_category_.json → docs/development/backend/_category_.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
{ | ||
"label": "Platform Services", | ||
"label": "Backend", | ||
"position": 3, | ||
"link": { | ||
"type": "generated-index" | ||
|
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion
2
...velopment/dev-environment/_category_.json → docs/development/environment/_category_.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
{ | ||
"label": "Develop Environment", | ||
"label": "Environment", | ||
"position": 2, | ||
"link": { | ||
"type": "generated-index" | ||
|
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"label": "Frontend", | ||
"position": 3, | ||
"link": { | ||
"type": "generated-index" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
# Making View | ||
|
||
This document will guide you through the process of creating a new view in | ||
[OJ Lab front](https://github.com/oj-lab/oj-lab-front) | ||
. | ||
|
||
You will get to know with three main directories: | ||
`/layouts`, `/pages` and `/components`. | ||
So that you can understand how the view is structured and what kind of codes you need to provide in each directory. | ||
|
||
:::tip | ||
|
||
We will discuss the usage of | ||
[React Hooks](https://reactjs.org/docs/hooks-intro.html) and state management | ||
in some parts of the document. | ||
|
||
If you are not familiar with React Hooks, you can check the official documentation. | ||
|
||
::: | ||
|
||
## Main Directories | ||
|
||
### Layouts | ||
|
||
Layout is a fundamental part of the view, | ||
it usually provides `header`, `footer`, `sidebar`, `nav` and other common components. | ||
In another word, layout is something it usually doesn't change between different pages. | ||
|
||
The `/layouts` directory contains the layout components. | ||
Currently, OJ Lab front has only one layout, | ||
so all the components are placed in the root of the directory | ||
(If we have more layouts in the future, we will create subdirectories for each layout). | ||
|
||
In OJ Lab front's `Router.tsx`, | ||
the layout component wraps other pages through React Router's [Outlet](https://reactrouter.com/en/main/components/outlet). | ||
|
||
```tsx | ||
// In Layout.tsx | ||
<div> | ||
{!props.children && <Outlet />} | ||
</div> | ||
|
||
// In Router.tsx | ||
<Route path="/" element={<Layout />}> | ||
<Route path="" element={<Navigate replace to="/problems" />} /> | ||
{/* Other routes */} | ||
</Route> | ||
``` | ||
|
||
### Pages | ||
|
||
Pages compose the main content of the view by combining different components and hooking in necessary states. | ||
|
||
```tsx | ||
const Judge: React.FC = () => { | ||
const uid = useParams().uid as string; | ||
const { getJudge } = useJudge(uid); | ||
|
||
const judge = getJudge(); | ||
|
||
return ( | ||
judge && ( | ||
<div className="grid gap-4 overflow-x-auto"> | ||
<div className="h-fit rounded border border-base-content/10 bg-base-100"> | ||
<JudgeTable data={[judge]} /> | ||
</div> | ||
<MarkdownRender content={getCodeBlock(judge.language, judge.code)} /> | ||
</div> | ||
) | ||
); | ||
}; | ||
``` | ||
|
||
In this example, we get the judge data by using the `useJudge` hook. | ||
The judge data is got by calling backend's API | ||
and then passed to the `JudgeTable` and `MarkdownRender` components. | ||
When the judge data is ready, the page will render the components. | ||
|
||
### Components | ||
|
||
Components are reusable pieces of code that can be used in different pages. | ||
|
||
:::warning | ||
|
||
There are some special circumstances where you need to use state in components. | ||
For example, the monaco editor component needs to change it's theme and size according to window settings. | ||
|
||
But usually, components should be stateless. | ||
|
||
::: | ||
|
||
In OJ Lab front, components are classified into `control`, | ||
`display`, `input`, `navigation` four categories. | ||
|
||
- `control` components are used to control the view, such as buttons, dropdowns, etc. | ||
- `display` components are used to display data, such as tables, cards, etc. | ||
- `input` components are used to get user input, such as forms, inputs, etc. | ||
- `navigation` components are used to navigate between pages, such as breadcrumbs, menus, etc. | ||
|
||
```tsx | ||
export interface UserMenuAction { | ||
name: string; | ||
href?: string; | ||
onClick?: () => void; | ||
} | ||
|
||
export interface UserMenuProps { | ||
avatarUrl?: string; | ||
actions?: Array<UserMenuAction>; | ||
} | ||
|
||
/** | ||
* @param {string} props.avatarUrl | ||
* @param {Array<{ name: string, href: string }>} props.navigation The name of navigation will be translated. | ||
*/ | ||
const UserMenu: React.FC<UserMenuProps> = (props) => { | ||
``` | ||
When working on a new view, you should first check if there are any existing components that can be reused. | ||
Then, if a new component is needed, you should design the props to pass into the component. | ||
## Tools | ||
### Tailwind CSS | ||
OJ Lab front uses [Tailwind CSS](https://tailwindcss.com/) as the main CSS framework. | ||
You can control the style of each component in a regular way by simply adding inline Tailwind CSS classes. | ||
With VSCode's [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) extension, | ||
writing Tailwind CSS classes becomes much easier. | ||
You may also need to use `joinClass` utility function to dynamically add classes to a component. | ||
```tsx | ||
<div | ||
className={joinClasses( | ||
"dropdown dropdown-end", | ||
open && "dropdown-open", | ||
)} | ||
onClick={() => { | ||
setOpen(!open); | ||
}} | ||
> | ||
``` | ||
|
||
:::tip | ||
|
||
There are also many other useful utility functions | ||
which can be used in different parts of the project | ||
in the `utils` directory. | ||
|
||
::: | ||
|
||
## Best Practices | ||
|
||
You should avoid using too many inline logic in the TSX code. | ||
Instead, you should extract the logic into hooks or functions. | ||
|
||
```tsx | ||
const ProblemActions: React.FC<ActionsProps> = (props) => { | ||
const onClickDelete = (e: React.MouseEvent) => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
|
||
props.onClickDelete(); | ||
const modal = document.getElementById( | ||
"delete_confirm_modal", | ||
) as HTMLDialogElement; | ||
modal?.showModal(); | ||
}; | ||
|
||
return ( | ||
<div className="flex space-x-1"> | ||
<button className="btn btn-square btn-ghost btn-sm rounded hover:bg-primary/20"> | ||
<PencilIcon className="h-5 w-5 text-primary" /> | ||
</button> | ||
<button | ||
className="btn btn-square btn-ghost btn-sm rounded hover:bg-error/20" | ||
onClick={onClickDelete} | ||
> | ||
<TrashIcon className="h-5 w-5 text-error" /> | ||
</button> | ||
</div> | ||
); | ||
}; | ||
``` | ||
|
||
In this example, the `onClickDelete` function is extracted from the `onClick` event handler. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
# Models and APIs | ||
|
||
In this document, we will introduce how to use APIs | ||
and how models help us safely constructing OJ Lab front's data transmission by TypeScript. | ||
|
||
## Models | ||
|
||
In OJ Lab front, models are classified into two types: `ServiceModel` and `ViewModel`. | ||
|
||
- `ServiceModel` is expected to be exactly the format of the data returned by the backend APIs | ||
- `ViewModel` on the other hand, provides a more user-friendly format for the front-end components. | ||
They are usually transformed from the `ServiceModel` through `/pipes` module. | ||
|
||
```ts | ||
import { formatBytes, formatNanoSeconds } from "@/utils/unit"; | ||
import * as JudgeServiceModel from "@/models/service/judge"; | ||
import * as JudgeViewModel from "@/models/view/judge"; | ||
|
||
export const judgeVerdictPipe = ( | ||
judgeVerdict: JudgeServiceModel.JudgeVerdict, | ||
): JudgeViewModel.JudgeVerdict => { | ||
let verdict = judgeVerdict.verdict; | ||
let time_usage = formatNanoSeconds(judgeVerdict.time_usage.nanos); | ||
let memory_usage = formatBytes(judgeVerdict.memory_usage_bytes); | ||
|
||
return { verdict, time_usage, memory_usage }; | ||
}; | ||
``` | ||
|
||
In this example, we define a pipe function to transform the `JudgeServiceModel.JudgeVerdict` to `JudgeViewModel.JudgeVerdict` with `formatBytes` and `formatNanoSeconds` utility functions. | ||
|
||
## APIs | ||
|
||
For RESTful APIs, we use `axios` to send requests and handle responses. | ||
|
||
```ts | ||
export async function getCurrentUser(): Promise<UserServiceModel.UserInfo> { | ||
let res = await axiosClient.get<UserServiceModel.UserInfo>( | ||
"/api/v1/user/current", | ||
); | ||
if (res.status !== 200) { | ||
throw Error("failed to get current user"); | ||
} | ||
return res.data; | ||
} | ||
``` | ||
|
||
`axiosClient` is a utility function that wraps `axios` with some default configurations. | ||
|
||
### Use APIs in Hook | ||
|
||
In OJ Lab front, we usually hook detailed APIs usage in customized hooks. | ||
|
||
```ts | ||
export const useJudge = (uid: string) => { | ||
const [judge, setJudge] = useState<JudgeServiceModel.JudgeInfo>(); | ||
useEffect(() => { | ||
JudgeService.getJudge(uid) | ||
.then((res) => { | ||
setJudge(res); | ||
}) | ||
.catch((err) => { | ||
console.log(err); | ||
}); | ||
}, [uid]); | ||
|
||
function getJudge() { | ||
return judge; | ||
} | ||
|
||
return { getJudge }; | ||
}; | ||
``` | ||
|
||
In the page component, we can use the `useJudge` hook to get the judge data and render the components. | ||
This part of code has been introduced in [Making View](./making-view.md#pages). | ||
|
||
### Use APIs in Redux Saga | ||
|
||
A more advanced usage is to use APIs in Redux Saga. | ||
This approach is usually used in global state management, | ||
for example, getting the current user information when the app is initialized. | ||
|
||
```ts | ||
function* getCurrentUserSaga() { | ||
const user: UserServiceModel.UserInfo = yield call(getCurrentUser); | ||
yield put(setUserInfo(user)); | ||
} | ||
|
||
const GetCurrentUserSagaPattern = "user/getCurrentUser/saga"; | ||
|
||
export const getCurrentUserAction: Action = { | ||
type: GetCurrentUserSagaPattern, | ||
}; | ||
|
||
export function* watchGetUserInfo() { | ||
yield takeEvery(GetCurrentUserSagaPattern, getCurrentUserSaga); | ||
} | ||
``` | ||
|
||
Then in the layout component, we can dispatch the `getCurrentUserAction` to trigger the saga. | ||
|
||
```tsx | ||
const Layout: React.FC<LayoutProps> = (props) => { | ||
let dispatch = useDispatch(); | ||
|
||
useEffect(() => { | ||
dispatch(getCurrentUserAction); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, []); | ||
|
||
// ... | ||
}; | ||
``` | ||
|
||
Finally, you can use the `useSelector` hook to get the user information in any component. | ||
|
||
```tsx | ||
const UserMenu: React.FC<UserMenuProps> = (props) => { | ||
// ... | ||
const userInfo = useSelector(userInfoSelector); | ||
const isLogined = userInfo !== undefined; | ||
|
||
// ... | ||
}; | ||
``` | ||
|
||
## Best Practices | ||
|
||
### Import with Alias | ||
|
||
To avoid the confusion and conflicts of the same name modules, | ||
we should use alias to import the models and APIs. | ||
|
||
```ts | ||
import * as JudgeServiceModel from "@/models/service/judge"; | ||
import * as JudgeService from "@/apis/judge"; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.