From 672680e117183f6eff15f98bffb8fae84d77d861 Mon Sep 17 00:00:00 2001 From: sunface Date: Fri, 5 Feb 2021 17:54:42 +0800 Subject: [PATCH] add editor article page --- config.yaml | 7 +- go.mod | 1 + go.sum | 2 + layouts/{ => nav}/nav-links.ts | 0 layouts/{ => nav}/nav.tsx | 70 ++----- layouts/page-container.tsx | 2 +- layouts/sidebar/sidebar-link.tsx | 61 ++++++ layouts/sidebar/sidebar.tsx | 56 ++++++ next-redirect.js | 6 +- package.json | 8 +- pages/_app.tsx | 4 + pages/courses.tsx | 2 +- pages/editor/articles.tsx | 190 ++++++++++++++++++ pages/index.tsx | 8 +- pages/login.tsx | 66 ++++++ pages/posts/[id].tsx | 2 +- pages/tags.tsx | 2 +- public/empty-posts.png | Bin 0 -> 28596 bytes server/internal/api/api.go | 5 + server/internal/api/editor.go | 44 ++++ server/internal/posts/article.go | 82 ++++++++ server/internal/posts/posts.go | 5 + server/internal/posts/series.go | 1 + server/internal/server.go | 16 +- server/internal/session/session.go | 2 +- server/internal/storage/sql_tables.go | 18 ++ server/internal/ui_config.go | 27 +++ server/pkg/common/resp.go | 3 +- server/pkg/config/config.go | 4 + server/pkg/errcode/err_code.go | 4 +- server/pkg/models/post.go | 22 ++ server/pkg/models/user.go | 21 +- src/components/articles/text-article-card.tsx | 37 ++++ src/components/card.tsx | 14 ++ src/data/editor-links.tsx | 24 +++ src/data/messages.ts | 1 + src/hooks/use-session.ts | 11 +- src/types/posts.ts | 11 + src/types/route.ts | 6 + src/types/session.ts | 6 +- src/utils/axios/request.ts | 17 +- src/utils/config.ts | 13 ++ src/utils/emitter.ts | 98 +++++++++ src/utils/events.ts | 5 + src/utils/localStorage.ts | 4 +- src/utils/session.ts | 9 + tsconfig.json | 2 +- yarn.lock | 54 ++++- 48 files changed, 968 insertions(+), 85 deletions(-) rename layouts/{ => nav}/nav-links.ts (100%) rename layouts/{ => nav}/nav.tsx (65%) create mode 100644 layouts/sidebar/sidebar-link.tsx create mode 100644 layouts/sidebar/sidebar.tsx create mode 100644 pages/editor/articles.tsx create mode 100644 pages/login.tsx create mode 100644 public/empty-posts.png create mode 100644 server/internal/api/api.go create mode 100644 server/internal/api/editor.go create mode 100644 server/internal/posts/article.go create mode 100644 server/internal/posts/posts.go create mode 100644 server/internal/posts/series.go create mode 100644 server/internal/ui_config.go create mode 100644 server/pkg/models/post.go create mode 100644 src/components/articles/text-article-card.tsx create mode 100644 src/components/card.tsx create mode 100644 src/data/editor-links.tsx create mode 100644 src/data/messages.ts create mode 100644 src/types/posts.ts create mode 100644 src/types/route.ts create mode 100644 src/utils/config.ts create mode 100644 src/utils/emitter.ts create mode 100644 src/utils/events.ts create mode 100644 src/utils/session.ts diff --git a/config.yaml b/config.yaml index 739d058b..8bf8ac59 100644 --- a/config.yaml +++ b/config.yaml @@ -4,10 +4,11 @@ common: log_level: "info" is_prod: false - #################################### Server ############################## +#################################### Server ############################## server: addr: ":6001" base_url: "/api" + #################################### User/Session ############################## user: # github username @@ -22,3 +23,7 @@ paths: # sqlite3 db files data: "" logs: "" + +#################################### Posts ############################## +posts: + brief_max_len: 100 \ No newline at end of file diff --git a/go.mod b/go.mod index 836a169a..1d3501f5 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/imdotdev/im.dev go 1.14 require ( + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef github.com/gin-gonic/gin v1.6.3 github.com/go-stack/stack v1.8.0 github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac diff --git a/go.sum b/go.sum index cb279220..242b1117 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= diff --git a/layouts/nav-links.ts b/layouts/nav/nav-links.ts similarity index 100% rename from layouts/nav-links.ts rename to layouts/nav/nav-links.ts diff --git a/layouts/nav.tsx b/layouts/nav/nav.tsx similarity index 65% rename from layouts/nav.tsx rename to layouts/nav/nav.tsx index ac9b6363..aaff2e3d 100644 --- a/layouts/nav.tsx +++ b/layouts/nav/nav.tsx @@ -29,9 +29,9 @@ 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 } from "react-icons/fa" +import { FaMoon, FaSun, FaUserAlt, FaRegSun, FaSignOutAlt, FaRegBookmark, FaChartBar, FaHome, FaArrowRight, FaGithub, FaFileAlt, FaBookmark, FaEdit } from "react-icons/fa" import Logo, { LogoIcon } from "src/components/logo" -import { MobileNavButton, MobileNavContent } from "./mobile-nav" +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" @@ -39,6 +39,9 @@ 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) => ( @@ -62,12 +65,12 @@ const GithubIcon = (props) => ( function HeaderContent() { - const { pathname } = useRouter() + const router = useRouter() + const {pathname} = router const mobileNav = useDisclosure() - const [session, storeSession]: [Session, any] = useSession() - const { isOpen: isLoginOpen, onOpen: onLoginOpen, onClose: onLoginClose } = useDisclosure() - + const session:Session = useSession() + const { toggleColorMode: toggleMode } = useColorMode() const text = useColorModeValue("dark", "light") const SwitchIcon = useColorModeValue(FaMoon, FaSun) @@ -77,20 +80,11 @@ function HeaderContent() { mobileNavBtnRef.current?.focus() }, [mobileNav.isOpen]) - const login = async () => { - const res = await requestApi.post("/login") - saveToken(res.data.token) - storeSession(res.data) - onLoginClose() - } - - const logout = async () => { - await requestApi.post("/logout") - removeToken() - storeSession(null) + const login = () => { + storage.set("current-page", pathname) + router.push('/login') } - return ( <> @@ -167,11 +161,11 @@ function HeaderContent() { Sunface - }>Dashboard - }>Bookmarks + {} href="/editor">编辑中心} + }>书签收藏 - }>Account Settings - logout()} icon={}>Log out + }>偏好设置 + logout()} icon={}>账号登出 : - 如果继续,则表示你同意im.dev的服务条款和隐私政策 - {/* */} - - - ) } diff --git a/layouts/page-container.tsx b/layouts/page-container.tsx index a480fd81..71697204 100644 --- a/layouts/page-container.tsx +++ b/layouts/page-container.tsx @@ -2,7 +2,7 @@ import { Badge, Box, chakra } from "@chakra-ui/react" import { SkipNavContent, SkipNavLink } from "@chakra-ui/skip-nav" import Container from "components/container" import Footer from "./footer" -import Nav from "./nav" +import Nav from "./nav/nav" import SEO from "components/seo" import { useRouter } from "next/router" import * as React from "react" diff --git a/layouts/sidebar/sidebar-link.tsx b/layouts/sidebar/sidebar-link.tsx new file mode 100644 index 00000000..f89452fe --- /dev/null +++ b/layouts/sidebar/sidebar-link.tsx @@ -0,0 +1,61 @@ +import { chakra, PropsOf, useColorModeValue } from "@chakra-ui/react" +import NextLink from "next/link" +import { useRouter } from "next/router" +import React from "react" + +const StyledLink = React.forwardRef(function StyledLink( + props: PropsOf & { isActive?: boolean }, + ref: React.Ref, +) { + const { isActive, icon,children, ...rest } = props + return ( + {icon} {children} + ) +}) + +type SidebarLinkProps = PropsOf & { + href?: string + icon?: React.ReactElement +} + +const SidebarLink = (props: SidebarLinkProps) => { + const { href, icon, children, ...rest } = props + + const { pathname } = useRouter() + const isActive = pathname === href + + return ( + + + {children} + + + ) +} + +export default SidebarLink diff --git a/layouts/sidebar/sidebar.tsx b/layouts/sidebar/sidebar.tsx new file mode 100644 index 00000000..910e00a3 --- /dev/null +++ b/layouts/sidebar/sidebar.tsx @@ -0,0 +1,56 @@ +import { Box, Stack } from "@chakra-ui/react" +import Card from "components/card" +import { useRouter } from "next/router" +import * as React from "react" +import { Route } from "src/types/route" +import SidebarLink from "./sidebar-link" + + +export function SidebarContent(props) { + const { routes, pathname, contentRef } = props + return ( + <> + + {routes.map((route:Route) => { + if (route.disabled) {return null} + return + {route.title} + + })} + + + ) +} + +const Sidebar = ({ routes, ...props }) => { + const { pathname } = useRouter() + const ref = React.useRef(null) + + return ( + + + + + + ) +} + +export default Sidebar + diff --git a/next-redirect.js b/next-redirect.js index 34da1af5..2179fb42 100644 --- a/next-redirect.js +++ b/next-redirect.js @@ -5,10 +5,10 @@ async function redirect() { destination: "https://discord.gg/dQHfcWF", permanent: true, }, - // GENERAL + // GENERAL { - source: "/getting-started", - destination: "/docs/getting-started", + source: "/editor", + destination: "/editor/articles", permanent: true, } ] diff --git a/package.json b/package.json index baddeda6..a71dbfb3 100644 --- a/package.json +++ b/package.json @@ -21,20 +21,26 @@ "@emotion/react": "^11.1.4", "@emotion/styled": "^11.0.0", "@octokit/rest": "^18.0.12", + "@types/validator": "^13.1.3", "axios": "^0.19.2", "date-fns": "^2.16.1", "docsearch.js": "^2.6.3", + "eventemitter3": "^4.0.4", + "formik": "^2.2.6", "framer-motion": "^3.1.1", "json-bigint": "^1.0.0", + "lodash": "^4.17.15", "moment": "^2.27.0", "next": "^10.0.4", "next-seo": "^4.17.0", "react": "^17.0.1", "react-dom": "^17.0.1", - "react-icons": "^4.1.0" + "react-icons": "^4.1.0", + "validator": "^13.5.2" }, "devDependencies": { "@next/bundle-analyzer": "^10.0.4", + "@types/lodash": "^4.14.123", "@types/moment": "^2.13.0", "@types/node": "^14.14.19", "@types/react": "^17.0.0", diff --git a/pages/_app.tsx b/pages/_app.tsx index dbe902c0..2192aaea 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -7,6 +7,8 @@ import { ChakraProvider } from "@chakra-ui/react" import theme from "theme" import FontFace from "src/components/font-face" import { getSeo } from "utils/seo" +import GAScript from "analytics/ga-script" +import {initUIConfig} from 'src/utils/config' Router.events.on("routeChangeComplete", (url) => { trackPageview(url) @@ -15,6 +17,7 @@ Router.events.on("routeChangeComplete", (url) => { const App = ({ Component, pageProps }) => { const seo = getSeo({ omitOpenGraphImage: false }) + initUIConfig() return ( <> @@ -29,6 +32,7 @@ const App = ({ Component, pageProps }) => { + ) diff --git a/pages/courses.tsx b/pages/courses.tsx index 97ade6e7..2b21f684 100644 --- a/pages/courses.tsx +++ b/pages/courses.tsx @@ -2,7 +2,7 @@ import { chakra } from "@chakra-ui/react" import Container from "components/container" import SEO from "components/seo" import siteConfig from "configs/site-config" -import Nav from "layouts/nav" +import Nav from "layouts/nav/nav" import PageContainer from "layouts/page-container" import React from "react" diff --git a/pages/editor/articles.tsx b/pages/editor/articles.tsx new file mode 100644 index 00000000..39fbe323 --- /dev/null +++ b/pages/editor/articles.tsx @@ -0,0 +1,190 @@ +import { createStandaloneToast, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider } 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 editorLinks from "src/data/editor-links" +import { requestApi } from "utils/axios/request" +import { useDisclosure } from "@chakra-ui/react" +import { Field, Form, Formik } from "formik" +import { config } from "utils/config" +import TextArticleCard from "components/articles/text-article-card" +import { Article } from "src/types/posts" +var validator = require('validator'); +const toast = createStandaloneToast() + +const newPost:Article = { title: '', url: '', cover: ''} +const ArticlesPage = () => { + const [posts, setPosts] = useState([]) + const [currentPost, setCurrentPost] = useState(newPost) + useEffect(() => { + getPosts() + }, []) + + const getPosts = () => { + requestApi.get(`/editor/articles`).then((res) => setPosts(res.data)).catch(_ => setPosts([])) + } + + const { isOpen, onOpen, onClose } = useDisclosure() + + function validateTitle(value) { + console.log(value) + let error + if (!value?.trim()) { + error = "标题不能为空" + } + return error + } + + function validateUrl(value) { + let error + if (!validator.isURL(value)) { + error = "URL格式不合法" + } + return error + } + + function validateBrief(value) { + let error + if (value && value.length > config.posts.briefMaxLen) { + error = `文本长度不能超过${config.posts.briefMaxLen}` + } + return error + } + + const submitArticle = async (values, _) => { + await requestApi.post(`/editor/article`, values) + onClose() + toast({ + description: "提交成功", + status: "success", + duration: 2000, + isClosable: true, + }) + setCurrentPost(newPost) + } + + const editArticle = (ar: Article) => { + setCurrentPost(ar) + onOpen() + } + + const onDeleteArticle = () => { + getPosts() + toast({ + description: "删除成功", + status: "success", + duration: 2000, + isClosable: true, + }) + } + + return ( + <> +