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

@ -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) {
<Input width={{base: '100px', md: '187px'}} value={props.ar.title} placeholder="Title..." onChange={props.changeTitle} focusBorderColor={useColorModeValue('teal.400', 'teal.100')} variant="flushed" />
</Box>
<EditModeSelect onChange={props.changeEditMode}/>
<Box
<HStack
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 />
<Button layerStyle="colorButton" ml="2" onClick={onOpen}></Button>
</Box>
</HStack>
</Flex>
<Drawer
isOpen={isOpen}

@ -20,7 +20,7 @@ import AccountMenu from "components/user-menu"
import { FaGithub, FaTwitter, FaUserPlus } from "react-icons/fa"
import Follow from "components/interaction/follow"
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 Link from "next/link"
import Logo from "components/logo"
@ -28,7 +28,7 @@ import { ReserveUrls } from "src/data/reserve-urls"
interface Props {
post: Post
post: Story
}
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 Container from "components/container"
import LikeButton from "components/story/unicorn-like"
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 SEO from "components/seo"
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 PageContainer from "layouts/page-container"
import { cloneDeep } from "lodash"
import { useRouter } from "next/router"
import { title } from "process"
import React, { useEffect, useState } from "react"
import { FaBookmark, FaGithub, FaRegBookmark, FaShare, FaShareAlt } from "react-icons/fa"
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 { Story } from "src/types/story"
import { requestApi } from "utils/axios/request"
import UnicornLike from "components/story/unicorn-like"
import SvgButton from "components/svg-button"
import Bookmark from "components/story/bookmark"
import PostSidebar from "components/story/post-sidebar"
import StorySidebar from "components/story/story-sidebar"
const PostPage = () => {
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 = () => {
<Heading size="lg" my="6" lineHeight="1.5">{post.title}</Heading>
<Divider my="4" />
<PostAuthor post={post} />
<StoryAuthor story={post} />
<Divider my="4" />
<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>
<Box pt="16">
<PostSidebar post={post} />
<StorySidebar story={post} />
</Box>
</HStack>

@ -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 = () => {
</Card>
:
<Card width="100%" height="fit-content" p="0" px="3">
<Posts posts={posts} showFooter={tagFilter === null}/>
<Stories stories={posts} showFooter={tagFilter === null}/>
</Card>
}
</Box>

@ -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"

@ -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"
<Divider mt="3" mb="5" />
{posts.length !== 0
?
<Posts posts={posts} showFooter={false}/>
<Stories stories={posts} showFooter={false}/>
:
<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 { 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 ?

@ -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 = () => {
</Flex>
{
posts.length === 0 ?
<>
<Center mt="4">
<Image height="25rem" src="/empty-posts.png" />
</Center>
<Center mt="8">
<Heading size="sm"></Heading>
</Center>
</>
<Empty />
:
<>
<VStack mt="4">
{posts.map(post =>
<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" />
</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
} 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 = () => {
</Flex>
</Card>
<Card width="100%" height="fit-content" p="0">
<Posts posts={posts} />
<Stories stories={posts} />
</Card>
</VStack>
<HomeSidebar />
@ -106,7 +106,7 @@ export const HomeSidebar = () => {
</Flex>
<Divider />
<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>
</Card>
</VStack>

@ -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 = () => {
<Divider mt="3"/>
{results.length === 0 && <Empty /> }
{results.length > 0 &&
<Posts posts={results} showFooter={false} type="compact" highlight={query}/>}
<Stories stories={results} showFooter={false} type="compact" highlight={query}/>}
</Card>
</Box>
</Flex>

@ -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"

@ -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 { 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');

@ -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 = () => {
</Card>
:
<Card width="100%" height="fit-content" p="0" px="3">
<Posts posts={posts} />
<Stories stories={posts} />
</Card>
}
</VStack>

@ -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

@ -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

@ -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`)

@ -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

@ -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)

@ -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 {

@ -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);
`,

@ -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
}

@ -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
}

@ -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

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

@ -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
}

@ -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
}

@ -48,7 +48,7 @@ export const Comments = ({storyID}: Props) => {
<Flex justifyContent="space-between">
<HStack spacing="4">
<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.Recent}>{upperFirst(SearchFilter.Recent)}</option>
</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 { 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 (
<>
<VStack alignItems="left">
{posts.map((post,i) =>
<Box py="2" borderBottom={showBorder(i)? `1px solid ${postBorderColor}`:null} key={post.id}>
<Card post={post} size={props.size} type={type} highlight={props.highlight}/>
{stories.map((story,i) =>
<Box py="2" borderBottom={showBorder(i)? `1px solid ${borderColor}`:null} key={story.id} px="1">
<Card story={story} size={props.size} type={type} highlight={props.highlight}/>
</Box>)}
</VStack>
{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 { 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<typeof chakra.div> & {
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 (
<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">
<Heading size="sm" onClick={() => router.push(`/${post.creator.username}`)} cursor="pointer">{post.creator.nickname === "" ? post.creator.username : post.creator.nickname}</Heading>
<Text layerStyle="textSecondary" fontSize={size==='lg' ? ".9rem" : ".8rem"}><chakra.span fontWeight="600" ml="1">{moment(post.created).fromNow()}</chakra.span></Text>
<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(story.created).fromNow()}</chakra.span></Text>
{showFooter && <HStack layerStyle="textSecondary" fontSize=".9rem" spacing="3">
<FaGithub /> <chakra.span>4 min read</chakra.span>
</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 { 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 (
<VStack alignItems="left" spacing={type === "classic" ? 4 : 2} p="2">
<PostAuthor post={post} showFooter={false} size="md" />
<Link href={`/${post.creator.username}/${post.id}`}>
<StoryAuthor story={story} showFooter={false} size="md" />
<a href={getStoryUrl(story)} target="_blank">
<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%'}>
<Heading size="md" fontSize={type==="classic" ? '1.4rem' : '1.2rem'}><Highlighter
highlightClassName="highlight-search-match"
textToHighlight={post.title}
textToHighlight={story.title}
searchWords={[props.highlight]}
/>
</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}>
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={post.brief}
textToHighlight={story.brief}
searchWords={[props.highlight]}
/></Text>
</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>
</Link>
</a>
<HStack pl="2" spacing="5">
<Like storyID={post.id} liked={post.liked} count={post.likes} fontSize="18px" />
<Link href={`/${post.creator.username}/${post.id}#comments`}>
<Like storyID={story.id} liked={story.liked} count={story.likes} fontSize="18px" />
<a href={`${getCommentsUrl(story)}#comments`} target="_blank">
<HStack opacity="0.9" cursor="pointer">
{getSvgIcon("comments", "1.3rem")}
<Text ml="2"><Count count={post.comments} /></Text>
<Text ml="2"><Count count={story.comments} /></Text>
</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>
</VStack>
)
}
export default PostCard
export default StoryCard

@ -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 (
<VStack alignItems="left" pos="fixed" display={{ base: "none", md: 'flex' }} width={["100%", "100%", "15%", "15%"]}>
<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 mt="6">
<Bookmark height="1.7rem" storyID={post.id} bookmarked={post.bookmarked} />
<Bookmark height="1.7rem" storyID={story.id} bookmarked={story.bookmarked} />
</Box>
<Box mt="4">
<SvgButton
@ -38,14 +50,14 @@ export const PostSidebar = (props: Props) => {
/>
</Box>
{post.creatorId === session?.user.id && <Box mt="4">
{story.creatorId === session?.user.id && <Box mt="4">
<SvgButton
aria-label="go to github"
variant="ghost"
layerStyle="textSecondary"
_focus={null}
fontWeight="300"
onClick={() => router.push(`${ReserveUrls.Editor}/post/${post.id}`)}
onClick={() => router.push(getEditUrl())}
icon="edit"
/>
</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 {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<typeof chakra.div> & {
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
<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">
{post.url ? <Tag size="sm" mr="2"></Tag> : <Tag size="sm" mr="2"></Tag>}
{post.title}
{showSource && <> {story.url ? <Tag size="sm" mr="2"></Tag> : <Tag size="sm" mr="2"></Tag>}</>}
{story.title ?story.title : 'No Title'}
</Heading>
<Text fontSize=".9rem">{gap}</Text>
</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":
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
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:
break;
}

@ -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`,

@ -8,6 +8,7 @@ export enum ReserveUrls {
Settings = "/settings",
Jobs = "/jobs",
Books = "/books",
Series = "/series",
Notifications = "/notifications",
Sponsors = "/sponsors",
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';
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
}

@ -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
}
export const updateUrl = (params: string) => {
export const updateUrl = (params?: string) => {
let url = window.location.origin + window.location.pathname
if (params != '') {
url = url + '?' + params

@ -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',

Loading…
Cancel
Save