pull/51/head
sunface 4 years ago
parent 664216ca87
commit 8c2bf9f379

@ -15,7 +15,9 @@ import {
Heading, Heading,
Tag as ChakraTag, Tag as ChakraTag,
TagLabel, TagLabel,
TagCloseButton TagCloseButton,
Spinner,
Text
} from "@chakra-ui/react" } from "@chakra-ui/react"
import { useViewportScroll } from "framer-motion" import { useViewportScroll } from "framer-motion"
import NextLink from "next/link" import NextLink from "next/link"
@ -25,16 +27,18 @@ import Card from "components/card"
import DarkMode from "components/dark-mode" import DarkMode from "components/dark-mode"
import EditModeSelect from "components/edit-mode-select" import EditModeSelect from "components/edit-mode-select"
import Tags from "components/tags/tags" import Tags from "components/tags/tags"
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import { FaCloud } from "react-icons/fa"
interface Props { interface Props {
ar : Post ar : Story
changeTitle: any changeTitle: any
changeEditMode: any changeEditMode: any
publish: any publish: any
onChange:any onChange:any
saved?: boolean
} }
function HeaderContent(props: Props) { function HeaderContent(props: Props) {
@ -64,12 +68,16 @@ function HeaderContent(props: Props) {
<Input width={{base: '100px', md: '187px'}} value={props.ar.title} placeholder="Title..." onChange={props.changeTitle} focusBorderColor={useColorModeValue('teal.400', 'teal.100')} variant="flushed" /> <Input width={{base: '100px', md: '187px'}} value={props.ar.title} placeholder="Title..." onChange={props.changeTitle} focusBorderColor={useColorModeValue('teal.400', 'teal.100')} variant="flushed" />
</Box> </Box>
<EditModeSelect onChange={props.changeEditMode}/> <EditModeSelect onChange={props.changeEditMode}/>
<Box <HStack
color={useColorModeValue("gray.500", "gray.400")} color={useColorModeValue("gray.500", "gray.400")}
spacing={[0,0,2,2]}
> >
{(props.saved !== null )&& <Box>
{!props.saved ? <HStack><Spinner /><Text>Saving</Text></HStack> : <HStack><FaCloud /><Text>Saved</Text></HStack>}
</Box>}
<DarkMode /> <DarkMode />
<Button layerStyle="colorButton" ml="2" onClick={onOpen}></Button> <Button layerStyle="colorButton" ml="2" onClick={onOpen}></Button>
</Box> </HStack>
</Flex> </Flex>
<Drawer <Drawer
isOpen={isOpen} isOpen={isOpen}

@ -20,7 +20,7 @@ import AccountMenu from "components/user-menu"
import { FaGithub, FaTwitter, FaUserPlus } from "react-icons/fa" import { FaGithub, FaTwitter, FaUserPlus } from "react-icons/fa"
import Follow from "components/interaction/follow" import Follow from "components/interaction/follow"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import { getSvgIcon } from "components/svg-icon" import { getSvgIcon } from "components/svg-icon"
import Link from "next/link" import Link from "next/link"
import Logo from "components/logo" import Logo from "components/logo"
@ -28,7 +28,7 @@ import { ReserveUrls } from "src/data/reserve-urls"
interface Props { interface Props {
post: Post post: Story
} }
function PostNav(props: Props) { function PostNav(props: Props) {

@ -1,35 +1,22 @@
import { Box, chakra, Divider, Flex, Heading, HStack, IconButton, Image, storageKey, VStack } from "@chakra-ui/react" import { Box, Divider, Heading, HStack, Image} from "@chakra-ui/react"
import Comments from "components/comments/comments" import Comments from "components/comments/comments"
import Container from "components/container"
import LikeButton from "components/story/unicorn-like"
import { MarkdownRender } from "components/markdown-editor/render" import { MarkdownRender } from "components/markdown-editor/render"
import PostAuthor from "components/story/post-author" import { StoryAuthor } from "components/story/story-author"
import TagTextCard from "components/story/tag-text-card" import TagTextCard from "components/story/tag-text-card"
import SEO from "components/seo" import SEO from "components/seo"
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import useSession from "hooks/use-session"
import Nav from "layouts/nav/nav"
import PostNav from "layouts/nav/post-nav" import PostNav from "layouts/nav/post-nav"
import PageContainer from "layouts/page-container" import PageContainer from "layouts/page-container"
import { cloneDeep } from "lodash"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import { title } from "process"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { FaBookmark, FaGithub, FaRegBookmark, FaShare, FaShareAlt } from "react-icons/fa" import { Story } from "src/types/story"
import { ReserveUrls } from "src/data/reserve-urls"
import { Comment } from "src/types/comments"
import { Post } from "src/types/posts"
import { Tag } from "src/types/tag"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import UnicornLike from "components/story/unicorn-like" import StorySidebar from "components/story/story-sidebar"
import SvgButton from "components/svg-button"
import Bookmark from "components/story/bookmark"
import PostSidebar from "components/story/post-sidebar"
const PostPage = () => { const PostPage = () => {
const router = useRouter() const router = useRouter()
const id = router.query.post_id const id = router.query.post_id
const [post, setPost]: [Post, any] = useState(null) const [post, setPost]: [Story, any] = useState(null)
useEffect(() => { useEffect(() => {
if (id) { if (id) {
getData() getData()
@ -66,7 +53,7 @@ const PostPage = () => {
<Heading size="lg" my="6" lineHeight="1.5">{post.title}</Heading> <Heading size="lg" my="6" lineHeight="1.5">{post.title}</Heading>
<Divider my="4" /> <Divider my="4" />
<PostAuthor post={post} /> <StoryAuthor story={post} />
<Divider my="4" /> <Divider my="4" />
<MarkdownRender md={post.md} py="2" mt="6" /> <MarkdownRender md={post.md} py="2" mt="6" />
@ -76,7 +63,7 @@ const PostPage = () => {
<Box mt="6" p="2"><Comments storyID={post.id} /></Box> <Box mt="6" p="2"><Comments storyID={post.id} /></Box>
</Box> </Box>
<Box pt="16"> <Box pt="16">
<PostSidebar post={post} /> <StorySidebar story={post} />
</Box> </Box>
</HStack> </HStack>

@ -4,21 +4,16 @@ import Container from "components/container"
import SEO from "components/seo" import SEO from "components/seo"
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import useSession from "hooks/use-session" import useSession from "hooks/use-session"
import Nav from "layouts/nav/nav"
import VerticalNav from "layouts/nav/vertical-nav"
import PageContainer from "layouts/page-container"
import PageContainer1 from "layouts/page-container1" import PageContainer1 from "layouts/page-container1"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { FaComment, FaCommentAlt, FaDove, FaEdit, FaFacebook, FaFile, FaGithub, FaHeart, FaPlus, FaRegStar, FaStackOverflow, FaStar, FaTwitter, FaWeibo, FaZhihu } from "react-icons/fa" import {FaFacebook, FaFile, FaGithub, FaHeart, FaPlus, FaRegStar, FaStackOverflow, FaStar, FaTwitter, FaWeibo, FaZhihu } from "react-icons/fa"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import { User } from "src/types/user" import { User } from "src/types/user"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import moment from 'moment' import moment from 'moment'
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import PostCard from "components/story/post-card" import Stories from "components/story/stories"
import userCustomTheme from "theme/user-custom"
import Posts from "components/story/posts"
import Link from "next/link" import Link from "next/link"
import Empty from "components/empty" import Empty from "components/empty"
import Count from "components/count" import Count from "components/count"
@ -29,8 +24,8 @@ const UserPage = () => {
const username = router.query.username const username = router.query.username
const session = useSession() const session = useSession()
const [user, setUser]: [User, any] = useState(null) const [user, setUser]: [User, any] = useState(null)
const [rawPosts, setRawPosts]: [Post[], any] = useState([]) const [rawPosts, setRawPosts]: [Story[], any] = useState([])
const [posts, setPosts]: [Post[], any] = useState([]) const [posts, setPosts]: [Story[], any] = useState([])
const [tags,setTags]:[Tag[],any] = useState([]) const [tags,setTags]:[Tag[],any] = useState([])
const [tagFilter,setTagFilter]:[Tag,any] = useState(null) const [tagFilter,setTagFilter]:[Tag,any] = useState(null)
@ -178,7 +173,7 @@ const UserPage = () => {
</Card> </Card>
: :
<Card width="100%" height="fit-content" p="0" px="3"> <Card width="100%" height="fit-content" p="0" px="3">
<Posts posts={posts} showFooter={tagFilter === null}/> <Stories stories={posts} showFooter={tagFilter === null}/>
</Card> </Card>
} }
</Box> </Box>

@ -7,7 +7,6 @@ import React, { useEffect, useState } from "react"
import {adminLinks} from "src/data/links" import {adminLinks} from "src/data/links"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import TagCard from "components/tags/tag-card" import TagCard from "components/tags/tag-card"
import { Post } from "src/types/posts"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import Link from "next/link" import Link from "next/link"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"

@ -20,8 +20,8 @@ import {
import { Tag } from "src/types/tag" import { Tag } from "src/types/tag"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import TagCard from 'src/components/tags/tag-card' import TagCard from 'src/components/tags/tag-card'
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import Posts from "components/story/posts" import Stories from "components/story/stories"
import { find } from "lodash" import { find } from "lodash"
import userCustomTheme from "theme/user-custom" import userCustomTheme from "theme/user-custom"
import Empty from "components/empty" import Empty from "components/empty"
@ -31,8 +31,8 @@ import Empty from "components/empty"
const BookmarksPage = () => { const BookmarksPage = () => {
const [filter, setFilter]:[Tag,any] = useState(null) const [filter, setFilter]:[Tag,any] = useState(null)
const [tags, setTags]: [Tag[], any] = useState([]) const [tags, setTags]: [Tag[], any] = useState([])
const [rawPosts,setRawPosts]: [Post[],any] = useState([]) const [rawPosts,setRawPosts]: [Story[],any] = useState([])
const [posts,setPosts]: [Post[],any] = useState([]) const [posts,setPosts]: [Story[],any] = useState([])
useEffect(() => { useEffect(() => {
getBookmarkPosts() getBookmarkPosts()
@ -107,7 +107,7 @@ import Empty from "components/empty"
<Divider mt="3" mb="5" /> <Divider mt="3" mb="5" />
{posts.length !== 0 {posts.length !== 0
? ?
<Posts posts={posts} showFooter={false}/> <Stories stories={posts} showFooter={false}/>
: :
<Empty /> <Empty />
} }

@ -0,0 +1,72 @@
import { Text, Box, Heading, Image, Center, Flex, VStack, Divider, useToast } from "@chakra-ui/react"
import Card from "components/card"
import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react"
import {editorLinks} from "src/data/links"
import { requestApi } from "utils/axios/request"
import { Story } from "src/types/story"
import { useRouter } from "next/router"
import PageContainer1 from "layouts/page-container1"
import Empty from "components/empty"
import TextStoryCard from "components/story/text-story-card"
const PostsPage = () => {
const [posts, setPosts] = useState([])
const router = useRouter()
const toast = useToast()
const getPosts = () => {
requestApi.get(`/story/posts/drafts`).then((res) => setPosts(res.data)).catch(_ => setPosts([]))
}
useEffect(() => {
getPosts()
}, [])
const editPost = (post: Story) => {
router.push(`/editor/post/${post.id}`)
}
const onDeletePost= async (id) => {
await requestApi.delete(`/story/post/${id}`)
getPosts()
toast({
description: "删除成功",
status: "success",
duration: 2000,
isClosable: true,
})
}
return (
<>
<PageContainer1 >
<Box display="flex">
<Sidebar routes={editorLinks} title="创作中心"/>
<Card ml="4" p="6" width="100%">
<Flex alignItems="center" justify="space-between">
<Heading size="md">稿({posts.length})</Heading>
</Flex>
{
posts.length === 0 ?
<Empty />
:
<>
<VStack mt="4">
{posts.map(post =>
<Box width="100%" key={post.id}>
<TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editPost(post)} onDelete={() => onDeletePost(post.id)} showSource={false}/>
<Divider mt="5" />
</Box>
)}
</VStack>
<Center><Text layerStyle="textSecondary" fontSize="sm" mt="5"></Text></Center>
</>
}
</Card>
</Box>
</PageContainer1>
</>
)
}
export default PostsPage

@ -1,28 +1,33 @@
import { Box, Button, useToast } from '@chakra-ui/react'; import { Box, Button, Text, useToast } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { MarkdownEditor } from 'components/markdown-editor/editor'; import { MarkdownEditor } from 'components/markdown-editor/editor';
import PageContainer from 'layouts/page-container'; import PageContainer from 'layouts/page-container';
import EditorNav from 'layouts/nav/editor-nav' import EditorNav from 'layouts/nav/editor-nav'
import { EditMode } from 'src/types/editor'; import { EditMode } from 'src/types/editor';
import { MarkdownRender } from 'components/markdown-editor/render'; import { MarkdownRender } from 'components/markdown-editor/render';
import { Post } from 'src/types/posts'; import { Story, StoryStatus } from 'src/types/story';
import { requestApi } from 'utils/axios/request'; import { requestApi } from 'utils/axios/request';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { config } from 'configs/config'; import { config } from 'configs/config';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import Card from 'components/card'; import Card from 'components/card';
import { updateUrl } from 'utils/url';
import { IDType } from 'src/types/id';
const content = `
# test
` let saveDraftHandler = undefined;
function PostEditPage() { function PostEditPage() {
const router = useRouter() const router = useRouter()
const { id } = router.query const { id } = router.query
const [editMode, setEditMode] = useState(EditMode.Edit) const [editMode, setEditMode] = useState(EditMode.Edit)
const [ar, setAr] = useState({ const [saved,setSaved] = useState(null)
md: content, const [ar, setAr]:[Story,any] = useState({
title: '' type: IDType.Post,
md: '',
title: '',
status: StoryStatus.Draft
}) })
const toast = useToast() const toast = useToast()
@ -33,14 +38,50 @@ function PostEditPage() {
}, [id]) }, [id])
const onMdChange = newMd => { const onMdChange = newMd => {
setAr({ const newAr = {
...ar, ...ar,
md: newMd md: newMd
}) }
setAr(newAr)
if (ar.status === StoryStatus.Draft) {
onSaveDraft(newAr)
}
}
const onSaveDraft = (post?) => {
if (saveDraftHandler === undefined) {
// 没有任何保存动作,开始保存
saveDraftHandler = setTimeout(() => saveDraft(post),2000)
return
} else if (saveDraftHandler !== null) {
// 不在保存过程中,连续输入, 取消之前的定时器重新设置handler
clearTimeout(saveDraftHandler)
saveDraftHandler = setTimeout(() => saveDraft(post),2000)
return
}
}
const saveDraft = async (post?) => {
saveDraftHandler = null
setSaved(false)
const res = await requestApi.post(`/story/post/draft`, post??ar)
setSaved(true)
saveDraftHandler = undefined
if (!ar.id) {
ar.id = res.data.id
let url = window.location.origin + `/editor/post/${ar.id}`
window.history.pushState({},null,url);
}
} }
const onChange = () => { const onChange = () => {
setAr(cloneDeep(ar)) const newAr = cloneDeep(ar)
if (ar.status === StoryStatus.Draft) {
onSaveDraft(newAr)
}
setAr(newAr)
} }
const onChangeTitle = title => { const onChangeTitle = title => {
@ -54,11 +95,25 @@ function PostEditPage() {
return return
} }
setAr({ ...ar, title: title }) const newAr = { ...ar, title: title }
if (ar.status === StoryStatus.Draft) {
onSaveDraft(newAr)
}
setAr(newAr)
} }
const publish = async () => { const publish = async () => {
const res = await requestApi.post(`/story/post`, ar) if (ar.tags?.length === 0) {
toast({
description: "请设置文章标签",
status: "error",
duration: 3000,
isClosable: true,
})
return
}
const res = await requestApi.post(`/story`, ar)
toast({ toast({
description: "发布成功", description: "发布成功",
status: "success", status: "success",
@ -76,6 +131,7 @@ function PostEditPage() {
changeEditMode={(v) => setEditMode(v)} changeEditMode={(v) => setEditMode(v)}
changeTitle={(e) => onChangeTitle(e.target.value)} changeTitle={(e) => onChangeTitle(e.target.value)}
publish={() => publish()} publish={() => publish()}
saved={saved}
/>} />}
> >
{editMode === EditMode.Edit ? {editMode === EditMode.Edit ?

@ -1,7 +1,5 @@
import { Menu,MenuButton,MenuList,MenuItem, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue, useToast } from "@chakra-ui/react" import { Menu,MenuButton,MenuList,MenuItem, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue, useToast } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container"
import Sidebar from "layouts/sidebar/sidebar" import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import {editorLinks} from "src/data/links" import {editorLinks} from "src/data/links"
@ -9,16 +7,18 @@ 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 "configs/config" import { config } from "configs/config"
import TextPostCard from "components/story/text-post-card" import TextStoryCard from "components/story/text-story-card"
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import { FaExternalLinkAlt, FaRegEdit } from "react-icons/fa" import { FaExternalLinkAlt, FaRegEdit } from "react-icons/fa"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import Link from "next/link" import Link from "next/link"
import PageContainer1 from "layouts/page-container1" import PageContainer1 from "layouts/page-container1"
import Empty from "components/empty"
import { IDType } from "src/types/id"
var validator = require('validator'); var validator = require('validator');
const newPost: Post = { title: '', url: '', cover: '' } const newPost: Story = {type: IDType.Post,title: '', url: '', cover: '' }
const PostsPage = () => { const PostsPage = () => {
const [currentPost, setCurrentPost] = useState(newPost) const [currentPost, setCurrentPost] = useState(newPost)
const [posts, setPosts] = useState([]) const [posts, setPosts] = useState([])
@ -26,7 +26,7 @@ const PostsPage = () => {
const router = useRouter() const router = useRouter()
const toast = useToast() const toast = useToast()
const getPosts = () => { const getPosts = () => {
requestApi.get(`/story/posts/editor`).then((res) => setPosts(res.data)).catch(_ => setPosts([])) requestApi.get(`/story/posts/editor?type=${IDType.Post}`).then((res) => setPosts(res.data)).catch(_ => setPosts([]))
} }
useEffect(() => { useEffect(() => {
@ -64,7 +64,7 @@ const PostsPage = () => {
} }
const submitPost = async (values, _) => { const submitPost = async (values, _) => {
await requestApi.post(`/story/post`, values) await requestApi.post(`/story`, values)
onClose() onClose()
toast({ toast({
description: "提交成功", description: "提交成功",
@ -76,7 +76,7 @@ const PostsPage = () => {
getPosts() getPosts()
} }
const editPost = (post: Post) => { const editPost = (post: Story) => {
if (post.url.trim() === "") { if (post.url.trim() === "") {
router.push(`/editor/post/${post.id}`) router.push(`/editor/post/${post.id}`)
} else { } else {
@ -119,20 +119,13 @@ const PostsPage = () => {
</Flex> </Flex>
{ {
posts.length === 0 ? posts.length === 0 ?
<> <Empty />
<Center mt="4">
<Image height="25rem" src="/empty-posts.png" />
</Center>
<Center mt="8">
<Heading size="sm"></Heading>
</Center>
</>
: :
<> <>
<VStack mt="4"> <VStack mt="4">
{posts.map(post => {posts.map(post =>
<Box width="100%" key={post.id}> <Box width="100%" key={post.id}>
<TextPostCard post={post} showActions={true} mt="4" onEdit={() => editPost(post)} onDelete={() => onDeletePost(post.id)} /> <TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editPost(post)} onDelete={() => onDeletePost(post.id)} />
<Divider mt="5" /> <Divider mt="5" />
</Box> </Box>
)} )}

@ -0,0 +1,180 @@
import { Menu, MenuButton, MenuList, MenuItem, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue, useToast } from "@chakra-ui/react"
import Card from "components/card"
import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react"
import { editorLinks } from "src/data/links"
import { requestApi } from "utils/axios/request"
import { useDisclosure } from "@chakra-ui/react"
import { Field, Form, Formik } from "formik"
import { config } from "configs/config"
import TextStoryCard from "components/story/text-story-card"
import { Story } from "src/types/story"
import { FaExternalLinkAlt, FaRegEdit } from "react-icons/fa"
import { useRouter } from "next/router"
import { ReserveUrls } from "src/data/reserve-urls"
import Link from "next/link"
import PageContainer1 from "layouts/page-container1"
import Empty from "components/empty"
import { IDType } from "src/types/id"
var validator = require('validator');
const newSeries: Story = { title: '', brief: '', cover: '',type: IDType.Series }
const PostsPage = () => {
const [currentSeries, setCurrentSeries] = useState(null)
const [posts, setPosts] = useState([])
const router = useRouter()
const toast = useToast()
const getPosts = () => {
requestApi.get(`/story/posts/editor?type=${IDType.Series}`).then((res) => setPosts(res.data)).catch(_ => setPosts([]))
}
useEffect(() => {
getPosts()
}, [])
function validateTitle(value) {
let error
if (!value?.trim()) {
error = "标题不能为空"
}
if (value?.length > config.posts.titleMaxLen) {
error = "标题长度不能超过128"
}
return error
}
function validateUrl(value) {
let error
if (value && !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 submitPost = async (values, _) => {
await requestApi.post(`/story`, values)
toast({
description: "提交成功",
status: "success",
duration: 2000,
isClosable: true,
})
setCurrentSeries(null)
getPosts()
}
const editPost = (post: Story) => {
if (post.url.trim() === "") {
router.push(`/editor/post/${post.id}`)
} else {
setCurrentSeries(post)
}
}
const onDeletePost = async (id) => {
await requestApi.delete(`/story/post/${id}`)
getPosts()
toast({
description: "删除成功",
status: "success",
duration: 2000,
isClosable: true,
})
}
return (
<>
<PageContainer1 >
<Box display="flex">
<Sidebar routes={editorLinks} title="创作中心" />
<Card ml="4" p="6" width="100%">
{currentSeries ?
<Formik
initialValues={currentSeries}
onSubmit={submitPost}
>
{(props) => (
<Form>
<VStack spacing="6">
<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="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={() => setCurrentSeries(null)}></Button>
</Box>
</Form>
)}
</Formik> :
<>
<Flex alignItems="center" justify="space-between">
<Heading size="md">({posts.length})</Heading>
<Button colorScheme="teal" size="sm" onClick={() => setCurrentSeries(newSeries)} _focus={null}></Button>
</Flex>
{
posts.length === 0 ? <Empty />
:
<>
<VStack mt="4">
{posts.map(post =>
<Box width="100%" key={post.id}>
<TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editPost(post)} onDelete={() => onDeletePost(post.id)} showSource={false}/>
<Divider mt="5" />
</Box>
)}
</VStack>
<Center><Text layerStyle="textSecondary" fontSize="sm" mt="5"></Text></Center>
</>
}
</>}
</Card>
</Box>
</PageContainer1>
</>
)
}
export default PostsPage

@ -9,8 +9,8 @@ import {
Divider Divider
} from "@chakra-ui/react" } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Posts from "components/story/posts" import Stories from "components/story/stories"
import SimplePostCard from "components/story/simple-post-card" import SimplePostCard from "components/story/simple-story-card"
import SEO from "components/seo" import SEO from "components/seo"
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import PageContainer1 from "layouts/page-container1" import PageContainer1 from "layouts/page-container1"
@ -69,7 +69,7 @@ const HomePage = () => {
</Flex> </Flex>
</Card> </Card>
<Card width="100%" height="fit-content" p="0"> <Card width="100%" height="fit-content" p="0">
<Posts posts={posts} /> <Stories stories={posts} />
</Card> </Card>
</VStack> </VStack>
<HomeSidebar /> <HomeSidebar />
@ -106,7 +106,7 @@ export const HomeSidebar = () => {
</Flex> </Flex>
<Divider /> <Divider />
<VStack px="4" pt="3" alignItems="left"> <VStack px="4" pt="3" alignItems="left">
<Posts posts={posts} card={SimplePostCard} size="sm" showFooter={false}></Posts> <Stories stories={posts} card={SimplePostCard} size="sm" showFooter={false}/>
</VStack> </VStack>
</Card> </Card>
</VStack> </VStack>

@ -2,7 +2,7 @@ import { Box, Divider, Flex, HStack, Input } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Empty from "components/empty" import Empty from "components/empty"
import SEO from "components/seo" import SEO from "components/seo"
import Posts from "components/story/posts" import Stories from "components/story/stories"
import SearchFilters from "components/search-filters" import SearchFilters from "components/search-filters"
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import PageContainer1 from "layouts/page-container1" import PageContainer1 from "layouts/page-container1"
@ -88,7 +88,7 @@ const PostsSearchPage = () => {
<Divider mt="3"/> <Divider mt="3"/>
{results.length === 0 && <Empty /> } {results.length === 0 && <Empty /> }
{results.length > 0 && {results.length > 0 &&
<Posts posts={results} showFooter={false} type="compact" highlight={query}/>} <Stories stories={results} showFooter={false} type="compact" highlight={query}/>}
</Card> </Card>
</Box> </Box>
</Flex> </Flex>

@ -2,7 +2,7 @@ import { Box, Divider, Flex, HStack, Input } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Empty from "components/empty" import Empty from "components/empty"
import SEO from "components/seo" import SEO from "components/seo"
import Posts from "components/story/posts" import Posts from "components/story/stories"
import SearchFilters from "components/search-filters" import SearchFilters from "components/search-filters"
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import PageContainer1 from "layouts/page-container1" import PageContainer1 from "layouts/page-container1"
@ -14,7 +14,7 @@ import { SearchFilter } from "src/types/search"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import { addParamToUrl, removeParamFromUrl } from "utils/url" import { addParamToUrl, removeParamFromUrl } from "utils/url"
import PostAuthor from "components/story/post-author" import PostAuthor from "components/story/story-author"
import UserCard from "components/users/user-card" import UserCard from "components/users/user-card"
import Users from "components/users/users" import Users from "components/users/users"

@ -0,0 +1,79 @@
import { Box, Divider, Heading, HStack, Image} from "@chakra-ui/react"
import Comments from "components/comments/comments"
import { MarkdownRender } from "components/markdown-editor/render"
import { StoryAuthor } from "components/story/story-author"
import TagTextCard from "components/story/tag-text-card"
import SEO from "components/seo"
import siteConfig from "configs/site-config"
import PostNav from "layouts/nav/post-nav"
import PageContainer from "layouts/page-container"
import { useRouter } from "next/router"
import React, { useEffect, useState } from "react"
import { Story } from "src/types/story"
import { requestApi } from "utils/axios/request"
import StorySidebar from "components/story/story-sidebar"
const PostPage = () => {
const router = useRouter()
const id = router.query.id
const [post, setPost]: [Story, any] = useState(null)
useEffect(() => {
if (id) {
getData()
}
}, [id])
useEffect(() => {
if (router && router.asPath.indexOf("#comments") > -1) {
setTimeout(() => {
location.href = "#comments"
}, 100)
}
}, [router])
const getData = async () => {
const res = await requestApi.get(`/story/post/${id}`)
setPost(res.data)
}
return (
<>
<SEO
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
{post && <PageContainer nav={<PostNav post={post} />} mt="2rem">
<>
<HStack alignItems="top" spacing={[0, 0, 14, 14]}>
<Box width={["100%", "100%", "75%", "75%"]} height="fit-content" pl={[0, 0, "0%", "10%"]}>
<Image src={post.cover} />
<Box px="2">
<Heading size="lg" my="6" lineHeight="1.5">{post.title}</Heading>
<Divider my="4" />
<StoryAuthor story={post} />
<Divider my="4" />
<MarkdownRender md={post.md} py="2" mt="6" />
</Box>
<HStack ml="2" spacing="3" mt="4">{post.rawTags.map(tag => <TagTextCard key={tag.id} tag={tag} />)}</HStack>
<Box mt="6" p="2"><Comments storyID={post.id} /></Box>
</Box>
<Box pt="16">
<StorySidebar story={post} />
</Box>
</HStack>
</>
</PageContainer>
}
</>
)
}
export default PostPage

@ -6,15 +6,8 @@ import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { adminLinks, settingLinks } from "src/data/links" import { adminLinks, settingLinks } from "src/data/links"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import TagCard from "components/tags/tag-card"
import { Post } from "src/types/posts"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import Link from "next/link"
import { ReserveUrls } from "src/data/reserve-urls"
import { Tag } from "src/types/tag"
import { route } from "next/dist/next-server/server/router"
import { Field, Form, Formik } from "formik" import { Field, Form, Formik } from "formik"
import useSession from "hooks/use-session"
import { config } from "configs/config" import { config } from "configs/config"
import Tags from "components/tags/tags" import Tags from "components/tags/tags"
var validator = require('validator'); var validator = require('validator');

@ -1,19 +1,16 @@
import { Box, Button, chakra, Flex, Heading, HStack, Image, Text, VStack } from "@chakra-ui/react" import { Box, Button, Flex, Heading, HStack, Image, Text, VStack } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Container from "components/container"
import Empty from "components/empty" import Empty from "components/empty"
import { MarkdownRender } from "components/markdown-editor/render" import { MarkdownRender } from "components/markdown-editor/render"
import Posts from "components/story/posts" import Stories from "components/story/stories"
import SEO from "components/seo" import SEO from "components/seo"
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import useSession from "hooks/use-session" import useSession from "hooks/use-session"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container"
import PageContainer1 from "layouts/page-container1" import PageContainer1 from "layouts/page-container1"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import { Tag } from "src/types/tag" import { Tag } from "src/types/tag"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import { isAdmin } from "utils/role" import { isAdmin } from "utils/role"
@ -23,7 +20,7 @@ import Count from "components/count"
const UserPage = () => { const UserPage = () => {
const router = useRouter() const router = useRouter()
const [posts, setPosts]: [Post[], any] = useState([]) const [posts, setPosts]: [Story[], any] = useState([])
const [tag, setTag]: [Tag, any] = useState(null) const [tag, setTag]: [Tag, any] = useState(null)
const [followed, setFollowed] = useState(null) const [followed, setFollowed] = useState(null)
@ -81,7 +78,7 @@ const UserPage = () => {
</Card> </Card>
: :
<Card width="100%" height="fit-content" p="0" px="3"> <Card width="100%" height="fit-content" p="0" px="3">
<Posts posts={posts} /> <Stories stories={posts} />
</Card> </Card>
} }
</VStack> </VStack>

@ -7,11 +7,29 @@ import (
"github.com/imdotdev/im.dev/server/internal/story" "github.com/imdotdev/im.dev/server/internal/story"
"github.com/imdotdev/im.dev/server/internal/user" "github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common" "github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
) )
func GetEditorPosts(c *gin.Context) { func GetEditorPosts(c *gin.Context) {
user := user.CurrentUser(c) user := user.CurrentUser(c)
ars, err := story.UserPosts(user, user.ID) tp := c.Query("type")
if !models.ValidStoryIDType(tp) {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
ars, err := story.UserPosts(tp, user, user.ID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(ars))
}
func GetEditorDrafts(c *gin.Context) {
user := user.CurrentUser(c)
ars, err := story.UserDrafts(nil, user.ID)
if err != nil { if err != nil {
c.JSON(err.Status, common.RespError(err.Message)) c.JSON(err.Status, common.RespError(err.Message))
return return
@ -25,7 +43,7 @@ func GetUserPosts(c *gin.Context) {
user := user.CurrentUser(c) user := user.CurrentUser(c)
posts, err := story.UserPosts(user, userID) posts, err := story.UserPosts(models.IDTypeUndefined, user, userID)
if err != nil { if err != nil {
c.JSON(err.Status, common.RespError(err.Message)) c.JSON(err.Status, common.RespError(err.Message))
return return

@ -11,8 +11,18 @@ import (
"github.com/imdotdev/im.dev/server/pkg/e" "github.com/imdotdev/im.dev/server/pkg/e"
) )
func SubmitPost(c *gin.Context) { func SubmitStory(c *gin.Context) {
res, err := story.SubmitPost(c) res, err := story.SubmitStory(c)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(res))
}
func SubmitPostDraft(c *gin.Context) {
res, err := story.SubmitPostDraft(c)
if err != nil { if err != nil {
c.JSON(err.Status, common.RespError(err.Message)) c.JSON(err.Status, common.RespError(err.Message))
return return
@ -48,11 +58,11 @@ func DeletePost(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil)) c.JSON(http.StatusOK, common.RespSuccess(nil))
} }
func GetStoryPost(c *gin.Context) { func GetStory(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
user := user.CurrentUser(c) user := user.CurrentUser(c)
ar, err := story.GetPost(id, "") ar, err := story.GetStory(id, "")
if err != nil { if err != nil {
c.JSON(err.Status, common.RespError(err.Message)) c.JSON(err.Status, common.RespError(err.Message))
return return

@ -13,6 +13,7 @@ var logger = log.RootLogger.New("logger", "cache")
var Users []*models.User var Users []*models.User
func Init() { func Init() {
time.Sleep(10 * time.Second)
for { for {
// load users // load users
rows, err := db.Conn.Query(`SELECT id,username,role,nickname,email,avatar,last_seen_at,created FROM user`) rows, err := db.Conn.Query(`SELECT id,username,role,nickname,email,avatar,last_seen_at,created FROM user`)

@ -14,14 +14,14 @@ import (
var logger = log.RootLogger.New("logger", "search") var logger = log.RootLogger.New("logger", "search")
func Posts(user *models.User, filter, query string) []*models.Post { func Posts(user *models.User, filter, query string) []*models.Story {
posts := make([]*models.Post, 0) posts := make([]*models.Story, 0)
// postsMap := make(map[string]*models.Post) // postsMap := make(map[string]*models.Post)
// search by title // search by title
sqlq := "%" + query + "%" sqlq := "%" + query + "%"
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,creator,created,updated from posts where title LIKE ? or brief LIKE ?", sqlq, sqlq) rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,creator,created,updated from story where status=? and (title LIKE ? or brief LIKE ?)", models.StatusPublished, sqlq, sqlq)
if err != nil { if err != nil {
logger.Warn("get user posts error", "error", err) logger.Warn("get user posts error", "error", err)
return posts return posts
@ -30,9 +30,9 @@ func Posts(user *models.User, filter, query string) []*models.Post {
posts = story.GetPosts(user, rows) posts = story.GetPosts(user, rows)
if filter == models.FilterFavorites { if filter == models.FilterFavorites {
sort.Sort(models.FavorPosts(posts)) sort.Sort(models.FavorStories(posts))
} else { } else {
sort.Sort(models.Posts(posts)) sort.Sort(models.Stories(posts))
} }
return posts return posts

@ -45,13 +45,15 @@ func (s *Server) Start() error {
r := router.Group("/api") r := router.Group("/api")
//story apis //story apis
r.GET("/story/post/:id", api.GetStoryPost) r.GET("/story/post/:id", api.GetStory)
r.GET("/story/comments/:id", api.GetStoryComments) r.GET("/story/comments/:id", api.GetStoryComments)
r.POST("/story/comment", IsLogin(), api.SubmitComment) r.POST("/story/comment", IsLogin(), api.SubmitComment)
r.DELETE("/story/comment/:id", IsLogin(), api.DeleteStoryComment) r.DELETE("/story/comment/:id", IsLogin(), api.DeleteStoryComment)
r.GET("/story/posts/editor", IsLogin(), api.GetEditorPosts) r.GET("/story/posts/editor", IsLogin(), api.GetEditorPosts)
r.GET("/story/posts/drafts", IsLogin(), api.GetEditorDrafts)
r.GET("/story/posts/home/:filter", api.GetHomePosts) r.GET("/story/posts/home/:filter", api.GetHomePosts)
r.POST("/story/post", IsLogin(), api.SubmitPost) r.POST("/story", IsLogin(), api.SubmitStory)
r.POST("/story/post/draft", IsLogin(), api.SubmitPostDraft)
r.DELETE("/story/post/:id", IsLogin(), api.DeletePost) r.DELETE("/story/post/:id", IsLogin(), api.DeletePost)
r.POST("/story/bookmark/:storyID", IsLogin(), api.Bookmark) r.POST("/story/bookmark/:storyID", IsLogin(), api.Bookmark)
r.GET("/story/bookmark/posts", IsLogin(), api.GetBookmarkPosts) r.GET("/story/bookmark/posts", IsLogin(), api.GetBookmarkPosts)

@ -28,7 +28,7 @@ func Init() error {
return err return err
} }
if err == sql.ErrNoRows { if err != nil {
log.RootLogger.Info("Database tables have not been created, start creating") log.RootLogger.Info("Database tables have not been created, start creating")
err = initTables() err = initTables()
if err != nil { if err != nil {

@ -48,12 +48,13 @@ var sqlTables = map[string]string{
); );
`, `,
"posts": `CREATE TABLE IF NOT EXISTS posts ( "story": `CREATE TABLE IF NOT EXISTS story (
id VARCHAR(255) PRIMARY KEY, id VARCHAR(255) PRIMARY KEY,
type VARCHAR(1) NOT NULL,
creator VARCHAR(255) NOT NULL, creator VARCHAR(255) NOT NULL,
slug VARCHAR(64) NOT NULL, slug VARCHAR(64) DEFAULT '',
title VARCHAR(255) NOT NULL, title VARCHAR(255) DEFAULT '',
md TEXT, md TEXT DEFAULT '',
url VARCHAR(255), url VARCHAR(255),
cover VARCHAR(255), cover VARCHAR(255),
brief TEXT, brief TEXT,
@ -61,10 +62,12 @@ var sqlTables = map[string]string{
created DATETIME NOT NULL, created DATETIME NOT NULL,
updated DATETIME updated DATETIME
); );
CREATE INDEX IF NOT EXISTS posts_creator CREATE INDEX IF NOT EXISTS story_type
ON posts (creator); ON story (type);
CREATE INDEX IF NOT EXISTS posts_created CREATE INDEX IF NOT EXISTS story_creator
ON posts (created); ON story (creator);
CREATE INDEX IF NOT EXISTS story_created
ON story (created);
`, `,
"likes": `CREATE TABLE IF NOT EXISTS likes ( "likes": `CREATE TABLE IF NOT EXISTS likes (
@ -136,14 +139,14 @@ var sqlTables = map[string]string{
"comments": `CREATE TABLE IF NOT EXISTS comments ( "comments": `CREATE TABLE IF NOT EXISTS comments (
id VARCHAR(255) PRIMARY KEY, id VARCHAR(255) PRIMARY KEY,
target_id VARCHAR(255), story_id VARCHAR(255),
creator VARCHAR(255), creator VARCHAR(255),
MD TEXT, MD TEXT,
created DATETIME NOT NULL, created DATETIME NOT NULL,
updated DATETIME updated DATETIME
); );
CREATE INDEX IF NOT EXISTS comments_targetid CREATE INDEX IF NOT EXISTS comments_storyid
ON comments (target_id); ON comments (story_id);
CREATE INDEX IF NOT EXISTS comments_creator CREATE INDEX IF NOT EXISTS comments_creator
ON comments (creator); ON comments (creator);
`, `,

@ -18,7 +18,7 @@ func AddComment(c *models.Comment) *e.Error {
md := utils.Compress(c.Md) md := utils.Compress(c.Md)
now := time.Now() now := time.Now()
_, err := db.Conn.Exec("INSERT INTO comments (id,target_id,creator,md,created,updated) VALUES(?,?,?,?,?,?)", _, err := db.Conn.Exec("INSERT INTO comments (id,story_id,creator,md,created,updated) VALUES(?,?,?,?,?,?)",
c.ID, c.TargetID, c.CreatorID, md, now, now) c.ID, c.TargetID, c.CreatorID, md, now, now)
if err != nil { if err != nil {
logger.Warn("add comment error", "error", err) logger.Warn("add comment error", "error", err)
@ -28,7 +28,7 @@ func AddComment(c *models.Comment) *e.Error {
// 更新story的comment数量 // 更新story的comment数量
// 查询到该comment所属的story id // 查询到该comment所属的story id
var storyID string var storyID string
err = db.Conn.QueryRow("select target_id from comments where id=?", c.TargetID).Scan(&storyID) err = db.Conn.QueryRow("select story_id from comments where id=?", c.TargetID).Scan(&storyID)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
logger.Warn("select comment error", "error", err) logger.Warn("select comment error", "error", err)
} else { } else {
@ -74,7 +74,7 @@ func EditComment(c *models.Comment) *e.Error {
func GetComments(storyID string, sorter string) ([]*models.Comment, *e.Error) { func GetComments(storyID string, sorter string) ([]*models.Comment, *e.Error) {
comments := make([]*models.Comment, 0) comments := make([]*models.Comment, 0)
rows, err := db.Conn.Query("SELECT id,target_id,creator,md,created,updated FROM comments WHERE target_id=?", storyID) rows, err := db.Conn.Query("SELECT id,story_id,creator,md,created,updated FROM comments WHERE story_id=?", storyID)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
logger.Warn("get comments error", "error", err) logger.Warn("get comments error", "error", err)
return comments, e.New(http.StatusInternalServerError, e.Internal) return comments, e.New(http.StatusInternalServerError, e.Internal)
@ -112,7 +112,7 @@ func GetComments(storyID string, sorter string) ([]*models.Comment, *e.Error) {
func GetComment(id string) (*models.Comment, *e.Error) { func GetComment(id string) (*models.Comment, *e.Error) {
c := &models.Comment{} c := &models.Comment{}
var rawMd []byte var rawMd []byte
err := db.Conn.QueryRow("SELECT id,target_id,creator,md,created,updated FROM comments WHERE id=?", id).Scan( err := db.Conn.QueryRow("SELECT id,story_id,creator,md,created,updated FROM comments WHERE id=?", id).Scan(
&c.ID, &c.TargetID, &c.CreatorID, &rawMd, &c.Created, &c.Updated, &c.ID, &c.TargetID, &c.CreatorID, &rawMd, &c.Created, &c.Updated,
) )
if err != nil { if err != nil {
@ -139,7 +139,7 @@ func DeleteComment(id string) *e.Error {
count := 0 count := 0
if isComment { if isComment {
// 如果是评论我们要计算replies的数量因为会一起删除 // 如果是评论我们要计算replies的数量因为会一起删除
err := db.Conn.QueryRow("select count(*) from comments where target_id=?", id).Scan(&count) err := db.Conn.QueryRow("select count(*) from comments where story_id=?", id).Scan(&count)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
logger.Warn("select comment error", "error", err) logger.Warn("select comment error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal) return e.New(http.StatusInternalServerError, e.Internal)
@ -161,7 +161,7 @@ func DeleteComment(id string) *e.Error {
} }
// delete children replies // delete children replies
_, err = tx.Exec("DELETE FROM comments WHERE target_id=?", id) _, err = tx.Exec("DELETE FROM comments WHERE story_id=?", id)
if err != nil { if err != nil {
logger.Warn("delete comment replies error", "error", err) logger.Warn("delete comment replies error", "error", err)
tx.Rollback() tx.Rollback()
@ -192,7 +192,7 @@ func GetCommentCount(storyID string) int {
func GetStoryIDByCommentID(cid string) (string, bool, error) { func GetStoryIDByCommentID(cid string) (string, bool, error) {
var targetID string var targetID string
err := db.Conn.QueryRow("select target_id from comments where id=?", cid).Scan(&targetID) err := db.Conn.QueryRow("select story_id from comments where id=?", cid).Scan(&targetID)
if err != nil { if err != nil {
return "", false, err return "", false, err
} }
@ -202,7 +202,7 @@ func GetStoryIDByCommentID(cid string) (string, bool, error) {
return targetID, true, nil return targetID, true, nil
case models.IDTypeComment: case models.IDTypeComment:
var nid string var nid string
err := db.Conn.QueryRow("select target_id from comments where id=?", targetID).Scan(&nid) err := db.Conn.QueryRow("select story_id from comments where id=?", targetID).Scan(&nid)
if err != nil { if err != nil {
return "", false, err return "", false, err
} }

@ -20,15 +20,19 @@ import (
"github.com/imdotdev/im.dev/server/pkg/utils" "github.com/imdotdev/im.dev/server/pkg/utils"
) )
func SubmitPost(c *gin.Context) (map[string]string, *e.Error) { func SubmitStory(c *gin.Context) (map[string]string, *e.Error) {
user := user.CurrentUser(c) user := user.CurrentUser(c)
post := &models.Post{} post := &models.Story{}
err := c.Bind(&post) err := c.Bind(&post)
if err != nil { if err != nil {
return nil, e.New(http.StatusBadRequest, e.ParamInvalid) return nil, e.New(http.StatusBadRequest, e.ParamInvalid)
} }
if !models.ValidStoryIDType(post.Type) {
return nil, e.New(http.StatusBadRequest, e.ParamInvalid)
}
if strings.TrimSpace(post.Title) == "" || utf8.RuneCountInString(post.Brief) > config.Data.Posts.BriefMaxLen { if strings.TrimSpace(post.Title) == "" || utf8.RuneCountInString(post.Brief) > config.Data.Posts.BriefMaxLen {
return nil, e.New(http.StatusBadRequest, "标题格式不合法") return nil, e.New(http.StatusBadRequest, "标题格式不合法")
} }
@ -47,16 +51,15 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
} }
if isExternal { if isExternal {
// internal post, need creator role // external post, need editor role
if !user.Role.IsCreator() { if !user.Role.IsEditor() {
return nil, e.New(http.StatusForbidden, e.NoEditorPermission) return nil, e.New(http.StatusForbidden, e.NoEditorPermission)
} }
} else { } else {
// external post, need editor role // internal post, need creator role
if !user.Role.IsEditor() { if !user.Role.IsCreator() {
return nil, e.New(http.StatusForbidden, e.NoEditorPermission) return nil, e.New(http.StatusForbidden, e.NoEditorPermission)
} }
if len(post.Md) <= config.Data.Posts.BriefMaxLen { if len(post.Md) <= config.Data.Posts.BriefMaxLen {
post.Brief = post.Md post.Brief = post.Md
} else { } else {
@ -71,10 +74,10 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
setSlug(user.ID, post) setSlug(user.ID, post)
if post.ID == "" { if post.ID == "" {
post.ID = utils.GenID(models.IDTypePost) post.ID = utils.GenID(post.Type)
//create //create
_, err := db.Conn.Exec("INSERT INTO posts (id,creator,slug, title, md, url, cover, brief,status, created, updated) VALUES(?,?,?,?,?,?,?,?,?,?,?)", _, err := db.Conn.Exec("INSERT INTO story (id,type,creator,slug, title, md, url, cover, brief,status, created, updated) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
post.ID, user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, models.StatusPublished, now, now) post.ID, post.Type, user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, models.StatusPublished, now, now)
if err != nil { if err != nil {
logger.Warn("submit post error", "error", err) logger.Warn("submit post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal) return nil, e.New(http.StatusInternalServerError, e.Internal)
@ -86,7 +89,7 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
return nil, e.New(http.StatusForbidden, e.NoEditorPermission) return nil, e.New(http.StatusForbidden, e.NoEditorPermission)
} }
_, err = db.Conn.Exec("UPDATE posts SET slug=?, title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?", _, err = db.Conn.Exec("UPDATE story SET slug=?, title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?",
post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, post.ID) post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, post.ID)
if err != nil { if err != nil {
logger.Warn("upate post error", "error", err) logger.Warn("upate post error", "error", err)
@ -107,6 +110,66 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
}, nil }, nil
} }
func SubmitPostDraft(c *gin.Context) (map[string]string, *e.Error) {
user := user.CurrentUser(c)
post := &models.Story{}
err := c.Bind(&post)
if err != nil {
return nil, e.New(http.StatusBadRequest, e.ParamInvalid)
}
if !user.Role.IsCreator() {
return nil, e.New(http.StatusForbidden, e.NoEditorPermission)
}
md := utils.Compress(post.Md)
now := time.Now()
if len(post.Md) <= config.Data.Posts.BriefMaxLen {
post.Brief = post.Md
} else {
post.Brief = string([]rune(post.Md)[:config.Data.Posts.BriefMaxLen])
}
if post.ID == "" {
post.ID = utils.GenID(models.IDTypePost)
//create
_, err := db.Conn.Exec("INSERT INTO story (id,type,creator,slug, title, md, url, cover, brief,status, created, updated) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
post.ID, models.IDTypePost, user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, models.StatusDraft, now, now)
fmt.Println(post.Brief)
if err != nil {
logger.Warn("submit post draft error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
} else {
// 只有创建者自己才能更新内容
creator, _ := GetPostCreator(post.ID)
if creator != user.ID {
return nil, e.New(http.StatusForbidden, e.NoEditorPermission)
}
_, err = db.Conn.Exec("UPDATE story SET slug=?, title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?",
post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, post.ID)
if err != nil {
logger.Warn("upate post draft error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
}
//update tags
err = tags.UpdateTargetTags(user.ID, post.ID, post.Tags)
if err != nil {
logger.Warn("upate tags error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
return map[string]string{
"id": post.ID,
}, nil
}
func DeletePost(id string) *e.Error { func DeletePost(id string) *e.Error {
tx, err := db.Conn.Begin() tx, err := db.Conn.Begin()
if err != nil { if err != nil {
@ -114,7 +177,7 @@ func DeletePost(id string) *e.Error {
return e.New(http.StatusInternalServerError, e.Internal) return e.New(http.StatusInternalServerError, e.Internal)
} }
_, err = tx.Exec("DELETE FROM posts WHERE id=?", id) _, err = tx.Exec("DELETE FROM story WHERE id=?", id)
if err != nil { if err != nil {
logger.Warn("delete post error", "error", err) logger.Warn("delete post error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal) return e.New(http.StatusInternalServerError, e.Internal)
@ -132,11 +195,11 @@ func DeletePost(id string) *e.Error {
return nil return nil
} }
func GetPost(id string, slug string) (*models.Post, *e.Error) { func GetStory(id string, slug string) (*models.Story, *e.Error) {
ar := &models.Post{} ar := &models.Story{}
var rawmd []byte var rawmd []byte
err := db.Conn.QueryRow("select id,slug,title,md,url,cover,brief,creator,created,updated from posts where id=? or slug=?", id, slug).Scan( err := db.Conn.QueryRow("select id,type,slug,title,md,url,cover,brief,creator,status,created,updated from story where id=?", id).Scan(
&ar.ID, &ar.Slug, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated, &ar.ID, &ar.Type, &ar.Slug, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Status, &ar.Created, &ar.Updated,
) )
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -165,7 +228,7 @@ func GetPost(id string, slug string) (*models.Post, *e.Error) {
func GetPostCreator(id string) (string, *e.Error) { func GetPostCreator(id string) (string, *e.Error) {
var uid string var uid string
err := db.Conn.QueryRow("SELECT creator FROM posts WHERE id=?", id).Scan(&uid) err := db.Conn.QueryRow("SELECT creator FROM story WHERE id=?", id).Scan(&uid)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return "", e.New(http.StatusNotFound, e.NotFound) return "", e.New(http.StatusNotFound, e.NotFound)
@ -181,24 +244,13 @@ func GetPostCreator(id string) (string, *e.Error) {
// 1. 长度不能超过127 // 1. 长度不能超过127
// 2. 每次title更新都要重新生成slug // 2. 每次title更新都要重新生成slug
// 3. 单个用户下的slug不能重复如果已经存在需要加上-1这种字符 // 3. 单个用户下的slug不能重复如果已经存在需要加上-1这种字符
func setSlug(creator string, post *models.Post) error { func setSlug(creator string, post *models.Story) error {
slug := utils.Slugify(post.Title) slug := utils.Slugify(post.Title)
if len(slug) > 100 { if len(slug) > 100 {
slug = slug[:100] slug = slug[:100]
} }
count := 0 post.Slug = slug
err := db.Conn.QueryRow("SELECT count(*) FROM posts WHERE creator=? and title=?", creator, post.Title).Scan(&count)
if err != nil {
logger.Warn("count slug error", "error", err)
return err
}
if count == 0 {
post.Slug = slug
} else {
post.Slug = fmt.Sprintf("%s-%d", slug, count)
}
return nil return nil
} }

@ -14,9 +14,9 @@ import (
"github.com/imdotdev/im.dev/server/pkg/models" "github.com/imdotdev/im.dev/server/pkg/models"
) )
func HomePosts(user *models.User, filter string) (models.Posts, *e.Error) { func HomePosts(user *models.User, filter string) (models.Stories, *e.Error) {
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,creator,created,updated from posts") rows, err := db.Conn.Query("select id,type,slug,title,url,cover,brief,creator,created,updated from story where status=?", models.StatusPublished)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err) logger.Warn("get user posts error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal) return nil, e.New(http.StatusInternalServerError, e.Internal)
@ -28,8 +28,15 @@ func HomePosts(user *models.User, filter string) (models.Posts, *e.Error) {
return posts, nil return posts, nil
} }
func UserPosts(user *models.User, uid string) (models.Posts, *e.Error) { func UserPosts(tp string, user *models.User, uid string) (models.Stories, *e.Error) {
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,creator,created,updated from posts where creator=?", uid) var rows *sql.Rows
var err error
if tp == models.IDTypeUndefined {
rows, err = db.Conn.Query("select id,type,slug,title,url,cover,brief,creator,created,updated from story where creator=? and status=?", uid, models.StatusPublished)
} else {
rows, err = db.Conn.Query("select id,type,slug,title,url,cover,brief,creator,created,updated from story where creator=? and type=? and status=?", uid, tp, models.StatusPublished)
}
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err) logger.Warn("get user posts error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal) return nil, e.New(http.StatusInternalServerError, e.Internal)
@ -41,7 +48,20 @@ func UserPosts(user *models.User, uid string) (models.Posts, *e.Error) {
return posts, nil return posts, nil
} }
func TagPosts(user *models.User, tagID string) (models.Posts, *e.Error) { func UserDrafts(user *models.User, uid string) (models.Stories, *e.Error) {
rows, err := db.Conn.Query("select id,type,slug,title,url,cover,brief,creator,created,updated from story where creator=? and status=?", uid, models.StatusDraft)
if err != nil && err != sql.ErrNoRows {
logger.Warn("get user drafts error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
posts := GetPosts(user, rows)
sort.Sort(posts)
return posts, nil
}
func TagPosts(user *models.User, tagID string) (models.Stories, *e.Error) {
// get post ids // get post ids
postIDs, err := tags.GetTargetIDs(tagID) postIDs, err := tags.GetTargetIDs(tagID)
if err != nil { if err != nil {
@ -51,7 +71,7 @@ func TagPosts(user *models.User, tagID string) (models.Posts, *e.Error) {
ids := strings.Join(postIDs, "','") ids := strings.Join(postIDs, "','")
q := fmt.Sprintf("select id,slug,title,url,cover,brief,creator,created,updated from posts where id in ('%s')", ids) q := fmt.Sprintf("select id,type,slug,title,url,cover,brief,creator,created,updated from story where id in ('%s') and status='%d'", ids, models.StatusPublished)
rows, err := db.Conn.Query(q) rows, err := db.Conn.Query(q)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err) logger.Warn("get user posts error", "error", err)
@ -64,7 +84,7 @@ func TagPosts(user *models.User, tagID string) (models.Posts, *e.Error) {
return posts, nil return posts, nil
} }
func BookmarkPosts(user *models.User, filter string) (models.Posts, *e.Error) { func BookmarkPosts(user *models.User, filter string) (models.Stories, *e.Error) {
// get post ids // get post ids
rows, err := db.Conn.Query("select story_id from bookmarks where user_id=?", user.ID) rows, err := db.Conn.Query("select story_id from bookmarks where user_id=?", user.ID)
if err != nil { if err != nil {
@ -81,7 +101,7 @@ func BookmarkPosts(user *models.User, filter string) (models.Posts, *e.Error) {
ids := strings.Join(postIDs, "','") ids := strings.Join(postIDs, "','")
q := fmt.Sprintf("select id,slug,title,url,cover,brief,creator,created,updated from posts where id in ('%s')", ids) q := fmt.Sprintf("select id,type,slug,title,url,cover,brief,creator,created,updated from story where id in ('%s')", ids)
rows, err = db.Conn.Query(q) rows, err = db.Conn.Query(q)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err) logger.Warn("get user posts error", "error", err)
@ -104,11 +124,11 @@ func BookmarkPosts(user *models.User, filter string) (models.Posts, *e.Error) {
return posts, nil return posts, nil
} }
func GetPosts(user *models.User, rows *sql.Rows) models.Posts { func GetPosts(user *models.User, rows *sql.Rows) models.Stories {
posts := make(models.Posts, 0) posts := make(models.Stories, 0)
for rows.Next() { for rows.Next() {
ar := &models.Post{} ar := &models.Story{}
err := rows.Scan(&ar.ID, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated) err := rows.Scan(&ar.ID, &ar.Type, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated)
if err != nil { if err != nil {
logger.Warn("scan post error", "error", err) logger.Warn("scan post error", "error", err)
continue continue

@ -10,6 +10,7 @@ var ReserverURLs = []string{
"/settings", "/settings",
"/jobs", "/jobs",
"/books", "/books",
"/series",
"/notifications", "/notifications",
"/sponsors", "/sponsors",
"/explore", "/explore",

@ -8,11 +8,13 @@ import (
) )
const ( const (
IDUndefined = "0" IDTypeUndefined = "0"
IDTypePost = "1" IDTypeTag = "1"
IDTypeComment = "2" IDTypeComment = "2"
IDTypeUser = "3" IDTypeUser = "3"
IDTypeTag = "4" IDTypePost = "4"
IDTypeSeries = "5"
IDTypeBook = "6"
) )
func GetIDType(id string) string { func GetIDType(id string) string {
@ -26,7 +28,11 @@ func GetIDType(id string) string {
func GetIdTypeTable(id string) string { func GetIdTypeTable(id string) string {
switch id[:1] { switch id[:1] {
case IDTypePost: case IDTypePost:
return "posts" return "story"
case IDTypeSeries:
return "story"
case IDTypeBook:
return "story"
case IDTypeComment: case IDTypeComment:
return "comments" return "comments"
case IDTypeUser: case IDTypeUser:
@ -34,7 +40,7 @@ func GetIdTypeTable(id string) string {
case IDTypeTag: case IDTypeTag:
return "tags" return "tags"
default: default:
return IDUndefined return IDTypeUndefined
} }
} }
@ -44,7 +50,7 @@ func IdExist(id string) bool {
} }
tbl := GetIdTypeTable(id) tbl := GetIdTypeTable(id)
if tbl == IDUndefined { if tbl == IDTypeUndefined {
return false return false
} }
@ -61,3 +67,11 @@ func IdExist(id string) bool {
return true return true
} }
func ValidStoryIDType(tp string) bool {
if tp == IDTypePost || tp == IDTypeSeries || tp == IDTypeBook {
return true
}
return false
}

@ -8,8 +8,9 @@ const (
StatusHidden = 3 StatusHidden = 3
) )
type Post struct { type Story struct {
ID string `json:"id"` ID string `json:"id"`
Type string `json:"type"`
Creator *UserSimple `json:"creator"` Creator *UserSimple `json:"creator"`
CreatorID string `json:"creatorId"` CreatorID string `json:"creatorId"`
Title string `json:"title"` Title string `json:"title"`
@ -30,18 +31,18 @@ type Post struct {
Updated time.Time `json:"updated"` Updated time.Time `json:"updated"`
} }
type Posts []*Post type Stories []*Story
func (ar Posts) Len() int { return len(ar) } func (s Stories) Len() int { return len(s) }
func (ar Posts) Swap(i, j int) { ar[i], ar[j] = ar[j], ar[i] } func (s Stories) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (ar Posts) Less(i, j int) bool { func (s Stories) Less(i, j int) bool {
return ar[i].Created.Unix() > ar[j].Created.Unix() return s[i].Created.Unix() > s[j].Created.Unix()
} }
type FavorPosts []*Post type FavorStories []*Story
func (ar FavorPosts) Len() int { return len(ar) } func (s FavorStories) Len() int { return len(s) }
func (ar FavorPosts) Swap(i, j int) { ar[i], ar[j] = ar[j], ar[i] } func (s FavorStories) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (ar FavorPosts) Less(i, j int) bool { func (s FavorStories) Less(i, j int) bool {
return ar[i].Likes > ar[j].Likes return s[i].Likes > s[j].Likes
} }

@ -48,7 +48,7 @@ export const Comments = ({storyID}: Props) => {
<Flex justifyContent="space-between"> <Flex justifyContent="space-between">
<HStack spacing="4"> <HStack spacing="4">
<Text fontWeight="600" fontSize="1.1rem">Comments ({countComments()})</Text> <Text fontWeight="600" fontSize="1.1rem">Comments ({countComments()})</Text>
<Select fontWeight="550" cursor="pointer" width="100px" value={sorter} onChange={e => {setSorter(e.currentTarget.value as SearchFilter);getComments(e.currentTarget.value)}} variant="unstyled"> <Select fontWeight="550" cursor="pointer" width="120px" value={sorter} onChange={e => {setSorter(e.currentTarget.value as SearchFilter);getComments(e.currentTarget.value)}} variant="unstyled">
<option value={SearchFilter.Favorites}>{upperFirst(SearchFilter.Favorites)}</option> <option value={SearchFilter.Favorites}>{upperFirst(SearchFilter.Favorites)}</option>
<option value={SearchFilter.Recent}>{upperFirst(SearchFilter.Recent)}</option> <option value={SearchFilter.Recent}>{upperFirst(SearchFilter.Recent)}</option>
</Select> </Select>

@ -0,0 +1,21 @@
import React from "react"
import { Box, BoxProps, useColorModeValue } from "@chakra-ui/react"
export const Card = (props: BoxProps) => {
const bg = useColorModeValue("white", "gray.780")
return (
<Box
bg={bg}
borderRadius=".5rem"
borderWidth="1px"
p={[2,2,4,4]}
// boxShadow="0 1px 1px 0 rgb(0 0 0 / 5%)"
backgroundImage="linear-gradient(45deg, rgb(237, 132, 129) 0%, rgb(51, 128, 236) 100%)"
backgroundPosition="initial initial"
backgroundRepeat="initial initial"
{...props}
/>
)
}
export default Card

@ -1,50 +0,0 @@
import React from "react"
import { Box, chakra, Flex, Heading, HStack, Image, Text, useMediaQuery, VStack } from "@chakra-ui/react"
import { Post } from "src/types/posts"
import PostAuthor from "./post-author"
import Link from "next/link"
import UnicornLike from "../interaction/like"
import { FaHeart, FaRegBookmark, FaRegComment, FaRegHeart } from "react-icons/fa"
import SvgButton from "components/svg-button"
import Bookmark from "./bookmark"
interface Props {
post: Post
size?: 'md' | 'sm'
}
export const SimplePostCard = (props: Props) => {
const { post,size='md' } = props
const [isLargeScreen] = useMediaQuery("(min-width: 768px)")
const Layout = isLargeScreen ? HStack : VStack
return (
<VStack alignItems="left" spacing="0">
<Link href={`/${post.creator.username}/${post.id}`}><Heading pb="2" size="sm" cursor="pointer">{post.title}</Heading></Link>
<HStack pl="1" spacing="5" fontSize={size==='md'? '1rem' : ".9rem"}>
<Link href={`/${post.creator.username}`}><Text cursor="pointer">{post.creator.nickname}</Text></Link>
<HStack opacity="0.9">
{post.liked ?
<Box color="red.400"><FaHeart fontSize="1.1rem" /></Box>
:
<FaRegHeart fontSize="1.1rem" />}
<Text ml="2">{post.likes}</Text>
</HStack>
<Link href={`/${post.creator.username}/${post.id}#comments`}>
<HStack opacity="0.9" cursor="pointer">
<FaRegComment fontSize="1.1rem" />
<Text ml="2">{post.comments}</Text>
</HStack>
</Link>
<Box style={{marginLeft: '4px'}}><Bookmark storyID={post.id} bookmarked={post.bookmarked} height=".95rem"/></Box>
</HStack>
</VStack>
)
}
export default SimplePostCard

@ -0,0 +1,48 @@
import React from "react"
import { Box, Heading, HStack, Text, useMediaQuery, VStack } from "@chakra-ui/react"
import { Story } from "src/types/story"
import Link from "next/link"
import { FaHeart, FaRegComment, FaRegHeart } from "react-icons/fa"
import Bookmark from "./bookmark"
import { getCommentsUrl, getStoryUrl } from "utils/story"
interface Props {
story: Story
size?: 'md' | 'sm'
}
export const SimpleStoryCard = (props: Props) => {
const { story,size='md' } = props
const [isLargeScreen] = useMediaQuery("(min-width: 768px)")
const Layout = isLargeScreen ? HStack : VStack
return (
<VStack alignItems="left" spacing="0">
<Link href={getStoryUrl(story)}><Heading pb="2" size="sm" cursor="pointer">{story.title}</Heading></Link>
<HStack pl="1" spacing="5" fontSize={size==='md'? '1rem' : ".9rem"}>
<Link href={`/${story.creator.username}`}><Text cursor="pointer">{story.creator.nickname}</Text></Link>
<HStack opacity="0.9">
{story.liked ?
<Box color="red.400"><FaHeart fontSize="1.1rem" /></Box>
:
<FaRegHeart fontSize="1.1rem" />}
<Text ml="2">{story.likes}</Text>
</HStack>
<a href={`${getCommentsUrl(story)}#comments`}>
<HStack opacity="0.9" cursor="pointer">
<FaRegComment fontSize="1.1rem" />
<Text ml="2">{story.comments}</Text>
</HStack>
</a>
<Box style={{marginLeft: '4px'}}><Bookmark storyID={story.id} bookmarked={story.bookmarked} height=".95rem"/></Box>
</HStack>
</VStack>
)
}
export default SimpleStoryCard

@ -1,11 +1,11 @@
import React from "react" import React from "react"
import { Box, Center, Text, useColorModeValue, VStack } from "@chakra-ui/react" import { Box, Center, Text, useColorModeValue, VStack } from "@chakra-ui/react"
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import PostCard from "./post-card" import StoryCard from "./story-card"
import userCustomTheme from "theme/user-custom" import userCustomTheme from "theme/user-custom"
interface Props { interface Props {
posts: Post[] stories: Story[]
card?: any card?: any
size?: 'sm' | 'md' size?: 'sm' | 'md'
showFooter?: boolean showFooter?: boolean
@ -14,12 +14,12 @@ interface Props {
} }
export const Posts = (props: Props) => { export const Stroies = (props: Props) => {
const { posts,card=PostCard,showFooter=true,type="classic"} = props const { stories,card=StoryCard,showFooter=true,type="classic"} = props
const postBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) const borderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark)
const Card = card const Card = card
const showBorder = i => { const showBorder = i => {
if (i < posts.length -1) { if (i < stories.length -1) {
return true return true
} }
@ -32,9 +32,9 @@ export const Posts = (props: Props) => {
return ( return (
<> <>
<VStack alignItems="left"> <VStack alignItems="left">
{posts.map((post,i) => {stories.map((story,i) =>
<Box py="2" borderBottom={showBorder(i)? `1px solid ${postBorderColor}`:null} key={post.id}> <Box py="2" borderBottom={showBorder(i)? `1px solid ${borderColor}`:null} key={story.id} px="1">
<Card post={post} size={props.size} type={type} highlight={props.highlight}/> <Card story={story} size={props.size} type={type} highlight={props.highlight}/>
</Box>)} </Box>)}
</VStack> </VStack>
{showFooter && <Center><Text layerStyle="textSecondary" fontSize="sm" py="4"></Text></Center>} {showFooter && <Center><Text layerStyle="textSecondary" fontSize="sm" py="4"></Text></Center>}
@ -42,4 +42,4 @@ export const Posts = (props: Props) => {
) )
} }
export default Posts export default Stroies

@ -3,7 +3,7 @@ import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Box, Avatar, V
import { Tag } from "src/types/tag" import { Tag } from "src/types/tag"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import NextLink from "next/link" import NextLink from "next/link"
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import moment from 'moment' import moment from 'moment'
import { FaGithub } from "react-icons/fa" import { FaGithub } from "react-icons/fa"
import Link from "next/link" import Link from "next/link"
@ -11,18 +11,18 @@ import { useRouter } from "next/router"
type Props = PropsOf<typeof chakra.div> & { type Props = PropsOf<typeof chakra.div> & {
size?: 'lg' | 'md' size?: 'lg' | 'md'
post : Post story : Story
showFooter?: boolean showFooter?: boolean
} }
export const PostAuthor= ({post,showFooter=true,size='lg'}:Props) =>{ export const StoryAuthor= ({story,showFooter=true,size='lg'}:Props) =>{
const router = useRouter() const router = useRouter()
return ( return (
<HStack spacing="4"> <HStack spacing="4">
<Avatar src={post.creator.avatar} size={size} onClick={() => router.push(`/${post.creator.username}`)} cursor="pointer"/> <Avatar src={story.creator.avatar} size={size} onClick={() => router.push(`/${story.creator.username}`)} cursor="pointer"/>
<VStack alignItems="left" spacing="1"> <VStack alignItems="left" spacing="1">
<Heading size="sm" onClick={() => router.push(`/${post.creator.username}`)} cursor="pointer">{post.creator.nickname === "" ? post.creator.username : post.creator.nickname}</Heading> <Heading size="sm" onClick={() => router.push(`/${story.creator.username}`)} cursor="pointer">{story.creator.nickname === "" ? story.creator.username : story.creator.nickname}</Heading>
<Text layerStyle="textSecondary" fontSize={size==='lg' ? ".9rem" : ".8rem"}><chakra.span fontWeight="600" ml="1">{moment(post.created).fromNow()}</chakra.span></Text> <Text layerStyle="textSecondary" fontSize={size==='lg' ? ".9rem" : ".8rem"}><chakra.span fontWeight="600" ml="1">{moment(story.created).fromNow()}</chakra.span></Text>
{showFooter && <HStack layerStyle="textSecondary" fontSize=".9rem" spacing="3"> {showFooter && <HStack layerStyle="textSecondary" fontSize=".9rem" spacing="3">
<FaGithub /> <chakra.span>4 min read</chakra.span> <FaGithub /> <chakra.span>4 min read</chakra.span>
</HStack>} </HStack>}
@ -31,4 +31,4 @@ export const PostAuthor= ({post,showFooter=true,size='lg'}:Props) =>{
) )
} }
export default PostAuthor export default StoryAuthor

@ -1,65 +1,69 @@
import React from "react" import React from "react"
import { Box, chakra, Flex, Heading, HStack, Image, Text, useMediaQuery, VStack } from "@chakra-ui/react" import { Box, Heading, HStack, Image, Text, useMediaQuery, VStack } from "@chakra-ui/react"
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import PostAuthor from "./post-author" import StoryAuthor from "./story-author"
import Link from "next/link" import Link from "next/link"
import Like from "../interaction/like" import Like from "../interaction/like"
import { FaHeart, FaRegHeart } from "react-icons/fa"
import Bookmark from "./bookmark" import Bookmark from "./bookmark"
import { getSvgIcon } from "components/svg-icon" import { getSvgIcon } from "components/svg-icon"
import Count from "components/count" import Count from "components/count"
import Highlighter from 'react-highlight-words'; import Highlighter from 'react-highlight-words';
import { IDType } from "src/types/id"
import { ReserveUrls } from "src/data/reserve-urls"
import { getCommentsUrl, getStoryUrl } from "utils/story"
interface Props { interface Props {
post: Post story: Story
type?: string type?: string
highlight?: string highlight?: string
} }
export const PostCard = (props: Props) => { export const StoryCard = (props: Props) => {
const { post, type = "classic" } = props const { story, type = "classic" } = props
const [isLargeScreen] = useMediaQuery("(min-width: 768px)") const [isLargeScreen] = useMediaQuery("(min-width: 768px)")
const Layout = isLargeScreen ? HStack : VStack const Layout = isLargeScreen ? HStack : VStack
return ( return (
<VStack alignItems="left" spacing={type === "classic" ? 4 : 2} p="2"> <VStack alignItems="left" spacing={type === "classic" ? 4 : 2} p="2">
<PostAuthor post={post} showFooter={false} size="md" /> <StoryAuthor story={story} showFooter={false} size="md" />
<Link href={`/${post.creator.username}/${post.id}`}> <a href={getStoryUrl(story)} target="_blank">
<Layout alignItems={isLargeScreen ? "top" : "left"} cursor="pointer" pl="2" pt="1"> <Layout alignItems={isLargeScreen ? "top" : "left"} cursor="pointer" pl="2" pt="1">
<VStack alignItems="left" spacing={type==="classic"? 3 : 2} width={isLargeScreen && type === "classic" ? "calc(100% - 18rem)" : '100%'}> <VStack alignItems="left" spacing={type==="classic"? 3 : 2} width={isLargeScreen && type === "classic" ? "calc(100% - 18rem)" : '100%'}>
<Heading size="md" fontSize={type==="classic" ? '1.4rem' : '1.2rem'}><Highlighter <Heading size="md" fontSize={type==="classic" ? '1.4rem' : '1.2rem'}><Highlighter
highlightClassName="highlight-search-match" highlightClassName="highlight-search-match"
textToHighlight={post.title} textToHighlight={story.title}
searchWords={[props.highlight]} searchWords={[props.highlight]}
/> />
</Heading> </Heading>
{type !== "classic" && <HStack>{post.rawTags.map(t => <Text layerStyle="textSecondary" fontSize="md">#{t.name}</Text>)}</HStack>} {type !== "classic" && <HStack>{story.rawTags.map(t => <Text layerStyle="textSecondary" fontSize="md">#{t.name}</Text>)}</HStack>}
<Text layerStyle={type === "classic" ? "textSecondary" : null}> <Text layerStyle={type === "classic" ? "textSecondary" : null}>
<Highlighter <Highlighter
highlightClassName="highlight-search-match" highlightClassName="highlight-search-match"
textToHighlight={post.brief} textToHighlight={story.brief}
searchWords={[props.highlight]} searchWords={[props.highlight]}
/></Text> /></Text>
</VStack> </VStack>
{post.cover && type === "classic" && <Image src={post.cover} width="18rem" height="120px" pt={isLargeScreen ? 0 : 2} borderRadius="4px" />} {story.cover && type === "classic" && <Image src={story.cover} width="18rem" height="120px" pt={isLargeScreen ? 0 : 2} borderRadius="4px" />}
</Layout> </Layout>
</Link> </a>
<HStack pl="2" spacing="5"> <HStack pl="2" spacing="5">
<Like storyID={post.id} liked={post.liked} count={post.likes} fontSize="18px" /> <Like storyID={story.id} liked={story.liked} count={story.likes} fontSize="18px" />
<Link href={`/${post.creator.username}/${post.id}#comments`}> <a href={`${getCommentsUrl(story)}#comments`} target="_blank">
<HStack opacity="0.9" cursor="pointer"> <HStack opacity="0.9" cursor="pointer">
{getSvgIcon("comments", "1.3rem")} {getSvgIcon("comments", "1.3rem")}
<Text ml="2"><Count count={post.comments} /></Text> <Text ml="2"><Count count={story.comments} /></Text>
</HStack> </HStack>
</Link> </a>
<Box style={{ marginLeft: '4px' }}><Bookmark height="1.05rem" storyID={post.id} bookmarked={post.bookmarked} /></Box> <Box style={{ marginLeft: '4px' }}><Bookmark height="1.05rem" storyID={story.id} bookmarked={story.bookmarked} /></Box>
</HStack> </HStack>
</VStack> </VStack>
) )
} }
export default PostCard export default StoryCard

@ -1,30 +1,42 @@
import React from "react" import React from "react"
import { Box, BoxProps, useColorModeValue, VStack } from "@chakra-ui/react" import { Box, BoxProps, useColorModeValue, VStack } from "@chakra-ui/react"
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import useSession from "hooks/use-session" import useSession from "hooks/use-session"
import Like from "../interaction/like" import Like from "../interaction/like"
import Bookmark from "./bookmark" import Bookmark from "./bookmark"
import SvgButton from "components/svg-button" import SvgButton from "components/svg-button"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import { IDType } from "src/types/id"
interface Props { interface Props {
post: Post story: Story
vertical?: boolean vertical?: boolean
} }
export const PostSidebar = (props: Props) => { export const StorySidebar = (props: Props) => {
const {post,vertical = true} = props const {story,vertical = true} = props
const session = useSession() const session = useSession()
const router = useRouter() const router = useRouter()
const getEditUrl = () => {
if (story.type === IDType.Post) {
return `${ReserveUrls.Editor}/post/${story.id}`
}
if (story.type === IDType.Series) {
return `${ReserveUrls.Editor}/series`
}
return ''
}
return ( return (
<VStack alignItems="left" pos="fixed" display={{ base: "none", md: 'flex' }} width={["100%", "100%", "15%", "15%"]}> <VStack alignItems="left" pos="fixed" display={{ base: "none", md: 'flex' }} width={["100%", "100%", "15%", "15%"]}>
<Box> <Box>
<Like count={post.likes} storyID={post.id} liked={post.liked} fontSize="24px" /> <Like count={story.likes} storyID={story.id} liked={story.liked} fontSize="24px" />
</Box> </Box>
<Box> <Box>
<Box mt="6"> <Box mt="6">
<Bookmark height="1.7rem" storyID={post.id} bookmarked={post.bookmarked} /> <Bookmark height="1.7rem" storyID={story.id} bookmarked={story.bookmarked} />
</Box> </Box>
<Box mt="4"> <Box mt="4">
<SvgButton <SvgButton
@ -38,14 +50,14 @@ export const PostSidebar = (props: Props) => {
/> />
</Box> </Box>
{post.creatorId === session?.user.id && <Box mt="4"> {story.creatorId === session?.user.id && <Box mt="4">
<SvgButton <SvgButton
aria-label="go to github" aria-label="go to github"
variant="ghost" variant="ghost"
layerStyle="textSecondary" layerStyle="textSecondary"
_focus={null} _focus={null}
fontWeight="300" fontWeight="300"
onClick={() => router.push(`${ReserveUrls.Editor}/post/${post.id}`)} onClick={() => router.push(getEditUrl())}
icon="edit" icon="edit"
/> />
</Box>} </Box>}
@ -54,4 +66,4 @@ export const PostSidebar = (props: Props) => {
) )
} }
export default PostSidebar export default StorySidebar

@ -1,29 +1,33 @@
import React from "react" import React from "react"
import {chakra, Heading, VStack, Text, HStack,Button, Flex,PropsOf, Tag, useMediaQuery } from "@chakra-ui/react" import {chakra, Heading, VStack, Text, HStack,Button, Flex,PropsOf, Tag, useMediaQuery } from "@chakra-ui/react"
import { Post } from "src/types/posts" import { Story } from "src/types/story"
import moment from 'moment' import moment from 'moment'
import { IDType } from "src/types/id"
import { getStoryUrl } from "utils/story"
type Props = PropsOf<typeof chakra.div> & { type Props = PropsOf<typeof chakra.div> & {
post: Post story: Story
showActions: boolean showActions: boolean
onEdit?: any onEdit?: any
onDelete?: any onDelete?: any
showSource?: boolean
} }
export const TextPostCard= (props:Props) =>{ export const TextStoryCard= (props:Props) =>{
const {post,showActions,onEdit,onDelete, ...rest} = props const {story,showActions,onEdit,onDelete,showSource=true ,...rest} = props
const [isSmallScreen] = useMediaQuery("(max-width: 768px)") const [isSmallScreen] = useMediaQuery("(max-width: 768px)")
const Lay = isSmallScreen ? VStack : Flex const Lay = isSmallScreen ? VStack : Flex
const gap = moment(post.created).fromNow() const gap = moment(story.created).fromNow()
return ( return (
//@ts-ignore //@ts-ignore
<Lay justifyContent="space-between" alignItems={isSmallScreen? "left" : "center"} {...rest}> <Lay justifyContent="space-between" alignItems={isSmallScreen? "left" : "center"} {...rest}>
<VStack alignItems="left" as="a" href={post.url ? post.url : `/${post.creator.username}/${post.id}`} spacing={{base: 4, md: 2}}> <VStack alignItems="left" as="a" href={story.url ?? getStoryUrl(story)} spacing={{base: 4, md: 2}}>
<Heading size="sm" display="flex" alignItems="center"> <Heading size="sm" display="flex" alignItems="center">
{post.url ? <Tag size="sm" mr="2"></Tag> : <Tag size="sm" mr="2"></Tag>} {showSource && <> {story.url ? <Tag size="sm" mr="2"></Tag> : <Tag size="sm" mr="2"></Tag>}</>}
{post.title} {story.title ?story.title : 'No Title'}
</Heading> </Heading>
<Text fontSize=".9rem">{gap}</Text> <Text fontSize=".9rem">{gap}</Text>
</VStack> </VStack>
@ -35,4 +39,4 @@ export const TextPostCard= (props:Props) =>{
) )
} }
export default TextPostCard export default TextStoryCard

@ -37,6 +37,9 @@ export function getSvgIcon(name,height="1.4rem") {
case "favorites": case "favorites":
svg = <svg fill="currentColor" height={height} viewBox="0 0 496 512"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 464c-119.1 0-216-96.9-216-216S128.9 40 248 40s216 96.9 216 216-96.9 216-216 216zm90.2-146.2C315.8 352.6 282.9 368 248 368s-67.8-15.4-90.2-42.2c-5.7-6.8-15.8-7.7-22.5-2-6.8 5.7-7.7 15.7-2 22.5C161.7 380.4 203.6 400 248 400s86.3-19.6 114.8-53.8c5.7-6.8 4.8-16.9-2-22.5-6.8-5.6-16.9-4.7-22.6 2.1zM168 240c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32z"></path></svg> svg = <svg fill="currentColor" height={height} viewBox="0 0 496 512"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 464c-119.1 0-216-96.9-216-216S128.9 40 248 40s216 96.9 216 216-96.9 216-216 216zm90.2-146.2C315.8 352.6 282.9 368 248 368s-67.8-15.4-90.2-42.2c-5.7-6.8-15.8-7.7-22.5-2-6.8 5.7-7.7 15.7-2 22.5C161.7 380.4 203.6 400 248 400s86.3-19.6 114.8-53.8c5.7-6.8 4.8-16.9-2-22.5-6.8-5.6-16.9-4.7-22.6 2.1zM168 240c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32z"></path></svg>
break break
case "drafts":
svg = <svg fill="currentColor" height={height} viewBox="0 0 384 512"><path d="M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zm-22.6 22.7c2.1 2.1 3.5 4.6 4.2 7.4H256V32.5c2.8.7 5.3 2.1 7.4 4.2l83.9 83.9zM336 480H48c-8.8 0-16-7.2-16-16V48c0-8.8 7.2-16 16-16h176v104c0 13.3 10.7 24 24 24h104v304c0 8.8-7.2 16-16 16zM219.2 247.2l29.6 29.6c1.8 1.8 1.8 4.6 0 6.4L136.4 395.6l-30.1 4.3c-5.9.8-11-4.2-10.2-10.2l4.3-30.1 112.4-112.4c1.8-1.8 4.6-1.8 6.4 0zm64.4 1.2l-16.4 16.4c-1.8 1.8-4.6 1.8-6.4 0l-29.6-29.6c-1.8-1.8-1.8-4.6 0-6.4l16.4-16.4c5.9-5.9 15.4-5.9 21.2 0l14.8 14.8c5.9 5.8 5.9 15.3 0 21.2z"></path></svg>
break
default: default:
break; break;
} }

@ -10,6 +10,12 @@ export const editorLinks: Route[] = [{
icon: getSvgIcon("post"), icon: getSvgIcon("post"),
disabled: false disabled: false
}, },
{
title: '草稿',
path: `${ReserveUrls.Editor}/drafts`,
icon: getSvgIcon("drafts"),
disabled: false
},
{ {
title: '系列', title: '系列',
path: `${ReserveUrls.Editor}/series`, path: `${ReserveUrls.Editor}/series`,

@ -8,6 +8,7 @@ export enum ReserveUrls {
Settings = "/settings", Settings = "/settings",
Jobs = "/jobs", Jobs = "/jobs",
Books = "/books", Books = "/books",
Series = "/series",
Notifications = "/notifications", Notifications = "/notifications",
Sponsors = "/sponsors", Sponsors = "/sponsors",
Explore = "/explore", Explore = "/explore",

@ -0,0 +1,9 @@
export enum IDType {
Undefined = "0",
Tag = "1",
Comment = "2",
User = "3",
Post = "4",
Series = "5",
Book = "6"
}

@ -2,9 +2,15 @@ import { UserSimple} from './user'
import { Tag } from './tag'; import { Tag } from './tag';
export enum StoryStatus {
Draft = 1,
Published = 2,
Hiddent = 3
}
export interface Post { export interface Story {
id?: string id?: string
type?: string
slug?: string slug?: string
creator?: UserSimple creator?: UserSimple
creatorId?: string creatorId?: string
@ -20,4 +26,5 @@ export interface Post {
liked? : boolean liked? : boolean
comments? : number comments? : number
bookmarked?: boolean bookmarked?: boolean
status?: number
} }

@ -0,0 +1,39 @@
import { ReserveUrls } from "src/data/reserve-urls"
import { IDType } from "src/types/id"
import { Story } from "src/types/story"
export const getStoryUrl = (story:Story) => {
if (story.url) {
return story.url
}
if (story.type === IDType.Post) {
return `/${story.creator.username}/${story.id}`
}
if (story.type === IDType.Series) {
return `${ReserveUrls.Series}/${story.id}`
}
if (story.type === IDType.Book) {
return `${ReserveUrls.Books}/${story.id}`
}
return "/"
}
export const getCommentsUrl = (story:Story) => {
if (story.type === IDType.Post) {
return `/${story.creator.username}/${story.id}`
}
if (story.type === IDType.Series) {
return `${ReserveUrls.Series}/${story.id}`
}
if (story.type === IDType.Book) {
return `${ReserveUrls.Books}/${story.id}`
}
return "/"
}

@ -38,7 +38,7 @@ export const getUrlParams = ():any => {
return queryString.parseUrl(window?.location.href).query return queryString.parseUrl(window?.location.href).query
} }
export const updateUrl = (params: string) => { export const updateUrl = (params?: string) => {
let url = window.location.origin + window.location.pathname let url = window.location.origin + window.location.pathname
if (params != '') { if (params != '') {
url = url + '?' + params url = url + '?' + params

@ -41,6 +41,10 @@ const customTheme = extendTheme({
color: mode(props.theme.colors.cyan['800'],props.theme.colors.cyan['200'] )(props), color: mode(props.theme.colors.cyan['800'],props.theme.colors.cyan['200'] )(props),
borderRadius: '6px' borderRadius: '6px'
}, },
'.chakra-form__label': {
fontSize: '.85rem !important',
fontWeight: '550 !important'
},
body: { body: {
background: mode("gray.50","gray.800" )(props), background: mode("gray.50","gray.800" )(props),
minHeight: '100vh', minHeight: '100vh',

Loading…
Cancel
Save