mirror of https://github.com/sunface/rust-course
parent
c764a3a26f
commit
633a1aea7e
@ -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))
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
@ -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
|
@ -0,0 +1,6 @@
|
||||
export enum SearchFilter {
|
||||
Best = "best",
|
||||
Featured = "feature",
|
||||
Recent = "recent",
|
||||
Favorites = "favorites"
|
||||
}
|
Loading…
Reference in new issue