add editor article page

pull/49/head
sunface 4 years ago
parent 3f7f70db3d
commit 672680e117

@ -4,10 +4,11 @@ common:
log_level: "info"
is_prod: false
#################################### Server ##############################
#################################### Server ##############################
server:
addr: ":6001"
base_url: "/api"
#################################### User/Session ##############################
user:
# github username
@ -22,3 +23,7 @@ paths:
# sqlite3 db files
data: ""
logs: ""
#################################### Posts ##############################
posts:
brief_max_len: 100

@ -3,6 +3,7 @@ module github.com/imdotdev/im.dev
go 1.14
require (
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/gin-gonic/gin v1.6.3
github.com/go-stack/stack v1.8.0
github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac

@ -19,6 +19,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=

@ -29,9 +29,9 @@ import siteConfig from "configs/site-config"
import { useViewportScroll } from "framer-motion"
import NextLink from "next/link"
import React from "react"
import { FaMoon, FaSun, FaUserAlt, FaRegSun, FaSignOutAlt, FaRegBookmark, FaChartBar, FaHome, FaArrowRight, FaGithub } from "react-icons/fa"
import { FaMoon, FaSun, FaUserAlt, FaRegSun, FaSignOutAlt, FaRegBookmark, FaChartBar, FaHome, FaArrowRight, FaGithub, FaFileAlt, FaBookmark, FaEdit } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo"
import { MobileNavButton, MobileNavContent } from "./mobile-nav"
import { MobileNavButton, MobileNavContent } from "../mobile-nav"
import AlgoliaSearch from "src/components/search/algolia-search"
import useSession from "hooks/use-session"
import { requestApi } from "utils/axios/request"
@ -39,6 +39,9 @@ import { removeToken, saveToken } from "utils/axios/getToken"
import { Session } from "src/types/session"
import navLinks from "./nav-links"
import { useRouter } from "next/router"
import events from "utils/events"
import storage from "utils/localStorage"
import { logout } from "utils/session"
const DiscordIcon = (props) => (
@ -62,12 +65,12 @@ const GithubIcon = (props) => (
function HeaderContent() {
const { pathname } = useRouter()
const router = useRouter()
const {pathname} = router
const mobileNav = useDisclosure()
const [session, storeSession]: [Session, any] = useSession()
const { isOpen: isLoginOpen, onOpen: onLoginOpen, onClose: onLoginClose } = useDisclosure()
const session:Session = useSession()
const { toggleColorMode: toggleMode } = useColorMode()
const text = useColorModeValue("dark", "light")
const SwitchIcon = useColorModeValue(FaMoon, FaSun)
@ -77,20 +80,11 @@ function HeaderContent() {
mobileNavBtnRef.current?.focus()
}, [mobileNav.isOpen])
const login = async () => {
const res = await requestApi.post("/login")
saveToken(res.data.token)
storeSession(res.data)
onLoginClose()
}
const logout = async () => {
await requestApi.post("/logout")
removeToken()
storeSession(null)
const login = () => {
storage.set("current-page", pathname)
router.push('/login')
}
return (
<>
<Flex w="100%" h="100%" align="center" justify="space-between" px={{ base: "4", md: "6" }}>
@ -167,11 +161,11 @@ function HeaderContent() {
<span>Sunface</span>
</MenuItem>
<MenuDivider />
<MenuItem icon={<FaChartBar fontSize="16" />}>Dashboard</MenuItem>
<MenuItem icon={<FaRegBookmark fontSize="16" />}>Bookmarks</MenuItem>
{<MenuItem as="a" icon={<FaEdit fontSize="16"/>} href="/editor"></MenuItem>}
<MenuItem icon={<FaBookmark fontSize="16" />}></MenuItem>
<MenuDivider />
<MenuItem icon={<FaRegSun fontSize="16" />}>Account Settings</MenuItem>
<MenuItem onClick={() => logout()} icon={<FaSignOutAlt fontSize="16" />}>Log out</MenuItem>
<MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem>
<MenuItem onClick={() => logout()} icon={<FaSignOutAlt fontSize="16" />}></MenuItem>
</MenuList>
</Menu> :
<Button
@ -179,7 +173,7 @@ function HeaderContent() {
ml="2"
colorScheme="teal"
fontSize=".8rem"
onClick={onLoginOpen}
onClick={() => login()}
// leftIcon={<FaUserAlt />}
>
SIGN IN
@ -193,36 +187,6 @@ function HeaderContent() {
</Flex>
</Flex>
<MobileNavContent isOpen={mobileNav.isOpen} onClose={mobileNav.onClose} />
<Modal isOpen={isLoginOpen} onClose={onLoginClose} autoFocus={false} size="xl" isCentered >
<ModalOverlay bg="rgba(0, 0, 0, 0.6)">
<Image src="/login-bg.svg" height="100%" />
</ModalOverlay>
<ModalContent p="9" pb="7">
<ModalBody textAlign="center" display="flex" alignItems="center" flexDirection="column">
<Logo width="12rem" />
<Text mt="8" fontSize="1.1rem" fontWeight="500">im.dev</Text>
<VStack mt="2" p="5" align="left" spacing="2" fontSize="15px">
<Box display="flex" flexDirection="row" alignItems="center">
<svg width="48px" height="48px" fill={useColorModeValue("teal","white")} version="1.1" viewBox="0 0 24 24"><path d="M7.036 14.836a1.003 1.003 0 01-1.418 0l-.709-.709a6.518 6.518 0 119.218-9.218l.71.71a1.003 1.003 0 010 1.417l-.71.71a1.003 1.003 0 01-1.418 0L12 7.035A3.51 3.51 0 007.036 12l.71.71a1.003 1.003 0 010 1.417l-.71.71zm2.128 3.546a1.003 1.003 0 010-1.418l.709-.71a1.003 1.003 0 011.418 0l.709.71A3.51 3.51 0 0016.964 12l-.71-.71a1.003 1.003 0 010-1.417l.71-.71a1.003 1.003 0 011.418 0l.709.71a6.518 6.518 0 11-9.218 9.218l-.71-.71zm0-9.218a1.504 1.504 0 012.127 0l3.545 3.545a1.504 1.504 0 01-2.127 2.127l-3.545-3.545a1.504 1.504 0 010-2.127z" fillRule="evenodd"></path></svg>
<Text ml="4" layerStyle="textSecondary"></Text>
</Box>
<Box display="flex" flexDirection="row" alignItems="center">
<svg width="48px" height="48px" fill={useColorModeValue("teal","white")} version="1.1" viewBox="0 0 24 24"><path d="M9 2v1a1 1 0 001 1h4a1 1 0 001-1V2h1a2 2 0 012 2v16a2 2 0 01-2 2H8a2 2 0 01-2-2V4a2 2 0 012-2h1zm1 16a1 1 0 000 2h4a1 1 0 000-2h-4z" fillRule="evenodd"></path></svg>
<Text ml="4" layerStyle="textSecondary"></Text>
</Box>
<Box display="flex" flexDirection="row" alignItems="center">
<svg width="48px" height="48px" fill={useColorModeValue("teal","white")} version="1.1" viewBox="0 0 24 24"><path d="M19 21l-3-.5.786-4.321A1 1 0 0015.802 15H8.198a1 1 0 00-.984 1.179L8 20.5 5 21a1 1 0 01-1-1v-.5c0-4.142 3.582-7.5 8-7.5s8 3.358 8 7.5v.5a1 1 0 01-1 1zm-7-2a1 1 0 110-2 1 1 0 010 2zm0-8a4 4 0 110-8 4 4 0 010 8z" fillRule="evenodd"></path></svg>
<Text ml="4" layerStyle="textSecondary"></Text>
</Box>
</VStack>
<Button onClick={() => login()} layerStyle="colorButton" mt="6" fontSize=".9rem" leftIcon={<FaGithub fontSize="1.0rem" /> }>使github</Button>
<Text mt="6" fontSize=".7rem" layerStyle="textSecondary">im.dev<Link textDecoration="underline"></Link><Link textDecoration="underline"></Link></Text>
{/* <Image src="/pokeman.svg" height="300px" /> */}
</ModalBody>
</ModalContent>
</Modal>
</>
)
}

@ -2,7 +2,7 @@ import { Badge, Box, chakra } from "@chakra-ui/react"
import { SkipNavContent, SkipNavLink } from "@chakra-ui/skip-nav"
import Container from "components/container"
import Footer from "./footer"
import Nav from "./nav"
import Nav from "./nav/nav"
import SEO from "components/seo"
import { useRouter } from "next/router"
import * as React from "react"

@ -0,0 +1,61 @@
import { chakra, PropsOf, useColorModeValue } from "@chakra-ui/react"
import NextLink from "next/link"
import { useRouter } from "next/router"
import React from "react"
const StyledLink = React.forwardRef(function StyledLink(
props: PropsOf<typeof chakra.a> & { isActive?: boolean },
ref: React.Ref<any>,
) {
const { isActive, icon,children, ...rest } = props
return (
<chakra.a
aria-current={isActive ? "page" : undefined}
width="100%"
px="3"
py="2"
rounded="md"
ref={ref}
fontSize="14px"
fontWeight="500"
color={useColorModeValue("gray.700", "whiteAlpha.900")}
transition="all 0.2s"
display="flex"
alignItems="center"
_activeLink={{
bg: useColorModeValue("teal.50", "rgba(48, 140, 122, 0.3)"),
color: useColorModeValue("teal.700", "teal.200"),
fontWeight: "600",
}}
{...rest}
><chakra.span fontSize="1.1rem">{icon}</chakra.span> <chakra.span ml="5">{children}</chakra.span></chakra.a>
)
})
type SidebarLinkProps = PropsOf<typeof chakra.div> & {
href?: string
icon?: React.ReactElement
}
const SidebarLink = (props: SidebarLinkProps) => {
const { href, icon, children, ...rest } = props
const { pathname } = useRouter()
const isActive = pathname === href
return (
<chakra.div
userSelect="none"
display="flex"
alignItems="center"
lineHeight="1.5rem"
{...rest}
>
<NextLink href={href} passHref>
<StyledLink isActive={isActive} icon={icon}>{children}</StyledLink>
</NextLink>
</chakra.div>
)
}
export default SidebarLink

@ -0,0 +1,56 @@
import { Box, Stack } from "@chakra-ui/react"
import Card from "components/card"
import { useRouter } from "next/router"
import * as React from "react"
import { Route } from "src/types/route"
import SidebarLink from "./sidebar-link"
export function SidebarContent(props) {
const { routes, pathname, contentRef } = props
return (
<>
<Stack as="ul">
{routes.map((route:Route) => {
if (route.disabled) {return null}
return <SidebarLink as="li" key={route.path} href={route.path} icon={route.icon}>
<span>{route.title}</span>
</SidebarLink>
})}
</Stack>
</>
)
}
const Sidebar = ({ routes, ...props }) => {
const { pathname } = useRouter()
const ref = React.useRef<HTMLDivElement>(null)
return (
<Card p="0" {...props}>
<Box
ref={ref}
as="nav"
aria-label="Main Navigation"
pos="sticky"
sx={{
overscrollBehavior: "contain",
}}
top="6.5rem"
pr="3"
pb="6"
pl="3"
pt="6"
overflowY="auto"
className="sidebar-content"
flexShrink={0}
display={{ base: "none", md: "block" }}
>
<SidebarContent routes={routes} pathname={pathname} contentRef={ref} />
</Box>
</Card>
)
}
export default Sidebar

@ -5,10 +5,10 @@ async function redirect() {
destination: "https://discord.gg/dQHfcWF",
permanent: true,
},
// GENERAL
// GENERAL
{
source: "/getting-started",
destination: "/docs/getting-started",
source: "/editor",
destination: "/editor/articles",
permanent: true,
}
]

@ -21,20 +21,26 @@
"@emotion/react": "^11.1.4",
"@emotion/styled": "^11.0.0",
"@octokit/rest": "^18.0.12",
"@types/validator": "^13.1.3",
"axios": "^0.19.2",
"date-fns": "^2.16.1",
"docsearch.js": "^2.6.3",
"eventemitter3": "^4.0.4",
"formik": "^2.2.6",
"framer-motion": "^3.1.1",
"json-bigint": "^1.0.0",
"lodash": "^4.17.15",
"moment": "^2.27.0",
"next": "^10.0.4",
"next-seo": "^4.17.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-icons": "^4.1.0"
"react-icons": "^4.1.0",
"validator": "^13.5.2"
},
"devDependencies": {
"@next/bundle-analyzer": "^10.0.4",
"@types/lodash": "^4.14.123",
"@types/moment": "^2.13.0",
"@types/node": "^14.14.19",
"@types/react": "^17.0.0",

@ -7,6 +7,8 @@ import { ChakraProvider } from "@chakra-ui/react"
import theme from "theme"
import FontFace from "src/components/font-face"
import { getSeo } from "utils/seo"
import GAScript from "analytics/ga-script"
import {initUIConfig} from 'src/utils/config'
Router.events.on("routeChangeComplete", (url) => {
trackPageview(url)
@ -15,6 +17,7 @@ Router.events.on("routeChangeComplete", (url) => {
const App = ({ Component, pageProps }) => {
const seo = getSeo({ omitOpenGraphImage: false })
initUIConfig()
return (
<>
<Head>
@ -29,6 +32,7 @@ const App = ({ Component, pageProps }) => {
<ChakraProvider theme={theme}>
<Component {...pageProps} />
</ChakraProvider>
<GAScript />
<FontFace />
</>
)

@ -2,7 +2,7 @@ import { chakra } from "@chakra-ui/react"
import Container from "components/container"
import SEO from "components/seo"
import siteConfig from "configs/site-config"
import Nav from "layouts/nav"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container"
import React from "react"

@ -0,0 +1,190 @@
import { createStandaloneToast, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider } from "@chakra-ui/react"
import Card from "components/card"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container"
import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react"
import editorLinks from "src/data/editor-links"
import { requestApi } from "utils/axios/request"
import { useDisclosure } from "@chakra-ui/react"
import { Field, Form, Formik } from "formik"
import { config } from "utils/config"
import TextArticleCard from "components/articles/text-article-card"
import { Article } from "src/types/posts"
var validator = require('validator');
const toast = createStandaloneToast()
const newPost:Article = { title: '', url: '', cover: ''}
const ArticlesPage = () => {
const [posts, setPosts] = useState([])
const [currentPost, setCurrentPost] = useState(newPost)
useEffect(() => {
getPosts()
}, [])
const getPosts = () => {
requestApi.get(`/editor/articles`).then((res) => setPosts(res.data)).catch(_ => setPosts([]))
}
const { isOpen, onOpen, onClose } = useDisclosure()
function validateTitle(value) {
console.log(value)
let error
if (!value?.trim()) {
error = "标题不能为空"
}
return error
}
function validateUrl(value) {
let error
if (!validator.isURL(value)) {
error = "URL格式不合法"
}
return error
}
function validateBrief(value) {
let error
if (value && value.length > config.posts.briefMaxLen) {
error = `文本长度不能超过${config.posts.briefMaxLen}`
}
return error
}
const submitArticle = async (values, _) => {
await requestApi.post(`/editor/article`, values)
onClose()
toast({
description: "提交成功",
status: "success",
duration: 2000,
isClosable: true,
})
setCurrentPost(newPost)
}
const editArticle = (ar: Article) => {
setCurrentPost(ar)
onOpen()
}
const onDeleteArticle = () => {
getPosts()
toast({
description: "删除成功",
status: "success",
duration: 2000,
isClosable: true,
})
}
return (
<>
<Nav />
<PageContainer>
<Box display="flex">
<Sidebar routes={editorLinks} width="250px" height="fit-content" />
<Card ml="4" p="6" width="100%">
<Flex alignItems="center" justify="space-between">
<Heading size="md">({posts.length})</Heading>
<Button colorScheme="teal" size="sm" onClick={onOpen} _focus={null}></Button>
</Flex>
{
posts.length === 0 ?
<>
<Center mt="4">
<Image height="25rem" src="/empty-posts.png" />
</Center>
<Center mt="8">
<Heading size="sm"></Heading>
</Center>
</>
:
<VStack mt="4">
{posts.map(post =>
<Box width="100%" key={post.id}>
<TextArticleCard article={post} showActions={true} mt="4" onEdit={() => editArticle(post)} onDelete={() => onDeleteArticle()} />
<Divider mt="5" />
</Box>
)}
</VStack>
}
<Center><Text layerStyle="textSecondary" fontSize="sm" mt="4"></Text></Center>
</Card>
</Box>
</PageContainer>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader></ModalHeader>
<ModalBody mb="2">
<Formik
initialValues={currentPost}
onSubmit={submitArticle}
>
{(props) => (
<Form>
<VStack>
<Field name="title" validate={validateTitle}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.title && form.touched.title} >
<FormLabel></FormLabel>
<Input {...field} placeholder="name" />
<FormErrorMessage>{form.errors.title}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="url" validate={validateUrl}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.url && form.touched.url}>
<FormLabel>URL</FormLabel>
<Input {...field} placeholder="https://..." />
<FormErrorMessage>{form.errors.url}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="cover" validate={validateUrl}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.cover && form.touched.cover}>
<FormLabel></FormLabel>
<Input {...field} placeholder="https://..." />
<FormErrorMessage>{form.errors.cover}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="brief" validate={validateBrief}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.brief && form.touched.brief}>
<FormLabel></FormLabel>
<Textarea {...field} placeholder="往往是开头的一段文字"></Textarea>
<FormErrorMessage>{form.errors.brief}</FormErrorMessage>
</FormControl>
)}
</Field>
</VStack>
<Box mt={6}>
<Button
colorScheme="teal"
variant="outline"
type="submit"
_focus={null}
>
</Button>
<Button variant="ghost" ml="4" _focus={null} onClick={onClose}></Button>
</Box>
</Form>
)}
</Formik>
</ModalBody>
</ModalContent>
</Modal>
</>
)
}
export default ArticlesPage

@ -1,8 +1,8 @@
import { chakra } from "@chakra-ui/react"
import Container from "components/container"
import Card from "components/card"
import SEO from "components/seo"
import siteConfig from "configs/site-config"
import Nav from "layouts/nav"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container"
import React from "react"
@ -14,8 +14,12 @@ const HomePage = () => (
/>
<Nav />
<PageContainer>
<Card width="200px">
<chakra.h1>NOT FOUND</chakra.h1>
<p>You just hit a route that doesn&#39;t exist... the sadness.</p>
</Card>
</PageContainer>
</>
)

@ -0,0 +1,66 @@
import React from "react"
import {
Text,
Box,
VStack,
Button,
Image,
useColorModeValue,
Link,
Center
} from "@chakra-ui/react"
import Logo from "components/logo"
import { FaGithub } from "react-icons/fa"
import { requestApi } from "utils/axios/request"
import { saveToken } from "utils/axios/getToken"
import storage from "utils/localStorage"
import { useRouter } from "next/router"
const LoginPage = () => {
const router = useRouter()
const login = async () => {
const res = await requestApi.post("/login")
saveToken(res.data.token)
storage.set('session', res.data)
const oldPage = storage.get('current-page')
if (oldPage) {
storage.remove('current-page')
router.push(oldPage)
} else {
router.push('/')
}
}
return (
<Box height="100vh" width="100%" display="flex" alignItems="center" justifyContent="center">
<Image src="/login-bg.svg" height="100%" position="absolute" />
<Box textAlign="center" display="flex" alignItems="center" flexDirection="column">
<Logo width="12rem" />
<Text mt="8" fontSize="1.1rem" fontWeight="500">im.dev</Text>
<VStack mt="2" p="5" align="left" spacing="2" fontSize="15px">
<Box display="flex" flexDirection="row" alignItems="center">
<svg width="48px" height="48px" fill={useColorModeValue("teal", "white")} version="1.1" viewBox="0 0 24 24"><path d="M7.036 14.836a1.003 1.003 0 01-1.418 0l-.709-.709a6.518 6.518 0 119.218-9.218l.71.71a1.003 1.003 0 010 1.417l-.71.71a1.003 1.003 0 01-1.418 0L12 7.035A3.51 3.51 0 007.036 12l.71.71a1.003 1.003 0 010 1.417l-.71.71zm2.128 3.546a1.003 1.003 0 010-1.418l.709-.71a1.003 1.003 0 011.418 0l.709.71A3.51 3.51 0 0016.964 12l-.71-.71a1.003 1.003 0 010-1.417l.71-.71a1.003 1.003 0 011.418 0l.709.71a6.518 6.518 0 11-9.218 9.218l-.71-.71zm0-9.218a1.504 1.504 0 012.127 0l3.545 3.545a1.504 1.504 0 01-2.127 2.127l-3.545-3.545a1.504 1.504 0 010-2.127z" fillRule="evenodd"></path></svg>
<Text ml="4" layerStyle="textSecondary"></Text>
</Box>
<Box display="flex" flexDirection="row" alignItems="center">
<svg width="48px" height="48px" fill={useColorModeValue("teal", "white")} version="1.1" viewBox="0 0 24 24"><path d="M9 2v1a1 1 0 001 1h4a1 1 0 001-1V2h1a2 2 0 012 2v16a2 2 0 01-2 2H8a2 2 0 01-2-2V4a2 2 0 012-2h1zm1 16a1 1 0 000 2h4a1 1 0 000-2h-4z" fillRule="evenodd"></path></svg>
<Text ml="4" layerStyle="textSecondary"></Text>
</Box>
<Box display="flex" flexDirection="row" alignItems="center">
<svg width="48px" height="48px" fill={useColorModeValue("teal", "white")} version="1.1" viewBox="0 0 24 24"><path d="M19 21l-3-.5.786-4.321A1 1 0 0015.802 15H8.198a1 1 0 00-.984 1.179L8 20.5 5 21a1 1 0 01-1-1v-.5c0-4.142 3.582-7.5 8-7.5s8 3.358 8 7.5v.5a1 1 0 01-1 1zm-7-2a1 1 0 110-2 1 1 0 010 2zm0-8a4 4 0 110-8 4 4 0 010 8z" fillRule="evenodd"></path></svg>
<Text ml="4" layerStyle="textSecondary"></Text>
</Box>
</VStack>
<Button onClick={() => login()} layerStyle="colorButton" mt="6" fontSize=".9rem" leftIcon={<FaGithub fontSize="1.0rem" />}>使github</Button>
<Text mt="6" fontSize=".7rem" layerStyle="textSecondary">im.dev<Link textDecoration="underline"></Link><Link textDecoration="underline"></Link></Text>
{/* <Image src="/pokeman.svg" height="300px" /> */}
</Box>
</Box>
)
}
export default LoginPage

@ -2,7 +2,7 @@ import { chakra } from "@chakra-ui/react"
import Container from "components/container"
import SEO from "components/seo"
import siteConfig from "configs/site-config"
import Nav from "layouts/nav"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container"
import React from "react"

@ -2,7 +2,7 @@ import { chakra } from "@chakra-ui/react"
import Container from "components/container"
import SEO from "components/seo"
import siteConfig from "configs/site-config"
import Nav from "layouts/nav"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container"
import React from "react"

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@ -0,0 +1,5 @@
package api
import "github.com/imdotdev/im.dev/server/pkg/log"
var logger = log.RootLogger.New("logger", "api")

@ -0,0 +1,44 @@
package api
import (
"database/sql"
"net/http"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/posts"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/pkg/common"
)
func GetEditorArticles(c *gin.Context) {
user := session.CurrentUser(c)
ars, err := posts.UserArticles(int64(user.ID))
if err != nil && err != sql.ErrNoRows {
logger.Warn("get user articles error", "error", err)
c.JSON(http.StatusInternalServerError, common.RespInternalError())
return
}
c.JSON(http.StatusOK, common.RespSuccess(ars))
}
func PostEditorArticle(c *gin.Context) {
err := posts.PostArticle(c)
if err != nil {
logger.Warn("post article error", "error", err)
c.JSON(400, common.RespError(err.Error()))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func DeleteEditorArticle(c *gin.Context) {
err := posts.DeleteArticle(c)
if err != nil {
logger.Warn("delete article error", "error", err)
c.JSON(400, common.RespError(err.Error()))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}

@ -0,0 +1,82 @@
package posts
import (
"errors"
"sort"
"strings"
"time"
"unicode/utf8"
"github.com/asaskevich/govalidator"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/pkg/config"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/errcode"
"github.com/imdotdev/im.dev/server/pkg/models"
)
func UserArticles(uid int64) (models.Articles, error) {
ars := make(models.Articles, 0)
rows, err := db.Conn.Query("select id,title,url,cover,brief,created,updated from articles where creator=?", uid)
if err != nil {
return ars, err
}
creator := &models.UserSimple{ID: uid}
creator.Query()
for rows.Next() {
ar := &models.Article{}
err := rows.Scan(&ar.ID, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Created, &ar.Updated)
if err != nil {
logger.Warn("scan articles error", "error", err)
continue
}
ar.Creator = creator
ars = append(ars, ar)
}
sort.Sort(ars)
return ars, nil
}
func PostArticle(c *gin.Context) error {
user := session.CurrentUser(c)
if !user.Role.IsEditor() {
return errors.New(errcode.NoEditorPermission)
}
ar := &models.Article{}
err := c.Bind(&ar)
if err != nil {
return err
}
if strings.TrimSpace(ar.Title) == "" || utf8.RuneCountInString(ar.Brief) > config.Data.Posts.BriefMaxLen || !govalidator.IsURL(ar.URL) || !govalidator.IsURL(ar.Cover) {
return errors.New(errcode.ParamInvalid)
}
now := time.Now()
if ar.ID == 0 {
//create
_, err = db.Conn.Exec("INSERT INTO articles (creator, title, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?)",
user.ID, ar.Title, ar.URL, ar.Cover, ar.Brief, now, now)
return err
}
_, err = db.Conn.Exec("UPDATE articles SET title=?, url=?, cover=?, brief=?, updated=? WHERE id=?",
ar.Title, ar.URL, ar.Cover, ar.Brief, now, ar.ID)
return err
}
func DeleteArticle(c *gin.Context) error {
user := session.CurrentUser(c)
if !user.Role.IsEditor() {
return errors.New(errcode.NoEditorPermission)
}
id := c.Param("id")
_, err := db.Conn.Exec("DELETE FROM articles WHERE id=?", id)
return err
}

@ -0,0 +1,5 @@
package posts
import "github.com/imdotdev/im.dev/server/pkg/log"
var logger = log.RootLogger.New("logger", "posts")

@ -4,6 +4,7 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/api"
"github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/internal/storage"
"github.com/imdotdev/im.dev/server/pkg/common"
@ -43,8 +44,19 @@ func (s *Server) Start() error {
{
r.POST("/login", session.Login)
r.POST("/logout", session.Logout)
r.GET("/uiconfig", GetUIConfig)
}
// login apis
lr := r.Group("", IsLogin())
{
editorR := lr.Group("/editor")
{
editorR.GET("/articles", api.GetEditorArticles)
editorR.POST("/article", api.PostEditorArticle)
editorR.DELETE("/article/:id", api.DeleteEditorArticle)
}
}
err := router.Run(config.Data.Server.Addr)
if err != nil {
logger.Crit("start backend server error", "error", err)
@ -83,9 +95,9 @@ func Cors() gin.HandlerFunc {
// Auth is a gin middleware for user auth
func IsLogin() gin.HandlerFunc {
return func(c *gin.Context) {
user := session.Current(c)
user := session.CurrentUser(c)
if user == nil {
c.JSON(http.StatusUnauthorized, common.RespError(nil, errcode.NeedLogin))
c.JSON(http.StatusUnauthorized, common.RespError(errcode.NeedLogin))
c.Abort()
return
}

@ -91,7 +91,7 @@ func storeSession(s *Session) error {
return nil
}
func Current(c *gin.Context) *models.User {
func CurrentUser(c *gin.Context) *models.User {
token := getToken(c)
createTime, _ := strconv.ParseInt(token, 10, 64)
if createTime != 0 {

@ -27,4 +27,22 @@ var sqlTables = map[string]string{
user_id INTEGER
);
`,
"articles": `CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
url VARCHAR(255) NOT NULL,
cover VARCHAR(255),
brief TEXT,
created DATETIME NOT NULL,
updated DATETIME
);
CREATE INDEX IF NOT EXISTS articles_creator
ON articles (creator);
CREATE INDEX IF NOT EXISTS articles_created
ON articles (created);
`,
}

@ -0,0 +1,27 @@
package internal
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/config"
)
type UIConfig struct {
Posts *UIPosts `json:"posts"`
}
type UIPosts struct {
BriefMaxLen int `json:"briefMaxLen"`
}
func GetUIConfig(c *gin.Context) {
conf := &UIConfig{
Posts: &UIPosts{
BriefMaxLen: config.Data.Posts.BriefMaxLen,
},
}
c.JSON(http.StatusOK, common.RespSuccess(conf))
}

@ -16,10 +16,9 @@ func RespSuccess(data interface{}) *Resp {
return r
}
func RespError(data interface{}, msg string) *Resp {
func RespError(msg string) *Resp {
r := &Resp{}
r.Status = Error
r.Data = data
r.Message = msg
return r

@ -31,6 +31,10 @@ type Config struct {
Data string
Logs string
}
Posts struct {
BriefMaxLen int `yaml:"brief_max_len"`
}
}
// Data ...

@ -2,4 +2,6 @@ package errcode
const DB = "database error"
const Internal = "server internal error"
const NeedLogin = "login status is needed"
const NeedLogin = "你需要登录才能访问该页面"
const NoEditorPermission = "只有编辑角色才能执行此操作"
const ParamInvalid = "请求参数不正确"

@ -0,0 +1,22 @@
package models
import "time"
type Article struct {
ID int64 `json:"id"`
Creator *UserSimple `json:"creator"`
Title string `json:"title"`
URL string `json:"url"`
Cover string `json:"cover"`
Brief string `json:"brief"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
type Articles []*Article
func (ar Articles) Len() int { return len(ar) }
func (ar Articles) Swap(i, j int) { ar[i], ar[j] = ar[j], ar[i] }
func (ar Articles) Less(i, j int) bool {
return ar[i].Created.Unix() > ar[j].Created.Unix()
}

@ -7,7 +7,7 @@ import (
)
type User struct {
ID int `json:"id"`
ID int64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
@ -29,3 +29,22 @@ func (user *User) Query(id int64, username string, email string) error {
return err
}
type UserSimple struct {
ID int64 `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
}
func (user *UserSimple) Query() error {
err := db.Conn.QueryRow(`SELECT id,username,nickname,avatar FROM user WHERE id=? or username=? or email=?`, user.ID).Scan(
&user.ID, &user.Username, &user.Nickname, &user.Avatar,
)
if user.Avatar == "" {
user.Avatar = DefaultAvatar
}
return err
}

@ -0,0 +1,37 @@
import React from "react"
import {chakra, Heading, VStack, Text, HStack,Button, Flex,PropsOf } from "@chakra-ui/react"
import { Article } from "src/types/posts"
import moment from 'moment'
import { requestApi } from "utils/axios/request"
type Props = PropsOf<typeof chakra.div> & {
article: Article
showActions: boolean
onEdit?: any
onDelete?: any
}
export const TextArticleCard= (props:Props) =>{
const {article,showActions,onEdit,onDelete, ...rest} = props
const gap = moment(article.created).fromNow()
const onDeleteArticle = async () => {
await requestApi.delete(`/editor/article/${article.id}`)
onDelete()
}
return (
<Flex justifyContent="space-between" {...rest}>
<VStack>
<Heading size="sm">{props.article.title}</Heading>
<Text fontSize=".9rem">{gap}</Text>
</VStack>
{props.showActions && <HStack>
<Button size="sm" colorScheme="teal" variant="outline" onClick={onEdit}>Edit</Button>
<Button size="sm" onClick={onDeleteArticle}>Delete</Button>
</HStack>}
</Flex>
)
}
export default TextArticleCard

@ -0,0 +1,14 @@
import React from "react"
import { Box, BoxProps } from "@chakra-ui/react"
export const Card = (props: BoxProps) => (
<Box
borderRadius=".5rem"
borderWidth="1px"
p="1rem"
boxShadow="0 1px 2px 0 rgb(0 0 0 / 5%)"
{...props}
/>
)
export default Card

@ -0,0 +1,24 @@
import React from 'react'
import { FaFileAlt, FaScroll, FaBookOpen } from 'react-icons/fa'
import { Route } from 'src/types/route'
const editorLinks: Route[] = [{
title: '文章',
path: '/editor/article',
icon: <FaFileAlt />,
disabled: false
},
{
title: '系列',
path: '/editor/series',
icon: <FaBookOpen />,
disabled: false
},
{
title: '课程',
path: '/editor/course',
icon: <FaScroll />,
disabled: false
},
]
export default editorLinks

@ -0,0 +1 @@
export const needLogin = "你需要登录才能访问该页面"

@ -1,14 +1,21 @@
import { useEffect, useState } from "react"
import { Session } from "src/types/session"
import events from "utils/events"
import storage from "utils/localStorage"
function useSession(): [Session, any] {
function useSession(): Session{
const [session, setSession] = useState(null)
useEffect(() => {
const sess = storage.get('session')
if (sess) {
setSession(sess)
}
events.on('set-session',storeSession)
return() => {
events.off('set-session',storeSession)
}
}, [])
@ -17,7 +24,7 @@ function useSession(): [Session, any] {
setSession(sess)
}
return [session, storeSession]
return session
}
export default useSession

@ -0,0 +1,11 @@
import {User} from './session'
export interface Article {
id?: number
creator?: User
title: string
url: string
cover: string
brief?: string
created?: string
}

@ -0,0 +1,6 @@
export interface Route {
path: string
icon: any
title: string
disabled: boolean
}

@ -8,9 +8,9 @@ export interface User {
id :number
username: string
nickname: string
role: string
avatar: string
email: string
role?: string
email?: string
lastSeenAt?: string
created: string
created?: string
}

@ -17,6 +17,8 @@ const JSONbigString = require('json-bigint')({ storeAsString: true })
import type { OutgoingHttpHeaders } from 'http'
import { createStandaloneToast } from "@chakra-ui/react"
import { logout } from 'utils/session'
import { getToken } from './getToken'
const toast = createStandaloneToast()
axios.defaults.transformResponse = [
@ -54,21 +56,30 @@ requestApi.interceptors.response.use(printResData)
// 对返回信息进行处理
requestApi.interceptors.response.use(
response => {
return response
return response.data
},
error => {
let message = "error msg missing"
let status = 200
if (error.response && error.response.data) {
message = error.response.data.message
status = error.response.status
} else {
message = error.message
message = error.text ?? error.message
}
if (status === 401) {
if (getToken()) {
// 当前登录状态已经过期,进行登出操作
logout()
}
}
toast({
title: `请求错误`,
description: message,
status: "error",
duration: 5000,
duration: 2000,
isClosable: true,
})

@ -0,0 +1,13 @@
import { requestApi } from "./axios/request"
export let config = {
posts: {
briefMaxLen: 10
}
}
export function initUIConfig() {
requestApi.get("/uiconfig").then((res) => {
console.log("初始化UI config:", res.data)
config = res.data
})}

@ -0,0 +1,98 @@
import EventEmitter3, { EventEmitter } from 'eventemitter3';
export type AppEvent<T> = {
readonly name: string;
payload?: T;
}
export class Emitter {
emitter: EventEmitter3;
constructor() {
this.emitter = new EventEmitter();
}
/**
* DEPRECATED.
*/
emit(name: string, data?: any): void;
/**
* Emits an `event` with `payload`.
*/
emit<T extends undefined>(event: AppEvent<T>): void;
//@ts-ignore
emit<T extends Partial<T> extends T ? Partial<T> : never>(event: AppEvent<T>): void;
emit<T>(event: AppEvent<T>, payload: T): void;
emit<T>(event: AppEvent<T> | string, payload?: T | any): void {
if (typeof event === 'string') {
// console.log(`Using strings as events is deprecated and will be removed in a future version. (${event})`);
this.emitter.emit(event, payload);
} else {
this.emitter.emit(event.name, payload);
}
}
/**
* DEPRECATED.
*/
on(name: string, handler: (payload?: any) => void, scope?: any): void;
/**
* Handles `event` with `handler()` when emitted.
*/
on<T extends undefined>(event: AppEvent<T>, handler: () => void, scope?: any): void;
//@ts-ignore
on<T extends Partial<T> extends T ? Partial<T> : never>(event: AppEvent<T>, handler: () => void, scope?: any): void;
on<T>(event: AppEvent<T>, handler: (payload: T) => void, scope?: any): void;
on<T>(event: AppEvent<T> | string, handler: (payload?: T | any) => void, scope?: any) {
if (typeof event === 'string') {
// console.log(`Using strings as events is deprecated and will be removed in a future version. (${event})`);
this.emitter.on(event, handler);
if (scope) {
const unbind = scope.$on('$destroy', () => {
this.emitter.off(event, handler);
unbind();
});
}
return;
}
this.emitter.on(event.name, handler);
if (scope) {
const unbind = scope.$on('$destroy', () => {
this.emitter.off(event.name, handler);
unbind();
});
}
}
/**
* DEPRECATED.
*/
off(name: string, handler: (payload?: any) => void): void;
off<T extends undefined>(event: AppEvent<T>, handler: () => void): void;
//@ts-ignore
off<T extends Partial<T> extends T ? Partial<T> : never>(event: AppEvent<T>, handler: () => void, scope?: any): void;
off<T>(event: AppEvent<T>, handler: (payload: T) => void): void;
off<T>(event: AppEvent<T> | string, handler: (payload?: T | any) => void) {
if (typeof event === 'string') {
// console.log(`Using strings as events is deprecated and will be removed in a future version. (${event})`);
this.emitter.off(event, handler);
return;
}
this.emitter.off(event.name, handler);
}
removeAllListeners(evt?: string) {
this.emitter.removeAllListeners(evt);
}
getEventCount(): number {
return (this.emitter as any)._eventsCount;
}
}

@ -0,0 +1,5 @@
import { Emitter } from './emitter';
export const events = new Emitter();
export default events;

@ -8,7 +8,7 @@ const storage = {
},
remove(key:string){
localStorage.removeItem(adminKey+key)
}
}
}
export default storage

@ -0,0 +1,9 @@
import { removeToken } from "./axios/getToken"
import { requestApi } from "./axios/request"
import events from "./events"
export const logout = async () => {
await requestApi.post("/logout")
removeToken()
events.emit('set-session', null)
}

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

@ -1099,7 +1099,7 @@
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
"@types/lodash@*", "@types/lodash@^4.14.123":
version "4.14.168"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
integrity sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==
@ -1151,6 +1151,11 @@
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf"
integrity sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw==
"@types/validator@^13.1.3":
version "13.1.3"
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.1.3.tgz#366b394aa3fbeed2392bf0a20ded606fa4a3d35e"
integrity sha512-DaOWN1zf7j+8nHhqXhIgNmS+ltAC53NXqGxYuBhWqWgqolRhddKzfZU814lkHQSTG0IUfQxU7Cg0gb8fFWo2mA==
"@types/warning@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52"
@ -1945,6 +1950,11 @@ deep-extend@^0.6.0:
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
deepmerge@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@ -2183,6 +2193,11 @@ event-target-shim@^5.0.0:
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
eventemitter3@^4.0.4:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
@ -2291,6 +2306,19 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
formik@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.6.tgz#378a4bafe4b95caf6acf6db01f81f3fe5147559d"
integrity sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA==
dependencies:
deepmerge "^2.1.1"
hoist-non-react-statics "^3.3.0"
lodash "^4.17.14"
lodash-es "^4.17.14"
react-fast-compare "^2.0.1"
tiny-warning "^1.0.2"
tslib "^1.10.0"
framer-motion@^3.1.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-3.2.1.tgz#66eeb883a0b5c425dd7767ecacdeac451c184cdb"
@ -2483,7 +2511,7 @@ hogan.js@^3.0.2:
mkdirp "0.3.0"
nopt "1.0.10"
hoist-non-react-statics@^3.3.1:
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@ -2774,6 +2802,11 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.14:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7"
integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==
lodash.mergewith@4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
@ -2784,7 +2817,7 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
lodash@^4.17.13, lodash@^4.17.19, lodash@^4.17.20:
lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@ -3447,6 +3480,11 @@ react-fast-compare@3.2.0:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-focus-lock@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.4.1.tgz#e842cc93da736b5c5d331799012544295cbcee4f"
@ -3984,6 +4022,11 @@ tiny-invariant@^1.0.6:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinycolor2@1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
@ -4152,6 +4195,11 @@ uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
validator@^13.5.2:
version "13.5.2"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.5.2.tgz#c97ae63ed4224999fb6f42c91eaca9567fe69a46"
integrity sha512-mD45p0rvHVBlY2Zuy3F3ESIe1h5X58GPfAtslBjY7EtTqGquZTj+VX/J4RnHWN8FKq0C9WRVt1oWAcytWRuYLQ==
verror@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"

Loading…
Cancel
Save