pull/50/head
sunface 4 years ago
parent 453d8c7718
commit 74b02b029e

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

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

@ -1,15 +0,0 @@
const navLinks = [{
title: '主页',
url: '/',
},
{
title: '标签',
url: '/tags',
},
{
title: '学习资料',
url: '/courses',
},
]
export default navLinks

@ -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) => (
<svg viewBox="0 0 146 146" {...props}>
<path
fill="currentColor"
d="M107.75 125.001s-4.5-5.375-8.25-10.125c16.375-4.625 22.625-14.875 22.625-14.875-5.125 3.375-10 5.75-14.375 7.375-6.25 2.625-12.25 4.375-18.125 5.375-12 2.25-23 1.625-32.375-.125-7.125-1.375-13.25-3.375-18.375-5.375-2.875-1.125-6-2.5-9.125-4.25-.375-.25-.75-.375-1.125-.625-.25-.125-.375-.25-.5-.375-2.25-1.25-3.5-2.125-3.5-2.125s6 10 21.875 14.75c-3.75 4.75-8.375 10.375-8.375 10.375-27.625-.875-38.125-19-38.125-19 0-40.25 18-72.875 18-72.875 18-13.5 35.125-13.125 35.125-13.125l1.25 1.5c-22.5 6.5-32.875 16.375-32.875 16.375s2.75-1.5 7.375-3.625c13.375-5.875 24-7.5 28.375-7.875.75-.125 1.375-.25 2.125-.25 7.625-1 16.25-1.25 25.25-.25 11.875 1.375 24.625 4.875 37.625 12 0 0-9.875-9.375-31.125-15.875l1.75-2S110 19.626 128 33.126c0 0 18 32.625 18 72.875 0 0-10.625 18.125-38.25 19zM49.625 66.626c-7.125 0-12.75 6.25-12.75 13.875s5.75 13.875 12.75 13.875c7.125 0 12.75-6.25 12.75-13.875.125-7.625-5.625-13.875-12.75-13.875zm45.625 0c-7.125 0-12.75 6.25-12.75 13.875s5.75 13.875 12.75 13.875c7.125 0 12.75-6.25 12.75-13.875s-5.625-13.875-12.75-13.875z"
fillRule="nonzero"
/>
</svg>
)
const GithubIcon = (props) => (
<svg viewBox="0 0 20 20" {...props}>
<path
fill="currentColor"
d="M10 0a10 10 0 0 0-3.16 19.49c.5.1.68-.22.68-.48l-.01-1.7c-2.78.6-3.37-1.34-3.37-1.34-.46-1.16-1.11-1.47-1.11-1.47-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.52 2.34 1.08 2.91.83.1-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.1.39-1.99 1.03-2.69a3.6 3.6 0 0 1 .1-2.64s.84-.27 2.75 1.02a9.58 9.58 0 0 1 5 0c1.91-1.3 2.75-1.02 2.75-1.02.55 1.37.2 2.4.1 2.64.64.7 1.03 1.6 1.03 2.69 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85l-.01 2.75c0 .26.18.58.69.48A10 10 0 0 0 10 0"
/>
</svg>
)
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() {
</NextLink>
<HStack display={{ base: "none", md: "flex" }} ml={{ base: 1, md: 4, lg: 12 }} fontSize="1rem" minWidth="250px">
{navLinks.map(link => <Link px="4" py="0.7rem" rounded="md" href={link.url} key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={asPath === link.url ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} _hover={null} _focus={null}>{link.title}</Link>)}
{navLinks.map(link => <Box px="4" py="0.7rem" rounded="md" key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={asPath === link.url ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} ><Link href={link.url}>{link.title}</Link></Box>)}
</HStack>
</Flex>
@ -106,7 +99,6 @@ function HeaderContent() {
<AlgoliaSearch />
<HStack spacing="5" display={{ base: "none", md: "flex" }}>
<Link
isExternal
aria-label="Go to Chakra UI GitHub page"
href={siteConfig.repo.url}
>
@ -155,7 +147,8 @@ function HeaderContent() {
<span>Sunface</span>
</MenuItem>
<MenuDivider />
{<MenuItem as="a" icon={<FaEdit fontSize="16" />} href="/editor"></MenuItem>}
{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>}
<MenuItem icon={<FaBookmark fontSize="16" />}></MenuItem>
<MenuDivider />
<MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem>
@ -186,7 +179,6 @@ function HeaderContent() {
}
function Header(props) {
const bg = useColorModeValue("white", "gray.800")
const ref = React.useRef<HTMLHeadingElement>()
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"

@ -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,
// }
]
}

@ -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 (
<PageContainer
nav={<Nav
changeEditMode={(v) => setEditMode(v)}
publish={() => publish()}
/>}
>
<Flex style={{ height: 'calc(100vh - 145px)' }}>
<Card width="65%">
{editMode === EditMode.Edit ?
<MarkdownEditor
options={{
overrides: {
Button: {
component: Button,
},
},
}}
onChange={(md) => onMdChange(md)}
md={tag.md}
/> :
<Box height="100%" p="6">
<MarkdownRender md={tag.md} />
</Box>
}
</Card>
<Card ml="6" width="35%">
<Heading size="xs">
Title
</Heading>
<Input value={tag.title} onChange={(e) => { tag.title = e.target.value; onChange()}} mt="4" variant="flushed" size="sm" placeholder="Tag title..." focusBorderColor="teal.400" />
<Heading size="xs" mt="8">
Name
</Heading>
<Input value={tag.name} onChange={(e) => { tag.name = e.target.value; onChange()}} mt="4" variant="flushed" size="sm" placeholder="Tag name..." focusBorderColor="teal.400" />
<Heading size="xs" mt="8">
</Heading>
<Input value={tag.cover} onChange={(e) => { tag.cover = e.target.value; onChange()}} mt="4" variant="flushed" size="sm" placeholder="图片链接你可以用github当图片存储服务" focusBorderColor="teal.400" />
<Heading size="xs" mt="8">
</Heading>
<Input value={tag.icon} onChange={(e) => { tag.icon = e.target.value; onChange()}} mt="4" variant="flushed" size="sm" placeholder="图片链接" focusBorderColor="teal.400" />
</Card>
</Flex>
</PageContainer>
);
}
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 (
<>
<Flex w="100%" h="100%" align="center" justify="space-between" px={{ base: "4", md: "6" }}>
<Flex align="center">
<NextLink href="/" passHref>
<chakra.a display={{ base: "none", md: "block" }} style={{ marginTop: '-5px' }} aria-label="Chakra UI, Back to homepage">
<Logo width="130" />
</chakra.a>
</NextLink>
<NextLink href="/" passHref>
<chakra.a display={{ base: "block", md: "none" }} aria-label="Chakra UI, Back to homepage">
<LogoIcon />
</chakra.a>
</NextLink>
</Flex>
<HStack {...group}>
{editOptions.map((value) => {
const radio = getRadioProps({ value })
return (
<RadioCard key={value} {...radio} bg="teal" color="white">
{value}
</RadioCard>
)
})}
</HStack>
<Box
color={useColorModeValue("gray.500", "gray.400")}
>
<IconButton
size="md"
fontSize="lg"
aria-label={`Switch to ${text} mode`}
variant="ghost"
color="current"
ml={{ base: "0", md: "1" }}
onClick={toggleMode}
_focus={null}
icon={<SwitchIcon />}
/>
<Button layerStyle="colorButton" ml="2" onClick={props.publish}></Button>
</Box>
</Flex>
</>
)
}
function Nav(props) {
const bg = useColorModeValue("white", "gray.800")
const ref = React.useRef<HTMLHeadingElement>()
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 (
<chakra.header
ref={ref}
shadow={y > 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"
>
<chakra.div height="4.5rem" mx="auto" maxW="1200px">
<HeaderContent {...props} />
</chakra.div>
</chakra.header>
)
}

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

@ -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()}
/>}
>
<Box style={{ height: 'calc(100vh - 145px)' }}>
<Card style={{ height: 'calc(100vh - 145px)' }}>
{editMode === EditMode.Edit ?
<MarkdownEditor
options={{
overrides: {
Button: {
component: Button,
},
},
}}
onChange={(md) => onMdChange(md)}
md={ar.md}
/> :
@ -94,7 +88,7 @@ function PostEditPage() {
<MarkdownRender md={ar.md} />
</Box>
}
</Box>
</Card>
</PageContainer>
);
}

