pull/50/head
sunface 4 years ago
parent c764a3a26f
commit 633a1aea7e

@ -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"

@ -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}
>
<chakra.div height="4.5rem" mx="auto" maxW="1200px">

@ -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 (
<chakra.header
transition="box-shadow 0.2s"

@ -46,7 +46,6 @@ import { navLinks } from "src/data/links"
return asPath === url
}
console.log(asPath,url)
return asPath.startsWith(url)
}
@ -116,7 +115,7 @@ import { navLinks } from "src/data/links"
zIndex="3"
left="0"
bottom="0"
bg={useColorModeValue('white', 'gray.800')}
bg={useColorModeValue('gray.50', 'gray.800')}
{...props}
>
<chakra.div height="100%">

@ -35,10 +35,11 @@ const StyledLink = React.forwardRef(function StyledLink(
type SidebarLinkProps = PropsOf<typeof chakra.div> & {
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}
>
<NextLink href={href} passHref>
<NextLink href={{pathname: href,query: query}} passHref>
<StyledLink isActive={isActive} icon={icon}>{children}</StyledLink>
</NextLink>
</chakra.div>

@ -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 (
<>
<Stack as="ul">
{routes.map((route: Route) => {
if (route.disabled) { return null }
return <SidebarLink as="li" key={route.path} href={route.path} icon={route.icon}>
return <SidebarLink query={query} as="li" key={route.path} href={route.path} icon={route.icon}>
<span>{route.title}</span>
</SidebarLink>
})}
@ -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<HTMLDivElement>(null)
@ -45,7 +45,7 @@ const Sidebar = ({ routes, title, ...props }) => {
flexShrink={0}
// display={{ base: "none", md: "block" }}
>
<SidebarContent routes={routes} pathname={pathname} contentRef={ref} />
<SidebarContent query={query} routes={routes} pathname={pathname} contentRef={ref} />
</Box>
</Card>
</VStack>

@ -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"

@ -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<HTMLHeadingElement>()
const [y, setY] = React.useState(0)
const { height = 0 } = ref.current?.getBoundingClientRect() ?? {}

@ -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 = () => {
<VStack alignItems="left" width={["100%", "100%", "70%", "70%"]} spacing="3">
<Card p="2">
<Flex justifyContent="space-between" alignItems="center">
<HStack>
<Button _focus={null} onClick={() => setFilter(PostFilter.Best)} size="sm" colorScheme={filter === PostFilter.Best ? 'teal' : null} leftIcon={getSvgIcon("hot")} variant="ghost" >Best</Button>
<Button _focus={null} onClick={() => setFilter(PostFilter.Featured)} size="sm" colorScheme={filter === PostFilter.Featured ? 'teal' : null} leftIcon={getSvgIcon("feature")} variant="ghost">Fetured</Button>
<Button _focus={null} onClick={() => setFilter(PostFilter.Recent)} size="sm" colorScheme={filter === PostFilter.Recent ? 'teal' : null} leftIcon={getSvgIcon("recent")} variant="ghost">Recent</Button>
</HStack>
<SearchFilters onChange={onFilterChange}/>
<Menu>
<MenuButton
as={IconButton}
@ -84,15 +84,14 @@ export default HomePage
export const HomeSidebar = () => {
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 (
<VStack alignItems="left" width="30%" display={{ base: "none", md: "flex" }}>

@ -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 (
<>
<SEO
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<PageContainer1>
<Flex width="100%">
<Sidebar routes={searchLinks} title="全站搜索" />
<Box ml="3" width={['100%', '100%', '50%', '50%']}>
<Card p="5">
<Input size="lg" placeholder="type to search..." variant="unstyled" />
</Card>
<Card mt="2">
<Empty />
</Card>
</Box>
</Flex>
</PageContainer1>
</>
)
}
export default CoursesPage

@ -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 (
<>
<SEO
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<PageContainer1>
<Flex width="100%">
<Sidebar query={query ?{q:query} : null} routes={searchLinks} title="全站搜索" />
<Box ml="3" width={['100%', '100%', '100%', '70%']}>
<Card p="5">
<Input value={tempQuery} onChange={(e) => setTempQuery(e.currentTarget.value)} onKeyUp={(e) => startSearch(e)} size="lg" placeholder="type to search..." variant="unstyled" />
</Card>
<Card mt="2" p="0" pt="4" px="4">
<SearchFilters filters={getFilters()} onChange={onFilterChange}/>
<Divider mt="3"/>
{results.length === 0 && <Empty /> }
{results.length > 0 &&
<Posts posts={results} showFooter={false} type="compact" highlight={query}/>}
</Card>
</Box>
</Flex>
</PageContainer1>
</>
)
}
export default PostsSearchPage

@ -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 (
<>
<SEO
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<PageContainer1>
<Flex width="100%">
<Sidebar query={query ?{q:query} : null} routes={searchLinks} title="全站搜索" />
<Box ml="3" width={['100%', '100%', '100%', '70%']}>
<Card p="5">
<Input value={tempQuery} onChange={(e) => setTempQuery(e.currentTarget.value)} onKeyUp={(e) => startSearch(e)} size="lg" placeholder="type to search..." variant="unstyled" />
</Card>
<Card mt="2" p="0" pt="4" px="4">
<SearchFilters filters={getFilters()} onChange={onFilterChange}/>
<Divider mt="3"/>
{results.length === 0 ? <Empty /> : <Users users={results} p="2" highlight={query}/>}
</Card>
</Box>
</Flex>
</PageContainer1>
</>
)
}
export default PostsSearchPage

@ -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))
}

@ -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)
}

