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

@ -11,7 +11,7 @@ import {
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import { useViewportScroll } from "framer-motion" import { useViewportScroll } from "framer-motion"
import NextLink from "next/link" import NextLink from "next/link"
import React from "react" import React, { useEffect, useState } from "react"
import { FaGithub } from "react-icons/fa" import { FaGithub } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo" import Logo, { LogoIcon } from "src/components/logo"
import { MobileNavButton, MobileNavContent } from "./mobile-nav" import { MobileNavButton, MobileNavContent } from "./mobile-nav"
@ -23,16 +23,31 @@ import DarkMode from "components/dark-mode"
import AccountMenu from "components/user-menu" import AccountMenu from "components/user-menu"
import { navLinks } from "src/data/links" import { navLinks } from "src/data/links"
import { getSvgIcon } from "components/svg-icon" import { getSvgIcon } from "components/svg-icon"
import { requestApi } from "utils/axios/request"
function HeaderContent() { function HeaderContent() {
const router = useRouter() const router = useRouter()
const { asPath } = router const { asPath } = router
const mobileNav = useDisclosure() const mobileNav = useDisclosure()
const mobileNavBtnRef = React.useRef<HTMLButtonElement>() 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(() => { useUpdateEffect(() => {
mobileNavBtnRef.current?.focus() mobileNavBtnRef.current?.focus()
}, [mobileNav.isOpen]) }, [mobileNav.isOpen])
@ -54,7 +69,7 @@ function HeaderContent() {
</NextLink> </NextLink>
<HStack ml={{ base: 1, md: 4, lg: 12 }} fontSize="1rem"> <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> </HStack>
</Flex> </Flex>

@ -14,7 +14,7 @@ import {
import siteConfig from "configs/site-config" import siteConfig from "configs/site-config"
import { useViewportScroll } from "framer-motion" import { useViewportScroll } from "framer-motion"
import NextLink from "next/link" import NextLink from "next/link"
import React from "react" import React, { useEffect, useState } from "react"
import { FaGithub, FaSearch } from "react-icons/fa" import { FaGithub, FaSearch } from "react-icons/fa"
import Logo, { LogoIcon } from "src/components/logo" import Logo, { LogoIcon } from "src/components/logo"
import { MobileNavButton, MobileNavContent } from "./mobile-nav" import { MobileNavButton, MobileNavContent } from "./mobile-nav"
@ -26,6 +26,7 @@ import {
import AccountMenu from "components/user-menu" import AccountMenu from "components/user-menu"
import { getSvgIcon } from "components/svg-icon" import { getSvgIcon } from "components/svg-icon"
import { navLinks } from "src/data/links" 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 { asPath } = router
const mobileNav = useDisclosure() 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 mobileNavBtnRef = React.useRef<HTMLButtonElement>()
const [isLargerThan768] = useMediaQuery("(min-width: 768px)") const [isLargerThan768] = useMediaQuery("(min-width: 768px)")
useUpdateEffect(() => { useUpdateEffect(() => {
@ -60,10 +72,11 @@ import { navLinks } from "src/data/links"
</NextLink> </NextLink>
<VStack pt="6" ml={{ base: 1, md: 4, lg: 12 }} fontSize="1rem" alignItems="left"> <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}> <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", }} > <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> {/* <Box width="25px">{link.icon}</Box> */}
<Text fontWeight="600">{link.title}</Text>
</HStack> </HStack>
</Link> </Link>
)} )}

@ -28,7 +28,10 @@ const StyledLink = React.forwardRef(function StyledLink(
fontWeight: "600", fontWeight: "600",
}} }}
{...rest} {...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() { async function redirect() {
return [ return [
// { {
// source: "/search", source: "/search",
// destination: "/search/posts", destination: "/search/posts",
// permanent: true, permanent: true,
// } }
] ]
} }

@ -22,6 +22,7 @@ import { IDType } from "src/types/id"
import UserCard from "components/users/user-card" import UserCard from "components/users/user-card"
import userCustomTheme from "theme/user-custom" import userCustomTheme from "theme/user-custom"
import SearchFilters from "components/search-filters" import SearchFilters from "components/search-filters"
import Follow from "components/interaction/follow"
const UserPage = () => { const UserPage = () => {
const { isOpen, onOpen, onClose } = useDisclosure() const { isOpen, onOpen, onClose } = useDisclosure()
@ -90,7 +91,7 @@ const UserPage = () => {
let res let res
if (tp === 1) { if (tp === 1) {
// followings // 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 = [] const ids = []
for (const f of res0.data) { for (const f of res0.data) {
ids.push(f.id) ids.push(f.id)
@ -98,9 +99,12 @@ const UserPage = () => {
res = await requestApi.post(`/user/ids`, ids) res = await requestApi.post(`/user/ids`, ids)
} else { } else if (tp === 0) {
// followers // 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) setFollowers(res.data)
if (res.data.length > 0) { if (res.data.length > 0) {
@ -123,7 +127,7 @@ const UserPage = () => {
<Box alignItems="left" pb="6"> <Box alignItems="left" pb="6">
<Card p="0" borderTop="none"> <Card p="0" borderTop="none">
<Box backgroundImage={`url(${user.cover})`} height="300px" width="100%" backgroundSize="cover" backgroundPosition="center" /> <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}`} /> <Image src={user.avatar} height="130px" borderRadius="50%" border={`4px solid ${borderColor}`} />
<Heading size="lg">{user.nickname}</Heading> <Heading size="lg">{user.nickname}</Heading>
{user.tagline && <Text layerStyle="textSecondary" fontWeight="450" fontSize="1.2rem" ml="1" mt="2">{user.tagline}</Text>} {user.tagline && <Text layerStyle="textSecondary" fontWeight="450" fontSize="1.2rem" ml="1" mt="2">{user.tagline}</Text>}
@ -136,8 +140,8 @@ const UserPage = () => {
</Link> </Link>
{ {
navbars.map(nv => navbars.map((nv,i) =>
<Link href={nv.type === NavbarType.Link ? nv.value : `${ReserveUrls.Series}/${nv.value}`}> <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"}> <Box cursor="pointer" fontWeight={isSubNavActive('react') ? "bold" : "550"} layerStyle={isSubNavActive('react') ? null : "textSecondary"}>
{nv.label} {nv.label}
</Box> </Box>
@ -148,7 +152,7 @@ const UserPage = () => {
</HStack> </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> <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> </VStack>
@ -163,8 +167,8 @@ const UserPage = () => {
</Box> </Box>
<Box width="50%"> <Box width="50%">
<Heading size="sm">Following</Heading> <Heading size="sm">{user.type === IDType.User ? "Following" : "Members"}</Heading>
<Text mt="1" cursor="pointer" onClick={() => viewFollowers(1)}><Count count={user.followings ?? 0} /></Text> <Text mt="1" cursor="pointer" onClick={user.type === IDType.User ? () => viewFollowers(1) : () => viewFollowers(2)}><Count count={user.followings ?? 0} /></Text>
</Box> </Box>
</Flex> </Flex>
</Card> </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 ( import (
"net/http" "net/http"
"strings"
"github.com/gin-gonic/gin" "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/internal/user"
"github.com/imdotdev/im.dev/server/pkg/common" "github.com/imdotdev/im.dev/server/pkg/common"
"github.com/imdotdev/im.dev/server/pkg/e" "github.com/imdotdev/im.dev/server/pkg/e"
@ -55,6 +57,10 @@ func GetUser(c *gin.Context) {
return return
} }
u := user.CurrentUser(c)
if u != nil {
userDetail.Followed = interaction.GetFollowed(userDetail.ID, u.ID)
}
c.JSON(http.StatusOK, common.RespSuccess(userDetail)) c.JSON(http.StatusOK, common.RespSuccess(userDetail))
} }
@ -82,7 +88,7 @@ func GetSession(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(sess)) c.JSON(http.StatusOK, common.RespSuccess(sess))
} }
func SubmitNavbar(c *gin.Context) { func SubmitUserNavbar(c *gin.Context) {
nav := &models.Navbar{} nav := &models.Navbar{}
err := c.Bind(&nav) err := c.Bind(&nav)
if err != nil || !models.ValidNavbarType(nav.Type) { if err != nil || !models.ValidNavbarType(nav.Type) {
@ -107,7 +113,7 @@ func SubmitNavbar(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil)) c.JSON(http.StatusOK, common.RespSuccess(nil))
} }
func GetNavbars(c *gin.Context) { func GetUserNavbars(c *gin.Context) {
userID := c.Param("userID") userID := c.Param("userID")
if userID == "0" { if userID == "0" {
@ -124,7 +130,7 @@ func GetNavbars(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(navbars)) c.JSON(http.StatusOK, common.RespSuccess(navbars))
} }
func DeleteNavbar(c *gin.Context) { func DeleteUserNavbar(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
u := user.CurrentUser(c) u := user.CurrentUser(c)
@ -136,3 +142,19 @@ func DeleteNavbar(c *gin.Context) {
c.JSON(http.StatusOK, common.RespSuccess(nil)) 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 package cache
import ( import (
"database/sql"
"time" "time"
"github.com/imdotdev/im.dev/server/internal/interaction" "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) 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) logger.Warn("query user profile error", "error", err)
} }

@ -34,7 +34,6 @@ func Follow(targetID string, userId string) *e.Error {
} }
if followed { if followed {
// 已经喜欢过该篇文章,更改为不喜欢
_, err := tx.Exec("DELETE FROM follows WHERE user_id=? and target_id=?", userId, targetID) _, err := tx.Exec("DELETE FROM follows WHERE user_id=? and target_id=?", userId, targetID)
if err != nil { if err != nil {
return e.New(http.StatusInternalServerError, e.Internal) return e.New(http.StatusInternalServerError, e.Internal)
@ -137,6 +136,7 @@ func GetFollowers(targetID, targetType string) ([]*models.User, *e.Error) {
if ok { if ok {
users = append(users, u) users = append(users, u)
u.Followed = GetFollowed(u.ID, targetID) 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" 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.GET("/user/session", api.GetSession)
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.SubmitUserNavbar)
r.GET("/user/navbars/:userID", api.GetNavbars) r.GET("/user/navbars/:userID", api.GetUserNavbars)
r.DELETE("/user/navbar/:id", IsLogin(), api.DeleteNavbar) r.DELETE("/user/navbar/:id", IsLogin(), api.DeleteUserNavbar)
// 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)
@ -99,9 +99,16 @@ func (s *Server) Start() error {
r.GET("/search/posts/:filter", api.SearchPosts) r.GET("/search/posts/:filter", api.SearchPosts)
r.GET("/search/users/:filter", api.SearchUsers) 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 // other apis
r.GET("/config", GetConfig) 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) err := router.Run(config.Data.Server.Addr)
if err != nil { if err != nil {
logger.Crit("start backend server error", "error", err) logger.Crit("start backend server error", "error", err)

@ -40,6 +40,12 @@ func Init() error {
return nil 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 { func initTables() error {
// create tables // create tables
for _, q := range sqlTables { for _, q := range sqlTables {
@ -56,13 +62,32 @@ func initTables() error {
} }
now := time.Now() now := time.Now()
_, err := db.Conn.Exec(`INSERT INTO user (id,username,email,role,nickname,avatar,created,updated) VALUES (?,?,?,?,?,?,?,?)`, _, err := db.Conn.Exec(`INSERT INTO user (id,type,username,nickname,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) utils.GenID(models.IDTypeUser), models.IDTypeUser, config.Data.User.SuperAdminUsername, "Admin", config.Data.User.SuperAdminEmail, models.ROLE_SUPER_ADMIN, "", "", now, now)
if err != nil { if err != nil {
log.RootLogger.Crit("init super admin error", "error:", err) log.RootLogger.Crit("init super admin error", "error:", err)
return 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 return nil
} }

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

@ -3,11 +3,12 @@ package storage
var sqlTables = map[string]string{ var sqlTables = map[string]string{
"user": `CREATE TABLE IF NOT EXISTS user ( "user": `CREATE TABLE IF NOT EXISTS user (
id VARCHAR(255) PRIMARY KEY, id VARCHAR(255) PRIMARY KEY,
type VARCHAR(1) NOT NULL,
username VARCHAR(255) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE,
nickname VARCHAR(255) DEFAULT '', nickname VARCHAR(255) NOT NULL,
avatar VARCHAR(255) DEFAULT '', avatar VARCHAR(255) DEFAULT '',
email VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) DEFAULT '',
role VARCHAR(20) NOT NULL, role VARCHAR(20) DEFAULT '',
last_seen_at DATETIME DEFAULT CURRENT_DATETIME, last_seen_at DATETIME DEFAULT CURRENT_DATETIME,
is_diabled BOOL NOT NULL DEFAULT 'false', is_diabled BOOL NOT NULL DEFAULT 'false',
@ -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 ( "story": `CREATE TABLE IF NOT EXISTS story (
id VARCHAR(255) PRIMARY KEY, id VARCHAR(255) PRIMARY KEY,
type VARCHAR(1) NOT NULL, type VARCHAR(1) NOT NULL,
@ -193,9 +207,18 @@ var sqlTables = map[string]string{
label VARCHAR(20), label VARCHAR(20),
type TINYINT, type TINYINT,
value VARCHAR(255), value VARCHAR(255),
weight TINYINT weight TINYINT DEFAULT 0
); );
CREATE INDEX IF NOT EXISTS user_navbar_userid CREATE INDEX IF NOT EXISTS user_navbar_userid
ON user_navbar (user_id); 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=?` q := `DELETE FROM sessions WHERE sid=?`
_, err := db.Conn.Exec(q, sid) _, err := db.Conn.Exec(q, sid)
if err != nil { 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 { if createTime != 0 {
// check whether token is expired // check whether token is expired
if (time.Now().Unix() - createTime/1e9) > config.Data.User.SessionExpire { if (time.Now().Unix() - createTime/1e9) > config.Data.User.SessionExpire {
deleteSession(token)
return nil return nil
} }
} }
@ -117,7 +116,6 @@ func GetSession(c *gin.Context) *Session {
if createTime != 0 { if createTime != 0 {
// check whether token is expired // check whether token is expired
if (time.Now().Unix() - createTime/1e9) > config.Data.User.SessionExpire { if (time.Now().Unix() - createTime/1e9) > config.Data.User.SessionExpire {
deleteSession(token)
return nil return nil
} }
} }

@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/imdotdev/im.dev/server/internal/interaction" "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/internal/tags"
"github.com/imdotdev/im.dev/server/pkg/db" "github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e" "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{} user := &models.User{}
err := user.Query(id, username, "") err := user.Query(id, username, "")
if err != nil { if err != nil {
if err == sql.ErrNoRows {
return nil, e.New(http.StatusNotFound, e.NotFound)
}
logger.Warn("query user error", "error", err) logger.Warn("query user error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal) 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.Skills = skills
user.Follows = interaction.GetFollows(user.ID) user.Follows = interaction.GetFollows(user.ID)
if user.Type == models.IDTypeUser {
user.Followings = interaction.GetFollowings(user.ID, models.IDTypeUser) user.Followings = interaction.GetFollowings(user.ID, models.IDTypeUser)
} else {
user.Followings = org.GetMemberCount(user.ID)
}
return user, nil return user, nil
} }
@ -120,3 +127,18 @@ func UpdateUser(u *models.User) *e.Error {
return nil 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 = "请求参数不正确" ParamInvalid = "请求参数不正确"
NotFound = "目标不存在" NotFound = "目标不存在"
NoPermission = "你没有权限执行此操作" NoPermission = "你没有权限执行此操作"
BadRequest = "非法操作"
AlreadyExist = "目标已经存在"
) )

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

@ -8,6 +8,7 @@ import (
type User struct { type User struct {
ID string `json:"id"` ID string `json:"id"`
Type string `json:"type"`
Username string `json:"username"` Username string `json:"username"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
@ -30,9 +31,9 @@ type User struct {
Facebook string `json:"facebook,omitempty"` Facebook string `json:"facebook,omitempty"`
Stackoverflow string `json:"stackoverflow,omitempty"` Stackoverflow string `json:"stackoverflow,omitempty"`
Follows int `json:"follows,omitempty"` Follows int `json:"follows"`
Followings int `json:"followings,omitempty"` Followings int `json:"followings"`
Followed bool `json:"followed,omitempty"` Followed bool `json:"followed"`
LastSeenAt time.Time `json:"lastSeenAt,omitempty"` LastSeenAt time.Time `json:"lastSeenAt,omitempty"`
Created time.Time `json:"created"` 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 { 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=?`, 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.Username, &user.Role, &user.Nickname, &user.Email, &user.Avatar, &user.LastSeenAt, &user.Created) 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 == "" { if user.Avatar == "" {
user.Avatar = DefaultAvatar user.Avatar = DefaultAvatar

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

@ -5,5 +5,6 @@ export enum IDType {
User = "3", User = "3",
Post = "4", Post = "4",
Series = "5", 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 { export interface User {
// basic info // basic info
id: string id: string
type?: string
username: string username: string
nickname: string nickname: string
avatar: string avatar: string

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