mirror of https://github.com/sunface/rust-course
parent
42db49c060
commit
3a1c406da4
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
package api
|
@ -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))
|
||||||
|
}
|
||||||
|
@ -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 = "请求参数不正确"
|
|
@ -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
|
@ -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
|
||||||
}
|
}
|
@ -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
|
Loading…
Reference in new issue