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

@ -28,7 +28,7 @@ const StyledLink = React.forwardRef(function StyledLink(
fontWeight: "600",
}}
{...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">
{
user &&
<Box alignItems="left">
<Box alignItems="left" pb="6">
<Card p="0" borderTop="none">
<Box backgroundImage={`url(${user.cover})`} height="300px" width="100%" backgroundSize="cover" backgroundPosition="center" />
<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 { 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 = () => {
</Flex>
{
tags.length === 0 ?
<>
<Center mt="4">
<Image height="25rem" src="/empty-posts.png" />
</Center>
<Center mt="8">
<Heading size="sm"></Heading>
</Center>
</>
<Empty />
:
<>
<VStack mt="4">

@ -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",

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

@ -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() {

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

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

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

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

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

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

@ -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 { 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 (
<Box
@ -9,8 +13,8 @@ export const Card = (props: BoxProps) => {
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}
/>
)
}

@ -30,7 +30,7 @@ export const TagCard= (props:Props) =>{
{showActions ?
<HStack>
<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> :
<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 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 = () => {
</Link>
<MenuDivider />
{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.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 />
<Link href={`${ReserveUrls.Settings}/profile`}><MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem></Link>
<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[] = [{
title: '文章',
path: `${ReserveUrls.Search}/posts`,

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

Loading…
Cancel
Save