pull/50/head
sunface 4 years ago
parent 42db49c060
commit 3a1c406da4

@ -26,4 +26,6 @@ paths:
#################################### Posts ############################## #################################### Posts ##############################
posts: posts:
brief_max_len: 100 brief_max_len: 100
# whether allow writing posts
writing_enabled: true

@ -7,6 +7,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
github.com/go-stack/stack v1.8.0 github.com/go-stack/stack v1.8.0
github.com/golang/snappy v0.0.2
github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac
github.com/karrick/godirwalk v1.16.1 // indirect github.com/karrick/godirwalk v1.16.1 // indirect
github.com/keegancsmith/rpc v1.3.0 // indirect github.com/keegancsmith/rpc v1.3.0 // indirect

@ -73,6 +73,8 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=

@ -0,0 +1,123 @@
import {
chakra,
Flex,
Button,
IconButton,
useColorMode,
useColorModeValue,
Box,
useRadioGroup,
HStack,
Input
} from "@chakra-ui/react"
import { useViewportScroll } from "framer-motion"
import NextLink from "next/link"
import React from "react"
import { FaMoon, FaSun } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo"
import RadioCard from "components/radio-card"
import { EditMode } from "src/types/editor"
function HeaderContent(props:any) {
const { toggleColorMode: toggleMode } = useColorMode()
const text = useColorModeValue("dark", "light")
const SwitchIcon = useColorModeValue(FaMoon, FaSun)
const editOptions = [EditMode.Edit,EditMode.Preview]
const { getRootProps, getRadioProps } = useRadioGroup({
name: "framework",
defaultValue: EditMode.Edit,
onChange: (v) => {
props.changeEditMode(v)
},
})
const group = getRootProps()
return (
<>
<Flex w="100%" h="100%" align="center" justify="space-between" px={{ base: "4", md: "6" }}>
<Flex align="center">
<NextLink href="/" passHref>
<chakra.a display={{ base: "none", md: "block" }} style={{ marginTop: '-5px' }} aria-label="Chakra UI, Back to homepage">
<Logo width="130" />
</chakra.a>
</NextLink>
<NextLink href="/" passHref>
<chakra.a display={{ base: "block", md: "none" }} aria-label="Chakra UI, Back to homepage">
<LogoIcon />
</chakra.a>
</NextLink>
</Flex>
<Box>
<Input value={props.ar.title} placeholder="Title..." onChange={props.changeTitle} focusBorderColor={useColorModeValue('teal.400','teal.100')} variant="flushed"/>
</Box>
<HStack {...group}>
{editOptions.map((value) => {
const radio = getRadioProps({ value })
return (
<RadioCard key={value} {...radio} bg="teal" color="white">
{value}
</RadioCard>
)
})}
</HStack>
<Box
color={useColorModeValue("gray.500", "gray.400")}
>
<IconButton
size="md"
fontSize="lg"
aria-label={`Switch to ${text} mode`}
variant="ghost"
color="current"
ml={{ base: "0", md: "1" }}
onClick={toggleMode}
_focus={null}
icon={<SwitchIcon />}
/>
<Button layerStyle="colorButton" ml="2" onClick={props.publish}></Button>
</Box>
</Flex>
</>
)
}
function EditorNav(props) {
const bg = useColorModeValue("white", "gray.800")
const ref = React.useRef<HTMLHeadingElement>()
const [y, setY] = React.useState(0)
const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}
const { scrollY } = useViewportScroll()
React.useEffect(() => {
return scrollY.onChange(() => setY(scrollY.get()))
}, [scrollY])
return (
<chakra.header
ref={ref}
shadow={y > height ? "sm" : undefined}
transition="box-shadow 0.2s"
pos="fixed"
top="0"
zIndex="3"
bg={bg}
left="0"
right="0"
borderTop="4px solid"
borderTopColor="teal.400"
width="full"
>
<chakra.div height="4.5rem" mx="auto" maxW="1200px">
<HeaderContent {...props} />
</chakra.div>
</chakra.header>
)
}
export default EditorNav

