feat: basic sketch
This commit is contained in:
parent
a4b0d0308c
commit
0b51183cd7
42
src/App.css
42
src/App.css
@ -1,42 +0,0 @@
|
|||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
35
src/App.tsx
35
src/App.tsx
@ -1,35 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import reactLogo from './assets/react.svg'
|
|
||||||
import viteLogo from '/vite.svg'
|
|
||||||
import './App.css'
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [count, setCount] = useState(0)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<a href="https://vitejs.dev" target="_blank">
|
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://react.dev" target="_blank">
|
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<h1>Vite + React</h1>
|
|
||||||
<div className="card">
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>
|
|
||||||
count is {count}
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="read-the-docs">
|
|
||||||
Click on the Vite and React logos to learn more
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
58
src/components/markdown.tsx
Normal file
58
src/components/markdown.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { Components } from "react-markdown/lib/ast-to-react";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import emoji from "remark-emoji";
|
||||||
|
import rehypeRaw from "rehype-raw";
|
||||||
|
import rehypeMathJaxSvg from "rehype-mathjax";
|
||||||
|
|
||||||
|
import { PrismAsync } from "react-syntax-highlighter";
|
||||||
|
import { oneLight } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||||
|
|
||||||
|
import "github-markdown-css";
|
||||||
|
|
||||||
|
interface MarkdownProps {
|
||||||
|
markdown: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Markdown(props: MarkdownProps) {
|
||||||
|
const remarkPlugins = [remarkGfm, emoji, remarkMath];
|
||||||
|
const rehypePlugins = [rehypeRaw, rehypeMathJaxSvg];
|
||||||
|
|
||||||
|
const renderers: Components = {
|
||||||
|
code: function makeCodeBlock({
|
||||||
|
inline,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
const codeBlock = (
|
||||||
|
<PrismAsync
|
||||||
|
{...props}
|
||||||
|
language={(match || ["", "text"])[1]}
|
||||||
|
PreTag="div"
|
||||||
|
children={String(children).replace(/\n$/, "")}
|
||||||
|
wrapLines={true}
|
||||||
|
showLineNumbers={true}
|
||||||
|
wrapLongLines={true}
|
||||||
|
style={oneLight}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const codeInline = <code {...props}>{children}</code>;
|
||||||
|
return !inline && match ? codeBlock : codeInline;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ReactMarkdown
|
||||||
|
className={"markdown-body"}
|
||||||
|
children={props.markdown}
|
||||||
|
remarkPlugins={remarkPlugins}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
components={renderers}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,69 +1,14 @@
|
|||||||
:root {
|
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
display: flex;
|
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
||||||
place-items: center;
|
"Helvetica Neue", sans-serif;
|
||||||
min-width: 320px;
|
-webkit-font-smoothing: antialiased;
|
||||||
min-height: 100vh;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
code {
|
||||||
font-size: 3.2em;
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||||
line-height: 1.1;
|
monospace;
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
|
||||||
color: #213547;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
83
src/main.tsx
83
src/main.tsx
@ -1,10 +1,75 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from "react-dom/client";
|
||||||
import App from './App.tsx'
|
import {
|
||||||
import './index.css'
|
createBrowserRouter,
|
||||||
|
createRoutesFromChildren,
|
||||||
|
matchRoutes,
|
||||||
|
RouterProvider,
|
||||||
|
useLocation,
|
||||||
|
useNavigationType,
|
||||||
|
} from "react-router-dom";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
import { Root, HomePage, ErrorPage } from "./pages/pages.tsx";
|
||||||
<React.StrictMode>
|
import { RouteConfigs } from "./routes.tsx";
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
import "./index.css";
|
||||||
)
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: "https://4f90ea95bb8b462c8d8432ddbabac9b8@o354675.ingest.sentry.io/4505537167491072",
|
||||||
|
integrations: [
|
||||||
|
new Sentry.BrowserTracing({
|
||||||
|
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
|
||||||
|
React.useEffect,
|
||||||
|
useLocation,
|
||||||
|
useNavigationType,
|
||||||
|
createRoutesFromChildren,
|
||||||
|
matchRoutes,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
new Sentry.Replay(),
|
||||||
|
],
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
tracePropagationTargets: ["localhost"],
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sentryCreateBrowserRouter =
|
||||||
|
Sentry.wrapCreateBrowserRouter(createBrowserRouter);
|
||||||
|
|
||||||
|
const router = sentryCreateBrowserRouter([
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <Root />,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <HomePage />,
|
||||||
|
},
|
||||||
|
...RouteConfigs.map((config) => {
|
||||||
|
const basic = {
|
||||||
|
path: config.path,
|
||||||
|
element: config.element,
|
||||||
|
};
|
||||||
|
if (config.loader) {
|
||||||
|
return { ...basic, loader: config.loader };
|
||||||
|
} else {
|
||||||
|
return basic;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
39
src/pages/error-page.tsx
Normal file
39
src/pages/error-page.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { useRouteError, isRouteErrorResponse } from "react-router-dom";
|
||||||
|
|
||||||
|
const ErrorPage = () => {
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: "80vh",
|
||||||
|
lineHeight: "0.8",
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = useRouteError();
|
||||||
|
const convertError = (error: unknown): string => {
|
||||||
|
if (isRouteErrorResponse(error)) {
|
||||||
|
return error.error?.message || error.statusText;
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
} else if (typeof error === "string") {
|
||||||
|
return error;
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
return "Unknown error";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
<h1>Oops!</h1>
|
||||||
|
<p>Sorry, an unexpected error has occurred.</p>
|
||||||
|
<p>
|
||||||
|
<i style={{ color: "#696969" }}>{convertError(error)}</i>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ErrorPage };
|
5
src/pages/home.tsx
Normal file
5
src/pages/home.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function HomePage() {
|
||||||
|
return <div>Home Page</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HomePage };
|
4
src/pages/pages.tsx
Normal file
4
src/pages/pages.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { Root } from "./root";
|
||||||
|
export { ErrorPage } from "./error-page";
|
||||||
|
export { HomePage } from "./home";
|
||||||
|
export { ProblemLoader, ProblemPage } from "./problem";
|
64
src/pages/problem.tsx
Normal file
64
src/pages/problem.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Row, Col, Card, Button, Space } from "antd";
|
||||||
|
import Markdown from "../components/markdown.tsx";
|
||||||
|
import { useLoaderData } from "react-router-dom";
|
||||||
|
|
||||||
|
interface LoaderProps {
|
||||||
|
params: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProblemProps {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ProblemLoader({ params }: LoaderProps) {
|
||||||
|
console.log(params.id);
|
||||||
|
const text = `
|
||||||
|
# Problem 1
|
||||||
|
|
||||||
|
| Time Limit | Memory Limit |
|
||||||
|
| ---------- | ------------ |
|
||||||
|
| 1s | 256MB |
|
||||||
|
|
||||||
|
\`\`\`cpp
|
||||||
|
#include<iostream>
|
||||||
|
using namespace std;
|
||||||
|
int main() {
|
||||||
|
cout << "hello world" << endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
I'm a paragraph.
|
||||||
|
|
||||||
|
inline \`code\` test
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
return { text: text };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProblemPage() {
|
||||||
|
const { text } = useLoaderData() as ProblemProps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row justify="center" align="top" gutter={[16, 16]}>
|
||||||
|
<Col span={18}>
|
||||||
|
<Markdown markdown={text} />
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Card title="卡片标题">
|
||||||
|
<Space>
|
||||||
|
<Button type="primary">Primary</Button>
|
||||||
|
<Button type="primary">Primary</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ProblemPage };
|
63
src/pages/root.tsx
Normal file
63
src/pages/root.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { Link, Outlet, useLocation, useNavigation } from "react-router-dom";
|
||||||
|
import { Layout, Menu, Skeleton, theme } from "antd";
|
||||||
|
|
||||||
|
import { NavConfigs } from "../routes.tsx";
|
||||||
|
|
||||||
|
const { Header, Footer, Content } = Layout;
|
||||||
|
|
||||||
|
function Root() {
|
||||||
|
const {
|
||||||
|
token: { colorBgContainer },
|
||||||
|
} = theme.useToken();
|
||||||
|
const nav = useNavigation();
|
||||||
|
const pathname = useLocation().pathname;
|
||||||
|
|
||||||
|
const pathToKey = () => {
|
||||||
|
const routes = NavConfigs.filter((c) => pathname.startsWith(c.prefix));
|
||||||
|
if (routes.length === 0) return ["home"];
|
||||||
|
return [routes[0].key];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Layout>
|
||||||
|
<Header style={{ alignItems: "center" }}>
|
||||||
|
<Menu
|
||||||
|
theme="dark"
|
||||||
|
mode="horizontal"
|
||||||
|
defaultSelectedKeys={["home"]}
|
||||||
|
selectedKeys={pathToKey()}
|
||||||
|
>
|
||||||
|
{NavConfigs.map((c) => (
|
||||||
|
<Menu.Item key={c.key}>
|
||||||
|
<Link to={c.to}>{c.label}</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</Header>
|
||||||
|
<Layout>
|
||||||
|
<Content
|
||||||
|
style={{
|
||||||
|
background: colorBgContainer,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingTop: "30px",
|
||||||
|
paddingBottom: "30px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: "90%" }}>
|
||||||
|
<Skeleton loading={nav.state === "loading"} active>
|
||||||
|
<Outlet />
|
||||||
|
</Skeleton>
|
||||||
|
</div>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
<Footer style={{ textAlign: "center" }}>
|
||||||
|
WOJ ©2023 Created by WHUPRJ
|
||||||
|
</Footer>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Root };
|
0
src/pages/search.tsx
Normal file
0
src/pages/search.tsx
Normal file
29
src/routes.tsx
Normal file
29
src/routes.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { HomePage } from "./pages/home.tsx";
|
||||||
|
import { ProblemLoader, ProblemPage } from "./pages/pages.tsx";
|
||||||
|
|
||||||
|
const RouteConfigs = [
|
||||||
|
{
|
||||||
|
path: "home",
|
||||||
|
element: <HomePage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "problem",
|
||||||
|
element: <div>problems</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "problem/:id",
|
||||||
|
element: <ProblemPage />,
|
||||||
|
loader: ProblemLoader,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "problem/:id/submit",
|
||||||
|
element: <div>problem submit</div>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const NavConfigs = [
|
||||||
|
{ key: "home", to: "home", label: "Home", prefix: "/home" },
|
||||||
|
{ key: "problem", to: "problem", label: "Problem", prefix: "/problem" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export { RouteConfigs, NavConfigs };
|
Reference in New Issue
Block a user