Compare commits
	
		
			2 Commits
		
	
	
		
			218a6c4c3a
			...
			48dd4a88bf
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 48dd4a88bf | |||
| fcd72690d5 | 
							
								
								
									
										7492
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7492
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -5,8 +5,6 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@apollo/client": "^3.3.15", |     "@apollo/client": "^3.3.15", | ||||||
|     "@craco/craco": "^6.4.0", |     "@craco/craco": "^6.4.0", | ||||||
|     "@curi/react-dom": "^2.0.4", |  | ||||||
|     "@curi/router": "^2.1.2", |  | ||||||
|     "@date-io/date-fns": "^1.3.13", |     "@date-io/date-fns": "^1.3.13", | ||||||
|     "@emotion/react": "^11.6.0", |     "@emotion/react": "^11.6.0", | ||||||
|     "@emotion/styled": "^11.6.0", |     "@emotion/styled": "^11.6.0", | ||||||
| @@ -41,6 +39,7 @@ | |||||||
|     "ramda": "^0.27.1", |     "ramda": "^0.27.1", | ||||||
|     "react": "^17.0.2", |     "react": "^17.0.2", | ||||||
|     "react-dom": "^17.0.2", |     "react-dom": "^17.0.2", | ||||||
|  |     "react-router-dom": "^6.0.2", | ||||||
|     "react-scripts": "4.0.3", |     "react-scripts": "4.0.3", | ||||||
|     "subscriptions-transport-ws": "^0.9.19", |     "subscriptions-transport-ws": "^0.9.19", | ||||||
|     "typescript": "^4.2.4", |     "typescript": "^4.2.4", | ||||||
| @@ -81,6 +80,7 @@ | |||||||
|     "@graphql-codegen/typescript-react-apollo": "2.2.3", |     "@graphql-codegen/typescript-react-apollo": "2.2.3", | ||||||
|     "@types/graphql": "^14.5.0", |     "@types/graphql": "^14.5.0", | ||||||
|     "@types/ramda": "^0.27.40", |     "@types/ramda": "^0.27.40", | ||||||
|  |     "@types/react-router-dom": "^5.3.2", | ||||||
|     "@types/sass": "^1.16.0", |     "@types/sass": "^1.16.0", | ||||||
|     "@types/yup": "^0.29.11", |     "@types/yup": "^0.29.11", | ||||||
|     "sass": "^1.32.11" |     "sass": "^1.32.11" | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								src/App.tsx
									
									
									
									
									
								
							| @@ -1,15 +1,12 @@ | |||||||
