mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 02:54:15 +00:00
added UserSelector
component
This commit is contained in:
parent
79189e6c6a
commit
9e4c8a8607
231
packages/app/src/components/UserSelector/index.jsx
Normal file
231
packages/app/src/components/UserSelector/index.jsx
Normal file
@ -0,0 +1,231 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
|
||||
import { UserPreview } from "components"
|
||||
import { Icons, createIconRender } from "components/Icons"
|
||||
|
||||
import useRequest from "comty.js/hooks/useRequest"
|
||||
|
||||
import SearchModel from "comty.js/models/search"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const ResultsTypeDecorators = {
|
||||
"friends": {
|
||||
icon: "MdPeople",
|
||||
label: "Recent"
|
||||
},
|
||||
"users": {
|
||||
icon: "Users",
|
||||
label: "Users"
|
||||
}
|
||||
}
|
||||
|
||||
const SelectableResults = (props) => {
|
||||
const [invitedUsers, setInvitedUsers] = React.useState(props.invitedUsers ?? [])
|
||||
|
||||
let { results } = props
|
||||
|
||||
if (!results) {
|
||||
return <antd.Empty />
|
||||
}
|
||||
|
||||
if (typeof results !== "object") {
|
||||
return <antd.Empty />
|
||||
}
|
||||
|
||||
let keys = React.useMemo(() => {
|
||||
let _keys = Object.keys(results)
|
||||
|
||||
// check if all keys are valid, if not replace as "others"
|
||||
_keys = _keys.map((type) => {
|
||||
if (ResultsTypeDecorators[type]) {
|
||||
return type
|
||||
}
|
||||
|
||||
return "others"
|
||||
})
|
||||
|
||||
return _keys
|
||||
}, [results])
|
||||
|
||||
if (keys.length === 0) {
|
||||
return <antd.Empty />
|
||||
}
|
||||
|
||||
const handleOnClick = (value) => {
|
||||
if (props.onInviteUser) {
|
||||
props.onInviteUser(value)
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!props.invitedUsers || !Array.isArray(props.invitedUsers)) {
|
||||
return
|
||||
}
|
||||
|
||||
setInvitedUsers(props.invitedUsers)
|
||||
}, [props.invitedUsers])
|
||||
|
||||
return <>
|
||||
{
|
||||
keys.map((type, index) => {
|
||||
const result = results[type]
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div key={index} className="user-selector_group">
|
||||
<div className="user-selector_group_header">
|
||||
{
|
||||
createIconRender(ResultsTypeDecorators[type].icon)
|
||||
}
|
||||
<span>{ResultsTypeDecorators[type].label ?? "Unknown"}</span>
|
||||
</div>
|
||||
|
||||
<div className="user-selector_group_results">
|
||||
{
|
||||
result.map((item, index) => {
|
||||
const invited = invitedUsers.find((user) => user._id === item._id)
|
||||
|
||||
return <div
|
||||
key={index}
|
||||
className={classnames(
|
||||
"user-selector_result_item",
|
||||
{
|
||||
["clicked"]: invited,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<UserPreview user={item} onClick={() => { }} />
|
||||
|
||||
<antd.Button
|
||||
type="primary"
|
||||
onClick={() => handleOnClick(item)}
|
||||
disabled={invited}
|
||||
>
|
||||
{
|
||||
invited ? "Invited" : "Invite"
|
||||
}
|
||||
</antd.Button>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
const UserSelector = (props) => {
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
|
||||
const [invitedUsers, setInvitedUsers] = React.useState([])
|
||||
|
||||
const [searchResult, setSearchResult] = React.useState(null)
|
||||
const [searchValue, setSearchValue] = React.useState("")
|
||||
|
||||
const [L_QuickSearch, R_QuickSearch, E_QuickSearch, M_QuickSearch] = useRequest(SearchModel.quickSearch,)
|
||||
|
||||
const makeSearch = async (value) => {
|
||||
if (value === "") {
|
||||
return setSearchResult(null)
|
||||
}
|
||||
|
||||
// make search request
|
||||
const result = await SearchModel.search(value)
|
||||
|
||||
return setSearchResult(result)
|
||||
}
|
||||
|
||||
const handleOnSearch = (e) => {
|
||||
// not allow to input space as first character
|
||||
if (e.target.value[0] === " ") {
|
||||
return
|
||||
}
|
||||
|
||||
setSearchValue(e.target.value)
|
||||
}
|
||||
|
||||
const handleUserInvite = (user) => {
|
||||
setInvitedUsers((users) => {
|
||||
return [...users, user]
|
||||
})
|
||||
|
||||
app.cores.sync.music.inviteToUser(user._id)
|
||||
|
||||
// create a timeout to remove user from invited list in 10 seconds
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(async () => {
|
||||
setLoading(true)
|
||||
|
||||
await makeSearch(searchValue)
|
||||
|
||||
setLoading(false)
|
||||
}, 400)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [searchValue])
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"user-selector",
|
||||
{ ["open"]: searchValue }
|
||||
)}
|
||||
>
|
||||
<antd.Input
|
||||
placeholder="Start typing to search..."
|
||||
onChange={handleOnSearch}
|
||||
value={searchValue}
|
||||
prefix={<Icons.Search />}
|
||||
autoFocus={props.autoFocus ?? false}
|
||||
onFocus={props.onFocus}
|
||||
onBlur={props.onUnfocus}
|
||||
/>
|
||||
|
||||
{
|
||||
searchResult && <div className="user-selector_results">
|
||||
{
|
||||
loading && <antd.Skeleton active />
|
||||
}
|
||||
{
|
||||
!loading && <SelectableResults
|
||||
results={searchResult}
|
||||
onInviteUser={handleUserInvite}
|
||||
invitedUsers={invitedUsers}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!searchResult && <div className="user-selector_results">
|
||||
{
|
||||
L_QuickSearch && <antd.Skeleton active />
|
||||
}
|
||||
{
|
||||
E_QuickSearch && <antd.Result
|
||||
status="error"
|
||||
title="Error loading results"
|
||||
/>
|
||||
}
|
||||
{
|
||||
!L_QuickSearch && !E_QuickSearch && <SelectableResults
|
||||
results={R_QuickSearch}
|
||||
onInviteUser={handleUserInvite}
|
||||
invitedUsers={invitedUsers}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export const openModal = (props) => {
|
||||
return app.ModalController.open(() => <UserSelector {...props} />)
|
||||
}
|
79
packages/app/src/components/UserSelector/index.less
Normal file
79
packages/app/src/components/UserSelector/index.less
Normal file
@ -0,0 +1,79 @@
|
||||
.user-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
.user-selector_actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.user-selector_results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
.user-selector_group {
|
||||
gap: 10px;
|
||||
|
||||
background-color: var(--background-color-primary);
|
||||
border-radius: 12px;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
.user-selector_group_header {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.user-selector_group_results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 5px;
|
||||
|
||||
.user-selector_result_item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
padding: 6px;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&.selected {
|
||||
background-color: var(--background-color-accent);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.userPreview {
|
||||
width: 100%;
|
||||
|
||||
.info {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user