diff --git a/layouts/nav/editor-nav.tsx b/layouts/nav/editor-nav.tsx
index b28f580a..3960e13f 100644
--- a/layouts/nav/editor-nav.tsx
+++ b/layouts/nav/editor-nav.tsx
@@ -2,8 +2,6 @@ import {
chakra,
Flex,
Button,
- IconButton,
- useColorMode,
useColorModeValue,
Box,
useRadioGroup,
@@ -14,26 +12,50 @@ import {
DrawerOverlay,
DrawerContent,
Divider,
- Heading
+ Heading,
+ Tag as ChakraTag,
+ TagLabel,
+ TagCloseButton
} from "@chakra-ui/react"
import { useViewportScroll } from "framer-motion"
import NextLink from "next/link"
-import React from "react"
-import { FaMoon, FaSun } from "react-icons/fa"
+import React, { useEffect, useState } from "react"
import Logo, { LogoIcon } from "src/components/logo"
import RadioCard from "components/radio-card"
import { EditMode } from "src/types/editor"
import Card from "components/card"
+import TagInput from "components/tag-input"
+import { Tag } from "src/types/tag"
+import { cloneDeep, remove } from "lodash"
+import { requestApi } from "utils/axios/request"
+import DarkMode from "components/dark-mode"
function HeaderContent(props: any) {
- const { toggleColorMode: toggleMode } = useColorMode()
- const text = useColorModeValue("dark", "light")
- const SwitchIcon = useColorModeValue(FaMoon, FaSun)
+ const [tags,setTags]:[Tag[],any] = useState([])
+ const [allTags,setAllTags] = useState([])
+
const { isOpen, onOpen, onClose } = useDisclosure()
+ useEffect(() => {
+ requestApi.get('/tags').then(res => {
+ setAllTags(res.data)
+ const t = []
+ props.ar.tags?.forEach(id => {
+ res.data.forEach(tag => {
+ if (tag.id === id) {
+ t.push(tag)
+ }
+ })
+ })
+
+ setTags(t)
+ })
+ },[props.ar])
+
+
const editOptions = [EditMode.Edit, EditMode.Preview]
const { getRootProps, getRadioProps } = useRadioGroup({
name: "framework",
@@ -43,7 +65,24 @@ function HeaderContent(props: any) {
},
})
const group = getRootProps()
+
+ const addTag = t => {
+ setTags(t)
+
+ const ids = []
+ t.forEach(tag => ids.push(tag.id))
+ props.ar.tags = ids
+ }
+
+ const removeTag = t => {
+ const newTags = cloneDeep(tags)
+ remove(newTags, tag => tag.id === t.id)
+ setTags(newTags)
+ const ids = []
+ newTags.forEach(tag => ids.push(tag.id))
+ props.ar.tags = ids
+ }
return (
<>
@@ -76,17 +115,7 @@ function HeaderContent(props: any) {
- }
- />
+
@@ -109,7 +138,24 @@ function HeaderContent(props: any) {
封面图片
- {props.ar.cover = e.target.value; props.onChange()}} mt="4" variant="flushed" size="sm" placeholder="图片链接,你可以用github当图片存储服务" focusBorderColor="teal.400"/>
+ {props.ar.cover = e.target.value; props.onChange()}} mt="4" variant="unstyled" size="sm" placeholder="输入链接,可以用github或postimg.cc当图片存储服务.." focusBorderColor="teal.400"/>
+
+
+
+
+ 设置标签
+
+
+
+ {tags.length > 0&&
+ {
+ tags.map(tag =>
+
+ {tag.title}
+ removeTag(tag)}/>
+ )
+ }
+ }
@@ -119,7 +165,6 @@ function HeaderContent(props: any) {
}
function EditorNav(props) {
- const bg = useColorModeValue("white", "gray.800")
const ref = React.useRef()
const [y, setY] = React.useState(0)
const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}
@@ -137,7 +182,7 @@ function EditorNav(props) {
pos="fixed"
top="0"
zIndex="3"
- // bg={bg}
+ bg={useColorModeValue('white','gray.800')}
left="0"
right="0"
borderTop="4px solid"
diff --git a/layouts/mobile-nav.tsx b/layouts/nav/mobile-nav.tsx
similarity index 100%
rename from layouts/mobile-nav.tsx
rename to layouts/nav/mobile-nav.tsx
diff --git a/layouts/nav/nav.tsx b/layouts/nav/nav.tsx
index b296a390..baebc059 100644
--- a/layouts/nav/nav.tsx
+++ b/layouts/nav/nav.tsx
@@ -1,37 +1,26 @@
import {
chakra,
Flex,
- Button,
HStack,
IconButton,
- useColorMode,
useColorModeValue,
useDisclosure,
useUpdateEffect,
- Menu,
- MenuButton,
- MenuList,
- MenuItem,
- MenuDivider,
- Image,
Box
} from "@chakra-ui/react"
import siteConfig from "configs/site-config"
import { useViewportScroll } from "framer-motion"
import NextLink from "next/link"
import React from "react"
-import { FaMoon, FaSun, FaUserAlt, FaRegSun, FaSignOutAlt,FaStar, FaGithub, FaBookmark, FaEdit } from "react-icons/fa"
+import { FaGithub } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo"
-import { MobileNavButton, MobileNavContent } from "../mobile-nav"
+import { MobileNavButton, MobileNavContent } from "./mobile-nav"
import AlgoliaSearch from "src/components/search/algolia-search"
-import useSession from "hooks/use-session"
-import { Session } from "src/types/session"
import { useRouter } from "next/router"
-import storage from "utils/localStorage"
-import { logout } from "utils/session"
-import { isAdmin, isEditor } from "utils/role"
import { ReserveUrls } from "src/data/reserve-urls"
import Link from "next/link"
+import DarkMode from "components/dark-mode"
+import AccountMenu from "components/account-menu"
const navLinks = [{
title: '主页',
@@ -53,22 +42,13 @@ function HeaderContent() {
const { asPath } = router
const mobileNav = useDisclosure()
- const session: Session = useSession()
- const { toggleColorMode: toggleMode } = useColorMode()
- const text = useColorModeValue("dark", "light")
- const SwitchIcon = useColorModeValue(FaMoon, FaSun)
const mobileNavBtnRef = React.useRef()
useUpdateEffect(() => {
mobileNavBtnRef.current?.focus()
}, [mobileNav.isOpen])
- const login = () => {
- console.log(router)
- storage.set("current-page", asPath)
- router.push(ReserveUrls.Login)
- }
return (
<>
@@ -86,92 +66,39 @@ function HeaderContent() {
- {navLinks.map(link => {link.title})}
+ {navLinks.map(link => {link.title})}
-
-
-
- }
- />
-
-
- }
- />
- {session ?
- :
-
- }
+
+ }
+ />
+
+
+
-
+
>
@@ -201,6 +128,7 @@ function Header(props) {
borderTop="4px solid"
borderTopColor="teal.400"
width="full"
+ bg={useColorModeValue('white', 'gray.800')}
{...props}
>
diff --git a/layouts/nav/post-nav.tsx b/layouts/nav/post-nav.tsx
new file mode 100644
index 00000000..9a44b273
--- /dev/null
+++ b/layouts/nav/post-nav.tsx
@@ -0,0 +1,127 @@
+import {
+ chakra,
+ Flex,
+ HStack,
+ IconButton,
+ useColorModeValue,
+ useDisclosure,
+ useUpdateEffect,
+ Heading,
+ Button,
+ Divider,
+ Text
+} from "@chakra-ui/react"
+import { useViewportScroll } from "framer-motion"
+import React from "react"
+import { SearchIcon } from "@chakra-ui/icons"
+import DarkMode from "components/dark-mode"
+import AccountMenu from "components/account-menu"
+import { FaGithub, FaTwitter, FaUserPlus } from "react-icons/fa"
+
+
+
+function HeaderContent() {
+ const mobileNav = useDisclosure()
+
+ const mobileNavBtnRef = React.useRef()
+
+ useUpdateEffect(() => {
+ mobileNavBtnRef.current?.focus()
+ }, [mobileNav.isOpen])
+
+ return (
+ <>
+
+
+ Sunface的博客
+ }>Follow
+
+
+
+
+ alert('search in this blog')}
+ icon={}
+ aria-label="search in this blog"
+ />
+
+
+
+
+
+
+ Home
+ Badges
+
+
+
+ }
+ />
+ }
+ />
+
+
+
+ >
+ )
+}
+
+function PostNav(props) {
+ const ref = React.useRef()
+ const [y, setY] = React.useState(0)
+ const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}
+
+ const { scrollY } = useViewportScroll()
+ React.useEffect(() => {
+ return scrollY.onChange(() => setY(scrollY.get()))
+ }, [scrollY])
+
+ return (
+ height ? "sm" : undefined}
+ transition="box-shadow 0.2s"
+ top="0"
+ zIndex="3"
+ left="0"
+ right="0"
+ borderTop="4px solid"
+ borderTopColor="teal.400"
+ width="full"
+ bg={useColorModeValue('white', 'gray.800')}
+ {...props}
+ >
+
+
+
+
+ )
+}
+
+export default PostNav
+
diff --git a/layouts/page-container.tsx b/layouts/page-container.tsx
index f7a289cc..9603f89e 100644
--- a/layouts/page-container.tsx
+++ b/layouts/page-container.tsx
@@ -1,4 +1,4 @@
-import { Badge, Box, chakra } from "@chakra-ui/react"
+import { Badge, Box, chakra,PropsOf } from "@chakra-ui/react"
import { SkipNavContent, SkipNavLink } from "@chakra-ui/skip-nav"
import Container from "components/container"
import Footer from "./footer"
@@ -24,13 +24,14 @@ function useHeadingFocusOnRouteChange() {
}, [])
}
-interface PageContainerProps {
+type PageContainerProps = PropsOf & {
children: React.ReactNode
nav?: any
}
+
function PageContainer(props: PageContainerProps) {
- const { children ,nav} = props
+ const { children ,nav, ...rest} = props
useHeadingFocusOnRouteChange()
return (
@@ -48,9 +49,10 @@ function PageContainer(props: PageContainerProps) {
{children}
diff --git a/pages/[username]/[post_slug].tsx b/pages/[username]/[post_slug].tsx
index 0951ca30..1a8756de 100644
--- a/pages/[username]/[post_slug].tsx
+++ b/pages/[username]/[post_slug].tsx
@@ -1,26 +1,136 @@
-import { chakra } from "@chakra-ui/react"
+import { Box, chakra, Divider, Flex, Heading, HStack, IconButton, Image, VStack } from "@chakra-ui/react"
import Container from "components/container"
+import LikeButton from "components/like-button"
+import { MarkdownRender } from "components/markdown-editor/render"
+import PostAuthor from "components/posts/post-author"
import SEO from "components/seo"
import siteConfig from "configs/site-config"
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 React from "react"
+import { title } from "process"
+import React, { useEffect, useState } from "react"
+import { FaBookmark, FaGithub, FaRegBookmark, FaShare, FaShareAlt } from "react-icons/fa"
+import { Post } from "src/types/posts"
+import { requestApi } from "utils/axios/request"
-const UserPage = () => {
+const PostPage = () => {
const router = useRouter()
+ const slug = router.query.post_slug
+ const [post, setPost]: [Post, any] = useState(null)
+
+ useEffect(() => {
+ if (slug) {
+ requestApi.get(`/post/${slug}`).then(res => setPost(res.data))
+ }
+ }, [slug])
+
+ const onLike = async () => {
+ await requestApi.post(`/post/like/${post.id}`)
+ const p = cloneDeep(post)
+
+ if (post.liked) {
+ p.likes += -1
+ p.liked = false
+ } else {
+ p.likes += 1
+ p.liked = true
+ }
+ setPost(p)
+ }
+
return (
- <>
-
-
-
- {router.query.username}的博文{router.query.post_slug}
-
- >
-)}
-
-export default UserPage
+ <>
+
+ } mt="2rem">
+ {post &&
+ <>
+
+
+
+
+ {post.title}
+
+
+
+
+
+
+
+
+
+
+
+ {/* */}
+ {/* */}
+
+ {/* */}
+
+
+ }
+ />
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {/* */}
+
+
+
+ }
+ />
+ }
+ />
+
+
+ >
+ }
+
+ >
+ )
+}
+
+export default PostPage
diff --git a/pages/[username]/index.tsx b/pages/[username]/index.tsx
index 99a92b3f..b1451add 100644
--- a/pages/[username]/index.tsx
+++ b/pages/[username]/index.tsx
@@ -15,8 +15,7 @@ const UserPage = () => {
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
-
-
+
{router.query.username}'s home
>
diff --git a/pages/admin/tags.tsx b/pages/admin/tags.tsx
index bc8942f3..7f68f2da 100644
--- a/pages/admin/tags.tsx
+++ b/pages/admin/tags.tsx
@@ -6,7 +6,7 @@ import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react"
import {adminLinks} from "src/data/links"
import { requestApi } from "utils/axios/request"
-import TagCard from "components/posts/tag-card"
+import TagCard from "components/posts/tag-edit-card"
import { Post } from "src/types/posts"
import { useRouter } from "next/router"
import Link from "next/link"
@@ -44,7 +44,6 @@ const PostsPage = () => {
return (
<>
-
diff --git a/pages/editor/post/[id].tsx b/pages/editor/post/[id].tsx
index 202f14e8..9c3f3994 100644
--- a/pages/editor/post/[id].tsx
+++ b/pages/editor/post/[id].tsx
@@ -1,4 +1,4 @@
-import { Box, Button, useToast} from '@chakra-ui/react';
+import { Box, Button, useToast } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';
import { MarkdownEditor } from 'components/markdown-editor/editor';
import PageContainer from 'layouts/page-container';
@@ -18,19 +18,19 @@ const content = `
function PostEditPage() {
const router = useRouter()
- const {id} = router.query
+ const { id } = router.query
const [editMode, setEditMode] = useState(EditMode.Edit)
- const [ar,setAr] = useState({
+ const [ar, setAr] = useState({
md: content,
title: ''
})
-
+
const toast = useToast()
useEffect(() => {
if (id && id !== 'new') {
requestApi.get(`/editor/post/${id}`).then(res => setAr(res.data))
}
- },[id])
+ }, [id])
const onMdChange = newMd => {
setAr({
@@ -38,7 +38,7 @@ function PostEditPage() {
md: newMd
})
}
-
+
const onChange = () => {
setAr(cloneDeep(ar))
}
@@ -50,20 +50,20 @@ function PostEditPage() {
status: "error",
duration: 2000,
isClosable: true,
- })
- return
+ })
+ return
}
- setAr({...ar, title: title})
+ setAr({ ...ar, title: title })
}
const publish = async () => {
const res = await requestApi.post(`/editor/post`, ar)
toast({
- description: "发布成功",
- status: "success",
- duration: 2000,
- isClosable: true,
+ description: "发布成功",
+ status: "success",
+ duration: 2000,
+ isClosable: true,
})
router.push(`/${res.data.username}/${res.data.slug}`)
}
@@ -78,17 +78,18 @@ function PostEditPage() {
publish={() => publish()}
/>}
>
-
- {editMode === EditMode.Edit ?
+ {editMode === EditMode.Edit ?
+
onMdChange(md)}
md={ar.md}
- /> :
+ /> :
+
- }
-
+
+ }
);
}
diff --git a/pages/editor/posts.tsx b/pages/editor/posts.tsx
index 99a19c9e..63c2211a 100644
--- a/pages/editor/posts.tsx
+++ b/pages/editor/posts.tsx
@@ -97,7 +97,6 @@ const PostsPage = () => {
return (
<>
-
diff --git a/pages/index.tsx b/pages/index.tsx
index cf7eda25..4fe694fd 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -12,7 +12,6 @@ const HomePage = () => (
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
-
NOT FOUND
diff --git a/pages/posts/[id].tsx b/pages/posts/[id].tsx
deleted file mode 100644
index 94d9a29f..00000000
--- a/pages/posts/[id].tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { chakra } from "@chakra-ui/react"
-import Container from "components/container"
-import SEO from "components/seo"
-import siteConfig from "configs/site-config"
-import Nav from "layouts/nav/nav"
-import PageContainer from "layouts/page-container"
-import React from "react"
-
-const PostPage = () => (
- <>
-
-
-
- Post
-
- >
-)
-
-export default PostPage
-
diff --git a/pages/tags/[name].tsx b/pages/tags/[name].tsx
index ccc0a736..942b1c8f 100644
--- a/pages/tags/[name].tsx
+++ b/pages/tags/[name].tsx
@@ -36,7 +36,6 @@ const UserPage = () => {
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
-
{tag.name &&
@@ -65,7 +64,7 @@ const UserPage = () => {
- 13.4K
+ {tag.postCount}
Posts
diff --git a/server/internal/api/article.go b/server/internal/api/article.go
deleted file mode 100644
index 778f64ec..00000000
--- a/server/internal/api/article.go
+++ /dev/null
@@ -1 +0,0 @@
-package api
diff --git a/server/internal/api/editor.go b/server/internal/api/editor.go
index a1dc85b7..6d500c0f 100644
--- a/server/internal/api/editor.go
+++ b/server/internal/api/editor.go
@@ -1,7 +1,6 @@
package api
import (
- "fmt"
"net/http"
"strconv"
@@ -63,7 +62,6 @@ func DeletePost(c *gin.Context) {
func GetEditorPost(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
- fmt.Println(c.Param("id"))
if id == 0 {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
@@ -81,7 +79,7 @@ func GetEditorPost(c *gin.Context) {
return
}
- ar, err := posts.GetPost(id)
+ ar, err := posts.GetPost(id, "")
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
diff --git a/server/internal/api/post.go b/server/internal/api/post.go
new file mode 100644
index 00000000..365c2ad6
--- /dev/null
+++ b/server/internal/api/post.go
@@ -0,0 +1,49 @@
+package api
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+ "github.com/imdotdev/im.dev/server/internal/posts"
+ "github.com/imdotdev/im.dev/server/internal/session"
+ "github.com/imdotdev/im.dev/server/pkg/common"
+ "github.com/imdotdev/im.dev/server/pkg/e"
+)
+
+func GetPost(c *gin.Context) {
+ slug := c.Param("slug")
+
+ ar, err := posts.GetPost(0, slug)
+ if err != nil {
+ c.JSON(err.Status, common.RespError(err.Message))
+ return
+ }
+
+ user := session.CurrentUser(c)
+ if user == nil {
+ ar.Liked = false
+ } else {
+ ar.Liked = posts.GetLiked(ar.ID, user.ID)
+
+ }
+
+ c.JSON(http.StatusOK, common.RespSuccess(ar))
+}
+
+func LikePost(c *gin.Context) {
+ user := session.CurrentUser(c)
+ id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
+ if id == 0 {
+ c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
+ return
+ }
+
+ err := posts.Like(id, user.ID)
+ if err != nil {
+ c.JSON(err.Status, common.RespError(err.Message))
+ return
+ }
+
+ c.JSON(http.StatusOK, common.RespSuccess(nil))
+}
diff --git a/server/internal/posts/like.go b/server/internal/posts/like.go
new file mode 100644
index 00000000..ab33c512
--- /dev/null
+++ b/server/internal/posts/like.go
@@ -0,0 +1,53 @@
+package posts
+
+import (
+ "database/sql"
+ "net/http"
+
+ "github.com/imdotdev/im.dev/server/pkg/db"
+ "github.com/imdotdev/im.dev/server/pkg/e"
+)
+
+func Like(postId int64, userId int64) *e.Error {
+ // 判断文章是否存在
+ exist := postExist(postId)
+ if !exist {
+ return e.New(http.StatusNotFound, e.NotFound)
+ }
+
+ // 查询当前like状态
+ liked := GetLiked(postId, userId)
+
+ if liked {
+ // 已经喜欢过该篇文章,更改为不喜欢
+ _, err := db.Conn.Exec("DELETE FROM post_like WHERE post_id=? and user_id=?", postId, userId)
+ if err != nil {
+ return e.New(http.StatusInternalServerError, e.Internal)
+ }
+ db.Conn.Exec("UPDATE posts SET like_count=like_count-1 WHERE id=?", postId)
+ } else {
+ _, err := db.Conn.Exec("INSERT INTO post_like (post_id,user_id) VALUES (?,?)", postId, userId)
+ if err != nil {
+ return e.New(http.StatusInternalServerError, e.Internal)
+ }
+ db.Conn.Exec("UPDATE posts SET like_count=like_count+1 WHERE id=?", postId)
+ }
+
+ return nil
+}
+
+func GetLiked(postID, userID int64) bool {
+ liked := false
+ var nid int64
+ err := db.Conn.QueryRow("SELECT post_id FROM post_like WHERE post_id=? and user_id=?", postID, userID).Scan(&nid)
+ if err != nil && err != sql.ErrNoRows {
+ logger.Warn("query post like error", "error", err)
+ return false
+ }
+
+ if nid != 0 {
+ liked = true
+ }
+
+ return liked
+}
diff --git a/server/internal/posts/post.go b/server/internal/posts/post.go
index 339f9e4a..e8e8c0bd 100644
--- a/server/internal/posts/post.go
+++ b/server/internal/posts/post.go
@@ -97,14 +97,17 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
md := utils.Compress(post.Md)
setSlug(user.ID, post)
+
if post.ID == 0 {
//create
- _, err = db.Conn.Exec("INSERT INTO posts (creator,slug, title, md, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?,?,?)",
+ res, err := db.Conn.Exec("INSERT INTO posts (creator,slug, title, md, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?,?,?)",
user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, now)
if err != nil {
logger.Warn("submit post error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
+
+ post.ID, _ = res.LastInsertId()
} else {
// 只有创建者自己才能更新内容
creator, _ := GetPostCreator(post.ID)
@@ -120,6 +123,23 @@ func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
}
}
+ //update tags
+ // "tag_post": `CREATE TABLE IF NOT EXISTS tag_post (
+ // tag_id INTEGER,
+ // post_id INTEGER
+ // );
+ _, err = db.Conn.Exec("DELETE FROM tag_post WHERE post_id=?", post.ID)
+ if err != nil {
+ logger.Warn("delete post tags error", "error", err)
+ }
+
+ for _, tag := range post.Tags {
+ _, err = db.Conn.Exec("INSERT INTO tag_post (tag_id,post_id) VALUES (?,?)", tag, post.ID)
+ if err != nil {
+ logger.Warn("add post tag error", "error", err)
+ }
+ }
+
return map[string]string{
"username": user.Username,
"slug": post.Slug,
@@ -136,11 +156,11 @@ func DeletePost(id int64) *e.Error {
return nil
}
-func GetPost(id int64) (*models.Post, *e.Error) {
+func GetPost(id int64, slug string) (*models.Post, *e.Error) {
ar := &models.Post{}
var rawmd []byte
- err := db.Conn.QueryRow("select id,slug,title,md,url,cover,brief,creator,created,updated from posts where id=?", id).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,slug,title,md,url,cover,brief,creator,like_count,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.Likes, &ar.Created, &ar.Updated,
)
if err != nil {
if err == sql.ErrNoRows {
@@ -155,6 +175,19 @@ func GetPost(id int64) (*models.Post, *e.Error) {
ar.Creator = &models.UserSimple{ID: ar.CreatorID}
err = ar.Creator.Query()
+ // get tags
+ tags := make([]int64, 0)
+ rows, err := db.Conn.Query("SELECT tag_id FROM tag_post WHERE post_id=?", id)
+ if err != nil && err != sql.ErrNoRows {
+ return nil, e.New(http.StatusInternalServerError, e.Internal)
+ }
+ for rows.Next() {
+ var tag int64
+ err = rows.Scan(&tag)
+ tags = append(tags, tag)
+ }
+ ar.Tags = tags
+
return ar, nil
}
@@ -172,6 +205,21 @@ func GetPostCreator(id int64) (int64, *e.Error) {
return uid, nil
}
+func postExist(id int64) bool {
+ var nid int64
+ err := db.Conn.QueryRow("SELECT id from posts WHERE id=?", id).Scan(&nid)
+ if err != nil {
+ logger.Warn("query post error", "error", err)
+ return false
+ }
+
+ if nid == 0 {
+ return false
+ }
+
+ return true
+}
+
//slug有三个规则
// 1. 长度不能超过127
// 2. 每次title更新,都要重新生成slug
@@ -189,7 +237,6 @@ func setSlug(creator int64, post *models.Post) error {
return err
}
- fmt.Println(count)
if count == 0 {
post.Slug = slug
} else {
diff --git a/server/internal/posts/tags.go b/server/internal/posts/tags.go
index 12fdd22f..049a87d7 100644
--- a/server/internal/posts/tags.go
+++ b/server/internal/posts/tags.go
@@ -84,6 +84,8 @@ func GetTags() (models.Tags, *e.Error) {
}
tags = append(tags, tag)
+
+ db.Conn.QueryRow("SELECT count(*) FROM tag_post WHERE tag_id=?", tag.ID).Scan(&tag.PostCount)
}
sort.Sort(tags)
@@ -118,5 +120,7 @@ func GetTag(name string) (*models.Tag, *e.Error) {
md, _ := utils.Uncompress(rawmd)
tag.Md = string(md)
+ db.Conn.QueryRow("SELECT count(*) FROM tag_post WHERE tag_id=?", tag.ID).Scan(&tag.PostCount)
+
return tag, nil
}
diff --git a/server/internal/server.go b/server/internal/server.go
index a0e1fff5..e7ceebff 100644
--- a/server/internal/server.go
+++ b/server/internal/server.go
@@ -45,6 +45,13 @@ func (s *Server) Start() error {
r.POST("/login", session.Login)
r.POST("/logout", session.Logout)
r.GET("/uiconfig", GetUIConfig)
+
+ }
+
+ postR := r.Group("/post")
+ {
+ postR.GET("/:slug", api.GetPost)
+ postR.POST("/like/:id", api.LikePost, IsLogin())
}
// login apis
diff --git a/server/internal/storage/sql_tables.go b/server/internal/storage/sql_tables.go
index 5d7bb1ff..cc648d79 100644
--- a/server/internal/storage/sql_tables.go
+++ b/server/internal/storage/sql_tables.go
@@ -37,7 +37,7 @@ var sqlTables = map[string]string{
url VARCHAR(255),
cover VARCHAR(255),
brief TEXT,
-
+ like_count INTEGER DEFAULT 0,
created DATETIME NOT NULL,
updated DATETIME
);
@@ -49,6 +49,16 @@ var sqlTables = map[string]string{
ON posts (creator, slug);
`,
+ "post_like": `CREATE TABLE IF NOT EXISTS post_like (
+ post_id INTEGER,
+ user_id INTEGER
+ );
+ CREATE INDEX IF NOT EXISTS post_like_postid
+ ON post_like (post_id);
+ CREATE INDEX IF NOT EXISTS post_like_userid
+ ON post_like (user_id);
+ `,
+
"tags": `CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator INTEGER NOT NULL,
@@ -57,7 +67,7 @@ var sqlTables = map[string]string{
icon VARCHAR(255),
cover VARCHAR(255),
md TEXT,
-
+ follower_count INTEGER DEFAULT 0,
created DATETIME NOT NULL,
updated DATETIME
);
@@ -66,4 +76,14 @@ var sqlTables = map[string]string{
CREATE INDEX IF NOT EXISTS tags_created
ON tags (created);
`,
+
+ "tag_post": `CREATE TABLE IF NOT EXISTS tag_post (
+ tag_id INTEGER,
+ post_id INTEGER
+ );
+ CREATE INDEX IF NOT EXISTS tag_post_tagid
+ ON tag_post (tag_id);
+ CREATE INDEX IF NOT EXISTS tag_post_postid
+ ON tag_post (post_id);
+ `,
}
diff --git a/server/internal/ui_config.go b/server/internal/ui_config.go
index fd2a4284..7f7b3607 100644
--- a/server/internal/ui_config.go
+++ b/server/internal/ui_config.go
@@ -9,21 +9,24 @@ import (
)
type UIConfig struct {
- Posts *UIPosts `json:"posts"`
+ Posts *PostsConfig `json:"posts"`
}
-type UIPosts struct {
+type PostsConfig struct {
TitleMaxLen int `json:"titleMaxLen"`
BriefMaxLen int `json:"briefMaxLen"`
WritingEnabled bool `json:"writingEnabled"`
+ MaxTags int `json:"maxTags"`
}
+// 在后台页面配置,存储到mysql中
func GetUIConfig(c *gin.Context) {
conf := &UIConfig{
- Posts: &UIPosts{
+ Posts: &PostsConfig{
TitleMaxLen: config.Data.Posts.TitleMaxLen,
BriefMaxLen: config.Data.Posts.BriefMaxLen,
WritingEnabled: config.Data.Posts.WritingEnabled,
+ MaxTags: 2,
},
}
diff --git a/server/pkg/models/post.go b/server/pkg/models/post.go
index af6f59f9..779885d1 100644
--- a/server/pkg/models/post.go
+++ b/server/pkg/models/post.go
@@ -3,17 +3,21 @@ package models
import "time"
type Post struct {
- ID int64 `json:"id"`
- Creator *UserSimple `json:"creator"`
- CreatorID int64 `json:"creatorId"`
- Title string `json:"title"`
- Slug string `json:"slug"`
- Md string `json:"md"`
- URL string `json:"url"`
- Cover string `json:"cover"`
- Brief string `json:"brief"`
- Created time.Time `json:"created"`
- Updated time.Time `json:"updated"`
+ ID int64 `json:"id"`
+ Creator *UserSimple `json:"creator"`
+ CreatorID int64 `json:"creatorId"`
+ Title string `json:"title"`
+ Slug string `json:"slug"`
+ Md string `json:"md"`
+ URL string `json:"url"`
+ Cover string `json:"cover"`
+ Brief string `json:"brief"`
+ Tags []int64 `json:"tags"`
+ Likes int `json:"likes"`
+ Liked bool `json:"liked"`
+ Recommands int `json:"recommands"`
+ Created time.Time `json:"created"`
+ Updated time.Time `json:"updated"`
}
type Posts []*Post
diff --git a/server/pkg/models/tag.go b/server/pkg/models/tag.go
index 762fcb7f..61ec34bb 100644
--- a/server/pkg/models/tag.go
+++ b/server/pkg/models/tag.go
@@ -3,15 +3,16 @@ package models
import "time"
type Tag struct {
- ID int64 `json:"id"`
- Creator int64 `json:"creator"`
- Title string `json:"title"`
- Name string `json:"name"`
- Md string `json:"md"`
- Cover string `json:"cover"`
- Icon string `json:"icon"`
- Created time.Time `json:"created"`
- Updated time.Time `json:"updated"`
+ ID int64 `json:"id"`
+ Creator int64 `json:"creator"`
+ Title string `json:"title"`
+ Name string `json:"name"`
+ Md string `json:"md"`
+ Cover string `json:"cover"`
+ Icon string `json:"icon"`
+ PostCount int `json:"postCount"`
+ Created time.Time `json:"created"`
+ Updated time.Time `json:"updated"`
}
type Tags []*Tag
diff --git a/src/components/account-menu.tsx b/src/components/account-menu.tsx
new file mode 100644
index 00000000..2720cdb3
--- /dev/null
+++ b/src/components/account-menu.tsx
@@ -0,0 +1,78 @@
+import React from "react"
+import {
+ IconButton,
+ Menu,
+ MenuButton,
+ MenuList,
+ MenuItem,
+ MenuDivider,
+ Image,
+ Button
+} from "@chakra-ui/react"
+import useSession from "hooks/use-session"
+import { Session } from "src/types/session"
+import { useRouter } from "next/router"
+import storage from "utils/localStorage"
+import { ReserveUrls } from "src/data/reserve-urls"
+import { FaRegSun, FaUserAlt ,FaBookmark, FaSignOutAlt,FaEdit,FaStar} from "react-icons/fa"
+import { isAdmin, isEditor } from "utils/role"
+import { logout } from "utils/session"
+import Link from "next/link"
+
+export const AccountMenu = () => {
+ const session: Session = useSession()
+ const router = useRouter()
+
+ const login = () => {
+ console.log(router)
+ storage.set("current-page", router.asPath)
+ router.push(ReserveUrls.Login)
+ }
+
+ return (
+ <>
+ {session ?
+ :
+
+ }
+ >
+ )
+}
+
+export default AccountMenu
diff --git a/src/components/container.tsx b/src/components/container.tsx
index 5814aebf..e58184ec 100644
--- a/src/components/container.tsx
+++ b/src/components/container.tsx
@@ -8,7 +8,7 @@ export const Container = (props: BoxProps) => (
pt="3"
maxW="1200px"
mx="auto"
- px={{ base: "4", md: "8" }}
+ px={[0,0,4,8]}
{...props}
/>
)
diff --git a/src/components/dark-mode.tsx b/src/components/dark-mode.tsx
new file mode 100644
index 00000000..0d85a7aa
--- /dev/null
+++ b/src/components/dark-mode.tsx
@@ -0,0 +1,24 @@
+import React from "react"
+import { IconButton, useColorMode, useColorModeValue } from "@chakra-ui/react"
+import { FaMoon, FaSun } from "react-icons/fa"
+
+export const DarkMode = () => {
+ const { toggleColorMode: toggleMode } = useColorMode()
+ const text = useColorModeValue("dark", "light")
+ const SwitchIcon = useColorModeValue(FaMoon, FaSun)
+
+ return (
+ }
+ />
+ )
+}
+
+export default DarkMode
diff --git a/src/components/like-button.tsx b/src/components/like-button.tsx
new file mode 100644
index 00000000..b72c28d8
--- /dev/null
+++ b/src/components/like-button.tsx
@@ -0,0 +1,41 @@
+import { chakra, HStack, IconButton, Image, Tooltip, useColorMode, useColorModeValue } from "@chakra-ui/react";
+
+interface Props {
+ type: string
+ count: number
+ onClick: any
+ liked: boolean
+}
+const LikeButton = (props: Props) => {
+ let imgSrc: string
+ let label: string
+ switch (props.type) {
+ case "like":
+ imgSrc = "https://cdn.hashnode.com/res/hashnode/image/upload/v1594643814744/9iXxz71TL.png?auto=compress"
+ label = "Love it"
+ break;
+ case "unicorn":
+ imgSrc = "https://cdn.hashnode.com/res/hashnode/image/upload/v1594643772437/FYDU5k2kQ.png?auto=compress"
+ label = "I love it"
+ default:
+ break;
+ }
+ return (
+
+
+ }
+ onClick={props.onClick}
+ border={props.liked ? `1px solid ${useColorModeValue('gray','pink')}` : null}
+ />
+
+ {props.count}
+
+ )
+}
+
+export default LikeButton
\ No newline at end of file
diff --git a/src/components/markdown-editor/editor.tsx b/src/components/markdown-editor/editor.tsx
index 57efd2b4..2ffb7c22 100644
--- a/src/components/markdown-editor/editor.tsx
+++ b/src/components/markdown-editor/editor.tsx
@@ -25,9 +25,10 @@ export function MarkdownEditor(props) {
return (
null}
onChange={handleEditorChange}
config={{
diff --git a/src/components/markdown-editor/render.tsx b/src/components/markdown-editor/render.tsx
index f34370bd..71c3face 100644
--- a/src/components/markdown-editor/render.tsx
+++ b/src/components/markdown-editor/render.tsx
@@ -11,6 +11,7 @@ type Props = PropsOf & {
fontSize?: string
}
+const ChakraMarkdown = chakra(Markdown)
export function MarkdownRender({ md,fontSize, ...rest }:Props) {
const rootRef = useRef();
@@ -23,10 +24,11 @@ export function MarkdownRender({ md,fontSize, ...rest }:Props) {
return (
-
+ >
);
}
\ No newline at end of file
diff --git a/src/components/posts/post-author.tsx b/src/components/posts/post-author.tsx
new file mode 100644
index 00000000..bccb9c89
--- /dev/null
+++ b/src/components/posts/post-author.tsx
@@ -0,0 +1,35 @@
+import React from "react"
+import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Box, Avatar, VStack} from "@chakra-ui/react"
+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 moment from 'moment'
+import { FaGithub } from "react-icons/fa"
+import Link from "next/link"
+import { useRouter } from "next/router"
+
+type Props = PropsOf & {
+ post : Post
+}
+
+export const PostAuthor= ({post}:Props) =>{
+ const router = useRouter()
+ console.log(post)
+ return (
+
+
+ router.push(`/${post.creator.username}`)} cursor="pointer"/>
+
+ router.push(`/${post.creator.username}`)} cursor="pointer">{post.creator.nickname === "" ? post.creator.username : post.creator.nickname}
+ 发布于{moment(post.created).fromNow()}
+
+ 4 min read
+
+
+
+
+ )
+}
+
+export default PostAuthor
diff --git a/src/components/posts/tag-card.tsx b/src/components/posts/tag-edit-card.tsx
similarity index 100%
rename from src/components/posts/tag-card.tsx
rename to src/components/posts/tag-edit-card.tsx
diff --git a/src/components/posts/tag-list-card.tsx b/src/components/posts/tag-list-card.tsx
new file mode 100644
index 00000000..9c4d4bda
--- /dev/null
+++ b/src/components/posts/tag-list-card.tsx
@@ -0,0 +1,28 @@
+import React from "react"
+import {Box, Heading, Image, Text, HStack,Button, Flex,PropsOf,Link} from "@chakra-ui/react"
+import { Tag } from "src/types/tag"
+import { ReserveUrls } from "src/data/reserve-urls"
+import NextLink from "next/link"
+
+interface Props {
+ tag: Tag
+}
+
+
+export const TagCard= (props:Props) =>{
+ const {tag} = props
+
+ return (
+
+
+
+ {tag.title}
+
+ {tag.postCount} posts
+
+
+
+ )
+}
+
+export default TagCard
diff --git a/src/components/posts/tag-text-card.tsx b/src/components/posts/tag-text-card.tsx
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/tag-input.tsx b/src/components/tag-input.tsx
new file mode 100644
index 00000000..9b9b1552
--- /dev/null
+++ b/src/components/tag-input.tsx
@@ -0,0 +1,68 @@
+import React, { useEffect, useState } from "react"
+import { Box, Popover, PopoverTrigger, Button, PopoverContent, PopoverBody, Input, useDisclosure, Divider, useToast } from "@chakra-ui/react"
+import { Tag } from "src/types/tag"
+import { requestApi } from "utils/axios/request"
+import { cloneDeep, findIndex } from "lodash"
+import TagCard from 'src/components/posts/tag-list-card'
+import { config } from "utils/config"
+interface Props {
+ options: Tag[]
+ selected: Tag[]
+ onChange: any
+}
+
+
+export const TagInput = (props: Props) => {
+ const toast = useToast()
+ const [tags, setTags]: [Tag[], any] = useState([])
+
+ const { onOpen, onClose, isOpen } = useDisclosure()
+
+ const filterTags = query => {
+ if (query.trim() === "") {
+ setTags([])
+ return
+ }
+
+ const newTags = []
+ props.options.forEach(tag => {
+ if (tag.title.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
+ if (findIndex(props.selected,t => t.id === tag.id) === -1) {
+ newTags.push(tag)
+ }
+ }
+ })
+
+ setTags(newTags)
+ }
+
+ const addTag = tag => {
+ const t = cloneDeep(props.selected)
+ t.push(tag)
+ props.onChange(t)
+ }
+
+ return (
+ <>
+ {props.selected.length <=config.posts.maxTags && filterTags(e.target.value)} onFocus={onOpen} onBlur={onClose} placeholder="start typing to search.." variant="unstyled" _focus={null} mt="3" />}
+ {tags.length > 0 &&
+
+
+
+ {tags.map((tag, i) => {
+ return addTag(tag)}>
+
+
+
+ {i < tags.length - 1 && }
+
+ })
+ }
+
+
+ }
+ >
+ )
+}
+
+export default TagInput
diff --git a/src/types/posts.ts b/src/types/posts.ts
index 221618a3..96d6c110 100644
--- a/src/types/posts.ts
+++ b/src/types/posts.ts
@@ -11,4 +11,8 @@ export interface Post {
cover?: string
brief?: string
created?: string
+ tags?: number[]
+ likes? : number
+ liked? : boolean
+ recommands? : number
}
\ No newline at end of file
diff --git a/src/types/tag.ts b/src/types/tag.ts
index aa063057..2140425f 100644
--- a/src/types/tag.ts
+++ b/src/types/tag.ts
@@ -6,4 +6,5 @@ export interface Tag {
icon?: string
cover?: string
created?: string
+ postCount?: number
}
\ No newline at end of file
diff --git a/src/utils/config.ts b/src/utils/config.ts
index 95dda93e..edd4219a 100644
--- a/src/utils/config.ts
+++ b/src/utils/config.ts
@@ -4,7 +4,8 @@ export let config = {
posts: {
titleMaxLen: 128,
briefMaxLen: 128,
- writingEnabled: false
+ writingEnabled: false,
+ maxTags: 0
}
}
diff --git a/theme.ts b/theme.ts
index 4dcc2ec8..8aaa35a4 100644
--- a/theme.ts
+++ b/theme.ts
@@ -1,6 +1,7 @@
import { extendTheme } from "@chakra-ui/react"
import { mode } from "@chakra-ui/theme-tools"
-import reactMarkdownStyls from 'theme/react-markdown-editor'
+import markdownEditor from 'theme/markdown-editor'
+import markdownRender from 'theme/markdown-render'
import layerStyles from 'theme/layer-styles'
const customTheme = extendTheme({
@@ -22,7 +23,7 @@ const customTheme = extendTheme({
styles: {
global: (props) => ({
body: {
- background: mode("gray.50","gray.800" )(props),
+ background: mode("white","gray.800" )(props),
color: mode("gray.700", "whiteAlpha.900")(props),
".deleted": {
color: "#ff8383 !important",
@@ -33,7 +34,8 @@ const customTheme = extendTheme({
fontStyle: "normal !important",
},
},
- ...reactMarkdownStyls(props)
+ ...markdownEditor(props),
+ ...markdownRender(props)
}),
},
textStyles: {
diff --git a/theme/react-markdown-editor.js b/theme/markdown-editor.ts
similarity index 93%
rename from theme/react-markdown-editor.js
rename to theme/markdown-editor.ts
index c2906ed8..5d5b7933 100644
--- a/theme/react-markdown-editor.js
+++ b/theme/markdown-editor.ts
@@ -1,8 +1,7 @@
import { mode } from "@chakra-ui/theme-tools"
import userCustomTheme from "./user-custom"
-export default function reactMarkdownStyles(props) {
- console.log(props)
+export default function markdownEditor(props) {
return {
'.rc-md-editor': {
borderWidth: '0px',
@@ -10,6 +9,7 @@ export default function reactMarkdownStyles(props) {
textarea: {
background: 'transparent!important',
color: mode("#2D3748!important", "rgba(255, 255, 255, 0.92)!important")(props),
+ fontSize: '16px !important'
},
'.rc-md-navigation' :{
background: 'transparent',
diff --git a/theme/markdown-render.ts b/theme/markdown-render.ts
new file mode 100644
index 00000000..781d88e9
--- /dev/null
+++ b/theme/markdown-render.ts
@@ -0,0 +1,62 @@
+import { mode } from "@chakra-ui/theme-tools"
+import userCustomTheme from "./user-custom"
+
+export default function markdownRender(props) {
+ return {
+ '.markdown-render': {
+ '.hljs' : {
+ padding: '1rem',
+ borderRadius: '8px'
+ },
+ 'ul,ol' : {
+ paddingLeft: '1rem',
+ margin: '1.2rem 0',
+ li: {
+ margin: '.8rem 0'
+ }
+ },
+ 'h1': {
+ fontSize: '2rem',
+ fontWeight: 'bold',
+ marginBottom: '0.8rem'
+ },
+ 'h2': {
+ fontSize: '1.8rem',
+ fontWeight: 'bold',
+ marginBottom: '0.6rem'
+ },
+ 'h3': {
+ fontSize: '1.6em',
+ fontWeight: '600',
+ marginBottom: '0.4rem'
+ },
+ 'h4': {
+ fontSize: '1.4em',
+ fontWeight: '600'
+ },
+ 'h5,h6': {
+ fontSize: '1.2em',
+ fontWeight: 'normal'
+ },
+ p: {
+ margin: '1.2rem 0',
+ },
+ blockquote: {
+ lineHeight: '2rem',
+ margin: '1.5rem 0',
+ p :{
+ paddingLeft: '1rem',
+ fontWeight: '500',
+ fontStyle: 'italic',
+ borderLeftWidth: '.25rem',
+ borderLeftColor: '#e5e7eb',
+ color: mode("inherit", "'rgb(189, 189, 189)'")(props),
+ fontSize: '1.2rem',
+ }
+ },
+ pre: {
+ margin: '1.6rem 0'
+ }
+ }
+ }
+}
\ No newline at end of file