pull/54/head
sunface 4 years ago
parent 686ab3430c
commit 78f037f813

@ -67,7 +67,7 @@ function PostEditPage() {
/>} />}
> >
<HStack style={{ height: 'calc(100vh - 145px)' }} alignItems="top"> <HStack style={{ height: 'calc(100vh - 145px)' }} alignItems="top">
<Card width="65%"> <Card width="65%" height="100%">
{editMode === EditMode.Edit ? {editMode === EditMode.Edit ?
<MarkdownEditor <MarkdownEditor
options={{ options={{

@ -82,7 +82,7 @@ const OnboardPage = () => {
return ( return (
<Center h="100vh" mt="-8"> <Center h="100vh" mt="-8">
<Card p="6" width="600px"> <Card p="6" width="800px">
{step === 1 ? <> {step === 1 ? <>
<Text layerStyle="textSecondary" fontWeight="bold">CREATE YOUR ACCOUNT</Text> <Text layerStyle="textSecondary" fontWeight="bold">CREATE YOUR ACCOUNT</Text>
<Heading size="md" mt="2">🤘 Let's start your {config.appName} journey</Heading> <Heading size="md" mt="2">🤘 Let's start your {config.appName} journey</Heading>

@ -1,4 +1,4 @@
import { Box, Button, Flex, Heading, HStack, Image, Text, VStack } from "@chakra-ui/react" import { Avatar, Box, Button, Flex, Heading, HStack, Image, Text, useToast, VStack } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Empty from "components/empty" import Empty from "components/empty"
import { MarkdownRender } from "components/markdown-editor/render" import { MarkdownRender } from "components/markdown-editor/render"
@ -17,16 +17,24 @@ import { isAdmin } from "utils/role"
import Follow from "components/interaction/follow" import Follow from "components/interaction/follow"
import Count from "components/count" import Count from "components/count"
import StoryFilters from "components/story/story-filter" import StoryFilters from "components/story/story-filter"
import { UserSimple } from "src/types/user"
import Users from "components/users/users"
import Head from "next/head"
import { getUserName } from "utils/user"
import Link from "next/link"
import { getSvgIcon } from "components/svg-icon"
const UserPage = () => { const UserPage = () => {
const router = useRouter() const router = useRouter()
const toast = useToast()
const [tag, setTag]: [Tag, any] = useState(null) const [tag, setTag]: [Tag, any] = useState(null)
const [moderators,setModerators]:[UserSimple[],any] = useState([])
const [followed, setFollowed] = useState(null) const [followed, setFollowed] = useState(null)
useEffect(() => { useEffect(() => {
if (tag) { if (tag) {
requestApi.get(`/interaction/followed/${tag.id}`).then(res => setFollowed(res.data)) requestApi.get(`/interaction/followed/${tag.id}`).then(res => setFollowed(res.data))
requestApi.get(`/tag/moderators/${tag.id}`).then(res => setModerators(res.data))
} }
}, [tag]) }, [tag])
@ -51,6 +59,30 @@ const UserPage = () => {
}, [router.query.name]) }, [router.query.name])
const session = useSession() const session = useSession()
const isModerator = () => {
if (isAdmin(session.user.role)) {
return true
}
for (const m of moderators) {
if (m.id === session.user.id) {
return true
}
}
return false
}
const removeStory = async id => {
await requestApi.delete(`/tag/story/${tag.id}/${id}`)
toast({
description: "从标签移除成功,刷新页面可看到效果",
status: "success",
duration: 3000,
isClosable: true,
})
}
return ( return (
<> <>
<SEO <SEO
@ -71,7 +103,7 @@ const UserPage = () => {
</Box> </Box>
<Box> <Box>
{followed !== null && <Follow followed={followed} targetID={tag.id} />} {followed !== null && <Follow followed={followed} targetID={tag.id} />}
{isAdmin(session?.user.role) && <Button ml="2" onClick={() => router.push(`${ReserveUrls.Admin}/tag/${tag.name}`)}>Edit</Button>} {isModerator() && <Button ml="2" onClick={() => router.push(`${ReserveUrls.Admin}/tag/${tag.name}`)}>Edit</Button>}
</Box> </Box>
</Flex> </Flex>
@ -80,8 +112,8 @@ const UserPage = () => {
<StoryFilters showBest={false} onChange={onFilterChange} /> <StoryFilters showBest={false} onChange={onFilterChange} />
</Card> </Card>
<Card width="100%" height="fit-content" p="0" px="3"> <Card width="100%" height="fit-content" p="0" px="3">
{tag.id && {tag.id &&
<Stories onLoad={initPosts} filter={filter} /> <Stories onLoad={initPosts} filter={filter} onRemove={removeStory}/>
} }
</Card> </Card>
</VStack> </VStack>
@ -101,9 +133,22 @@ const UserPage = () => {
</Card> </Card>
<Card mt="4"> <Card mt="4">
<Heading size="sm">About this tag</Heading> <HStack><Heading size="sm">About this tag </Heading></HStack>
<Box mt="2"><MarkdownRender md={tag.md} fontSize="1rem"></MarkdownRender></Box> <Box mt="2"><MarkdownRender md={tag.md} fontSize="1rem"></MarkdownRender></Box>
</Card> </Card>
{moderators.length > 0 && <Card mt="4">
<Heading size="sm">Tag moderators</Heading>
<VStack alignItems="left" mt="4">
{moderators.map(m => <a href={`/${m.username}`} target="_blank">
<HStack cursor="pointer">
<Avatar width="45px" height="45px" src={m.avatar}/>
<Heading size="sm">{getUserName(m)}</Heading>
</HStack>
</a>)}
</VStack>
</Card>}
</VStack> </VStack>
</HStack>} </HStack>}
</PageContainer1> </PageContainer1>

@ -63,12 +63,6 @@ func GetTagsByIDs(c *gin.Context) {
} }
func SubmitTag(c *gin.Context) { func SubmitTag(c *gin.Context) {
user := user.CurrentUser(c)
if !user.Role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoEditorPermission))
return
}
tag := &models.Tag{} tag := &models.Tag{}
err := c.Bind(&tag) err := c.Bind(&tag)
if err != nil { if err != nil {
@ -76,6 +70,12 @@ func SubmitTag(c *gin.Context) {
return return
} }
user := user.CurrentUser(c)
if !tags.IsModerator(tag.ID, user) {
c.JSON(http.StatusForbidden, common.RespError(e.NoEditorPermission))
return
}
tag.Creator = user.ID tag.Creator = user.ID
err1 := tags.SubmitTag(tag) err1 := tags.SubmitTag(tag)
if err1 != nil { if err1 != nil {
@ -157,6 +157,7 @@ func AddModerator(c *gin.Context) {
user := user.CurrentUser(c) user := user.CurrentUser(c)
if !user.Role.IsSuperAdmin() { if !user.Role.IsSuperAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission)) c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
} }
err := tags.AddModerator(req.TagID, req.Username) err := tags.AddModerator(req.TagID, req.Username)
@ -188,3 +189,27 @@ func DeleteModerator(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil)) c.JSON(http.StatusOK, common.RespSuccess(nil))
} }
func RemoveTagStory(c *gin.Context) {
tagID := c.Param("tagID")
storyID := c.Param("storyID")
if tagID == "" || storyID == "" {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
user := user.CurrentUser(c)
if !tags.IsModerator(tagID, user) {
c.JSON(http.StatusForbidden, common.RespError(e.NoEditorPermission))
return
}
err := tags.RemoveTagStory(tagID, storyID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}

@ -6,9 +6,11 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"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/config" "github.com/imdotdev/im.dev/server/pkg/config"
"github.com/imdotdev/im.dev/server/pkg/db" "github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
) )
type Config struct { type Config struct {
@ -58,6 +60,12 @@ func UpdateConfig(c *gin.Context) {
d := make(map[string]interface{}) d := make(map[string]interface{})
c.Bind(&d) c.Bind(&d)
currentUser := user.CurrentUser(c)
if !currentUser.Role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
b, _ := json.Marshal(&d) b, _ := json.Marshal(&d)
_, err := db.Conn.Exec(`UPDATE config SET data=?,updated=? WHERE id=?`, b, time.Now(), 1) _, err := db.Conn.Exec(`UPDATE config SET data=?,updated=? WHERE id=?`, b, time.Now(), 1)
if err != nil { if err != nil {

@ -104,6 +104,8 @@ func (s *Server) Start() error {
r.GET("/tag/moderators/:id", api.GetTagModerators) r.GET("/tag/moderators/:id", api.GetTagModerators)
r.POST("/tag/moderator", IsLogin(), api.AddModerator) r.POST("/tag/moderator", IsLogin(), api.AddModerator)
r.DELETE("/tag/moderator/:tagID/:userID", IsLogin(), api.DeleteModerator) r.DELETE("/tag/moderator/:tagID/:userID", IsLogin(), api.DeleteModerator)
r.DELETE("/tag/story/:tagID/:storyID", IsLogin(), api.RemoveTagStory)
// user apis // user apis
r.GET("/user/all", api.GetUsers) r.GET("/user/all", api.GetUsers)
r.POST("/user/ids", api.GetUsersByIDs) r.POST("/user/ids", api.GetUsersByIDs)

@ -311,3 +311,27 @@ func DeleteModerator(tagID, userID string) *e.Error {
return nil return nil
} }
func IsModerator(tagID string, user *models.User) bool {
if user.Role.IsAdmin() {
return true
}
var uid string
db.Conn.QueryRow("SELECT user_id FROM tag_moderators WHERE tag_id=? and user_id=?", tagID, user.ID).Scan(&uid)
if uid == user.ID {
return true
}
return false
}
func RemoveTagStory(tagID, storyID string) *e.Error {
_, err := db.Conn.Exec("DELETE FROM tags_using WHERE tag_id=? and target_id=?", tagID, storyID)
if err != nil {
logger.Warn("remove tag story error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}

@ -17,11 +17,12 @@ interface Props {
showOrg?: boolean showOrg?: boolean
onLoad?: any onLoad?: any
filter?: string filter?: string
onRemove?: any
} }
export const Stroies = (props: Props) => { export const Stroies = (props: Props) => {
const { card = StoryCard, showFooter = true, type = "classic", showPinned = false, showOrg = true, onLoad, filter } = props const { card = StoryCard, showFooter = true, type = "classic", showPinned = false, showOrg = true, onLoad, filter,onRemove } = props
const [posts, setPosts] = useState([]) const [posts, setPosts] = useState([])
const [noMore, setNoMore] = useState(false) const [noMore, setNoMore] = useState(false)
@ -93,7 +94,7 @@ export const Stroies = (props: Props) => {
<VStack alignItems="left"> <VStack alignItems="left">
{posts.map((story, i) => {posts.map((story, i) =>
<Box py="2" borderBottom={showBorder(i) ? `1px solid ${borderColor}` : null} key={story.id} px="1"> <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} showPinned={showPinned} showOrg={showOrg} /> <Card story={story} size={props.size} type={type} highlight={props.highlight} showPinned={showPinned} showOrg={showOrg} onRemove={onRemove} />
</Box>)} </Box>)}
</VStack> </VStack>
{isFetching && 'Fetching more list items...'} {isFetching && 'Fetching more list items...'}

@ -1,5 +1,5 @@
import React from "react" import React from "react"
import { Box, Heading, HStack, Image, Tag, Text, useMediaQuery, VStack } from "@chakra-ui/react" import { Box, Flex, Heading, HStack, Image, Tag, Text, useMediaQuery, VStack, AlertDialog, AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogBody, AlertDialogFooter, Button } from "@chakra-ui/react"
import { Story } from "src/types/story" import { Story } from "src/types/story"
import StoryAuthor from "./story-author" import StoryAuthor from "./story-author"
import Link from "next/link" import Link from "next/link"
@ -9,7 +9,6 @@ import { getSvgIcon } from "components/svg-icon"
import Count from "components/count" import Count from "components/count"
import Highlighter from 'react-highlight-words'; import Highlighter from 'react-highlight-words';
import { IDType } from "src/types/id" import { IDType } from "src/types/id"
import { ReserveUrls } from "src/data/reserve-urls"
import { getCommentsUrl, getStoryUrl } from "utils/story" import { getCommentsUrl, getStoryUrl } from "utils/story"
interface Props { interface Props {
@ -17,6 +16,7 @@ interface Props {
type?: string type?: string
highlight?: string highlight?: string
showOrg?: boolean showOrg?: boolean
onRemove?: any
} }
@ -25,47 +25,85 @@ export const StoryCard = (props: Props) => {
const [isLargeScreen] = useMediaQuery("(min-width: 768px)") const [isLargeScreen] = useMediaQuery("(min-width: 768px)")
const Layout = isLargeScreen ? HStack : VStack const Layout = isLargeScreen ? HStack : VStack
const [isOpen, setIsOpen] = React.useState(false)
const onClose = () => setIsOpen(false)
const cancelRef = React.useRef()
return ( return (
<VStack alignItems="left" spacing={type === "classic" ? 4 : 2} p="2"> <>
<StoryAuthor story={story} showFooter={false} size="md" showOrg={props.showOrg}/> <VStack alignItems="left" spacing={type === "classic" ? 4 : 2} p="2">
<a href={getStoryUrl(story)} target="_blank"> <Flex justifyContent="space-between" alignItems="center">
<Layout alignItems={isLargeScreen ? "top" : "left"} cursor="pointer" pl="2" pt="1"> <StoryAuthor story={story} showFooter={false} size="md" showOrg={props.showOrg} />
<VStack alignItems="left" spacing={type === "classic" ? 3 : 2} width={isLargeScreen && type === "classic" ? "calc(100% - 15rem)" : '100%'}> {props.onRemove && <Box cursor="pointer" onClick={() => setIsOpen(true)}>{getSvgIcon("close", "1.1rem")}</Box>}
<Heading size="md" fontSize={type === "classic" ? '1.3rem' : '1.2rem'}> </Flex>
<Highlighter <a href={getStoryUrl(story)} target="_blank">
highlightClassName="highlight-search-match" <Layout alignItems={isLargeScreen ? "top" : "left"} cursor="pointer" pl="2" pt="1">
textToHighlight={story.title} <VStack alignItems="left" spacing={type === "classic" ? 3 : 2} width={isLargeScreen && type === "classic" ? "calc(100% - 15rem)" : '100%'}>
searchWords={[props.highlight]} <Heading size="md" fontSize={type === "classic" ? '1.3rem' : '1.2rem'}>
/> <Highlighter
{story.type === IDType.Series && <Tag size="sm" ml="2" mt="2px">SERIES</Tag>} highlightClassName="highlight-search-match"
{story.pinned && <Tag size="sm" ml="2" mt="2px"></Tag>} textToHighlight={story.title}
</Heading> searchWords={[props.highlight]}
{type !== "classic" && <HStack>{story.rawTags.map(t => <Text layerStyle="textSecondary" fontSize="md">#{t.name}</Text>)}</HStack>} />
<Text layerStyle={type === "classic" ? "textSecondary" : null}> {story.type === IDType.Series && <Tag size="sm" ml="2" mt="2px">SERIES</Tag>}
<Highlighter {story.pinned && <Tag size="sm" ml="2" mt="2px"></Tag>}
highlightClassName="highlight-search-match" </Heading>
textToHighlight={story.brief}
searchWords={[props.highlight]}
/></Text>
</VStack>
{story.cover && type === "classic" && <Image src={story.cover} width="15rem" height="120px" pt={isLargeScreen ? 0 : 2} borderRadius="4px" />}
</Layout>
</a>
<HStack pl="2" spacing="5"> {type !== "classic" && <HStack>{story.rawTags.map(t => <Text layerStyle="textSecondary" fontSize="md">#{t.name}</Text>)}</HStack>}
<Like storyID={story.id} liked={story.liked} count={story.likes} fontSize="18px" /> <Text layerStyle={type === "classic" ? "textSecondary" : null}>
<a href={`${getCommentsUrl(story)}#comments`} target="_blank"> <Highlighter
<HStack opacity="0.9" cursor="pointer"> highlightClassName="highlight-search-match"
{getSvgIcon("comments", "1.3rem")} textToHighlight={story.brief}
<Text ml="2"><Count count={story.comments} /></Text> searchWords={[props.highlight]}
</HStack> /></Text>
</VStack>
{story.cover && type === "classic" && <Image src={story.cover} width="15rem" height="120px" pt={isLargeScreen ? 0 : 2} borderRadius="4px" />}
</Layout>
</a> </a>
<HStack pl="2" spacing="5">
<Like storyID={story.id} liked={story.liked} count={story.likes} fontSize="18px" />
<a href={`${getCommentsUrl(story)}#comments`} target="_blank">
<HStack opacity="0.9" cursor="pointer">
{getSvgIcon("comments", "1.3rem")}
<Text ml="2"><Count count={story.comments} /></Text>
</HStack>
</a>
<Box style={{ marginLeft: '4px' }}><Bookmark height="1.05rem" storyID={story.id} bookmarked={story.bookmarked} /></Box>
</HStack>
</VStack>
<AlertDialog
isOpen={isOpen}
leastDestructiveRef={cancelRef}
onClose={onClose}
>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
- {story.title}
</AlertDialogHeader>
<AlertDialogBody>
Are you sure? You can't undo this action afterwards.
</AlertDialogBody>
<Box style={{ marginLeft: '4px' }}><Bookmark height="1.05rem" storyID={story.id} bookmarked={story.bookmarked} /></Box> <AlertDialogFooter>
</HStack> <Button ref={cancelRef} onClick={onClose}>
</VStack> Cancel
</Button>
<Button colorScheme="red" onClick={() => {setIsOpen(false);props.onRemove(story.id)}} ml={3}>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
) )
} }

Loading…
Cancel
Save