pull/50/head
sunface 4 years ago
parent f66164e834
commit 4a148aa80c

@ -38,6 +38,7 @@
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-icons": "^4.1.0",
"short-number": "^1.0.7",
"validator": "^13.5.2"
},
"devDependencies": {

@ -1,4 +1,4 @@
import { Box, chakra, Divider, Flex, Heading, HStack, IconButton, Image, VStack } from "@chakra-ui/react"
import { Box, chakra, Divider, Flex, Heading, HStack, IconButton, Image, storageKey, VStack } from "@chakra-ui/react"
import Comments from "components/comments/comments"
import Container from "components/container"
import LikeButton from "components/posts/unicorn-like"
@ -23,13 +23,14 @@ import { Tag } from "src/types/tag"
import { requestApi } from "utils/axios/request"
import UnicornLike from "components/posts/unicorn-like"
import SvgButton from "components/svg-button"
import Bookmark from "components/posts/bookmark"
import PostSidebar from "components/posts/post-sidebar"
const PostPage = () => {
const router = useRouter()
const id = router.query.post_id
const [post, setPost]: [Post, any] = useState(null)
const [comments,setComments]: [Comment[],any] = useState([])
const session = useSession()
const [comments, setComments]: [Comment[], any] = useState([])
useEffect(() => {
if (id) {
getData()
@ -41,9 +42,9 @@ const PostPage = () => {
if (router && router.asPath.indexOf("#comments") > -1) {
setTimeout(() => {
location.href = "#comments"
},100)
}, 100)
}
},[router])
}, [router])
const getData = async () => {
const res = await requestApi.get(`/post/${id}`)
@ -52,20 +53,6 @@ const PostPage = () => {
getComments(res.data.id)
}
const onLike = async () => {
await requestApi.post(`/story/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)
}
const getComments = async (id) => {
const res = await requestApi.get(`/story/comments/${id}`)
setComments(res.data)
@ -81,7 +68,7 @@ const PostPage = () => {
{post &&
<>
<HStack alignItems="top" spacing={[0, 0, 14, 14]}>
<Box width={["100%", "100%", "75%", "75%"]} height="fit-content" pl={[0,0,"0%","10%"]}>
<Box width={["100%", "100%", "75%", "75%"]} height="fit-content" pl={[0, 0, "0%", "10%"]}>
<Image src={post.cover} />
<Box px="2">
<Heading size="xl" my="6" lineHeight="1.5">{post.title}</Heading>
@ -92,84 +79,12 @@ const PostPage = () => {
<MarkdownRender md={post.md} py="2" mt="6" />
</Box>
<HStack ml="4" spacing="3">{post.rawTags.map(tag => <TagTextCard tag={tag}/>)}</HStack>
<HStack ml="2" spacing="3" mt="4">{post.rawTags.map(tag => <TagTextCard key={tag.id} tag={tag} />)}</HStack>
<HStack display={{ base: "flex", md: 'none' }} spacing="4" justifyContent="center">
<Box>
{/* <LikeButton type="like" count={post.likes} onClick={onLike}/> */}
<UnicornLike count={post.likes} onClick={onLike} liked={post.liked}/>
<Box mt="6" p="2"><Comments storyID={post.id} comments={comments} onChange={() => getComments(post.id)} /></Box>
</Box>
<Box>
<IconButton
aria-label="go to github"
variant="ghost"
layerStyle="textSecondary"
_focus={null}
fontSize="1.7rem"
fontWeight="300"
icon={<svg height="1.8rem" fill="currentColor" viewBox="0 0 384 512"><path d="M336 0H48C21.49 0 0 21.49 0 48v464l192-112 192 112V48c0-26.51-21.49-48-48-48zm16 456.287l-160-93.333-160 93.333V48c0-8.822 7.178-16 16-16h288c8.822 0 16 7.178 16 16v408.287z"></path></svg>}
/>
<IconButton
aria-label="go to github"
variant="ghost"
layerStyle="textSecondary"
_focus={null}
fontWeight="300"
icon={<svg height="1.8rem" fill="currentColor" viewBox="0 0 448 512"><path d="M352 320c-28.6 0-54.2 12.5-71.8 32.3l-95.5-59.7c9.6-23.4 9.7-49.8 0-73.2l95.5-59.7c17.6 19.8 43.2 32.3 71.8 32.3 53 0 96-43 96-96S405 0 352 0s-96 43-96 96c0 13 2.6 25.3 7.2 36.6l-95.5 59.7C150.2 172.5 124.6 160 96 160c-53 0-96 43-96 96s43 96 96 96c28.6 0 54.2-12.5 71.8-32.3l95.5 59.7c-4.7 11.3-7.2 23.6-7.2 36.6 0 53 43 96 96 96s96-43 96-96c-.1-53-43.1-96-96.1-96zm0-288c35.3 0 64 28.7 64 64s-28.7 64-64 64-64-28.7-64-64 28.7-64 64-64zM96 320c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64zm256 160c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64z"></path></svg>}
/>
</Box>
</HStack>
<Box mt="6" p="2"><Comments storyID={post.id} comments={comments} onChange={() => getComments(post.id)}/></Box>
</Box>
<Box>
<VStack alignItems="left" pos="fixed" display={{ base: "none", md: 'flex' }} width={["100%", "100%", "15%", "15%"]}>
<Box pt="16">
{/* <HStack mt="16"> */}
{/* <LikeButton type="like" count={post.likes} onClick={onLike} /> */}
<UnicornLike count={post.likes} onClick={onLike} liked={post.liked}/>
{/* </HStack> */}
</Box>
<Box>
<SvgButton
mt="6"
aria-label="go to github"
variant="ghost"
layerStyle="textSecondary"
_focus={null}
fontSize="1.7rem"
fontWeight="300"
icon="bookmark"
onClick={null}
/>
<Box mt="4">
<SvgButton
aria-label="go to github"
variant="ghost"
layerStyle="textSecondary"
_focus={null}
fontWeight="300"
icon="share"
onClick={() => location.href="#comments"}
/>
</Box>
{post.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}`)}
icon="edit"
/>
</Box>}
</Box>
</VStack>
<PostSidebar post={post}/>
</Box>
</HStack>
@ -182,3 +97,4 @@ const PostPage = () => {
export default PostPage

@ -21,6 +21,7 @@ import userCustomTheme from "theme/user-custom"
import Posts from "components/posts/posts"
import Link from "next/link"
import Empty from "components/empty"
import Count from "components/count"
const UserPage = () => {
const router = useRouter()
@ -60,8 +61,8 @@ const UserPage = () => {
<Heading fontSize="1.8rem">{user.nickname}</Heading>
{user.tagline && <Text layerStyle="textSecondary" fontWeight="450" fontSize="1.2rem" ml="1" mt="2">{user.tagline}</Text>}
<Flex layerStyle="textSecondary" spacing="2" pt="1" alignItems="center">
<chakra.span><FaHeart /></chakra.span><chakra.span ml="1">Followers <chakra.a fontWeight="600">0</chakra.a></chakra.span>
<chakra.span ml="5"><FaStar /></chakra.span><chakra.span ml="1">Following <chakra.a fontWeight="600">0</chakra.a></chakra.span>
<chakra.span><FaHeart /></chakra.span><chakra.span ml="1">Followers <chakra.a fontWeight="600"><Count count={12312312312}/></chakra.a></chakra.span>
<chakra.span ml="5"><FaStar /></chakra.span><chakra.span ml="1">Following <chakra.a fontWeight="600"><Count count={0}/></chakra.a></chakra.span>
</Flex>
<Box pt="3" position="absolute" right="15px" top="60px">{session?.user.id === user.id ? <Button onClick={() => router.push(`${ReserveUrls.Settings}/profile`)} variant="outline" leftIcon={<svg height="1.3rem" fill="currentColor" viewBox="0 0 512 512"><path d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zm-95.196 140.45L174 420.745V386h-48v-48H91.255l224.059-224.059 82.745 82.745zM126.147 468.598l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>}><chakra.span display={{base:"none",md:"block"}}>Edit Profile</chakra.span></Button>
: <Button colorScheme="teal">Follow</Button>}</Box>

@ -1,50 +0,0 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"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"
)
func GetEditorPosts(c *gin.Context) {
user := user.CurrentUser(c)
ars, err := story.UserPosts(int64(user.ID))
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(ars))
}
func GetEditorPost(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
user := user.CurrentUser(c)
creator, err := story.GetPostCreator(id)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
if user.ID != creator {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
ar, err := story.GetPost(id, "")
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(ar))
}

@ -0,0 +1,60 @@
package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/story"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
)
func GetEditorPosts(c *gin.Context) {
user := user.CurrentUser(c)
ars, err := story.UserPosts(user, int64(user.ID))
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(ars))
}
func GetUserPosts(c *gin.Context) {
userID, _ := strconv.ParseInt(c.Param("userID"), 10, 64)
user := user.CurrentUser(c)
posts, err := story.UserPosts(user, userID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(posts))
}
func GetTagPosts(c *gin.Context) {
tagID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
user := user.CurrentUser(c)
posts, err := story.TagPosts(user, tagID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(posts))
}
func GetHomePosts(c *gin.Context) {
filter := c.Param("filter")
user := user.CurrentUser(c)
posts, err := story.HomePosts(user, filter)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(posts))
}

