diff --git a/layouts/sidebar/sidebar-link.tsx b/layouts/sidebar/sidebar-link.tsx
index 7fceb910..64a11a7b 100644
--- a/layouts/sidebar/sidebar-link.tsx
+++ b/layouts/sidebar/sidebar-link.tsx
@@ -28,7 +28,7 @@ const StyledLink = React.forwardRef(function StyledLink(
fontWeight: "600",
}}
{...rest}
- >{icon} {children}
+ >{icon && {icon} }{children}
)
})
diff --git a/pages/[username]/index.tsx b/pages/[username]/index.tsx
index 2fbe9a62..ed8fe251 100644
--- a/pages/[username]/index.tsx
+++ b/pages/[username]/index.tsx
@@ -82,7 +82,7 @@ const UserPage = () => {
{
user &&
-
+
diff --git a/pages/admin/tags.tsx b/pages/admin/tags.tsx
index 688652c4..2e22bed4 100644
--- a/pages/admin/tags.tsx
+++ b/pages/admin/tags.tsx
@@ -13,6 +13,7 @@ import { ReserveUrls } from "src/data/reserve-urls"
import { Tag } from "src/types/tag"
import { route } from "next/dist/next-server/server/router"
import PageContainer1 from "layouts/page-container1"
+import Empty from "components/empty"
const PostsPage = () => {
@@ -54,14 +55,7 @@ const PostsPage = () => {
{
tags.length === 0 ?
- <>
-
-
-
-
- 你还没创建任何标签
-
- >
+
:
<>
diff --git a/pages/editor/post/[id].tsx b/pages/editor/post/[id].tsx
index a72e7529..7df4cc06 100644
--- a/pages/editor/post/[id].tsx
+++ b/pages/editor/post/[id].tsx
@@ -104,7 +104,7 @@ function PostEditPage() {
}
const publish = async () => {
- if (ar.tags?.length === 0) {
+ if (!ar.tags || ar.tags?.length === 0) {
toast({
description: "请设置文章标签",
status: "error",
diff --git a/pages/follow/followers.tsx b/pages/follow/followers.tsx
new file mode 100644
index 00000000..e420d346
--- /dev/null
+++ b/pages/follow/followers.tsx
@@ -0,0 +1,79 @@
+import {Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast } from "@chakra-ui/react"
+import Card from "components/card"
+import Nav from "layouts/nav/nav"
+import PageContainer from "layouts/page-container"
+import Sidebar from "layouts/sidebar/sidebar"
+import React, { useEffect, useState } from "react"
+import {adminLinks, followLinks} from "src/data/links"
+import { requestApi } from "utils/axios/request"
+import TagCard from "components/tags/tag-card"
+import { useRouter } from "next/router"
+import Link from "next/link"
+import { ReserveUrls } from "src/data/reserve-urls"
+import { Tag } from "src/types/tag"
+import { route } from "next/dist/next-server/server/router"
+import PageContainer1 from "layouts/page-container1"
+import Empty from "components/empty"
+
+
+const FollowersPage = () => {
+ const [tags, setTags] = useState([])
+ const router = useRouter()
+ const toast = useToast()
+ const getTags = () => {
+ requestApi.get(`/tag/all`).then((res) => setTags(res.data)).catch(_ => setTags([]))
+ }
+
+ useEffect(() => {
+ getTags()
+ }, [])
+
+ const editTag = (tag: Tag) => {
+ router.push(`${ReserveUrls.Admin}/tag/${tag.name}`)
+ }
+
+ const deleteTag= async (id) => {
+ await requestApi.delete(`/tag/${id}`)
+ getTags()
+ toast({
+ description: "删除成功",
+ status: "success",
+ duration: 2000,
+ isClosable: true,
+ })
+ }
+
+ return (
+ <>
+
+
+
+
+
+ 标签列表({tags.length})
+
+
+ {
+ tags.length === 0 ?
+
+ :
+ <>
+
+ {tags.map(tag =>
+
+ editTag(tag)} onDelete={() => deleteTag(tag.id)} />
+
+
+ )}
+
+ 没有更多标签了
+ >
+ }
+
+
+
+ >
+ )
+}
+export default FollowersPage
+
diff --git a/pages/follow/tags.tsx b/pages/follow/tags.tsx
new file mode 100644
index 00000000..e539998f
--- /dev/null
+++ b/pages/follow/tags.tsx
@@ -0,0 +1,97 @@
+import { Text, Box, Heading, Image, Divider, useToast, HStack, Slider, SliderTrack, SliderFilledTrack, SliderThumb, Wrap, WrapItem } from "@chakra-ui/react"
+import Card from "components/card"
+import Sidebar from "layouts/sidebar/sidebar"
+import React, { useEffect, useState } from "react"
+import { followLinks } from "src/data/links"
+import { requestApi } from "utils/axios/request"
+import { useRouter } from "next/router"
+import PageContainer1 from "layouts/page-container1"
+import { IDType } from "src/types/id"
+import Empty from "components/empty"
+
+
+const TagsPage = () => {
+ const [tags, setTags] = useState([])
+ const [following, setFollowing] = useState([])
+ useEffect(() => {
+ getFollowing()
+ }, [])
+ const getFollowing = async () => {
+ const res = await requestApi.get(`/interaction/following/0?type=${IDType.Tag}`)
+ const ids = []
+ for (const f of res.data) {
+ ids.push(f.id)
+ }
+ setFollowing(res.data)
+
+ const res1 = await requestApi.post(`/tag/ids`, ids)
+ setTags(res1.data)
+ }
+
+ const getTagWeight = tag => {
+ for (const f of following) {
+ if (f.id === tag.id) {
+ return f.weight
+ }
+ }
+
+ return 0
+ }
+
+ return (
+ <>
+
+
+
+
+ Adjust tag weight to modify your home feed. Higher values mean more appearances.
+
+ {
+ tags.length === 0 ?
+
+ :
+
+
+ {tags.map(tag =>
+
+ )}
+
+
+
+ }
+
+
+
+ >
+ )
+}
+export default TagsPage
+
+
+function FollowingTag(props) {
+ const [weight, setWeight] = React.useState(props.weight)
+ const onWeightChange = async w => {
+ await requestApi.post(`/interaction/following/weight`, { id: props.tag.id, weight: weight })
+ setWeight(w)
+ }
+
+ return (
+
+
+
+
+ {props.tag.title}
+ #{props.tag.name}
+
+
+
+ setWeight(w)} onChangeEnd={onWeightChange}>
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/pages/follow/users.tsx b/pages/follow/users.tsx
new file mode 100644
index 00000000..0bcc39da
--- /dev/null
+++ b/pages/follow/users.tsx
@@ -0,0 +1,79 @@
+import {Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast } from "@chakra-ui/react"
+import Card from "components/card"
+import Nav from "layouts/nav/nav"
+import PageContainer from "layouts/page-container"
+import Sidebar from "layouts/sidebar/sidebar"
+import React, { useEffect, useState } from "react"
+import {adminLinks, followLinks} from "src/data/links"
+import { requestApi } from "utils/axios/request"
+import TagCard from "components/tags/tag-card"
+import { useRouter } from "next/router"
+import Link from "next/link"
+import { ReserveUrls } from "src/data/reserve-urls"
+import { Tag } from "src/types/tag"
+import { route } from "next/dist/next-server/server/router"
+import PageContainer1 from "layouts/page-container1"
+import Empty from "components/empty"
+
+
+const UsersPage = () => {
+ const [tags, setTags] = useState([])
+ const router = useRouter()
+ const toast = useToast()
+ const getTags = () => {
+ requestApi.get(`/tag/all`).then((res) => setTags(res.data)).catch(_ => setTags([]))
+ }
+
+ useEffect(() => {
+ getTags()
+ }, [])
+
+ const editTag = (tag: Tag) => {
+ router.push(`${ReserveUrls.Admin}/tag/${tag.name}`)
+ }
+
+ const deleteTag= async (id) => {
+ await requestApi.delete(`/tag/${id}`)
+ getTags()
+ toast({
+ description: "删除成功",
+ status: "success",
+ duration: 2000,
+ isClosable: true,
+ })
+ }
+
+ return (
+ <>
+
+
+
+
+
+ 标签列表({tags.length})
+
+
+ {
+ tags.length === 0 ?
+
+ :
+ <>
+
+ {tags.map(tag =>
+
+ editTag(tag)} onDelete={() => deleteTag(tag.id)} />
+
+
+ )}
+
+ 没有更多标签了
+ >
+ }
+
+
+
+ >
+ )
+}
+export default UsersPage
+
diff --git a/server/internal/api/interaction.go b/server/internal/api/interaction.go
index 562c933b..713d8d9a 100644
--- a/server/internal/api/interaction.go
+++ b/server/internal/api/interaction.go
@@ -8,6 +8,7 @@ import (
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e"
+ "github.com/imdotdev/im.dev/server/pkg/models"
)
func Follow(c *gin.Context) {
@@ -59,3 +60,38 @@ func Like(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
+
+func GetFollowing(c *gin.Context) {
+ userID := c.Param("userID")
+ targetType := c.Query("type")
+ if userID == "" || !models.ValidFollowIDType(targetType) {
+ c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
+ return
+ }
+
+ if userID == "0" {
+ u := user.CurrentUser(c)
+ userID = u.ID
+ }
+
+ tags, err := interaction.GetFollowing(userID, targetType)
+ if err != nil {
+ c.JSON(err.Status, common.RespError(err.Message))
+ return
+ }
+
+ c.JSON(http.StatusOK, common.RespSuccess(tags))
+}
+
+func SetFollowingWeight(c *gin.Context) {
+ f := &models.Following{}
+ c.Bind(&f)
+ u := user.CurrentUser(c)
+ err := interaction.SetFolloingWeight(u.ID, f)
+ if err != nil {
+ c.JSON(err.Status, common.RespError(err.Message))
+ return
+ }
+
+ c.JSON(http.StatusOK, common.RespSuccess(nil))
+}
diff --git a/server/internal/api/tag.go b/server/internal/api/tag.go
index 084fb094..8dccb7b8 100644
--- a/server/internal/api/tag.go
+++ b/server/internal/api/tag.go
@@ -33,6 +33,23 @@ func GetTags(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(res))
}
+func GetTagsByIDs(c *gin.Context) {
+ ids := make([]string, 0)
+ err := c.Bind(&ids)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
+ return
+ }
+
+ ts, err0 := tags.GetTagsByIDs(ids)
+ if err != nil {
+ c.JSON(err0.Status, common.RespError(err0.Message))
+ return
+ }
+
+ c.JSON(http.StatusOK, common.RespSuccess(ts))
+}
+
func SubmitTag(c *gin.Context) {
user := user.CurrentUser(c)
if !user.Role.IsAdmin() {
diff --git a/server/internal/interaction/follow.go b/server/internal/interaction/follow.go
index 3ffbf77e..507fdd88 100644
--- a/server/internal/interaction/follow.go
+++ b/server/internal/interaction/follow.go
@@ -3,6 +3,7 @@ package interaction
import (
"database/sql"
"net/http"
+ "sort"
"time"
"github.com/imdotdev/im.dev/server/pkg/db"
@@ -91,3 +92,31 @@ func GetFollows(targetID string) int {
return follows
}
+
+func GetFollowing(userID, targetType string) ([]*models.Following, *e.Error) {
+ rows, err := db.Conn.Query("SELECT target_id,weight from follows where user_id=? and target_type=?", userID, targetType)
+ if err != nil {
+ logger.Warn("get following error", "error", err)
+ return nil, e.New(http.StatusInternalServerError, e.Internal)
+ }
+
+ following := make(models.Followings, 0)
+ for rows.Next() {
+ f := &models.Following{}
+ rows.Scan(&f.ID, &f.Weight)
+ following = append(following, f)
+ }
+
+ sort.Sort(following)
+ return following, nil
+}
+
+func SetFolloingWeight(userID string, f *models.Following) *e.Error {
+ _, err := db.Conn.Exec("UPDATE follows SET weight=? WHERE user_id=? and target_id=?", f.Weight, userID, f.ID)
+ if err != nil {
+ logger.Warn("set following weight error", "error", err)
+ return e.New(http.StatusInternalServerError, e.Internal)
+ }
+
+ return nil
+}
diff --git a/server/internal/server.go b/server/internal/server.go
index c29f1b2e..bebb0ce3 100644
--- a/server/internal/server.go
+++ b/server/internal/server.go
@@ -69,6 +69,7 @@ func (s *Server) Start() error {
r.POST("/tag", IsLogin(), api.SubmitTag)
r.DELETE("/tag/:id", IsLogin(), api.DeleteTag)
r.GET("/tag/all", api.GetTags)
+ r.POST("tag/ids", api.GetTagsByIDs)
r.GET("/tag/posts/:id", api.GetTagPosts)
r.GET("/tag/info/:name", api.GetTag)
r.GET("/tag/user/:userID", api.GetUserTags)
@@ -85,7 +86,9 @@ func (s *Server) Start() error {
// interaction apis
r.POST("/interaction/like/:id", IsLogin(), api.Like)
r.POST("/interaction/follow/:id", IsLogin(), api.Follow)
+ r.POST("/interaction/following/weight", IsLogin(), api.SetFollowingWeight)
r.GET("/interaction/followed/:id", api.Followed)
+ r.GET("/interaction/following/:userID", api.GetFollowing)
// search apis
r.GET("/search/posts/:filter", api.SearchPosts)
diff --git a/server/internal/storage/sql_tables.go b/server/internal/storage/sql_tables.go
index 80fe0765..eb6b6a98 100644
--- a/server/internal/storage/sql_tables.go
+++ b/server/internal/storage/sql_tables.go
@@ -92,6 +92,7 @@ var sqlTables = map[string]string{
user_id VARCHAR(255),
target_id VARCHAR(255),
target_type VARCHAR(1),
+ weight TINYINT DEFAULT 1,
created DATETIME NOT NULL
);
CREATE INDEX IF NOT EXISTS follows_userid
diff --git a/server/internal/tags/tags.go b/server/internal/tags/tags.go
index 3450898e..8a154641 100644
--- a/server/internal/tags/tags.go
+++ b/server/internal/tags/tags.go
@@ -103,6 +103,20 @@ func GetTags() (models.Tags, *e.Error) {
return tags, nil
}
+func GetTagsByIDs(ids []string) ([]*models.Tag, *e.Error) {
+ tags := make([]*models.Tag, 0, len(ids))
+ for _, id := range ids {
+ tag, err := GetSimpleTag(id, "")
+ if err != nil {
+ logger.Warn("get tag error", "error", err)
+ continue
+ }
+ tags = append(tags, tag)
+ }
+
+ return tags, nil
+}
+
func DeleteTag(id int64) *e.Error {
_, err := db.Conn.Exec("DELETE FROM tags WHERE id=?", id)
if err != nil {
diff --git a/server/pkg/common/reserve_urls.go b/server/pkg/common/reserve_urls.go
index 7e1e523e..4f9b5cb0 100644
--- a/server/pkg/common/reserve_urls.go
+++ b/server/pkg/common/reserve_urls.go
@@ -4,6 +4,7 @@ package common
var ReserverURLs = []string{
"/tags",
"/courses",
+ "/follow",
"/editor",
"/admin",
"/bookmarks",
diff --git a/server/pkg/models/id_type.go b/server/pkg/models/id_type.go
index ae91554d..9a62947b 100644
--- a/server/pkg/models/id_type.go
+++ b/server/pkg/models/id_type.go
@@ -75,3 +75,11 @@ func ValidStoryIDType(tp string) bool {
return false
}
+
+func ValidFollowIDType(tp string) bool {
+ if tp == IDTypeUser || tp == IDTypeTag {
+ return true
+ }
+
+ return false
+}
diff --git a/server/pkg/models/interaction.go b/server/pkg/models/interaction.go
new file mode 100644
index 00000000..93182405
--- /dev/null
+++ b/server/pkg/models/interaction.go
@@ -0,0 +1,14 @@
+package models
+
+type Following struct {
+ ID string `json:"id"`
+ Weight int `json:"weight"`
+}
+
+type Followings []*Following
+
+func (s Followings) Len() int { return len(s) }
+func (s Followings) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
+func (s Followings) Less(i, j int) bool {
+ return s[i].Weight > s[j].Weight
+}
diff --git a/src/components/card.tsx b/src/components/card.tsx
index 01151004..396f65e4 100644
--- a/src/components/card.tsx
+++ b/src/components/card.tsx
@@ -1,7 +1,11 @@
import React from "react"
import { Box, BoxProps, useColorModeValue } from "@chakra-ui/react"
-export const Card = (props: BoxProps) => {
+interface Props {
+ shadowed?: boolean
+}
+
+export const Card = ({shadowed, ...rest}: BoxProps&Props) => {
const bg = useColorModeValue("white", "gray.780")
return (
{
borderRadius=".5rem"
borderWidth="1px"
p={[2,2,4,4]}
- // boxShadow="0 1px 1px 0 rgb(0 0 0 / 5%)"
- {...props}
+ boxShadow={shadowed? "0 1px 1px 0 rgb(0 0 0 / 5%)" : null}
+ {...rest}
/>
)
}
diff --git a/src/components/tags/tag-card.tsx b/src/components/tags/tag-card.tsx
index d83a1c0b..e70b4558 100644
--- a/src/components/tags/tag-card.tsx
+++ b/src/components/tags/tag-card.tsx
@@ -30,7 +30,7 @@ export const TagCard= (props:Props) =>{
{showActions ?
-
+ {/* */}
:
posts
}
diff --git a/src/components/user-menu.tsx b/src/components/user-menu.tsx
index 0a63b77d..95e0c35d 100644
--- a/src/components/user-menu.tsx
+++ b/src/components/user-menu.tsx
@@ -14,7 +14,7 @@ import { Session } from "src/types/user"
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 { FaRegSun, FaUserAlt ,FaBookmark, FaSignOutAlt,FaEdit,FaStar, FaHeart} from "react-icons/fa"
import { isAdmin, isEditor } from "utils/role"
import { logout } from "utils/session"
import Link from "next/link"
@@ -56,8 +56,9 @@ export const UserMenu = () => {
{isEditor(session.user.role) && } >创作中心}
- {isAdmin(session.user.role) && } >管理员}
}>书签收藏
+ }>我的关注
+ {isAdmin(session.user.role) && } >管理员}
}>偏好设置
diff --git a/src/data/links.tsx b/src/data/links.tsx
index 86e324d5..2e2d082b 100644
--- a/src/data/links.tsx
+++ b/src/data/links.tsx
@@ -24,6 +24,24 @@ export const editorLinks: Route[] = [{
}
]
+export const followLinks: any[] = [
+ {
+ title: 'Following tags',
+ path: `${ReserveUrls.Follow}/tags`,
+ disabled: false
+ },
+ {
+ title: 'Following users',
+ path: `${ReserveUrls.Follow}/users`,
+ disabled: false
+ },
+ {
+ title: 'Followers',
+ path: `${ReserveUrls.Follow}/followers`,
+ disabled: false
+ },
+]
+
export const searchLinks: any[] = [{
title: '文章',
path: `${ReserveUrls.Search}/posts`,
diff --git a/src/data/reserve-urls.ts b/src/data/reserve-urls.ts
index 43acd2db..8f721029 100644
--- a/src/data/reserve-urls.ts
+++ b/src/data/reserve-urls.ts
@@ -2,6 +2,7 @@
export enum ReserveUrls {
Tags = "/tags",
Courses = "/courses",
+ Follow = "/follow",
Editor = "/editor",
Admin = "/admin",
Bookmarks = "/bookmarks",