pull/51/head
sunface 4 years ago
parent a5f9c744c2
commit f6fc6ddc84

@ -28,7 +28,7 @@ const StyledLink = React.forwardRef(function StyledLink(
fontWeight: "600", fontWeight: "600",
}} }}
{...rest} {...rest}
><chakra.span mr="5" fontSize="1.1rem" display={{base:"none",md:"block"}} width="20px">{icon}</chakra.span> <chakra.span>{children}</chakra.span></chakra.a> >{icon && <chakra.span mr="5" fontSize="1.1rem" display={{base:"none",md:"block"}} width="20px">{icon}</chakra.span> }<chakra.span>{children}</chakra.span></chakra.a>
) )
}) })

@ -82,7 +82,7 @@ const UserPage = () => {
<PageContainer1 p="0"> <PageContainer1 p="0">
{ {
user && user &&
<Box alignItems="left"> <Box alignItems="left" pb="6">
<Card p="0" borderTop="none"> <Card p="0" borderTop="none">
<Box backgroundImage={`url(${user.cover})`} height="300px" width="100%" backgroundSize="cover" backgroundPosition="center" /> <Box backgroundImage={`url(${user.cover})`} height="300px" width="100%" backgroundSize="cover" backgroundPosition="center" />
<VStack maxHeight="200px" position="relative" top="-70px" spacing="3"> <VStack maxHeight="200px" position="relative" top="-70px" spacing="3">

@ -13,6 +13,7 @@ import { ReserveUrls } from "src/data/reserve-urls"
import { Tag } from "src/types/tag" import { Tag } from "src/types/tag"
import { route } from "next/dist/next-server/server/router" import { route } from "next/dist/next-server/server/router"
import PageContainer1 from "layouts/page-container1" import PageContainer1 from "layouts/page-container1"
import Empty from "components/empty"
const PostsPage = () => { const PostsPage = () => {
@ -54,14 +55,7 @@ const PostsPage = () => {
</Flex> </Flex>
{ {
tags.length === 0 ? tags.length === 0 ?
<> <Empty />
<Center mt="4">
<Image height="25rem" src="/empty-posts.png" />
</Center>
<Center mt="8">
<Heading size="sm"></Heading>
</Center>
</>
: :
<> <>
<VStack mt="4"> <VStack mt="4">

@ -104,7 +104,7 @@ function PostEditPage() {
} }
const publish = async () => { const publish = async () => {
if (ar.tags?.length === 0) { if (!ar.tags || ar.tags?.length === 0) {
toast({ toast({
description: "请设置文章标签", description: "请设置文章标签",
status: "error", status: "error",

@ -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 (
<>
<PageContainer1>
<Box display="flex">
<Sidebar routes={followLinks} title="我的关注" />
<Card ml="4" p="6" width="100%">
<Flex alignItems="center" justify="space-between">
<Heading size="md">({tags.length})</Heading>
<Button colorScheme="teal" size="sm" _focus={null}><Link href={`${ReserveUrls.Admin}/tag/new`}></Link></Button>
</Flex>
{
tags.length === 0 ?
<Empty />
:
<>
<VStack mt="4">
{tags.map(tag =>
<Box width="100%" key={tag.id}>
<TagCard tag={tag} showActions={true} mt="4" onEdit={() => editTag(tag)} onDelete={() => deleteTag(tag.id)} />
<Divider mt="5" />
</Box>
)}
</VStack>
<Center><Text layerStyle="textSecondary" fontSize="sm" mt="5"></Text></Center>
</>
}
</Card>
</Box>
</PageContainer1>
</>
)
}
export default FollowersPage

@ -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 (
<>
<PageContainer1>
<Box display="flex">
<Sidebar routes={followLinks} title="我的关注" />
<Card ml="4" p="6" width="100%">
<Text fontSize=".95rem" fontWeight="600">Adjust tag weight to modify your home feed. Higher values mean more appearances.</Text>
<Divider my="6" />
{
tags.length === 0 ?
<Empty />
:
<Wrap spacing="10px">
{tags.map(tag =>
<WrapItem width={["100%","100%","100%","31%"]}><FollowingTag key={tag.id} tag={tag} weight={getTagWeight(tag)} /> </WrapItem>
)}
</Wrap>
}
</Card>
</Box>
</PageContainer1>
</>
)
}
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 (
<Card shadowed mt="2" width="100%">
<HStack spacing="4">
<Image src={props.tag.icon} width="50px" />
<Box>
<Heading size="sm">{props.tag.title}</Heading>
<Text>#{props.tag.name}</Text>
</Box>
</HStack>
<Box px="1">
<Slider min={1} max={10} mt="4" size="sm" focusThumbOnChange={false} value={weight} onChange={w => setWeight(w)} onChangeEnd={onWeightChange}>
<SliderTrack>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb _focus={null} fontSize="sm" boxSize="32px" children={weight} />
</Slider>
</Box>
</Card>
)
}

@ -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 (
<>
<PageContainer1>
<Box display="flex">
<Sidebar routes={followLinks} title="我的关注" />
<Card ml="4" p="6" width="100%">
<Flex alignItems="center" justify="space-between">
<Heading size="md">({tags.length})</Heading>
<Button colorScheme="teal" size="sm" _focus={null}><Link href={`${ReserveUrls.Admin}/tag/new`}></Link></Button>
</Flex>
{
tags.length === 0 ?
<Empty />
:
<>
<VStack mt="4">
{tags.map(tag =>
<Box width="100%" key={tag.id}>
<TagCard tag={tag} showActions={true} mt="4" onEdit={() => editTag(tag)} onDelete={() => deleteTag(tag.id)} />
<Divider mt="5" />
</Box>
)}
</VStack>
<Center><Text layerStyle="textSecondary" fontSize="sm" mt="5"></Text></Center>
</>
}
</Card>
</Box>
</PageContainer1>
</>
)
}
export default UsersPage

@ -8,6 +8,7 @@ import (
"github.com/imdotdev/im.dev/server/internal/user" "github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common" "github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e" "github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
) )
func Follow(c *gin.Context) { func Follow(c *gin.Context) {
@ -59,3 +60,38 @@ func Like(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil)) 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))
}

@ -33,6 +33,23 @@ func GetTags(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(res)) 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) { func SubmitTag(c *gin.Context) {
user := user.CurrentUser(c) user := user.CurrentUser(c)
if !user.Role.IsAdmin() { if !user.Role.IsAdmin() {

@ -3,6 +3,7 @@ package interaction
import ( import (
"database/sql" "database/sql"
"net/http" "net/http"
"sort"
"time" "time"
"github.com/imdotdev/im.dev/server/pkg/db" "github.com/imdotdev/im.dev/server/pkg/db"
@ -91,3 +92,31 @@ func GetFollows(targetID string) int {
return follows 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
}

@ -69,6 +69,7 @@ func (s *Server) Start() error {
r.POST("/tag", IsLogin(), api.SubmitTag) r.POST("/tag", IsLogin(), api.SubmitTag)
r.DELETE("/tag/:id", IsLogin(), api.DeleteTag) r.DELETE("/tag/:id", IsLogin(), api.DeleteTag)
r.GET("/tag/all", api.GetTags) r.GET("/tag/all", api.GetTags)
r.POST("tag/ids", api.GetTagsByIDs)
r.GET("/tag/posts/:id", api.GetTagPosts) r.GET("/tag/posts/:id", api.GetTagPosts)
r.GET("/tag/info/:name", api.GetTag) r.GET("/tag/info/:name", api.GetTag)
r.GET("/tag/user/:userID", api.GetUserTags) r.GET("/tag/user/:userID", api.GetUserTags)
@ -85,7 +86,9 @@ func (s *Server) Start() error {
// interaction apis // interaction apis
r.POST("/interaction/like/:id", IsLogin(), api.Like) r.POST("/interaction/like/:id", IsLogin(), api.Like)
r.POST("/interaction/follow/:id", IsLogin(), api.Follow) 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/followed/:id", api.Followed)
r.GET("/interaction/following/:userID", api.GetFollowing)
// search apis // search apis
r.GET("/search/posts/:filter", api.SearchPosts) r.GET("/search/posts/:filter", api.SearchPosts)

@ -92,6 +92,7 @@ var sqlTables = map[string]string{
user_id VARCHAR(255), user_id VARCHAR(255),
target_id VARCHAR(255), target_id VARCHAR(255),
target_type VARCHAR(1), target_type VARCHAR(1),
weight TINYINT DEFAULT 1,
created DATETIME NOT NULL created DATETIME NOT NULL
); );
CREATE INDEX IF NOT EXISTS follows_userid CREATE INDEX IF NOT EXISTS follows_userid

@ -103,6 +103,20 @@ func GetTags() (models.Tags, *e.Error) {
return tags, nil 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 { func DeleteTag(id int64) *e.Error {
_, err := db.Conn.Exec("DELETE FROM tags WHERE id=?", id) _, err := db.Conn.Exec("DELETE FROM tags WHERE id=?", id)
if err != nil { if err != nil {

@ -4,6 +4,7 @@ package common
var ReserverURLs = []string{ var ReserverURLs = []string{
"/tags", "/tags",
"/courses", "/courses",
"/follow",
"/editor", "/editor",
"/admin", "/admin",
"/bookmarks", "/bookmarks",

@ -75,3 +75,11 @@ func ValidStoryIDType(tp string) bool {
return false return false
} }
func ValidFollowIDType(tp string) bool {
if tp == IDTypeUser || tp == IDTypeTag {
return true
}
return false
}

@ -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
}

@ -1,7 +1,11 @@
import React from "react" import React from "react"
import { Box, BoxProps, useColorModeValue } from "@chakra-ui/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") const bg = useColorModeValue("white", "gray.780")
return ( return (
<Box <Box
@ -9,8 +13,8 @@ export const Card = (props: BoxProps) => {
borderRadius=".5rem" borderRadius=".5rem"
borderWidth="1px" borderWidth="1px"
p={[2,2,4,4]} p={[2,2,4,4]}
// boxShadow="0 1px 1px 0 rgb(0 0 0 / 5%)" boxShadow={shadowed? "0 1px 1px 0 rgb(0 0 0 / 5%)" : null}
{...props} {...rest}
/> />
) )
} }

@ -30,7 +30,7 @@ export const TagCard= (props:Props) =>{
{showActions ? {showActions ?
<HStack> <HStack>
<Button size="sm" colorScheme="teal" variant="outline" onClick={onEdit}>Edit</Button> <Button size="sm" colorScheme="teal" variant="outline" onClick={onEdit}>Edit</Button>
<Button size="sm" onClick={onDelete} variant="ghost">Delete</Button> {/* <Button size="sm" onClick={onDelete} variant="ghost">Delete</Button> */}
</HStack> : </HStack> :
<ChakraTag py="1" px="3" colorScheme="cyan"><Count count={tag.posts} />&nbsp;posts</ChakraTag> <ChakraTag py="1" px="3" colorScheme="cyan"><Count count={tag.posts} />&nbsp;posts</ChakraTag>
} }

@ -14,7 +14,7 @@ import { Session } from "src/types/user"
import { useRouter } from "next/router" import { useRouter } from "next/router"
import storage from "utils/localStorage" import storage from "utils/localStorage"
import { ReserveUrls } from "src/data/reserve-urls" 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 { isAdmin, isEditor } from "utils/role"
import { logout } from "utils/session" import { logout } from "utils/session"
import Link from "next/link" import Link from "next/link"
@ -56,8 +56,9 @@ export const UserMenu = () => {
</Link> </Link>
<MenuDivider /> <MenuDivider />
{isEditor(session.user.role) && <Link href={`${ReserveUrls.Editor}/posts`}><MenuItem icon={<FaEdit fontSize="16" />} ></MenuItem></Link>} {isEditor(session.user.role) && <Link href={`${ReserveUrls.Editor}/posts`}><MenuItem icon={<FaEdit fontSize="16" />} ></MenuItem></Link>}
{isAdmin(session.user.role) && <Link href={`${ReserveUrls.Admin}/tags`}><MenuItem icon={<FaStar fontSize="16" />} ></MenuItem></Link>}
<Link href={`${ReserveUrls.Bookmarks}`}><MenuItem icon={<FaBookmark fontSize="16" />}></MenuItem></Link> <Link href={`${ReserveUrls.Bookmarks}`}><MenuItem icon={<FaBookmark fontSize="16" />}></MenuItem></Link>
<Link href={`${ReserveUrls.Follow}/tags`}><MenuItem icon={<FaHeart fontSize="16" />}></MenuItem></Link>
{isAdmin(session.user.role) && <Link href={`${ReserveUrls.Admin}/tags`}><MenuItem icon={<FaStar fontSize="16" />} ></MenuItem></Link>}
<MenuDivider /> <MenuDivider />
<Link href={`${ReserveUrls.Settings}/profile`}><MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem></Link> <Link href={`${ReserveUrls.Settings}/profile`}><MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem></Link>
<MenuItem onClick={() => logout()} icon={<FaSignOutAlt fontSize="16" />}></MenuItem> <MenuItem onClick={() => logout()} icon={<FaSignOutAlt fontSize="16" />}></MenuItem>

@ -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[] = [{ export const searchLinks: any[] = [{
title: '文章', title: '文章',
path: `${ReserveUrls.Search}/posts`, path: `${ReserveUrls.Search}/posts`,

@ -2,6 +2,7 @@
export enum ReserveUrls { export enum ReserveUrls {
Tags = "/tags", Tags = "/tags",
Courses = "/courses", Courses = "/courses",
Follow = "/follow",
Editor = "/editor", Editor = "/editor",
Admin = "/admin", Admin = "/admin",
Bookmarks = "/bookmarks", Bookmarks = "/bookmarks",

Loading…
Cancel
Save