@ -2,7 +2,6 @@ package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/story"
@ -51,18 +50,16 @@ func DeletePost(c *gin.Context) {
func GetPost(c *gin.Context) {
id := c.Param("id")
user := user.CurrentUser(c)
ar, err := story.GetPost(id, "")
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
user := user.CurrentUser(c)
if user == nil {
ar.Liked = false
} else {
if user != nil {
ar.Liked = story.GetLiked(ar.ID, user.ID)
ar.Bookmarked, _ = story.Bookmarked(user.ID, ar.ID)
}
c.JSON(http.StatusOK, common.RespSuccess(ar))
@ -85,59 +82,44 @@ func LikeStory(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func GetUserPosts(c *gin.Context) {
userID, _ := strconv.ParseInt(c.Param("userID"), 10, 64)
func Bookmark(c *gin.Context) {
storyID := c.Param("storyID")
posts, err := story.UserPosts(userID)
user := user.CurrentUser(c)
err := story.Bookmark(user.ID, storyID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
user := user.CurrentUser(c)
if user != nil {
for _, post := range posts {
post.Liked = story.GetLiked(post.ID, user.ID)
}
}
c.JSON(http.StatusOK, common.RespSuccess(posts))
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func GetTagPosts(c *gin.Context) {
tagID, _ := strconv.ParseInt(c.Param("id"), 10, 64)
func GetEditorPost(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
posts, err := story.TagPosts(tagID)
user := user.CurrentUser(c)
creator, err := story.GetPostCreator(id)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
user := user.CurrentUser(c)
if user != nil {
for _, post := range posts {
post.Liked = story.GetLiked(post.ID, user.ID)
}
if user.ID != creator {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
c.JSON(http.StatusOK, common.RespSuccess(posts))
}
func GetHomePosts(c *gin.Context) {
filter := c.Param("filter")
posts, err := story.HomePosts(filter)
ar, err := story.GetPost(id, "")
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
user := user.CurrentUser(c)
if user != nil {
for _, post := range posts {
post.Liked = story.GetLiked(post.ID, user.ID)
}
}
c.JSON(http.StatusOK, common.RespSuccess(posts))
c.JSON(http.StatusOK, common.RespSuccess(ar))
}

@ -79,6 +79,9 @@ func (s *Server) Start() error {
r.GET("/home/posts/:filter", api.GetHomePosts)
r.GET("/session", IsLogin(), api.GetSession)
r.POST("/bookmark/:storyID", IsLogin(), api.Bookmark)
err := router.Run(config.Data.Server.Addr)
if err != nil {
logger.Crit("start backend server error", "error", err)

@ -141,4 +141,13 @@ var sqlTables = map[string]string{
count INTEGER DEFAULT 0
);
`,
"bookmarks": `CREATE TABLE IF NOT EXISTS bookmarks (
user_id INTEGER,
story_id VARCHAR(255),
created DATETIME
);
CREATE INDEX IF NOT EXISTS bookmarks_userid
ON bookmarks (user_id);
`,
}

@ -0,0 +1,52 @@
package story
import (
"database/sql"
"net/http"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
)
func Bookmark(userID int64, storyID string) *e.Error {
storyExist := Exist(storyID)
if !storyExist {
return e.New(http.StatusNotFound, e.NotFound)
}
bookmarked, err := Bookmarked(userID, storyID)
if err != nil {
return e.New(http.StatusInternalServerError, e.Internal)
}
if !bookmarked {
_, err = db.Conn.Exec("insert into bookmarks (user_id,story_id) values (?,?)", userID, storyID)
if err != nil {
logger.Warn("add bookmark error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
} else {
_, err = db.Conn.Exec("delete from bookmarks where user_id=? and story_id=?", userID, storyID)
if err != nil {
logger.Warn("delete bookmark error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
}
return nil
}
func Bookmarked(userID int64, storyID string) (bool, error) {
var nid string
err := db.Conn.QueryRow("select story_id from bookmarks where user_id=? and story_id=?", userID, storyID).Scan(&nid)
if err != nil && err != sql.ErrNoRows {
logger.Warn("get bookmarked error", "error", err)
return false, err
}
if err == sql.ErrNoRows {
return false, nil
}
return true, nil
}

@ -4,7 +4,6 @@ import (
"database/sql"
"fmt"
"net/http"
"sort"
"strings"
"time"
"unicode/utf8"
@ -20,81 +19,6 @@ import (
"github.com/imdotdev/im.dev/server/pkg/utils"
)
func UserPosts(uid int64) (models.Posts, *e.Error) {
ars := make(models.Posts, 0)
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,likes,views,created,updated from posts where creator=?", uid)
if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal)
}
creator := &models.UserSimple{ID: uid}
creator.Query()
for rows.Next() {
ar := &models.Post{}
err := rows.Scan(&ar.ID, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Likes, &ar.Views, &ar.Created, &ar.Updated)
if err != nil {
logger.Warn("scan post error", "error", err)
continue
}
ar.Creator = creator
ar.Comments = GetCommentCount(ar.ID)
ars = append(ars, ar)
}
sort.Sort(ars)
return ars, nil
}
func TagPosts(tagID int64) (models.Posts, *e.Error) {
ars := make(models.Posts, 0)
// get post ids
rows, err := db.Conn.Query("select post_id from tag_post where tag_id=?", tagID)
if err != nil {
logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal)
}
postIDs := make([]string, 0)
for rows.Next() {
var id string
rows.Scan(&id)
postIDs = append(postIDs, id)
}
ids := strings.Join(postIDs, "','")
q := fmt.Sprintf("select id,slug,title,url,cover,brief,likes,views,creator,created,updated from posts 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)
return ars, e.New(http.StatusInternalServerError, e.Internal)
}
for rows.Next() {
ar := &models.Post{}
err := rows.Scan(&ar.ID, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Likes, &ar.Views, &ar.CreatorID, &ar.Created, &ar.Updated)
if err != nil {
logger.Warn("scan post error", "error", err)
continue
}
creator := &models.UserSimple{ID: ar.CreatorID}
creator.Query()
ar.Creator = creator
ar.Comments = GetCommentCount(ar.ID)
ars = append(ars, ar)
}
sort.Sort(ars)
return ars, nil
}
func SubmitPost(c *gin.Context) (map[string]string, *e.Error) {
user := user.CurrentUser(c)
@ -249,6 +173,7 @@ func GetPost(id string, slug string) (*models.Post, *e.Error) {
logger.Warn("update post view count error", "error", err)
}
//get bookmared
return ar, nil
}
@ -266,35 +191,6 @@ func GetPostCreator(id string) (int64, *e.Error) {
return uid, nil
}
func HomePosts(filter string) (models.Posts, *e.Error) {
ars := make(models.Posts, 0)
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,likes,views,creator,created,updated from posts")
if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err)
return ars, e.New(http.StatusInternalServerError, e.Internal)
}
for rows.Next() {
ar := &models.Post{}
err := rows.Scan(&ar.ID, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Likes, &ar.Views, &ar.CreatorID, &ar.Created, &ar.Updated)
if err != nil {
logger.Warn("scan post error", "error", err)
continue
}
creator := &models.UserSimple{ID: ar.CreatorID}
creator.Query()
ar.Creator = creator
ar.Comments = GetCommentCount(ar.ID)
ars = append(ars, ar)
}
sort.Sort(ars)
return ars, nil
}
func postExist(id string) bool {
var nid string
err := db.Conn.QueryRow("SELECT id from posts WHERE id=?", id).Scan(&nid)

@ -0,0 +1,101 @@
package story
import (
"database/sql"
"fmt"
"net/http"
"sort"
"strings"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
)
func HomePosts(user *models.User, filter string) (models.Posts, *e.Error) {
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,likes,views,creator,created,updated from posts")
if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
posts := getPosts(user, rows)
sort.Sort(posts)
return posts, nil
}
func UserPosts(user *models.User, uid int64) (models.Posts, *e.Error) {
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,likes,views,creator,created,updated from posts where creator=?", uid)
if err != nil && err != sql.ErrNoRows {
logger.Warn("get user posts 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 int64) (models.Posts, *e.Error) {
// get post ids
rows, err := db.Conn.Query("select post_id from tag_post where tag_id=?", tagID)
if err != nil {
logger.Warn("get user posts error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
postIDs := make([]string, 0)
for rows.Next() {
var id string
rows.Scan(&id)
postIDs = append(postIDs, id)
}
ids := strings.Join(postIDs, "','")
q := fmt.Sprintf("select id,slug,title,url,cover,brief,likes,views,creator,created,updated from posts 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)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
posts := getPosts(user, rows)
sort.Sort(posts)
return posts, nil
}
func getPosts(user *models.User, rows *sql.Rows) models.Posts {
posts := make(models.Posts, 0)
for rows.Next() {
ar := &models.Post{}
err := rows.Scan(&ar.ID, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Likes, &ar.Views, &ar.CreatorID, &ar.Created, &ar.Updated)
if err != nil {
logger.Warn("scan post error", "error", err)
continue
}
// 获取作者信息
creator := &models.UserSimple{ID: ar.CreatorID}
creator.Query()
ar.Creator = creator
// 获取评论信息
ar.Comments = GetCommentCount(ar.ID)
// 获取当前登录用户的like
if user != nil {
ar.Liked = GetLiked(ar.ID, user.ID)
}
// 获取当前登录用户的bookmark
ar.Bookmarked, _ = Bookmarked(user.ID, ar.ID)
posts = append(posts, ar)
}
return posts
}

@ -24,6 +24,7 @@ type Post struct {
Liked bool `json:"liked"`
Comments int `json:"comments"`
Views int `json:"views"`
Bookmarked bool `json:"bookmarked"`
Status int `json:"status"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`

@ -5,7 +5,7 @@ import Card from "components/card"
import { getUserName } from "utils/user"
import moment from 'moment'
import { MarkdownRender } from "components/markdown-editor/render"
import HeartLike from "components/posts/heart-like"
import Like from "components/posts/like"
import { FaRegEdit, FaRegFlag, FaRegTrashAlt, FaReply, FaTrash } from "react-icons/fa"
import { User } from "src/types/session"
import CommentEditor from "./editor"
@ -40,11 +40,6 @@ export const CommentCard = (props: Props) => {
onChange()
}
const likeComment = async (id) => {
await requestApi.post(`/story/like/${id}`)
onChange()
}
return (
<>{
editorVisible ? (user && <CommentEditor user={user} md={comment.md} onSubmit={md => {setEditorVisible(false);changeComment(md)}} onCancel={() => setEditorVisible(false)} menu={false}/>) :
@ -66,7 +61,7 @@ export const CommentCard = (props: Props) => {
<MarkdownRender md={comment.md} pl="16" pr="2" mt="3" />
<Flex justifyContent="space-between" pl="16" pr="2">
<HeartLike liked={comment.liked} count={comment.likes} onClick={() => likeComment(comment.id)} />
<Like liked={comment.liked} count={comment.likes} storyID={comment.id} />
<HStack>
{user && <IconButton
aria-label="go to github"

@ -5,7 +5,7 @@ import Card from "components/card"
import { getUserName } from "utils/user"
import moment from 'moment'
import { MarkdownRender } from "components/markdown-editor/render"
import HeartLike from "components/posts/heart-like"
import Like from "components/posts/like"
import { FaRegEdit, FaRegFlag, FaRegTrashAlt, FaReply, FaTrash } from "react-icons/fa"
import { User } from "src/types/session"
import CommentEditor from "./editor"
@ -76,7 +76,7 @@ export const Reply = (props: Props) => {
<MarkdownRender md={comment.md} pl="16" pr="2" mt="3" />
<Flex justifyContent="space-between" pl="16" pr="2">
<HeartLike liked={comment.liked} count={comment.likes} onClick={() => likeReply(comment.id)} />
<Like liked={comment.liked} count={comment.likes} storyID={comment.id} />
<HStack>
{user && <IconButton
aria-label="go to github"

@ -0,0 +1,15 @@
import React from "react"
import { chakra } from "@chakra-ui/react"
var shortNumber = require('short-number');
interface Props {
count: number
}
export const Count = (props: Props) => {
return (
<chakra.span title={props.count.toString()}>{shortNumber(props.count)}</chakra.span>
)
}
export default Count

@ -0,0 +1,35 @@
import { chakra, HStack, IconButton, Image, Tooltip, useColorMode, useColorModeValue } from "@chakra-ui/react";
import SvgButton from "components/svg-button";
import { useState } from "react";
import { FaHeart, FaRegHeart } from "react-icons/fa";
import { requestApi } from "utils/axios/request";
interface Props {
storyID: string
bookmarked: boolean
height?: string
}
const Bookmark = (props: Props) => {
const {storyID, height="1.4rem"} = props
const [bookmarked,setBookmarked] = useState(props.bookmarked)
const bookmark = async () => {
await requestApi.post(`/bookmark/${storyID}`)
setBookmarked(!bookmarked)
}
return (
<SvgButton
aria-label="bookmark"
variant="ghost"
layerStyle="textSecondary"
_focus={null}
icon={bookmarked ?"bookmarked" :"bookmark"}
onClick={bookmark}
height={height}
/>
)
}
export default Bookmark

@ -1,39 +0,0 @@
import { chakra, HStack, IconButton, Image, Tooltip, useColorMode, useColorModeValue } from "@chakra-ui/react";
import { FaHeart, FaRegHeart } from "react-icons/fa";
interface Props {
count: number
onClick: any
liked: boolean
}
const UnicornLike = (props: Props) => {
const label = "I like it"
return (
<HStack alignItems="center">
<Tooltip label={label} size="sm">
{props.liked? <IconButton
aria-label="go to github"
variant="ghost"
_focus={null}
color="red.400"
icon={<FaHeart />}
onClick={props.onClick}
fontSize="20px"
/> :
<IconButton
aria-label="go to github"
variant="ghost"
_focus={null}
color="gray.500"
icon={<FaRegHeart />}
onClick={props.onClick}
fontSize="20px"
/>}
</Tooltip>
<chakra.span layerStyle="textSecondary" fontWeight="600">{props.count}</chakra.span>
</HStack>
)
}
export default UnicornLike

@ -0,0 +1,61 @@
import { chakra, HStack, IconButton, Tooltip} from "@chakra-ui/react";
import Count from "components/count";
import { useState } from "react";
import { FaHeart, FaRegHeart } from "react-icons/fa";
import { requestApi } from "utils/axios/request";
interface Props {
storyID: string
count: number
liked: boolean
fontSize?: string
spacing?: string
}
const Like = (props: Props) => {
const {fontSize="20px",spacing="0"} = props
const label = "I like it"
const [liked,setLiked] = useState(props.liked)
const [count,setCount] = useState(props.count)
const like = async () => {
await requestApi.post(`/story/like/${props.storyID}`)
if (liked) {
setCount(count-1)
} else {
setCount(count+1)
}
setLiked(!liked)
}
return (
<HStack alignItems="center" spacing={spacing}>
<Tooltip label={label} size="sm">
{liked? <IconButton
aria-label="go to github"
variant="ghost"
_focus={null}
color="red.400"
icon={<FaHeart />}
onClick={like}
fontSize={fontSize}
/> :
<IconButton
aria-label="go to github"
variant="ghost"
_focus={null}
color="gray.500"
icon={<FaRegHeart />}
onClick={like}
fontSize={fontSize}
/>}
</Tooltip>
<chakra.span layerStyle="textSecondary" fontWeight="600"><Count count={count}/></chakra.span>
</HStack>
)
}
export default Like

@ -3,9 +3,11 @@ import { Box, chakra, Flex, Heading, HStack, Image, Text, useMediaQuery, VStack
import { Post } from "src/types/posts"
import PostAuthor from "./post-author"
import Link from "next/link"
import UnicornLike from "./heart-like"
import { FaHeart, FaRegBookmark, FaRegComment, FaRegHeart } from "react-icons/fa"
import SvgButton from "components/svg-button"
import Like from "./like"
import { FaHeart, FaRegHeart } from "react-icons/fa"
import Bookmark from "./bookmark"
import { getSvgIcon } from "components/svg-icon"
import Count from "components/count"
interface Props {
post: Post
@ -30,24 +32,17 @@ export const PostCard = (props: Props) => {
</Link>
<HStack pl="2" spacing="5">
<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>
<Like storyID={post.id} liked={post.liked} count={post.likes} fontSize="18px"/>
<Link href={`/${post.creator.username}/${post.id}#comments`}>
<HStack opacity="0.9" cursor="pointer">
<FaRegComment fontSize="1.1rem" />
<Text ml="2">{post.comments}</Text>
{getSvgIcon("comments", "1.3rem")}
<Text ml="2"><Count count={post.comments}/></Text>
</HStack>
</Link>
<SvgButton icon="bookmark" height="1rem" onClick={null} style={{marginLeft: '4px'}}/>
<Box style={{ marginLeft: '4px' }}><Bookmark height="1.05rem" storyID={post.id} bookmarked={post.bookmarked} /></Box>
</HStack>
</VStack>
)

@ -0,0 +1,57 @@
import React from "react"
import { Box, BoxProps, useColorModeValue, VStack } from "@chakra-ui/react"
import { Post } from "src/types/posts"
import useSession from "hooks/use-session"
import Like from "./like"
import Bookmark from "./bookmark"
import SvgButton from "components/svg-button"
import { useRouter } from "next/router"
import { ReserveUrls } from "src/data/reserve-urls"
interface Props {
post: Post
vertical?: boolean
}
export const PostSidebar = (props: Props) => {
const {post,vertical = true} = props
const session = useSession()
const router = useRouter()
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" />
</Box>
<Box>
<Box mt="6">
<Bookmark height="1.7rem" storyID={post.id} bookmarked={post.bookmarked} />
</Box>
<Box mt="4">
<SvgButton
aria-label="go to github"
variant="ghost"
layerStyle="textSecondary"
_focus={null}
fontWeight="300"
icon="share"
onClick={() => location.href = "#comments"}
/>
</Box>
{post.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}`)}
icon="edit"
/>
</Box>}
</Box>
</VStack>
)
}
export default PostSidebar

@ -2,25 +2,28 @@ import React from "react"
import {chakra, PropsOf, IconButton } from "@chakra-ui/react"
type Props = PropsOf<typeof chakra.div> & {
icon: 'bookmark' | 'edit' | 'share'
icon: any
onClick: any
height?: string
}
export const SvgButton= (props:Props) =>{
const {icon,height="1.7rem",...rest} = props
const {icon,height="1.6rem",...rest} = props
let iconSvg
switch (icon) {
case "bookmarked":
iconSvg = <svg height={height} fill="currentColor" viewBox="0 0 384 512"><path d="M0 512V48C0 21.49 21.49 0 48 0h288c26.51 0 48 21.49 48 48v464L192 400 0 512z"></path></svg>
break;
case "bookmark":
iconSvg = <svg height={height} fill="currentColor" viewBox="0 0 384 512"><path d="M336 0H48C21.49 0 0 21.49 0 48v464l192-112 192 112V48c0-26.51-21.49-48-48-48zm16 456.287l-160-93.333-160 93.333V48c0-8.822 7.178-16 16-16h288c8.822 0 16 7.178 16 16v408.287z"></path></svg>
break;
case "share":
iconSvg = <svg height="1.7rem" fill="currentColor" viewBox="0 0 448 512"><path d="M352 320c-28.6 0-54.2 12.5-71.8 32.3l-95.5-59.7c9.6-23.4 9.7-49.8 0-73.2l95.5-59.7c17.6 19.8 43.2 32.3 71.8 32.3 53 0 96-43 96-96S405 0 352 0s-96 43-96 96c0 13 2.6 25.3 7.2 36.6l-95.5 59.7C150.2 172.5 124.6 160 96 160c-53 0-96 43-96 96s43 96 96 96c28.6 0 54.2-12.5 71.8-32.3l95.5 59.7c-4.7 11.3-7.2 23.6-7.2 36.6 0 53 43 96 96 96s96-43 96-96c-.1-53-43.1-96-96.1-96zm0-288c35.3 0 64 28.7 64 64s-28.7 64-64 64-64-28.7-64-64 28.7-64 64-64zM96 320c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64zm256 160c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64z"></path></svg>
iconSvg = <svg height={height} fill="currentColor" viewBox="0 0 448 512"><path d="M352 320c-28.6 0-54.2 12.5-71.8 32.3l-95.5-59.7c9.6-23.4 9.7-49.8 0-73.2l95.5-59.7c17.6 19.8 43.2 32.3 71.8 32.3 53 0 96-43 96-96S405 0 352 0s-96 43-96 96c0 13 2.6 25.3 7.2 36.6l-95.5 59.7C150.2 172.5 124.6 160 96 160c-53 0-96 43-96 96s43 96 96 96c28.6 0 54.2-12.5 71.8-32.3l95.5 59.7c-4.7 11.3-7.2 23.6-7.2 36.6 0 53 43 96 96 96s96-43 96-96c-.1-53-43.1-96-96.1-96zm0-288c35.3 0 64 28.7 64 64s-28.7 64-64 64-64-28.7-64-64 28.7-64 64-64zM96 320c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64zm256 160c-35.3 0-64-28.7-64-64s28.7-64 64-64 64 28.7 64 64-28.7 64-64 64z"></path></svg>
break
case "edit":
iconSvg = <svg height="1.5rem" fill="currentColor" viewBox="0 0 512 512"><path d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zm-95.196 140.45L174 420.745V386h-48v-48H91.255l224.059-224.059 82.745 82.745zM126.147 468.598l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>
iconSvg = <svg height={height} fill="currentColor" viewBox="0 0 512 512"><path d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zm-95.196 140.45L174 420.745V386h-48v-48H91.255l224.059-224.059 82.745 82.745zM126.147 468.598l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>
break
default:
break;

@ -1,6 +1,9 @@
export function getSvgIcon(name,height="1.4rem") {
let svg
switch (name) {
case "comments":
svg = <svg fill="currentColor" height={height} viewBox="0 0 512 512"><path d="M280 272H136c-4.4 0-8 3.6-8 8v16c0 4.4 3.6 8 8 8h144c4.4 0 8-3.6 8-8v-16c0-4.4-3.6-8-8-8zm96-96H136c-4.4 0-8 3.6-8 8v16c0 4.4 3.6 8 8 8h240c4.4 0 8-3.6 8-8v-16c0-4.4-3.6-8-8-8zM256 32C114.6 32 0 125.1 0 240c0 47.6 19.9 91.2 52.9 126.3C38 405.7 7 439.1 6.5 439.5c-6.6 7-8.4 17.2-4.6 26S14.4 480 24 480c61.5 0 110-25.7 139.1-46.3C192 442.8 223.2 448 256 448c141.4 0 256-93.1 256-208S397.4 32 256 32zm0 384c-28.3 0-56.3-4.3-83.2-12.8l-15.2-4.8-13 9.2c-23 16.3-58.5 35.3-102.6 39.6 12-15.1 29.8-40.4 40.8-69.6l7.1-18.7-13.7-14.6C47.3 313.7 32 277.6 32 240c0-97 100.5-176 224-176s224 79 224 176-100.5 176-224 176z"></path></svg>
break
case "hot":
svg = <svg fill="currentColor" height={height} viewBox="0 0 448 512"><path d="M448 281.6c0-53.27-51.98-163.13-124.44-230.4-20.8 19.3-39.58 39.59-56.22 59.97C240.08 73.62 206.28 35.53 168 0 69.74 91.17 0 209.96 0 281.6 0 408.85 100.29 512 224 512c.53 0 1.04-.08 1.58-.08.32 0 .6.08.92.08 1.88 0 3.71-.35 5.58-.42C352.02 507.17 448 406.04 448 281.6zm-416 0c0-50.22 47.51-147.44 136.05-237.09 27.38 27.45 52.44 56.6 73.39 85.47l24.41 33.62 26.27-32.19a573.83 573.83 0 0130.99-34.95C379.72 159.83 416 245.74 416 281.6c0 54.69-21.53 104.28-56.28 140.21 12.51-35.29 10.88-75.92-8.03-112.02a357.34 357.34 0 00-10.83-19.19l-22.63-37.4-28.82 32.87-25.86 29.5c-24.93-31.78-59.31-75.5-63.7-80.54l-24.65-28.39-24.08 28.87C108.16 287 80 324.21 80 370.41c0 19.02 3.62 36.66 9.77 52.79C54.17 387.17 32 337.03 32 281.6zm193.54 198.32C162.86 479.49 112 437.87 112 370.41c0-33.78 21.27-63.55 63.69-114.41 6.06 6.98 86.48 109.68 86.48 109.68l51.3-58.52a334.43 334.43 0 019.87 17.48c23.92 45.66 13.83 104.1-29.26 134.24-17.62 12.33-39.14 19.71-62.37 20.73-2.06.07-4.09.29-6.17.31z"></path></svg>
break

@ -23,4 +23,5 @@ export interface Post {
likes? : number
liked? : boolean
comments? : number
bookmarked?: boolean
}

@ -3735,6 +3735,11 @@ shell-quote@1.7.2:
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
short-number@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/short-number/-/short-number-1.0.7.tgz#5ab8021b86f20a4bdd1bbe01a85c22ff1cd15514"
integrity sha512-e3cb811QXHiAH2H5rgZwXHsmg2+aG6XK0QyKF/+pyDIZFBSz/G8bkr8avdBrL5PVIfhIq8rlohjFvSB3azzpEQ==
signal-exit@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"

Loading…
Cancel
Save