From 633a1aea7ee660e3b546570196aafb0c62772f84 Mon Sep 17 00:00:00 2001 From: sunface Date: Mon, 8 Mar 2021 16:06:14 +0800 Subject: [PATCH] update --- layouts/nav/editor-nav.tsx | 2 +- layouts/nav/nav.tsx | 2 +- layouts/nav/post-nav.tsx | 11 ++- layouts/nav/vertical-nav.tsx | 3 +- layouts/sidebar/sidebar-link.tsx | 5 +- layouts/sidebar/sidebar.tsx | 8 +- package.json | 2 + pages/admin/tag/[id].tsx | 2 +- pages/index.tsx | 25 +++---- pages/search/[type].tsx | 42 ----------- pages/search/posts.tsx | 102 +++++++++++++++++++++++++ pages/search/users.tsx | 103 ++++++++++++++++++++++++++ server/internal/api/search.go | 40 ++++++++++ server/internal/cache/cache.go | 9 +++ server/internal/search/search.go | 56 ++++++++++++++ server/internal/server.go | 15 ++-- server/internal/story/posts.go | 10 +-- server/internal/user/session.go | 2 +- server/pkg/models/search.go | 16 ++++ server/pkg/models/user.go | 3 +- src/components/interaction/follow.tsx | 7 +- src/components/search-filters.tsx | 32 ++++++++ src/components/story/post-card.tsx | 30 ++++++-- src/components/story/posts.tsx | 6 +- src/components/svg-icon.tsx | 7 +- src/components/tags/tags.tsx | 2 +- src/components/users/user-card.tsx | 52 +++++++++++++ src/components/users/users.tsx | 32 ++++++++ src/data/links.tsx | 15 ++-- src/types/posts.ts | 8 +- src/types/search.ts | 6 ++ src/types/user.ts | 3 +- src/utils/url.ts | 39 +++++++++- theme.ts | 9 ++- yarn.lock | 51 ++++++++++++- 35 files changed, 643 insertions(+), 114 deletions(-) delete mode 100644 pages/search/[type].tsx create mode 100644 pages/search/posts.tsx create mode 100644 pages/search/users.tsx create mode 100644 server/internal/api/search.go create mode 100644 server/internal/search/search.go create mode 100644 server/pkg/models/search.go create mode 100644 src/components/search-filters.tsx create mode 100644 src/components/users/user-card.tsx create mode 100644 src/components/users/users.tsx create mode 100644 src/types/search.ts diff --git a/layouts/nav/editor-nav.tsx b/layouts/nav/editor-nav.tsx index f6201d24..62da05ca 100644 --- a/layouts/nav/editor-nav.tsx +++ b/layouts/nav/editor-nav.tsx @@ -124,7 +124,7 @@ function EditorNav(props) { pos="fixed" top="0" zIndex="3" - bg={useColorModeValue('white','gray.800')} + bg={useColorModeValue('gray.50','gray.800')} left="0" right="0" width="full" diff --git a/layouts/nav/nav.tsx b/layouts/nav/nav.tsx index 454fef10..2bc2b47a 100644 --- a/layouts/nav/nav.tsx +++ b/layouts/nav/nav.tsx @@ -128,7 +128,7 @@ function Header(props) { left="0" right="0" width="full" - bg={useColorModeValue('white', 'gray.800')} + bg={useColorModeValue('gray.50', 'gray.800')} {...props} > diff --git a/layouts/nav/post-nav.tsx b/layouts/nav/post-nav.tsx index e2388e71..0e0a12c5 100644 --- a/layouts/nav/post-nav.tsx +++ b/layouts/nav/post-nav.tsx @@ -34,13 +34,22 @@ interface Props { function PostNav(props: Props) { const { post } = props const [followed, setFollowed] = useState(null) - + const enterBodyBg = useColorModeValue('white',"#1A202C") + const leaveBodyBg = useColorModeValue('#F7FAFC',"#1A202C") useEffect(() => { if (post) { requestApi.get(`/interaction/followed/${post.creator.id}`).then(res => setFollowed(res.data)) } }, []) + useEffect(() => { + console.log(enterBodyBg) + document.body.style.backgroundColor = enterBodyBg + return () => { + document.body.style.backgroundColor = leaveBodyBg + } + }, [enterBodyBg]) + return ( diff --git a/layouts/sidebar/sidebar-link.tsx b/layouts/sidebar/sidebar-link.tsx index 38fdcb6e..7fceb910 100644 --- a/layouts/sidebar/sidebar-link.tsx +++ b/layouts/sidebar/sidebar-link.tsx @@ -35,10 +35,11 @@ const StyledLink = React.forwardRef(function StyledLink( type SidebarLinkProps = PropsOf & { href?: string icon?: React.ReactElement + query?: any } const SidebarLink = (props: SidebarLinkProps) => { - const { href, icon, children, ...rest } = props + const { href, icon, children,query, ...rest } = props const { asPath } = useRouter() const isActive = asPath.indexOf(href) > -1 @@ -51,7 +52,7 @@ const SidebarLink = (props: SidebarLinkProps) => { lineHeight="1.5rem" {...rest} > - + {children} diff --git a/layouts/sidebar/sidebar.tsx b/layouts/sidebar/sidebar.tsx index 8e8cb3ad..150db9b9 100644 --- a/layouts/sidebar/sidebar.tsx +++ b/layouts/sidebar/sidebar.tsx @@ -7,13 +7,13 @@ import SidebarLink from "./sidebar-link" export function SidebarContent(props) { - const { routes, pathname, contentRef } = props + const { routes, pathname, contentRef, query } = props return ( <> {routes.map((route: Route) => { if (route.disabled) { return null } - return + return {route.title} })} @@ -22,7 +22,7 @@ export function SidebarContent(props) { ) } -const Sidebar = ({ routes, title, ...props }) => { +const Sidebar = ({ routes, title,query=null, ...props }) => { const { pathname } = useRouter() const ref = React.useRef(null) @@ -45,7 +45,7 @@ const Sidebar = ({ routes, title, ...props }) => { flexShrink={0} // display={{ base: "none", md: "block" }} > - + diff --git a/package.json b/package.json index a6897e35..cf985b3b 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,10 @@ "moment": "^2.27.0", "next": "^10.0.4", "next-seo": "^4.17.0", + "query-string": "^6.3.0", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-highlight-words": "^0.17.0", "react-icons": "^4.1.0", "short-number": "^1.0.7", "validator": "^13.5.2" diff --git a/pages/admin/tag/[id].tsx b/pages/admin/tag/[id].tsx index 64850f00..f964233b 100644 --- a/pages/admin/tag/[id].tsx +++ b/pages/admin/tag/[id].tsx @@ -174,7 +174,7 @@ function HeaderContent(props: any) { } function Nav(props) { - const bg = useColorModeValue("white", "gray.800") + const bg = useColorModeValue("gray.50", "gray.800") const ref = React.useRef() const [y, setY] = React.useState(0) const { height = 0 } = ref.current?.getBoundingClientRect() ?? {} diff --git a/pages/index.tsx b/pages/index.tsx index c193dc10..4f903e8a 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -9,20 +9,20 @@ import { Divider } from "@chakra-ui/react" import Card from "components/card" -import PostCard from "components/story/post-card" import Posts from "components/story/posts" import SimplePostCard from "components/story/simple-post-card" import SEO from "components/seo" -import { getSvgIcon } from "components/svg-icon" import siteConfig from "configs/site-config" import PageContainer1 from "layouts/page-container1" import React, { useEffect, useState } from "react" -import { PostFilter } from "src/types/posts" import { requestApi } from "utils/axios/request" +import { SearchFilter } from "src/types/search" +import SearchFilters from "components/search-filters" + const HomePage = () => { + let filter:string const [posts, setPosts] = useState([]) - const [filter, setFilter] = useState(PostFilter.Best) const initData = async () => { const res = await requestApi.get(`/story/posts/home/${filter}`) setPosts(res.data) @@ -30,7 +30,11 @@ const HomePage = () => { useEffect(() => { initData() - }, [filter]) + }, []) + + const onFilterChange = filter => { + + } return ( <> @@ -43,11 +47,7 @@ const HomePage = () => { - - - - - + { const [posts, setPosts] = useState([]) - const [filter, setFilter] = useState(PostFilter.Best) const initData = async () => { - const res = await requestApi.get(`/story/posts/home/${filter}`) + const res = await requestApi.get(`/story/posts/home/aa`) setPosts(res.data) } useEffect(() => { initData() - }, [filter]) + }, []) return ( diff --git a/pages/search/[type].tsx b/pages/search/[type].tsx deleted file mode 100644 index ecd627c5..00000000 --- a/pages/search/[type].tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Box, chakra, Flex, Input } from "@chakra-ui/react" -import Card from "components/card" -import Container from "components/container" -import Empty from "components/empty" -import SEO from "components/seo" -import siteConfig from "configs/site-config" -import PageContainer1 from "layouts/page-container1" -import Sidebar from "layouts/sidebar/sidebar" -import { useRouter } from "next/router" -import React from "react" -import { searchLinks } from "src/data/links" - -const CoursesPage = () => { - const router = useRouter() - const type = router.query.type - - return ( - <> - - - - - - - - - - - - - - - - ) -} - -export default CoursesPage - - diff --git a/pages/search/posts.tsx b/pages/search/posts.tsx new file mode 100644 index 00000000..6b92c7e6 --- /dev/null +++ b/pages/search/posts.tsx @@ -0,0 +1,102 @@ +import { Box, Divider, Flex, HStack, Input } from "@chakra-ui/react" +import Card from "components/card" +import Empty from "components/empty" +import SEO from "components/seo" +import Posts from "components/story/posts" +import SearchFilters from "components/search-filters" +import siteConfig from "configs/site-config" +import PageContainer1 from "layouts/page-container1" +import Sidebar from "layouts/sidebar/sidebar" +import { useRouter } from "next/router" +import React, { useEffect, useState } from "react" +import { searchLinks } from "src/data/links" +import { SearchFilter } from "src/types/search" + +import { requestApi } from "utils/axios/request" +import { addParamToUrl, removeParamFromUrl } from "utils/url" + +const PostsSearchPage = () => { + let filter = SearchFilter.Best + const router = useRouter() + const q = router.query.q + + const [results,setResults] = useState([]) + const [query,setQuery] = useState("") + const [tempQuery,setTempQuery] = useState("") + + useEffect(() => { + if (q) { + setQuery(q as string) + setTempQuery(q as string) + initData() + } + },[q]) + + useEffect(() => { + initData() + },[query]) + + const initData = async () => { + if (query) { + const res = await requestApi.get(`/search/posts/${filter}?query=${query}`) + setResults(res.data) + } + } + + const onFilterChange = f => { + filter = f + initData() + } + + const startSearch = e => { + if (e.keyCode == 13) { + if (tempQuery === '') { + removeParamFromUrl(["q"]) + setResults([]) + } else { + addParamToUrl({q: tempQuery}) + } + setQuery(tempQuery) + } + } + + function getFilters():[] { + for (const link of searchLinks) { + if (link.path.indexOf("posts") > -1) { + return link.filters + } + } + + return [] + } + + return ( + <> + + + + + + + setTempQuery(e.currentTarget.value)} onKeyUp={(e) => startSearch(e)} size="lg" placeholder="type to search..." variant="unstyled" /> + + + + + {results.length === 0 && } + {results.length > 0 && + } + + + + + + ) +} + +export default PostsSearchPage + + diff --git a/pages/search/users.tsx b/pages/search/users.tsx new file mode 100644 index 00000000..a54b529d --- /dev/null +++ b/pages/search/users.tsx @@ -0,0 +1,103 @@ +import { Box, Divider, Flex, HStack, Input } from "@chakra-ui/react" +import Card from "components/card" +import Empty from "components/empty" +import SEO from "components/seo" +import Posts from "components/story/posts" +import SearchFilters from "components/search-filters" +import siteConfig from "configs/site-config" +import PageContainer1 from "layouts/page-container1" +import Sidebar from "layouts/sidebar/sidebar" +import { useRouter } from "next/router" +import React, { useEffect, useState } from "react" +import { searchLinks } from "src/data/links" +import { SearchFilter } from "src/types/search" + +import { requestApi } from "utils/axios/request" +import { addParamToUrl, removeParamFromUrl } from "utils/url" +import PostAuthor from "components/story/post-author" +import UserCard from "components/users/user-card" +import Users from "components/users/users" + +const PostsSearchPage = () => { + let filter = SearchFilter.Best + const router = useRouter() + const q = router.query.q + + const [results,setResults] = useState([]) + const [query,setQuery] = useState("") + const [tempQuery,setTempQuery] = useState("") + + useEffect(() => { + if (q) { + setQuery(q as string) + setTempQuery(q as string) + initData() + } + },[q]) + + useEffect(() => { + initData() + },[query]) + + const initData = async () => { + if (query) { + const res = await requestApi.get(`/search/users/${filter}?query=${query}`) + setResults(res.data) + } + } + + const onFilterChange = f => { + filter = f + initData() + } + + const startSearch = e => { + if (e.keyCode == 13) { + if (tempQuery === '') { + removeParamFromUrl(["q"]) + setResults([]) + } else { + addParamToUrl({q: tempQuery}) + } + setQuery(tempQuery) + } + } + + function getFilters():[] { + for (const link of searchLinks) { + if (link.path.indexOf("users") > -1) { + return link.filters + } + } + + return [] + } + + return ( + <> + + + + + + + setTempQuery(e.currentTarget.value)} onKeyUp={(e) => startSearch(e)} size="lg" placeholder="type to search..." variant="unstyled" /> + + + + + {results.length === 0 ? : } + + + + + + ) +} + +export default PostsSearchPage + + diff --git a/server/internal/api/search.go b/server/internal/api/search.go new file mode 100644 index 00000000..6808f1bd --- /dev/null +++ b/server/internal/api/search.go @@ -0,0 +1,40 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/imdotdev/im.dev/server/internal/search" + "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 SearchPosts(c *gin.Context) { + filter := c.Param("filter") + query := c.Query("query") + if !models.ValidSearchFilter(filter) || query == "" { + c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid)) + return + } + + user := user.CurrentUser(c) + posts := search.Posts(user, filter, query) + + c.JSON(http.StatusOK, common.RespSuccess(posts)) +} + +func SearchUsers(c *gin.Context) { + filter := c.Param("filter") + query := c.Query("query") + if !models.ValidSearchFilter(filter) || query == "" { + c.JSON(http.StatusBadRequest, common.RespError(e.ParamInvalid)) + return + } + + user := user.CurrentUser(c) + users := search.Users(user, filter, query) + + c.JSON(http.StatusOK, common.RespSuccess(users)) +} diff --git a/server/internal/cache/cache.go b/server/internal/cache/cache.go index 913949cd..edd1aaf3 100644 --- a/server/internal/cache/cache.go +++ b/server/internal/cache/cache.go @@ -29,6 +29,15 @@ func Init() { logger.Warn("scan user error", "error", err) continue } + + err = db.Conn.QueryRow("SELECT tagline from user_profile WHERE id=?", user.ID).Scan(&user.Tagline) + if err != nil { + logger.Warn("query user profile error", "error", err) + } + + if user.Cover == "" { + user.Cover = models.DefaultCover + } users = append(users, user) } diff --git a/server/internal/search/search.go b/server/internal/search/search.go new file mode 100644 index 00000000..ea48255e --- /dev/null +++ b/server/internal/search/search.go @@ -0,0 +1,56 @@ +package search + +import ( + "sort" + "strings" + + "github.com/imdotdev/im.dev/server/internal/cache" + "github.com/imdotdev/im.dev/server/internal/interaction" + "github.com/imdotdev/im.dev/server/internal/story" + "github.com/imdotdev/im.dev/server/pkg/db" + "github.com/imdotdev/im.dev/server/pkg/log" + "github.com/imdotdev/im.dev/server/pkg/models" +) + +var logger = log.RootLogger.New("logger", "search") + +func Posts(user *models.User, filter, query string) models.Posts { + posts := make(models.Posts, 0) + + // postsMap := make(map[string]*models.Post) + + // search by title + rows, err := db.Conn.Query("select id,slug,title,url,cover,brief,creator,created,updated from posts where title LIKE ?", "%"+query+"%") + if err != nil { + logger.Warn("get user posts error", "error", err) + return posts + } + + posts = story.GetPosts(user, rows) + sort.Sort(posts) + + return posts +} + +func Users(user *models.User, filter, query string) []*models.User { + allUsers := cache.Users + + users := make([]*models.User, 0) + for _, u := range allUsers { + if strings.Contains(strings.ToLower(u.Nickname), strings.ToLower(query)) { + users = append(users, u) + continue + } + + if strings.Contains(strings.ToLower(u.Username), strings.ToLower(query)) { + users = append(users, u) + continue + } + } + + for _, u := range users { + u.Followed = interaction.GetFollowed(u.ID, user.ID) + } + + return users +} diff --git a/server/internal/server.go b/server/internal/server.go index 0758eabc..56de9800 100644 --- a/server/internal/server.go +++ b/server/internal/server.go @@ -31,11 +31,11 @@ func (s *Server) Start() error { return err } - if config.Data.Common.IsProd { - gin.SetMode((gin.ReleaseMode)) - } else { - gin.SetMode(gin.DebugMode) - } + // if config.Data.Common.IsProd { + gin.SetMode((gin.ReleaseMode)) + // } else { + // gin.SetMode(gin.DebugMode) + // } go cache.Init() go func() { @@ -77,6 +77,11 @@ func (s *Server) Start() error { r.POST("/interaction/like/:id", IsLogin(), api.Like) r.POST("/interaction/follow/:id", IsLogin(), api.Follow) r.GET("/interaction/followed/:id", api.Followed) + + // search apis + r.GET("/search/posts/:filter", api.SearchPosts) + r.GET("/search/users/:filter", api.SearchUsers) + // other apis r.GET("/config", GetConfig) diff --git a/server/internal/story/posts.go b/server/internal/story/posts.go index aea07bfc..2f9d1f83 100644 --- a/server/internal/story/posts.go +++ b/server/internal/story/posts.go @@ -22,7 +22,7 @@ func HomePosts(user *models.User, filter string) (models.Posts, *e.Error) { return nil, e.New(http.StatusInternalServerError, e.Internal) } - posts := getPosts(user, rows) + posts := GetPosts(user, rows) sort.Sort(posts) return posts, nil @@ -35,7 +35,7 @@ func UserPosts(user *models.User, uid string) (models.Posts, *e.Error) { return nil, e.New(http.StatusInternalServerError, e.Internal) } - posts := getPosts(user, rows) + posts := GetPosts(user, rows) sort.Sort(posts) return posts, nil @@ -58,7 +58,7 @@ func TagPosts(user *models.User, tagID string) (models.Posts, *e.Error) { return nil, e.New(http.StatusInternalServerError, e.Internal) } - posts := getPosts(user, rows) + posts := GetPosts(user, rows) sort.Sort(posts) return posts, nil @@ -88,7 +88,7 @@ func BookmarkPosts(user *models.User, filter string) (models.Posts, *e.Error) { return nil, e.New(http.StatusInternalServerError, e.Internal) } - posts := getPosts(user, rows) + posts := GetPosts(user, rows) for _, post := range posts { _, rawTags, err := tags.GetTargetTags(post.ID) @@ -104,7 +104,7 @@ func BookmarkPosts(user *models.User, filter string) (models.Posts, *e.Error) { return posts, nil } -func getPosts(user *models.User, rows *sql.Rows) models.Posts { +func GetPosts(user *models.User, rows *sql.Rows) models.Posts { posts := make(models.Posts, 0) for rows.Next() { ar := &models.Post{} diff --git a/server/internal/user/session.go b/server/internal/user/session.go index 616ffc9b..81e72d47 100644 --- a/server/internal/user/session.go +++ b/server/internal/user/session.go @@ -14,7 +14,7 @@ import ( "github.com/imdotdev/im.dev/server/pkg/models" ) -var logger = log.RootLogger.New("logger", "session") +var logger = log.RootLogger.New("logger", "user") type Session struct { Token string `json:"token"` diff --git a/server/pkg/models/search.go b/server/pkg/models/search.go new file mode 100644 index 00000000..671b16c5 --- /dev/null +++ b/server/pkg/models/search.go @@ -0,0 +1,16 @@ +package models + +const ( + FilterBest = "best" + FilterFeature = "feature" + FilterRecent = "recent" + FilterFavorites = "favorites" +) + +func ValidSearchFilter(f string) bool { + if f == FilterBest || f == FilterFeature || f == FilterRecent || f == FilterFavorites { + return true + } + + return false +} diff --git a/server/pkg/models/user.go b/server/pkg/models/user.go index 68ef2f41..c61c50b6 100644 --- a/server/pkg/models/user.go +++ b/server/pkg/models/user.go @@ -30,7 +30,8 @@ type User struct { Facebook string `json:"facebook"` Stackoverflow string `json:"stackoverflow"` - Follows int `json:"follows"` + Follows int `json:"follows"` + Followed bool `json:"followed"` LastSeenAt time.Time `json:"lastSeenAt,omitempty"` Created time.Time `json:"created"` diff --git a/src/components/interaction/follow.tsx b/src/components/interaction/follow.tsx index db7724e5..baf893d9 100644 --- a/src/components/interaction/follow.tsx +++ b/src/components/interaction/follow.tsx @@ -6,9 +6,10 @@ import { requestApi } from "utils/axios/request"; interface Props { targetID: string followed: boolean - fontSize?: string + size?: string } const Follow = (props: Props) => { + const {size="md"} =props const [followed, setFollowed] = useState(props.followed) const follow = async () => { @@ -19,9 +20,9 @@ const Follow = (props: Props) => { return ( <> {followed ? - + : - + } ) diff --git a/src/components/search-filters.tsx b/src/components/search-filters.tsx new file mode 100644 index 00000000..f43e4069 --- /dev/null +++ b/src/components/search-filters.tsx @@ -0,0 +1,32 @@ +import React, { useEffect, useState } from "react" +import { Box, BoxProps, Button, HStack, useColorModeValue } from "@chakra-ui/react" + +import { getSvgIcon } from "components/svg-icon" +import { SearchFilter } from "src/types/search" + +interface Props { + filters?: SearchFilter[] + onChange: any +} + +export const SearchFilters = (props:Props) => { + const {filters=[SearchFilter.Best,SearchFilter.Featured,SearchFilter.Recent],onChange} = props + const [filter, setFilter] = useState(SearchFilter.Best) + + const changeFilter = f => { + onChange(f) + setFilter(f) + } + return ( + + { + filters.map(f => + ) + } + + ) +} + +export default SearchFilters diff --git a/src/components/story/post-card.tsx b/src/components/story/post-card.tsx index 6fe996d1..3778949b 100644 --- a/src/components/story/post-card.tsx +++ b/src/components/story/post-card.tsx @@ -8,35 +8,49 @@ import { FaHeart, FaRegHeart } from "react-icons/fa" import Bookmark from "./bookmark" import { getSvgIcon } from "components/svg-icon" import Count from "components/count" +import Highlighter from 'react-highlight-words'; interface Props { post: Post + type?: string + highlight?: string } export const PostCard = (props: Props) => { - const { post } = props + const { post, type = "classic" } = props const [isLargeScreen] = useMediaQuery("(min-width: 768px)") const Layout = isLargeScreen ? HStack : VStack return ( - + - - {post.title} - {post.brief} + + + + {type !== "classic" && {post.rawTags.map(t => #{t.name})}} + + - {post.cover && } + {post.cover && type === "classic" && } - + {getSvgIcon("comments", "1.3rem")} - + diff --git a/src/components/story/posts.tsx b/src/components/story/posts.tsx index 5e827abe..e5f8be1e 100644 --- a/src/components/story/posts.tsx +++ b/src/components/story/posts.tsx @@ -9,11 +9,13 @@ interface Props { card?: any size?: 'sm' | 'md' showFooter?: boolean + type?: string + highlight?: string } export const Posts = (props: Props) => { - const { posts,card=PostCard,showFooter=true} = props + const { posts,card=PostCard,showFooter=true,type="classic"} = props const postBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) const Card = card const showBorder = i => { @@ -32,7 +34,7 @@ export const Posts = (props: Props) => { {posts.map((post,i) => - + )} {showFooter &&
没有更多文章了
} diff --git a/src/components/svg-icon.tsx b/src/components/svg-icon.tsx index c9ad7976..555d68ed 100644 --- a/src/components/svg-icon.tsx +++ b/src/components/svg-icon.tsx @@ -4,7 +4,7 @@ export function getSvgIcon(name,height="1.4rem") { case "comments": svg = break - case "hot": + case "best": svg = break case "home": @@ -29,11 +29,14 @@ export function getSvgIcon(name,height="1.4rem") { svg = break case "search": - svg = + svg = break case "user": svg = break + case "favorites": + svg = + break default: break; } diff --git a/src/components/tags/tags.tsx b/src/components/tags/tags.tsx index 5bc905f6..9b69143f 100644 --- a/src/components/tags/tags.tsx +++ b/src/components/tags/tags.tsx @@ -6,7 +6,7 @@ import { Tag } from "src/types/tag" import { cloneDeep, remove } from "lodash" interface Props { - tags: number[] + tags: string[] onChange: any size?: 'lg' | 'md' } diff --git a/src/components/users/user-card.tsx b/src/components/users/user-card.tsx new file mode 100644 index 00000000..102f4937 --- /dev/null +++ b/src/components/users/user-card.tsx @@ -0,0 +1,52 @@ +import React from "react" +import {chakra, Heading, Image, Text, HStack,Button, Flex,PropsOf,Box, Avatar, VStack, propNames} from "@chakra-ui/react" +import moment from 'moment' +import { FaGithub } from "react-icons/fa" +import { useRouter } from "next/router" +import { User } from "src/types/user" +import { getUserName } from "utils/user" +import Follow from "components/interaction/follow" +import Highlighter from 'react-highlight-words'; + +type Props = PropsOf & { + user : User + highlight?: string +} + +export const UserCard= ({user,highlight}:Props) =>{ + const router = useRouter() + return ( + + + router.push(`/${user.username}`)} cursor="pointer"/> + + + router.push(`/${user.username}`)} cursor="pointer"> + + + @ + + + {user.tagline && + + } + + + + + ) +} + +export default UserCard diff --git a/src/components/users/users.tsx b/src/components/users/users.tsx new file mode 100644 index 00000000..9ac996aa --- /dev/null +++ b/src/components/users/users.tsx @@ -0,0 +1,32 @@ +import React from "react" +import { Box, BoxProps, chakra, PropsOf, useColorModeValue, VStack } from "@chakra-ui/react" +import UserCard from './user-card' +import { User } from "src/types/user" +import userCustomTheme from "theme/user-custom" + +type Props = PropsOf & { + users: User[] + highlight?: string +} + +export const Users = (props: Props) => { + const { users,highlight, ...rest } = props + const postBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) + const showBorder = i => { + if (i < users.length - 1) { + return true + } + } + return ( + + {users.map((u,i) => + + + + + )} + + ) +} + +export default Users diff --git a/src/data/links.tsx b/src/data/links.tsx index 52946c9c..ebe3170f 100644 --- a/src/data/links.tsx +++ b/src/data/links.tsx @@ -2,6 +2,7 @@ import { getSvgIcon } from 'components/svg-icon' import React from 'react' import { FaFileAlt, FaScroll, FaBookOpen, FaTags, FaUserCircle, FaRegFile, FaUser, FaRegUser } from 'react-icons/fa' import { Route } from 'src/types/route' +import { SearchFilter } from 'src/types/search' import { ReserveUrls } from './reserve-urls' export const editorLinks: Route[] = [{ title: '文章', @@ -17,23 +18,19 @@ export const editorLinks: Route[] = [{ } ] -export const searchLinks: Route[] = [{ +export const searchLinks: any[] = [{ title: '文章', path: `${ReserveUrls.Search}/posts`, icon: getSvgIcon("post"), - disabled: false -}, -{ - title: '标签', - path: `${ReserveUrls.Search}/tags`, - icon: getSvgIcon("tags","1.2rem"), - disabled: false + disabled: false, + filters: [SearchFilter.Best,SearchFilter.Recent] }, { title: '用户', path: `${ReserveUrls.Search}/users`, icon: getSvgIcon('user','1.5rem'), - disabled: false + disabled: false, + filters: [SearchFilter.Best] } ] diff --git a/src/types/posts.ts b/src/types/posts.ts index 35f3ce59..94df403a 100644 --- a/src/types/posts.ts +++ b/src/types/posts.ts @@ -1,17 +1,13 @@ import { UserSimple} from './user' import { Tag } from './tag'; -export enum PostFilter { - Best = "best", - Featured = "featured", - Recent = "recent" -} + export interface Post { id?: string slug?: string creator?: UserSimple - creatorId?: number + creatorId?: string title?: string md?: string url?: string diff --git a/src/types/search.ts b/src/types/search.ts new file mode 100644 index 00000000..b88b3af3 --- /dev/null +++ b/src/types/search.ts @@ -0,0 +1,6 @@ +export enum SearchFilter { + Best = "best", + Featured = "feature", + Recent = "recent", + Favorites = "favorites" +} \ No newline at end of file diff --git a/src/types/user.ts b/src/types/user.ts index cae8d9d9..9d1a630f 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -33,7 +33,8 @@ export interface User { stackoverflow?: string follows?: number - + followed?: boolean + lastSeenAt?: string created?: string } diff --git a/src/utils/url.ts b/src/utils/url.ts index 6f4bf384..b6330215 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -1,3 +1,8 @@ +import { extend } from 'lodash' +import queryString from 'query-string' + + + export const getHost = (url: string) => { const urlMatched = url.match(/https?:\/\/([^/]+)\//i) let domain = '' @@ -7,4 +12,36 @@ export const getHost = (url: string) => { return domain } export const clearApiVersion = (api: string) => api && api.replace(/\/v\d$/, '') - \ No newline at end of file + + + export const addParamToUrl = (param: any) => { + const currentQuery = getUrlParams() + extend(currentQuery, param) + const params = queryString.stringify(currentQuery) + updateUrl(params) +} + +export const setParamToUrl = (params) => { + updateUrl(queryString.stringify(params)) +} + +export const removeParamFromUrl = (paramKeys: string[]) => { + const currentQuery = getUrlParams() + paramKeys.forEach((key) => delete currentQuery[key]) + + const params = queryString.stringify(currentQuery) + + updateUrl(params) +} + +export const getUrlParams = ():any => { + return queryString.parseUrl(window?.location.href).query +} + +export const updateUrl = (params: string) => { + let url = window.location.origin + window.location.pathname + if (params != '') { + url = url + '?' + params + } + window.history.pushState({},null,url); +} \ No newline at end of file diff --git a/theme.ts b/theme.ts index 2d368e0d..c7560ad0 100644 --- a/theme.ts +++ b/theme.ts @@ -42,7 +42,7 @@ const customTheme = extendTheme({ borderRadius: '6px' }, body: { - background: mode("white","gray.800" )(props), + background: mode("gray.50","gray.800" )(props), minHeight: '100vh', color: mode("gray.700", "whiteAlpha.900")(props), ".deleted": { @@ -164,6 +164,13 @@ const customTheme = extendTheme({ }, }, components : { + Avatar: { + sizes: { + md: { + + } + } + }, Heading: { sizes: { lg: { diff --git a/yarn.lock b/yarn.lock index cd0f790b..0d529df5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1936,6 +1936,11 @@ debug@^2.6.9: dependencies: ms "2.0.0" +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + decompress-response@^4.2.0: version "4.2.1" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" @@ -2258,6 +2263,11 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs= + find-cache-dir@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" @@ -2499,6 +2509,11 @@ hey-listen@^1.0.8: resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== +highlight-words-core@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/highlight-words-core/-/highlight-words-core-1.2.2.tgz#1eff6d7d9f0a22f155042a00791237791b1eeaaa" + integrity sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg== + highlight.js@^9.16.2: version "9.18.5" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" @@ -2867,6 +2882,11 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +memoize-one@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906" + integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -3370,7 +3390,7 @@ process@0.11.10, process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -prop-types@15.7.2, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@15.7.2, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -3424,6 +3444,16 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +query-string@^6.3.0: + version "6.14.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== + dependencies: + decode-uri-component "^0.2.0" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + querystring-es3@^0.2.0, querystring-es3@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -3512,6 +3542,15 @@ react-focus-lock@2.4.1: use-callback-ref "^1.2.1" use-sidecar "^1.0.1" +react-highlight-words@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.17.0.tgz#e79a559a2de301548339d7216264d6cd0f1eed6f" + integrity sha512-uX1Qh5IGjnLuJT0Zok234QDwRC8h4hcVMnB99Cb7aquB1NlPPDiWKm0XpSZOTdSactvnClCk8LOmVlP+75dgHA== + dependencies: + highlight-words-core "^1.2.0" + memoize-one "^4.0.0" + prop-types "^15.5.8" + react-icons@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.1.0.tgz#9ca9bcbf2e3aee8e86e378bb9d465842947bbfc3" @@ -3814,6 +3853,11 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -3875,6 +3919,11 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= + string-hash@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b"