pull/50/head
sunface 4 years ago
parent 3a1c406da4
commit 453d8c7718

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

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

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

@ -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) {
</NextLink>
</Flex>
<Box>
<Input value={props.ar.title} placeholder="Title..." onChange={props.changeTitle} focusBorderColor={useColorModeValue('teal.400','teal.100')} variant="flushed"/>
<Input value={props.ar.title} placeholder="Title..." onChange={props.changeTitle} focusBorderColor={useColorModeValue('teal.400', 'teal.100')} variant="flushed" />
</Box>
<HStack {...group}>
{editOptions.map((value) => {
@ -79,9 +88,33 @@ function HeaderContent(props:any) {
_focus={null}
icon={<SwitchIcon />}
/>
<Button layerStyle="colorButton" ml="2" onClick={props.publish}></Button>
<Button layerStyle="colorButton" ml="2" onClick={onOpen}></Button>
</Box>
</Flex>
<Drawer
isOpen={isOpen}
placement="right"
onClose={onClose}
size="md"
motionPreset="none"
>
<DrawerOverlay>
<DrawerContent p="4">
<Flex justifyContent="space-between" alignItems="center">
<Heading size="sm"></Heading>
<Button layerStyle="colorButton" ml="2" onClick={props.publish}></Button>
</Flex>
<Divider mt="5" mb="5"/>
<Card>
<Heading size="xs">
</Heading>
<Input value={props.ar.cover} onChange={(e) => {props.ar.cover = e.target.value; props.onChange()}} mt="4" variant="flushed" size="sm" placeholder="图片链接你可以用github当图片存储服务" focusBorderColor="teal.400"/>
</Card>
</DrawerContent>
</DrawerOverlay>
</Drawer>
</>
)
}

@ -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 (
<PageContainer
nav={<EditorNav
ar={ar}
onChange={onChange}
changeEditMode={(v) => 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}
/> :
<Box height="100%" p="6">

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

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

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

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

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

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

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

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

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

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

@ -26,7 +26,7 @@ export const TextPostCard= (props:Props) =>{
</VStack>
{props.showActions && <HStack>
<Button size="sm" colorScheme="teal" variant="outline" onClick={onEdit}>Edit</Button>
<Button size="sm" onClick={props.onDelete}>Delete</Button>
<Button size="sm" onClick={props.onDelete} variant="ghost">Delete</Button>
</HStack>}
</Flex>
)

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

@ -2,7 +2,8 @@ import { requestApi } from "./axios/request"
export let config = {
posts: {
briefMaxLen: 10,
titleMaxLen: 128,
briefMaxLen: 128,
writingEnabled: false
}
}

@ -10,6 +10,6 @@ export default function layerStyles(theme) {
cursor: 'pointer'
},
_focus: null
}
},
}
}
Loading…
Cancel
Save