update user navbars

pull/52/head
codemystery 4 years ago
parent 25acb1cf9b
commit 4feea1b987

@ -11,7 +11,8 @@ export let config = {
}, },
user: { user: {
nicknameMaxLen: 64, nicknameMaxLen: 64,
usernameMaxLen: 39 usernameMaxLen: 39,
navbarMaxLen: 20
} }
} }

@ -9,7 +9,7 @@ import { useRouter } from "next/router"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { FaFacebook, FaFile, FaGithub, FaHeart, FaPlus, FaRegStar, FaStackOverflow, FaStar, FaTwitter, FaWeibo, FaZhihu } from "react-icons/fa" import { FaFacebook, FaFile, FaGithub, FaHeart, FaPlus, FaRegStar, FaStackOverflow, FaStar, FaTwitter, FaWeibo, FaZhihu } from "react-icons/fa"
import { ReserveUrls } from "src/data/reserve-urls" import { ReserveUrls } from "src/data/reserve-urls"
import { User } from "src/types/user" import { Navbar, NavbarType, User } from "src/types/user"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import moment from 'moment' import moment from 'moment'
import { Story } from "src/types/story" import { Story } from "src/types/story"
@ -35,6 +35,7 @@ const UserPage = () => {
const [tags, setTags]: [Tag[], any] = useState([]) const [tags, setTags]: [Tag[], any] = useState([])
const [tagFilter, setTagFilter]: [Tag, any] = useState(null) const [tagFilter, setTagFilter]: [Tag, any] = useState(null)
const [followers, setFollowers]: [User[], any] = useState([]) const [followers, setFollowers]: [User[], any] = useState([])
const [navbars,setNavbars]:[Navbar[],any] = useState([])
const borderColor = useColorModeValue('white', 'transparent') const borderColor = useColorModeValue('white', 'transparent')
const stackBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) const stackBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark)
useEffect(() => { useEffect(() => {
@ -48,12 +49,17 @@ const UserPage = () => {
setUser(res.data) setUser(res.data)
getTags(res.data.id) getTags(res.data.id)
getNavbars(res.data.id)
const res1 = await requestApi.get(`/user/posts/${res.data.id}`) const res1 = await requestApi.get(`/user/posts/${res.data.id}`)
setPosts(res1.data) setPosts(res1.data)
setRawPosts(res1.data) setRawPosts(res1.data)
} }
const getNavbars = async userID => {
const res = await requestApi.get(`/user/navbars/${userID}`)
setNavbars(res.data)
}
const getTags = async (userID) => { const getTags = async (userID) => {
const res = await requestApi.get(`/tag/user/${userID}`) const res = await requestApi.get(`/tag/user/${userID}`)
setTags(res.data) setTags(res.data)
@ -128,12 +134,16 @@ const UserPage = () => {
HOME HOME
</Box> </Box>
</Link> </Link>
<Link href={`/${username}?nav=react`}> {
<Box cursor="pointer" fontWeight={isSubNavActive('react') ? "bold" : "550"} layerStyle={isSubNavActive('react') ? null : "textSecondary"}> navbars.map(nv =>
REACT <Link href={nv.type === NavbarType.Link ? nv.value : `${ReserveUrls.Series}/${nv.value}`}>
</Box> <Box cursor="pointer" fontWeight={isSubNavActive('react') ? "bold" : "550"} layerStyle={isSubNavActive('react') ? null : "textSecondary"}>
</Link> {nv.label}
</Box>
</Link>)
}
</HStack> </HStack>

@ -1,110 +1,188 @@
import { Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, HStack, Wrap, useMediaQuery, Avatar, Textarea, } from "@chakra-ui/react" import { Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, HStack, Wrap, useMediaQuery, Avatar, Textarea, Table, Thead, Tr, Th, Tbody, Td, IconButton, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, NumberInput, NumberInputField, NumberInputStepper, NumberIncrementStepper, NumberDecrementStepper } from "@chakra-ui/react"
import Card from "components/card" import Card from "components/card"
import Nav from "layouts/nav/nav"
import PageContainer from "layouts/page-container" import PageContainer from "layouts/page-container"
import Sidebar from "layouts/sidebar/sidebar" import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { adminLinks, settingLinks } from "src/data/links" import { settingLinks } from "src/data/links"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import { useRouter } from "next/router"
import { Field, Form, Formik } from "formik"
import { config } from "configs/config" import { config } from "configs/config"
import Tags from "components/tags/tags" import { getSvgIcon } from "components/svg-icon"
var validator = require('validator'); import { Navbar, NavbarType } from "src/types/user"
import { cloneDeep } from "lodash"
import { IDType } from "src/types/id"
import { Story } from "src/types/story"
const UserNavbarPage = () => { const UserNavbarPage = () => {
const [user, setUser] = useState(null) const [navbars, setNavbars]:[Navbar[],any] = useState([])
const [skills, setSkills] = useState([]) const [series, setSeries]: [Story[], any] = useState([])
const [isLargerThan1280] = useMediaQuery("(min-width: 768px)") const [currentNavbar, setCurrentNavbar]: [Navbar, any] = useState(null)
const { isOpen, onOpen, onClose } = useDisclosure()
const toast = useToast()
useEffect(() => { useEffect(() => {
requestApi.get("/user/self").then(res => setUser(res.data)) getNavbars()
getSeries()
}, []) }, [])
const router = useRouter()
const toast = useToast()
const submitUser = async (values, _) => { const getNavbars = async () => {
await requestApi.post(`/user/update`, values) const res = await requestApi.get("/user/navbars/0")
setUser(values) setNavbars(res.data)
toast({
description: "更新成功",
status: "success",
duration: 2000,
isClosable: true,
})
} }
function validateNickname(value) { const getSeries = async () => {
let error const res = await requestApi.get(`/story/posts/editor?type=${IDType.Series}`)
if (!value?.trim()) { setSeries(res.data)
error = "昵称不能为空" }
const submitNavbar = async () => {
if (!currentNavbar.label || !currentNavbar.value) {
toast({
description: "值不能为空",
status: "error",
duration: 2000,
isClosable: true,
})
return
} }
if (value?.length > config.user.nicknameMaxLen) { if (currentNavbar.label.length > config.user.navbarMaxLen) {
error = `长度不能超过${config.user.nicknameMaxLen}` toast({
description: `Label长度不能超过${config.user.navbarMaxLen}`,
status: "error",
duration: 2000,
isClosable: true,
})
return
} }
return error
}
function validateEmail(value) { await requestApi.post(`/user/navbar`, currentNavbar)
let email = value?.trim() setCurrentNavbar(null)
let error onClose()
getNavbars()
}
if (email?.length > config.user.usernameMaxLen) { const onAddNavbar = () => {
error = `长度不能超过${config.user.usernameMaxLen}` setCurrentNavbar({ weight: 0, type: NavbarType.Link, label: "", value: "https://" })
return error onOpen()
} }
if (email) { const onEditNavbar = nav => {
if (!validator.isEmail(email)) { setCurrentNavbar(nav)
error = "Email格式不合法" onOpen()
return error
}
}
return error
} }
const onNavbarChange = () => {
const nv = cloneDeep(currentNavbar)
setCurrentNavbar(nv)
}
function validateUrl(value, canBeEmpty = true) { const onNvTypeChange = v => {
let url = value?.trim() const tp = parseInt(v);
let error currentNavbar.type = tp
if (!canBeEmpty) { if (tp === NavbarType.Link) {
if (!url) { currentNavbar.value = ""
error = "url不能为空" } else {
return error currentNavbar.value = series[0].id
}
} }
onNavbarChange()
if (url) { }
if (!validator.isURL(value)) { const getSeriesTitle = id => {
error = "URL格式不合法" for (const s of series) {
return error if (s.id === id) {
return s.title
} }
} }
return error return ""
} }
function validateLen(value) { const onDeleteNavbar = async id => {
let error requestApi.delete(`/user/navbar/${id}`)
if (value?.length > config.commonMaxlen) { getNavbars()
error = `长度不能超过${config.commonMaxlen}`
}
return error
} }
const Layout = isLargerThan1280 ? HStack : VStack
return ( return (
<> <>
<PageContainer> <PageContainer>
<Box display="flex"> <Box display="flex">
<Sidebar routes={settingLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="博客设置" /> <Sidebar routes={settingLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="博客设置" />
{user && <Card ml="4" width="100%"> <Card ml="4" width="100%">
<Heading></Heading> <Flex justifyContent="space-between" alignItems="center">
</Card>} <Heading size="sm"></Heading>
<Button colorScheme="teal" size="sm" onClick={onAddNavbar} _focus={null}></Button>
</Flex>
<Table variant="simple" mt="4">
<Thead>
<Tr>
<Th>Label</Th>
<Th>Type</Th>
<Th>Value</Th>
<Th>Weight</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{
navbars.map((nv,i) => <Tr key={i}>
<Td>{nv.label}</Td>
<Td>{nv.type === NavbarType.Link ? "link" : "series"}</Td>
<Td>{nv.type === NavbarType.Link ? nv.value : getSeriesTitle(nv.value)}</Td>
<Td>{nv.weight}</Td>
<Td>
<IconButton aria-label="edit navbar" variant="ghost" icon={getSvgIcon('edit', ".95rem")} onClick={() => onEditNavbar(nv)}/>
<IconButton aria-label="delete navbar" variant="ghost" icon={getSvgIcon('close', "1rem")} onClick={() => onDeleteNavbar(nv.id)} />
</Td>
</Tr>)
}
</Tbody>
</Table>
</Card>
</Box> </Box>
</PageContainer> </PageContainer>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
{currentNavbar && <ModalContent>
<ModalHeader>{currentNavbar.label ? "编辑菜单项" : "新建菜单项"}</ModalHeader>
<ModalBody mb="2">
<VStack spacing="4" alignItems="left">
<HStack spacing="4">
<Heading size="xs">Label</Heading>
<Input value={currentNavbar.label} _focus={null} variant="flushed" onChange={e => { currentNavbar.label = e.currentTarget.value; onNavbarChange() }}></Input>
</HStack>
<HStack spacing="4">
<Heading size="xs">Type</Heading>
<Select value={currentNavbar.type} onChange={e => onNvTypeChange(e.currentTarget.value)} variant="flushed" _focus={null}>
<option value={NavbarType.Link}>Link</option>
<option value={NavbarType.Series}>Series</option>
</Select>
</HStack>
<HStack spacing="4">
<Heading size="xs">Value</Heading>
{currentNavbar.type === NavbarType.Link ? <Input value={currentNavbar.value} _focus={null} variant="flushed" onChange={e => { currentNavbar.value = e.currentTarget.value; onNavbarChange() }} /> :
<Select value={currentNavbar.value} variant="flushed" _focus={null} onChange={e => { currentNavbar.value = e.currentTarget.value; onNavbarChange() }}>
{series.map(s => <option key={s.id} value={s.id}>{s.title}</option>)}
</Select>}
</HStack>
<HStack spacing="4">
<Heading size="xs">Weight</Heading>
<NumberInput min={0} max={10} value={currentNavbar.weight} variant="flushed" onChange={e => { currentNavbar.weight = parseInt(e); onNavbarChange() }}>
<NumberInputField _focus={null} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</HStack>
</VStack>
<Button colorScheme="teal" variant="outline" mt="6" onClick={submitNavbar}></Button>
</ModalBody>
</ModalContent>}
</Modal>
</> </>
) )
} }

@ -98,7 +98,7 @@ func SubmitNavbar(c *gin.Context) {
} }
nav.UserID = u.ID nav.UserID = u.ID
err1 := user.AddNavbar(nav) err1 := user.SubmitNavbar(nav)
if err != nil { if err != nil {
c.JSON(err1.Status, common.RespError(err1.Message)) c.JSON(err1.Status, common.RespError(err1.Message))
return return
@ -106,3 +106,33 @@ func SubmitNavbar(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil)) c.JSON(http.StatusOK, common.RespSuccess(nil))
} }
func GetNavbars(c *gin.Context) {
userID := c.Param("userID")
if userID == "0" {
u := user.CurrentUser(c)
userID = u.ID
}
navbars, err := user.GetNavbars(userID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(navbars))
}
func DeleteNavbar(c *gin.Context) {
id := c.Param("id")
u := user.CurrentUser(c)
err := user.DeleteNavbar(u.ID, id)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}

@ -25,6 +25,7 @@ type PostsConfig struct {
type UserConfig struct { type UserConfig struct {
NicknameMaxLen int `json:"nicknameMaxLen"` NicknameMaxLen int `json:"nicknameMaxLen"`
UsernameMaxLen int `json:"usernameMaxLen"` UsernameMaxLen int `json:"usernameMaxLen"`
NavabarMaxLen int `json:"navbarMaxLen"`
} }
// 在后台页面配置存储到mysql中 // 在后台页面配置存储到mysql中
@ -41,6 +42,7 @@ func GetConfig(c *gin.Context) {
User: &UserConfig{ User: &UserConfig{
UsernameMaxLen: 39, UsernameMaxLen: 39,
NicknameMaxLen: 64, NicknameMaxLen: 64,
NavabarMaxLen: 20,
}, },
} }

@ -85,6 +85,8 @@ func (s *Server) Start() error {
r.POST("/user/login", user.Login) r.POST("/user/login", user.Login)
r.POST("/user/logout", user.Logout) r.POST("/user/logout", user.Logout)
r.POST("/user/navbar", IsLogin(), api.SubmitNavbar) r.POST("/user/navbar", IsLogin(), api.SubmitNavbar)
r.GET("/user/navbars/:userID", api.GetNavbars)
r.DELETE("/user/navbar/:id", IsLogin(), api.DeleteNavbar)
// interaction apis // interaction apis
r.POST("/interaction/like/:id", IsLogin(), api.Like) r.POST("/interaction/like/:id", IsLogin(), api.Like)
r.POST("/interaction/follow/:id", IsLogin(), api.Follow) r.POST("/interaction/follow/:id", IsLogin(), api.Follow)

@ -188,6 +188,7 @@ var sqlTables = map[string]string{
`, `,
"user_navbar": `CREATE TABLE IF NOT EXISTS user_navbar ( "user_navbar": `CREATE TABLE IF NOT EXISTS user_navbar (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id VARCHAR(255), user_id VARCHAR(255),
label VARCHAR(20), label VARCHAR(20),
type TINYINT, type TINYINT,

@ -9,11 +9,17 @@ import (
"github.com/imdotdev/im.dev/server/pkg/models" "github.com/imdotdev/im.dev/server/pkg/models"
) )
func AddNavbar(nav *models.Navbar) *e.Error { func SubmitNavbar(nav *models.Navbar) *e.Error {
_, err := db.Conn.Exec("INSERT INTO user_navbar (user_id,label,type,value,weight) VALUES (?,?,?,?,?)", var err error
nav.UserID, nav.Label, nav.Type, nav.Value, nav.Weight) if nav.ID == 0 {
_, err = db.Conn.Exec("INSERT INTO user_navbar (user_id,label,type,value,weight) VALUES (?,?,?,?,?)",
nav.UserID, nav.Label, nav.Type, nav.Value, nav.Weight)
} else {
_, err = db.Conn.Exec("UPDATE user_navbar SET label=?,type=?,value=?,weight=? WHERE id=? and user_id=?", nav.Label, nav.Type, nav.Value, nav.Weight, nav.ID, nav.UserID)
}
if err != nil { if err != nil {
logger.Warn("add user navbar error", "error", err) logger.Warn("submit user navbar error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal) return e.New(http.StatusInternalServerError, e.Internal)
} }
@ -21,7 +27,7 @@ func AddNavbar(nav *models.Navbar) *e.Error {
} }
func GetNavbars(userID string) (models.Navbars, *e.Error) { func GetNavbars(userID string) (models.Navbars, *e.Error) {
rows, err := db.Conn.Query("SELECT label,type,value,weight FROM navbar WHERE user_id=?", userID) rows, err := db.Conn.Query("SELECT id,label,type,value,weight FROM user_navbar WHERE user_id=?", userID)
if err != nil { if err != nil {
logger.Warn("get user navbar error", "error", err) logger.Warn("get user navbar error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal) return nil, e.New(http.StatusInternalServerError, e.Internal)
@ -30,7 +36,7 @@ func GetNavbars(userID string) (models.Navbars, *e.Error) {
navs := make(models.Navbars, 0) navs := make(models.Navbars, 0)
for rows.Next() { for rows.Next() {
nav := &models.Navbar{} nav := &models.Navbar{}
rows.Scan(&nav.Label, &nav.Type, &nav.Value, &nav.Weight) rows.Scan(&nav.ID, &nav.Label, &nav.Type, &nav.Value, &nav.Weight)
navs = append(navs, nav) navs = append(navs, nav)
} }
@ -38,3 +44,13 @@ func GetNavbars(userID string) (models.Navbars, *e.Error) {
return navs, nil return navs, nil
} }
func DeleteNavbar(userID string, id string) *e.Error {
_, err := db.Conn.Exec("DELETE FROM user_navbar WHERE id=? and user_id=?", id, userID)
if err != nil {
logger.Warn("submit user navbar error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}

@ -6,6 +6,7 @@ const (
) )
type Navbar struct { type Navbar struct {
ID int `json:"id"`
UserID string `json:"userID"` UserID string `json:"userID"`
Label string `json:"label"` Label string `json:"label"`
Type int `json:"type"` Type int `json:"type"`

@ -52,6 +52,9 @@ export function getSvgIcon(name, height = "1.4rem") {
case "navbar": case "navbar":
svg = <svg height={height} fill="currentColor" viewBox="0 0 448 512"><path d="M424 96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zM264 96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zM104 96H24c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96H32v-64h64v64zm328 96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zm-152-96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zm-152-96H24c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96H32v-64h64v64z"></path></svg> svg = <svg height={height} fill="currentColor" viewBox="0 0 448 512"><path d="M424 96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zM264 96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zM104 96H24c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96H32v-64h64v64zm328 96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zm-152-96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zm-152-96H24c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96H32v-64h64v64z"></path></svg>
break break
case "close":
svg = <svg height={height} fill="currentColor" viewBox="0 0 320 512"><path d="M193.94 256L296.5 153.44l21.15-21.15c3.12-3.12 3.12-8.19 0-11.31l-22.63-22.63c-3.12-3.12-8.19-3.12-11.31 0L160 222.06 36.29 98.34c-3.12-3.12-8.19-3.12-11.31 0L2.34 120.97c-3.12 3.12-3.12 8.19 0 11.31L126.06 256 2.34 379.71c-3.12 3.12-3.12 8.19 0 11.31l22.63 22.63c3.12 3.12 8.19 3.12 11.31 0L160 289.94 262.56 392.5l21.15 21.15c3.12 3.12 8.19 3.12 11.31 0l22.63-22.63c3.12-3.12 3.12-8.19 0-11.31L193.94 256z"></path></svg>
break
default: default:
break; break;
} }

@ -45,4 +45,17 @@ export interface UserSimple {
username: string username: string
nickname: string nickname: string
avatar: string avatar: string
}
export enum NavbarType {
Link = 1,
Series = 2
}
export interface Navbar {
id?: number,
label: string
type: number
value: string
weight: number
} }
Loading…
Cancel
Save