Add a date selector to the search function (#494)

* add date picker

* lint

* add date picker

* lint

* refine

* fix/finish the date picker UI part

* finish query parameter passing & incremental cleanup

* fix/finish the date picker UI part

* finish query parameter passing & incremental cleanup

* fix bad merge

* fix linting errors

* wrap for mobile

* add date picker

* lint

* add date picker

* lint

* refine

* fix/finish the date picker UI part

* finish query parameter passing & incremental cleanup

* fix/finish the date picker UI part

* finish query parameter passing & incremental cleanup

* fix bad merge

* fix linting errors

* wrap for mobile

* merge glitch?

* enhance a little

---------

Co-authored-by: rleed <rleed1@pm.me>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
rleed 2023-10-04 16:44:06 -03:00 committed by GitHub
parent b3aee502a0
commit 247744a83c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 206 additions and 53 deletions

View File

@ -79,7 +79,7 @@ export default {
items
}
},
search: async (parent, { q: query, sub, cursor, sort, what, when }, { me, models, search }) => {
search: async (parent, { q: query, sub, cursor, sort, what, when, from: whenFrom, to: whenTo }, { me, models, search }) => {
const decodedCursor = decodeCursor(cursor)
let sitems
@ -199,6 +199,16 @@ export default {
break
}
const whenRange = when === 'custom'
? {
gte: new Date(whenFrom),
lte: new Date(Math.min(new Date(whenTo), decodedCursor.time))
}
: {
lte: decodedCursor.time,
gte: whenGte
}
try {
sitems = await search.search({
index: 'item',
@ -232,10 +242,7 @@ export default {
{
range:
{
createdAt: {
lte: decodedCursor.time,
gte: whenGte
}
createdAt: whenRange
}
},
{ range: { wvotes: { gt: -1 * ITEM_FILTER_THRESHOLD } } }

View File

@ -7,7 +7,7 @@ export default gql`
pageTitleAndUnshorted(url: String!): TitleUnshorted
dupes(url: String!): [Item!]
related(cursor: String, title: String, id: ID, minMatch: String, limit: Int): Items
search(q: String, sub: String, cursor: String, what: String, sort: String, when: String): Items
search(q: String, sub: String, cursor: String, what: String, sort: String, when: String, from: String, to: String): Items
auctionPosition(sub: String, id: ID, bid: Int!): Int!
itemRepetition(parentId: ID): Int!
}

View File

@ -21,6 +21,8 @@ import TextareaAutosize from 'react-textarea-autosize'
import { useToast } from './toast'
import { useInvoiceable } from './invoice'
import { numWithUnits } from '../lib/format'
import ReactDatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'
export function SubmitButton ({
children, variant, value, onClick, disabled, cost, ...props
@ -589,3 +591,29 @@ export function Select ({ label, items, groupClassName, onChange, noForm, overri
</FormGroup>
)
}
export function DatePicker ({ fromName, toName, noForm, onMount, ...props }) {
const formik = noForm ? null : useFormikContext()
const onChangeHandler = props.onChange
const [,, fromHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: fromName })
const [,, toHelpers] = noForm ? [{}, {}, {}] : useField({ ...props, name: toName })
useEffect(() => {
if (onMount) {
const [from, to] = onMount()
fromHelpers.setValue(from)
toHelpers.setValue(to)
}
}, [])
return (
<ReactDatePicker
{...props}
onChange={([from, to], e) => {
fromHelpers.setValue(from?.toISOString())
toHelpers.setValue(to?.toISOString())
onChangeHandler(formik, [from, to], e)
}}
/>
)
}

View File

@ -2,7 +2,7 @@ import Container from 'react-bootstrap/Container'
import styles from './search.module.css'
import SearchIcon from '../svgs/search-line.svg'
import { useEffect, useRef, useState } from 'react'
import { Form, Input, Select, SubmitButton } from './form'
import { Form, Input, Select, DatePicker, SubmitButton } from './form'
import { useRouter } from 'next/router'
export default function Search ({ sub }) {
@ -35,6 +35,7 @@ export default function Search ({ sub }) {
if (values.what === '' || values.what === 'all') delete values.what
if (values.sort === '' || values.sort === 'zaprank') delete values.sort
if (values.when === '' || values.when === 'forever') delete values.when
if (values.when !== 'custom') { delete values.from; delete values.to }
await router.push({
pathname: prefix + '/search',
query: values
@ -46,13 +47,20 @@ export default function Search ({ sub }) {
const what = router.pathname.startsWith('/stackers') ? 'stackers' : router.query.what || 'all'
const sort = router.query.sort || 'zaprank'
const when = router.query.when || 'forever'
const from = router.query.from || new Date().toISOString()
const to = router.query.to || new Date().toISOString()
const [datePicker, setDatePicker] = useState(when === 'custom')
// The following state is needed for the date picker (and driven by the date picker).
// Substituting router.query or formik values would cause network lag and/or timezone issues.
const [range, setRange] = useState({ start: new Date(from), end: new Date(to) })
return (
<>
<div className={styles.searchSection}>
<Container className={`px-md-0 ${styles.searchContainer} ${filter ? styles.leaveRoom : ''}`}>
<Container className={`px-md-0 ${styles.searchContainer}`}>
<Form
initial={{ q, what, sort, when }}
initial={{ q, what, sort, when, from, to }}
onSubmit={search}
>
<div className={`${styles.active} my-3`}>
@ -74,37 +82,61 @@ export default function Search ({ sub }) {
</SubmitButton>
</div>
{filter &&
<div className='text-muted fw-bold d-flex align-items-center'>
<Select
groupClassName='me-2 mb-0'
onChange={(formik, e) => search({ ...formik?.values, what: e.target.value })}
name='what'
size='sm'
overrideValue={what}
items={['all', 'posts', 'comments', 'stackers']}
/>
{what !== 'stackers' &&
<>
by
<Select
groupClassName='mx-2 mb-0'
onChange={(formik, e) => search({ ...formik?.values, sort: e.target.value })}
name='sort'
size='sm'
overrideValue={sort}
items={['zaprank', 'match', 'recent', 'comments', 'sats']}
/>
for
<Select
groupClassName='mb-0 ms-2'
onChange={(formik, e) => search({ ...formik?.values, when: e.target.value })}
name='when'
size='sm'
overrideValue={when}
items={['forever', 'day', 'week', 'month', 'year']}
/>
</>}
<div className='text-muted fw-bold d-flex align-items-center flex-wrap pb-2'>
<div className='text-muted fw-bold d-flex align-items-center pb-2'>
<Select
groupClassName='me-2 mb-0'
onChange={(formik, e) => search({ ...formik?.values, what: e.target.value })}
name='what'
size='sm'
overrideValue={what}
items={['all', 'posts', 'comments', 'stackers']}
/>
{what !== 'stackers' &&
<>
by
<Select
groupClassName='mx-2 mb-0'
onChange={(formik, e) => search({ ...formik?.values, sort: e.target.value })}
name='sort'
size='sm'
overrideValue={sort}
items={['zaprank', 'match', 'recent', 'comments', 'sats']}
/>
for
<Select
groupClassName='mb-0 mx-2'
onChange={(formik, e) => {
search({ ...formik?.values, when: e.target.value, from: from || new Date().toISOString(), to: to || new Date().toISOString() })
setDatePicker(e.target.value === 'custom')
if (e.target.value === 'custom') setRange({ start: new Date(), end: new Date() })
}}
name='when'
size='sm'
overrideValue={when}
items={['custom', 'forever', 'day', 'week', 'month', 'year']}
/>
</>}
</div>
{datePicker &&
<DatePicker
fromName='from' toName='to'
className='form-control p-0 px-2 mb-2 text-center'
onMount={() => {
setRange({ start: new Date(from), end: new Date(to) })
return [from, to]
}}
onChange={(formik, [start, end], e) => {
setRange({ start, end })
search({ ...formik?.values, from: start && start.toISOString(), to: end && end.toISOString() })
}}
selected={range.start}
startDate={range.start} endDate={range.end}
selectsRange
dateFormat='MM/dd/yy'
maxDate={new Date()}
minDate={new Date('2021-05-01')}
/>}
</div>}
</Form>
</Container>

View File

@ -7,14 +7,9 @@
}
.searchContainer {
height: 88px;
position: relative;
}
.leaveRoom {
height: 130px !important;
}
.search {
width: 50px;
height: 50px;

View File

@ -48,11 +48,11 @@ export const SUB_ITEMS = gql`
export const SUB_SEARCH = gql`
${SUB_FIELDS}
${ITEM_FULL_FIELDS}
query SubSearch($sub: String, $q: String, $cursor: String, $sort: String, $what: String, $when: String) {
query SubSearch($sub: String, $q: String, $cursor: String, $sort: String, $what: String, $when: String, $from: String, $to: String) {
sub(name: $sub) {
...SubFields
}
search(sub: $sub, q: $q, cursor: $cursor, sort: $sort, what: $what, when: $when) {
search(sub: $sub, q: $q, cursor: $cursor, sort: $sort, what: $what, when: $when, from: $from, to: $to) {
cursor
items {
...ItemFullFields

102
package-lock.json generated
View File

@ -60,6 +60,7 @@
"react-avatar-editor": "^13.0.0",
"react-bootstrap": "^2.8.0",
"react-countdown": "^2.3.5",
"react-datepicker": "^4.18.0",
"react-dom": "^18.2.0",
"react-longpressable": "^1.1.1",
"react-markdown": "^8.0.7",
@ -5971,9 +5972,12 @@
}
},
"node_modules/date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
@ -14489,6 +14493,23 @@
"react-dom": ">= 15"
}
},
"node_modules/react-datepicker": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.18.0.tgz",
"integrity": "sha512-0MYt3HmLbHVk1sw4v+RCbLAVg5TA3jWP7RyjZbo53PC+SEi+pjdgc92lB53ai/ENZaTOhbXmgni9GzvMrorMAw==",
"dependencies": {
"@popperjs/core": "^2.11.8",
"classnames": "^2.2.6",
"date-fns": "^2.30.0",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.13.0",
"react-popper": "^2.3.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18",
"react-dom": "^16.9.0 || ^17 || ^18"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@ -14599,6 +14620,38 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/react-onclickoutside": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz",
"integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==",
"funding": {
"type": "individual",
"url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
},
"peerDependencies": {
"react": "^15.5.x || ^16.x || ^17.x || ^18.x",
"react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
}
},
"node_modules/react-popper": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
"dependencies": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
},
"peerDependencies": {
"@popperjs/core": "^2.0.0",
"react": "^16.8.0 || ^17 || ^18",
"react-dom": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-popper/node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
"node_modules/react-resize-detector": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-8.1.0.tgz",
@ -22658,9 +22711,12 @@
"integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA=="
},
"date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"requires": {
"@babel/runtime": "^7.21.0"
}
},
"debug": {
"version": "4.3.4",
@ -28271,6 +28327,19 @@
"prop-types": "^15.7.2"
}
},
"react-datepicker": {
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.18.0.tgz",
"integrity": "sha512-0MYt3HmLbHVk1sw4v+RCbLAVg5TA3jWP7RyjZbo53PC+SEi+pjdgc92lB53ai/ENZaTOhbXmgni9GzvMrorMAw==",
"requires": {
"@popperjs/core": "^2.11.8",
"classnames": "^2.2.6",
"date-fns": "^2.30.0",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.13.0",
"react-popper": "^2.3.0"
}
},
"react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@ -28356,6 +28425,27 @@
}
}
},
"react-onclickoutside": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz",
"integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A=="
},
"react-popper": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
"requires": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
},
"dependencies": {
"react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
}
}
},
"react-resize-detector": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-8.1.0.tgz",

View File

@ -63,6 +63,7 @@
"react-avatar-editor": "^13.0.0",
"react-bootstrap": "^2.8.0",
"react-countdown": "^2.3.5",
"react-datepicker": "^4.18.0",
"react-dom": "^18.2.0",
"react-longpressable": "^1.1.1",
"react-markdown": "^8.0.7",