From 453d8c7718b649d9775e3908c9a389d4be1120ed Mon Sep 17 00:00:00 2001 From: sunface Date: Mon, 8 Feb 2021 14:31:04 +0800 Subject: [PATCH] update --- config.yaml | 5 +- go.mod | 1 + go.sum | 4 + layouts/editor-nav.tsx | 43 ++++- pages/editor/post/[id].tsx | 37 ++++- pages/editor/posts.tsx | 10 +- server/internal/api/editor.go | 5 +- server/internal/posts/article.go | 141 ----------------- server/internal/posts/post.go | 200 ++++++++++++++++++++++++ server/internal/storage/sql_tables.go | 4 +- server/internal/ui_config.go | 2 + server/pkg/config/config.go | 1 + server/pkg/models/post.go | 1 + server/pkg/models/role.go | 20 +-- server/pkg/utils/slug.go | 23 +++ src/components/posts/text-post-card.tsx | 2 +- src/types/role.ts | 9 +- src/utils/config.ts | 3 +- theme/layer-styles.js | 2 +- 19 files changed, 326 insertions(+), 187 deletions(-) delete mode 100644 server/internal/posts/article.go create mode 100644 server/internal/posts/post.go create mode 100644 server/pkg/utils/slug.go diff --git a/config.yaml b/config.yaml index 62bf1219..d83207aa 100644 --- a/config.yaml +++ b/config.yaml @@ -25,7 +25,8 @@ paths: logs: "" #################################### Posts ############################## -posts: - brief_max_len: 100 +posts: + title_max_len: 128 + brief_max_len: 128 # whether allow writing posts writing_enabled: true \ No newline at end of file diff --git a/go.mod b/go.mod index e0574cf1..2df8ce07 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/gin-gonic/gin v1.6.3 github.com/go-stack/stack v1.8.0 github.com/golang/snappy v0.0.2 + github.com/gosimple/slug v1.9.0 github.com/inconshreveable/log15 v0.0.0-20201112154412-8562bdadbbac github.com/karrick/godirwalk v1.16.1 // indirect github.com/keegancsmith/rpc v1.3.0 // indirect diff --git a/go.sum b/go.sum index 7a956b62..64467362 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= +github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -180,6 +182,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= +github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= github.com/ramya-rao-a/go-outline v0.0.0-20200117021646-2a048b4510eb h1:ilZSL4VaIq4Hsi+lH928xQKnSWymFug6r2gJomUBpW8= github.com/ramya-rao-a/go-outline v0.0.0-20200117021646-2a048b4510eb/go.mod h1:1WL5IqM+CnRCAbXetRnL1YVoS9KtU2zMhOi/5oAVPo4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= diff --git a/layouts/editor-nav.tsx b/layouts/editor-nav.tsx index 5a5c8ae7..6732487c 100644 --- a/layouts/editor-nav.tsx +++ b/layouts/editor-nav.tsx @@ -8,7 +8,14 @@ import { Box, useRadioGroup, HStack, - Input + Input, + Drawer, + useDisclosure, + DrawerOverlay, + DrawerContent, + Text, + Divider, + Heading } from "@chakra-ui/react" import { useViewportScroll } from "framer-motion" import NextLink from "next/link" @@ -17,16 +24,18 @@ import { FaMoon, FaSun } from "react-icons/fa" import Logo, { LogoIcon } from "src/components/logo" import RadioCard from "components/radio-card" import { EditMode } from "src/types/editor" +import Card from "components/card" -function HeaderContent(props:any) { +function HeaderContent(props: any) { const { toggleColorMode: toggleMode } = useColorMode() const text = useColorModeValue("dark", "light") const SwitchIcon = useColorModeValue(FaMoon, FaSun) + const { isOpen, onOpen, onClose } = useDisclosure() - const editOptions = [EditMode.Edit,EditMode.Preview] + const editOptions = [EditMode.Edit, EditMode.Preview] const { getRootProps, getRadioProps } = useRadioGroup({ name: "framework", defaultValue: EditMode.Edit, @@ -53,7 +62,7 @@ function HeaderContent(props:any) { - + {editOptions.map((value) => { @@ -79,9 +88,33 @@ function HeaderContent(props:any) { _focus={null} icon={} /> - + + + + + + 文章设置 + + + + + + + 封面图片 + + {props.ar.cover = e.target.value; props.onChange()}} mt="4" variant="flushed" size="sm" placeholder="图片链接,你可以用github当图片存储服务" focusBorderColor="teal.400"/> + + + + ) } diff --git a/pages/editor/post/[id].tsx b/pages/editor/post/[id].tsx index 4e07c899..bf95ded5 100644 --- a/pages/editor/post/[id].tsx +++ b/pages/editor/post/[id].tsx @@ -1,4 +1,4 @@ -import { Box, Button,createStandaloneToast} from '@chakra-ui/react'; +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'; @@ -8,7 +8,8 @@ import { MarkdownRender } from 'components/markdown-editor/render'; import { Post } from 'src/types/posts'; import { requestApi } from 'utils/axios/request'; import { useRouter } from 'next/router'; -const toast = createStandaloneToast() +import { config } from 'utils/config'; +import { cloneDeep } from 'lodash'; const content = ` # test原创 @@ -23,36 +24,56 @@ function PostEditPage() { title: '' }) + const toast = useToast() useEffect(() => { if (id && id !== 'new') { requestApi.get(`/editor/post/${id}`).then(res => setAr(res.data)) } },[id]) - const onChange = newMd => { + + const onMdChange = newMd => { setAr({ ...ar, md: newMd }) } + const onChange = () => { + setAr(cloneDeep(ar)) + } + + const onChangeTitle = title => { + if (title.length > config.posts.titleMaxLen) { + toast({ + description: `Title长度不能超过${config.posts.titleMaxLen}`, + status: "error", + duration: 2000, + isClosable: true, + }) + return + } + + setAr({...ar, title: title}) + } + const publish = async () => { - await requestApi.post(`/editor/post`, ar) + const res = await requestApi.post(`/editor/post`, ar) toast({ description: "发布成功", status: "success", duration: 2000, isClosable: true, }) - router.push('/editor/posts') + router.push(`/${res.data.username}/${res.data.slug}`) } - console.log(ar) return ( setEditMode(v)} - changeTitle={(e) => {setAr({...ar, title: e.target.value})}} + changeTitle={(e) => onChangeTitle(e.target.value)} publish={() => publish()} />} > @@ -66,7 +87,7 @@ function PostEditPage() { }, }, }} - onChange={(md) => onChange(md)} + onChange={(md) => onMdChange(md)} md={ar.md} /> : diff --git a/pages/editor/posts.tsx b/pages/editor/posts.tsx index 1c1fb0f9..9daccaad 100644 --- a/pages/editor/posts.tsx +++ b/pages/editor/posts.tsx @@ -1,4 +1,4 @@ -import { Menu,MenuButton,MenuList,MenuItem,createStandaloneToast, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue } from "@chakra-ui/react" +import { Menu,MenuButton,MenuList,MenuItem, Text, Box, Heading, Image, HStack, Center, Button, Flex, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, VStack, Textarea, Divider, useColorModeValue, useToast } from "@chakra-ui/react" import Card from "components/card" import Nav from "layouts/nav/nav" import PageContainer from "layouts/page-container" @@ -14,7 +14,6 @@ import { Post } from "src/types/posts" import { FaExternalLinkAlt, FaRegEdit } from "react-icons/fa" import { useRouter } from "next/router" var validator = require('validator'); -const toast = createStandaloneToast() const newPost: Post = { title: '', url: '', cover: '' } const PostsPage = () => { @@ -22,6 +21,7 @@ const PostsPage = () => { const [posts, setPosts] = useState([]) const { isOpen, onOpen, onClose } = useDisclosure() const router = useRouter() + const toast = useToast() const getPosts = () => { requestApi.get(`/editor/posts`).then((res) => setPosts(res.data)).catch(_ => setPosts([])) } @@ -32,11 +32,15 @@ const PostsPage = () => { function validateTitle(value) { - console.log(value) let error if (!value?.trim()) { error = "标题不能为空" } + + if (value?.length > config.posts.titleMaxLen) { + error = "标题长度不能超过128" + } + return error } diff --git a/server/internal/api/editor.go b/server/internal/api/editor.go index 136a0879..a1dc85b7 100644 --- a/server/internal/api/editor.go +++ b/server/internal/api/editor.go @@ -24,14 +24,13 @@ func GetEditorPosts(c *gin.Context) { } func SubmitPost(c *gin.Context) { - err := posts.SubmitPost(c) + res, err := posts.SubmitPost(c) if err != nil { - logger.Warn("submit post error", "error", err) c.JSON(err.Status, common.RespError(err.Message)) return } - c.JSON(http.StatusOK, common.RespSuccess(nil)) + c.JSON(http.StatusOK, common.RespSuccess(res)) } func DeletePost(c *gin.Context) { diff --git a/server/internal/posts/article.go b/server/internal/posts/article.go deleted file mode 100644 index 1760a612..00000000 --- a/server/internal/posts/article.go +++ /dev/null @@ -1,141 +0,0 @@ -package posts - -import ( - "database/sql" - "net/http" - "sort" - "strings" - "time" - "unicode/utf8" - - "github.com/asaskevich/govalidator" - "github.com/gin-gonic/gin" - "github.com/imdotdev/im.dev/server/internal/session" - "github.com/imdotdev/im.dev/server/pkg/config" - "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 UserPosts(uid int64) (models.Posts, *e.Error) { - ars := make(models.Posts, 0) - rows, err := db.Conn.Query("select id,title,url,cover,brief,created,updated from posts where creator=?", uid) - if err != nil { - if err == sql.ErrNoRows { - return ars, e.New(http.StatusNotFound, e.NotFound) - } - logger.Warn("get user posts error", "error", err) - return ars, e.New(http.StatusInternalServerError, e.Internal) - } - - creator := &models.UserSimple{ID: uid} - creator.Query() - for rows.Next() { - ar := &models.Post{} - err := rows.Scan(&ar.ID, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Created, &ar.Updated) - if err != nil { - logger.Warn("scan post error", "error", err) - continue - } - - ar.Creator = creator - ars = append(ars, ar) - } - - sort.Sort(ars) - return ars, nil -} - -func SubmitPost(c *gin.Context) *e.Error { - user := session.CurrentUser(c) - if !user.Role.IsEditor() { - return e.New(http.StatusForbidden, e.NoEditorPermission) - } - - ar := &models.Post{} - err := c.Bind(&ar) - if err != nil { - return e.New(http.StatusBadRequest, e.ParamInvalid) - } - - if strings.TrimSpace(ar.Title) == "" || utf8.RuneCountInString(ar.Brief) > config.Data.Posts.BriefMaxLen { - return e.New(http.StatusBadRequest, e.ParamInvalid) - } - - if strings.TrimSpace(ar.URL) != "" && !govalidator.IsURL(ar.URL) { - return e.New(http.StatusBadRequest, e.ParamInvalid) - } - - if strings.TrimSpace(ar.Cover) != "" && !govalidator.IsURL(ar.Cover) { - return e.New(http.StatusBadRequest, e.ParamInvalid) - } - - now := time.Now() - - md := utils.Compress(ar.Md) - if ar.ID == 0 { - //create - _, err = db.Conn.Exec("INSERT INTO posts (creator, title, md, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?,?)", - user.ID, ar.Title, md, ar.URL, ar.Cover, ar.Brief, now, now) - if err != nil { - logger.Warn("submit post error", "error", err) - return e.New(http.StatusInternalServerError, e.Internal) - } - } else { - _, err = db.Conn.Exec("UPDATE posts SET title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?", - ar.Title, md, ar.URL, ar.Cover, ar.Brief, now, ar.ID) - if err != nil { - logger.Warn("upate post error", "error", err) - return e.New(http.StatusInternalServerError, e.Internal) - } - } - - return nil -} - -func DeletePost(id int64) *e.Error { - _, err := db.Conn.Exec("DELETE FROM posts WHERE id=?", id) - if err != nil { - logger.Warn("delete post error", "error", err) - return e.New(http.StatusInternalServerError, e.Internal) - } - - return nil -} - -func GetPost(id int64) (*models.Post, *e.Error) { - ar := &models.Post{} - var rawmd []byte - err := db.Conn.QueryRow("select id,title,md,url,cover,brief,creator,created,updated from posts where id=?", id).Scan( - &ar.ID, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated, - ) - 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) - ar.Md = string(md) - ar.Creator = &models.UserSimple{ID: ar.CreatorID} - err = ar.Creator.Query() - - return ar, nil -} - -func GetPostCreator(id int64) (int64, *e.Error) { - var uid int64 - err := db.Conn.QueryRow("SELECT creator FROM posts WHERE id=?", id).Scan(&uid) - if err != nil { - if err == sql.ErrNoRows { - return 0, e.New(http.StatusNotFound, e.NotFound) - } - logger.Warn("get post creator error", "error", err) - return 0, e.New(http.StatusInternalServerError, e.Internal) - } - - return uid, nil -} diff --git a/server/internal/posts/post.go b/server/internal/posts/post.go new file mode 100644 index 00000000..339f9e4a --- /dev/null +++ b/server/internal/posts/post.go @@ -0,0 +1,200 @@ +package posts + +import ( + "database/sql" + "fmt" + "net/http" + "sort" + "strings" + "time" + "unicode/utf8" + + "github.com/asaskevich/govalidator" + "github.com/gin-gonic/gin" + "github.com/imdotdev/im.dev/server/internal/session" + "github.com/imdotdev/im.dev/server/pkg/config" + "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 UserPosts(uid int64) (models.Posts, *e.Error) { + ars := make(models.Posts, 0) + rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,created,updated from posts where creator=?", uid) + if err != nil { + if err == sql.ErrNoRows { + return ars, e.New(http.StatusNotFound, e.NotFound) + } + logger.Warn("get user posts error", "error", err) + return ars, e.New(http.StatusInternalServerError, e.Internal) + } + + creator := &models.UserSimple{ID: uid} + creator.Query() + for rows.Next() { + ar := &models.Post{} + err := rows.Scan(&ar.ID, &ar.Slug, &ar.Title, &ar.URL, &ar.Cover, &ar.Brief, &ar.Created, &ar.Updated) + if err != nil { + logger.Warn("scan post error", "error", err) + continue + } + + ar.Creator = creator + ars = append(ars, ar) + } + + sort.Sort(ars) + return ars, nil +} + +func SubmitPost(c *gin.Context) (map[string]string, *e.Error) { + user := session.CurrentUser(c) + + post := &models.Post{} + err := c.Bind(&post) + if err != nil { + return nil, e.New(http.StatusBadRequest, e.ParamInvalid) + } + + if strings.TrimSpace(post.Title) == "" || utf8.RuneCountInString(post.Brief) > config.Data.Posts.BriefMaxLen { + return nil, e.New(http.StatusBadRequest, "标题格式不合法") + } + + if strings.TrimSpace(post.URL) != "" && !govalidator.IsURL(post.URL) { + return nil, e.New(http.StatusBadRequest, "URL格式不正确") + } + + if strings.TrimSpace(post.Cover) != "" && !govalidator.IsURL(post.Cover) { + return nil, e.New(http.StatusBadRequest, "图片链接格式不正确") + } + + isExternal := true + if strings.TrimSpace(post.URL) == "" { + isExternal = false + } + + if isExternal { + // internal post, need creator role + if !user.Role.IsCreator() { + return nil, e.New(http.StatusForbidden, e.NoEditorPermission) + } + } else { + // external post, need editor role + if !user.Role.IsEditor() { + return nil, e.New(http.StatusForbidden, e.NoEditorPermission) + } + + if len(post.Md) <= config.Data.Posts.BriefMaxLen { + post.Brief = post.Md + } else { + post.Brief = string([]rune(post.Md)[:config.Data.Posts.BriefMaxLen]) + } + } + + now := time.Now() + + md := utils.Compress(post.Md) + + setSlug(user.ID, post) + if post.ID == 0 { + //create + _, err = db.Conn.Exec("INSERT INTO posts (creator,slug, title, md, url, cover, brief, created, updated) VALUES(?,?,?,?,?,?,?,?,?)", + user.ID, post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, now) + if err != nil { + logger.Warn("submit post error", "error", err) + return nil, e.New(http.StatusInternalServerError, e.Internal) + } + } else { + // 只有创建者自己才能更新内容 + creator, _ := GetPostCreator(post.ID) + if creator != user.ID { + return nil, e.New(http.StatusForbidden, e.NoEditorPermission) + } + + _, err = db.Conn.Exec("UPDATE posts SET slug=?, title=?, md=?, url=?, cover=?, brief=?, updated=? WHERE id=?", + post.Slug, post.Title, md, post.URL, post.Cover, post.Brief, now, post.ID) + if err != nil { + logger.Warn("upate post error", "error", err) + return nil, e.New(http.StatusInternalServerError, e.Internal) + } + } + + return map[string]string{ + "username": user.Username, + "slug": post.Slug, + }, nil +} + +func DeletePost(id int64) *e.Error { + _, err := db.Conn.Exec("DELETE FROM posts WHERE id=?", id) + if err != nil { + logger.Warn("delete post error", "error", err) + return e.New(http.StatusInternalServerError, e.Internal) + } + + return nil +} + +func GetPost(id int64) (*models.Post, *e.Error) { + ar := &models.Post{} + var rawmd []byte + err := db.Conn.QueryRow("select id,slug,title,md,url,cover,brief,creator,created,updated from posts where id=?", id).Scan( + &ar.ID, &ar.Slug, &ar.Title, &rawmd, &ar.URL, &ar.Cover, &ar.Brief, &ar.CreatorID, &ar.Created, &ar.Updated, + ) + 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) + ar.Md = string(md) + ar.Creator = &models.UserSimple{ID: ar.CreatorID} + err = ar.Creator.Query() + + return ar, nil +} + +func GetPostCreator(id int64) (int64, *e.Error) { + var uid int64 + err := db.Conn.QueryRow("SELECT creator FROM posts WHERE id=?", id).Scan(&uid) + if err != nil { + if err == sql.ErrNoRows { + return 0, e.New(http.StatusNotFound, e.NotFound) + } + logger.Warn("get post creator error", "error", err) + return 0, e.New(http.StatusInternalServerError, e.Internal) + } + + return uid, nil +} + +//slug有三个规则 +// 1. 长度不能超过127 +// 2. 每次title更新,都要重新生成slug +// 3. 单个用户下的slug不能重复,如果已经存在,需要加上-1这种字符 +func setSlug(creator int64, post *models.Post) error { + slug := utils.Slugify(post.Title) + if len(slug) > 100 { + slug = slug[:100] + } + + count := 0 + err := db.Conn.QueryRow("SELECT count(*) FROM posts WHERE creator=? and title=?", creator, post.Title).Scan(&count) + if err != nil { + logger.Warn("count slug error", "error", err) + return err + } + + fmt.Println(count) + if count == 0 { + post.Slug = slug + } else { + post.Slug = fmt.Sprintf("%s-%d", slug, count) + } + + return nil +} diff --git a/server/internal/storage/sql_tables.go b/server/internal/storage/sql_tables.go index 6b0c8616..52ac992e 100644 --- a/server/internal/storage/sql_tables.go +++ b/server/internal/storage/sql_tables.go @@ -31,7 +31,7 @@ var sqlTables = map[string]string{ "posts": `CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, creator INTEGER NOT NULL, - + slug VARCHAR(64) NOT NULL, title VARCHAR(255) NOT NULL, md TEXT, url VARCHAR(255), @@ -45,5 +45,7 @@ var sqlTables = map[string]string{ ON posts (creator); CREATE INDEX IF NOT EXISTS posts_created ON posts (created); + CREATE UNIQUE INDEX IF NOT EXISTS posts_creator_slug + ON posts (creator, slug); `, } diff --git a/server/internal/ui_config.go b/server/internal/ui_config.go index 92d8b7c7..fd2a4284 100644 --- a/server/internal/ui_config.go +++ b/server/internal/ui_config.go @@ -13,6 +13,7 @@ type UIConfig struct { } type UIPosts struct { + TitleMaxLen int `json:"titleMaxLen"` BriefMaxLen int `json:"briefMaxLen"` WritingEnabled bool `json:"writingEnabled"` } @@ -20,6 +21,7 @@ type UIPosts struct { func GetUIConfig(c *gin.Context) { conf := &UIConfig{ Posts: &UIPosts{ + TitleMaxLen: config.Data.Posts.TitleMaxLen, BriefMaxLen: config.Data.Posts.BriefMaxLen, WritingEnabled: config.Data.Posts.WritingEnabled, }, diff --git a/server/pkg/config/config.go b/server/pkg/config/config.go index 9604c179..2f1d552f 100644 --- a/server/pkg/config/config.go +++ b/server/pkg/config/config.go @@ -33,6 +33,7 @@ type Config struct { } Posts struct { + TitleMaxLen int `yaml:"title_max_len"` BriefMaxLen int `yaml:"brief_max_len"` WritingEnabled bool `yaml:"writing_enabled"` } diff --git a/server/pkg/models/post.go b/server/pkg/models/post.go index 18f5aca1..af6f59f9 100644 --- a/server/pkg/models/post.go +++ b/server/pkg/models/post.go @@ -7,6 +7,7 @@ type Post struct { Creator *UserSimple `json:"creator"` CreatorID int64 `json:"creatorId"` Title string `json:"title"` + Slug string `json:"slug"` Md string `json:"md"` URL string `json:"url"` Cover string `json:"cover"` diff --git a/server/pkg/models/role.go b/server/pkg/models/role.go index d83204bc..16170d4c 100644 --- a/server/pkg/models/role.go +++ b/server/pkg/models/role.go @@ -5,6 +5,7 @@ type RoleType string const ( ROLE_NORMAL = "Normal" ROLE_EDITOR = "Editor" + ROLE_CREATOR = "Creator" ROLE_ADMIN = "Admin" ROLE_SUPER_ADMIN = "SuperAdmin" ) @@ -21,21 +22,6 @@ func (r RoleType) IsEditor() bool { return r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_SUPER_ADMIN } -func IsAdmin(r RoleType) bool { - return r.IsAdmin() -} - -func RoleSortWeight(role RoleType) int { - switch role { - case ROLE_NORMAL: - return 0 - case ROLE_EDITOR: - return 1 - case ROLE_ADMIN: - return 2 - case ROLE_SUPER_ADMIN: - return 3 - default: - return 0 - } +func (r RoleType) IsCreator() bool { + return r == ROLE_CREATOR || r == ROLE_EDITOR || r == ROLE_SUPER_ADMIN } diff --git a/server/pkg/utils/slug.go b/server/pkg/utils/slug.go new file mode 100644 index 00000000..bd10e03c --- /dev/null +++ b/server/pkg/utils/slug.go @@ -0,0 +1,23 @@ +package utils + +import ( + "encoding/base64" + "strings" + + "github.com/gosimple/slug" +) + +func Slugify(raw string) string { + s := slug.Make(strings.ToLower(raw)) + if s == "" { + // If the raw name is only characters outside of the + // sluggable characters, the slug creation will return an + // empty string which will mess up URLs. This failsafe picks + // that up and creates the slug as a base64 identifier instead. + s = base64.RawURLEncoding.EncodeToString([]byte(raw)) + if slug.MaxLength != 0 && len(s) > slug.MaxLength { + s = s[:slug.MaxLength] + } + } + return s +} diff --git a/src/components/posts/text-post-card.tsx b/src/components/posts/text-post-card.tsx index a4364057..ed4d8082 100644 --- a/src/components/posts/text-post-card.tsx +++ b/src/components/posts/text-post-card.tsx @@ -26,7 +26,7 @@ export const TextPostCard= (props:Props) =>{ {props.showActions && - + } ) diff --git a/src/types/role.ts b/src/types/role.ts index 271ccfb4..7e3510f4 100644 --- a/src/types/role.ts +++ b/src/types/role.ts @@ -1,6 +1,7 @@ export enum Role { - NORMAL = "Normal", - EDITOR = "Editor", - ADMIN = "Admin", - SUPER_ADMIN = "SuperAdmin" + NORMAL = "Normal", + EDITOR = "Editor", + ROLE_CREATOR = "Creator", + ADMIN = "Admin", + SUPER_ADMIN = "SuperAdmin" } \ No newline at end of file diff --git a/src/utils/config.ts b/src/utils/config.ts index 51f4ccc5..95dda93e 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -2,7 +2,8 @@ import { requestApi } from "./axios/request" export let config = { posts: { - briefMaxLen: 10, + titleMaxLen: 128, + briefMaxLen: 128, writingEnabled: false } } diff --git a/theme/layer-styles.js b/theme/layer-styles.js index 81714c14..0ba7e960 100644 --- a/theme/layer-styles.js +++ b/theme/layer-styles.js @@ -10,6 +10,6 @@ export default function layerStyles(theme) { cursor: 'pointer' }, _focus: null - } + }, } } \ No newline at end of file