@ -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 = () => {
</MenuButton>
<MenuList color={useColorModeValue("gray.500","gray.400")}>
<MenuItem icon={<FaExternalLinkAlt fontSize="14" />} onClick={onOpen}></MenuItem>
<MenuItem icon={<FaRegEdit fontSize="16" />} as="a" href="/editor/post/new"></MenuItem>
<Link href={`${ReserveUrls.Editor}/post/new`}><MenuItem icon={<FaRegEdit fontSize="16" />} ></MenuItem></Link>
</MenuList>
</Menu>
:

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

@ -3,3 +3,5 @@ package api
import "github.com/imdotdev/im.dev/server/pkg/log"
var logger = log.RootLogger.New("logger", "api")
/* 鉴权、数据合法性验证都在api模块进行处理 */

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

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

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

@ -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);
`,
}

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

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

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

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

@ -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<typeof chakra.div> & {
md: string
fontSize?: string
}
export function MarkdownRender({ md,...rest }:Props) {
export function MarkdownRender({ md,fontSize, ...rest }:Props) {
const rootRef = useRef<HTMLDivElement>();
useEffect(() => {
@ -20,7 +23,18 @@ export function MarkdownRender({ md,...rest }:Props) {
return (
<div ref={rootRef} style={{height:'100%'}}>
<Markdown children={md} {...rest} style={{height:'100%',fontSize: '14px'}}></Markdown>
<Markdown
children={md}
{...rest}
style={{height:'100%',fontSize: fontSize??'14px'}}
options={{
overrides: {
WebsiteLink: {
component: WebsiteLink,
},
},
}}
></Markdown>
</div>
);
}

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

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

@ -1,7 +1,7 @@
export default function layerStyles(theme) {
return {
textSecondary: {
opacity: "0.8"
opacity: "0.8",
},
colorButton: {
bg: "linear-gradient(270deg,#0076f5,#0098a3)",

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

@ -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
Loading…
Cancel
Save