improve searcher

This commit is contained in:
SrGooglo 2023-07-12 17:23:59 +00:00
parent 70fb020842
commit a03321fcef
2 changed files with 175 additions and 87 deletions

View File

@ -1,90 +1,132 @@
import React from "react"
import * as antd from "antd"
import classnames from "classnames"
import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
import lodash from "lodash"
import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
import { Translation } from "react-i18next"
import { UserPreview } from "components"
import { Icons, createIconRender } from "components/Icons"
import UserPreview from "components/UserPreview"
import MusicTrack from "components/MusicTrack"
import PlaylistItem from "components/PlaylistItem"
import SearchModel from "models/search"
import "./index.less"
const ResultRenders = {
users: (props) => {
const { item, onClick } = props
return <div className="suggestion">
<UserPreview onClick={onClick} user={item} />
</div>
}
}
const ResultsTypeDecorators = {
users: {
icon: "Users",
label: "Users"
label: "Users",
renderItem: (props) => {
const { item, onClick } = props
return <div className="suggestion">
<UserPreview onClick={onClick} user={item} />
</div>
}
},
tracks: {
icon: "Album",
label: "Tracks",
renderItem: (props) => {
const { item, onClick } = props
return <div className="suggestion" onClick={onClick}>
<MusicTrack track={item} />
</div>
}
},
playlists: {
icon: "Album",
label: "Playlists",
renderItem: (props) => {
return <div className="suggestion">
<PlaylistItem playlist={props.item} />
</div>
}
}
}
const Results = (props) => {
let { results } = props
if (!results) {
return <antd.Empty />
}
if (typeof results !== "object") {
return <antd.Empty />
return null
}
let keys = Object.keys(results)
let groupsKeys = Object.keys(results)
if (keys.length === 0) {
return <antd.Empty />
}
// check if all keys are valid, if not replace as "others"
keys = keys.map((type) => {
if (ResultRenders[type]) {
return type
}
return "others"
// filter out empty groups
groupsKeys = groupsKeys.filter((key) => {
return results[key].length > 0
})
const handleOnClick = (type, value) => {
if (typeof props.onClick !== "function") {
console.warn("Searcher: onClick is not a function")
return
}
return props.onClick(type, value)
}
return keys.map((type) => {
const decorator = ResultsTypeDecorators[type] ?? {
label: keys,
icon: <Icons.Search />
}
return <div className="category" id={type}>
<div className="suggestions">
<span>
<span className="icon">{createIconRender(decorator.icon)}</span>
<span className="label">{decorator.label}</span>
</span>
{
results[type].map((item) => {
return React.createElement(ResultRenders[type], { item, onClick: (...props) => handleOnClick(type, ...props) })
})
}
</div>
if (groupsKeys.length === 0) {
return <div className="searcher no_results">
<antd.Result
status="info"
title="No results"
subTitle="We are sorry, but we could not find any results for your search."
/>
</div>
})
}
}
const handleClick = (props) => {
if (props.close) {
props.close()
}
}
return <div
className={classnames(
"searcher_results",
{
["one_column"]: groupsKeys.length === 1,
}
)}
>
{
groupsKeys.map((key, index) => {
const decorator = ResultsTypeDecorators[key] ?? {
icon: null,
label: key,
renderItem: () => null
}
return <div
className="searcher_results_category"
key={index}
>
<div className="searcher_results_category_header">
<h1>
{
createIconRender(decorator.icon)
}
<Translation>
{(t) => t(decorator.label)}
</Translation>
</h1>
</div>
<div className="searcher_results_category_suggestions" id={key}>
{
results[key].map((item, index) => {
return decorator.renderItem({
key: index,
item,
onClick: handleClick,
...decorator.props,
})
})
}
</div>
</div>
})
}
</div>
}
export default (props) => {
const [loading, setLoading] = React.useState(false)

View File

@ -9,7 +9,14 @@
transition: all 0.3s ease-in-out;
gap: 20px;
gap: 10px;
.results {
border-radius: 10px;
overflow-x: hidden;
overflow-y: overlay;
}
&.small {
.ant-input-affix-wrapper {
@ -28,6 +35,11 @@
}
.ant-input-affix-wrapper {
position: sticky;
top: 0;
left: 0;
display: inline-flex;
flex-direction: row;
@ -37,6 +49,7 @@
padding: 0;
margin: 0;
min-height: 60px;
height: 60px;
border-radius: 10px;
@ -59,57 +72,90 @@
margin: 0;
}
}
}
.results {
position: relative;
.searcher_results {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 50vw;
gap: 10px;
&.one_column {
grid-template-columns: 1fr;
}
&.no_results {
display: flex;
flex-direction: column;
align-items: flex-start;
align-items: center;
justify-content: center;
}
width: 100%;
padding: 10px;
.searcher_results_category {
background-color: var(--background-color-primary);
padding: 20px;
border-radius: 8px;
gap: 10px;
height: fit-content;
width: 100%;
.category {
h3 {
font-size: 1rem;
font-weight: 600;
gap: 20px;
.searcher_results_category_header {
h1 {
margin: 0;
}
}
width: 100%;
.searcher_results_category_suggestions {
display: flex;
flex-direction: column;
gap: 10px;
.suggestions {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 10px;
gap: 10px;
.playlistItem {
background-color: var(--background-color-accent);
.suggestion {
width: 100%;
max-width: 300px;
height: 80px;
padding: 10px;
.playlistItem_cover {
width: 80px;
height: 80px;
border-radius: 8px;
img {
height: 80px;
width: 80px;
}
}
background-color: var(--background-color-accent);
.playlistItem_info {
max-width: unset;
h1 {
font-size: 1rem;
white-space: break-spaces;
}
}
}
.music-track {
background-color: var(--background-color-accent);
}
}
#playlists {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
}
.ant-empty {
align-self: center;
}
}