| import { useResponse } from '@curi/react-dom'; | import { BrowserRouter } from "react-router-dom"; | ||||||
| import './App.css'; | import "./App.css"; | ||||||
| import { DefaultLayout } from './layouts'; | import { AppRoutes } from "./routes"; | ||||||
|  |  | ||||||
| function App() { | function App() { | ||||||
| const { response } = useResponse(); |  | ||||||
|  |  | ||||||
| const { body: Body } = response; |  | ||||||
|   return ( |   return ( | ||||||
|     <DefaultLayout> |     <BrowserRouter> | ||||||
|       <Body response={response} /> |       <AppRoutes /> | ||||||
|     </DefaultLayout> |     </BrowserRouter> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,8 +14,8 @@ import { | |||||||
| } from "../generated/graphql"; | } from "../generated/graphql"; | ||||||
| import { DateTimePicker } from "formik-material-ui-pickers"; | import { DateTimePicker } from "formik-material-ui-pickers"; | ||||||
| import { gql, useMutation } from "@apollo/client"; | import { gql, useMutation } from "@apollo/client"; | ||||||
| import { useRouter } from "@curi/react-universal"; |  | ||||||
| import * as R from "ramda"; | import * as R from "ramda"; | ||||||
|  | import { useNavigate } from "react-router-dom"; | ||||||
|  |  | ||||||
| const CREATE_ARTICLE = gql` | const CREATE_ARTICLE = gql` | ||||||
|   mutation createArticle($createArticleInput: CreateArticleInput!) { |   mutation createArticle($createArticleInput: CreateArticleInput!) { | ||||||
| @@ -59,7 +59,7 @@ interface Props { | |||||||
|  |  | ||||||
| export const ArticleEditor: FC<Props> = ({ article }) => { | export const ArticleEditor: FC<Props> = ({ article }) => { | ||||||
|   const classes = useStyles(); |   const classes = useStyles(); | ||||||
|   const router = useRouter(); |   const navigate = useNavigate(); | ||||||
|   const formik = useRef<FormikProps<Values>>(null); |   const formik = useRef<FormikProps<Values>>(null); | ||||||
|  |  | ||||||
|   const validationSchema = Yup.object({ |   const validationSchema = Yup.object({ | ||||||
| @@ -144,10 +144,7 @@ export const ArticleEditor: FC<Props> = ({ article }) => { | |||||||
|         }, |         }, | ||||||
|       }) |       }) | ||||||
|         .then(() => { |         .then(() => { | ||||||
|           router.navigate({ |           navigate("/articles", { replace: true }); | ||||||
|             url: router.url({ name: "articles" }), |  | ||||||
|             method: "replace", |  | ||||||
|           }); |  | ||||||
|         }) |         }) | ||||||
|         .finally(() => { |         .finally(() => { | ||||||
|           setSubmitting(false); |           setSubmitting(false); | ||||||
| @@ -168,10 +165,7 @@ export const ArticleEditor: FC<Props> = ({ article }) => { | |||||||
|         }, |         }, | ||||||
|       }) |       }) | ||||||
|         .then(() => { |         .then(() => { | ||||||
|           router.navigate({ |           navigate("/articles", { replace: true }); | ||||||
|             url: router.url({ name: "articles" }), |  | ||||||
|             method: "replace", |  | ||||||
|           }); |  | ||||||
|         }) |         }) | ||||||
|         .finally(() => { |         .finally(() => { | ||||||
|           setSubmitting(false); |           setSubmitting(false); | ||||||
|   | |||||||
| @@ -12,10 +12,10 @@ import { | |||||||
| import React, { FC } from "react"; | import React, { FC } from "react"; | ||||||
| import { Article } from "../generated/graphql"; | import { Article } from "../generated/graphql"; | ||||||
| import EditIcon from "@mui/icons-material/Edit"; | import EditIcon from "@mui/icons-material/Edit"; | ||||||
| import { useRouter } from "@curi/react-dom"; | import { ARTICLES, REMOVE_ARTICLE } from "./articles.constants"; | ||||||
| import { ARTICLES, REMOVE_ARTICLE } from './articles.constants'; | import { Delete } from "@mui/icons-material"; | ||||||
| import { Delete } from '@mui/icons-material'; |  | ||||||
| import { format } from "date-fns"; | import { format } from "date-fns"; | ||||||
|  | import { useNavigate } from "react-router-dom"; | ||||||
|  |  | ||||||
| export const ArticleIndex: FC = () => { | export const ArticleIndex: FC = () => { | ||||||
|   const { data } = useQuery<{ |   const { data } = useQuery<{ | ||||||
| @@ -24,7 +24,7 @@ export const ArticleIndex: FC = () => { | |||||||
|  |  | ||||||
|   const [removeArticle] = useMutation<any, { id: string }>(REMOVE_ARTICLE); |   const [removeArticle] = useMutation<any, { id: string }>(REMOVE_ARTICLE); | ||||||
|  |  | ||||||
|   const router = useRouter(); |   const navigate = useNavigate(); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <section> |     <section> | ||||||
| @@ -57,15 +57,9 @@ export const ArticleIndex: FC = () => { | |||||||
|                 <TableCell> |                 <TableCell> | ||||||
|                   <IconButton |                   <IconButton | ||||||
|                     aria-label="edit" |                     aria-label="edit" | ||||||
|                     onClick={() => |                     onClick={() => navigate(`/articles/${article.id}`)} | ||||||
|                       router.navigate({ |                     size="large" | ||||||
|                         url: router.url({ |                   > | ||||||
|                           name: "modify-article", |  | ||||||
|                           params: article, |  | ||||||
|                         }), |  | ||||||
|                       }) |  | ||||||
|                     } |  | ||||||
|                     size="large"> |  | ||||||
|                     <EditIcon /> |                     <EditIcon /> | ||||||
|                   </IconButton> |                   </IconButton> | ||||||
|                   <IconButton |                   <IconButton | ||||||
| @@ -75,7 +69,8 @@ export const ArticleIndex: FC = () => { | |||||||
|                         variables: article, |                         variables: article, | ||||||
|                       }) |                       }) | ||||||
|                     } |                     } | ||||||
|                     size="large"> |                     size="large" | ||||||
|  |                   > | ||||||
|                     <Delete /> |                     <Delete /> | ||||||
|                   </IconButton> |                   </IconButton> | ||||||
|                 </TableCell> |                 </TableCell> | ||||||
|   | |||||||
| @@ -1,20 +0,0 @@ | |||||||
| import { ActiveHookProps, Link, LinkProps, useActive } from '@curi/react-dom'; |  | ||||||
| import React, { FC, ReactNode } from 'react'; |  | ||||||
|  |  | ||||||
| export type ActiveLinkProps = ActiveHookProps & |  | ||||||
|   LinkProps & { |  | ||||||
|     className?: string; |  | ||||||
|     children: ReactNode; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
| export const ActiveLink:FC<ActiveLinkProps> = ({ name, params, partial, className = "", ...rest }) => { |  | ||||||
|   const active = useActive({ name, params, partial }); |  | ||||||
|   return ( |  | ||||||
|     <Link |  | ||||||
|       name={name} |  | ||||||
|       params={params} |  | ||||||
|       {...rest} |  | ||||||
|       className={active ? `${className} active` : className} |  | ||||||
|     /> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| @@ -1,31 +0,0 @@ | |||||||
| import { useApolloClient } from "@apollo/client"; |  | ||||||
| import { createRouterComponent } from "@curi/react-dom"; |  | ||||||
| import { createRouter, announce } from "@curi/router"; |  | ||||||
| import { browser } from "@hickory/browser"; |  | ||||||
| import { FC, useEffect, useState } from "react"; |  | ||||||
| import routes from "../../routes"; |  | ||||||
| import { LinearProgress } from "@mui/material"; |  | ||||||
|  |  | ||||||
| const Component: FC = ({ children }) => { |  | ||||||
|   const client = useApolloClient(); |  | ||||||
|   const [body, setBody] = useState<any>(null); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |  | ||||||
|     const router = createRouter(browser, routes, { |  | ||||||
|       sideEffects: [ |  | ||||||
|         announce(({ response }) => { |  | ||||||
|           return `Navigated to ${response.location.pathname}`; |  | ||||||
|         }), |  | ||||||
|       ], |  | ||||||
|       external: { client }, |  | ||||||
|     }); |  | ||||||
|     const Router = createRouterComponent(router); |  | ||||||
|     router.once(() => { |  | ||||||
|       setBody(<Router>{children}</Router>); |  | ||||||
|     }); |  | ||||||
|   }, [setBody, client, children]); |  | ||||||
|  |  | ||||||
|   return body ?? <LinearProgress />; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default Component; |  | ||||||
| @@ -9,7 +9,6 @@ import { MuiPickersUtilsProvider } from "@material-ui/pickers"; | |||||||
| import DateFnsUtils from "@date-io/date-fns"; | import DateFnsUtils from "@date-io/date-fns"; | ||||||
| import zhLocale from "date-fns/locale/zh-CN"; | import zhLocale from "date-fns/locale/zh-CN"; | ||||||
| import { SnackbarProvider } from "notistack"; | import { SnackbarProvider } from "notistack"; | ||||||
| import Router from "./commons/route/router"; |  | ||||||
| import { AuthProvider } from "./commons/auth/auth.provider"; | import { AuthProvider } from "./commons/auth/auth.provider"; | ||||||
| import { | import { | ||||||
|   ThemeProvider, |   ThemeProvider, | ||||||
| @@ -18,8 +17,6 @@ import { | |||||||
|   createTheme, |   createTheme, | ||||||
| } from "@mui/material/styles"; | } from "@mui/material/styles"; | ||||||
|  |  | ||||||
| import makeStyles from "@mui/styles/makeStyles"; |  | ||||||
|  |  | ||||||
| declare module "@mui/styles/defaultTheme" { | declare module "@mui/styles/defaultTheme" { | ||||||
|   // eslint-disable-next-line @typescript-eslint/no-empty-interface |   // eslint-disable-next-line @typescript-eslint/no-empty-interface | ||||||
|   interface DefaultTheme extends Theme {} |   interface DefaultTheme extends Theme {} | ||||||
| @@ -27,10 +24,6 @@ declare module "@mui/styles/defaultTheme" { | |||||||
|  |  | ||||||
| const theme = createTheme(); | const theme = createTheme(); | ||||||
|  |  | ||||||
| const useStyles = makeStyles((theme) => ({ |  | ||||||
|   root: {}, |  | ||||||
| })); |  | ||||||
|  |  | ||||||
| ReactDOM.render( | ReactDOM.render( | ||||||
|   <React.StrictMode> |   <React.StrictMode> | ||||||
|     <StyledEngineProvider injectFirst> |     <StyledEngineProvider injectFirst> | ||||||
| @@ -39,9 +32,7 @@ ReactDOM.render( | |||||||
|           <AuthProvider> |           <AuthProvider> | ||||||
|             <AppApolloClientProvider> |             <AppApolloClientProvider> | ||||||
|               <MuiPickersUtilsProvider utils={DateFnsUtils} locale={zhLocale}> |               <MuiPickersUtilsProvider utils={DateFnsUtils} locale={zhLocale}> | ||||||
|                 <Router> |                 <App /> | ||||||
|                   <App /> |  | ||||||
|                 </Router> |  | ||||||
|               </MuiPickersUtilsProvider> |               </MuiPickersUtilsProvider> | ||||||
|             </AppApolloClientProvider> |             </AppApolloClientProvider> | ||||||
|           </AuthProvider> |           </AuthProvider> | ||||||
|   | |||||||
| @@ -1,10 +1,7 @@ | |||||||
| import React, { FC } from "react"; | import React, { FC, Suspense } from "react"; | ||||||
| import clsx from "clsx"; | import { useTheme, Theme, CSSObject, styled } from "@mui/material/styles"; | ||||||
| import { useTheme, Theme } from "@mui/material/styles"; |  | ||||||
| import createStyles from '@mui/styles/createStyles'; |  | ||||||
| import makeStyles from '@mui/styles/makeStyles'; |  | ||||||
| import Drawer from "@mui/material/Drawer"; | import Drawer from "@mui/material/Drawer"; | ||||||
| import AppBar from "@mui/material/AppBar"; | import AppBar, { AppBarProps } from "@mui/material/AppBar"; | ||||||
| import Toolbar from "@mui/material/Toolbar"; | import Toolbar from "@mui/material/Toolbar"; | ||||||
| import List from "@mui/material/List"; | import List from "@mui/material/List"; | ||||||
| import CssBaseline from "@mui/material/CssBaseline"; | import CssBaseline from "@mui/material/CssBaseline"; | ||||||
| @@ -17,76 +14,83 @@ import ChevronRightIcon from "@mui/icons-material/ChevronRight"; | |||||||
| import ListItem from "@mui/material/ListItem"; | import ListItem from "@mui/material/ListItem"; | ||||||
| import ListItemIcon from "@mui/material/ListItemIcon"; | import ListItemIcon from "@mui/material/ListItemIcon"; | ||||||
| import ListItemText from "@mui/material/ListItemText"; | import ListItemText from "@mui/material/ListItemText"; | ||||||
| import { AddCircle, Description, LocalOffer } from '@mui/icons-material'; | import { AddCircle, Description, LocalOffer } from "@mui/icons-material"; | ||||||
| import { Link } from '@curi/react-dom'; | import { Box } from "@mui/system"; | ||||||
|  | import { Link } from "react-router-dom"; | ||||||
| const drawerWidth = 240; | const drawerWidth = 240; | ||||||
|  |  | ||||||
| const useStyles = makeStyles((theme: Theme) => | const openedMixin = (theme: Theme): CSSObject => ({ | ||||||
|   createStyles({ |   width: drawerWidth, | ||||||
|     root: { |   transition: theme.transitions.create("width", { | ||||||
|       display: "flex", |     easing: theme.transitions.easing.sharp, | ||||||
|     }, |     duration: theme.transitions.duration.enteringScreen, | ||||||
|     appBar: { |   }), | ||||||
|       zIndex: theme.zIndex.drawer + 1, |   overflowX: "hidden", | ||||||
|       transition: theme.transitions.create(["width", "margin"], { | }); | ||||||
|         easing: theme.transitions.easing.sharp, |  | ||||||
|         duration: theme.transitions.duration.leavingScreen, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|     appBarShift: { |  | ||||||
|       marginLeft: drawerWidth, |  | ||||||
|       width: `calc(100% - ${drawerWidth}px)`, |  | ||||||
|       transition: theme.transitions.create(["width", "margin"], { |  | ||||||
|         easing: theme.transitions.easing.sharp, |  | ||||||
|         duration: theme.transitions.duration.enteringScreen, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|     menuButton: { |  | ||||||
|       marginRight: 36, |  | ||||||
|     }, |  | ||||||
|     hide: { |  | ||||||
|       display: "none", |  | ||||||
|     }, |  | ||||||
|     drawer: { |  | ||||||
|       width: drawerWidth, |  | ||||||
|       flexShrink: 0, |  | ||||||
|       whiteSpace: "nowrap", |  | ||||||
|     }, |  | ||||||
|     drawerOpen: { |  | ||||||
|       width: drawerWidth, |  | ||||||
|       transition: theme.transitions.create("width", { |  | ||||||
|         easing: theme.transitions.easing.sharp, |  | ||||||
|         duration: theme.transitions.duration.enteringScreen, |  | ||||||
|       }), |  | ||||||
|     }, |  | ||||||
|     drawerClose: { |  | ||||||
|       transition: theme.transitions.create("width", { |  | ||||||
|         easing: theme.transitions.easing.sharp, |  | ||||||
|         duration: theme.transitions.duration.leavingScreen, |  | ||||||
|       }), |  | ||||||
|       overflowX: "hidden", |  | ||||||
|       width: theme.spacing(7) + 1, |  | ||||||
|       [theme.breakpoints.up("sm")]: { |  | ||||||
|         width: theme.spacing(9) + 1, |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|     toolbar: { |  | ||||||
|       display: "flex", |  | ||||||
|       alignItems: "center", |  | ||||||
|       justifyContent: "flex-end", |  | ||||||
|       padding: theme.spacing(0, 1), |  | ||||||
|       // necessary for content to be below app bar |  | ||||||
|       ...theme.mixins.toolbar, |  | ||||||
|     }, |  | ||||||
|     content: { |  | ||||||
|       flexGrow: 1, |  | ||||||
|       padding: theme.spacing(3), |  | ||||||
|     }, |  | ||||||
|   }) |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| export const DefaultLayout: FC = ({children}) => { | const closedMixin = (theme: Theme): CSSObject => ({ | ||||||
|   const classes = useStyles(); |   transition: theme.transitions.create("width", { | ||||||
|  |     easing: theme.transitions.easing.sharp, | ||||||
|  |     duration: theme.transitions.duration.leavingScreen, | ||||||
|  |   }), | ||||||
|  |   overflowX: "hidden", | ||||||
|  |   width: `calc(${theme.spacing(7)} + 1px)`, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const DrawerHeader = styled("div")(({ theme }) => ({ | ||||||
|  |   display: "flex", | ||||||
|  |   alignItems: "center", | ||||||
|  |   justifyContent: "flex-end", | ||||||
|  |   padding: theme.spacing(0, 1), | ||||||
|  |   // necessary for content to be below app bar | ||||||
|  |   ...theme.mixins.toolbar, | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | const Main = styled("main")(({ theme }) => ({ | ||||||
|  |   padding: theme.spacing(2, 2), | ||||||
|  |   [theme.breakpoints.up("md")]: { | ||||||
|  |     padding: theme.spacing(4, 3), | ||||||
|  |   }, | ||||||
|  |   width: "100%", | ||||||
|  |   marginTop: theme.mixins.toolbar.minHeight, | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | const StyledAppBar = styled(AppBar, { | ||||||
|  |   shouldForwardProp: (prop) => prop !== "open", | ||||||
|  | })<AppBarProps & { open?: boolean }>(({ theme, open }) => ({ | ||||||
|  |   zIndex: theme.zIndex.drawer + 1, | ||||||
|  |   transition: theme.transitions.create(["width", "margin"], { | ||||||
|  |     easing: theme.transitions.easing.sharp, | ||||||
|  |     duration: theme.transitions.duration.leavingScreen, | ||||||
|  |   }), | ||||||
|  |   ...(open && { | ||||||
|  |     marginLeft: drawerWidth, | ||||||
|  |     width: `calc(100% - ${drawerWidth}px)`, | ||||||
|  |     transition: theme.transitions.create(["width", "margin"], { | ||||||
|  |       easing: theme.transitions.easing.sharp, | ||||||
|  |       duration: theme.transitions.duration.enteringScreen, | ||||||
|  |     }), | ||||||
|  |   }), | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | const StyledDrawer = styled(Drawer, { | ||||||
|  |   shouldForwardProp: (prop) => prop !== "open", | ||||||
|  | })(({ theme, open }) => ({ | ||||||
|  |   width: drawerWidth, | ||||||
|  |   flexShrink: 0, | ||||||
|  |   whiteSpace: "nowrap", | ||||||
|  |   boxSizing: "border-box", | ||||||
|  |   ...(open && { | ||||||
|  |     ...openedMixin(theme), | ||||||
|  |     "& .MuiDrawer-paper": openedMixin(theme), | ||||||
|  |   }), | ||||||
|  |   ...(!open && { | ||||||
|  |     ...closedMixin(theme), | ||||||
|  |     "& .MuiDrawer-paper": closedMixin(theme), | ||||||
|  |   }), | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | export const DefaultLayout: FC = ({ children }) => { | ||||||
|   const theme = useTheme(); |   const theme = useTheme(); | ||||||
|   const [open, setOpen] = React.useState(false); |   const [open, setOpen] = React.useState(false); | ||||||
|  |  | ||||||
| @@ -99,86 +103,70 @@ export const DefaultLayout: FC = ({children}) => { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className={classes.root}> |     <Box sx={{ display: "flex" }}> | ||||||
|         <CssBaseline /> |       <CssBaseline /> | ||||||
|         <AppBar |       <StyledAppBar position="fixed" open={open}> | ||||||
|           position="fixed" |         <Toolbar> | ||||||
|           className={clsx(classes.appBar, { |           <IconButton | ||||||
|             [classes.appBarShift]: open, |             color="inherit" | ||||||
|           })} |             aria-label="open drawer" | ||||||
|         > |             onClick={handleDrawerOpen} | ||||||
|           <Toolbar> |             edge="start" | ||||||
|             <IconButton |             sx={{ | ||||||
|               color="inherit" |               marginRight: "36px", | ||||||
|               aria-label="open drawer" |               ...(open && { display: "none" }), | ||||||
|               onClick={handleDrawerOpen} |             }} | ||||||
|               edge="start" |             size="large" | ||||||
|               className={clsx(classes.menuButton, { |           > | ||||||
|                 [classes.hide]: open, |             <MenuIcon /> | ||||||
|               })} |           </IconButton> | ||||||
|               size="large"> |           <Typography variant="h6" noWrap> | ||||||
|               <MenuIcon /> |             Mini variant drawer | ||||||
|             </IconButton> |           </Typography> | ||||||
|             <Typography variant="h6" noWrap> |         </Toolbar> | ||||||
|               Mini variant drawer |       </StyledAppBar> | ||||||
|             </Typography> |       <StyledDrawer variant="permanent" open={open}> | ||||||
|           </Toolbar> |         <DrawerHeader> | ||||||
|         </AppBar> |           <IconButton onClick={handleDrawerClose} size="large"> | ||||||
|         <Drawer |             {theme.direction === "rtl" ? ( | ||||||
|           variant="permanent" |               <ChevronRightIcon /> | ||||||
|           className={clsx(classes.drawer, { |             ) : ( | ||||||
|             [classes.drawerOpen]: open, |               <ChevronLeftIcon /> | ||||||
|             [classes.drawerClose]: !open, |             )} | ||||||
|           })} |           </IconButton> | ||||||
|           classes={{ |         </DrawerHeader> | ||||||
|             paper: clsx({ |         <Divider /> | ||||||
|               [classes.drawerOpen]: open, |         <List> | ||||||
|               [classes.drawerClose]: !open, |           <Link to="/articles/create"> | ||||||
|             }), |             <ListItem button> | ||||||
|           }} |               <ListItemIcon> | ||||||
|         > |                 <AddCircle /> | ||||||
|           <div className={classes.toolbar}> |               </ListItemIcon> | ||||||
|             <IconButton onClick={handleDrawerClose} size="large"> |               <ListItemText primary="New Article" /> | ||||||
|               {theme.direction === "rtl" ? ( |             </ListItem> | ||||||
|                 <ChevronRightIcon /> |           </Link> | ||||||
|               ) : ( |           <Link to="/articles"> | ||||||
|                 <ChevronLeftIcon /> |             <ListItem button> | ||||||
|               )} |               <ListItemIcon> | ||||||
|             </IconButton> |                 <Description /> | ||||||
|           </div> |               </ListItemIcon> | ||||||
|           <Divider /> |               <ListItemText primary="Articles" /> | ||||||
|           <List> |             </ListItem> | ||||||
|             <Link name="create-article"> |           </Link> | ||||||
|               <ListItem button> |           <Link to="/tags"> | ||||||
|                 <ListItemIcon> |             <ListItem button> | ||||||
|                   <AddCircle /> |               <ListItemIcon> | ||||||
|                 </ListItemIcon> |                 <LocalOffer /> | ||||||
|                 <ListItemText primary="New Article" /> |               </ListItemIcon> | ||||||
|               </ListItem> |               <ListItemText primary="Tags" /> | ||||||
|             </Link> |             </ListItem> | ||||||
|             <Link name="articles"> |           </Link> | ||||||
|               <ListItem button> |         </List> | ||||||
|                 <ListItemIcon> |         <Divider /> | ||||||
|                   <Description /> |       </StyledDrawer> | ||||||
|                 </ListItemIcon> |       <Main> | ||||||
|                 <ListItemText primary="Articles" /> |         <Suspense fallback={"Loading"}>{children}</Suspense> | ||||||
|               </ListItem> |       </Main> | ||||||
|             </Link> |     </Box> | ||||||
|             <Link name="tags"> |  | ||||||
|               <ListItem button> |  | ||||||
|                 <ListItemIcon> |  | ||||||
|                   <LocalOffer /> |  | ||||||
|                 </ListItemIcon> |  | ||||||
|                 <ListItemText primary="Tags" /> |  | ||||||
|               </ListItem> |  | ||||||
|             </Link> |  | ||||||
|           </List> |  | ||||||
|           <Divider /> |  | ||||||
|         </Drawer> |  | ||||||
|         <main className={classes.content}> |  | ||||||
|           <div className={classes.toolbar} /> |  | ||||||
|           { children } |  | ||||||
|         </main> |  | ||||||
|     </div> |  | ||||||
|   ); |   ); | ||||||
| } | }; | ||||||
|   | |||||||
							
								
								
									
										156
									
								
								src/routes.tsx
									
									
									
									
									
								
							
							
						
						
									
										156
									
								
								src/routes.tsx
									
									
									
									
									
								
							| @@ -1,74 +1,96 @@ | |||||||
| import { ApolloClient } from "@apollo/client"; | import { useApolloClient } from "@apollo/client"; | ||||||
| import { prepareRoutes } from "@curi/router"; | import * as R from "ramda"; | ||||||
| import { omit } from 'ramda'; | import { FC, lazy, useMemo } from "react"; | ||||||
|  | import { useParams, useRoutes } from "react-router-dom"; | ||||||
| import { ARTICLE } from "./articles"; | import { ARTICLE } from "./articles"; | ||||||
| import { Article } from './generated/graphql'; | import { Article } from "./generated/graphql"; | ||||||
|  | import { DefaultLayout } from "./layouts"; | ||||||
|  |  | ||||||
| export default prepareRoutes([ | const CreateArticle = lazy(() => | ||||||
|   { |   import( | ||||||
|     name: "dashboard", |     /* webpackChunkName: "article-editor" */ "./articles/article-editor" | ||||||
|     path: "", |   ).then((m) => ({ | ||||||
|     respond() { |     default: m.ArticleEditor, | ||||||
|       return { body: () => <div>DashBoard</div> }; |   })) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | const ModifyArticle: FC = () => { | ||||||
|  |   const { id } = useParams(); | ||||||
|  |   const client = useApolloClient(); | ||||||
|  |   const Lazy = useMemo( | ||||||
|  |     () => | ||||||
|  |       lazy(async () => { | ||||||
|  |         const [{ ArticleEditor }, article] = await Promise.all([ | ||||||
|  |           import( | ||||||
|  |             /* webpackChunkName: "article-editor" */ "./articles/article-editor" | ||||||
|  |           ), | ||||||
|  |           client | ||||||
|  |             .query<{ article: Article }, { id: string }>({ | ||||||
|  |               query: ARTICLE, | ||||||
|  |               variables: { id: id! }, | ||||||
|  |             }) | ||||||
|  |             .then(({ data }) => { | ||||||
|  |               return R.omit(["__typename"], data.article); | ||||||
|  |             }), | ||||||
|  |         ]); | ||||||
|  |         return { default: () => <ArticleEditor article={article} /> }; | ||||||
|  |       }), | ||||||
|  |     [id, client] | ||||||
|  |   ); | ||||||
|  |   return <Lazy />; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const ArticleIndex = lazy(() => | ||||||
|  |   import(/* webpackChunkName: "articles" */ "./articles").then((m) => ({ | ||||||
|  |     default: m.ArticleIndex, | ||||||
|  |   })) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | const TagIndex = lazy(() => | ||||||
|  |   import(/* webpackChunkName: "tags" */ "./tags").then((m) => ({ | ||||||
|  |     default: m.TagsIndex, | ||||||
|  |   })) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | export const AppRoutes: FC = () => { | ||||||
|  |   const element = useRoutes([ | ||||||
|  |     { | ||||||
|  |       index: true, | ||||||
|  |       element: <div>DashBoard</div>, | ||||||
|     }, |     }, | ||||||
|   }, |     { | ||||||
|   { |       path: "/articles/create", | ||||||
|     name: "create-article", |       element: ( | ||||||
|     path: "articles/create", |         <DefaultLayout> | ||||||
|     resolve() { |           <CreateArticle /> | ||||||
|       const body = import( |         </DefaultLayout> | ||||||
|         /* webpackChunkName: "article-editor" */ "./articles" |       ), | ||||||
|       ).then((m) => m.ArticleEditor); |  | ||||||
|       return body; |  | ||||||
|     }, |     }, | ||||||
|     respond({ resolved }) { |     { | ||||||
|       return { body: resolved }; |       path: "/articles/:id", | ||||||
|  |       element: ( | ||||||
|  |         <DefaultLayout> | ||||||
|  |           <ModifyArticle /> | ||||||
|  |         </DefaultLayout> | ||||||
|  |       ), | ||||||
|     }, |     }, | ||||||
|   }, |     { | ||||||
|   { |       path: "articles", | ||||||
|     name: "modify-article", |       element: ( | ||||||
|     path: "articles/:id", |         <DefaultLayout> | ||||||
|     async resolve(matched, { client }: { client: ApolloClient<any> }) { |           <ArticleIndex /> | ||||||
|       const [ArticleEditor, result] = await Promise.all([ |         </DefaultLayout> | ||||||
|         import(/* webpackChunkName: "article-editor" */ "./articles").then( |       ), | ||||||
|           (m) => m.ArticleEditor |  | ||||||
|         ), |  | ||||||
|         client.query<{ article: Article }, { id: string }>({ |  | ||||||
|           query: ARTICLE, |  | ||||||
|           variables: { id: matched!.params.id }, |  | ||||||
|         }), |  | ||||||
|       ]); |  | ||||||
|       console.log(ArticleEditor, result); |  | ||||||
|       return () => ( |  | ||||||
|         <ArticleEditor article={omit(["__typename"], result.data.article)} /> |  | ||||||
|       ); |  | ||||||
|     }, |     }, | ||||||
|     respond({ resolved }) { |     { | ||||||
|       return { body: resolved }; |       path: "tags", | ||||||
|  |       element: ( | ||||||
|  |         <DefaultLayout> | ||||||
|  |           <TagIndex /> | ||||||
|  |         </DefaultLayout> | ||||||
|  |       ), | ||||||
|     }, |     }, | ||||||
|   }, |   ]); | ||||||
|   { |  | ||||||
|     name: "articles", |   return element; | ||||||
|     path: "articles", | }; | ||||||
|     resolve() { |  | ||||||
|       return import(/* webpackChunkName: "articles" */ "./articles").then( |  | ||||||
|         (m) => m.ArticleIndex |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     respond({ resolved }) { |  | ||||||
|       return { body: resolved }; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: "tags", |  | ||||||
|     path: "tags", |  | ||||||
|     resolve() { |  | ||||||
|       return import(/* webpackChunkName: "tags" */ "./tags").then( |  | ||||||
|         (m) => m.TagsIndex |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|     respond({ resolved }) { |  | ||||||
|       return { body: resolved }; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| ]); |  | ||||||
|   | |||||||
| @@ -1,197 +0,0 @@ | |||||||
| import { Button, Grid, Paper } from "@mui/material"; |  | ||||||
| import createStyles from '@mui/styles/createStyles'; |  | ||||||
| import makeStyles from '@mui/styles/makeStyles'; |  | ||||||
| import { Field, Form, Formik, FormikHelpers } from "formik"; |  | ||||||
| import { FC } from "react"; |  | ||||||
| import { Editor } from "../commons/editor/vditor"; |  | ||||||
| import * as Yup from "yup"; |  | ||||||
| import { Article, CreateArticleInput, UpdateArticleInput } from "../generated/graphql"; |  | ||||||
| import { DateTimePicker } from "formik-material-ui-pickers"; |  | ||||||
| import { gql, useMutation } from "@apollo/client"; |  | ||||||
| import { useRouter } from "@curi/react-universal"; |  | ||||||
|  |  | ||||||
| const CREATE_ARTICLE = gql` |  | ||||||
|   mutation createArticle($createArticleInput: CreateArticleInput!) { |  | ||||||
|     createArticle(createArticleInput: $createArticleInput) { |  | ||||||
|       id |  | ||||||
|       title |  | ||||||
|       content |  | ||||||
|       publishedAt |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| `; |  | ||||||
|  |  | ||||||
| const UPDATE_ARTICLE = gql` |  | ||||||
|   mutation updateArticle($updateArticleInput: UpdateArticleInput!) { |  | ||||||
|     updateArticle(updateArticleInput: $updateArticleInput) { |  | ||||||
|       id |  | ||||||
|       title |  | ||||||
|       content |  | ||||||
|       publishedAt |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| `; |  | ||||||
|  |  | ||||||
| const useStyles = makeStyles((theme) => |  | ||||||
|   createStyles({ |  | ||||||
|     form: { |  | ||||||
|       padding: "15px 30px", |  | ||||||
|     }, |  | ||||||
|     editor: { |  | ||||||
|       height: "700px", |  | ||||||
|     }, |  | ||||||
|   }) |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| type Values = CreateArticleInput | UpdateArticleInput; |  | ||||||
| interface Props { |  | ||||||
|   article?: Values; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const ArticleEditor: FC<Props> = ({ article }) => { |  | ||||||
|   const classes = useStyles(); |  | ||||||
|  |  | ||||||
|   const validationSchema = Yup.object({ |  | ||||||
|     content: Yup.string() |  | ||||||
|       .max(65535, "文章内容不得超过 65535 个字符") |  | ||||||
|       .required("文章内容不得为空"), |  | ||||||
|     publishedAt: Yup.date(), |  | ||||||
|   }); |  | ||||||
|   const router = useRouter(); |  | ||||||
|  |  | ||||||
|   const [createArticle] = useMutation<{createArticle: Article} ,{ |  | ||||||
|     createArticleInput: CreateArticleInput; |  | ||||||
|   }>(CREATE_ARTICLE, { |  | ||||||
|     update(cache, { data }) { |  | ||||||
|       cache.modify({ |  | ||||||
|         fields: { |  | ||||||
|           articles(existingArticles = []) { |  | ||||||
|             const newArticleRef = cache.writeFragment({ |  | ||||||
|               data: data!.createArticle, |  | ||||||
|               fragment: gql` |  | ||||||
|                 fragment NewArticle on Article { |  | ||||||
|                   id |  | ||||||
|                   title |  | ||||||
|                   content |  | ||||||
|                   publishedAt |  | ||||||
|                 } |  | ||||||
|               `, |  | ||||||
|             }); |  | ||||||
|             return [newArticleRef, ...existingArticles]; |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }); |  | ||||||
|     }, |  | ||||||
|   }); |  | ||||||
|   const [updateArticle] = useMutation<{ |  | ||||||
|     updateArticleInput: UpdateArticleInput; |  | ||||||
|   }>(UPDATE_ARTICLE); |  | ||||||
|  |  | ||||||
|   const SubmitForm = ( |  | ||||||
|     values: Values, |  | ||||||
|     { setSubmitting }: FormikHelpers<Values> |  | ||||||
|   ) => { |  | ||||||
|     if ("id" in article!) { |  | ||||||
|       const title = /# (.+)$/m.exec(values.content ?? "")?.[1]; |  | ||||||
|       if (!title) { |  | ||||||
|         console.log("no title"); |  | ||||||
|         setSubmitting(false); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       updateArticle({ |  | ||||||
|         variables: { |  | ||||||
|           updateArticleInput: { |  | ||||||
|             ...values, |  | ||||||
|             title, |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }) |  | ||||||
|         .then(() => { |  | ||||||
|           router.navigate({ |  | ||||||
|             url: router.url({ name: "articles" }), |  | ||||||
|             method: "replace", |  | ||||||
|           }); |  | ||||||
|         }) |  | ||||||
|         .finally(() => { |  | ||||||
|           setSubmitting(false); |  | ||||||
|         }); |  | ||||||
|     } else { |  | ||||||
|       const title = /# (.+)$/m.exec(values.content ?? "")?.[1]; |  | ||||||
|       if (!title) { |  | ||||||
|         console.log("no title"); |  | ||||||
|         setSubmitting(false); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       createArticle({ |  | ||||||
|         variables: { |  | ||||||
|           createArticleInput: { |  | ||||||
|             ...values as CreateArticleInput, |  | ||||||
|             title, |  | ||||||
|           }, |  | ||||||
|         }, |  | ||||||
|       }) |  | ||||||
|         .then(() => { |  | ||||||
|           router.navigate({ |  | ||||||
|             url: router.url({ name: "articles" }), |  | ||||||
|             method: "replace", |  | ||||||
|           }); |  | ||||||
|         }) |  | ||||||
|         .finally(() => { |  | ||||||
|           setSubmitting(false); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|     setSubmitting(false); |  | ||||||
|   }; |  | ||||||
|   return ( |  | ||||||
|     <Paper> |  | ||||||
|       <Formik |  | ||||||
|         initialValues={article!} |  | ||||||
|         validationSchema={validationSchema} |  | ||||||
|         autoComplete="off" |  | ||||||
|         onSubmit={SubmitForm} |  | ||||||
|       > |  | ||||||
|         {({ submitForm, isSubmitting }) => ( |  | ||||||
|           <Form className={classes.form}> |  | ||||||
|             <Grid container spacing={3}> |  | ||||||
|               <Grid item xs={12}> |  | ||||||
|                 <Editor |  | ||||||
|                   className={classes.editor} |  | ||||||
|                   name="content" |  | ||||||
|                   label="Content" |  | ||||||
|                 /> |  | ||||||
|               </Grid> |  | ||||||
|               <Grid item xs={12}> |  | ||||||
|                 <Field |  | ||||||
|                   ampm={false} |  | ||||||
|                   component={DateTimePicker} |  | ||||||
|                   label="Published At" |  | ||||||
|                   name="publishedAt" |  | ||||||
|                   format="yyyy-MM-dd HH:mm:ss" |  | ||||||
|                 /> |  | ||||||
|               </Grid> |  | ||||||
|               <Grid item xs={12}> |  | ||||||
|                 <Button |  | ||||||
|                   variant="contained" |  | ||||||
|                   color="primary" |  | ||||||
|                   disabled={isSubmitting} |  | ||||||
|                   onClick={submitForm} |  | ||||||
|                 > |  | ||||||
|                   Submit |  | ||||||
|                 </Button> |  | ||||||
|               </Grid> |  | ||||||
|             </Grid> |  | ||||||
|           </Form> |  | ||||||
|         )} |  | ||||||
|       </Formik> |  | ||||||
|     </Paper> |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| ArticleEditor.defaultProps = { |  | ||||||
|   article: { |  | ||||||
|     title: "", |  | ||||||
|     content: "", |  | ||||||
|     publishedAt: new Date(), |  | ||||||
|     tags: [], |  | ||||||
|   } as CreateArticleInput, |  | ||||||
| }; |  | ||||||
| @@ -1,3 +1,2 @@ | |||||||
| export * from "./article-editor"; |  | ||||||
| export * from "./tags-index"; | export * from "./tags-index"; | ||||||
| export * from "./tags.constants"; | export * from "./tags.constants"; | ||||||
|   | |||||||
| @@ -12,9 +12,9 @@ import { | |||||||
| import { FC } from "react"; | import { FC } from "react"; | ||||||
| import { Tag } from "../generated/graphql"; | import { Tag } from "../generated/graphql"; | ||||||
| import EditIcon from "@mui/icons-material/Edit"; | import EditIcon from "@mui/icons-material/Edit"; | ||||||
| import { useRouter } from "@curi/react-dom"; |  | ||||||
| import { Delete } from "@mui/icons-material"; | import { Delete } from "@mui/icons-material"; | ||||||
| import { REMOVE_TAG, TAGS } from "./tags.constants"; | import { REMOVE_TAG, TAGS } from "./tags.constants"; | ||||||
|  | import { useNavigate } from "react-router-dom"; | ||||||
|  |  | ||||||
| export const TagsIndex: FC = () => { | export const TagsIndex: FC = () => { | ||||||
|   const { data } = useQuery<{ |   const { data } = useQuery<{ | ||||||
| @@ -23,7 +23,7 @@ export const TagsIndex: FC = () => { | |||||||
|  |  | ||||||
|   const [removeArticle] = useMutation<any, { id: string }>(REMOVE_TAG); |   const [removeArticle] = useMutation<any, { id: string }>(REMOVE_TAG); | ||||||
|  |  | ||||||
|   const router = useRouter(); |   const navigate = useNavigate(); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <section> |     <section> | ||||||
| @@ -48,15 +48,9 @@ export const TagsIndex: FC = () => { | |||||||
|                 <TableCell> |                 <TableCell> | ||||||
|                   <IconButton |                   <IconButton | ||||||
|                     aria-label="edit" |                     aria-label="edit" | ||||||
|                     onClick={() => |                     onClick={() => navigate(`/tags/${tag.id}/modify`)} | ||||||
|                       router.navigate({ |                     size="large" | ||||||
|                         url: router.url({ |                   > | ||||||
|                           name: "modify-tag", |  | ||||||
|                           params: tag, |  | ||||||
|                         }), |  | ||||||
|                       }) |  | ||||||
|                     } |  | ||||||
|                     size="large"> |  | ||||||
|                     <EditIcon /> |                     <EditIcon /> | ||||||
|                   </IconButton> |                   </IconButton> | ||||||
|                   <IconButton |                   <IconButton | ||||||
| @@ -66,7 +60,8 @@ export const TagsIndex: FC = () => { | |||||||
|                         variables: tag, |                         variables: tag, | ||||||
|                       }) |                       }) | ||||||
|                     } |                     } | ||||||
|                     size="large"> |                     size="large" | ||||||
|  |                   > | ||||||
|                     <Delete /> |                     <Delete /> | ||||||
|                   </IconButton> |                   </IconButton> | ||||||
|                 </TableCell> |                 </TableCell> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user