add pin for posts

pull/52/head
codemystery 4 years ago
parent 715daebdb4
commit af49765582

@ -202,7 +202,7 @@ const UserPage = () => {
</Card>
:
<Card width="100%" height="fit-content" p="0" px="3">
<Stories stories={posts} showFooter={tagFilter === null} />
<Stories stories={posts} showFooter={tagFilter === null} showPinned={true}/>
</Card>
}
</Box>

@ -96,6 +96,10 @@ const PostsPage = () => {
})
}
const onPinPost = async id => {
await requestApi.post(`/story/pin/${id}`)
getPosts()
}
return (
<>
<PageContainer1 >
@ -125,7 +129,7 @@ const PostsPage = () => {
<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)} />
<TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editPost(post)} onDelete={() => onDeletePost(post.id)} onPin={() => onPinPost(post.id)} />
<Divider mt="5" />
</Box>
)}

@ -145,6 +145,11 @@ const PostsPage = () => {
setCurrentSeries(newSeries)
}
const onPinPost = async id => {
await requestApi.post(`/story/pin/${id}`)
getSeries()
}
return (
<>
<PageContainer1 >
@ -272,7 +277,7 @@ const PostsPage = () => {
<VStack mt="4">
{series.map(post =>
<Box width="100%" key={post.id}>
<TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editSeries(post)} onDelete={() => onDeleteSeries(post.id)} showSource={false} />
<TextStoryCard story={post} showActions={true} mt="4" onEdit={() => editSeries(post)} onDelete={() => onDeleteSeries(post.id)} showSource={false} onPin={() => onPinPost(post.id)}/>
<Divider mt="5" />
</Box>
)}

@ -197,3 +197,25 @@ func GetSeries(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(series))
}
type PinData struct {
TargetID string `json:"targetID"`
StoryID string `json:"storyID"`
}
func PinStory(c *gin.Context) {
storyID := c.Param("storyID")
u := user.CurrentUser(c)
if !models.IsStoryCreator(u.ID, storyID) {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
}
err := story.PinStory(storyID, u.ID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}

@ -54,6 +54,7 @@ func (s *Server) Start() error {
r.GET("/story/posts/drafts", IsLogin(), api.GetEditorDrafts)
r.GET("/story/posts/home/:filter", api.GetHomePosts)
r.POST("/story", IsLogin(), api.SubmitStory)
r.POST("/story/pin/:storyID", IsLogin(), api.PinStory)
r.POST("/story/series", api.GetSeries)
r.POST("/story/series/post/:id", IsLogin(), api.SubmitSeriesPost)
r.GET("/story/series/post/:id", api.GetSeriesPost)

@ -177,4 +177,13 @@ var sqlTables = map[string]string{
CREATE INDEX IF NOT EXISTS series_post_postid
ON series_post (post_id);
`,
"pin": `CREATE TABLE IF NOT EXISTS pin (
target_id VARCHAR(255),
story_id VARCHAR(255),
created DATETIME
);
CREATE INDEX IF NOT EXISTS pin_targetid
ON pin (target_id);
`,
}

@ -0,0 +1,51 @@
package story
import (
"database/sql"
"net/http"
"time"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
)
func PinStory(storyID string, targetID string) *e.Error {
pinned := false
var nid string
err := db.Conn.QueryRow("SELECT target_id FROM pin WHERE target_id=? and story_id=?", targetID, storyID).Scan(&nid)
if err != nil && err != sql.ErrNoRows {
logger.Warn("query pinned error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
if nid == targetID {
pinned = true
}
if pinned {
_, err = db.Conn.Exec("DELETE FROM pin WHERE target_id=? and story_id=?", targetID, storyID)
if err != nil {
logger.Warn("delete pin error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
} else {
_, err = db.Conn.Exec("INSERT INTO pin (target_id,story_id,created) VALUES (?,?,?)", targetID, storyID, time.Now())
if err != nil {
logger.Warn("add pin error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
}
return nil
}
func GetPinned(storyID string, targetID string) bool {
var nid string
err := db.Conn.QueryRow("SELECT target_id FROM pin WHERE target_id=? and story_id=?", targetID, storyID).Scan(&nid)
if err != nil {
return false
}
return true
}

@ -47,7 +47,21 @@ func UserPosts(tp string, user *models.User, uid string) (models.Stories, *e.Err
posts := GetPosts(user, rows)
sort.Sort(posts)
return posts, nil
pinned := make([]*models.Story, 0)
unpinned := make([]*models.Story, 0)
for _, post := range posts {
post.Pinned = GetPinned(post.ID, user.ID)
if post.Pinned {
pinned = append(pinned, post)
} else {
unpinned = append(unpinned, post)
}
}
newPosts := append(pinned, unpinned...)
return newPosts, nil
}
func UserDrafts(user *models.User, uid string) (models.Stories, *e.Error) {

@ -1,7 +1,6 @@
package models
import (
"fmt"
"time"
"github.com/imdotdev/im.dev/server/pkg/db"
@ -28,6 +27,7 @@ type Story struct {
RawTags []*Tag `json:"rawTags"`
Likes int `json:"likes"`
Liked bool `json:"liked"`
Pinned bool `json:"pinned,omitempty"`
Comments int `json:"comments"`
Views int `json:"views"`
Bookmarked bool `json:"bookmarked"`
@ -78,7 +78,6 @@ func (s SeriesPosts) Less(i, j int) bool {
func IsStoryCreator(userID string, storyID string) bool {
var nid string
err := db.Conn.QueryRow("SELECT creator FROM story WHERE id=?", storyID).Scan(&nid)
fmt.Println(userID, storyID, err)
if err != nil {
return false
}

@ -53,7 +53,7 @@ const Like = (props: Props) => {
/>}
</Tooltip>
<chakra.span layerStyle="textSecondary" fontWeight="600"><Count count={count}/></chakra.span>
<chakra.span opacity="0.8" fontSize={props.fontSize}><Count count={count} /></chakra.span>
</HStack>
)
}

@ -21,21 +21,19 @@ export const SimpleStoryCard = (props: Props) => {
return (
<VStack alignItems="left" spacing="0">
<Link href={getStoryUrl(story)}><Heading pb="2" fontSize=".9rem" 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 pl="1" spacing="2" fontSize={size==='md'? '1rem' : ".9rem"}>
<Link href={`/${story.creator.username}`}><Text cursor="pointer" fontSize="0.8rem">{story.creator.nickname}</Text></Link>
<HStack opacity="0.9">
<Like liked={story.liked} count={story.likes} storyID={story.id} fontSize="1rem"/>
<Like liked={story.liked} count={story.likes} storyID={story.id} fontSize="0.8rem"/>
</HStack>
<a href={`${getCommentsUrl(story)}#comments`}>
<HStack opacity="0.9" cursor="pointer">
{getSvgIcon("comments", "1rem")}
<Text ml="2">{story.comments}</Text>
<HStack opacity="0.9" cursor="pointer" spacing="3">
{getSvgIcon("comments1", "0.9rem")}
<Text fontSize="0.8rem" opacity="0.8">{story.comments}</Text>
</HStack>
</a>
<Box style={{marginLeft: '4px'}}><Bookmark storyID={story.id} bookmarked={story.bookmarked} height=".9rem"/></Box>
</HStack>
</VStack>
)

@ -9,13 +9,14 @@ interface Props {
card?: any
size?: 'sm' | 'md'
showFooter?: boolean
showPinned?: boolean
type?: string
highlight?: string
}
export const Stroies = (props: Props) => {
const { stories,card=StoryCard,showFooter=true,type="classic"} = props
const { stories,card=StoryCard,showFooter=true,type="classic",showPinned = false} = props
const borderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark)
const Card = card
const showBorder = i => {
@ -34,7 +35,7 @@ export const Stroies = (props: Props) => {
<VStack alignItems="left">
{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}/>
<Card story={story} size={props.size} type={type} highlight={props.highlight} showPinned={showPinned}/>
</Box>)}
</VStack>
{showFooter && <Center><Text layerStyle="textSecondary" fontSize="sm" py="4"></Text></Center>}

@ -30,14 +30,15 @@ export const StoryCard = (props: Props) => {
<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'}>
<VStack alignItems="left" spacing={type === "classic" ? 3 : 2} width={isLargeScreen && type === "classic" ? "calc(100% - 15rem)" : '100%'}>
<Heading size="md" fontSize={type === "classic" ? '1.3rem' : '1.2rem'}>
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={story.title}
searchWords={[props.highlight]}
/>
{story.type === IDType.Series && <Tag size="sm" mt="1" ml="2">SERIES</Tag>}
{story.type === IDType.Series && <Tag size="sm" ml="2" mt="2px">SERIES</Tag>}
{story.pinned && <Tag size="sm" ml="2" mt="2px"></Tag>}
</Heading>
{type !== "classic" && <HStack>{story.rawTags.map(t => <Text layerStyle="textSecondary" fontSize="md">#{t.name}</Text>)}</HStack>}
<Text layerStyle={type === "classic" ? "textSecondary" : null}>
@ -47,7 +48,7 @@ export const StoryCard = (props: Props) => {
searchWords={[props.highlight]}
/></Text>
</VStack>
{story.cover && type === "classic" && <Image src={story.cover} width="18rem" height="120px" pt={isLargeScreen ? 0 : 2} borderRadius="4px" />}
{story.cover && type === "classic" && <Image src={story.cover} width="15rem" height="120px" pt={isLargeScreen ? 0 : 2} borderRadius="4px" />}
</Layout>
</a>

@ -1,26 +1,29 @@
import React from "react"
import {chakra, Heading, VStack, Text, HStack,Button, Flex,PropsOf, Tag, useMediaQuery } from "@chakra-ui/react"
import { chakra, Heading, VStack, Text, HStack, Button, Flex, PropsOf, Tag, useMediaQuery, IconButton, Tooltip } from "@chakra-ui/react"
import { Story } from "src/types/story"
import moment from 'moment'
import { IDType } from "src/types/id"
import { getStoryUrl } from "utils/story"
import { FaPaperclip, FaRegTrashAlt, FaTrash } from "react-icons/fa"
import { getSvgIcon } from "components/svg-icon"
type Props = PropsOf<typeof chakra.div> & {
story: Story
showActions: boolean
onEdit?: any
onDelete?: any
onPin?: any
showSource?: boolean
}
export const TextStoryCard = (props: Props) => {
const {story,showActions,onEdit,onDelete,showSource=true ,...rest} = props
const { story, showActions, onEdit, onDelete, showSource = true,onPin, ...rest } = props
const [isSmallScreen] = useMediaQuery("(max-width: 768px)")
const Lay = isSmallScreen ? VStack : Flex
const gap = moment(story.created).fromNow()
return (
//@ts-ignore
<Lay justifyContent="space-between" alignItems={isSmallScreen ? "left" : "center"} {...rest}>
@ -31,9 +34,37 @@ export const TextStoryCard= (props:Props) =>{
</Heading>
<Text fontSize=".9rem">{gap}</Text>
</VStack>
{props.showActions && <HStack pt={{base: 3, md: 0}}>
<Button size="sm" colorScheme="teal" variant="outline" onClick={onEdit}>Edit</Button>
<Button size="sm" onClick={props.onDelete} variant="ghost">Delete</Button>
{props.showActions && <HStack pt={{ base: 3, md: 0 }} layerStyle="textSecondary">
<Tooltip label={story.pinned? "取消置顶" : "置顶"}>
<IconButton
aria-label="a icon button"
variant="ghost"
_focus={null}
icon={<FaPaperclip />}
onClick={onPin}
color={story.pinned? "teal" : null}
/>
</Tooltip>
<Tooltip label="编辑">
<IconButton
aria-label="a icon button"
variant="ghost"
_focus={null}
icon={getSvgIcon("edit", "1rem")}
onClick={onEdit}
/>
</Tooltip>
<Tooltip label="删除">
<IconButton
aria-label="a icon button"
variant="ghost"
_focus={null}
icon={<FaRegTrashAlt />}
onClick={props.onDelete}
/>
</Tooltip>
</HStack>}
</Lay>
)

@ -1,6 +1,9 @@
export function getSvgIcon(name, height = "1.4rem") {
let svg
switch (name) {
case "comments1":
svg = <svg fill="currentColor" height={height} viewBox="0 0 576 512"><path d="M569.9 441.1c-.5-.4-22.6-24.2-37.9-54.9 27.5-27.1 44-61.1 44-98.2 0-80-76.5-146.1-176.2-157.9C368.4 72.5 294.3 32 208 32 93.1 32 0 103.6 0 192c0 37 16.5 71 44 98.2-15.3 30.7-37.3 54.5-37.7 54.9-6.3 6.7-8.1 16.5-4.4 25 3.6 8.5 12 14 21.2 14 53.5 0 96.7-20.2 125.2-38.8 9.1 2.1 18.4 3.7 28 4.8 31.5 57.5 105.5 98 191.8 98 20.8 0 40.8-2.4 59.8-6.8 28.5 18.5 71.6 38.8 125.2 38.8 9.2 0 17.5-5.5 21.2-14 3.6-8.5 1.9-18.3-4.4-25zM155.4 314l-13.2-3-11.4 7.4c-20.1 13.1-50.5 28.2-87.7 32.5 8.8-11.3 20.2-27.6 29.5-46.4L83 283.7l-16.5-16.3C50.7 251.9 32 226.2 32 192c0-70.6 79-128 176-128s176 57.4 176 128-79 128-176 128c-17.7 0-35.4-2-52.6-6zm289.8 100.4l-11.4-7.4-13.2 3.1c-17.2 4-34.9 6-52.6 6-65.1 0-122-25.9-152.4-64.3C326.9 348.6 416 278.4 416 192c0-9.5-1.3-18.7-3.3-27.7C488.1 178.8 544 228.7 544 288c0 34.2-18.7 59.9-34.5 75.4L493 379.7l10.3 20.7c9.4 18.9 20.8 35.2 29.5 46.4-37.1-4.2-67.5-19.4-87.6-32.4z"></path></svg>
break
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
@ -40,6 +43,12 @@ export function getSvgIcon(name,height="1.4rem") {
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
case "share":
svg = <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":
svg = <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;
}

@ -40,6 +40,11 @@ export const interactionLinks: any[] = [
path: `${ReserveUrls.Interaction}/followers`,
disabled: false
},
{
title: 'Followers',
path: `${ReserveUrls.Interaction}/followers`,
disabled: false
},
]
export const searchLinks: any[] = [{

@ -24,6 +24,7 @@ export interface Story {
rawTags?: Tag[]
likes? : number
liked? : boolean
pinned?: boolean
comments? : number
bookmarked?: boolean
status?: number

Loading…
Cancel
Save