implement search feature

This commit is contained in:
SrGooglo 2023-05-30 01:11:00 +00:00
parent 88c27c807e
commit 9bcf2900f8
2 changed files with 214 additions and 106 deletions

View File

@ -1,11 +1,18 @@
import React from "react" import React from "react"
import * as antd from "antd" import * as antd from "antd"
import classnames from "classnames" import classnames from "classnames"
import { ImageViewer, UserPreview } from "components"
import { Icons } from "components/Icons"
import { Translation } from "react-i18next" import { Translation } from "react-i18next"
import Searcher from "components/Searcher"
import { ImageViewer, UserPreview } from "components"
import { Icons, createIconRender } from "components/Icons"
import { WithPlayerContext } from "contexts/WithPlayerContext"
import FeedModel from "models/feed" import FeedModel from "models/feed"
import PlaylistModel from "models/playlists"
import MusicTrack from "components/MusicTrack"
import "./index.less" import "./index.less"
@ -25,7 +32,23 @@ const PlaylistsList = (props) => {
return return
} }
setOffset((value) => value - hopNumber) setOffset((value) => {
const newOffset = value - hopNumber
// check if newOffset is NaN
if (newOffset !== newOffset) {
return false
}
if (typeof makeRequest === "function") {
makeRequest({
trim: newOffset,
limit: hopNumber,
})
}
return newOffset
})
} }
const onClickNext = () => { const onClickNext = () => {
@ -33,14 +56,24 @@ const PlaylistsList = (props) => {
return return
} }
setOffset((value) => value + hopNumber) setOffset((value) => {
const newOffset = value + hopNumber
// check if newOffset is NaN
if (newOffset !== newOffset) {
return false
} }
React.useEffect(() => {
if (typeof makeRequest === "function") { if (typeof makeRequest === "function") {
makeRequest() makeRequest({
trim: newOffset,
limit: hopNumber,
})
}
return newOffset
})
} }
}, [offset])
React.useEffect(() => { React.useEffect(() => {
if (result) { if (result) {
@ -135,6 +168,10 @@ const PlaylistItem = (props) => {
onMouseLeave={() => setCoverHover(false)} onMouseLeave={() => setCoverHover(false)}
onClick={onClickPlay} onClick={onClickPlay}
> >
<div className="playlistItem_cover_mask">
<Icons.MdPlayArrow />
</div>
<ImageViewer <ImageViewer
src={playlist.thumbnail ?? "/assets/no_song.png"} src={playlist.thumbnail ?? "/assets/no_song.png"}
/> />
@ -144,7 +181,10 @@ const PlaylistItem = (props) => {
<div className="playlistItem_info_title" onClick={onClick}> <div className="playlistItem_info_title" onClick={onClick}>
<h1>{playlist.title}</h1> <h1>{playlist.title}</h1>
</div> </div>
<UserPreview user={playlist.user} />
{
playlist.publisher && <UserPreview user={playlist.publisher} />
}
</div> </div>
</div> </div>
} }
@ -191,100 +231,127 @@ const MayLike = (props) => {
</div> </div>
} }
const SearchResultItem = (props) => { const ResultGroupsDecorators = {
return <div> "playlists": {
<h1>SearchResultItem</h1> icon: "MdPlaylistPlay",
label: "Playlists",
renderItem: (props) => {
return <PlaylistItem
key={props.key}
playlist={props.item}
/>
}
},
"tracks": {
icon: "MdMusicNote",
label: "Tracks",
renderItem: (props) => {
return <MusicTrack
key={props.key}
track={props.item}
onClick={() => app.cores.player.start(props.item)}
/>
}
}
}
const SearchResults = ({
data
}) => {
if (typeof data !== "object") {
return null
}
let groupsKeys = Object.keys(data)
// filter out empty groups
groupsKeys = groupsKeys.filter((key) => {
return data[key].length > 0
})
if (groupsKeys.length === 0) {
return <div className="music-explorer_search_results no_results">
<antd.Result
status="info"
title="No results"
subTitle="We are sorry, but we could not find any results for your search."
/>
</div>
}
return <div
className={classnames(
"music-explorer_search_results",
{
["one_column"]: groupsKeys.length === 1,
}
)}
>
<WithPlayerContext>
{
groupsKeys.map((key, index) => {
const decorator = ResultGroupsDecorators[key] ?? {
icon: null,
label: key,
renderItem: () => null
}
return <div className="music-explorer_search_results_group" key={index}>
<div className="music-explorer_search_results_group_header">
<h1>
{
createIconRender(decorator.icon)
}
<Translation>
{(t) => t(decorator.label)}
</Translation>
</h1>
</div>
<div className="music-explorer_search_results_group_list">
{
data[key].map((item, index) => {
return decorator.renderItem({
key: index,
item
})
})
}
</div>
</div>
})
}
</WithPlayerContext>
</div> </div>
} }
export default (props) => { export default (props) => {
const [searchLoading, setSearchLoading] = React.useState(false) const [searchResults, setSearchResults] = React.useState(false)
const [searchFocused, setSearchFocused] = React.useState(false)
const [searchValue, setSearchValue] = React.useState("")
const [searchResult, setSearchResult] = React.useState([])
const handleSearchValueChange = (e) => {
// not allow to input space as first character
if (e.target.value[0] === " ") {
return
}
setSearchValue(e.target.value)
}
const makeSearch = async (value) => {
setSearchResult([])
await new Promise((resolve) => setTimeout(resolve, 1000))
setSearchResult([
{
title: "test",
thumbnail: "/assets/no_song.png",
},
{
title: "test2",
thumbnail: "/assets/no_song.png",
}
])
}
React.useEffect(() => {
const timer = setTimeout(async () => {
setSearchLoading(true)
await makeSearch(searchValue)
setSearchLoading(false)
}, 400)
if (searchValue === "") {
if (typeof props.onEmpty === "function") {
//props.onEmpty()
}
} else {
if (typeof props.onFilled === "function") {
//props.onFilled()
}
}
return () => clearTimeout(timer)
}, [searchValue])
return <div return <div
className={classnames( className={classnames(
"musicExplorer", "musicExplorer",
{ {
["search-focused"]: searchFocused, //["search-focused"]: searchFocused,
} }
)} )}
> >
<div className="searcher"> <Searcher
<antd.Input useUrlQuery
placeholder="Search for music" renderResults={false}
prefix={<Icons.Search />} model={PlaylistModel.search}
onFocus={() => setSearchFocused(true)} onSearchResult={setSearchResults}
onBlur={() => setSearchFocused(false)} onEmpty={() => setSearchResults(false)}
onChange={handleSearchValueChange}
value={searchValue}
/> />
<div className="searcher_result">
{ {
searchLoading && <antd.Skeleton active /> searchResults && <SearchResults
} data={searchResults}
{
searchFocused && searchValue !== "" && searchResult.length > 0 && searchResult.map((result, index) => {
return <SearchResultItem
key={index}
result={result}
/> />
})
} }
</div>
</div>
<div className="feed_main"> {
!searchResults && <div className="feed_main">
<RecentlyPlayed /> <RecentlyPlayed />
<PlaylistsList <PlaylistsList
@ -299,5 +366,6 @@ export default (props) => {
fetchMethod={FeedModel.getGlobalMusicFeed} fetchMethod={FeedModel.getGlobalMusicFeed}
/> />
</div> </div>
}
</div> </div>
} }

View File

@ -97,7 +97,7 @@
min-width: 400px; min-width: 400px;
max-width: 800px; max-width: 800px;
//overflow: hidden; overflow: visible;
box-sizing: border-box !important; box-sizing: border-box !important;
@ -111,6 +111,10 @@
&.cover-hovering { &.cover-hovering {
.playlistItem_cover { .playlistItem_cover {
transform: scale(1.1); transform: scale(1.1);
.playlistItem_cover_mask {
opacity: 1;
}
} }
.playlistItem_info { .playlistItem_info {
@ -135,6 +139,8 @@
} }
.playlistItem_cover { .playlistItem_cover {
position: relative;
height: 10vh; height: 10vh;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
@ -145,6 +151,36 @@
object-fit: cover; object-fit: cover;
border-radius: 8px; border-radius: 8px;
background-color: black;
z-index: 50
}
.playlistItem_cover_mask {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
opacity: 0;
transition: all 0.2s ease-in-out;
z-index: 55;
background-color: rgba(var(--layoutBackgroundColor), 0.6);
color: var(--text-color);
font-size: 3rem;
} }
} }
@ -169,6 +205,10 @@
overflow: hidden; overflow: hidden;
&:hover {
text-decoration: underline;
}
h1, h1,
h4 { h4 {
overflow: hidden; overflow: hidden;