@ -3,7 +3,6 @@ import {
Flex, Flex,
Button, Button,
HStack, HStack,
Icon,
IconButton, IconButton,
Link, Link,
useColorMode, useColorMode,
@ -16,14 +15,6 @@ import {
MenuItem, MenuItem,
MenuDivider, MenuDivider,
Image, Image,
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalCloseButton,
Text,
Box,
VStack,
} from "@chakra-ui/react" } from "@chakra-ui/react"
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import { useViewportScroll } from "framer-motion" import { useViewportScroll } from "framer-motion"
@ -66,7 +57,7 @@ const GithubIcon = (props) => (
function HeaderContent() { function HeaderContent() {
const router = useRouter() const router = useRouter()
const { pathname } = router const { asPath } = router
const mobileNav = useDisclosure() const mobileNav = useDisclosure()
const session: Session = useSession() const session: Session = useSession()
@ -81,7 +72,8 @@ function HeaderContent() {
}, [mobileNav.isOpen]) }, [mobileNav.isOpen])
const login = () => { const login = () => {
storage.set("current-page", pathname) console.log(router)
storage.set("current-page", asPath)
router.push('/login') router.push('/login')
} }
@ -101,7 +93,7 @@ function HeaderContent() {
</NextLink> </NextLink>
<HStack display={{ base: "none", md: "flex" }} ml={{ base: 1, md: 4, lg: 12 }} fontSize="1rem" minWidth="250px"> <HStack display={{ base: "none", md: "flex" }} ml={{ base: 1, md: 4, lg: 12 }} fontSize="1rem" minWidth="250px">
{navLinks.map(link => <Link px="4" py="0.7rem" rounded="md" href={link.url} key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={pathname === link.url ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} _hover={null} _focus={null}>{link.title}</Link>)} {navLinks.map(link => <Link px="4" py="0.7rem" rounded="md" href={link.url} key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={asPath === link.url ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} _hover={null} _focus={null}>{link.title}</Link>)}
</HStack> </HStack>
</Flex> </Flex>
@ -109,7 +101,7 @@ function HeaderContent() {
w="100%" w="100%"
maxW="600px" maxW="600px"
align="center" align="center"
color="gray.400" color={useColorModeValue("gray.500","gray.400")}
> >
<AlgoliaSearch /> <AlgoliaSearch />
<HStack spacing="5" display={{ base: "none", md: "flex" }}> <HStack spacing="5" display={{ base: "none", md: "flex" }}>
@ -151,7 +143,7 @@ function HeaderContent() {
boxSize="2.8em" boxSize="2.8em"
borderRadius="full" borderRadius="full"
src="https://placekitten.com/100/100" src="https://placekitten.com/100/100"
alt="Fluffybuns the destroyer" alt="user"
/> : /> :
<FaUserAlt /> <FaUserAlt />
} }
@ -163,7 +155,7 @@ function HeaderContent() {
<span>Sunface</span> <span>Sunface</span>
</MenuItem> </MenuItem>
<MenuDivider /> <MenuDivider />
{<MenuItem as="a" icon={<FaEdit fontSize="16" />} href="/editor"></MenuItem>} {<MenuItem as="a" icon={<FaEdit fontSize="16" />} href="/editor"></MenuItem>}
<MenuItem icon={<FaBookmark fontSize="16" />}></MenuItem> <MenuItem icon={<FaBookmark fontSize="16" />}></MenuItem>
<MenuDivider /> <MenuDivider />
<MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem> <MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem>

@ -26,10 +26,11 @@ function useHeadingFocusOnRouteChange() {
interface PageContainerProps { interface PageContainerProps {
children: React.ReactNode children: React.ReactNode
nav?: any
} }
function PageContainer(props: PageContainerProps) { function PageContainer(props: PageContainerProps) {
const { children } = props const { children ,nav} = props
useHeadingFocusOnRouteChange() useHeadingFocusOnRouteChange()
return ( return (
@ -39,7 +40,7 @@ function PageContainer(props: PageContainerProps) {
description={siteConfig.seo.description} description={siteConfig.seo.description}
/> />
<SkipNavLink zIndex={20}>Skip to Content</SkipNavLink> <SkipNavLink zIndex={20}>Skip to Content</SkipNavLink>
<Nav /> {nav ? nav : <Nav />}
<Container as="main" className="main-content"> <Container as="main" className="main-content">
<Box display={{ base: "block", md: "flex" }}> <Box display={{ base: "block", md: "flex" }}>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>

@ -8,7 +8,7 @@ async function redirect() {
// GENERAL // GENERAL
{ {
source: "/editor", source: "/editor",
destination: "/editor/articles", destination: "/editor/posts",
permanent: true, permanent: true,
} }
] ]

@ -28,18 +28,22 @@
"eventemitter3": "^4.0.4", "eventemitter3": "^4.0.4",
"formik": "^2.2.6", "formik": "^2.2.6",
"framer-motion": "^3.1.1", "framer-motion": "^3.1.1",
"highlight.js": "^9.16.2",
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"markdown-to-jsx": "^7.1.1",
"moment": "^2.27.0", "moment": "^2.27.0",
"next": "^10.0.4", "next": "^10.0.4",
"next-seo": "^4.17.0", "next-seo": "^4.17.0",
"react": "^17.0.1", "react": "^17.0.1",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-icons": "^4.1.0", "react-icons": "^4.1.0",
"react-markdown-editor-lite": "^1.2.4",
"validator": "^13.5.2" "validator": "^13.5.2"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^10.0.4", "@next/bundle-analyzer": "^10.0.4",
"@types/highlight.js": "^9.12.3",
"@types/lodash": "^4.14.123", "@types/lodash": "^4.14.123",
"@types/moment": "^2.13.0", "@types/moment": "^2.13.0",
"@types/node": "^14.14.19", "@types/node": "^14.14.19",

@ -0,0 +1,26 @@
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/nav"
import PageContainer from "layouts/page-container"
import { useRouter } from "next/router"
import React from "react"
const UserPage = () => {
const router = useRouter()
return (
<>
<SEO
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<Nav />
<PageContainer>
<chakra.h1>{router.query.username}{router.query.post_slug}</chakra.h1>
</PageContainer>
</>
)}
export default UserPage

@ -0,0 +1,26 @@
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/nav"
import PageContainer from "layouts/page-container"
import { useRouter } from "next/router"
import React from "react"
const UserPage = () => {
const router = useRouter()
return (
<>
<SEO
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<Nav />
<PageContainer>
<chakra.h1>{router.query.username}'s home</chakra.h1>
</PageContainer>
</>
)}
export default UserPage

@ -0,0 +1,83 @@
import { Box, Button,createStandaloneToast} from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';
import { MarkdownEditor } from 'components/markdown-editor/editor';
import PageContainer from 'layouts/page-container';
import EditorNav from 'layouts/editor-nav'
import { EditMode } from 'src/types/editor';
import { MarkdownRender } from 'components/markdown-editor/render';
import { Post } from 'src/types/posts';
import { requestApi } from 'utils/axios/request';
import { useRouter } from 'next/router';
const toast = createStandaloneToast()
const content = `
# test
`
function PostEditPage() {
const router = useRouter()
const {id} = router.query
const [editMode, setEditMode] = useState(EditMode.Edit)
const [ar,setAr] = useState({
md: content,
title: ''
})
useEffect(() => {
if (id && id !== 'new') {
requestApi.get(`/editor/post/${id}`).then(res => setAr(res.data))
}
},[id])
const onChange = newMd => {
setAr({
...ar,
md: newMd
})
}
const publish = async () => {
await requestApi.post(`/editor/post`, ar)
toast({
description: "发布成功",
status: "success",
duration: 2000,
isClosable: true,
})
router.push('/editor/posts')
}
console.log(ar)
return (
<PageContainer
nav={<EditorNav
ar={ar}
changeEditMode={(v) => setEditMode(v)}
changeTitle={(e) => {setAr({...ar, title: e.target.value})}}
publish={() => publish()}
/>}
>
<Box style={{ height: 'calc(100vh - 145px)' }}>
{editMode === EditMode.Edit ?
<MarkdownEditor
options={{
overrides: {
Button: {
component: Button,
},
},
}}
onChange={(md) => onChange(md)}
md={ar.md}
/> :
<Box height="100%" p="6">
<MarkdownRender md={ar.md} />
</Box>
}
</Box>
</PageContainer>
);
}
export default PostEditPage

@ -1,4 +1,4 @@
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 { Menu,MenuButton,MenuList,MenuItem,createStandaloneToast, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Nav from "layouts/nav/nav" import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container" import PageContainer from "layouts/page-container"
@ -9,24 +9,27 @@ import { requestApi } from "utils/axios/request"
import { useDisclosure } from "@chakra-ui/react" import { useDisclosure } from "@chakra-ui/react"
import { Field, Form, Formik } from "formik" import { Field, Form, Formik } from "formik"
import { config } from "utils/config" import { config } from "utils/config"
import TextArticleCard from "components/articles/text-article-card" import TextPostCard from "components/posts/text-post-card"
import { Article } from "src/types/posts" import { Post } from "src/types/posts"
import { FaExternalLinkAlt, FaRegEdit } from "react-icons/fa"
import { useRouter } from "next/router"
var validator = require('validator'); var validator = require('validator');
const toast = createStandaloneToast() const toast = createStandaloneToast()
const newPost: Article = { title: '', url: '', cover: '' } const newPost: Post = { title: '', url: '', cover: '' }
const ArticlesPage = () => { const PostsPage = () => {
const [posts, setPosts] = useState([])
const [currentPost, setCurrentPost] = useState(newPost) const [currentPost, setCurrentPost] = useState(newPost)
const [posts, setPosts] = useState([])
const { isOpen, onOpen, onClose } = useDisclosure()
const router = useRouter()
const getPosts = () => {
requestApi.get(`/editor/posts`).then((res) => setPosts(res.data)).catch(_ => setPosts([]))
}
useEffect(() => { useEffect(() => {
getPosts() getPosts()
}, []) }, [])
const getPosts = () => {
requestApi.get(`/editor/articles`).then((res) => setPosts(res.data)).catch(_ => setPosts([]))
}
const { isOpen, onOpen, onClose } = useDisclosure()
function validateTitle(value) { function validateTitle(value) {
console.log(value) console.log(value)
@ -53,8 +56,8 @@ const ArticlesPage = () => {
return error return error
} }
const submitArticle = async (values, _) => { const submitPost = async (values, _) => {
await requestApi.post(`/editor/article`, values) await requestApi.post(`/editor/post`, values)
onClose() onClose()
toast({ toast({
description: "提交成功", description: "提交成功",
@ -66,12 +69,17 @@ const ArticlesPage = () => {
getPosts() getPosts()
} }
const editArticle = (ar: Article) => { const editPost = (post: Post) => {
setCurrentPost(ar) if (post.url.trim() === "") {
onOpen() router.push(`/editor/post/${post.id}`)
} else {
setCurrentPost(post)
onOpen()
}
} }
const onDeleteArticle = () => { const onDeletePost= async (id) => {
await requestApi.delete(`/editor/post/${id}`)
getPosts() getPosts()
toast({ toast({
description: "删除成功", description: "删除成功",
@ -90,7 +98,18 @@ const ArticlesPage = () => {
<Card ml="4" p="6" width="100%"> <Card ml="4" p="6" width="100%">
<Flex alignItems="center" justify="space-between"> <Flex alignItems="center" justify="space-between">
<Heading size="md">({posts.length})</Heading> <Heading size="md">({posts.length})</Heading>
<Button colorScheme="teal" size="sm" onClick={onOpen} _focus={null}></Button> {config.posts.writingEnabled ?
<Menu>
<MenuButton as={Button} colorScheme="teal" size="sm" _focus={null}>
</MenuButton>
<MenuList color={useColorModeValue("gray.500","gray.400")}>
<MenuItem icon={<FaExternalLinkAlt fontSize="14" />} onClick={onOpen}></MenuItem>
<MenuItem icon={<FaRegEdit fontSize="16" />} as="a" href="/editor/post/new"></MenuItem>
</MenuList>
</Menu>
:
<Button colorScheme="teal" size="sm" onClick={onOpen} _focus={null}></Button>}
</Flex> </Flex>
{ {
posts.length === 0 ? posts.length === 0 ?
@ -107,12 +126,12 @@ const ArticlesPage = () => {
<VStack mt="4"> <VStack mt="4">
{posts.map(post => {posts.map(post =>
<Box width="100%" key={post.id}> <Box width="100%" key={post.id}>
<TextArticleCard article={post} showActions={true} mt="4" onEdit={() => editArticle(post)} onDelete={() => onDeleteArticle()} /> <TextPostCard post={post} showActions={true} mt="4" onEdit={() => editPost(post)} onDelete={() => onDeletePost(post.id)} />
<Divider mt="5" /> <Divider mt="5" />
</Box> </Box>
)} )}
</VStack> </VStack>
<Center><Text layerStyle="textSecondary" fontSize="sm" mt="6"></Text></Center> <Center><Text layerStyle="textSecondary" fontSize="sm" mt="5"></Text></Center>
</> </>
} }
</Card> </Card>
@ -122,11 +141,11 @@ const ArticlesPage = () => {
<Modal isOpen={isOpen} onClose={onClose}> <Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader>{currentPost.id ? "编辑文章" : "新建文章"}</ModalHeader>
<ModalBody mb="2"> <ModalBody mb="2">
<Formik <Formik
initialValues={currentPost} initialValues={currentPost}
onSubmit={submitArticle} onSubmit={submitPost}
> >
{(props) => ( {(props) => (
<Form> <Form>
@ -175,7 +194,7 @@ const ArticlesPage = () => {
type="submit" type="submit"
_focus={null} _focus={null}
> >
</Button> </Button>
<Button variant="ghost" ml="4" _focus={null} onClick={onClose}></Button> <Button variant="ghost" ml="4" _focus={null} onClick={onClose}></Button>
</Box> </Box>
@ -188,5 +207,5 @@ const ArticlesPage = () => {
</> </>
) )
} }
export default ArticlesPage export default PostsPage

@ -1,44 +1,92 @@
package api package api
import ( import (
"database/sql" "fmt"
"net/http" "net/http"
"strconv"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/posts" "github.com/imdotdev/im.dev/server/internal/posts"
"github.com/imdotdev/im.dev/server/internal/session" "github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/pkg/common" "github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e"
) )
func GetEditorArticles(c *gin.Context) { func GetEditorPosts(c *gin.Context) {
user := session.CurrentUser(c) user := session.CurrentUser(c)
ars, err := posts.UserArticles(int64(user.ID)) ars, err := posts.UserPosts(int64(user.ID))
if err != nil && err != sql.ErrNoRows { if err != nil {
logger.Warn("get user articles error", "error", err) c.JSON(err.Status, common.RespError(err.Message))
c.JSON(http.StatusInternalServerError, common.RespInternalError())
return return
} }
c.JSON(http.StatusOK, common.RespSuccess(ars)) c.JSON(http.StatusOK, common.RespSuccess(ars))
} }
func PostEditorArticle(c *gin.Context) { func SubmitPost(c *gin.Context) {
err := posts.PostArticle(c) err := posts.SubmitPost(c)
if err != nil { if err != nil {
logger.Warn("post article error", "error", err) logger.Warn("submit post error", "error", err)
c.JSON(400, common.RespError(err.Error())) c.JSON(err.Status, common.RespError(err.Message))
return return
} }
c.JSON(http.StatusOK, common.RespSuccess(nil)) c.JSON(http.StatusOK, common.RespSuccess(nil))
} }
func DeleteEditorArticle(c *gin.Context) { func DeletePost(c *gin.Context) {
err := posts.DeleteArticle(c) id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if id == 0 {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
user := session.CurrentUser(c)
creator, err := posts.GetPostCreator(id)
if err != nil { if err != nil {
logger.Warn("delete article error", "error", err) c.JSON(err.Status, common.RespError(err.Message))
c.JSON(400, common.RespError(err.Error())) return
}
if user.ID != creator {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return return
} }
err = posts.DeletePost(id)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil)) c.JSON(http.StatusOK, common.RespSuccess(nil))
} }
func GetEditorPost(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
fmt.Println(c.Param("id"))
if id == 0 {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
user := session.CurrentUser(c)
creator, err := posts.GetPostCreator(id)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
if user.ID != creator {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
ar, err := posts.GetPost(id)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(ar))
}

@ -1,7 +1,8 @@
package posts package posts
import ( import (
"errors" "database/sql"
"net/http"
"sort" "sort"
"strings" "strings"
"time" "time"
@ -12,24 +13,29 @@ import (
"github.com/imdotdev/im.dev/server/internal/session" "github.com/imdotdev/im.dev/server/internal/session"
"github.com/imdotdev/im.dev/server/pkg/config" "github.com/imdotdev/im.dev/server/pkg/config"
"github.com/imdotdev/im.dev/server/pkg/db" "github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/errcode" "github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models" "github.com/imdotdev/im.dev/server/pkg/models"
"github.com/imdotdev/im.dev/server/pkg/utils"
) )
func UserArticles(uid int64) (models.Articles, error) { func UserPosts(uid int64) (models.Posts, *e.Error) {
ars := make(models.Articles, 0) ars := make(models.Posts, 0)
rows, err := db.Conn.Query("select id,title,url,cover,brief,created,updated from articles where creator=?", uid) rows, err := db.Conn.Query("select id,title,url,cover,brief,created,updated from posts where creator=?", uid)
if err != nil { if err != nil {
return ars, err if err == sql.ErrNoRows {
return ars, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal)
} }
creator := &models.UserSimple{ID: uid} creator := &models.UserSimple{ID: uid}
creator.Query() creator.Query()
for rows.Next() { for rows.Next() {
ar := &models.Article{} ar := &models.Post{}
err := rows.Scan(&ar.ID, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Created, &ar.Updated) err := rows.Scan(&ar.ID, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Created, &ar.Updated)
if err != nil { if err != nil {
logger.Warn("scan articles error", "error", err) logger.Warn("scan post error", "error", err)
continue continue
} }
@ -41,42 +47,95 @@ func UserArticles(uid int64) (models.Articles, error) {
return ars, nil return ars, nil
} }
func PostArticle(c *gin.Context) error { func SubmitPost(c *gin.Context) *e.Error {
user := session.CurrentUser(c) user := session.CurrentUser(c)
if !user.Role.IsEditor() { if !user.Role.IsEditor() {
return errors.New(errcode.NoEditorPermission) return e.New(http.StatusForbidden, e.NoEditorPermission)
} }
ar := &models.Article{} ar := &models.Post{}
err := c.Bind(&ar) err := c.Bind(&ar)
if err != nil { if err != nil {
return err return e.New(http.StatusBadRequest, e.ParamInvalid)
}
if strings.TrimSpace(ar.Title) == "" || utf8.RuneCountInString(ar.Brief) > config.Data.Posts.BriefMaxLen {
return e.New(http.StatusBadRequest, e.ParamInvalid)
} }
if strings.TrimSpace(ar.Title) == "" || utf8.RuneCountInString(ar.Brief) > config.Data.Posts.BriefMaxLen || !govalidator.IsURL(ar.URL) || !govalidator.IsURL(ar.Cover) { if strings.TrimSpace(ar.URL) != "" && !govalidator.IsURL(ar.URL) {
return errors.New(errcode.ParamInvalid) return e.New(http.StatusBadRequest, e.ParamInvalid)
}
if strings.TrimSpace(ar.Cover) != "" && !govalidator.IsURL(ar.Cover) {
return e.New(http.StatusBadRequest, e.ParamInvalid)
} }
now := time.Now() now := time.Now()
md := utils.Compress(ar.Md)
if ar.ID == 0 { if ar.ID == 0 {
//create //create
_, err = db.Conn.Exec("INSERT INTO articles (creator, title, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?)", _, err = db.Conn.Exec("INSERT INTO posts (creator, title, md, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?,?)",
user.ID, ar.Title, ar.URL, ar.Cover, ar.Brief, now, now) user.ID, ar.Title, md, ar.URL, ar.Cover, ar.Brief, now, now)
return err if err != nil {
logger.Warn("submit post error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
} else {
_, err = db.Conn.Exec("UPDATE posts SET title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?",
ar.Title, md, ar.URL, ar.Cover, ar.Brief, now, ar.ID)
if err != nil {
logger.Warn("upate post error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
} }
_, err = db.Conn.Exec("UPDATE articles SET title=?, url=?, cover=?, brief=?, updated=? WHERE id=?", return nil
ar.Title, ar.URL, ar.Cover, ar.Brief, now, ar.ID)
return err
} }
func DeleteArticle(c *gin.Context) error { func DeletePost(id int64) *e.Error {
user := session.CurrentUser(c) _, err := db.Conn.Exec("DELETE FROM posts WHERE id=?", id)
if !user.Role.IsEditor() { if err != nil {
return errors.New(errcode.NoEditorPermission) logger.Warn("delete post error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}
func GetPost(id int64) (*models.Post, *e.Error) {
ar := &models.Post{}
var rawmd []byte
err := db.Conn.QueryRow("select id,title,md,url,cover,brief,creator,created,updated from posts where id=?", id).Scan(
&ar.ID, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("get post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
md, _ := utils.Uncompress(rawmd)
ar.Md = string(md)
ar.Creator = &models.UserSimple{ID: ar.CreatorID}
err = ar.Creator.Query()
return ar, nil
}
func GetPostCreator(id int64) (int64, *e.Error) {
var uid int64
err := db.Conn.QueryRow("SELECT creator FROM posts WHERE id=?", id).Scan(&uid)
if err != nil {
if err == sql.ErrNoRows {
return 0, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("get post creator error", "error", err)
return 0, e.New(http.StatusInternalServerError, e.Internal)
} }
id := c.Param("id") return uid, nil
_, err := db.Conn.Exec("DELETE FROM articles WHERE id=?", id)
return err
} }

@ -9,7 +9,7 @@ import (
"github.com/imdotdev/im.dev/server/internal/storage" "github.com/imdotdev/im.dev/server/internal/storage"
"github.com/imdotdev/im.dev/server/pkg/common" "github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/config" "github.com/imdotdev/im.dev/server/pkg/config"
"github.com/imdotdev/im.dev/server/pkg/errcode" "github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/log" "github.com/imdotdev/im.dev/server/pkg/log"
) )
@ -52,10 +52,12 @@ func (s *Server) Start() error {
{ {
editorR := lr.Group("/editor") editorR := lr.Group("/editor")
{ {
editorR.GET("/articles", api.GetEditorArticles) editorR.GET("/posts", api.GetEditorPosts)
editorR.POST("/article", api.PostEditorArticle) editorR.POST("/post", api.SubmitPost)
editorR.DELETE("/article/:id", api.DeleteEditorArticle) editorR.DELETE("/post/:id", api.DeletePost)
editorR.GET("/post/:id", api.GetEditorPost)
} }
} }
err := router.Run(config.Data.Server.Addr) err := router.Run(config.Data.Server.Addr)
if err != nil { if err != nil {
@ -97,7 +99,7 @@ func IsLogin() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
user := session.CurrentUser(c) user := session.CurrentUser(c)
if user == nil { if user == nil {
c.JSON(http.StatusUnauthorized, common.RespError(errcode.NeedLogin)) c.JSON(http.StatusUnauthorized, common.RespError(e.NeedLogin))
c.Abort() c.Abort()
return return
} }

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

@ -13,13 +13,15 @@ type UIConfig struct {
} }
type UIPosts struct { type UIPosts struct {
BriefMaxLen int `json:"briefMaxLen"` BriefMaxLen int `json:"briefMaxLen"`
WritingEnabled bool `json:"writingEnabled"`
} }
func GetUIConfig(c *gin.Context) { func GetUIConfig(c *gin.Context) {
conf := &UIConfig{ conf := &UIConfig{
Posts: &UIPosts{ Posts: &UIPosts{
BriefMaxLen: config.Data.Posts.BriefMaxLen, BriefMaxLen: config.Data.Posts.BriefMaxLen,
WritingEnabled: config.Data.Posts.WritingEnabled,
}, },
} }

@ -1,6 +1,6 @@
package common package common
import "github.com/imdotdev/im.dev/server/pkg/errcode" import "github.com/imdotdev/im.dev/server/pkg/e"
type Resp struct { type Resp struct {
Status string `json:"status"` Status string `json:"status"`
@ -27,7 +27,7 @@ func RespError(msg string) *Resp {
func RespInternalError() *Resp { func RespInternalError() *Resp {
r := &Resp{} r := &Resp{}
r.Status = Error r.Status = Error
r.Message = errcode.Internal r.Message = e.Internal
return r return r
} }

@ -33,7 +33,8 @@ type Config struct {
} }
Posts struct { Posts struct {
BriefMaxLen int `yaml:"brief_max_len"` BriefMaxLen int `yaml:"brief_max_len"`
WritingEnabled bool `yaml:"writing_enabled"`
} }
} }

@ -0,0 +1,23 @@
package e
type Error struct {
Status int
Message string
}
func New(status int, msg string) *Error {
return &Error{
Status: status,
Message: msg,
}
}
const (
DB = "数据库异常"
Internal = "服务器内部错误"
NeedLogin = "你需要登录才能访问该页面"
NoEditorPermission = "只有编辑角色才能执行此操作"
ParamInvalid = "请求参数不正确"
NotFound = "目标不存在"
NoPermission = "你没有权限执行此操作"
)

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

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

@ -38,7 +38,7 @@ type UserSimple struct {
} }
func (user *UserSimple) Query() error { 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( err := db.Conn.QueryRow(`SELECT id,username,nickname,avatar FROM user WHERE id=?`, user.ID).Scan(
&user.ID, &user.Username, &user.Nickname, &user.Avatar, &user.ID, &user.Username, &user.Nickname, &user.Avatar,
) )

@ -0,0 +1,12 @@
package utils
import "github.com/golang/snappy"
func Compress(s string) []byte {
encoded := snappy.Encode(nil, []byte(s))
return encoded
}
func Uncompress(b []byte) ([]byte, error) {
return snappy.Decode(nil, b)
}

@ -1,37 +0,0 @@
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 alignItems="left">
<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,45 @@
import React from 'react';
import 'highlight.js/styles/atom-one-dark.css';
import { chakra,PropsOf } from '@chakra-ui/react';
import dynamic from 'next/dynamic';
import 'react-markdown-editor-lite/lib/index.css';
const MdEditor = dynamic(() => import('react-markdown-editor-lite'), {
ssr: false
});
type Props = PropsOf<typeof chakra.div> & {
md: string
onChange: any
}
export function MarkdownEditor(props) {
function handleEditorChange({html, text}) {
props.onChange(text)
}
return (
<MdEditor
width="100%"
value={props.md}
style={{ height: "102%" }}
renderHTML={_ => null}
onChange={handleEditorChange}
config={{
canView: false,
view:{
menu: true,
md: true,
html: false,
fullScreen: true,
}
}}
/>
);
}

@ -0,0 +1,26 @@
import React, { useRef, useEffect } from 'react';
import Markdown from 'markdown-to-jsx';
import hljs from 'highlight.js';
import 'highlight.js/styles/atom-one-dark.css';
import { chakra,PropsOf } from '@chakra-ui/react';
type Props = PropsOf<typeof chakra.div> & {
md: string
}
export function MarkdownRender({ md,...rest }:Props) {
const rootRef = useRef<HTMLDivElement>();
useEffect(() => {
rootRef.current.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});
}, [md]);
return (
<div ref={rootRef} style={{height:'100%'}}>
<Markdown children={md} {...rest} style={{height:'100%',fontSize: '14px'}}></Markdown>
</div>
);
}

@ -0,0 +1,35 @@
import React from "react"
import {chakra, Heading, VStack, Text, HStack,Button, Flex,PropsOf, Tag } from "@chakra-ui/react"
import { Post } from "src/types/posts"
import moment from 'moment'
type Props = PropsOf<typeof chakra.div> & {
post: Post
showActions: boolean
onEdit?: any
onDelete?: any
}
export const TextPostCard= (props:Props) =>{
const {post,showActions,onEdit,onDelete, ...rest} = props
const gap = moment(post.created).fromNow()
return (
<Flex justifyContent="space-between" {...rest}>
<VStack alignItems="left" as="a" href={post.url ? post.url : `/${post.creator.username}/${post.slug}`}>
<Heading size="sm" display="flex" alignItems="center">
{post.url ? <Tag size="sm" mr="2"></Tag> : <Tag size="sm" mr="2"></Tag>}
{post.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={props.onDelete}>Delete</Button>
</HStack>}
</Flex>
)
}
export default TextPostCard

@ -0,0 +1,35 @@
import { Box, useRadio } from "@chakra-ui/react"
function RadioCard(props) {
const { getInputProps, getCheckboxProps} = useRadio(props)
const input = getInputProps()
const checkbox = getCheckboxProps()
return (
<Box as="label">
<input {...input} />
<Box
{...checkbox}
cursor="pointer"
borderWidth="1px"
borderRadius="md"
boxShadow="md"
_checked={{
bg: props.bg,
color: props.color,
borderColor: props.bg,
}}
_focus={{
boxShadow: "outline",
}}
px={2}
py={1}
>
{props.children}
</Box>
</Box>
)
}
export default RadioCard

@ -3,7 +3,7 @@ import { FaFileAlt, FaScroll, FaBookOpen } from 'react-icons/fa'
import { Route } from 'src/types/route' import { Route } from 'src/types/route'
const editorLinks: Route[] = [{ const editorLinks: Route[] = [{
title: '文章', title: '文章',
path: '/editor/article', path: '/editor/posts',
icon: <FaFileAlt />, icon: <FaFileAlt />,
disabled: false disabled: false
}, },

@ -0,0 +1,4 @@
export enum EditMode {
Edit = '编辑',
Preview = '预览'
}

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

@ -2,7 +2,8 @@ import { requestApi } from "./axios/request"
export let config = { export let config = {
posts: { posts: {
briefMaxLen: 10 briefMaxLen: 10,
writingEnabled: false
} }
} }

@ -1,5 +1,7 @@
import { extendTheme } from "@chakra-ui/react" import { extendTheme } from "@chakra-ui/react"
import { mode } from "@chakra-ui/theme-tools" import { mode } from "@chakra-ui/theme-tools"
import reactMarkdownStyls from 'theme/react-markdown-editor'
import layerStyles from 'theme/layer-styles'
const customTheme = extendTheme({ const customTheme = extendTheme({
config: { config: {
@ -8,20 +10,7 @@ const customTheme = extendTheme({
}, },
//predefined mixin //predefined mixin
layerStyles: { layerStyles: layerStyles(),
textSecondary: {
opacity: "0.8"
},
colorButton: {
bg: "linear-gradient(270deg,#0076f5,#0098a3)",
color: "white",
_hover: {
cursor: 'pointer'
},
_focus: null
}
},
fonts: { fonts: {
heading: "Inter, sans-serif", heading: "Inter, sans-serif",
body: "Inter, sans-serif", body: "Inter, sans-serif",
@ -43,6 +32,7 @@ const customTheme = extendTheme({
fontStyle: "normal !important", fontStyle: "normal !important",
}, },
}, },
...reactMarkdownStyls(props)
}), }),
}, },
textStyles: { textStyles: {

@ -0,0 +1,15 @@
export default function layerStyles(theme) {
return {
textSecondary: {
opacity: "0.8"
},
colorButton: {
bg: "linear-gradient(270deg,#0076f5,#0098a3)",
color: "white",
_hover: {
cursor: 'pointer'
},
_focus: null
}
}
}

@ -0,0 +1,34 @@
import { mode } from "@chakra-ui/theme-tools"
import userCustomTheme from "./user-custom"
export default function reactMarkdownStyles(props) {
return {
'.rc-md-editor': {
borderWidth: '0px',
background: 'transparent',
textarea: {
background: 'transparent!important',
color: mode("#2D3748!important", "rgba(255, 255, 255, 0.92)!important")(props),
},
'.rc-md-navigation' :{
background: 'transparent',
borderBottomColor: mode(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark + '!important')(props),
'.navigation-nav' :{
'.button': {
color: mode("#2D3748!important", "rgba(255, 255, 255, 0.92)!important")(props),
}
}
},
'.drop-wrap' : {
background: mode("white", "#1A202C")(props),
borderWidth: '1px',
borderColor: mode(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark + '!important')(props),
},
'.header-list .list-item': {
_hover: {
background: 'transparent'
}
}
}
}
}

@ -0,0 +1,10 @@
import { extendTheme } from "@chakra-ui/react"
const theme = extendTheme()
const userCustomTheme = {
borderColor: {
light: theme.colors.gray['200'],
dark: theme.colors.whiteAlpha['300']
}
}
export default userCustomTheme

@ -1092,6 +1092,11 @@
dependencies: dependencies:
tslib "^2.0.0" tslib "^2.0.0"
"@types/highlight.js@^9.12.3":
version "9.12.4"
resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.4.tgz#8c3496bd1b50cc04aeefd691140aa571d4dbfa34"
integrity sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==
"@types/lodash.mergewith@4.6.6": "@types/lodash.mergewith@4.6.6":
version "4.6.6" version "4.6.6"
resolved "https://registry.yarnpkg.com/@types/lodash.mergewith/-/lodash.mergewith-4.6.6.tgz#c4698f5b214a433ff35cb2c75ee6ec7f99d79f10" resolved "https://registry.yarnpkg.com/@types/lodash.mergewith/-/lodash.mergewith-4.6.6.tgz#c4698f5b214a433ff35cb2c75ee6ec7f99d79f10"
@ -1652,7 +1657,7 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
inherits "^2.0.1" inherits "^2.0.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
classnames@2.2.6: classnames@2.2.6, classnames@^2.2.6:
version "2.2.6" version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@ -2494,6 +2499,11 @@ hey-listen@^1.0.8:
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
highlight.js@^9.16.2:
version "9.18.5"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825"
integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==
hmac-drbg@^1.0.0: hmac-drbg@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -2843,6 +2853,11 @@ make-dir@^3.0.2:
dependencies: dependencies:
semver "^6.0.0" semver "^6.0.0"
markdown-to-jsx@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.1.1.tgz#269145a585650258691a9a1ec9fb4b273ed2d32a"
integrity sha512-PJgNmvdKM7f7dFDgr4N2ZQv5OuJ2dwwBZvKel61BO7JLh2QQLDs880uQO+OjsEKNmhCZ0ZOIKkCXFEuY9G0yEA==
md5.js@^1.3.4: md5.js@^1.3.4:
version "1.3.5" version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -3507,6 +3522,13 @@ react-is@16.13.1, react-is@^16.7.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-markdown-editor-lite@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/react-markdown-editor-lite/-/react-markdown-editor-lite-1.2.4.tgz#d47ea00f62e7c82ae8803a990d011d4f44015c20"
integrity sha512-zUMDsrRxmjNeZEaVkL2oXlOpSJOmVtI5wwRv71MevyvwA/gZQOE7oJP6dSRe84HMccuoU2v1+FxNWKYJtMdPdw==
dependencies:
classnames "^2.2.6"
react-refresh@0.8.3: react-refresh@0.8.3:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"

Loading…
Cancel
Save