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

@ -21,11 +21,13 @@ import { Tag } from "src/types/tag"
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"
const UserPage = () => {
const { isOpen, onOpen, onClose } = useDisclosure()
const router = useRouter()
const username = router.query.username
const nav = router.query.nav
const session = useSession()
const [user, setUser]: [User, any] = useState(null)
const [rawPosts, setRawPosts]: [Story[], any] = useState([])
@ -100,6 +102,9 @@ const UserPage = () => {
}
}
const isSubNavActive = (id) => {
return id === nav
}
return (
<>
<SEO
@ -112,22 +117,47 @@ 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="200px" position="relative" top="-70px" spacing="3">
<VStack maxHeight="205px" 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>}
<Flex layerStyle="textSecondary" spacing="2" pt="1" alignItems="center">
{/* <chakra.span><FaHeart /></chakra.span> */}
<chakra.span cursor="pointer" onClick={() => viewFollowers(0)}>Followers <chakra.a fontWeight="600"><Count count={user.follows} /></chakra.a></chakra.span>
{/* <chakra.span ml="5"><FaStar /></chakra.span> */}
<chakra.span ml="3" cursor="pointer" onClick={() => viewFollowers(1)}>Following <chakra.a fontWeight="600"><Count count={user.followings??0} /></chakra.a></chakra.span>
</Flex>
<HStack pt="3" spacing="5">
<Link href={`/${username}`}>
<Box cursor="pointer" fontWeight={isSubNavActive(undefined) ? "bold" : "550"} layerStyle={isSubNavActive(undefined) ? null : "textSecondary"}>
HOME
</Box>
</Link>
<Link href={`/${username}?nav=react`}>
<Box cursor="pointer" fontWeight={isSubNavActive('react') ? "bold" : "550"} layerStyle={isSubNavActive('react') ? null : "textSecondary"}>
REACT
</Box>
</Link>
</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>
</VStack>
</Card>
<HStack spacing={[0, 0, 4, 4]} mt="4" alignItems="top">
<VStack alignItems="left" spacing="4" width="350px" display={{ base: "none", md: "flex" }}>
<Card>
<Flex layerStyle="textSecondary" spacing="2" pt="1" alignItems="center">
<Box width="50%">
<Heading size="sm">Followers</Heading>
<Text mt="1" cursor="pointer" onClick={() => viewFollowers(0)}><Count count={user.follows} /></Text>
</Box>
<Box width="50%">
<Heading size="sm">Following</Heading>
<Text mt="1" cursor="pointer" onClick={() => viewFollowers(1)}><Count count={user.followings ?? 0} /></Text>
</Box>
</Flex>
</Card>
<Card>
{user.about &&
<>
@ -184,8 +214,8 @@ const UserPage = () => {
<Wrap mt="4" p="1">
{
tags.map(tag =>
<Button size="sm" variant="ghost" p="0" onClick={() => filterPostsByTag(tag)} _focus={null}>
<Box className={tagFilter?.id === tag.id ? "tag-bg" : null} py="2" px="3">{tag.name} &nbsp; {tag.posts}</Box>
<Button key={tag.id} size="sm" variant="text" p="0" onClick={() => filterPostsByTag(tag)} _focus={null}>
<Box className={tagFilter?.id === tag.id ? "tag-bg" : null} py="2" px="1">{tag.name} &nbsp; {tag.posts}</Box>
</Button>
)
}
@ -201,8 +231,8 @@ const UserPage = () => {
<Empty />
</Card>
:
<Card width="100%" height="fit-content" p="0" px="3">
<Stories stories={posts} showFooter={tagFilter === null} showPinned={true}/>
<Card width="100%" height="fit-content" p="0">
<Stories stories={posts} showFooter={tagFilter === null} showPinned={true} />
</Card>
}
</Box>

@ -37,7 +37,7 @@ const FollowersPage = () => {
<VStack alignItems="left" divider={<StackDivider borderColor={borderColor} />} >
{users.map(user =>
<UserCard user={user} />
<UserCard key={user.id} user={user} />
)}
</VStack>
}

@ -53,7 +53,7 @@ const TagsPage = () => {
<Wrap spacing="10px">
{tags.map(tag =>
<WrapItem width={["100%","100%","100%","31%"]}><FollowingTag key={tag.id} tag={tag} weight={getTagWeight(tag)} /> </WrapItem>
<WrapItem key={tag.id} width={["100%","100%","100%","31%"]}><FollowingTag tag={tag} weight={getTagWeight(tag)} /> </WrapItem>
)}

@ -44,7 +44,7 @@ const UsersPage = () => {
<VStack alignItems="left" divider={<StackDivider borderColor={borderColor} />} >
{users.map(user =>
<UserCard user={user} />
<UserCard key={user.id} user={user} />
)}
</VStack>
}

@ -0,0 +1,112 @@
import { Text, Box, Heading, Image, Center, Button, Flex, VStack, Divider, useToast, FormControl, FormLabel, FormHelperText, Input, FormErrorMessage, HStack, Wrap, useMediaQuery, Avatar, Textarea, } from "@chakra-ui/react"
import Card from "components/card"
import Nav from "layouts/nav/nav"
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 { useRouter } from "next/router"
import { Field, Form, Formik } from "formik"
import { config } from "configs/config"
import Tags from "components/tags/tags"
var validator = require('validator');
const UserNavbarPage = () => {
const [user, setUser] = useState(null)
const [skills, setSkills] = useState([])
const [isLargerThan1280] = useMediaQuery("(min-width: 768px)")
useEffect(() => {
requestApi.get("/user/self").then(res => setUser(res.data))
}, [])
const router = useRouter()
const toast = useToast()
const submitUser = async (values, _) => {
await requestApi.post(`/user/update`, values)
setUser(values)
toast({
description: "更新成功",
status: "success",
duration: 2000,
isClosable: true,
})
}
function validateNickname(value) {
let error
if (!value?.trim()) {
error = "昵称不能为空"
}
if (value?.length > config.user.nicknameMaxLen) {
error = `长度不能超过${config.user.nicknameMaxLen}`
}
return error
}
function validateEmail(value) {
let email = value?.trim()
let error
if (email?.length > config.user.usernameMaxLen) {
error = `长度不能超过${config.user.usernameMaxLen}`
return error
}
if (email) {
if (!validator.isEmail(email)) {
error = "Email格式不合法"
return error
}
}
return error
}
function validateUrl(value, canBeEmpty = true) {
let url = value?.trim()
let error
if (!canBeEmpty) {
if (!url) {
error = "url不能为空"
return error
}
}
if (url) {
if (!validator.isURL(value)) {
error = "URL格式不合法"
return error
}
}
return error
}
function validateLen(value) {
let error
if (value?.length > config.commonMaxlen) {
error = `长度不能超过${config.commonMaxlen}`
}
return error
}
const Layout = isLargerThan1280 ? HStack : VStack
return (
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={settingLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="博客设置" />
{user && <Card ml="4" width="100%">
<Heading></Heading>
</Card>}
</Box>
</PageContainer>
</>
)
}
export default UserNavbarPage

@ -99,7 +99,7 @@ const UserProfilePage = () => {
<>
<PageContainer>
<Box display="flex">
<Sidebar routes={settingLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="偏好设置" />
<Sidebar routes={settingLinks} width={["120px", "120px", "250px", "250px"]} height="fit-content" title="博客设置" />
{user && <VStack alignItems="left" ml="4" width="100%">
<Formik
initialValues={user}

@ -81,3 +81,28 @@ func GetSession(c *gin.Context) {
sess := user.GetSession(c)
c.JSON(http.StatusOK, common.RespSuccess(sess))
}
func SubmitNavbar(c *gin.Context) {
nav := &models.Navbar{}
err := c.Bind(&nav)
if err != nil || !models.ValidNavbarType(nav.Type) {
c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid))
return
}
u := user.CurrentUser(c)
if nav.Type == models.NavbarTypeSeries {
if !models.IsStoryCreator(u.ID, nav.Value) {
c.JSON(http.StatusForbidden, common.RespError(e.NoPermission))
}
}
nav.UserID = u.ID
err1 := user.AddNavbar(nav)
if err != nil {
c.JSON(err1.Status, common.RespError(err1.Message))
return
}
c.JSON(http.StatusOK, common.RespSuccess(nil))
}

@ -20,7 +20,7 @@ func Posts(user *models.User, filter, query string) []*models.Story {
// search by title
sqlq := "%" + query + "%"
rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,creator,created,updated from story where status=? and (title LIKE ? or brief LIKE ?)", models.StatusPublished, sqlq, sqlq)
rows, err := db.Conn.Query("select id,type,slug,title,url,cover,brief,creator,created,updated from story where status=? and (title LIKE ? or brief LIKE ?)", models.StatusPublished, sqlq, sqlq)
if err != nil {
logger.Warn("get user posts error", "error", err)
return posts

@ -84,7 +84,7 @@ 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)
// interaction apis
r.POST("/interaction/like/:id", IsLogin(), api.Like)
r.POST("/interaction/follow/:id", IsLogin(), api.Follow)

@ -186,4 +186,15 @@ var sqlTables = map[string]string{
CREATE INDEX IF NOT EXISTS pin_targetid
ON pin (target_id);
`,
"user_navbar": `CREATE TABLE IF NOT EXISTS user_navbar (
user_id VARCHAR(255),
label VARCHAR(20),
type TINYINT,
value VARCHAR(255),
weight TINYINT
);
CREATE INDEX IF NOT EXISTS user_navbar_userid
ON user_navbar (user_id);
`,
}

@ -0,0 +1,40 @@
package user
import (
"net/http"
"sort"
"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 AddNavbar(nav *models.Navbar) *e.Error {
_, err := db.Conn.Exec("INSERT INTO user_navbar (user_id,label,type,value,weight) VALUES (?,?,?,?,?)",
nav.UserID, nav.Label, nav.Type, nav.Value, nav.Weight)
if err != nil {
logger.Warn("add user navbar error", "error", err)
return e.New(http.StatusInternalServerError, e.Internal)
}
return nil
}
func GetNavbars(userID string) (models.Navbars, *e.Error) {
rows, err := db.Conn.Query("SELECT label,type,value,weight FROM navbar WHERE user_id=?", userID)
if err != nil {
logger.Warn("get user navbar error", "error", err)
return nil, e.New(http.StatusInternalServerError, e.Internal)
}
navs := make(models.Navbars, 0)
for rows.Next() {
nav := &models.Navbar{}
rows.Scan(&nav.Label, &nav.Type, &nav.Value, &nav.Weight)
navs = append(navs, nav)
}
sort.Sort(navs)
return navs, nil
}

@ -0,0 +1,30 @@
package models
const (
NavbarTypeLink = 1
NavbarTypeSeries = 2
)
type Navbar struct {
UserID string `json:"userID"`
Label string `json:"label"`
Type int `json:"type"`
Value string `json:"value"`
Weight int `json:"weight"`
}
type Navbars []*Navbar
func (t Navbars) Len() int { return len(t) }
func (t Navbars) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t Navbars) Less(i, j int) bool {
return t[i].Weight > t[j].Weight
}
func ValidNavbarType(tp int) bool {
if tp == NavbarTypeLink || tp == NavbarTypeSeries {
return true
}
return false
}

@ -22,7 +22,7 @@ export const SearchFilters = (props:Props) => {
<HStack>
{
filters.map(f =>
<Button _focus={null} onClick={() => changeFilter(f)} size="sm" colorScheme={filter === f ? 'teal' : null} leftIcon={getSvgIcon(f)} variant="ghost" >
<Button key={f} _focus={null} onClick={() => changeFilter(f)} size="sm" colorScheme={filter === f ? 'teal' : null} leftIcon={getSvgIcon(f)} variant="ghost" >
{f}
</Button>)
}

@ -49,6 +49,9 @@ export function getSvgIcon(name, height = "1.4rem") {
case "edit":
svg = <svg height={height} 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>
break
case "navbar":
svg = <svg height={height} fill="currentColor" viewBox="0 0 448 512"><path d="M424 96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zM264 96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zM104 96H24c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96H32v-64h64v64zm328 96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zm-152-96h-80c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96h-64v-64h64v64zm-152-96H24c-13.22 0-24 10.77-24 24v80c0 13.23 10.78 24 24 24h80c13.22 0 24-10.77 24-24v-80c0-13.23-10.78-24-24-24zm-8 96H32v-64h64v64z"></path></svg>
break
default:
break;
}

@ -58,9 +58,9 @@ export const UserMenu = () => {
{isEditor(session.user.role) && <Link href={`${ReserveUrls.Editor}/posts`}><MenuItem icon={<FaEdit fontSize="16" />} ></MenuItem></Link>}
<Link href={`${ReserveUrls.Bookmarks}`}><MenuItem icon={<FaBookmark fontSize="16" />}></MenuItem></Link>
<Link href={`${ReserveUrls.Interaction}/following-tags`}><MenuItem icon={<FaHeart fontSize="16" />}></MenuItem></Link>
{isAdmin(session.user.role) && <Link href={`${ReserveUrls.Admin}/tags`}><MenuItem icon={<FaStar fontSize="16" />} ></MenuItem></Link>}
<MenuDivider />
<Link href={`${ReserveUrls.Settings}/profile`}><MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem></Link>
{isAdmin(session.user.role) && <Link href={`${ReserveUrls.Admin}/tags`}><MenuItem icon={<FaStar fontSize="16" />} ></MenuItem></Link>}
<Link href={`${ReserveUrls.Settings}/profile`}><MenuItem icon={<FaRegSun fontSize="16" />}></MenuItem></Link>
<MenuItem onClick={() => logout()} icon={<FaSignOutAlt fontSize="16" />}></MenuItem>
</MenuList>
</Menu> :

@ -40,11 +40,6 @@ export const interactionLinks: any[] = [
path: `${ReserveUrls.Interaction}/followers`,
disabled: false
},
{
title: 'Followers',
path: `${ReserveUrls.Interaction}/followers`,
disabled: false
},
]
export const searchLinks: any[] = [{
@ -72,11 +67,18 @@ export const adminLinks: Route[] = [{
export const settingLinks: Route[] = [{
title: '用户设置',
title: '用户信息',
path: `${ReserveUrls.Settings}/profile`,
icon: <FaUserCircle />,
disabled: false
}]
},
{
title: '博客菜单',
path: `${ReserveUrls.Settings}/navbar`,
icon: getSvgIcon("navbar"),
disabled: false
},
]
export const navLinks = [{

Loading…
Cancel
Save