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">
<Card width="65%">
<Card width="65%" height="100%">
{editMode === EditMode.Edit ?
<MarkdownEditor
options={{

@ -82,7 +82,7 @@ const OnboardPage = () => {
return (
<Center h="100vh" mt="-8">
<Card p="6" width="600px">
<Card p="6" width="800px">
{step === 1 ? <>
<Text layerStyle="textSecondary" fontWeight="bold">CREATE YOUR ACCOUNT</Text>
<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 Empty from "components/empty"
import { MarkdownRender } from "components/markdown-editor/render"
@ -17,16 +17,24 @@ import { isAdmin } from "utils/role"
import Follow from "components/interaction/follow"
import Count from "components/count"
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 router = useRouter()
const toast = useToast()
const [tag, setTag]: [Tag, any] = useState(null)
const [moderators,setModerators]:[UserSimple[],any] = useState([])
const [followed, setFollowed] = useState(null)
useEffect(() => {
if (tag) {
requestApi.get(`/interaction/followed/${tag.id}`).then(res => setFollowed(res.data))
requestApi.get(`/tag/moderators/${tag.id}`).then(res => setModerators(res.data))
}
}, [tag])
@ -51,6 +59,30 @@ const UserPage = () => {
}, [router.query.name])
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 (
<>
<SEO
@ -71,7 +103,7 @@ const UserPage = () => {
</Box>
<Box>
{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>
</Flex>
@ -80,8 +112,8 @@ const UserPage = () => {
<StoryFilters showBest={false} onChange={onFilterChange} />
</Card>
<Card width="100%" height="fit-content" p="0" px="3">
{tag.id &&
<Stories onLoad={initPosts} filter={filter} />
{tag.id &&
<Stories onLoad={initPosts} filter={filter} onRemove={removeStory}/>
}
</Card>
</VStack>
@ -101,9 +133,22 @@ const UserPage = () => {
</Card>
<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>
</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>
</HStack>}
</PageContainer1>

@ -63,12 +63,6 @@ func GetTagsByIDs(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{}
err := c.Bind(&tag)
if err != nil {
@ -76,6 +70,12 @@ func SubmitTag(c *gin.Context) {
return
}
user := user.CurrentUser(c)
if !tags.IsModerator(tag.ID, user) {
c.JSON(http.StatusForbidden, common.RespError(e.NoEditorPermission))
return
}
tag.Creator = user.ID
err1 := tags.SubmitTag(tag)
if err1 != nil {
@ -157,6 +157,7 @@ func AddModerator(c *gin.Context) {
user := user.CurrentUser(c)
if !user.Role.IsSuperAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
err := tags.AddModerator(req.TagID, req.Username)
@ -188,3 +189,27 @@ func DeleteModerator(c *gin.Context) {
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"
"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/config"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
)
type Config struct {
@ -58,6 +60,12 @@ func UpdateConfig(c *gin.Context) {
d := make(map[string]interface{})
c.Bind(&d)
currentUser := user.CurrentUser(c)
if !currentUser.Role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
b, _ := json.Marshal(&d)
_, err := db.Conn.Exec(`UPDATE config SET data=?,updated=? WHERE id=?`, b, time.Now(), 1)
if err != nil {

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

@ -311,3 +311,27 @@ func DeleteModerator(tagID, userID string) *e.Error {
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
onLoad?: any
filter?: string
onRemove?: any
}
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 [noMore, setNoMore] = useState(false)
@ -93,7 +94,7 @@ export const Stroies = (props: Props) => {
<VStack alignItems="left">
{posts.map((story, i) =>
<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>)}
</VStack>
{isFetching && 'Fetching more list items...'}

@ -1,5 +1,5 @@
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 StoryAuthor from "./story-author"
import Link from "next/link"
@ -9,7 +9,6 @@ import { getSvgIcon } from "components/svg-icon"
import Count from "components/count"
import Highlighter from 'react-highlight-words';
import { IDType } from "src/types/id"
import { ReserveUrls } from "src/data/reserve-urls"
import { getCommentsUrl, getStoryUrl } from "utils/story"
interface Props {
@ -17,6 +16,7 @@ interface Props {
type?: string
highlight?: string
showOrg?: boolean
onRemove?: any
}
@ -25,47 +25,85 @@ export const StoryCard = (props: Props) => {
const [isLargeScreen] = useMediaQuery("(min-width: 768px)")
const Layout = isLargeScreen ? HStack : VStack
const [isOpen, setIsOpen] = React.useState(false)
const onClose = () => setIsOpen(false)
const cancelRef = React.useRef()
return (
<VStack alignItems="left" spacing={type === "classic" ? 4 : 2} p="2">
<StoryAuthor story={story} showFooter={false} size="md" showOrg={props.showOrg}/>
<a href={getStoryUrl(story)} target="_blank">
<Layout alignItems={isLargeScreen ? "top" : "left"} cursor="pointer" pl="2" pt="1">
<VStack alignItems="left" spacing={type === "classic" ? 3 : 2} width={isLargeScreen && type === "classic" ? "calc(100% - 15rem)" : '100%'}>
<Heading size="md" fontSize={type === "classic" ? '1.3rem' : '1.2rem'}>
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={story.title}
searchWords={[props.highlight]}
/>
{story.type === IDType.Series && <Tag size="sm" ml="2" mt="2px">SERIES</Tag>}
{story.pinned && <Tag size="sm" ml="2" mt="2px"></Tag>}
</Heading>
{type !== "classic" && <HStack>{story.rawTags.map(t => <Text layerStyle="textSecondary" fontSize="md">#{t.name}</Text>)}</HStack>}
<Text layerStyle={type === "classic" ? "textSecondary" : null}>
<Highlighter
highlightClassName="highlight-search-match"
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>
<>
<VStack alignItems="left" spacing={type === "classic" ? 4 : 2} p="2">
<Flex justifyContent="space-between" alignItems="center">
<StoryAuthor story={story} showFooter={false} size="md" showOrg={props.showOrg} />
{props.onRemove && <Box cursor="pointer" onClick={() => setIsOpen(true)}>{getSvgIcon("close", "1.1rem")}</Box>}
</Flex>
<a href={getStoryUrl(story)} target="_blank">
<Layout alignItems={isLargeScreen ? "top" : "left"} cursor="pointer" pl="2" pt="1">
<VStack alignItems="left" spacing={type === "classic" ? 3 : 2} width={isLargeScreen && type === "classic" ? "calc(100% - 15rem)" : '100%'}>
<Heading size="md" fontSize={type === "classic" ? '1.3rem' : '1.2rem'}>
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={story.title}
searchWords={[props.highlight]}
/>
{story.type === IDType.Series && <Tag size="sm" ml="2" mt="2px">SERIES</Tag>}
{story.pinned && <Tag size="sm" ml="2" mt="2px"></Tag>}
</Heading>
<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>
{type !== "classic" && <HStack>{story.rawTags.map(t => <Text layerStyle="textSecondary" fontSize="md">#{t.name}</Text>)}</HStack>}
<Text layerStyle={type === "classic" ? "textSecondary" : null}>
<Highlighter
highlightClassName="highlight-search-match"
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">
<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>
</HStack>
</VStack>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose}>
Cancel
</Button>
<Button colorScheme="red" onClick={() => {setIsOpen(false);props.onRemove(story.id)}} ml={3}>
Delete
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
</>
)
}

Loading…
Cancel
Save