From 8c2bf9f37944208f33ac9aafbf0936977d678bb0 Mon Sep 17 00:00:00 2001 From: sunface Date: Tue, 9 Mar 2021 17:09:25 +0800 Subject: [PATCH] update --- layouts/nav/editor-nav.tsx | 18 +- layouts/nav/post-nav.tsx | 4 +- pages/[username]/[post_id].tsx | 27 +-- pages/[username]/index.tsx | 17 +- pages/admin/tags.tsx | 1 - pages/bookmarks.tsx | 10 +- pages/editor/drafts.tsx | 72 +++++++ pages/editor/post/[id].tsx | 82 ++++++-- pages/editor/posts.tsx | 27 +-- pages/editor/series.tsx | 180 ++++++++++++++++++ pages/index.tsx | 8 +- pages/search/posts.tsx | 4 +- pages/search/users.tsx | 4 +- pages/series/[id].tsx | 79 ++++++++ pages/settings/profile.tsx | 7 - pages/tags/[name].tsx | 13 +- server/internal/api/posts.go | 22 ++- server/internal/api/story.go | 18 +- server/internal/cache/cache.go | 1 + server/internal/search/search.go | 10 +- server/internal/server.go | 6 +- server/internal/storage/init.go | 2 +- server/internal/storage/sql_tables.go | 25 +-- server/internal/story/comment.go | 16 +- server/internal/story/post.go | 112 ++++++++--- server/internal/story/posts.go | 44 +++-- server/pkg/common/reserve_urls.go | 1 + server/pkg/models/id_type.go | 30 ++- server/pkg/models/{post.go => story.go} | 23 +-- src/components/comments/comments.tsx | 2 +- src/components/pop-ad.tsx | 21 ++ src/components/story/no-more-posts.tsx | 0 src/components/story/simple-post-card.tsx | 50 ----- src/components/story/simple-story-card.tsx | 48 +++++ .../story/{posts.tsx => stories.tsx} | 22 +-- .../{post-author.tsx => story-author.tsx} | 14 +- .../story/{post-card.tsx => story-card.tsx} | 44 +++-- .../{post-sidebar.tsx => story-sidebar.tsx} | 30 ++- ...text-post-card.tsx => text-story-card.tsx} | 22 ++- src/components/svg-icon.tsx | 3 + src/data/links.tsx | 6 + src/data/reserve-urls.ts | 1 + src/types/id.ts | 9 + src/types/{posts.ts => story.ts} | 9 +- src/utils/story.ts | 39 ++++ src/utils/url.ts | 2 +- theme.ts | 4 + 47 files changed, 889 insertions(+), 300 deletions(-) create mode 100644 pages/editor/drafts.tsx create mode 100644 pages/editor/series.tsx create mode 100644 pages/series/[id].tsx rename server/pkg/models/{post.go => story.go} (63%) create mode 100644 src/components/pop-ad.tsx delete mode 100644 src/components/story/no-more-posts.tsx delete mode 100644 src/components/story/simple-post-card.tsx create mode 100644 src/components/story/simple-story-card.tsx rename src/components/story/{posts.tsx => stories.tsx} (57%) rename src/components/story/{post-author.tsx => story-author.tsx} (68%) rename src/components/story/{post-card.tsx => story-card.tsx} (55%) rename src/components/story/{post-sidebar.tsx => story-sidebar.tsx} (64%) rename src/components/story/{text-post-card.tsx => text-story-card.tsx} (60%) create mode 100644 src/types/id.ts rename src/types/{posts.ts => story.ts} (73%) create mode 100644 src/utils/story.ts diff --git a/layouts/nav/editor-nav.tsx b/layouts/nav/editor-nav.tsx index 62da05ca..16c3eca5 100644 --- a/layouts/nav/editor-nav.tsx +++ b/layouts/nav/editor-nav.tsx @@ -15,7 +15,9 @@ import { Heading, Tag as ChakraTag, TagLabel, - TagCloseButton + TagCloseButton, + Spinner, + Text } from "@chakra-ui/react" import { useViewportScroll } from "framer-motion" import NextLink from "next/link" @@ -25,16 +27,18 @@ import Card from "components/card" import DarkMode from "components/dark-mode" import EditModeSelect from "components/edit-mode-select" 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 { - ar : Post + ar : Story changeTitle: any changeEditMode: any publish: any onChange:any + saved?: boolean } function HeaderContent(props: Props) { @@ -64,12 +68,16 @@ function HeaderContent(props: Props) { - + {(props.saved !== null )&& + {!props.saved ? Saving : Saved} + } - + { const router = useRouter() const id = router.query.post_id - const [post, setPost]: [Post, any] = useState(null) + const [post, setPost]: [Story, any] = useState(null) useEffect(() => { if (id) { getData() @@ -66,7 +53,7 @@ const PostPage = () => { {post.title} - + @@ -76,7 +63,7 @@ const PostPage = () => { - + diff --git a/pages/[username]/index.tsx b/pages/[username]/index.tsx index 0f687ede..2fbe9a62 100644 --- a/pages/[username]/index.tsx +++ b/pages/[username]/index.tsx @@ -4,21 +4,16 @@ import Container from "components/container" import SEO from "components/seo" import siteConfig from "configs/site-config" 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 { useRouter } from "next/router" 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 { User } from "src/types/user" import { requestApi } from "utils/axios/request" import moment from 'moment' -import { Post } from "src/types/posts" -import PostCard from "components/story/post-card" -import userCustomTheme from "theme/user-custom" -import Posts from "components/story/posts" +import { Story } from "src/types/story" +import Stories from "components/story/stories" import Link from "next/link" import Empty from "components/empty" import Count from "components/count" @@ -29,8 +24,8 @@ const UserPage = () => { const username = router.query.username const session = useSession() const [user, setUser]: [User, any] = useState(null) - const [rawPosts, setRawPosts]: [Post[], any] = useState([]) - const [posts, setPosts]: [Post[], any] = useState([]) + const [rawPosts, setRawPosts]: [Story[], any] = useState([]) + const [posts, setPosts]: [Story[], any] = useState([]) const [tags,setTags]:[Tag[],any] = useState([]) const [tagFilter,setTagFilter]:[Tag,any] = useState(null) @@ -178,7 +173,7 @@ const UserPage = () => { : - + } diff --git a/pages/admin/tags.tsx b/pages/admin/tags.tsx index 604a123e..688652c4 100644 --- a/pages/admin/tags.tsx +++ b/pages/admin/tags.tsx @@ -7,7 +7,6 @@ import React, { useEffect, useState } from "react" import {adminLinks} from "src/data/links" 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 Link from "next/link" import { ReserveUrls } from "src/data/reserve-urls" diff --git a/pages/bookmarks.tsx b/pages/bookmarks.tsx index 5d0e31f1..2c687a70 100644 --- a/pages/bookmarks.tsx +++ b/pages/bookmarks.tsx @@ -20,8 +20,8 @@ import { import { Tag } from "src/types/tag" import { requestApi } from "utils/axios/request" import TagCard from 'src/components/tags/tag-card' -import { Post } from "src/types/posts" -import Posts from "components/story/posts" +import { Story } from "src/types/story" +import Stories from "components/story/stories" import { find } from "lodash" import userCustomTheme from "theme/user-custom" import Empty from "components/empty" @@ -31,8 +31,8 @@ import Empty from "components/empty" const BookmarksPage = () => { const [filter, setFilter]:[Tag,any] = useState(null) const [tags, setTags]: [Tag[], any] = useState([]) - const [rawPosts,setRawPosts]: [Post[],any] = useState([]) - const [posts,setPosts]: [Post[],any] = useState([]) + const [rawPosts,setRawPosts]: [Story[],any] = useState([]) + const [posts,setPosts]: [Story[],any] = useState([]) useEffect(() => { getBookmarkPosts() @@ -107,7 +107,7 @@ import Empty from "components/empty" {posts.length !== 0 ? - + : } diff --git a/pages/editor/drafts.tsx b/pages/editor/drafts.tsx new file mode 100644 index 00000000..00d9e753 --- /dev/null +++ b/pages/editor/drafts.tsx @@ -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 ( + <> + + + + + + 草稿列表({posts.length}) + + { + posts.length === 0 ? + + : + <> + + {posts.map(post => + + editPost(post)} onDelete={() => onDeletePost(post.id)} showSource={false}/> + + + )} + +
没有更多文章了
+ + } +
+
+
+ + ) +} +export default PostsPage + diff --git a/pages/editor/post/[id].tsx b/pages/editor/post/[id].tsx index dd423ba0..a72e7529 100644 --- a/pages/editor/post/[id].tsx +++ b/pages/editor/post/[id].tsx @@ -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 { MarkdownEditor } from 'components/markdown-editor/editor'; import PageContainer from 'layouts/page-container'; import EditorNav from 'layouts/nav/editor-nav' import { EditMode } from 'src/types/editor'; 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 { useRouter } from 'next/router'; import { config } from 'configs/config'; import { cloneDeep } from 'lodash'; import Card from 'components/card'; +import { updateUrl } from 'utils/url'; +import { IDType } from 'src/types/id'; -const content = ` -# test原创 -` + + +let saveDraftHandler = undefined; function PostEditPage() { const router = useRouter() const { id } = router.query const [editMode, setEditMode] = useState(EditMode.Edit) - const [ar, setAr] = useState({ - md: content, - title: '' + const [saved,setSaved] = useState(null) + const [ar, setAr]:[Story,any] = useState({ + type: IDType.Post, + md: '', + title: '', + status: StoryStatus.Draft }) const toast = useToast() @@ -33,14 +38,50 @@ function PostEditPage() { }, [id]) const onMdChange = newMd => { - setAr({ + const newAr = { ...ar, 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 = () => { - setAr(cloneDeep(ar)) + const newAr = cloneDeep(ar) + if (ar.status === StoryStatus.Draft) { + onSaveDraft(newAr) + } + + setAr(newAr) } const onChangeTitle = title => { @@ -54,11 +95,25 @@ function PostEditPage() { return } - setAr({ ...ar, title: title }) + const newAr = { ...ar, title: title } + if (ar.status === StoryStatus.Draft) { + onSaveDraft(newAr) + } + + setAr(newAr) } 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({ description: "发布成功", status: "success", @@ -76,6 +131,7 @@ function PostEditPage() { changeEditMode={(v) => setEditMode(v)} changeTitle={(e) => onChangeTitle(e.target.value)} publish={() => publish()} + saved={saved} />} > {editMode === EditMode.Edit ? diff --git a/pages/editor/posts.tsx b/pages/editor/posts.tsx index a1da2a91..63b357fb 100644 --- a/pages/editor/posts.tsx +++ b/pages/editor/posts.tsx @@ -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 Card from "components/card" -import Nav from "layouts/nav/nav" -import PageContainer from "layouts/page-container" import Sidebar from "layouts/sidebar/sidebar" import React, { useEffect, useState } from "react" import {editorLinks} from "src/data/links" @@ -9,16 +7,18 @@ import { requestApi } from "utils/axios/request" import { useDisclosure } from "@chakra-ui/react" import { Field, Form, Formik } from "formik" import { config } from "configs/config" -import TextPostCard from "components/story/text-post-card" -import { Post } from "src/types/posts" +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 newPost: Post = { title: '', url: '', cover: '' } +const newPost: Story = {type: IDType.Post,title: '', url: '', cover: '' } const PostsPage = () => { const [currentPost, setCurrentPost] = useState(newPost) const [posts, setPosts] = useState([]) @@ -26,7 +26,7 @@ const PostsPage = () => { const router = useRouter() const toast = useToast() 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(() => { @@ -64,7 +64,7 @@ const PostsPage = () => { } const submitPost = async (values, _) => { - await requestApi.post(`/story/post`, values) + await requestApi.post(`/story`, values) onClose() toast({ description: "提交成功", @@ -76,7 +76,7 @@ const PostsPage = () => { getPosts() } - const editPost = (post: Post) => { + const editPost = (post: Story) => { if (post.url.trim() === "") { router.push(`/editor/post/${post.id}`) } else { @@ -119,20 +119,13 @@ const PostsPage = () => { { posts.length === 0 ? - <> -
- -
-
- 你还没创建任何文章 -
- + : <> {posts.map(post => - editPost(post)} onDelete={() => onDeletePost(post.id)} /> + editPost(post)} onDelete={() => onDeletePost(post.id)} /> )} diff --git a/pages/editor/series.tsx b/pages/editor/series.tsx new file mode 100644 index 00000000..6376e406 --- /dev/null +++ b/pages/editor/series.tsx @@ -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 ( + <> + + + + + {currentSeries ? + + {(props) => ( +
+ + + {({ field, form }) => ( + + 标题 + + {form.errors.title} + + )} + + + {({ field, form }) => ( + + 封面图片 + + {form.errors.cover} + + )} + + + {({ field, form }) => ( + + 简介 + + {form.errors.brief} + + )} + + + + + + +
+ )} +
: + <> + + 系列({posts.length}) + + + { + posts.length === 0 ? + : + <> + + {posts.map(post => + + editPost(post)} onDelete={() => onDeletePost(post.id)} showSource={false}/> + + + )} + +
没有更多文章了
+ + } + } +
+
+
+ + ) +} +export default PostsPage + diff --git a/pages/index.tsx b/pages/index.tsx index 4f903e8a..c801c624 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -9,8 +9,8 @@ import { Divider } from "@chakra-ui/react" import Card from "components/card" -import Posts from "components/story/posts" -import SimplePostCard from "components/story/simple-post-card" +import Stories from "components/story/stories" +import SimplePostCard from "components/story/simple-story-card" import SEO from "components/seo" import siteConfig from "configs/site-config" import PageContainer1 from "layouts/page-container1" @@ -69,7 +69,7 @@ const HomePage = () => { - +
@@ -106,7 +106,7 @@ export const HomeSidebar = () => { - + diff --git a/pages/search/posts.tsx b/pages/search/posts.tsx index 3cec2a03..d995cfc6 100644 --- a/pages/search/posts.tsx +++ b/pages/search/posts.tsx @@ -2,7 +2,7 @@ import { Box, Divider, Flex, HStack, Input } from "@chakra-ui/react" import Card from "components/card" import Empty from "components/empty" import SEO from "components/seo" -import Posts from "components/story/posts" +import Stories from "components/story/stories" import SearchFilters from "components/search-filters" import siteConfig from "configs/site-config" import PageContainer1 from "layouts/page-container1" @@ -88,7 +88,7 @@ const PostsSearchPage = () => { {results.length === 0 && } {results.length > 0 && - } + } diff --git a/pages/search/users.tsx b/pages/search/users.tsx index af85c1b9..dcc178c8 100644 --- a/pages/search/users.tsx +++ b/pages/search/users.tsx @@ -2,7 +2,7 @@ import { Box, Divider, Flex, HStack, Input } from "@chakra-ui/react" import Card from "components/card" import Empty from "components/empty" import SEO from "components/seo" -import Posts from "components/story/posts" +import Posts from "components/story/stories" import SearchFilters from "components/search-filters" import siteConfig from "configs/site-config" import PageContainer1 from "layouts/page-container1" @@ -14,7 +14,7 @@ import { SearchFilter } from "src/types/search" import { requestApi } from "utils/axios/request" 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 Users from "components/users/users" diff --git a/pages/series/[id].tsx b/pages/series/[id].tsx new file mode 100644 index 00000000..60e358b7 --- /dev/null +++ b/pages/series/[id].tsx @@ -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 ( + <> + + {post && } mt="2rem"> + <> + + + + + {post.title} + + + + + + + + {post.rawTags.map(tag => )} + + + + + + + + + + + } + + ) +} + +export default PostPage + + diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx index ae2a04de..0b5cfbd3 100644 --- a/pages/settings/profile.tsx +++ b/pages/settings/profile.tsx @@ -6,15 +6,8 @@ import Sidebar from "layouts/sidebar/sidebar" import React, { useEffect, useState } from "react" import { adminLinks, settingLinks } from "src/data/links" 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 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 useSession from "hooks/use-session" import { config } from "configs/config" import Tags from "components/tags/tags" var validator = require('validator'); diff --git a/pages/tags/[name].tsx b/pages/tags/[name].tsx index 986a1ef8..dfdf123f 100644 --- a/pages/tags/[name].tsx +++ b/pages/tags/[name].tsx @@ -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 Container from "components/container" import Empty from "components/empty" 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 siteConfig from "configs/site-config" 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 { useRouter } from "next/router" import React, { useEffect, useState } from "react" 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 { requestApi } from "utils/axios/request" import { isAdmin } from "utils/role" @@ -23,7 +20,7 @@ import Count from "components/count" const UserPage = () => { const router = useRouter() - const [posts, setPosts]: [Post[], any] = useState([]) + const [posts, setPosts]: [Story[], any] = useState([]) const [tag, setTag]: [Tag, any] = useState(null) const [followed, setFollowed] = useState(null) @@ -81,7 +78,7 @@ const UserPage = () => { : - + } diff --git a/server/internal/api/posts.go b/server/internal/api/posts.go index 90386fcb..d3c4b0a3 100644 --- a/server/internal/api/posts.go +++ b/server/internal/api/posts.go @@ -7,11 +7,29 @@ import ( "github.com/imdotdev/im.dev/server/internal/story" "github.com/imdotdev/im.dev/server/internal/user" "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) { 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 { c.JSON(err.Status, common.RespError(err.Message)) return @@ -25,7 +43,7 @@ func GetUserPosts(c *gin.Context) { user := user.CurrentUser(c) - posts, err := story.UserPosts(user, userID) + posts, err := story.UserPosts(models.IDTypeUndefined, user, userID) if err != nil { c.JSON(err.Status, common.RespError(err.Message)) return diff --git a/server/internal/api/story.go b/server/internal/api/story.go index 0981409d..f45c0a6e 100644 --- a/server/internal/api/story.go +++ b/server/internal/api/story.go @@ -11,8 +11,18 @@ import ( "github.com/imdotdev/im.dev/server/pkg/e" ) -func SubmitPost(c *gin.Context) { - res, err := story.SubmitPost(c) +func SubmitStory(c *gin.Context) { + 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 { c.JSON(err.Status, common.RespError(err.Message)) return @@ -48,11 +58,11 @@ func DeletePost(c *gin.Context) { c.JSON(http.StatusOK, common.RespSuccess(nil)) } -func GetStoryPost(c *gin.Context) { +func GetStory(c *gin.Context) { id := c.Param("id") user := user.CurrentUser(c) - ar, err := story.GetPost(id, "") + ar, err := story.GetStory(id, "") if err != nil { c.JSON(err.Status, common.RespError(err.Message)) return diff --git a/server/internal/cache/cache.go b/server/internal/cache/cache.go index a8f1ae68..ce6a8d83 100644 --- a/server/internal/cache/cache.go +++ b/server/internal/cache/cache.go @@ -13,6 +13,7 @@ var logger = log.RootLogger.New("logger", "cache") var Users []*models.User func Init() { + time.Sleep(10 * time.Second) for { // load users rows, err := db.Conn.Query(`SELECT id,username,role,nickname,email,avatar,last_seen_at,created FROM user`) diff --git a/server/internal/search/search.go b/server/internal/search/search.go index 659067d5..f4d626b4 100644 --- a/server/internal/search/search.go +++ b/server/internal/search/search.go @@ -14,14 +14,14 @@ import ( var logger = log.RootLogger.New("logger", "search") -func Posts(user *models.User, filter, query string) []*models.Post { - posts := make([]*models.Post, 0) +func Posts(user *models.User, filter, query string) []*models.Story { + posts := make([]*models.Story, 0) // postsMap := make(map[string]*models.Post) // search by title 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 { logger.Warn("get user posts error", "error", err) return posts @@ -30,9 +30,9 @@ func Posts(user *models.User, filter, query string) []*models.Post { posts = story.GetPosts(user, rows) if filter == models.FilterFavorites { - sort.Sort(models.FavorPosts(posts)) + sort.Sort(models.FavorStories(posts)) } else { - sort.Sort(models.Posts(posts)) + sort.Sort(models.Stories(posts)) } return posts diff --git a/server/internal/server.go b/server/internal/server.go index 56de9800..6726e1ee 100644 --- a/server/internal/server.go +++ b/server/internal/server.go @@ -45,13 +45,15 @@ func (s *Server) Start() error { r := router.Group("/api") //story apis - r.GET("/story/post/:id", api.GetStoryPost) + r.GET("/story/post/:id", api.GetStory) r.GET("/story/comments/:id", api.GetStoryComments) r.POST("/story/comment", IsLogin(), api.SubmitComment) r.DELETE("/story/comment/:id", IsLogin(), api.DeleteStoryComment) 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.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.POST("/story/bookmark/:storyID", IsLogin(), api.Bookmark) r.GET("/story/bookmark/posts", IsLogin(), api.GetBookmarkPosts) diff --git a/server/internal/storage/init.go b/server/internal/storage/init.go index 93ac1f87..f39818a5 100644 --- a/server/internal/storage/init.go +++ b/server/internal/storage/init.go @@ -28,7 +28,7 @@ func Init() error { return err } - if err == sql.ErrNoRows { + if err != nil { log.RootLogger.Info("Database tables have not been created, start creating") err = initTables() if err != nil { diff --git a/server/internal/storage/sql_tables.go b/server/internal/storage/sql_tables.go index a18bcf25..8bda580a 100644 --- a/server/internal/storage/sql_tables.go +++ b/server/internal/storage/sql_tables.go @@ -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, + type VARCHAR(1) NOT NULL, creator VARCHAR(255) NOT NULL, - slug VARCHAR(64) NOT NULL, - title VARCHAR(255) NOT NULL, - md TEXT, + slug VARCHAR(64) DEFAULT '', + title VARCHAR(255) DEFAULT '', + md TEXT DEFAULT '', url VARCHAR(255), cover VARCHAR(255), brief TEXT, @@ -61,10 +62,12 @@ var sqlTables = map[string]string{ created DATETIME NOT NULL, updated DATETIME ); - CREATE INDEX IF NOT EXISTS posts_creator - ON posts (creator); - CREATE INDEX IF NOT EXISTS posts_created - ON posts (created); + CREATE INDEX IF NOT EXISTS story_type + ON story (type); + CREATE INDEX IF NOT EXISTS story_creator + ON story (creator); + CREATE INDEX IF NOT EXISTS story_created + ON story (created); `, "likes": `CREATE TABLE IF NOT EXISTS likes ( @@ -136,14 +139,14 @@ var sqlTables = map[string]string{ "comments": `CREATE TABLE IF NOT EXISTS comments ( id VARCHAR(255) PRIMARY KEY, - target_id VARCHAR(255), + story_id VARCHAR(255), creator VARCHAR(255), MD TEXT, created DATETIME NOT NULL, updated DATETIME ); - CREATE INDEX IF NOT EXISTS comments_targetid - ON comments (target_id); + CREATE INDEX IF NOT EXISTS comments_storyid + ON comments (story_id); CREATE INDEX IF NOT EXISTS comments_creator ON comments (creator); `, diff --git a/server/internal/story/comment.go b/server/internal/story/comment.go index 8ea0721e..bedd4634 100644 --- a/server/internal/story/comment.go +++ b/server/internal/story/comment.go @@ -18,7 +18,7 @@ func AddComment(c *models.Comment) *e.Error { md := utils.Compress(c.Md) 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) if err != nil { logger.Warn("add comment error", "error", err) @@ -28,7 +28,7 @@ func AddComment(c *models.Comment) *e.Error { // 更新story的comment数量 // 查询到该comment所属的story id 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 { logger.Warn("select comment error", "error", err) } else { @@ -74,7 +74,7 @@ func EditComment(c *models.Comment) *e.Error { func GetComments(storyID string, sorter string) ([]*models.Comment, *e.Error) { 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 { logger.Warn("get comments error", "error", err) 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) { c := &models.Comment{} 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, ) if err != nil { @@ -139,7 +139,7 @@ func DeleteComment(id string) *e.Error { count := 0 if isComment { // 如果是评论,我们要计算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 { logger.Warn("select comment error", "error", err) return e.New(http.StatusInternalServerError, e.Internal) @@ -161,7 +161,7 @@ func DeleteComment(id string) *e.Error { } // 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 { logger.Warn("delete comment replies error", "error", err) tx.Rollback() @@ -192,7 +192,7 @@ func GetCommentCount(storyID string) int { func GetStoryIDByCommentID(cid string) (string, bool, error) { 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 { return "", false, err } @@ -202,7 +202,7 @@ func GetStoryIDByCommentID(cid string) (string, bool, error) { return targetID, true, nil case models.IDTypeComment: 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 { return "", false, err } diff --git a/server/internal/story/post.go b/server/internal/story/post.go index 989a31c6..957c4369 100644 --- a/server/internal/story/post.go +++ b/server/internal/story/post.go @@ -20,15 +20,19 @@ import ( "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) - post := &models.Post{} + post := &models.Story{} err := c.Bind(&post) if err != nil { 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 { return nil, e.New(http.StatusBadRequest, "标题格式不合法") } @@ -47,16 +51,15 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) { } if isExternal { - // internal post, need creator role - if !user.Role.IsCreator() { + // external post, need editor role + if !user.Role.IsEditor() { return nil, e.New(http.StatusForbidden, e.NoEditorPermission) } } else { - // external post, need editor role - if !user.Role.IsEditor() { + // internal post, need creator role + if !user.Role.IsCreator() { return nil, e.New(http.StatusForbidden, e.NoEditorPermission) } - if len(post.Md) <= config.Data.Posts.BriefMaxLen { post.Brief = post.Md } else { @@ -71,10 +74,10 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) { setSlug(user.ID, post) if post.ID == "" { - post.ID = utils.GenID(models.IDTypePost) + post.ID = utils.GenID(post.Type) //create - _, err := db.Conn.Exec("INSERT INTO posts (id,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) + _, err := db.Conn.Exec("INSERT INTO story (id,type,creator,slug, title, md, url, cover, brief,status, created, updated) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", + post.ID, post.Type, user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, models.StatusPublished, now, now) if err != nil { logger.Warn("submit post error", "error", err) 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) } - _, 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) if err != nil { logger.Warn("upate post error", "error", err) @@ -107,6 +110,66 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) { }, 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 { tx, err := db.Conn.Begin() if err != nil { @@ -114,7 +177,7 @@ func DeletePost(id string) *e.Error { 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 { logger.Warn("delete post error", "error", err) return e.New(http.StatusInternalServerError, e.Internal) @@ -132,11 +195,11 @@ func DeletePost(id string) *e.Error { return nil } -func GetPost(id string, slug string) (*models.Post, *e.Error) { - ar := &models.Post{} +func GetStory(id string, slug string) (*models.Story, *e.Error) { + ar := &models.Story{} 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( - &ar.ID, &ar.Slug, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated, + 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.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 == sql.ErrNoRows { @@ -165,7 +228,7 @@ func GetPost(id string, slug string) (*models.Post, *e.Error) { func GetPostCreator(id string) (string, *e.Error) { 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 == sql.ErrNoRows { return "", e.New(http.StatusNotFound, e.NotFound) @@ -181,24 +244,13 @@ func GetPostCreator(id string) (string, *e.Error) { // 1. 长度不能超过127 // 2. 每次title更新,都要重新生成slug // 3. 单个用户下的slug不能重复,如果已经存在,需要加上-1这种字符 -func setSlug(creator string, post *models.Post) error { +func setSlug(creator string, post *models.Story) error { slug := utils.Slugify(post.Title) if len(slug) > 100 { slug = slug[:100] } - count := 0 - 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) - } + post.Slug = slug return nil } diff --git a/server/internal/story/posts.go b/server/internal/story/posts.go index 2f9d1f83..6770489f 100644 --- a/server/internal/story/posts.go +++ b/server/internal/story/posts.go @@ -14,9 +14,9 @@ import ( "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 { logger.Warn("get user posts error", "error", err) 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 } -func UserPosts(user *models.User, uid string) (models.Posts, *e.Error) { - rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,creator,created,updated from posts where creator=?", uid) +func UserPosts(tp string, user *models.User, uid string) (models.Stories, *e.Error) { + 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 { logger.Warn("get user posts error", "error", err) 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 } -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 postIDs, err := tags.GetTargetIDs(tagID) if err != nil { @@ -51,7 +71,7 @@ func TagPosts(user *models.User, tagID string) (models.Posts, *e.Error) { 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) if err != nil && err != sql.ErrNoRows { 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 } -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 rows, err := db.Conn.Query("select story_id from bookmarks where user_id=?", user.ID) if err != nil { @@ -81,7 +101,7 @@ func BookmarkPosts(user *models.User, filter string) (models.Posts, *e.Error) { 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) if err != nil && err != sql.ErrNoRows { 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 } -func GetPosts(user *models.User, rows *sql.Rows) models.Posts { - posts := make(models.Posts, 0) +func GetPosts(user *models.User, rows *sql.Rows) models.Stories { + posts := make(models.Stories, 0) for rows.Next() { - ar := &models.Post{} - err := rows.Scan(&ar.ID, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated) + ar := &models.Story{} + 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 { logger.Warn("scan post error", "error", err) continue diff --git a/server/pkg/common/reserve_urls.go b/server/pkg/common/reserve_urls.go index 85e0a813..7e1e523e 100644 --- a/server/pkg/common/reserve_urls.go +++ b/server/pkg/common/reserve_urls.go @@ -10,6 +10,7 @@ var ReserverURLs = []string{ "/settings", "/jobs", "/books", + "/series", "/notifications", "/sponsors", "/explore", diff --git a/server/pkg/models/id_type.go b/server/pkg/models/id_type.go index be85b4a3..a1375ab8 100644 --- a/server/pkg/models/id_type.go +++ b/server/pkg/models/id_type.go @@ -8,11 +8,13 @@ import ( ) const ( - IDUndefined = "0" - IDTypePost = "1" - IDTypeComment = "2" - IDTypeUser = "3" - IDTypeTag = "4" + IDTypeUndefined = "0" + IDTypeTag = "1" + IDTypeComment = "2" + IDTypeUser = "3" + IDTypePost = "4" + IDTypeSeries = "5" + IDTypeBook = "6" ) func GetIDType(id string) string { @@ -26,7 +28,11 @@ func GetIDType(id string) string { func GetIdTypeTable(id string) string { switch id[:1] { case IDTypePost: - return "posts" + return "story" + case IDTypeSeries: + return "story" + case IDTypeBook: + return "story" case IDTypeComment: return "comments" case IDTypeUser: @@ -34,7 +40,7 @@ func GetIdTypeTable(id string) string { case IDTypeTag: return "tags" default: - return IDUndefined + return IDTypeUndefined } } @@ -44,7 +50,7 @@ func IdExist(id string) bool { } tbl := GetIdTypeTable(id) - if tbl == IDUndefined { + if tbl == IDTypeUndefined { return false } @@ -61,3 +67,11 @@ func IdExist(id string) bool { return true } + +func ValidStoryIDType(tp string) bool { + if tp == IDTypePost || tp == IDTypeSeries || tp == IDTypeBook { + return true + } + + return false +} diff --git a/server/pkg/models/post.go b/server/pkg/models/story.go similarity index 63% rename from server/pkg/models/post.go rename to server/pkg/models/story.go index c6ba3815..695b3f27 100644 --- a/server/pkg/models/post.go +++ b/server/pkg/models/story.go @@ -8,8 +8,9 @@ const ( StatusHidden = 3 ) -type Post struct { +type Story struct { ID string `json:"id"` + Type string `json:"type"` Creator *UserSimple `json:"creator"` CreatorID string `json:"creatorId"` Title string `json:"title"` @@ -30,18 +31,18 @@ type Post struct { Updated time.Time `json:"updated"` } -type Posts []*Post +type Stories []*Story -func (ar Posts) Len() int { return len(ar) } -func (ar Posts) Swap(i, j int) { ar[i], ar[j] = ar[j], ar[i] } -func (ar Posts) Less(i, j int) bool { - return ar[i].Created.Unix() > ar[j].Created.Unix() +func (s Stories) Len() int { return len(s) } +func (s Stories) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s Stories) Less(i, j int) bool { + return s[i].Created.Unix() > s[j].Created.Unix() } -type FavorPosts []*Post +type FavorStories []*Story -func (ar FavorPosts) Len() int { return len(ar) } -func (ar FavorPosts) Swap(i, j int) { ar[i], ar[j] = ar[j], ar[i] } -func (ar FavorPosts) Less(i, j int) bool { - return ar[i].Likes > ar[j].Likes +func (s FavorStories) Len() int { return len(s) } +func (s FavorStories) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s FavorStories) Less(i, j int) bool { + return s[i].Likes > s[j].Likes } diff --git a/src/components/comments/comments.tsx b/src/components/comments/comments.tsx index 15cff5dc..b0513a8b 100644 --- a/src/components/comments/comments.tsx +++ b/src/components/comments/comments.tsx @@ -48,7 +48,7 @@ export const Comments = ({storyID}: Props) => { Comments ({countComments()}) - {setSorter(e.currentTarget.value as SearchFilter);getComments(e.currentTarget.value)}} variant="unstyled"> diff --git a/src/components/pop-ad.tsx b/src/components/pop-ad.tsx new file mode 100644 index 00000000..76a6e3c6 --- /dev/null +++ b/src/components/pop-ad.tsx @@ -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 ( + + ) +} + +export default Card diff --git a/src/components/story/no-more-posts.tsx b/src/components/story/no-more-posts.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/story/simple-post-card.tsx b/src/components/story/simple-post-card.tsx deleted file mode 100644 index ee297282..00000000 --- a/src/components/story/simple-post-card.tsx +++ /dev/null @@ -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 ( - - {post.title} - - {post.creator.nickname} - - - {post.liked ? - - : - } - {post.likes} - - - - - - {post.comments} - - - - - - - - - ) -} - -export default SimplePostCard diff --git a/src/components/story/simple-story-card.tsx b/src/components/story/simple-story-card.tsx new file mode 100644 index 00000000..471bf18f --- /dev/null +++ b/src/components/story/simple-story-card.tsx @@ -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 ( + + {story.title} + + {story.creator.nickname} + + + {story.liked ? + + : + } + {story.likes} + + + + + + {story.comments} + + + + + + + + + ) +} + +export default SimpleStoryCard diff --git a/src/components/story/posts.tsx b/src/components/story/stories.tsx similarity index 57% rename from src/components/story/posts.tsx rename to src/components/story/stories.tsx index e5f8be1e..55da74eb 100644 --- a/src/components/story/posts.tsx +++ b/src/components/story/stories.tsx @@ -1,11 +1,11 @@ import React from "react" import { Box, Center, Text, useColorModeValue, VStack } from "@chakra-ui/react" -import { Post } from "src/types/posts" -import PostCard from "./post-card" +import { Story } from "src/types/story" +import StoryCard from "./story-card" import userCustomTheme from "theme/user-custom" interface Props { - posts: Post[] + stories: Story[] card?: any size?: 'sm' | 'md' showFooter?: boolean @@ -14,12 +14,12 @@ interface Props { } -export const Posts = (props: Props) => { - const { posts,card=PostCard,showFooter=true,type="classic"} = props - const postBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) +export const Stroies = (props: Props) => { + const { stories,card=StoryCard,showFooter=true,type="classic"} = props + const borderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) const Card = card const showBorder = i => { - if (i < posts.length -1) { + if (i < stories.length -1) { return true } @@ -32,9 +32,9 @@ export const Posts = (props: Props) => { return ( <> - {posts.map((post,i) => - - + {stories.map((story,i) => + + )} {showFooter &&
没有更多文章了
} @@ -42,4 +42,4 @@ export const Posts = (props: Props) => { ) } -export default Posts +export default Stroies diff --git a/src/components/story/post-author.tsx b/src/components/story/story-author.tsx similarity index 68% rename from src/components/story/post-author.tsx rename to src/components/story/story-author.tsx index e55a1c6b..60e7ca9c 100644 --- a/src/components/story/post-author.tsx +++ b/src/components/story/story-author.tsx @@ -3,7 +3,7 @@ import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Box, Avatar, V import { Tag } from "src/types/tag" import { ReserveUrls } from "src/data/reserve-urls" import NextLink from "next/link" -import { Post } from "src/types/posts" +import { Story } from "src/types/story" import moment from 'moment' import { FaGithub } from "react-icons/fa" import Link from "next/link" @@ -11,18 +11,18 @@ import { useRouter } from "next/router" type Props = PropsOf & { size?: 'lg' | 'md' - post : Post + story : Story showFooter?: boolean } -export const PostAuthor= ({post,showFooter=true,size='lg'}:Props) =>{ +export const StoryAuthor= ({story,showFooter=true,size='lg'}:Props) =>{ const router = useRouter() return ( - router.push(`/${post.creator.username}`)} cursor="pointer"/> + router.push(`/${story.creator.username}`)} cursor="pointer"/> - router.push(`/${post.creator.username}`)} cursor="pointer">{post.creator.nickname === "" ? post.creator.username : post.creator.nickname} - 发布于{moment(post.created).fromNow()} + router.push(`/${story.creator.username}`)} cursor="pointer">{story.creator.nickname === "" ? story.creator.username : story.creator.nickname} + 发布于{moment(story.created).fromNow()} {showFooter && 4 min read } @@ -31,4 +31,4 @@ export const PostAuthor= ({post,showFooter=true,size='lg'}:Props) =>{ ) } -export default PostAuthor +export default StoryAuthor diff --git a/src/components/story/post-card.tsx b/src/components/story/story-card.tsx similarity index 55% rename from src/components/story/post-card.tsx rename to src/components/story/story-card.tsx index de3f7673..77c6c5bf 100644 --- a/src/components/story/post-card.tsx +++ b/src/components/story/story-card.tsx @@ -1,65 +1,69 @@ 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 { Box, Heading, HStack, Image, Text, useMediaQuery, VStack } from "@chakra-ui/react" +import { Story } from "src/types/story" +import StoryAuthor from "./story-author" import Link from "next/link" import Like from "../interaction/like" -import { FaHeart, FaRegHeart } from "react-icons/fa" import Bookmark from "./bookmark" import { getSvgIcon } from "components/svg-icon" import Count from "components/count" 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 { - post: Post + story: Story type?: string highlight?: string } -export const PostCard = (props: Props) => { - const { post, type = "classic" } = props +export const StoryCard = (props: Props) => { + const { story, type = "classic" } = props const [isLargeScreen] = useMediaQuery("(min-width: 768px)") const Layout = isLargeScreen ? HStack : VStack + + return ( - - + + - {type !== "classic" && {post.rawTags.map(t => #{t.name})}} + {type !== "classic" && {story.rawTags.map(t => #{t.name})}} - {post.cover && type === "classic" && } + {story.cover && type === "classic" && } - + - - + + {getSvgIcon("comments", "1.3rem")} - + - + - + ) } -export default PostCard +export default StoryCard diff --git a/src/components/story/post-sidebar.tsx b/src/components/story/story-sidebar.tsx similarity index 64% rename from src/components/story/post-sidebar.tsx rename to src/components/story/story-sidebar.tsx index 9d6b50ee..d5d2d45f 100644 --- a/src/components/story/post-sidebar.tsx +++ b/src/components/story/story-sidebar.tsx @@ -1,30 +1,42 @@ import React from "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 Like from "../interaction/like" import Bookmark from "./bookmark" import SvgButton from "components/svg-button" import { useRouter } from "next/router" import { ReserveUrls } from "src/data/reserve-urls" +import { IDType } from "src/types/id" interface Props { - post: Post + story: Story vertical?: boolean } -export const PostSidebar = (props: Props) => { - const {post,vertical = true} = props +export const StorySidebar = (props: Props) => { + const {story,vertical = true} = props const session = useSession() 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 ( - + - + { /> - {post.creatorId === session?.user.id && + {story.creatorId === session?.user.id && router.push(`${ReserveUrls.Editor}/post/${post.id}`)} + onClick={() => router.push(getEditUrl())} icon="edit" /> } @@ -54,4 +66,4 @@ export const PostSidebar = (props: Props) => { ) } -export default PostSidebar +export default StorySidebar diff --git a/src/components/story/text-post-card.tsx b/src/components/story/text-story-card.tsx similarity index 60% rename from src/components/story/text-post-card.tsx rename to src/components/story/text-story-card.tsx index 8396391a..651dacc7 100644 --- a/src/components/story/text-post-card.tsx +++ b/src/components/story/text-story-card.tsx @@ -1,29 +1,33 @@ import React from "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 { IDType } from "src/types/id" +import { getStoryUrl } from "utils/story" type Props = PropsOf & { - post: Post + story: Story showActions: boolean onEdit?: any onDelete?: any + showSource?: boolean } -export const TextPostCard= (props:Props) =>{ - const {post,showActions,onEdit,onDelete, ...rest} = props +export const TextStoryCard= (props:Props) =>{ + const {story,showActions,onEdit,onDelete,showSource=true ,...rest} = props const [isSmallScreen] = useMediaQuery("(max-width: 768px)") const Lay = isSmallScreen ? VStack : Flex - const gap = moment(post.created).fromNow() + const gap = moment(story.created).fromNow() + return ( //@ts-ignore - + - {post.url ? 外部 : 原创} - {post.title} + {showSource && <> {story.url ? 外部 : 原创}} + {story.title ?story.title : 'No Title'} 发布于{gap} @@ -35,4 +39,4 @@ export const TextPostCard= (props:Props) =>{ ) } -export default TextPostCard +export default TextStoryCard diff --git a/src/components/svg-icon.tsx b/src/components/svg-icon.tsx index 555d68ed..ffcbf4a4 100644 --- a/src/components/svg-icon.tsx +++ b/src/components/svg-icon.tsx @@ -37,6 +37,9 @@ export function getSvgIcon(name,height="1.4rem") { case "favorites": svg = break + case "drafts": + svg = + break default: break; } diff --git a/src/data/links.tsx b/src/data/links.tsx index 0ee9de9f..86e324d5 100644 --- a/src/data/links.tsx +++ b/src/data/links.tsx @@ -10,6 +10,12 @@ export const editorLinks: Route[] = [{ icon: getSvgIcon("post"), disabled: false }, +{ + title: '草稿', + path: `${ReserveUrls.Editor}/drafts`, + icon: getSvgIcon("drafts"), + disabled: false +}, { title: '系列', path: `${ReserveUrls.Editor}/series`, diff --git a/src/data/reserve-urls.ts b/src/data/reserve-urls.ts index cfd63ab7..43acd2db 100644 --- a/src/data/reserve-urls.ts +++ b/src/data/reserve-urls.ts @@ -8,6 +8,7 @@ export enum ReserveUrls { Settings = "/settings", Jobs = "/jobs", Books = "/books", + Series = "/series", Notifications = "/notifications", Sponsors = "/sponsors", Explore = "/explore", diff --git a/src/types/id.ts b/src/types/id.ts new file mode 100644 index 00000000..b6af20fe --- /dev/null +++ b/src/types/id.ts @@ -0,0 +1,9 @@ +export enum IDType { + Undefined = "0", + Tag = "1", + Comment = "2", + User = "3", + Post = "4", + Series = "5", + Book = "6" +} \ No newline at end of file diff --git a/src/types/posts.ts b/src/types/story.ts similarity index 73% rename from src/types/posts.ts rename to src/types/story.ts index 94df403a..db800078 100644 --- a/src/types/posts.ts +++ b/src/types/story.ts @@ -2,9 +2,15 @@ import { UserSimple} from './user' import { Tag } from './tag'; +export enum StoryStatus { + Draft = 1, + Published = 2, + Hiddent = 3 +} -export interface Post { +export interface Story { id?: string + type?: string slug?: string creator?: UserSimple creatorId?: string @@ -20,4 +26,5 @@ export interface Post { liked? : boolean comments? : number bookmarked?: boolean + status?: number } \ No newline at end of file diff --git a/src/utils/story.ts b/src/utils/story.ts new file mode 100644 index 00000000..5d610cce --- /dev/null +++ b/src/utils/story.ts @@ -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 "/" +} \ No newline at end of file diff --git a/src/utils/url.ts b/src/utils/url.ts index b6330215..710a3cd0 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -38,7 +38,7 @@ export const getUrlParams = ():any => { 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 if (params != '') { url = url + '?' + params diff --git a/theme.ts b/theme.ts index c7560ad0..66f4d39c 100644 --- a/theme.ts +++ b/theme.ts @@ -41,6 +41,10 @@ const customTheme = extendTheme({ color: mode(props.theme.colors.cyan['800'],props.theme.colors.cyan['200'] )(props), borderRadius: '6px' }, + '.chakra-form__label': { + fontSize: '.85rem !important', + fontWeight: '550 !important' + }, body: { background: mode("gray.50","gray.800" )(props), minHeight: '100vh',