diff --git a/pages/[username]/index.tsx b/pages/[username]/index.tsx
index b9edcc0f..cfdf1eac 100644
--- a/pages/[username]/index.tsx
+++ b/pages/[username]/index.tsx
@@ -202,7 +202,7 @@ const UserPage = () => {
:
-
+
}
diff --git a/pages/editor/posts.tsx b/pages/editor/posts.tsx
index 63b357fb..8545179d 100644
--- a/pages/editor/posts.tsx
+++ b/pages/editor/posts.tsx
@@ -96,6 +96,10 @@ const PostsPage = () => {
})
}
+ const onPinPost = async id => {
+ await requestApi.post(`/story/pin/${id}`)
+ getPosts()
+ }
return (
<>
@@ -125,7 +129,7 @@ const PostsPage = () => {
{posts.map(post =>
- editPost(post)} onDelete={() => onDeletePost(post.id)} />
+ editPost(post)} onDelete={() => onDeletePost(post.id)} onPin={() => onPinPost(post.id)} />
)}
diff --git a/pages/editor/series.tsx b/pages/editor/series.tsx
index 3944edab..665378d7 100644
--- a/pages/editor/series.tsx
+++ b/pages/editor/series.tsx
@@ -145,6 +145,11 @@ const PostsPage = () => {
setCurrentSeries(newSeries)
}
+ const onPinPost = async id => {
+ await requestApi.post(`/story/pin/${id}`)
+ getSeries()
+ }
+
return (
<>
@@ -272,7 +277,7 @@ const PostsPage = () => {
{series.map(post =>
- editSeries(post)} onDelete={() => onDeleteSeries(post.id)} showSource={false} />
+ editSeries(post)} onDelete={() => onDeleteSeries(post.id)} showSource={false} onPin={() => onPinPost(post.id)}/>
)}
diff --git a/server/internal/api/story.go b/server/internal/api/story.go
index 4a2c143e..8644c246 100644
--- a/server/internal/api/story.go
+++ b/server/internal/api/story.go
@@ -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))
+}
diff --git a/server/internal/server.go b/server/internal/server.go
index d475da35..7ff46203 100644
--- a/server/internal/server.go
+++ b/server/internal/server.go
@@ -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)
diff --git a/server/internal/storage/sql_tables.go b/server/internal/storage/sql_tables.go
index eb6b6a98..dd1a3048 100644
--- a/server/internal/storage/sql_tables.go
+++ b/server/internal/storage/sql_tables.go
@@ -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);
+ `,
}
diff --git a/server/internal/story/pin.go b/server/internal/story/pin.go
new file mode 100644
index 00000000..788d276c
--- /dev/null
+++ b/server/internal/story/pin.go
@@ -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
+}
diff --git a/server/internal/story/posts.go b/server/internal/story/posts.go
index b6a5445b..10897110 100644
--- a/server/internal/story/posts.go
+++ b/server/internal/story/posts.go
@@ -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) {
diff --git a/server/pkg/models/story.go b/server/pkg/models/story.go
index 014b277c..69549fe6 100644
--- a/server/pkg/models/story.go
+++ b/server/pkg/models/story.go
@@ -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
}
diff --git a/src/components/interaction/like.tsx b/src/components/interaction/like.tsx
index 4014ec24..a374b2e6 100644
--- a/src/components/interaction/like.tsx
+++ b/src/components/interaction/like.tsx
@@ -53,7 +53,7 @@ const Like = (props: Props) => {
/>}
-
+
)
}
diff --git a/src/components/story/simple-story-card.tsx b/src/components/story/simple-story-card.tsx
index b09eaa65..e0045c89 100644
--- a/src/components/story/simple-story-card.tsx
+++ b/src/components/story/simple-story-card.tsx
@@ -21,21 +21,19 @@ export const SimpleStoryCard = (props: Props) => {
return (
{story.title}
-
- {story.creator.nickname}
+
+ {story.creator.nickname}
-
+
-
- {getSvgIcon("comments", "1rem")}
- {story.comments}
+
+ {getSvgIcon("comments1", "0.9rem")}
+ {story.comments}
-
-
)
diff --git a/src/components/story/stories.tsx b/src/components/story/stories.tsx
index 55da74eb..125a4297 100644
--- a/src/components/story/stories.tsx
+++ b/src/components/story/stories.tsx
@@ -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) => {
{stories.map((story,i) =>
-
+
)}
{showFooter && 没有更多文章了}
diff --git a/src/components/story/story-card.tsx b/src/components/story/story-card.tsx
index 3a547de9..6d242de0 100644
--- a/src/components/story/story-card.tsx
+++ b/src/components/story/story-card.tsx
@@ -30,14 +30,15 @@ export const StoryCard = (props: Props) => {
-
-
+
+
- {story.type === IDType.Series && SERIES}
+ {story.type === IDType.Series && SERIES}
+ {story.pinned && 置顶}
{type !== "classic" && {story.rawTags.map(t => #{t.name})}}
@@ -47,7 +48,7 @@ export const StoryCard = (props: Props) => {
searchWords={[props.highlight]}
/>
- {story.cover && type === "classic" && }
+ {story.cover && type === "classic" && }
diff --git a/src/components/story/text-story-card.tsx b/src/components/story/text-story-card.tsx
index 234e9090..cb225dbf 100644
--- a/src/components/story/text-story-card.tsx
+++ b/src/components/story/text-story-card.tsx
@@ -1,42 +1,73 @@
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 & {
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
-
+export const TextStoryCard = (props: 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
-
-
+
+
{showSource && <> {story.url ? 外部 : 原创}>}
- {story.title ?story.title : 'No Title'}
+ {story.title ? story.title : 'No Title'}
发布于{gap}
- {props.showActions &&
-
-
+ {props.showActions &&
+
+ }
+ onClick={onPin}
+ color={story.pinned? "teal" : null}
+ />
+
+
+
+
+
+
+
+ }
+ onClick={props.onDelete}
+ />
+
}
-
+
)
-}
+}
export default TextStoryCard
diff --git a/src/components/svg-icon.tsx b/src/components/svg-icon.tsx
index ffcbf4a4..f6adb3b8 100644
--- a/src/components/svg-icon.tsx
+++ b/src/components/svg-icon.tsx
@@ -1,17 +1,20 @@
-export function getSvgIcon(name,height="1.4rem") {
+export function getSvgIcon(name, height = "1.4rem") {
let svg
switch (name) {
+ case "comments1":
+ svg =
+ break
case "comments":
svg =
break
case "best":
- svg =
+ svg =
break
case "home":
svg =
break
case "tags":
- svg =
+ svg =
break
case "post":
svg =
@@ -20,7 +23,7 @@ export function getSvgIcon(name,height="1.4rem") {
svg =
break
case "explore":
- svg =
+ svg =
break
case "feature":
svg =
@@ -29,19 +32,25 @@ export function getSvgIcon(name,height="1.4rem") {
svg =
break
case "search":
- svg =
+ svg =
break
case "user":
- svg =
+ svg =
break
case "favorites":
- svg =
+ svg =
break
case "drafts":
svg =
break
+ case "share":
+ svg =
+ break
+ case "edit":
+ svg =
+ break
default:
- break;
+ break;
}
return svg
diff --git a/src/data/links.tsx b/src/data/links.tsx
index 464780c4..a9b55b28 100644
--- a/src/data/links.tsx
+++ b/src/data/links.tsx
@@ -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[] = [{
diff --git a/src/types/story.ts b/src/types/story.ts
index db800078..543f88d2 100644
--- a/src/types/story.ts
+++ b/src/types/story.ts
@@ -24,6 +24,7 @@ export interface Story {
rawTags?: Tag[]
likes? : number
liked? : boolean
+ pinned?: boolean
comments? : number
bookmarked?: boolean
status?: number