@ -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
}

@ -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)

@ -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{}

@ -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"`

@ -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
}

@ -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"`

@ -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 ?
<Button colorScheme="teal" onClick={follow} _focus={null} leftIcon={<FaCheck />}>Following</Button>
<Button size={size} colorScheme="teal" onClick={follow} _focus={null} leftIcon={<FaCheck />}>Following</Button>
:
<Button colorScheme="teal" variant="outline" leftIcon={<FaPlus />} onClick={follow} _focus={null}>Follow</Button>
<Button size={size} colorScheme="teal" variant="outline" leftIcon={<FaPlus />} onClick={follow} _focus={null}>Follow</Button>
}
</>
)

@ -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 (
<HStack>
{
filters.map(f =>
<Button _focus={null} onClick={() => changeFilter(f)} size="sm" colorScheme={filter === f ? 'teal' : null} leftIcon={getSvgIcon(f)} variant="ghost" >
{f}
</Button>)
}
</HStack>
)
}
export default SearchFilters

@ -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 (
<VStack alignItems="left" spacing="4" p="2">
<VStack alignItems="left" spacing={type === "classic" ? 4 : 2} p="2">
<PostAuthor post={post} showFooter={false} size="md" />
<Link href={`/${post.creator.username}/${post.id}`}>
<Layout alignItems={isLargeScreen ? "top" : "left"} cursor="pointer" pl="2" pt="1">
<VStack alignItems="left" spacing="3" width={isLargeScreen ? "calc(100% - 18rem)" : '100%'}>
<Heading size="md">{post.title}</Heading>
<Text layerStyle="textSecondary" maxW="400px">{post.brief}</Text>
<VStack alignItems="left" spacing="3" width={isLargeScreen && type === "classic" ? "calc(100% - 18rem)" : '100%'}>
<Heading size="md"><Highlighter
highlightClassName="highlight-search-match"
textToHighlight={post.title}
searchWords={[props.highlight]}
/>
</Heading>
{type !== "classic" && <HStack>{post.rawTags.map(t => <Text layerStyle="textSecondary" fontSize="sm">#{t.name}</Text>)}</HStack>}
<Text layerStyle={type === "classic" ? "textSecondary" : null}>
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={post.brief}
searchWords={[props.highlight]}
/></Text>
</VStack>
{post.cover && <Image src={post.cover} width="18rem" height="120px" pt={isLargeScreen ? 0 : 2} borderRadius="4px" />}
{post.cover && type === "classic" && <Image src={post.cover} width="18rem" height="120px" pt={isLargeScreen ? 0 : 2} borderRadius="4px" />}
</Layout>
</Link>
<HStack pl="2" spacing="5">
<Like storyID={post.id} liked={post.liked} count={post.likes} fontSize="18px"/>
<Like storyID={post.id} liked={post.liked} count={post.likes} fontSize="18px" />
<Link href={`/${post.creator.username}/${post.id}#comments`}>
<HStack opacity="0.9" cursor="pointer">
{getSvgIcon("comments", "1.3rem")}
<Text ml="2"><Count count={post.comments}/></Text>
<Text ml="2"><Count count={post.comments} /></Text>
</HStack>
</Link>

@ -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) => {
<VStack alignItems="left">
{posts.map((post,i) =>
<Box py="2" borderBottom={showBorder(i)? `1px solid ${postBorderColor}`:null} key={post.id}>
<Card post={post} size={props.size}/>
<Card post={post} size={props.size} type={type} highlight={props.highlight}/>
</Box>)}
</VStack>
{showFooter && <Center><Text layerStyle="textSecondary" fontSize="sm" py="4"></Text></Center>}

@ -4,7 +4,7 @@ export function getSvgIcon(name,height="1.4rem") {
case "comments":
svg = <svg fill="currentColor" height={height} viewBox="0 0 512 512"><path d="M280 272H136c-4.4 0-8 3.6-8 8v16c0 4.4 3.6 8 8 8h144c4.4 0 8-3.6 8-8v-16c0-4.4-3.6-8-8-8zm96-96H136c-4.4 0-8 3.6-8 8v16c0 4.4 3.6 8 8 8h240c4.4 0 8-3.6 8-8v-16c0-4.4-3.6-8-8-8zM256 32C114.6 32 0 125.1 0 240c0 47.6 19.9 91.2 52.9 126.3C38 405.7 7 439.1 6.5 439.5c-6.6 7-8.4 17.2-4.6 26S14.4 480 24 480c61.5 0 110-25.7 139.1-46.3C192 442.8 223.2 448 256 448c141.4 0 256-93.1 256-208S397.4 32 256 32zm0 384c-28.3 0-56.3-4.3-83.2-12.8l-15.2-4.8-13 9.2c-23 16.3-58.5 35.3-102.6 39.6 12-15.1 29.8-40.4 40.8-69.6l7.1-18.7-13.7-14.6C47.3 313.7 32 277.6 32 240c0-97 100.5-176 224-176s224 79 224 176-100.5 176-224 176z"></path></svg>
break
case "hot":
case "best":
svg = <svg fill="currentColor" height={height} viewBox="0 0 448 512"><path d="M448 281.6c0-53.27-51.98-163.13-124.44-230.4-20.8 19.3-39.58 39.59-56.22 59.97C240.08 73.62 206.28 35.53 168 0 69.74 91.17 0 209.96 0 281.6 0 408.85 100.29 512 224 512c.53 0 1.04-.08 1.58-.08.32 0 .6.08.92.08 1.88 0 3.71-.35 5.58-.42C352.02 507.17 448 406.04 448 281.6zm-416 0c0-50.22 47.51-147.44 136.05-237.09 27.38 27.45 52.44 56.6 73.39 85.47l24.41 33.62 26.27-32.19a573.83 573.83 0 0130.99-34.95C379.72 159.83 416 245.74 416 281.6c0 54.69-21.53 104.28-56.28 140.21 12.51-35.29 10.88-75.92-8.03-112.02a357.34 357.34 0 00-10.83-19.19l-22.63-37.4-28.82 32.87-25.86 29.5c-24.93-31.78-59.31-75.5-63.7-80.54l-24.65-28.39-24.08 28.87C108.16 287 80 324.21 80 370.41c0 19.02 3.62 36.66 9.77 52.79C54.17 387.17 32 337.03 32 281.6zm193.54 198.32C162.86 479.49 112 437.87 112 370.41c0-33.78 21.27-63.55 63.69-114.41 6.06 6.98 86.48 109.68 86.48 109.68l51.3-58.52a334.43 334.43 0 019.87 17.48c23.92 45.66 13.83 104.1-29.26 134.24-17.62 12.33-39.14 19.71-62.37 20.73-2.06.07-4.09.29-6.17.31z"></path></svg>
break
case "home":
@ -29,11 +29,14 @@ export function getSvgIcon(name,height="1.4rem") {
svg = <svg fill="currentColor" height={height} viewBox="0 0 512 512"><path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm216 248c0 118.7-96.1 216-216 216-118.7 0-216-96.1-216-216 0-118.7 96.1-216 216-216 118.7 0 216 96.1 216 216zm-148.9 88.3l-81.2-59c-3.1-2.3-4.9-5.9-4.9-9.7V116c0-6.6 5.4-12 12-12h14c6.6 0 12 5.4 12 12v146.3l70.5 51.3c5.4 3.9 6.5 11.4 2.6 16.8l-8.2 11.3c-3.9 5.3-11.4 6.5-16.8 2.6z"></path></svg>
break
case "search":
svg = <svg viewBox="0 0 200 200" fill="currentColor" height={height} ><g clip-path="url(#clip0)"><path d="M186.804 176.609l-44.092-44.091a4.054 4.054 0 00-2.905-1.197h-3.521c11.724-12.68 18.902-29.599 18.902-48.227C155.188 43.82 123.366 12 84.094 12 44.82 12 13 43.821 13 83.094c0 39.272 31.821 71.094 71.094 71.094 18.628 0 35.547-7.178 48.227-18.868v3.487c0 1.093.445 2.119 1.197 2.905l44.091 44.092a4.107 4.107 0 005.811 0l3.384-3.384a4.107 4.107 0 000-5.811zM84.094 143.25c-33.257 0-60.156-26.899-60.156-60.156s26.899-60.156 60.156-60.156 60.156 26.899 60.156 60.156-26.899 60.156-60.156 60.156z"></path></g><defs><clipPath><path transform="translate(13 12)" d="M0 0h175v175H0z"></path></clipPath></defs></svg>
svg = <svg viewBox="0 0 200 200" fill="currentColor" height={height} ><g clipPath="url(#clip0)"><path d="M186.804 176.609l-44.092-44.091a4.054 4.054 0 00-2.905-1.197h-3.521c11.724-12.68 18.902-29.599 18.902-48.227C155.188 43.82 123.366 12 84.094 12 44.82 12 13 43.821 13 83.094c0 39.272 31.821 71.094 71.094 71.094 18.628 0 35.547-7.178 48.227-18.868v3.487c0 1.093.445 2.119 1.197 2.905l44.091 44.092a4.107 4.107 0 005.811 0l3.384-3.384a4.107 4.107 0 000-5.811zM84.094 143.25c-33.257 0-60.156-26.899-60.156-60.156s26.899-60.156 60.156-60.156 60.156 26.899 60.156 60.156-26.899 60.156-60.156 60.156z"></path></g><defs><clipPath><path transform="translate(13 12)" d="M0 0h175v175H0z"></path></clipPath></defs></svg>
break
case "user":
svg = <svg fill="currentColor" height={height} viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"></path><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM7.07 18.28c.43-.9 3.05-1.78 4.93-1.78s4.51.88 4.93 1.78C15.57 19.36 13.86 20 12 20s-3.57-.64-4.93-1.72zm11.29-1.45c-1.43-1.74-4.9-2.33-6.36-2.33s-4.93.59-6.36 2.33C4.62 15.49 4 13.82 4 12c0-4.41 3.59-8 8-8s8 3.59 8 8c0 1.82-.62 3.49-1.64 4.83zM12 6c-1.94 0-3.5 1.56-3.5 3.5S10.06 13 12 13s3.5-1.56 3.5-3.5S13.94 6 12 6zm0 5c-.83 0-1.5-.67-1.5-1.5S11.17 8 12 8s1.5.67 1.5 1.5S12.83 11 12 11z"></path></svg>
break
case "favorites":
svg = <svg fill="currentColor" height={height} viewBox="0 0 496 512"><path d="M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 464c-119.1 0-216-96.9-216-216S128.9 40 248 40s216 96.9 216 216-96.9 216-216 216zm90.2-146.2C315.8 352.6 282.9 368 248 368s-67.8-15.4-90.2-42.2c-5.7-6.8-15.8-7.7-22.5-2-6.8 5.7-7.7 15.7-2 22.5C161.7 380.4 203.6 400 248 400s86.3-19.6 114.8-53.8c5.7-6.8 4.8-16.9-2-22.5-6.8-5.6-16.9-4.7-22.6 2.1zM168 240c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32zm160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32-32 14.3-32 32 14.3 32 32 32z"></path></svg>
break
default:
break;
}

@ -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'
}

@ -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<typeof chakra.div> & {
user : User
highlight?: string
}
export const UserCard= ({user,highlight}:Props) =>{
const router = useRouter()
return (
<Flex alignItems="center" justifyContent="space-between">
<HStack spacing="4" p="2">
<Avatar src={user.avatar} onClick={() => router.push(`/${user.username}`)} cursor="pointer"/>
<VStack alignItems="left" spacing="1">
<HStack>
<Heading size="sm" onClick={() => router.push(`/${user.username}`)} cursor="pointer">
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={getUserName(user)}
searchWords={[highlight]}
/>
</Heading>
<Text layerStyle="textSecondary">@
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={user.username}
searchWords={[highlight]}
/> </Text>
</HStack>
{user.tagline && <Text>
<Highlighter
highlightClassName="highlight-search-match"
textToHighlight={user.tagline}
searchWords={[highlight]}
/>
</Text>}
</VStack>
</HStack>
<Follow followed={user.followed} targetID={user.id} size="sm"/>
</Flex>
)
}
export default UserCard

@ -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<typeof chakra.div> & {
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 (
<VStack alignItems="left" {...rest}>
{users.map((u,i) =>
<Box borderBottom={showBorder(i) ? `1px solid ${postBorderColor}` : null} key={u.id}>
<UserCard user={u} highlight={highlight}/>
</Box>
)}
</VStack>
)
}
export default Users

@ -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]
}
]

@ -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

@ -0,0 +1,6 @@
export enum SearchFilter {
Best = "best",
Featured = "feature",
Recent = "recent",
Favorites = "favorites"
}

@ -33,7 +33,8 @@ export interface User {
stackoverflow?: string
follows?: number
followed?: boolean
lastSeenAt?: string
created?: string
}

@ -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$/, '')
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);
}

@ -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: {

@ -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"

Loading…
Cancel
Save