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 {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
||||
"Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
83
src/main.tsx
83
src/main.tsx
@ -1,10 +1,75 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
createRoutesFromChildren,
|
||||
matchRoutes,
|
||||
RouterProvider,
|
||||
useLocation,
|
||||
useNavigationType,
|
||||
} from "react-router-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
import { Root, HomePage, ErrorPage } from "./pages/pages.tsx";
|
||||
import { RouteConfigs } from "./routes.tsx";
|
||||
|
||||
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