feat: add layout
This commit is contained in:
parent
08621df30b
commit
e5bb8bea6e
@ -15,10 +15,17 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@chakra-ui/icons": "^2.1.1",
|
||||||
|
"@chakra-ui/react": "^2.8.2",
|
||||||
|
"@emotion/react": "^11.11.3",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
"@reduxjs/toolkit": "^2.2.1",
|
"@reduxjs/toolkit": "^2.2.1",
|
||||||
|
"framer-motion": "^11.0.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-redux": "^9.1.0"
|
"react-icons": "^5.0.1",
|
||||||
|
"react-redux": "^9.1.0",
|
||||||
|
"react-router-dom": "^6.22.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.57",
|
"@types/react": "^18.2.57",
|
||||||
|
1629
pnpm-lock.yaml
1629
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
39
src/App.css
39
src/App.css
@ -1,39 +0,0 @@
|
|||||||
.App {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-logo {
|
|
||||||
height: 40vmin;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.App-logo {
|
|
||||||
animation: App-logo-float infinite 3s ease-in-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-header {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: calc(10px + 2vmin);
|
|
||||||
}
|
|
||||||
|
|
||||||
.App-link {
|
|
||||||
color: rgb(112, 76, 182);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes App-logo-float {
|
|
||||||
0% {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
}
|
|
48
src/App.tsx
48
src/App.tsx
@ -1,48 +0,0 @@
|
|||||||
import "./App.css";
|
|
||||||
import { Counter } from "./features/counter/Counter";
|
|
||||||
import { Quotes } from "./features/quotes/Quotes";
|
|
||||||
import logo from "./logo.svg";
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<header className="App-header">
|
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
|
||||||
<Counter />
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to reload.
|
|
||||||
</p>
|
|
||||||
<Quotes />
|
|
||||||
<span>
|
|
||||||
<span>Learn </span>
|
|
||||||
<a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
|
|
||||||
React
|
|
||||||
</a>
|
|
||||||
<span>, </span>
|
|
||||||
<a className="App-link" href="https://redux.js.org" target="_blank" rel="noopener noreferrer">
|
|
||||||
Redux
|
|
||||||
</a>
|
|
||||||
<span>, </span>
|
|
||||||
<a
|
|
||||||
className="App-link"
|
|
||||||
href="https://redux-toolkit.js.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Redux Toolkit
|
|
||||||
</a>
|
|
||||||
<span>, </span>
|
|
||||||
<a className="App-link" href="https://react-redux.js.org" target="_blank" rel="noopener noreferrer">
|
|
||||||
React Redux
|
|
||||||
</a>
|
|
||||||
,<span> and </span>
|
|
||||||
<a className="App-link" href="https://reselect.js.org" target="_blank" rel="noopener noreferrer">
|
|
||||||
Reselect
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
30
src/components/Footer.tsx
Normal file
30
src/components/Footer.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Box, Flex, Text, useColorModeValue } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
return (
|
||||||
|
<Box w="100%" bg={useColorModeValue("gray.50", "gray.900")} color={useColorModeValue("gray.700", "gray.200")}>
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
pt="8"
|
||||||
|
_before={{
|
||||||
|
content: '""',
|
||||||
|
borderBottom: "1px solid",
|
||||||
|
borderColor: useColorModeValue("gray.200", "gray.700"),
|
||||||
|
flexGrow: 1,
|
||||||
|
mr: 8,
|
||||||
|
}}
|
||||||
|
_after={{
|
||||||
|
content: '""',
|
||||||
|
borderBottom: "1px solid",
|
||||||
|
borderColor: useColorModeValue("gray.200", "gray.700"),
|
||||||
|
flexGrow: 1,
|
||||||
|
ml: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" textAlign="center">
|
||||||
|
© {new Date().getFullYear()} WOJ Created by WHUPRJ.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
58
src/components/Header.tsx
Normal file
58
src/components/Header.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
Link as ChakraLink,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuDivider,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useColorMode,
|
||||||
|
useColorModeValue,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { Link as ReactRouterLink } from "react-router-dom";
|
||||||
|
import { MoonIcon, SunIcon } from "@chakra-ui/icons";
|
||||||
|
|
||||||
|
export const Header = () => {
|
||||||
|
const { colorMode, toggleColorMode } = useColorMode();
|
||||||
|
return (
|
||||||
|
<Box width="100%" bg={useColorModeValue("gray.100", "gray.900")} px={4}>
|
||||||
|
<Flex h={16} alignItems="center" justifyContent="space-between">
|
||||||
|
<Box>
|
||||||
|
<ChakraLink as={ReactRouterLink} to="/home">
|
||||||
|
<Text as="b" fontSize="lg">
|
||||||
|
Woo Online Judge
|
||||||
|
</Text>
|
||||||
|
</ChakraLink>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Stack direction="row" spacing={7}>
|
||||||
|
<Button onClick={toggleColorMode}>{colorMode === "light" ? <MoonIcon /> : <SunIcon />}</Button>
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={Button} rounded="full" variant="link" cursor="pointer" minW={0}>
|
||||||
|
<Avatar size="sm" src={"https://api.dicebear.com/7.x/shapes/svg"} />
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList alignItems="center">
|
||||||
|
<Center p={2}>
|
||||||
|
<Avatar size="2xl" src={"https://api.dicebear.com/7.x/shapes/svg"} />
|
||||||
|
</Center>
|
||||||
|
<Center>
|
||||||
|
<p>Username</p>
|
||||||
|
</Center>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem>Account Settings</MenuItem>
|
||||||
|
<MenuItem>Logout</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -1,11 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g fill="#764ABC"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,10 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
|
import { ChakraProvider } from "@chakra-ui/react";
|
||||||
|
|
||||||
import App from "./App";
|
|
||||||
import { store } from "./app/store";
|
import { store } from "./app/store";
|
||||||
import "./index.css";
|
import { router } from "./routes";
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
const container = document.getElementById("root");
|
||||||
|
|
||||||
@ -14,7 +15,9 @@ if (container) {
|
|||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<App />
|
<ChakraProvider>
|
||||||
|
<RouterProvider router={createBrowserRouter(router)} />
|
||||||
|
</ChakraProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
47
src/pages/ErrorPage.tsx
Normal file
47
src/pages/ErrorPage.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { isRouteErrorResponse, useNavigate, useRouteError } from "react-router-dom";
|
||||||
|
import { Alert, AlertDescription, AlertIcon, AlertTitle, Button, ButtonGroup } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
export const ErrorPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const error = useRouteError();
|
||||||
|
|
||||||
|
const convertError = (error: unknown): string => {
|
||||||
|
if (isRouteErrorResponse(error)) {
|
||||||
|
return `${error.status} ${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 (
|
||||||
|
<Alert
|
||||||
|
status="error"
|
||||||
|
variant="subtle"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
textAlign="center"
|
||||||
|
height="200px"
|
||||||
|
>
|
||||||
|
<AlertIcon boxSize="40px" mr={0} />
|
||||||
|
<AlertTitle mt={4} mb={1} fontSize="lg">
|
||||||
|
{" "}
|
||||||
|
Sorry, an unexpected error has occurred.{" "}
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription maxWidth="sm">{convertError(error)}</AlertDescription>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button variant="solid" colorScheme="orange" onClick={() => navigate(-1)}>
|
||||||
|
Previous Page
|
||||||
|
</Button>
|
||||||
|
<Button variant="solid" colorScheme="orange" onClick={() => navigate("/")}>
|
||||||
|
Home
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
9
src/pages/HomePage.tsx
Normal file
9
src/pages/HomePage.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Container, Heading } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
export const HomePage = () => {
|
||||||
|
return (
|
||||||
|
<Container maxW="container.lg">
|
||||||
|
<Heading>Hello World!</Heading>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
27
src/pages/Root.tsx
Normal file
27
src/pages/Root.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box, Flex, SkeletonCircle, SkeletonText } from "@chakra-ui/react";
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
|
import Footer from "../components/Footer";
|
||||||
|
import { Header } from "../components/Header";
|
||||||
|
|
||||||
|
const SkeletonPage = () => (
|
||||||
|
<Box padding="6" boxShadow="lg" bg="white">
|
||||||
|
<SkeletonCircle size="10" />
|
||||||
|
<SkeletonText mt="4" noOfLines={6} spacing="4" skeletonHeight="2" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Root = () => (
|
||||||
|
<Flex direction="column" align="center" maxW={{ xl: "1200px" }} m="0 auto">
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<Box width="100%" flex="1">
|
||||||
|
<React.Suspense fallback={<SkeletonPage />}>
|
||||||
|
<Outlet />
|
||||||
|
</React.Suspense>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</Flex>
|
||||||
|
);
|
24
src/routes.tsx
Normal file
24
src/routes.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { RouteObject } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Root } from "./pages/Root";
|
||||||
|
import { ErrorPage } from "./pages/ErrorPage";
|
||||||
|
import { HomePage } from "./pages/HomePage";
|
||||||
|
|
||||||
|
export const router: RouteObject[] = [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <Root />,
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
errorElement: <ErrorPage />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <HomePage />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
Loading…
Reference in New Issue
Block a user