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