diff --git a/go.mod b/go.mod
index 2df8ce07..53468c09 100644
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@ require (
github.com/ramya-rao-a/go-outline v0.0.0-20200117021646-2a048b4510eb // indirect
github.com/rogpeppe/godef v1.1.2 // indirect
github.com/spf13/cobra v1.1.1
+ github.com/spf13/pflag v1.0.5
github.com/stamblerre/gocode v1.0.0 // indirect
github.com/uudashr/gopkgs v1.3.2 // indirect
golang.org/x/mod v0.4.1 // indirect
diff --git a/layouts/editor-nav.tsx b/layouts/nav/editor-nav.tsx
similarity index 99%
rename from layouts/editor-nav.tsx
rename to layouts/nav/editor-nav.tsx
index 6732487c..b28f580a 100644
--- a/layouts/editor-nav.tsx
+++ b/layouts/nav/editor-nav.tsx
@@ -13,7 +13,6 @@ import {
useDisclosure,
DrawerOverlay,
DrawerContent,
- Text,
Divider,
Heading
} from "@chakra-ui/react"
@@ -138,7 +137,7 @@ function EditorNav(props) {
pos="fixed"
top="0"
zIndex="3"
- bg={bg}
+ // bg={bg}
left="0"
right="0"
borderTop="4px solid"
diff --git a/layouts/nav/nav-links.ts b/layouts/nav/nav-links.ts
deleted file mode 100644
index 9af79cfa..00000000
--- a/layouts/nav/nav-links.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-const navLinks = [{
- title: '主页',
- url: '/',
-},
-{
- title: '标签',
- url: '/tags',
-},
-{
- title: '学习资料',
- url: '/courses',
-},
-]
-
-export default navLinks
\ No newline at end of file
diff --git a/layouts/nav/nav.tsx b/layouts/nav/nav.tsx
index 6c8606fa..b296a390 100644
--- a/layouts/nav/nav.tsx
+++ b/layouts/nav/nav.tsx
@@ -4,7 +4,6 @@ import {
Button,
HStack,
IconButton,
- Link,
useColorMode,
useColorModeValue,
useDisclosure,
@@ -15,44 +14,38 @@ import {
MenuItem,
MenuDivider,
Image,
+ Box
} from "@chakra-ui/react"
import siteConfig from "configs/site-config"
import { useViewportScroll } from "framer-motion"
import NextLink from "next/link"
import React from "react"
-import { FaMoon, FaSun, FaUserAlt, FaRegSun, FaSignOutAlt, FaRegBookmark, FaChartBar, FaHome, FaArrowRight, FaGithub, FaFileAlt, FaBookmark, FaEdit } from "react-icons/fa"
+import { FaMoon, FaSun, FaUserAlt, FaRegSun, FaSignOutAlt,FaStar, FaGithub, FaBookmark, FaEdit } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo"
import { MobileNavButton, MobileNavContent } from "../mobile-nav"
import AlgoliaSearch from "src/components/search/algolia-search"
import useSession from "hooks/use-session"
-import { requestApi } from "utils/axios/request"
-import { removeToken, saveToken } from "utils/axios/getToken"
import { Session } from "src/types/session"
-import navLinks from "./nav-links"
import { useRouter } from "next/router"
-import events from "utils/events"
import storage from "utils/localStorage"
import { logout } from "utils/session"
-
-
-const DiscordIcon = (props) => (
-
-)
-
-const GithubIcon = (props) => (
-
-)
+import { isAdmin, isEditor } from "utils/role"
+import { ReserveUrls } from "src/data/reserve-urls"
+import Link from "next/link"
+
+const navLinks = [{
+ title: '主页',
+ url: '/',
+},
+{
+ title: '标签',
+ url: ReserveUrls.Tags,
+},
+{
+ title: '学习资料',
+ url: ReserveUrls.Courses,
+},
+]
function HeaderContent() {
@@ -74,7 +67,7 @@ function HeaderContent() {
const login = () => {
console.log(router)
storage.set("current-page", asPath)
- router.push('/login')
+ router.push(ReserveUrls.Login)
}
return (
@@ -93,7 +86,7 @@ function HeaderContent() {
- {navLinks.map(link => {link.title})}
+ {navLinks.map(link => {link.title})}
@@ -106,7 +99,6 @@ function HeaderContent() {
@@ -155,7 +147,8 @@ function HeaderContent() {
Sunface
- {} href="/editor">创作中心}
+ {isEditor(session.user.role) && } >创作中心}
+ {isAdmin(session.user.role) && } >管理员}
}>书签收藏
}>偏好设置
@@ -186,7 +179,6 @@ function HeaderContent() {
}
function Header(props) {
- const bg = useColorModeValue("white", "gray.800")
const ref = React.useRef()
const [y, setY] = React.useState(0)
const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}
@@ -204,7 +196,6 @@ function Header(props) {
pos="fixed"
top="0"
zIndex="3"
- bg={bg}
left="0"
right="0"
borderTop="4px solid"
diff --git a/next-redirect.js b/next-redirect.js
index c7ce7514..f8dd012a 100644
--- a/next-redirect.js
+++ b/next-redirect.js
@@ -1,16 +1,10 @@
async function redirect() {
return [
- {
- source: "/discord",
- destination: "https://discord.gg/dQHfcWF",
- permanent: true,
- },
- // GENERAL
- {
- source: "/editor",
- destination: "/editor/posts",
- permanent: true,
- }
+ // {
+ // source: "/discord",
+ // destination: "https://discord.gg/dQHfcWF",
+ // permanent: true,
+ // }
]
}
diff --git a/pages/admin/tag/[id].tsx b/pages/admin/tag/[id].tsx
new file mode 100644
index 00000000..4b6dd92b
--- /dev/null
+++ b/pages/admin/tag/[id].tsx
@@ -0,0 +1,207 @@
+import { Box, Button, Flex, useColorMode, useColorModeValue, useDisclosure, useRadioGroup, useToast, chakra, Input, HStack, IconButton, Heading, Divider } from '@chakra-ui/react';
+import React, { useEffect, useState } from 'react';
+import { MarkdownEditor } from 'components/markdown-editor/editor';
+import PageContainer from 'layouts/page-container';
+import { EditMode } from 'src/types/editor';
+import { MarkdownRender } from 'components/markdown-editor/render';
+import { requestApi } from 'utils/axios/request';
+import { useRouter } from 'next/router';
+import { config } from 'utils/config';
+import { cloneDeep } from 'lodash';
+import { FaMoon, FaSun } from 'react-icons/fa';
+import Link from 'next/link';
+import NextLink from "next/link"
+import Logo, { LogoIcon } from 'components/logo';
+import RadioCard from 'components/radio-card';
+import { useViewportScroll } from 'framer-motion';
+import Card from 'components/card';
+import { Tag } from 'src/types/tag';
+
+
+function PostEditPage() {
+ const router = useRouter()
+ const { id } = router.query
+ const [editMode, setEditMode] = useState(EditMode.Edit)
+ const [tag, setTag]:[Tag,any] = useState({
+ md: `标签介绍,支持markdown`,
+ title: ''
+ })
+
+ const toast = useToast()
+ useEffect(() => {
+ if (id && id !== 'new') {
+ requestApi.get(`/tag/${id}`).then(res => setTag(res.data))
+ }
+ }, [id])
+
+ const onMdChange = newMd => {
+ setTag({
+ ...tag,
+ md: newMd
+ })
+ }
+
+ const onChange = () => {
+ setTag(cloneDeep(tag))
+ }
+
+
+ const publish = async () => {
+ const res = await requestApi.post(`/admin/tag`, tag)
+ toast({
+ description: "发布成功",
+ status: "success",
+ duration: 2000,
+ isClosable: true,
+ })
+ router.push(`/tags/${tag.name}`)
+ }
+
+ return (
+ setEditMode(v)}
+ publish={() => publish()}
+ />}
+ >
+
+
+ {editMode === EditMode.Edit ?
+ onMdChange(md)}
+ md={tag.md}
+ /> :
+
+
+
+ }
+
+
+
+ Title
+
+ { tag.title = e.target.value; onChange()}} mt="4" variant="flushed" size="sm" placeholder="Tag title..." focusBorderColor="teal.400" />
+
+
+ Name
+
+ { tag.name = e.target.value; onChange()}} mt="4" variant="flushed" size="sm" placeholder="Tag name..." focusBorderColor="teal.400" />
+
+
+ 封面
+
+ { tag.cover = e.target.value; onChange()}} mt="4" variant="flushed" size="sm" placeholder="图片链接,你可以用github当图片存储服务" focusBorderColor="teal.400" />
+
+
+ 图标
+
+ { tag.icon = e.target.value; onChange()}} mt="4" variant="flushed" size="sm" placeholder="图片链接" focusBorderColor="teal.400" />
+
+
+
+ );
+}
+
+export default PostEditPage
+
+function HeaderContent(props: any) {
+ const { toggleColorMode: toggleMode } = useColorMode()
+ const text = useColorModeValue("dark", "light")
+ const SwitchIcon = useColorModeValue(FaMoon, FaSun)
+
+ const editOptions = [EditMode.Edit, EditMode.Preview]
+ const { getRootProps, getRadioProps } = useRadioGroup({
+ name: "framework",
+ defaultValue: EditMode.Edit,
+ onChange: (v) => {
+ props.changeEditMode(v)
+ },
+ })
+ const group = getRootProps()
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {editOptions.map((value) => {
+ const radio = getRadioProps({ value })
+ return (
+
+ {value}
+
+ )
+ })}
+
+
+ }
+ />
+
+
+
+ >
+ )
+}
+
+function Nav(props) {
+ const bg = useColorModeValue("white", "gray.800")
+ const ref = React.useRef()
+ const [y, setY] = React.useState(0)
+ const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}
+
+ const { scrollY } = useViewportScroll()
+ React.useEffect(() => {
+ return scrollY.onChange(() => setY(scrollY.get()))
+ }, [scrollY])
+
+ return (
+ height ? "sm" : undefined}
+ transition="box-shadow 0.2s"
+ pos="fixed"
+ top="0"
+ zIndex="3"
+ bg={bg}
+ left="0"
+ right="0"
+ borderTop="4px solid"
+ borderTopColor="teal.400"
+ width="full"
+ >
+
+
+
+
+ )
+}
diff --git a/pages/admin/tags.tsx b/pages/admin/tags.tsx
new file mode 100644
index 00000000..bc8942f3
--- /dev/null
+++ b/pages/admin/tags.tsx
@@ -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 (
+ <>
+
+
+
+
+
+
+ 标签列表({tags.length})
+
+
+ {
+ tags.length === 0 ?
+ <>
+
+
+
+
+ 你还没创建任何标签
+
+ >
+ :
+ <>
+
+ {tags.map(tag =>
+
+ editTag(tag)} onDelete={() => deleteTag(tag.id)} />
+
+
+ )}
+
+ 没有更多标签了
+ >
+ }
+
+
+
+ >
+ )
+}
+export default PostsPage
+
diff --git a/pages/editor/post/[id].tsx b/pages/editor/post/[id].tsx
index bf95ded5..202f14e8 100644
--- a/pages/editor/post/[id].tsx
+++ b/pages/editor/post/[id].tsx
@@ -2,7 +2,7 @@ import { Box, Button, useToast} from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';
import { MarkdownEditor } from 'components/markdown-editor/editor';
import PageContainer from 'layouts/page-container';
-import EditorNav from 'layouts/editor-nav'
+import EditorNav from 'layouts/nav/editor-nav'
import { EditMode } from 'src/types/editor';
import { MarkdownRender } from 'components/markdown-editor/render';
import { Post } from 'src/types/posts';
@@ -10,6 +10,7 @@ import { requestApi } from 'utils/axios/request';
import { useRouter } from 'next/router';
import { config } from 'utils/config';
import { cloneDeep } from 'lodash';
+import Card from 'components/card';
const content = `
# test原创
@@ -77,16 +78,9 @@ function PostEditPage() {
publish={() => publish()}
/>}
>
-
+
{editMode === EditMode.Edit ?
onMdChange(md)}
md={ar.md}
/> :
@@ -94,7 +88,7 @@ function PostEditPage() {
}
-
+
);
}
diff --git a/pages/editor/posts.tsx b/pages/editor/posts.tsx
index 9daccaad..99a19c9e 100644
--- a/pages/editor/posts.tsx
+++ b/pages/editor/posts.tsx
@@ -4,7 +4,7 @@ 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 editorLinks from "src/data/editor-links"
+import {editorLinks} from "src/data/links"
import { requestApi } from "utils/axios/request"
import { useDisclosure } from "@chakra-ui/react"
import { Field, Form, Formik } from "formik"
@@ -13,6 +13,8 @@ import TextPostCard from "components/posts/text-post-card"
import { Post } from "src/types/posts"
import { FaExternalLinkAlt, FaRegEdit } from "react-icons/fa"
import { useRouter } from "next/router"
+import { ReserveUrls } from "src/data/reserve-urls"
+import Link from "next/link"
var validator = require('validator');
const newPost: Post = { title: '', url: '', cover: '' }
@@ -109,7 +111,7 @@ const PostsPage = () => {
} onClick={onOpen}>外部链接
- } as="a" href="/editor/post/new">原创博客
+ } >原创博客
:
diff --git a/pages/tags/[name].tsx b/pages/tags/[name].tsx
new file mode 100644
index 00000000..ccc0a736
--- /dev/null
+++ b/pages/tags/[name].tsx
@@ -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 (
+ <>
+
+
+
+ {tag.name &&
+
+
+
+
+
+
+ {tag.title}
+ #{tag.name}
+
+
+
+ {isAdmin(session.user.role) && }
+
+
+
+
+
+
+
+
+
+ 59.8K
+ Followers
+
+
+
+ 13.4K
+ Posts
+
+
+
+
+
+ About this tag
+
+
+
+ }
+
+ >
+ )
+}
+
+export default UserPage
+
diff --git a/server/cmd/sql.go b/server/cmd/sql.go
new file mode 100644
index 00000000..05c98310
--- /dev/null
+++ b/server/cmd/sql.go
@@ -0,0 +1,63 @@
+/*
+Copyright © 2020 NAME HERE
+
+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")
+}
diff --git a/server/internal/api/api.go b/server/internal/api/api.go
index 193cac41..4ec29f76 100644
--- a/server/internal/api/api.go
+++ b/server/internal/api/api.go
@@ -3,3 +3,5 @@ package api
import "github.com/imdotdev/im.dev/server/pkg/log"
var logger = log.RootLogger.New("logger", "api")
+
+/* 鉴权、数据合法性验证都在api模块进行处理 */
diff --git a/server/internal/api/tags.go b/server/internal/api/tags.go
new file mode 100644
index 00000000..2590abfc
--- /dev/null
+++ b/server/internal/api/tags.go
@@ -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))
+}
diff --git a/server/internal/posts/tags.go b/server/internal/posts/tags.go
new file mode 100644
index 00000000..12fdd22f
--- /dev/null
+++ b/server/internal/posts/tags.go
@@ -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
+}
diff --git a/server/internal/server.go b/server/internal/server.go
index a95440f1..a0e1fff5 100644
--- a/server/internal/server.go
+++ b/server/internal/server.go
@@ -58,6 +58,14 @@ func (s *Server) Start() error {
editorR.GET("/post/:id", api.GetEditorPost)
}
+ adminR := lr.Group("/admin")
+ {
+ adminR.POST("/tag", api.SubmitTag)
+ adminR.DELETE("/tag/:id", api.DeleteTag)
+ }
+
+ lr.GET("/tags", api.GetTags)
+ lr.GET("/tag/:name", api.GetTag)
}
err := router.Run(config.Data.Server.Addr)
if err != nil {
diff --git a/server/internal/storage/manage.go b/server/internal/storage/manage.go
new file mode 100644
index 00000000..16340040
--- /dev/null
+++ b/server/internal/storage/manage.go
@@ -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)
+ }
+}
diff --git a/server/internal/storage/sql_tables.go b/server/internal/storage/sql_tables.go
index 52ac992e..5d7bb1ff 100644
--- a/server/internal/storage/sql_tables.go
+++ b/server/internal/storage/sql_tables.go
@@ -47,5 +47,23 @@ var sqlTables = map[string]string{
ON posts (created);
CREATE UNIQUE INDEX IF NOT EXISTS posts_creator_slug
ON posts (creator, slug);
-`,
+ `,
+
+ "tags": `CREATE TABLE IF NOT EXISTS tags (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ creator INTEGER NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ icon VARCHAR(255),
+ cover VARCHAR(255),
+ md TEXT,
+
+ created DATETIME NOT NULL,
+ updated DATETIME
+ );
+ CREATE UNIQUE INDEX IF NOT EXISTS tags_name
+ ON tags (name);
+ CREATE INDEX IF NOT EXISTS tags_created
+ ON tags (created);
+ `,
}
diff --git a/server/pkg/common/reserve_urls.go b/server/pkg/common/reserve_urls.go
new file mode 100644
index 00000000..8dc65c13
--- /dev/null
+++ b/server/pkg/common/reserve_urls.go
@@ -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",
+}
diff --git a/server/pkg/db/db.go b/server/pkg/db/db.go
index 7874f9b5..5375dc44 100644
--- a/server/pkg/db/db.go
+++ b/server/pkg/db/db.go
@@ -2,15 +2,6 @@ package db
import (
"database/sql"
- "strings"
)
var Conn *sql.DB
-
-func IsErrUniqueConstraint(err error) bool {
- if strings.Contains(err.Error(), "UNIQUE") {
- return true
- }
-
- return false
-}
diff --git a/server/pkg/e/err_check.go b/server/pkg/e/err_check.go
new file mode 100644
index 00000000..0bd4b355
--- /dev/null
+++ b/server/pkg/e/err_check.go
@@ -0,0 +1,11 @@
+package e
+
+import "strings"
+
+func IsErrUniqueConstraint(err error) bool {
+ if strings.Contains(err.Error(), "UNIQUE") {
+ return true
+ }
+
+ return false
+}
diff --git a/server/pkg/models/tag.go b/server/pkg/models/tag.go
new file mode 100644
index 00000000..762fcb7f
--- /dev/null
+++ b/server/pkg/models/tag.go
@@ -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()
+}
diff --git a/src/components/card.tsx b/src/components/card.tsx
index db772de5..cde67f74 100644
--- a/src/components/card.tsx
+++ b/src/components/card.tsx
@@ -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 (
+
+ )
+}
export default Card
diff --git a/src/components/markdown-editor/editor.tsx b/src/components/markdown-editor/editor.tsx
index 0cc5ae9d..57efd2b4 100644
--- a/src/components/markdown-editor/editor.tsx
+++ b/src/components/markdown-editor/editor.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import 'highlight.js/styles/atom-one-dark.css';
-import { chakra,PropsOf } from '@chakra-ui/react';
+import { chakra,PropsOf} from '@chakra-ui/react';
import dynamic from 'next/dynamic';
import 'react-markdown-editor-lite/lib/index.css';
diff --git a/src/components/markdown-editor/render.tsx b/src/components/markdown-editor/render.tsx
index a864e90f..f34370bd 100644
--- a/src/components/markdown-editor/render.tsx
+++ b/src/components/markdown-editor/render.tsx
@@ -2,14 +2,17 @@ import React, { useRef, useEffect } from 'react';
import Markdown from 'markdown-to-jsx';
import hljs from 'highlight.js';
import 'highlight.js/styles/atom-one-dark.css';
-import { chakra,PropsOf } from '@chakra-ui/react';
+import { chakra,PropsOf} from '@chakra-ui/react';
+import WebsiteLink from 'components/website-link';
+
type Props = PropsOf & {
md: string
+ fontSize?: string
}
-export function MarkdownRender({ md,...rest }:Props) {
+export function MarkdownRender({ md,fontSize, ...rest }:Props) {
const rootRef = useRef();
useEffect(() => {
@@ -20,7 +23,18 @@ export function MarkdownRender({ md,...rest }:Props) {
return (
-
+
);
}
\ No newline at end of file
diff --git a/src/components/posts/tag-card.tsx b/src/components/posts/tag-card.tsx
new file mode 100644
index 00000000..371732b4
--- /dev/null
+++ b/src/components/posts/tag-card.tsx
@@ -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 & {
+ tag: Tag
+ showActions: boolean
+ onEdit?: any
+ onDelete?: any
+}
+
+
+export const TagCard= (props:Props) =>{
+ const {tag,showActions,onEdit,onDelete, ...rest} = props
+
+ return (
+
+
+
+
+ {tag.title}
+
+
+ {props.showActions &&
+
+
+ }
+
+ )
+}
+
+export default TagCard
diff --git a/src/components/website-link.tsx b/src/components/website-link.tsx
new file mode 100644
index 00000000..be429372
--- /dev/null
+++ b/src/components/website-link.tsx
@@ -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 =
+ break;
+ case "twitter":
+ title = "Twitter"
+ icon0 =
+ break;
+ default:
+ title = "Official website"
+ icon0 =
+ break;
+ }
+ return (
+
+
+ {icon0}
+ {title}
+
+
+
+
+ )
+}
+
+export default WebsiteLink
diff --git a/src/data/editor-links.tsx b/src/data/editor-links.tsx
deleted file mode 100644
index 2b6b751a..00000000
--- a/src/data/editor-links.tsx
+++ /dev/null
@@ -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: ,
- disabled: false
-},
-{
- title: '系列',
- path: '/editor/series',
- icon: ,
- disabled: false
-},
-{
- title: '课程',
- path: '/editor/course',
- icon: ,
- disabled: false
-},
-]
-
-export default editorLinks
\ No newline at end of file
diff --git a/src/data/links.tsx b/src/data/links.tsx
new file mode 100644
index 00000000..6f710a75
--- /dev/null
+++ b/src/data/links.tsx
@@ -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: ,
+ disabled: false
+},
+{
+ title: '系列',
+ path: `${ReserveUrls.Editor}/series`,
+ icon: ,
+ disabled: false
+},
+{
+ title: '课程',
+ path: `${ReserveUrls.Editor}/course`,
+ icon: ,
+ disabled: false
+}
+]
+
+export const adminLinks: Route[] = [{
+ title: '标签管理',
+ path: `${ReserveUrls.Admin}/tags`,
+ icon: ,
+ disabled: false
+}
+]
diff --git a/src/data/reserve-urls.ts b/src/data/reserve-urls.ts
new file mode 100644
index 00000000..d39da0db
--- /dev/null
+++ b/src/data/reserve-urls.ts
@@ -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",
+}
\ No newline at end of file
diff --git a/src/types/tag.ts b/src/types/tag.ts
new file mode 100644
index 00000000..aa063057
--- /dev/null
+++ b/src/types/tag.ts
@@ -0,0 +1,9 @@
+export interface Tag {
+ id?: number
+ name?: string
+ title?: string
+ md?: string
+ icon?: string
+ cover?: string
+ created?: string
+}
\ No newline at end of file
diff --git a/src/utils/role.ts b/src/utils/role.ts
new file mode 100644
index 00000000..00ded4ea
--- /dev/null
+++ b/src/utils/role.ts
@@ -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
+}
diff --git a/theme.ts b/theme.ts
index d1540df8..4dcc2ec8 100644
--- a/theme.ts
+++ b/theme.ts
@@ -22,6 +22,7 @@ const customTheme = extendTheme({
styles: {
global: (props) => ({
body: {
+ background: mode("gray.50","gray.800" )(props),
color: mode("gray.700", "whiteAlpha.900")(props),
".deleted": {
color: "#ff8383 !important",
diff --git a/theme/layer-styles.js b/theme/layer-styles.js
index 0ba7e960..a9f947c9 100644
--- a/theme/layer-styles.js
+++ b/theme/layer-styles.js
@@ -1,7 +1,7 @@
export default function layerStyles(theme) {
return {
textSecondary: {
- opacity: "0.8"
+ opacity: "0.8",
},
colorButton: {
bg: "linear-gradient(270deg,#0076f5,#0098a3)",
diff --git a/theme/react-markdown-editor.js b/theme/react-markdown-editor.js
index 95741d68..c2906ed8 100644
--- a/theme/react-markdown-editor.js
+++ b/theme/react-markdown-editor.js
@@ -2,6 +2,7 @@ import { mode } from "@chakra-ui/theme-tools"
import userCustomTheme from "./user-custom"
export default function reactMarkdownStyles(props) {
+ console.log(props)
return {
'.rc-md-editor': {
borderWidth: '0px',
@@ -20,7 +21,7 @@ export default function reactMarkdownStyles(props) {
}
},
'.drop-wrap' : {
- background: mode("white", "#1A202C")(props),
+ background: mode("white", "gray.800")(props),
borderWidth: '1px',
borderColor: mode(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark + '!important')(props),
},
diff --git a/theme/user-custom.js b/theme/user-custom.js
index ffb6a5c8..5fe64e04 100644
--- a/theme/user-custom.js
+++ b/theme/user-custom.js
@@ -4,7 +4,11 @@ const userCustomTheme = {
borderColor: {
light: theme.colors.gray['200'],
dark: theme.colors.whiteAlpha['300']
- }
+ },
+ hoverBg: {
+ light: theme.colors.gray['100'],
+ dark: theme.colors.whiteAlpha['200']
+ },
}
export default userCustomTheme
\ No newline at end of file