pull/52/head
codemystery 4 years ago
parent 4feea1b987
commit aeaf7aacd2

@ -11,7 +11,7 @@ import {
import siteConfig from "configs/site-config"
import { useViewportScroll } from "framer-motion"
import NextLink from "next/link"
import React from "react"
import React, { useEffect, useState } from "react"
import { FaGithub } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo"
import { MobileNavButton, MobileNavContent } from "./mobile-nav"
@ -23,16 +23,31 @@ import DarkMode from "components/dark-mode"
import AccountMenu from "components/user-menu"
import { navLinks } from "src/data/links"
import { getSvgIcon } from "components/svg-icon"
import { requestApi } from "utils/axios/request"
function HeaderContent() {
const router = useRouter()
const { asPath } = router
const mobileNav = useDisclosure()
const mobileNavBtnRef = React.useRef<HTMLButtonElement>()
const [navs,setNavs] = useState(navLinks)
useEffect(() => {
requestApi.get("/navbars").then(res => {
const nvs = []
res.data.forEach(nv => nvs.push({
title: nv.label,
url: nv.value
}))
setNavs(nvs)
})
},[])
useUpdateEffect(() => {
mobileNavBtnRef.current?.focus()
}, [mobileNav.isOpen])
@ -54,7 +69,7 @@ function HeaderContent() {
</NextLink>
<HStack ml={{ base: 1, md: 4, lg: 12 }} fontSize="1rem">
{navLinks.map(link => <Box px={[0,0,4,4]} py="0.7rem" rounded="md" key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={asPath === link.url ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} ><Link href={link.url}>{link.title}</Link></Box>)}
{navs.map(link => <Box px={[0,0,4,4]} py="0.7rem" rounded="md" key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={asPath === link.url ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} ><Link href={link.url}>{link.title}</Link></Box>)}
</HStack>
</Flex>

@ -14,7 +14,7 @@ import {
import siteConfig from "configs/site-config"
import { useViewportScroll } from "framer-motion"
import NextLink from "next/link"
import React from "react"
import React, { useEffect, useState } from "react"
import { FaGithub, FaSearch } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo"
import { MobileNavButton, MobileNavContent } from "./mobile-nav"
@ -26,6 +26,7 @@ import {
import AccountMenu from "components/user-menu"
import { getSvgIcon } from "components/svg-icon"
import { navLinks } from "src/data/links"
import { requestApi } from "utils/axios/request"
@ -34,7 +35,18 @@ import { navLinks } from "src/data/links"
const { asPath } = router
const mobileNav = useDisclosure()
const [navs,setNavs] = useState(navLinks)
useEffect(() => {
requestApi.get("/navbars").then(res => {
const nvs = []
res.data.forEach(nv => nvs.push({
title: nv.label,
url: nv.value
}))
setNavs(nvs)
})
},[])
const mobileNavBtnRef = React.useRef<HTMLButtonElement>()
const [isLargerThan768] = useMediaQuery("(min-width: 768px)")
useUpdateEffect(() => {
@ -60,10 +72,11 @@ import { navLinks } from "src/data/links"
</NextLink>
<VStack pt="6" ml={{ base: 1, md: 4, lg: 12 }} fontSize="1rem" alignItems="left">
{navLinks.map(link =>
{navs.map(link =>
<Link href={link.url} key={link.title}>
<HStack cursor="pointer" px="4" py="0.7rem" rounded="md" key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={isActive(link.baseUrl) ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} >
<Box width="25px">{link.icon}</Box><Text fontWeight="600">{link.title}</Text>
<HStack cursor="pointer" px="4" py="0.7rem" rounded="md" key={link.url} color={useColorModeValue("gray.700", "whiteAlpha.900")} aria-current={isActive(link.baseUrl??link.url) ? "page" : undefined} _activeLink={{ bg: useColorModeValue("transparent", "rgba(48, 140, 122, 0.3)"), color: useColorModeValue("teal.500", "teal.200"), fontWeight: "bold", }} >
{/* <Box width="25px">{link.icon}</Box> */}
<Text fontWeight="600">{link.title}</Text>
</HStack>
</Link>
)}

@ -28,7 +28,10 @@ const StyledLink = React.forwardRef(function StyledLink(
fontWeight: "600",
}}
{...rest}
>{icon && <chakra.span mr="5" fontSize="1.1rem" display={{base:"none",md:"block"}} width="20px">{icon}</chakra.span> }<chakra.span>{children}</chakra.span></chakra.a>
>
{icon && <chakra.span mr="5" fontSize="1.1rem" display={{base:"none",md:"block"}} width="20px">{icon}</chakra.span> }
<chakra.span>{children}</chakra.span>
</chakra.a>
)
})

@ -1,10 +1,10 @@
async function redirect() {
return [
// {
// source: "/search",
// destination: "/search/posts",
// permanent: true,
// }
{
source: "/search",
destination: "/search/posts",
permanent: true,
}
]
}

@ -22,6 +22,7 @@ import { IDType } from "src/types/id"
import UserCard from "components/users/user-card"
import userCustomTheme from "theme/user-custom"
import SearchFilters from "components/search-filters"
import Follow from "components/interaction/follow"
const UserPage = () => {
const { isOpen, onOpen, onClose } = useDisclosure()
@ -90,7 +91,7 @@ const UserPage = () => {
let res
if (tp === 1) {
// followings
const res0 = await requestApi.get(`/interaction/following/${user.id}?type=${IDType.User}`)
const res0 = await requestApi.get(`/interaction/following/${user.id}?type=${user.id.substring(0,1)}`)
const ids = []
for (const f of res0.data) {
ids.push(f.id)
@ -98,9 +99,12 @@ const UserPage = () => {
res = await requestApi.post(`/user/ids`, ids)
} else {
} else if (tp === 0) {
// followers
res = await requestApi.get(`/interaction/followers/${user.id}?type=${IDType.User}`)
res = await requestApi.get(`/interaction/followers/${user.id}?type=${user.id.substring(0,1)}`)
} else if (tp === 2) {
// org members
res = await requestApi.get(`/org/members/${user.id}`)
}
setFollowers(res.data)
if (res.data.length > 0) {
@ -123,7 +127,7 @@ const UserPage = () => {
<Box alignItems="left" pb="6">
<Card p="0" borderTop="none">
<Box backgroundImage={`url(${user.cover})`} height="300px" width="100%" backgroundSize="cover" backgroundPosition="center" />
<VStack maxHeight="205px" position="relative" top="-70px" spacing="3">
<VStack maxHeight={user.tagline? "205px" : "165px"} position="relative" top="-70px" spacing="3">
<Image src={user.avatar} height="130px" borderRadius="50%" border={`4px solid ${borderColor}`} />
<Heading size="lg">{user.nickname}</Heading>
{user.tagline && <Text layerStyle="textSecondary" fontWeight="450" fontSize="1.2rem" ml="1" mt="2">{user.tagline}</Text>}
@ -136,8 +140,8 @@ const UserPage = () => {
</Link>
{
navbars.map(nv =>
<Link href={nv.type === NavbarType.Link ? nv.value : `${ReserveUrls.Series}/${nv.value}`}>
navbars.map((nv,i) =>
<Link key={i} href={nv.type === NavbarType.Link ? nv.value : `${ReserveUrls.Series}/${nv.value}`}>
<Box cursor="pointer" fontWeight={isSubNavActive('react') ? "bold" : "550"} layerStyle={isSubNavActive('react') ? null : "textSecondary"}>
{nv.label}
</Box>
@ -148,7 +152,7 @@ const UserPage = () => {
</HStack>
<Box pt="3" position="absolute" right="15px" top="60px">{session?.user.id === user.id ? <Button onClick={() => router.push(`${ReserveUrls.Settings}/profile`)} variant="outline" leftIcon={<svg height="1.3rem" fill="currentColor" viewBox="0 0 512 512"><path d="M493.255 56.236l-37.49-37.49c-24.993-24.993-65.515-24.994-90.51 0L12.838 371.162.151 485.346c-1.698 15.286 11.22 28.203 26.504 26.504l114.184-12.687 352.417-352.417c24.992-24.994 24.992-65.517-.001-90.51zm-95.196 140.45L174 420.745V386h-48v-48H91.255l224.059-224.059 82.745 82.745zM126.147 468.598l-58.995 6.555-30.305-30.305 6.555-58.995L63.255 366H98v48h48v34.745l-19.853 19.853zm344.48-344.48l-49.941 49.941-82.745-82.745 49.941-49.941c12.505-12.505 32.748-12.507 45.255 0l37.49 37.49c12.506 12.506 12.507 32.747 0 45.255z"></path></svg>}><chakra.span display={{ base: "none", md: "block" }}>Edit Profile</chakra.span></Button>
: <Button colorScheme="teal">Follow</Button>}</Box>
: <Follow followed={user.followed} targetID={user.id}/>}</Box>
</VStack>
@ -163,8 +167,8 @@ const UserPage = () => {
</Box>
<Box width="50%">
<Heading size="sm">Following</Heading>
<Text mt="1" cursor="pointer" onClick={() => viewFollowers(1)}><Count count={user.followings ?? 0} /></Text>
<Heading size="sm">{user.type === IDType.User ? "Following" : "Members"}</Heading>
<Text mt="1" cursor="pointer" onClick={user.type === IDType.User ? () => viewFollowers(1) : () => viewFollowers(2)}><Count count={user.followings ?? 0} /></Text>
</Box>
</Flex>
</Card>

@ -0,0 +1,158 @@
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 PageContainer from "layouts/page-container"
import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react"
import { adminLinks, settingLinks } from "src/data/links"
import { requestApi } from "utils/axios/request"
import { config } from "configs/config"
import { getSvgIcon } from "components/svg-icon"
import { Navbar, NavbarType } from "src/types/user"
import { cloneDeep } from "lodash"
import { IDType } from "src/types/id"
import { Story } from "src/types/story"
import PageContainer1 from "layouts/page-container1"
const UserNavbarPage = () => {
const [navbars, setNavbars]:[Navbar[],any] = useState([])
const [series, setSeries]: [Story[], any] = useState([])
const [currentNavbar, setCurrentNavbar]: [Navbar, any] = useState(null)
const { isOpen, onOpen, onClose } = useDisclosure()
const toast = useToast()
useEffect(() => {
getNavbars()
getSeries()
}, [])
const getNavbars = async () => {
const res = await requestApi.get("/navbars")
setNavbars(res.data)
}
const getSeries = async () => {
const res = await requestApi.get(`/story/posts/editor?type=${IDType.Series}`)
setSeries(res.data)
}
const submitNavbar = async () => {
if (!currentNavbar.label || !currentNavbar.value) {
toast({
description: "值不能为空",
status: "error",
duration: 2000,
isClosable: true,
})
return
}
if (currentNavbar.label.length > config.user.navbarMaxLen) {
toast({
description: `Label长度不能超过${config.user.navbarMaxLen}`,
status: "error",
duration: 2000,
isClosable: true,
})
return
}
await requestApi.post(`/navbar`, currentNavbar)
setCurrentNavbar(null)
onClose()
getNavbars()
}
const onAddNavbar = () => {
setCurrentNavbar({ weight: 0, type: NavbarType.Link, label: "", value: "" })
onOpen()
}
const onEditNavbar = nav => {
setCurrentNavbar(nav)
onOpen()
}
const onNavbarChange = () => {
const nv = cloneDeep(currentNavbar)
setCurrentNavbar(nv)
}
const onDeleteNavbar = async id => {
requestApi.delete(`/navbar/${id}`)
setTimeout( () => getNavbars(),300)
}
return (
<>
<PageContainer1>
<Box display="flex">
<Sidebar routes={adminLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="博客设置" />
<Card ml="4" width="100%">
<Flex justifyContent="space-between" alignItems="center">
<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>Value</Th>
<Th>Weight</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{
navbars.map((nv,i) => <Tr key={i}>
<Td>{nv.label}</Td>
<Td>{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>
</PageContainer1>
<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">Value</Heading>
<Input value={currentNavbar.value} _focus={null} variant="flushed" onChange={e => { currentNavbar.value = e.currentTarget.value; onNavbarChange() }} placeholder="enter a url, e.g /search"/>
</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>
</>
)
}
export default UserNavbarPage

@ -0,0 +1,163 @@
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, useColorModeValue, StackDivider } from "@chakra-ui/react"
import Card from "components/card"
import PageContainer from "layouts/page-container"
import Sidebar from "layouts/sidebar/sidebar"
import React, { useEffect, useState } from "react"
import { settingLinks } from "src/data/links"
import { requestApi } from "utils/axios/request"
import { Org } from "src/types/org"
import { Field, Form, Formik } from "formik"
import { config } from "configs/config"
import { isUsernameChar, usernameInvalidTips } from "utils/user"
import { isAdmin } from "utils/role"
import userCustomTheme from "theme/user-custom"
import { useRouter } from "next/router"
import Link from "next/link"
const UserOrgsPage = () => {
const [orgs, setOrgs]:[Org[],any] = useState([])
const { isOpen, onOpen, onClose } = useDisclosure()
const router = useRouter()
const stackBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark)
useEffect(() => {
getOrgs()
}, [])
const getOrgs = async () => {
const res = await requestApi.get("/org/byUserID/0")
setOrgs(res.data)
}
const createOrg = async (values:Org) => {
await requestApi.post(`/org/create`, values)
onClose()
router.push(`/${values.username}`)
}
const onCreateOrg = () => {
onOpen()
}
const validateUsername = async value => {
let error
if (!value?.trim()) {
return "不能为空"
}
if (value?.length > config.user.usernameMaxLen) {
return `长度不能超过${config.user.usernameMaxLen}`
}
for (const c of value) {
if (!isUsernameChar(c)) {
return usernameInvalidTips
}
}
const res = await requestApi.get(`/username/exist/${value}`)
if (res.data) {
return `The name '${value}' is already taken.`
}
return error
}
function validateNickname(value) {
let error
if (!value?.trim()) {
error = "不能为空"
}
if (value?.length > config.user.usernameMaxLen) {
error = `长度不能超过${config.user.usernameMaxLen}`
}
return error
}
return (
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={settingLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="博客设置" />
<Card ml="4" width="100%">
<Flex justifyContent="space-between" alignItems="center">
<Heading size="sm"></Heading>
<Button colorScheme="teal" size="sm" onClick={onCreateOrg} _focus={null}></Button>
</Flex>
<VStack mt="3" divider={<StackDivider borderColor={stackBorderColor} />} alignItems="left">
{
orgs.map(o => <Flex key={o.id} justifyContent="space-between" alignItems="center" p="2">
<Link href={`/${o.username}`}>
<HStack cursor="pointer">
<Image src={o.avatar} height="30px"/>
<Heading size="sm" fontSize="1rem">{o.nickname}</Heading>
<Text layerStyle="textSecondary">{isAdmin(o.role) ? 'admin' : 'member'}</Text>
</HStack>
</Link>
<Button variant="ghost">View</Button>
</Flex>)
}
</VStack>
</Card>
</Box>
</PageContainer>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
{<ModalContent>
<ModalHeader></ModalHeader>
<ModalBody mb="2">
<Formik
initialValues={{username: '',nickname:''} as Org}
onSubmit={createOrg}
>
{(props) => (
<Form>
<VStack>
<Field name="username" validate={validateUsername}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.username && form.touched.username} >
<FormLabel>Username</FormLabel>
<Input {...field} placeholder="name" />
<FormErrorMessage>{form.errors.username}</FormErrorMessage>
</FormControl>
)}
</Field>
<Field name="nickname" validate={validateNickname}>
{({ field, form }) => (
<FormControl isInvalid={form.errors.nickname && form.touched.nickname} >
<FormLabel>Nickname</FormLabel>
<Input {...field} placeholder="name" />
<FormErrorMessage>{form.errors.nickname}</FormErrorMessage>
</FormControl>
)}
</Field>
</VStack>
<Box mt={6}>
<Button
colorScheme="teal"
variant="outline"
type="submit"
_focus={null}
>
</Button>
<Button variant="ghost" ml="4" _focus={null} onClick={onClose}></Button>
</Box>
</Form>
)}
</Formik>
</ModalBody>
</ModalContent>}
</Modal>
</>
)
}
export default UserOrgsPage

@ -1,63 +0,0 @@
import useCaretPosition from 'components/markdown-editor/position'
import TestStyles from 'theme/caret.styles'
import React, { useRef, useState, useEffect, Fragment } from 'react'
import { render } from 'react-dom'
const App = () => {
const triggerRef = useRef(null)
const [showTrigger, setShowTrigger] = useState(false)
const {
x: triggerX,
y: triggerY,
getPosition: getPositionTrigger,
} = useCaretPosition(triggerRef)
const handleCustomUI = (e) => {
const previousCharacter = e.target.value
.charAt(triggerRef.current.selectionStart - 2)
.trim()
const character = e.target.value
.charAt(triggerRef.current.selectionStart - 1)
.trim()
if (character === '@' && previousCharacter === '') {
setShowTrigger(true)
}
if (character === '' && showTrigger) {
setShowTrigger(false)
}
}
useEffect(() => {
if (triggerRef.current) {
getPositionTrigger(triggerRef)
}
}, [])
return (
<>
<TestStyles />
<section>
<textarea
ref={triggerRef}
placeholder="Type the @ symbol to trigger UI"
spellCheck="false"
onKeyUp={handleCustomUI}
onInput={() => getPositionTrigger(triggerRef)}
/>
<span
className="marker marker--trigger"
style={{
display: showTrigger ? 'block' : 'none',
//@ts-ignore
'--y': triggerY,
'--x': triggerX,
}}>
Triggered UI! <span role="img">😎</span>
</span>
</section>
</>
)
}
export default App

@ -0,0 +1,45 @@
// Copyright © 2019 NAME HERE <EMAIL ADDRESS>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"github.com/imdotdev/im.dev/server/internal/storage"
"github.com/imdotdev/im.dev/server/pkg/config"
"github.com/imdotdev/im.dev/server/pkg/log"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var resetNavbarCmd = &cobra.Command{
Use: "resetNav",
Short: "reset navbars to home,tags,search",
Long: ``,
// Uncomment the following line if your bare application
// has an action associated with it:
Run: func(cmd *cobra.Command, args []string) {
config.Init("config.yaml")
log.InitLogger(config.Data.Common.LogLevel)
err := storage.ResetNavbars()
if err != nil {
log.RootLogger.Crit("reset navbars error", "error", err)
} else {
log.RootLogger.Info("reset navbars successfully")
}
},
}
func init() {
rootCmd.AddCommand(resetNavbarCmd)
}

@ -0,0 +1,75 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/org"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
)
func CreateOrg(c *gin.Context) {
o := &models.User{}
c.Bind(&o)
exist, err := user.NameExist(o.Username)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
if exist {
c.JSON(http.StatusConflict, common.RespError(e.AlreadyExist))
return
}
u := user.CurrentUser(c)
err = org.Create(o, u.ID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func UpdateOrg(c *gin.Context) {
}
func GetOrgByUserID(c *gin.Context) {
userID := c.Param("userID")
if userID == "0" {
u := user.CurrentUser(c)
if u == nil {
c.JSON(http.StatusBadRequest, common.RespError(e.BadRequest))
return
}
userID = u.ID
}
orgs, err := org.GetOrgByUserID(userID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(orgs))
}
func GetOrgMembers(c *gin.Context) {
orgID := c.Param("id")
u := user.CurrentUser(c)
users, err := org.GetMembers(u, orgID)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(users))
}

@ -2,8 +2,10 @@ package api
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/interaction"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e"
@ -55,6 +57,10 @@ func GetUser(c *gin.Context) {
return
}
u := user.CurrentUser(c)
if u != nil {
userDetail.Followed = interaction.GetFollowed(userDetail.ID, u.ID)
}
c.JSON(http.StatusOK, common.RespSuccess(userDetail))
}
@ -82,7 +88,7 @@ func GetSession(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(sess))
}
func SubmitNavbar(c *gin.Context) {
func SubmitUserNavbar(c *gin.Context) {
nav := &models.Navbar{}
err := c.Bind(&nav)
if err != nil || !models.ValidNavbarType(nav.Type) {
@ -107,7 +113,7 @@ func SubmitNavbar(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func GetNavbars(c *gin.Context) {
func GetUserNavbars(c *gin.Context) {
userID := c.Param("userID")
if userID == "0" {
@ -124,7 +130,7 @@ func GetNavbars(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(navbars))
}
func DeleteNavbar(c *gin.Context) {
func DeleteUserNavbar(c *gin.Context) {
id := c.Param("id")
u := user.CurrentUser(c)
@ -136,3 +142,19 @@ func DeleteNavbar(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func NameExist(c *gin.Context) {
name := c.Param("name")
if strings.TrimSpace(name) == "" {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
exist, err := user.NameExist(name)
if err != nil {
c.JSON(err.Status, common.RespError(err.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(exist))
}

@ -1,6 +1,7 @@
package cache
import (
"database/sql"
"time"
"github.com/imdotdev/im.dev/server/internal/interaction"
@ -33,7 +34,7 @@ func Init() {
}
err = db.Conn.QueryRow("SELECT tagline from user_profile WHERE id=?", user.ID).Scan(&user.Tagline)
if err != nil {
if err != nil && err != sql.ErrNoRows {
logger.Warn("query user profile error", "error", err)
}

@ -34,7 +34,6 @@ func Follow(targetID string, userId string) *e.Error {
}
if followed {
// 已经喜欢过该篇文章,更改为不喜欢
_, err := tx.Exec("DELETE FROM follows WHERE user_id=? and target_id=?", userId, targetID)
if err != nil {
return e.New(http.StatusInternalServerError, e.Internal)
@ -137,6 +136,7 @@ func GetFollowers(targetID, targetType string) ([]*models.User, *e.Error) {
if ok {
users = append(users, u)
u.Followed = GetFollowed(u.ID, targetID)
u.Follows = GetFollows(u.ID)
}
}

@ -2,4 +2,4 @@ package interaction
import "github.com/imdotdev/im.dev/server/pkg/log"
var logger = log.RootLogger.New("logger", "api")
var logger = log.RootLogger.New("logger", "interaction")

@ -0,0 +1,79 @@
package internal
import (
"net/http"
"sort"
"github.com/gin-gonic/gin"
"github.com/imdotdev/im.dev/server/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/models"
)
func GetNavbars(c *gin.Context) {
navbars := make(models.Navbars, 0)
rows, err := db.Conn.Query("SELECT id,label,value,weight FROM navbar")
if err != nil {
c.JSON(http.StatusInternalServerError, common.RespError(e.Internal))
return
}
for rows.Next() {
nv := &models.Navbar{}
rows.Scan(&nv.ID, &nv.Label, &nv.Value, &nv.Weight)
navbars = append(navbars, nv)
}
sort.Sort(navbars)
c.JSON(http.StatusOK, common.RespSuccess(navbars))
}
func SubmitNavbar(c *gin.Context) {
nav := &models.Navbar{}
c.Bind(&nav)
u := user.CurrentUser(c)
if !u.Role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
var err error
if nav.ID == 0 {
_, err = db.Conn.Exec("INSERT INTO navbar (label,value,weight) VALUES (?,?,?)",
nav.Label, nav.Value, nav.Weight)
} else {
_, err = db.Conn.Exec("UPDATE navbar SET label=?,value=?,weight=? WHERE id=?", nav.Label, nav.Value, nav.Weight, nav.ID)
}
if err != nil {
logger.Warn("submit navbar error", "error", err)
c.JSON(http.StatusInternalServerError, common.RespError(e.Internal))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}
func DeleteNavbar(c *gin.Context) {
u := user.CurrentUser(c)
if !u.Role.IsAdmin() {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
return
}
id := c.Param("id")
_, err := db.Conn.Exec("DELETE FROM navbar WHERE id=?", id)
if err != nil {
logger.Warn("delete navbar error", "error", err)
c.JSON(http.StatusInternalServerError, common.RespError(e.Internal))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}

@ -0,0 +1,102 @@
package org
import (
"database/sql"
"net/http"
"time"
"github.com/imdotdev/im.dev/server/internal/interaction"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/log"
"github.com/imdotdev/im.dev/server/pkg/models"
"github.com/imdotdev/im.dev/server/pkg/utils"
)
var logger = log.RootLogger.New("logger", "org")
func GetOrgByUserID(userID string) ([]*models.User, *e.Error) {
orgs := make([]*models.User, 0)
rows, err := db.Conn.Query("SELECT org_id,role FROM org_member WHERE user_id=?", userID)
if err != nil {
logger.Warn("get user orgs error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
for rows.Next() {
var oid, role string
rows.Scan(&oid, &role)
org, ok := models.UsersMapCache[oid]
if ok {
org.Role = models.RoleType(role)
orgs = append(orgs, org)
}
}
return orgs, nil
}
func Create(o *models.User, userID string) *e.Error {
o.ID = utils.GenID(models.IDTypeOrg)
tx, err := db.Conn.Begin()
if err != nil {
logger.Warn("start sql transaction error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
now := time.Now()
_, err = tx.Exec("INSERT INTO user (id,type,username,nickname,created,updated) VALUES (?,?,?,?,?,?)", o.ID, models.IDTypeOrg, o.Username, o.Nickname, now, now)
if err != nil {
logger.Warn("add org error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
_, err = tx.Exec("INSERT INTO org_member (org_id,user_id,role,created) VALUES (?,?,?,?)", o.ID, userID, models.ROLE_ADMIN, now)
if err != nil {
logger.Warn("add org member error", "error", err)
tx.Rollback()
return e.New(http.StatusInternalServerError, e.Internal)
}
tx.Commit()
return nil
}
func GetMembers(user *models.User, orgID string) ([]*models.User, *e.Error) {
rows, err := db.Conn.Query("SELECT user_id from org_member where org_id=?", orgID)
if err != nil {
logger.Warn("get org members error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
users := make([]*models.User, 0)
for rows.Next() {
var id string
rows.Scan(&id)
u, ok := models.UsersMapCache[id]
if ok {
users = append(users, u)
if user != nil {
u.Followed = interaction.GetFollowed(u.ID, user.ID)
u.Follows = interaction.GetFollows(u.ID)
}
}
}
return users, nil
}
func GetMemberCount(orgID string) int {
var count int
err := db.Conn.QueryRow("SELECT count(*) FROM org_member WHERE org_id=?", orgID).Scan(&count)
if err != nil && err != sql.ErrNoRows {
logger.Warn("get org member count error", "error", err)
}
return count
}

@ -84,9 +84,9 @@ func (s *Server) Start() error {
r.GET("/user/session", api.GetSession)
r.POST("/user/login", user.Login)
r.POST("/user/logout", user.Logout)
r.POST("/user/navbar", IsLogin(), api.SubmitNavbar)
r.GET("/user/navbars/:userID", api.GetNavbars)
r.DELETE("/user/navbar/:id", IsLogin(), api.DeleteNavbar)
r.POST("/user/navbar", IsLogin(), api.SubmitUserNavbar)
r.GET("/user/navbars/:userID", api.GetUserNavbars)
r.DELETE("/user/navbar/:id", IsLogin(), api.DeleteUserNavbar)
// interaction apis
r.POST("/interaction/like/:id", IsLogin(), api.Like)
r.POST("/interaction/follow/:id", IsLogin(), api.Follow)
@ -99,9 +99,16 @@ func (s *Server) Start() error {
r.GET("/search/posts/:filter", api.SearchPosts)
r.GET("/search/users/:filter", api.SearchUsers)
// org apis
r.POST("/org/create", IsLogin(), api.CreateOrg)
r.GET("/org/byUserID/:userID", api.GetOrgByUserID)
r.GET("/org/members/:id", api.GetOrgMembers)
// other apis
r.GET("/config", GetConfig)
r.GET("/navbars", GetNavbars)
r.POST("/navbar", IsLogin(), SubmitNavbar)
r.DELETE("/navbar/:id", IsLogin(), DeleteNavbar)
r.GET("/username/exist/:name", api.NameExist)
err := router.Run(config.Data.Server.Addr)
if err != nil {
logger.Crit("start backend server error", "error", err)

@ -40,6 +40,12 @@ func Init() error {
return nil
}
var navbars = []*models.Navbar{
&models.Navbar{Label: "主页", Value: "/", Weight: 0},
&models.Navbar{Label: "标签", Value: "/tags", Weight: 1},
&models.Navbar{Label: "Search", Value: "/search/posts", Weight: 2},
}
func initTables() error {
// create tables
for _, q := range sqlTables {
@ -56,13 +62,32 @@ func initTables() error {
}
now := time.Now()
_, err := db.Conn.Exec(`INSERT INTO user (id,username,email,role,nickname,avatar,created,updated) VALUES (?,?,?,?,?,?,?,?)`,
utils.GenID(models.IDTypeUser), config.Data.User.SuperAdminUsername, config.Data.User.SuperAdminEmail, models.ROLE_SUPER_ADMIN, "", "", now, now)
_, err := db.Conn.Exec(`INSERT INTO user (id,type,username,nickname,email,role,nickname,avatar,created,updated) VALUES (?,?,?,?,?,?,?,?,?,?)`,
utils.GenID(models.IDTypeUser), models.IDTypeUser, config.Data.User.SuperAdminUsername, "Admin", config.Data.User.SuperAdminEmail, models.ROLE_SUPER_ADMIN, "", "", now, now)
if err != nil {
log.RootLogger.Crit("init super admin error", "error:", err)
return err
}
// insert init navbars
err = initNavbars()
if err != nil {
log.RootLogger.Crit("init navbar error", "error:", err)
return err
}
return nil
}
func initNavbars() error {
for _, nv := range navbars {
_, err := db.Conn.Exec(`INSERT INTO navbar (label,value,weight) VALUES (?,?,?)`,
nv.Label, nv.Value, nv.Weight)
if err != nil {
return err
}
}
return nil
}

@ -58,3 +58,12 @@ func DropTables(names []string) {
log.RootLogger.Info("sql table dropped ok", "table_name", tbl)
}
}
func ResetNavbars() error {
err := connectDatabase()
if err != nil {
panic(err)
}
return initNavbars()
}

@ -2,15 +2,16 @@ package storage
var sqlTables = map[string]string{
"user": `CREATE TABLE IF NOT EXISTS user (
id VARCHAR(255) PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
nickname VARCHAR(255) DEFAULT '',
avatar VARCHAR(255) DEFAULT '',
email VARCHAR(255) UNIQUE NOT NULL,
role VARCHAR(20) NOT NULL,
id VARCHAR(255) PRIMARY KEY,
type VARCHAR(1) NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE,
nickname VARCHAR(255) NOT NULL,
avatar VARCHAR(255) DEFAULT '',
email VARCHAR(255) DEFAULT '',
role VARCHAR(20) DEFAULT '',
last_seen_at DATETIME DEFAULT CURRENT_DATETIME,
is_diabled BOOL NOT NULL DEFAULT 'false',
is_diabled BOOL NOT NULL DEFAULT 'false',
created DATETIME NOT NULL,
updated DATETIME NOT NULL
@ -40,7 +41,7 @@ var sqlTables = map[string]string{
stackoverflow VARCHAR(255),
updated DATETIME
);`,
);`,
"sessions": `CREATE TABLE IF NOT EXISTS sessions (
sid VARCHAR(255) primary key,
@ -48,6 +49,19 @@ var sqlTables = map[string]string{
);
`,
"org_member": `CREATE TABLE IF NOT EXISTS org_member (
org_id VARCHAR(255),
user_id VARCHAR(255),
role VARCHAR(20) NOT NULL,
created DATETIME NOT NULL
);
CREATE INDEX IF NOT EXISTS orgm_orgid
ON org_member (org_id);
CREATE INDEX IF NOT EXISTS orgm_userid
ON org_member (user_id);
`,
"story": `CREATE TABLE IF NOT EXISTS story (
id VARCHAR(255) PRIMARY KEY,
type VARCHAR(1) NOT NULL,
@ -193,9 +207,18 @@ var sqlTables = map[string]string{
label VARCHAR(20),
type TINYINT,
value VARCHAR(255),
weight TINYINT
weight TINYINT DEFAULT 0
);
CREATE INDEX IF NOT EXISTS user_navbar_userid
ON user_navbar (user_id);
`,
"navbar": `CREATE TABLE IF NOT EXISTS navbar (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label VARCHAR(20),
icon VARCHAR(40),
value VARCHAR(255),
weight TINYINT DEFAULT 0
);
`,
}

@ -77,7 +77,7 @@ func deleteSession(sid string) {
q := `DELETE FROM sessions WHERE sid=?`
_, err := db.Conn.Exec(q, sid)
if err != nil {
logger.Info("delete session error", "error", err)
logger.Warn("delete session error", "error", err)
}
}
@ -97,7 +97,6 @@ func CurrentUser(c *gin.Context) *models.User {
if createTime != 0 {
// check whether token is expired
if (time.Now().Unix() - createTime/1e9) > config.Data.User.SessionExpire {
deleteSession(token)
return nil
}
}
@ -117,7 +116,6 @@ func GetSession(c *gin.Context) *Session {
if createTime != 0 {
// check whether token is expired
if (time.Now().Unix() - createTime/1e9) > config.Data.User.SessionExpire {
deleteSession(token)
return nil
}
}

@ -7,6 +7,7 @@ import (
"time"
"github.com/imdotdev/im.dev/server/internal/interaction"
"github.com/imdotdev/im.dev/server/internal/org"
"github.com/imdotdev/im.dev/server/internal/tags"
"github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e"
@ -47,6 +48,9 @@ func GetUserDetail(id string, username string) (*models.User, *e.Error) {
user := &models.User{}
err := user.Query(id, username, "")
if err != nil {
if err == sql.ErrNoRows {
return nil, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("query user error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
@ -74,8 +78,11 @@ func GetUserDetail(id string, username string) (*models.User, *e.Error) {
user.Skills = skills
user.Follows = interaction.GetFollows(user.ID)
user.Followings = interaction.GetFollowings(user.ID, models.IDTypeUser)
if user.Type == models.IDTypeUser {
user.Followings = interaction.GetFollowings(user.ID, models.IDTypeUser)
} else {
user.Followings = org.GetMemberCount(user.ID)
}
return user, nil
}
@ -120,3 +127,18 @@ func UpdateUser(u *models.User) *e.Error {
return nil
}
func NameExist(name string) (bool, *e.Error) {
var username string
err := db.Conn.QueryRow("SELECT username FROM user WHERE username=?", name).Scan(&username)
if err != nil && err != sql.ErrNoRows {
logger.Warn("check name exist error", "error", err)
return false, e.New(http.StatusInternalServerError, e.Internal)
}
if err == sql.ErrNoRows {
return false, nil
}
return true, nil
}

@ -20,4 +20,6 @@ const (
ParamInvalid = "请求参数不正确"
NotFound = "目标不存在"
NoPermission = "你没有权限执行此操作"
BadRequest = "非法操作"
AlreadyExist = "目标已经存在"
)

@ -15,6 +15,7 @@ const (
IDTypePost = "4"
IDTypeSeries = "5"
IDTypeBook = "6"
IDTypeOrg = "7"
)
func GetIDType(id string) string {
@ -39,6 +40,8 @@ func GetIdTypeTable(id string) string {
return "user"
case IDTypeTag:
return "tags"
case IDTypeOrg:
return "user"
default:
return IDTypeUndefined
}
@ -77,7 +80,7 @@ func ValidStoryIDType(tp string) bool {
}
func ValidFollowIDType(tp string) bool {
if tp == IDTypeUser || tp == IDTypeTag {
if tp == IDTypeUser || tp == IDTypeTag || tp == IDTypeOrg {
return true
}

@ -8,6 +8,7 @@ import (
type User struct {
ID string `json:"id"`
Type string `json:"type"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
@ -30,9 +31,9 @@ type User struct {
Facebook string `json:"facebook,omitempty"`
Stackoverflow string `json:"stackoverflow,omitempty"`
Follows int `json:"follows,omitempty"`
Followings int `json:"followings,omitempty"`
Followed bool `json:"followed,omitempty"`
Follows int `json:"follows"`
Followings int `json:"followings"`
Followed bool `json:"followed"`
LastSeenAt time.Time `json:"lastSeenAt,omitempty"`
Created time.Time `json:"created"`
@ -47,8 +48,8 @@ func (ar Users) Less(i, j int) bool {
}
func (user *User) Query(id string, username string, email string) error {
err := db.Conn.QueryRow(`SELECT id,username,role,nickname,email,avatar,last_seen_at,created FROM user WHERE id=? or username=? or email=?`,
id, username, email).Scan(&user.ID, &user.Username, &user.Role, &user.Nickname, &user.Email, &user.Avatar, &user.LastSeenAt, &user.Created)
err := db.Conn.QueryRow(`SELECT id,type,username,role,nickname,email,avatar,last_seen_at,created FROM user WHERE id=? or username=? or email=?`,
id, username, email).Scan(&user.ID, &user.Type, &user.Username, &user.Role, &user.Nickname, &user.Email, &user.Avatar, &user.LastSeenAt, &user.Created)
if user.Avatar == "" {
user.Avatar = DefaultAvatar

@ -1,6 +1,6 @@
import { getSvgIcon } from 'components/svg-icon'
import React from 'react'
import { FaFileAlt, FaScroll, FaBookOpen, FaTags, FaUserCircle, FaRegFile, FaUser, FaRegUser } from 'react-icons/fa'
import { FaFileAlt, FaScroll, FaBookOpen, FaTags, FaUserCircle, FaRegFile, FaUser, FaRegUser, FaUserFriends } from 'react-icons/fa'
import { Route } from 'src/types/route'
import { SearchFilter } from 'src/types/search'
import { ReserveUrls } from './reserve-urls'
@ -63,7 +63,14 @@ export const adminLinks: Route[] = [{
path: `${ReserveUrls.Admin}/tags`,
icon: getSvgIcon("tags"),
disabled: false
}]
},
{
title: '菜单管理',
path: `${ReserveUrls.Admin}/navbar`,
icon: getSvgIcon("navbar"),
disabled: false
},
]
export const settingLinks: Route[] = [{
@ -78,6 +85,12 @@ export const settingLinks: Route[] = [{
icon: getSvgIcon("navbar"),
disabled: false
},
{
title: '组织管理',
path: `${ReserveUrls.Settings}/orgs`,
icon: <FaUserFriends />,
disabled: false
},
]

@ -5,5 +5,6 @@ export enum IDType {
User = "3",
Post = "4",
Series = "5",
Book = "6"
Book = "6",
Org = "7"
}

@ -0,0 +1,22 @@
export interface Org {
id: string
username: string
nickname: string
avatar?: string
cover?: string
email?: string
website?: string
location?: string
tagline?: string
about?: string
stack?: string
twitter?: string
github?: string
weibo?: string
created?: string
role?: string
}

@ -8,6 +8,7 @@ export interface Session {
export interface User {
// basic info
id: string
type?: string
username: string
nickname: string
avatar: string

@ -9,4 +9,6 @@ export function isUsernameChar(c) {
}
return false
}
}
export const usernameInvalidTips = "May only contain alphanumeric characters or single hyphens, and cannot begin or end with a hyphen."
Loading…
Cancel
Save