diff --git a/layouts/nav/nav.tsx b/layouts/nav/nav.tsx index 2bc2b47a..5e5bbca6 100644 --- a/layouts/nav/nav.tsx +++ b/layouts/nav/nav.tsx @@ -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() + 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() { - {navLinks.map(link => {link.title})} + {navs.map(link => {link.title})} diff --git a/layouts/nav/vertical-nav.tsx b/layouts/nav/vertical-nav.tsx index bd8a0aed..75f7ad9c 100644 --- a/layouts/nav/vertical-nav.tsx +++ b/layouts/nav/vertical-nav.tsx @@ -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() const [isLargerThan768] = useMediaQuery("(min-width: 768px)") useUpdateEffect(() => { @@ -60,10 +72,11 @@ import { navLinks } from "src/data/links" - {navLinks.map(link => + {navs.map(link => - - {link.icon}{link.title} + + {/* {link.icon} */} + {link.title} )} diff --git a/layouts/sidebar/sidebar-link.tsx b/layouts/sidebar/sidebar-link.tsx index 64a11a7b..1359d289 100644 --- a/layouts/sidebar/sidebar-link.tsx +++ b/layouts/sidebar/sidebar-link.tsx @@ -28,7 +28,10 @@ const StyledLink = React.forwardRef(function StyledLink( fontWeight: "600", }} {...rest} - >{icon && {icon} }{children} + > + {icon && {icon} } + {children} + ) }) diff --git a/next-redirect.js b/next-redirect.js index a0b19089..56aa8720 100644 --- a/next-redirect.js +++ b/next-redirect.js @@ -1,10 +1,10 @@ async function redirect() { return [ - // { - // source: "/search", - // destination: "/search/posts", - // permanent: true, - // } + { + source: "/search", + destination: "/search/posts", + permanent: true, + } ] } diff --git a/pages/[username]/index.tsx b/pages/[username]/index.tsx index 5725124d..e49997b0 100644 --- a/pages/[username]/index.tsx +++ b/pages/[username]/index.tsx @@ -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 = () => { - + {user.nickname} {user.tagline && {user.tagline}} @@ -136,8 +140,8 @@ const UserPage = () => { { - navbars.map(nv => - + navbars.map((nv,i) => + {nv.label} @@ -148,7 +152,7 @@ const UserPage = () => { {session?.user.id === user.id ? - : } + : } @@ -163,8 +167,8 @@ const UserPage = () => { - Following - viewFollowers(1)}> + {user.type === IDType.User ? "Following" : "Members"} + viewFollowers(1) : () => viewFollowers(2)}> diff --git a/pages/admin/navbar.tsx b/pages/admin/navbar.tsx new file mode 100644 index 00000000..90e0f586 --- /dev/null +++ b/pages/admin/navbar.tsx @@ -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 ( + <> + + + + + + 菜单设置 + + + + + + + + + + + + + { + navbars.map((nv,i) => + + + + + ) + } + + +
LabelValueWeight
{nv.label}{nv.value}{nv.weight} + onEditNavbar(nv)}/> + onDeleteNavbar(nv.id)} /> +
+
+
+
+ + + + {currentNavbar && + {currentNavbar.label ? "编辑菜单项" : "新建菜单项"} + + + + Label + { currentNavbar.label = e.currentTarget.value; onNavbarChange() }}> + + + + Value + { currentNavbar.value = e.currentTarget.value; onNavbarChange() }} placeholder="enter a url, e.g /search"/> + + + + Weight + { currentNavbar.weight = parseInt(e); onNavbarChange() }}> + + + + + + + + + + + } + + + ) +} +export default UserNavbarPage + diff --git a/pages/settings/orgs.tsx b/pages/settings/orgs.tsx new file mode 100644 index 00000000..57aaa662 --- /dev/null +++ b/pages/settings/orgs.tsx @@ -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 ( + <> + + + + + + 组织管理 + + + + } alignItems="left"> + { + orgs.map(o => + + + + {o.nickname} + {isAdmin(o.role) ? 'admin' : 'member'} + + + + + ) + } + + + + + + + + + { + 新建组织 + + + {(props) => ( +
+ + + {({ field, form }) => ( + + Username + + {form.errors.username} + + )} + + + {({ field, form }) => ( + + Nickname + + {form.errors.nickname} + + )} + + + + + + +
+ )} +
+
+
} +
+ + ) +} +export default UserOrgsPage + diff --git a/pages/test.tsx b/pages/test.tsx deleted file mode 100644 index 16de7a32..00000000 --- a/pages/test.tsx +++ /dev/null @@ -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 ( - <> - -
-