mirror of https://github.com/sunface/rust-course
parent
453d8c7718
commit
74b02b029e
@ -1,15 +0,0 @@
|
||||
const navLinks = [{
|
||||
title: '主页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
url: '/tags',
|
||||
},
|
||||
{
|
||||
title: '学习资料',
|
||||
url: '/courses',
|
||||
},
|
||||
]
|
||||
|
||||
export default navLinks
|
@ -0,0 +1,86 @@
|
||||
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} from "src/data/links"
|
||||
import { requestApi } from "utils/axios/request"
|
||||
import TagCard from "components/posts/tag-card"
|
||||
import { Post } from "src/types/posts"
|
||||
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"
|
||||
|
||||
|
||||
const PostsPage = () => {
|
||||
const [tags, setTags] = useState([])
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const getTags = () => {
|
||||
requestApi.get(`/tags`).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(`/admin/tag/${id}`)
|
||||
getTags()
|
||||
toast({
|
||||
description: "删除成功",
|
||||
status: "success",
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav />
|
||||
<PageContainer>
|
||||
<Box display="flex">
|
||||
<Sidebar routes={adminLinks} width="250px" height="fit-content" />
|
||||
<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 ?
|
||||
<>
|
||||
<Center mt="4">
|
||||
<Image height="25rem" src="/empty-posts.png" />
|
||||
</Center>
|
||||
<Center mt="8">
|
||||
<Heading size="sm">你还没创建任何标签</Heading>
|
||||
</Center>
|
||||
</>
|
||||
:
|
||||
<>
|
||||
<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>
|
||||
</PageContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default PostsPage
|
||||
|
@ -0,0 +1,86 @@
|
||||
import { Box, Button, chakra, Flex, Heading, HStack, Image, Text } from "@chakra-ui/react"
|
||||
import Card from "components/card"
|
||||
import Container from "components/container"
|
||||
import { MarkdownRender } from "components/markdown-editor/render"
|
||||
import SEO from "components/seo"
|
||||
import siteConfig from "configs/site-config"
|
||||
import useSession from "hooks/use-session"
|
||||
import Nav from "layouts/nav/nav"
|
||||
import PageContainer from "layouts/page-container"
|
||||
import { useRouter } from "next/router"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { ReserveUrls } from "src/data/reserve-urls"
|
||||
import { Tag } from "src/types/tag"
|
||||
import { requestApi } from "utils/axios/request"
|
||||
import { isAdmin } from "utils/role"
|
||||
|
||||
const UserPage = () => {
|
||||
const router = useRouter()
|
||||
|
||||
const [tag, setTag]: [Tag, any] = useState({})
|
||||
const getTag = async () => {
|
||||
const res = await requestApi.get(`/tag/${router.query.name}`)
|
||||
setTag(res.data)
|
||||
}
|
||||
useEffect(() => {
|
||||
if (router.query.name) {
|
||||
getTag()
|
||||
}
|
||||
}, [router.query.name])
|
||||
|
||||
const session = useSession()
|
||||
|
||||
return (
|
||||
<>
|
||||
<SEO
|
||||
title={siteConfig.seo.title}
|
||||
description={siteConfig.seo.description}
|
||||
/>
|
||||
<Nav />
|
||||
<PageContainer>
|
||||
{tag.name && <HStack alignItems="top" spacing="4">
|
||||
<Box width="70%">
|
||||
<Card p="0">
|
||||
<Image src={tag.cover} />
|
||||
<Image src={tag.icon} width="80px" position="relative" top="-40px" left="40px"/>
|
||||
<Flex justifyContent="space-between" alignItems="center" px="8" pb="6" mt="-1rem">
|
||||
<Box>
|
||||
<Heading size="lg">{tag.title}</Heading>
|
||||
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">#{tag.name}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button colorScheme="teal">Follow</Button>
|
||||
{isAdmin(session.user.role) && <Button ml="2" onClick={() => router.push(`${ReserveUrls.Admin}/tag/${tag.name}`)}>Edit</Button>}
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
</Card>
|
||||
</Box>
|
||||
<Box width="30%">
|
||||
<Card>
|
||||
<Flex justifyContent="space-between" alignItems="center" px={[0,2,4,8]}>
|
||||
<Box>
|
||||
<Heading size="lg">59.8K</Heading>
|
||||
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">Followers</Text>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Heading size="lg">13.4K</Heading>
|
||||
<Text layerStyle="textSecondary" fontWeight="500" fontSize="1.2rem" mt="1" ml="1">Posts</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
<Card mt="4">
|
||||
<Heading size="sm">About this tag</Heading>
|
||||
<Box mt="2"><MarkdownRender md={tag.md} fontSize="1rem"></MarkdownRender></Box>
|
||||
</Card>
|
||||
</Box>
|
||||
</HStack>}
|
||||
</PageContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserPage
|
||||
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright © 2020 NAME HERE <EMAIL ADDRESS>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
// "fmt"
|
||||
|
||||
"github.com/imdotdev/im.dev/server/internal/storage"
|
||||
"github.com/imdotdev/im.dev/server/pkg/config"
|
||||
"github.com/imdotdev/im.dev/server/pkg/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var createFlags []string
|
||||
var dropFlags []string
|
||||
|
||||
// sqlCmd represents the sql command
|
||||
var sqlCmd = &cobra.Command{
|
||||
Use: "sql",
|
||||
Short: "Manage sqls,e.g create/drop table",
|
||||
Long: ``,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
config.Init("config.yaml")
|
||||
log.InitLogger(config.Data.Common.LogLevel)
|
||||
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
switch f.Name {
|
||||
case "create":
|
||||
if len(createFlags) > 0 {
|
||||
storage.CreateTables(createFlags)
|
||||
}
|
||||
break
|
||||
case "drop":
|
||||
if len(dropFlags) > 0 {
|
||||
storage.DropTables(dropFlags)
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(sqlCmd)
|
||||
|
||||
sqlCmd.Flags().StringSliceVar(&createFlags, "create", nil, "Create Sql Tables")
|
||||
|
||||
sqlCmd.Flags().StringSliceVar(&dropFlags, "drop", nil, "Drop Sql Tables")
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/imdotdev/im.dev/server/internal/posts"
|
||||
"github.com/imdotdev/im.dev/server/internal/session"
|
||||
"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 GetTag(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
res, err := posts.GetTag(name)
|
||||
if err != nil {
|
||||
c.JSON(err.Status, common.RespError(err.Message))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, common.RespSuccess(res))
|
||||
}
|
||||
|
||||
func GetTags(c *gin.Context) {
|
||||
res, err := posts.GetTags()
|
||||
if err != nil {
|
||||
c.JSON(err.Status, common.RespError(err.Message))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, common.RespSuccess(res))
|
||||
}
|
||||
|
||||
func SubmitTag(c *gin.Context) {
|
||||
user := session.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 {
|
||||
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
|
||||
return
|
||||
}
|
||||
|
||||
tag.Creator = user.ID
|
||||
err1 := posts.SubmitTag(tag)
|
||||
if err1 != nil {
|
||||
c.JSON(err1.Status, common.RespError(err1.Message))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, common.RespSuccess(nil))
|
||||
}
|
||||
|
||||
func DeleteTag(c *gin.Context) {
|
||||
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
|
||||
return
|
||||
}
|
||||
|
||||
user := session.CurrentUser(c)
|
||||
if !user.Role.IsAdmin() {
|
||||
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
|
||||
}
|
||||
|
||||
err := posts.DeleteTag(id)
|
||||
if err != nil {
|
||||
c.JSON(err.Status, common.RespError(err.Message))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, common.RespSuccess(nil))
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
package posts
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/imdotdev/im.dev/server/pkg/db"
|
||||
"github.com/imdotdev/im.dev/server/pkg/e"
|
||||
"github.com/imdotdev/im.dev/server/pkg/models"
|
||||
"github.com/imdotdev/im.dev/server/pkg/utils"
|
||||
)
|
||||
|
||||
func SubmitTag(tag *models.Tag) *e.Error {
|
||||
if strings.TrimSpace(tag.Title) == "" {
|
||||
return e.New(http.StatusBadRequest, "title格式不合法")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(tag.Name) == "" {
|
||||
return e.New(http.StatusBadRequest, "name格式不合法")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(tag.Name) == "new" {
|
||||
return e.New(http.StatusBadRequest, "name不能为new")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(tag.Cover) != "" && !govalidator.IsURL(tag.Cover) {
|
||||
return e.New(http.StatusBadRequest, "图片链接格式不正确")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(tag.Icon) != "" && !govalidator.IsURL(tag.Icon) {
|
||||
return e.New(http.StatusBadRequest, "图片链接格式不正确")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
md := utils.Compress(tag.Md)
|
||||
|
||||
if tag.ID == 0 {
|
||||
//create
|
||||
_, err := db.Conn.Exec("INSERT INTO tags (creator,name, title, md, icon, cover, created, updated) VALUES(?,?,?,?,?,?,?,?)",
|
||||
tag.Creator, tag.Name, tag.Title, md, tag.Icon, tag.Cover, now, now)
|
||||
if err != nil {
|
||||
if e.IsErrUniqueConstraint(err) {
|
||||
return e.New(http.StatusConflict, "同样的Tag name已存在")
|
||||
}
|
||||
|
||||
logger.Warn("submit post error", "error", err)
|
||||
return e.New(http.StatusInternalServerError, e.Internal)
|
||||
}
|
||||
} else {
|
||||
_, err := db.Conn.Exec("UPDATE tags SET name=?, title=?, md=?, icon=?, cover=?, updated=? WHERE id=?",
|
||||
tag.Name, tag.Title, md, tag.Icon, tag.Cover, now, tag.ID)
|
||||
if err != nil {
|
||||
logger.Warn("upate post error", "error", err)
|
||||
return e.New(http.StatusInternalServerError, e.Internal)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetTags() (models.Tags, *e.Error) {
|
||||
tags := make(models.Tags, 0)
|
||||
|
||||
rows, err := db.Conn.Query("SELECT id,creator,title,name,icon,cover,created,updated from tags")
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return tags, nil
|
||||
}
|
||||
logger.Warn("get tags error", "error", err)
|
||||
return tags, e.New(http.StatusInternalServerError, e.Internal)
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
tag := &models.Tag{}
|
||||
err := rows.Scan(&tag.ID, &tag.Creator, &tag.Title, &tag.Name, &tag.Icon, &tag.Cover, &tag.Created, &tag.Updated)
|
||||
if err != nil {
|
||||
logger.Warn("scan tags error", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
sort.Sort(tags)
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func DeleteTag(id int64) *e.Error {
|
||||
_, err := db.Conn.Exec("DELETE FROM tags WHERE id=?", id)
|
||||
if err != nil {
|
||||
logger.Warn("delete post error", "error", err)
|
||||
return e.New(http.StatusInternalServerError, e.Internal)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetTag(name string) (*models.Tag, *e.Error) {
|
||||
tag := &models.Tag{}
|
||||
var rawmd []byte
|
||||
err := db.Conn.QueryRow("SELECT id,creator,title,name,icon,cover,created,updated,md from tags where name=?", name).Scan(
|
||||
&tag.ID, &tag.Creator, &tag.Title, &tag.Name, &tag.Icon, &tag.Cover, &tag.Created, &tag.Updated, &rawmd,
|
||||
)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, e.New(http.StatusNotFound, e.NotFound)
|
||||
}
|
||||
logger.Warn("get post error", "error", err)
|
||||
return nil, e.New(http.StatusInternalServerError, e.Internal)
|
||||
}
|
||||
|
||||
md, _ := utils.Uncompress(rawmd)
|
||||
tag.Md = string(md)
|
||||
|
||||
return tag, nil
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/imdotdev/im.dev/server/pkg/db"
|
||||
"github.com/imdotdev/im.dev/server/pkg/log"
|
||||
)
|
||||
|
||||
func CreateTables(names []string) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
DropTables(names)
|
||||
}
|
||||
}()
|
||||
err := connectDatabase()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, tbl := range names {
|
||||
q, ok := sqlTables[tbl]
|
||||
if !ok {
|
||||
log.RootLogger.Crit("target sql table not exist", "table_name", tbl)
|
||||
panic("create sql of '" + tbl + "' table not exist")
|
||||
}
|
||||
|
||||
// check table already exists
|
||||
_, err := db.Conn.Query(fmt.Sprintf("SELECT * from %s LIMIT 1", tbl))
|
||||
if err == nil || err == sql.ErrNoRows {
|
||||
log.RootLogger.Info("Table already exist,skip creating", "table_name", tbl)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = db.Conn.Exec(q)
|
||||
if err != nil {
|
||||
log.RootLogger.Crit("database error", "error", err.Error())
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
log.RootLogger.Info("sql table created ok", "table_name", tbl)
|
||||
}
|
||||
}
|
||||
|
||||
func DropTables(names []string) {
|
||||
err := connectDatabase()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, tbl := range names {
|
||||
q := fmt.Sprintf("DROP TABLE IF EXISTS %s", tbl)
|
||||
_, err := db.Conn.Exec(q)
|
||||
if err != nil {
|
||||
log.RootLogger.Warn("drop table error", "error", err, "query", q)
|
||||
}
|
||||
log.RootLogger.Info("sql table dropped ok", "table_name", tbl)
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package common
|
||||
|
||||
// 需要同时修改ui/src/data/reserve-urls.ts
|
||||
var ReserverURLs = []string{
|
||||
"/tags", "/courses", "/editor", "/admin", "/bookmarks", "/settings", "/jobs", "/books", "/notifications", "/sponsors", "/explore", "/login",
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package e
|
||||
|
||||
import "strings"
|
||||
|
||||
func IsErrUniqueConstraint(err error) bool {
|
||||
if strings.Contains(err.Error(), "UNIQUE") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Tag struct {
|
||||
ID int64 `json:"id"`
|
||||
Creator int64 `json:"creator"`
|
||||
Title string `json:"title"`
|
||||
Name string `json:"name"`
|
||||
Md string `json:"md"`
|
||||
Cover string `json:"cover"`
|
||||
Icon string `json:"icon"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
type Tags []*Tag
|
||||
|
||||
func (t Tags) Len() int { return len(t) }
|
||||
func (t Tags) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||
func (t Tags) Less(i, j int) bool {
|
||||
return t[i].Created.Unix() > t[j].Created.Unix()
|
||||
}
|
@ -1,14 +1,18 @@
|
||||
import React from "react"
|
||||
import { Box, BoxProps } from "@chakra-ui/react"
|
||||
import { Box, BoxProps, useColorModeValue } from "@chakra-ui/react"
|
||||
|
||||
export const Card = (props: BoxProps) => (
|
||||
export const Card = (props: BoxProps) => {
|
||||
const bg = useColorModeValue("white", "gray.780")
|
||||
return (
|
||||
<Box
|
||||
bg={bg}
|
||||
borderRadius=".5rem"
|
||||
borderWidth="1px"
|
||||
p="1rem"
|
||||
p={["0rem",".5rem","1rem"]}
|
||||
boxShadow="0 1px 2px 0 rgb(0 0 0 / 5%)"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
||||
|
@ -0,0 +1,34 @@
|
||||
import React from "react"
|
||||
import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Link} from "@chakra-ui/react"
|
||||
import { Tag } from "src/types/tag"
|
||||
import { ReserveUrls } from "src/data/reserve-urls"
|
||||
import NextLink from "next/link"
|
||||
|
||||
type Props = PropsOf<typeof chakra.div> & {
|
||||
tag: Tag
|
||||
showActions: boolean
|
||||
onEdit?: any
|
||||
onDelete?: any
|
||||
}
|
||||
|
||||
|
||||
export const TagCard= (props:Props) =>{
|
||||
const {tag,showActions,onEdit,onDelete, ...rest} = props
|
||||
|
||||
return (
|
||||
<Flex justifyContent="space-between" {...rest}>
|
||||
<NextLink href={`${ReserveUrls.Tags}/${tag.name}`}>
|
||||
<Heading size="sm" display="flex" alignItems="center" cursor="pointer">
|
||||
<Image src={tag.icon} width="43px" mr="2"/>
|
||||
{tag.title}
|
||||
</Heading>
|
||||
</NextLink>
|
||||
{props.showActions && <HStack>
|
||||
<Button size="sm" colorScheme="teal" variant="outline" onClick={onEdit}>Edit</Button>
|
||||
<Button size="sm" onClick={props.onDelete} variant="ghost">Delete</Button>
|
||||
</HStack>}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagCard
|
@ -0,0 +1,40 @@
|
||||
import React from "react"
|
||||
import { Box, BoxProps, Flex, useColorModeValue ,Text, Link, HStack} from "@chakra-ui/react"
|
||||
import { FaGithub, FaGlobeAsia, FaGreaterThan, FaLink, FaLocationArrow, FaTwitter } from "react-icons/fa"
|
||||
import userCustomTheme from "theme/user-custom"
|
||||
|
||||
interface Props {
|
||||
type: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export const WebsiteLink = ({type, url,...rest}: Props) => {
|
||||
let icon0
|
||||
let title
|
||||
switch (type) {
|
||||
case "github":
|
||||
title= "Github"
|
||||
icon0 = <FaGithub />
|
||||
break;
|
||||
case "twitter":
|
||||
title = "Twitter"
|
||||
icon0 = <FaTwitter />
|
||||
break;
|
||||
default:
|
||||
title = "Official website"
|
||||
icon0 = <FaGlobeAsia />
|
||||
break;
|
||||
}
|
||||
return (
|
||||
<Flex justifyContent="space-between" alignItems="center" cursor="pointer" as="a" href={url} target="_blank" py="1" px="2" layerStyle="textSecondary" fontSize="1.1rem" {...rest} _hover={{bg: useColorModeValue(userCustomTheme.hoverBg.light,userCustomTheme.hoverBg.dark)}}>
|
||||
<HStack>
|
||||
{icon0}
|
||||
<Text ml="2">{title}</Text>
|
||||
</HStack>
|
||||
|
||||
<FaLocationArrow fontSize="13px" />
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default WebsiteLink
|
@ -1,24 +0,0 @@
|
||||
import React from 'react'
|
||||
import { FaFileAlt, FaScroll, FaBookOpen } from 'react-icons/fa'
|
||||
import { Route } from 'src/types/route'
|
||||
const editorLinks: Route[] = [{
|
||||
title: '文章',
|
||||
path: '/editor/posts',
|
||||
icon: <FaFileAlt />,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
title: '系列',
|
||||
path: '/editor/series',
|
||||
icon: <FaBookOpen />,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
title: '课程',
|
||||
path: '/editor/course',
|
||||
icon: <FaScroll />,
|
||||
disabled: false
|
||||
},
|
||||
]
|
||||
|
||||
export default editorLinks
|
@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { FaFileAlt, FaScroll, FaBookOpen, FaTags } from 'react-icons/fa'
|
||||
import { Route } from 'src/types/route'
|
||||
import { ReserveUrls } from './reserve-urls'
|
||||
export const editorLinks: Route[] = [{
|
||||
title: '文章',
|
||||
path: `${ReserveUrls.Editor}/posts`,
|
||||
icon: <FaFileAlt />,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
title: '系列',
|
||||
path: `${ReserveUrls.Editor}/series`,
|
||||
icon: <FaBookOpen />,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
title: '课程',
|
||||
path: `${ReserveUrls.Editor}/course`,
|
||||
icon: <FaScroll />,
|
||||
disabled: false
|
||||
}
|
||||
]
|
||||
|
||||
export const adminLinks: Route[] = [{
|
||||
title: '标签管理',
|
||||
path: `${ReserveUrls.Admin}/tags`,
|
||||
icon: <FaTags />,
|
||||
disabled: false
|
||||
}
|
||||
]
|
@ -0,0 +1,15 @@
|
||||
// 需要同时修改server/pkg/common/reserve_urls.go
|
||||
export enum ReserveUrls {
|
||||
Tags = "/tags",
|
||||
Courses = "/courses",
|
||||
Editor = "/editor",
|
||||
Admin = "/admin",
|
||||
Bookmarks = "/bookmarks",
|
||||
Settings = "/settings",
|
||||
Jobs = "/jobs",
|
||||
Books = "/books",
|
||||
Notifications = "/notifications",
|
||||
Sponsors = "/sponsors",
|
||||
Explore = "/explore",
|
||||
Login = "/login",
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
export interface Tag {
|
||||
id?: number
|
||||
name?: string
|
||||
title?: string
|
||||
md?: string
|
||||
icon?: string
|
||||
cover?: string
|
||||
created?: string
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { Role } from "src/types/role";
|
||||
|
||||
export function isAdmin(role) {
|
||||
return role === Role.ADMIN || role === Role.SUPER_ADMIN
|
||||
}
|
||||
|
||||
export function isEditor(role) {
|
||||
return role === Role.ADMIN || role === Role.SUPER_ADMIN || role === Role.EDITOR
|
||||
}
|
Loading…
Reference in new issue