Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

search results infinite scroll #20

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"react-dom": "^15.4.1",
"react-router": "^3.0.2",
"react-scripts": "0.7.0",
"react-waypoint": "^5.1.0",
"recursive-readdir": "2.1.0",
"rimraf": "2.5.4",
"strip-ansi": "3.0.1",
Expand Down
44 changes: 31 additions & 13 deletions client/src/components/PackageSearch/Client.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
import fetch from 'isomorphic-fetch'

function search (query, range, dev, cb) {
query = encodeURIComponent(query)
range = encodeURIComponent(range)

return fetch(`/api/query/${query}${range ? '/' : ''}${range}${dev ? '?dev=1' : ''}`, {
accept: 'application/json'
}).then(checkStatus)
.then(parseJSON)
.then(cb)
.catch(function (err) {
cb({
error: true,
message: err.message
return (limit = 50, gt) => {
const _query = encodeURIComponent(query)
const _range = encodeURIComponent(range)

return fetch(withParamteres(`/api/query/${_query}${_range ? '/' : ''}${_range}`, [
{ name: 'dev', value: 1, condition: dev },
{ name: 'limit', value: limit },
{ name: 'gt', value: gt }
]), {
accept: 'application/json'
}).then(checkStatus)
.then(parseJSON)
.then(cb)
.catch(function (err) {
cb({
error: true,
message: err.message
})
})
})
}
}

function withParamteres (path, parameters) {
const condition = (val) => typeof val.condition === 'undefined'
? typeof val.value !== 'undefined'
: val.condition

return parameters.reduce((acc, val) =>
condition(val)
? `${acc}${(acc.indexOf('?') === -1) ? '?' : '&'}${val.name}=${val.value}`
: acc
, path)
}

function checkStatus (response) {
Expand Down
156 changes: 36 additions & 120 deletions client/src/components/PackageSearch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import ReactDOM from 'react-dom'
import classnames from 'classnames'

import SearchInfo from '../SearchInfo'
import SearchResults from '../SearchResults'
import Waypoint from 'react-waypoint'

import './style.css'

Expand All @@ -15,6 +17,17 @@ const resetState = {
}

const PackageSearch = React.createClass({

propTypes: {
limit: React.PropTypes.number
},

getDefaultProps () {
return {
limit: 50
}
},

getResetState (name, range, dev) {
let state = Object.assign({}, resetState, {})

Expand Down Expand Up @@ -118,6 +131,10 @@ const PackageSearch = React.createClass({

let _name = name

const res = (result) => [...(this.state.results||[]), ...result]

const gt = (result) => result[result.length - 1] && result[result.length - 1].name

Client.search(name, range, dev, (result) => {
if (result.error) {
let errorState = Object.assign({}, resetState, {
Expand All @@ -127,12 +144,13 @@ const PackageSearch = React.createClass({
this.setState(errorState)
} else {
this.setState({
results: result,
results: res(result),
queryName: _name,
isLoading: false
isLoading: false,
gt: gt(result)
})
}
})
})(this.props.limit, this.state.gt)
},

componentWillReceiveProps (nextProps) {
Expand All @@ -143,6 +161,8 @@ const PackageSearch = React.createClass({

let newState = this.getResetState(packageParam, versionParam, devParam)

newState.gt = undefined

this.setState(newState, () => {
this.runSearch(packageParam, versionParam, devParam)
})
Expand Down Expand Up @@ -201,6 +221,18 @@ const PackageSearch = React.createClass({
)
},

onEndOfPage (event) {
if (event.event == null) {
// waypoint is within viewport
return false
}

const resultsLength = this.state.results ? this.state.results.length : 0
if (resultsLength % this.props.limit === 0) {
this.runSearch()
}
},

render () {
const { searchValueForName, searchValueForRange } = this.state
const showClearIconForName = searchValueForName.length > 0
Expand Down Expand Up @@ -305,126 +337,10 @@ const PackageSearch = React.createClass({
) : null}
<SearchResults {...this.state} />
</div>
<Waypoint onEnter={this.onEndOfPage} />
</div>
)
}
})

function SearchResults (props) {
const {
results,
searchValueForName,
searchValueForRange,
errorMessage
} = props

if (errorMessage.length > 0) {
return (
<h2 className='ui center disabled aligned icon header'>
<i className='circular warning sign icon' />
Error:
<p>
<small>"{ errorMessage }"</small>
</p>
</h2>
)
}

if (results === null) {
return (
<Message>
Search for { searchValueForName || 'a package' }
{ searchValueForRange ? ('@' + searchValueForRange) : '' }
</Message>
)
}

if (results.length === 0) {
return (
<Message>
No results found
</Message>
)
}

return (
<div>
{results.length > 0 ? <SearchResultsModules {...props} /> : null}
</div>
)
}

const Message = ({ children }) => (
<h2 className='ui center disabled aligned icon header'>
<i className='circular search icon' />
{children}
</h2>
)

function SearchResultsCount ({ results }) {
if (!results) {
return (
<p>
{ ' '.replace(/ /g, '\u00a0') }
</p>
)
} else {
return (
<p>
Found <b>{results}</b> dependents:
</p>
)
}
}

class SearchResultsModules extends React.Component {
shouldComponentUpdate (nextProps) {
return (
nextProps.queryName !== this.props.queryName
)
}

render () {
const {
results,
queryName
} = this.props

return (
<div>
<SearchResultsCount
results={results.length}
/>
<table className='ui selectable structured large table'>
<thead className='left'>
<tr>
<th className='six wide'>Dependent package name</th>
<th>Latest dependent version</th>
<th>Range for <i>{ queryName }</i></th>
</tr>
</thead>
<tbody>
{
results.map((_package, idx) => (
<tr key={idx}>
<td>
<a
href={'https://www.npmjs.com/package/' + _package.name}
target='_blank'
>
{_package.name}
</a>
</td>
<td className='left aligned'>{_package.version}</td>
<td className='left aligned'>{_package.range}</td>
</tr>
))
}
</tbody>
</table>
</div>
)
}
}

export default PackageSearch
120 changes: 120 additions & 0 deletions client/src/components/SearchResults/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from 'react'

function SearchResults (props) {
const {
results,
searchValueForName,
searchValueForRange,
errorMessage
} = props

if (errorMessage.length > 0) {
return (
<h2 className='ui center disabled aligned icon header'>
<i className='circular warning sign icon' />
Error:
<p>
<small>"{ errorMessage }"</small>
</p>
</h2>
)
}

if (results === null) {
return (
<Message>
Search for { searchValueForName || 'a package' }
{ searchValueForRange ? ('@' + searchValueForRange) : '' }
</Message>
)
}

if (results.length === 0) {
return (
<Message>
No results found
</Message>
)
}

return (
<div>
{results.length > 0 ? <SearchResultsModules {...props} /> : null}
</div>
)
}

const Message = ({ children }) => (
<h2 className='ui center disabled aligned icon header'>
<i className='circular search icon' />
{children}
</h2>
)

function SearchResultsCount ({ results }) {
if (!results) {
return (
<p>
{ ' '.replace(/ /g, '\u00a0') }
</p>
)
} else {
return (
<p>
Found <b>{results}</b> dependents.
</p>
)
}
}

class SearchResultsModules extends React.Component {
shouldComponentUpdate (nextProps) {
return (
nextProps.queryName !== this.props.queryName || this.props.results.length < nextProps.results.length
)
}

render () {
const {
results,
queryName
} = this.props

return (
<div>
<table className='ui selectable structured large table'>
<thead className='left'>
<tr>
<th className='six wide'>Dependent package name</th>
<th>Latest dependent version</th>
<th>Range for <i>{ queryName }</i></th>
</tr>
</thead>
<tbody>
{
results.map((_package, idx) => (
<tr key={idx}>
<td>
<a
href={'https://www.npmjs.com/package/' + _package.name}
target='_blank'
>
{_package.name}
</a>
</td>
<td className='left aligned'>{_package.version}</td>
<td className='left aligned'>{_package.range}</td>
</tr>
))
}
</tbody>
</table>
<SearchResultsCount
results={results.length}
/>
</div>
)
}
}

export default SearchResults