import React , { useState , useEffect , useReducer } from 'react' ;
import { Switch , Route , Link , useParams , useHistory } from 'react-router-dom' ;
import './light.css' ;
import { Button , Container , Dropdown , Grid , Header , Icon , Image , Input , Item , Segment , Table } from 'semantic-ui-react' ;
import { statusColor , isAdmin , isInstructor , BasicTable , staticUrl , requester } from './utils.js' ;
import { NotFound } from './Misc.js' ;
import { AdminMemberInfo , AdminMemberPause , AdminMemberForm , AdminMemberCards , AdminMemberTraining , AdminMemberCertifications } from './AdminMembers.js' ;
import { AdminMemberTransactions } from './AdminTransactions.js' ;
import { AdminHistory } from './Admin.js' ;
import { StorageButton } from './Storage.js' ;
import AbortController from 'abort-controller' ;
const memberSorts = {
recently _vetted : 'Recently Vetted' ,
last _scanned : 'Last Scanned' ,
pinball _score : 'Pinball Score' ,
newest _active : 'Newest' ,
//newest_overall: 'Newest Overall',
oldest _active : 'Oldest' ,
//oldest_overall: 'Oldest Overall',
recently _inactive : 'Recently Inactive' ,
is _director : 'Directors' ,
is _instructor : 'Instructors' ,
due : 'Due' ,
overdue : 'Overdue' ,
storage : 'Storage' ,
everyone : 'Everyone' ,
} ;
export function MembersDropdown ( props ) {
const { token , name , onChange , value , initial , autofocus , filterActive } = props ;
const [ response , setResponse ] = useState ( { results : [ ] } ) ;
const searchDefault = { seq : 0 , q : initial || '' , sort : 'newest_active' } ;
const [ search , setSearch ] = useState ( searchDefault ) ;
useEffect ( ( ) => {
requester ( '/search/' , 'POST' , token , search )
. then ( res => {
if ( ! search . seq || res . seq > response . seq ) {
if ( filterActive ) {
setResponse ( { ... res , results : res . results . filter ( x => [ 'Prepaid' , 'Current' , 'Due' , 'Overdue' ] . includes ( x . member . status ) ) } ) ;
} else {
setResponse ( res ) ;
}
}
} )
. catch ( err => {
console . log ( err ) ;
} ) ;
} , [ search ] ) ;
const options = response . results . map ( ( x , i ) => ( {
key : x . member . id ,
value : x . member . id ,
text : x . member . preferred _name + ' ' + x . member . last _name ,
image : { avatar : true , src : x . member . photo _small ? staticUrl + '/' + x . member . photo _small : '/nophoto.png' } ,
} ) ) ;
return (
< Dropdown
clearable
fluid
selection
search
name = { name }
options = { options }
value = { value }
placeholder = 'Search for Member'
onChange = { onChange }
onSearchChange = { ( e , v ) => setSearch ( { seq : parseInt ( e . timeStamp ) , q : v . searchQuery , sort : 'newest_active' } ) }
searchInput = { { autoFocus : autofocus } }
openOnFocus = { ! autofocus }
/ >
) ;
} ;
let responseCache = false ;
let pageCache = 0 ;
let sortCache = '' ;
let searchCache = '' ;
const loadMoreStrings = [
'Load More' ,
'Load EVEN More' ,
'Load WAY More' ,
'Why did you stop? LOAD MORE!' ,
'GIVE ME MORE NAMES!!' ,
'Shower me with names, baby' ,
'I don\'t care about the poor server, MORE NAMES!' ,
'Names make me hotter than two rats in a wool sock' ,
'Holy shit, I can\'t get enough names' ,
'I don\'t have anything better to do than LOAD NAMES!' ,
'I need names because I love N̶ a̸ M̸ E̵ S̴ it\'s not to late to stop but I can\'t because it feels so good god help me' ,
'The One who loads the names will liquify the NERVES of the sentient whilst I o̴ ̐ ̭ b̴ ̾ ̙ s̷ ͝ ̺ e̶ ̄ ̟ r̷ ̓ ̦ v̸ ̐ ͚ e̸ ̈́ ̨ ̷ ̒ ̞ t̸ ͘ ͅ h̴ ͂ ͜ e̵ ̕ ̜ i̶ ̾ ͜ r̷ ̃ ͜ ̵ ͊ ̹ L̷ ͝ ̣ O̸ ̏ ͚ A̶ ̈́ ̘ D̴ ́ ̰ I̸ ̚ ̧ N̵ ̎ ͖ G̷ ͒ ̣ ' ,
'The Song of Names will will e̶ ͋ ̟ ̤ x̷ ̀ ͘ ͜ ̜ t̴ ̀ ̳ i̸ ͑ ̇ ̪ n̷ ̍ ̘ g̵ ̓ ̥ ̗ u̴ ̑ ̈́ ̤ i̷ ̿ ͚ s̸ ̓ ̨ ̪ h̶ ̇ ̓ ̡ ̷ ͊ ̲ t̴ ̇ ̫ h̸ ͗ ̙ ͕ e̸ ̃ ̈́ ̰ ̡ ̷ ̉ ̏ ̘ ̫ v̸ ͗ ̕ ̟ ̧ o̴ ̾ ͕ ͜ i̷ ͛ ̿ ̢ ͅ c̴ ̈́ ̂ ͕ ̥ e̵ ̏ ͕ s̶ ͋ ̀ ̹ ̶ ́ ͠ ̰ ͜ o̷ ̌ ̰ ̯ f̵ ̛ ̊ ̥ ̸ ̒ ͝ ̟ ̟ m̸ ̀ ̂ ̯ o̶ ͛ ̌ ͜ ̝ r̸ ̀ ̞ t̴ ̇ ͗ ̥ a̶ ̈́ ̢ l̶ ͘ ̄ ̯ ̵ ̈́ ̫ m̷ ̑ ̂ ̦ a̶ ͝ ͕ ̨ n̴ ͝ ̎ ̭ from the sphere I can see it can you see it it is beautiful' ,
'The final suffering of T̷ ̂ ͝ ̯ H̴ ̏ ̉ ̰ E̸ ̀ ̓ ̰ ̷ ̒ ͅ ̟ N̷ ̾ ̠ A̵ ̈́ ̟ ̨ M̶ ͝ ̾ ̡ E̸ ̐ ͐ ̥ ̟ S̸ ̍ ̖ are lies all is lost the pony he come h̷ ͂ ̾ ͒ ̔ ͝ ̲ ̺ e̶ ͒ ͠ ̭ ̻ ̷ ͘ ̽ ̽ ̈́ ̒ ̙ ̘ ͈ ̬ ̰ c̵ ͝ ͎ ̺ ̞ ̰ o̷ ͐ ͠ ̛ ̏ ͑ ͚ ̱ ̺ ̰ ̺ m̴ ͝ ̓ ̈ ̖ ̰ e̷ ̆ ̜ s̶ ͝ ̓ ̛ ̹ ̤ ̦ ͉ the i̵ ́ ̅ ̊ ͒ ͠ ̌ ͠ ͊ ̓ ̠ ̞ ̙ ̦ ̱ ̠ c̴ ͑ ̻ ̺ ̙ ͕ ̲ ͚ ͔ ̩ ̥ h̷ ́ ̉ ̾ ̾ ͠ ̦ ̰ ̠ ̯ ̳ ̖ ̧ ̘ o̴ ̎ ̕ ͐ ̊ ͊ ̇ ͋ ̒ ͛ ̅ ͆ ̌ ͂ ̈ ͈ ̯ ̟ ̣ ̲ ͙ ̦ ̖ ̖ ͍ ̞ ̞ ̻ r̷ ̐ ̑ ̉ ̋ ͋ ̉ ͒ ͋ ̍ ́ ̒ ͘ ͐ ͐ ̝ ̲ ̜ ͇ ͉ ̣ ̹ ̖ ͕ ̻ ͅ ̡ ̵ ̄ ̿ ̓ ̈́ ̳ ̖ ͕ ̩ ̝ ̮ ͈ ̻ ̣ ̤ ͎ ̟ ͓ ̜ p̴ ̑ ͌ ͊ ͝ ͑ ̓ ̂ ̽ ͑ ̰ ̝ ͓ ̣ ͍ ̫ ̞ ͓ e̶ ̐ ̋ ́ ̆ ͊ ͘ ͌ ̋ ̛ ̄ ́ ̪ ̜ r̶ ͌ ̔ ̽ ̫ ̬ ͈ m̶ ͌ ̈́ ̛ ͋ ̾ ̈ ̀ ͑ ̽ ̋ ͝ ̏ ̊ ͋ ̱ ̣ ͍ e̶ ̋ ̀ ̈ ̃ ͠ ̨ a̵ ̽ ̿ ̉ ́ ̔ ͠ ̒ ͌ ̓ ̕ ͌ ̂ ̌ ̣ ͜ ̨ ̫ ̡ ̮ ͙ ͈ ͚ ̞ ̰ ̠ ̥ ͇ ̣ t̷ ̐ ͋ ͆ ́ ͛ ̿ ̚ ́ ̏ ̆ ̯ ͚ ̭ ̮ ̠ e̶ ̌ ̢ ̩ ̨ ̞ s̸ ̽ ̚ ͠ ̈́ ͍ ̱ ̻ ͕ ̪ ̗ ̻ ͖ ͇ ̱ ̳ ̧ ̢ ̴ ̒ ͉ ̝ ̖ ̤ ͚ ̖ ̩ ̻ ͅ ̪ a̸ ̀ ͋ ́ ̊ ̓ ͌ ͝ ̕ ́ ͒ ̙ ̥ ̩ ̠ ̝ ̪ ̰ l̵ ́ ͑ ͋ ̐ ̈́ ̓ ͂ ͆ ̅ ̈́ ̎ ̆ ̋ ̇ ̖ ̖ ͚ ̱ ͎ ̤ ̟ ̲ ̺ ͎ l̸ ͠ ͊ ̏ ͐ ̟ ͉ ̞ ͇ ̱ ͉ ̙ ͇ ͅ ̢ ̧ ' ,
] ;
export function Members ( props ) {
const [ response , setResponse ] = useState ( responseCache ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ page , setPage ] = useState ( pageCache ) ;
const [ sort , setSort ] = useState ( sortCache ) ;
const [ search , setSearch ] = useState ( searchCache ) ;
const [ controller , setController ] = useState ( false ) ;
const { token , user } = props ;
const history = useHistory ( ) ;
const makeRequest = ( { loadPage , q , sort _key } ) => {
let pageNum = 0 ;
if ( loadPage ) {
pageNum = page + 1 ;
setPage ( pageNum ) ;
pageCache = pageNum ;
} else {
setResponse ( false ) ;
setPage ( 0 ) ;
pageCache = 0 ;
}
if ( controller ) {
controller . abort ( ) ;
}
const ctl = new AbortController ( ) ;
setController ( ctl ) ;
const signal = ctl . signal ;
const data = { page : pageNum } ;
if ( q ) data . q = q ;
if ( sort _key ) data . sort = sort _key ;
requester ( '/search/' , 'POST' , token , data , signal )
. then ( res => {
const r = loadPage ? { ... response , results : [ ... response . results , ... res . results ] } : res ;
setResponse ( r ) ;
responseCache = r ;
setLoading ( false ) ;
} )
. catch ( err => {
console . log ( 'Aborted.' ) ;
} ) ;
}
const loadMore = ( ) => {
setLoading ( true ) ;
makeRequest ( { loadPage : true , q : search , sort _key : sort } ) ;
} ;
const doSort = ( sort _key ) => {
setSort ( sort _key ) ;
sortCache = sort _key ;
setSearch ( '' ) ;
searchCache = '' ;
makeRequest ( { loadPage : false , sort _key : sort _key } ) ;
} ;
const doSearch = ( q ) => {
if ( q ) {
setSearch ( q ) ;
searchCache = q ;
setSort ( '' ) ;
sortCache = '' ;
makeRequest ( { loadPage : false , q : q } ) ;
} else {
doSort ( 'recently_vetted' ) ;
}
} ;
const handleChange = ( event ) => {
const q = event . target . value ;
doSearch ( q ) ;
} ;
useEffect ( ( ) => {
if ( ! responseCache ) {
doSort ( 'recently_vetted' ) ;
}
} , [ ] ) ;
return (
< Container >
< Header size = 'large' > Member List < / H e a d e r >
< p > Search by name , email , Spacebar username , member ID , or member shelf : < / p >
< Input autoFocus focus icon = 'search'
placeholder = 'Search...'
value = { search }
onChange = { handleChange }
aria - label = 'search products'
style = { { marginRight : '0.5rem' } }
/ >
{ search . length ?
< Button
content = 'Clear'
onClick = { ( ) => doSearch ( '' ) }
/ > : ' '
}
< p > < / p >
< p >
Sort by { ' ' }
{ Object . entries ( memberSorts ) . map ( ( x , i ) =>
< React . Fragment key = { x [ 0 ] } >
< a onClick = { ( ) => doSort ( x [ 0 ] ) } > { x [ 1 ] } < / a >
{ i < Object . keys ( memberSorts ) . length - 1 && ', ' }
< / R e a c t . F r a g m e n t >
) } .
< / p >
< Header size = 'medium' >
{ search . length ? 'Search Results' : memberSorts [ sort ] }
< / H e a d e r >
{ sort === 'last_scanned' &&
( user . member . allow _last _scanned ?
< p > Hide yourself from this list on the < Link to = '/account' > Account Settings < / L i n k > p a g e . < / p >
:
< p > Participate in this list on the < Link to = '/account' > Account Settings < / L i n k > p a g e . < / p >
)
}
{ response ?
< >
< p > { response . total } result { response . total === 1 ? '' : 's' } : < / p >
< Item . Group unstackable divided >
{ ! ! response . results . length &&
response . results . map ( ( x , i ) =>
< Item key = { x . member . id } as = { Link } to = { '/members/' + x . member . id } >
< div className = 'list-num' > { i + 1 } < / d i v >
< Item . Image size = 'tiny' src = { x . member . photo _small ? staticUrl + '/' + x . member . photo _small : '/nophoto.png' } / >
< Item . Content verticalAlign = 'top' >
< Item . Header >
< Icon name = 'circle' color = { statusColor [ x . member . status ] } / >
{ x . member . preferred _name } { x . member . last _name }
< / I t e m . H e a d e r >
{ sort === 'pinball_score' ?
< >
< Item . Description > Score : { x . member . pinball _score . toLocaleString ( ) || 'Unknown' } < / I t e m . D e s c r i p t i o n >
< Item . Description > Rank : { i === 0 ? 'Pinball Wizard' : 'Not the Pinball Wizard' } < / I t e m . D e s c r i p t i o n >
< / >
:
< >
< Item . Description >
Shelf : { x . member . storage . length ?
x . member . storage . sort ( ( a , b ) => a . location === 'member_shelves' ? - 1 : 1 ) . map ( ( x , i ) =>
< StorageButton storage = { x } / >
)
:
'None'
}
< / I t e m . D e s c r i p t i o n >
{ sort === 'newest_active' ?
< Item . Description > Started : { x . member . current _start _date || 'Unknown' } < / I t e m . D e s c r i p t i o n >
:
< Item . Description > Joined : { x . member . application _date || 'Unknown' } < / I t e m . D e s c r i p t i o n >
}
< Item . Description >
{ x . member . public _bio . substring ( 0 , 100 ) }
{ x . member . public _bio . length > 100 && '...' }
< / I t e m . D e s c r i p t i o n >
< / >
}
< / I t e m . C o n t e n t >
< / I t e m >
)
}
< / I t e m . G r o u p >
{ ! search && response . total !== response . results . length &&
< Button content = { loading ? 'Reticulating splines...' : loadMoreStrings [ page ] } onClick = { loadMore } disabled = { loading } / >
}
< / >
:
< p > Loading ... < / p >
}
< / C o n t a i n e r >
) ;
} ;
let resultCache = { } ;
export function MemberDetail ( props ) {
const id = parseInt ( useParams ( ) . id )
const [ result , setResult ] = useState ( resultCache [ id ] || false ) ;
const [ refreshCount , refreshResult ] = useReducer ( x => x + 1 , 0 ) ;
const [ error , setError ] = useState ( false ) ;
const { token , user } = props ;
const member = result . member || false ;
const memberFullName = [ member . preferred _name , member . last _name ] . join ( ' ' )
const isSponsoring = user . member . sponsorship ? . find ( m => m . id === id )
const isMe = user . member . id === id
const photo = member ? . photo _large || member ? . photo _small || false ;
useEffect ( ( ) => {
requester ( '/search/' + id + '/' , 'GET' , token )
. then ( res => {
setResult ( res ) ;
resultCache [ id ] = res ;
} )
. catch ( err => {
console . log ( err ) ;
setError ( true ) ;
} ) ;
} , [ refreshCount ] ) ;
function sponsorMember ( value ) {
return ( ) => {
requester ( ` /sponsorship/ ${ id } /offer/ ` , 'POST' , token , { value } )
. then ( res => {
const _user = { ... user }
const sponsorship = _user . member . sponsorship
if ( value ) sponsorship . push ( { id } )
else sponsorship . splice ( sponsorship . findIndex ( m => m . id === id ) , 1 )
props . setUser ( _user )
} )
. catch ( err => {
console . log ( err ) ;
setError ( true ) ;
} ) ;
}
}
return (
< Container >
{ ! error ?
member ?
< div >
< Header size = 'large' > { member . preferred _name } { member . last _name } < / H e a d e r >
{ isAdmin ( user ) &&
< p > Admin : { ' ' }
< Link to = { '/members/' + member . id } > Profile < / L i n k > { ' - ' }
< Link to = { '/members/' + member . id + '/details' } > Details < / L i n k > { ' - ' }
< Link to = { '/members/' + member . id + '/cards' } > Cards < / L i n k > { ' - ' }
< Link to = { '/members/' + member . id + '/lockouts' } > Lockouts < / L i n k > { ' - ' }
< Link to = { '/members/' + member . id + '/training' } > Training < / L i n k > { ' - ' }
< Link to = { '/members/' + member . id + '/transactions' } > Transactions < / L i n k > { ' - ' }
< Link to = { '/members/' + member . id + '/history' } > History < / L i n k >
< / p >
}
< Switch >
{ isAdmin ( user ) && < Route path = '/members/:id/details' >
< Grid stackable columns = { 2 } >
< Grid . Column width = { 8 } >
< AdminMemberInfo result = { result } refreshResult = { refreshResult } { ... props } / >
< Segment padded >
< AdminMemberPause result = { result } refreshResult = { refreshResult } { ... props } / >
< / S e g m e n t >
< / G r i d . C o l u m n >
< Grid . Column width = { 8 } >
< Segment padded >
< AdminMemberForm result = { result } refreshResult = { refreshResult } { ... props } / >
< / S e g m e n t >
< / G r i d . C o l u m n >
< / G r i d >
< / R o u t e > }
{ isAdmin ( user ) && < Route path = '/members/:id/cards' >
< AdminMemberCards result = { result } refreshResult = { refreshResult } { ... props } / >
< / R o u t e > }
{ isAdmin ( user ) && < Route path = '/members/:id/lockouts' >
< AdminMemberCertifications result = { result } refreshResult = { refreshResult } { ... props } / >
< / R o u t e > }
{ isAdmin ( user ) && < Route path = '/members/:id/training' >
< AdminMemberTraining result = { result } refreshResult = { refreshResult } { ... props } / >
< / R o u t e > }
{ isAdmin ( user ) && < Route path = '/members/:id/transactions' >
< AdminMemberTransactions result = { result } refreshResult = { refreshResult } { ... props } / >
< / R o u t e > }
{ isAdmin ( user ) && < Route path = '/members/:id/history' >
< AdminHistory filterMember = { member . id } { ... props } / >
< / R o u t e > }
< Route path = '/members/:id' >
< Grid stackable columns = { 2 } >
< Grid . Column width = { 5 } >
< p >
< Image rounded size = 'medium' src = { photo ? staticUrl + '/' + photo : '/nophoto.png' } / >
< / p >
< >
< BasicTable >
< Table . Body >
< Table . Row >
< Table . Cell > Status : < / T a b l e . C e l l >
< Table . Cell >
< Icon name = 'circle' color = { statusColor [ member . status ] } / >
{ member . status || 'Unknown' }
< / T a b l e . C e l l >
< / T a b l e . R o w >
< Table . Row >
< Table . Cell > Joined : < / T a b l e . C e l l >
< Table . Cell > { member . application _date || 'Unknown' } < / T a b l e . C e l l >
< / T a b l e . R o w >
< Table . Row >
< Table . Cell > Public Bio : < / T a b l e . C e l l >
< / T a b l e . R o w >
< / T a b l e . B o d y >
< / B a s i c T a b l e >
< p className = 'bio-paragraph' >
{ member . public _bio || 'None yet.' }
< / p >
{ ! isMe && ! isSponsoring && < Button onClick = { sponsorMember ( true ) } > Vouch for { member . preferred _name } < / B u t t o n > }
{ ! isMe && isSponsoring && < Button onClick = { sponsorMember ( false ) } > Revoke guarantee < / B u t t o n > }
< / >
< / G r i d . C o l u m n >
< Grid . Column width = { 11 } >
{ isInstructor ( user ) && ! isAdmin ( user ) && < Segment padded >
< AdminMemberTraining result = { result } refreshResult = { refreshResult } { ... props } / >
< / S e g m e n t > }
< / G r i d . C o l u m n >
< / G r i d >
< / R o u t e >
< / S w i t c h >
< / d i v >
:
< p > Loading ... < / p >
:
< NotFound / >
}
< / C o n t a i n e r >
) ;
} ;