working search

This commit is contained in:
keyan 2022-01-27 13:18:48 -06:00
parent cc567d301e
commit afed19430c
14 changed files with 170 additions and 46 deletions

View File

@ -275,7 +275,7 @@ export default {
comments: async (parent, { id, sort }, { models }) => { comments: async (parent, { id, sort }, { models }) => {
return comments(models, id, sort) return comments(models, id, sort)
}, },
search: async (parent, { query, cursor }, { models, search }) => { search: async (parent, { q: query, cursor }, { models, search }) => {
const decodedCursor = decodeCursor(cursor) const decodedCursor = decodeCursor(cursor)
const sitems = await search.search({ const sitems = await search.search({
@ -503,7 +503,7 @@ export default {
const [{ count }] = await models.$queryRaw` const [{ count }] = await models.$queryRaw`
SELECT count(*) SELECT count(*)
FROM "Item" FROM "Item"
WHERE path <@ text2ltree(${item.path}) AND id != ${item.id}` WHERE path <@ text2ltree(${item.path}) AND id != ${Number(item.id)}`
return count || 0 return count || 0
}, },
sats: async (item, args, { models }) => { sats: async (item, args, { models }) => {
@ -512,9 +512,9 @@ export default {
sats: true sats: true
}, },
where: { where: {
itemId: item.id, itemId: Number(item.id),
userId: { userId: {
not: item.userId not: Number(item.userId)
}, },
act: { act: {
not: 'BOOST' not: 'BOOST'
@ -530,9 +530,9 @@ export default {
sats: true sats: true
}, },
where: { where: {
itemId: item.id, itemId: Number(item.id),
userId: { userId: {
not: item.userId not: Number(item.userId)
}, },
act: 'VOTE' act: 'VOTE'
} }
@ -546,7 +546,7 @@ export default {
sats: true sats: true
}, },
where: { where: {
itemId: item.id, itemId: Number(item.id),
act: 'BOOST' act: 'BOOST'
} }
}) })
@ -561,7 +561,7 @@ export default {
sats: true sats: true
}, },
where: { where: {
itemId: item.id, itemId: Number(item.id),
userId: me.id, userId: me.id,
OR: [ OR: [
{ {

View File

@ -9,7 +9,7 @@ export default gql`
pageTitle(url: String!): String pageTitle(url: String!): String
dupes(url: String!): [Item!] dupes(url: String!): [Item!]
allItems(cursor: String): Items allItems(cursor: String): Items
search(query: String, cursor: String): Items search(q: String, cursor: String): Items
} }
type ItemActResult { type ItemActResult {
@ -49,6 +49,7 @@ export default gql`
parent: Item parent: Item
root: Item root: Item
user: User! user: User!
userId: Int!
depth: Int! depth: Int!
mine: Boolean! mine: Boolean!
boost: Int! boost: Int!

View File

@ -12,6 +12,7 @@ import { useRouter } from 'next/router'
import CommentEdit from './comment-edit' import CommentEdit from './comment-edit'
import Countdown from './countdown' import Countdown from './countdown'
import { NOFOLLOW_LIMIT } from '../lib/constants' import { NOFOLLOW_LIMIT } from '../lib/constants'
import { ignoreClick } from '../lib/clicks'
function Parent ({ item, rootText }) { function Parent ({ item, rootText }) {
const ParentFrag = () => ( const ParentFrag = () => (
@ -43,6 +44,26 @@ const truncateString = (string = '', maxLength = 140) =>
? `${string.substring(0, maxLength)} […]` ? `${string.substring(0, maxLength)} […]`
: string : string
export function CommentFlat ({ item, ...props }) {
const router = useRouter()
return (
<div
className='clickToContext py-2'
onClick={e => {
if (ignoreClick(e)) {
return
}
router.push({
pathname: '/items/[id]',
query: { id: item.root.id, commentId: item.id }
}, `/items/${item.root.id}`)
}}
>
<Comment item={item} {...props} />
</div>
)
}
export default function Comment ({ export default function Comment ({
item, children, replyOpen, includeParent, item, children, replyOpen, includeParent,
rootText, noComments, noReply, truncate rootText, noComments, noReply, truncate

View File

@ -1,12 +1,9 @@
import { useQuery } from '@apollo/client' import { useQuery } from '@apollo/client'
import { MORE_FLAT_COMMENTS } from '../fragments/comments' import { MORE_FLAT_COMMENTS } from '../fragments/comments'
import Comment, { CommentSkeleton } from './comment' import { CommentFlat, CommentSkeleton } from './comment'
import { useRouter } from 'next/router'
import MoreFooter from './more-footer' import MoreFooter from './more-footer'
import { ignoreClick } from '../lib/clicks'
export default function CommentsFlat ({ variables, comments, cursor, ...props }) { export default function CommentsFlat ({ variables, comments, cursor, ...props }) {
const router = useRouter()
const { data, fetchMore } = useQuery(MORE_FLAT_COMMENTS, { const { data, fetchMore } = useQuery(MORE_FLAT_COMMENTS, {
variables variables
}) })
@ -21,23 +18,9 @@ export default function CommentsFlat ({ variables, comments, cursor, ...props })
return ( return (
<> <>
{comments.map(item => ( {comments.map(item =>
<div <CommentFlat key={item.id} item={item} {...props} />
key={item.id} )}
className='clickToContext py-2'
onClick={e => {
if (ignoreClick(e)) {
return
}
router.push({
pathname: '/items/[id]',
query: { id: item.root.id, commentId: item.id }
}, `/items/${item.root.id}`)
}}
>
<Comment item={item} {...props} />
</div>
))}
<MoreFooter cursor={cursor} fetchMore={fetchMore} Skeleton={CommentsFlatSkeleton} /> <MoreFooter cursor={cursor} fetchMore={fetchMore} Skeleton={CommentsFlatSkeleton} />
</> </>
) )

View File

@ -42,12 +42,12 @@ function ItemEmbed ({ item }) {
return null return null
} }
function TopLevelItem ({ item }) { function TopLevelItem ({ item, noReply }) {
return ( return (
<Item item={item}> <Item item={item}>
{item.text && <ItemText item={item} />} {item.text && <ItemText item={item} />}
{item.url && <ItemEmbed item={item} />} {item.url && <ItemEmbed item={item} />}
<Reply parentId={item.id} replyOpen /> {!noReply && <Reply parentId={item.id} replyOpen />}
</Item> </Item>
) )
} }

View File

@ -109,10 +109,12 @@ export default function Item ({ item, rank, children }) {
export function ItemSkeleton ({ rank, children }) { export function ItemSkeleton ({ rank, children }) {
return ( return (
<> <>
{rank && {rank
<div className={styles.rank}> ? (
{rank} <div className={styles.rank}>
</div>} {rank}
</div>)
: <div />}
<div className={`${styles.item} ${styles.skeleton}`}> <div className={`${styles.item} ${styles.skeleton}`}>
<UpVote className={styles.upvote} /> <UpVote className={styles.upvote} />
<div className={styles.hunk}> <div className={styles.hunk}>

View File

@ -4,6 +4,7 @@ import styles from './items.module.css'
import { MORE_ITEMS } from '../fragments/items' import { MORE_ITEMS } from '../fragments/items'
import MoreFooter from './more-footer' import MoreFooter from './more-footer'
import React from 'react' import React from 'react'
import Comment from './comment'
export default function Items ({ variables, rank, items, pins, cursor }) { export default function Items ({ variables, rank, items, pins, cursor }) {
const { data, fetchMore } = useQuery(MORE_ITEMS, { variables }) const { data, fetchMore } = useQuery(MORE_ITEMS, { variables })
@ -24,7 +25,9 @@ export default function Items ({ variables, rank, items, pins, cursor }) {
{items.map((item, i) => ( {items.map((item, i) => (
<React.Fragment key={item.id}> <React.Fragment key={item.id}>
{pinMap && pinMap[i + 1] && <Item item={pinMap[i + 1]} key={pinMap[i + 1].id} />} {pinMap && pinMap[i + 1] && <Item item={pinMap[i + 1]} key={pinMap[i + 1].id} />}
<Item item={item} rank={rank && i + 1} key={item.id} /> {item.parentId
? <><div /><div className='pb-3'><Comment item={item} noReply includeParent /></div></>
: <Item item={item} rank={rank && i + 1} key={item.id} />}
</React.Fragment> </React.Fragment>
))} ))}
</div> </div>

View File

@ -1,4 +1,4 @@
.grid { .grid {
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto minmax(0, 1fr);
} }

View File

@ -0,0 +1,50 @@
import { useQuery } from '@apollo/client'
import { ItemSkeleton } from './item'
import styles from './items.module.css'
import { ITEM_SEARCH } from '../fragments/items'
import MoreFooter from './more-footer'
import React from 'react'
import Comment from './comment'
import ItemFull from './item-full'
export default function SearchItems ({ variables, items, pins, cursor }) {
const { data, fetchMore } = useQuery(ITEM_SEARCH, { variables })
if (!data && !items) {
return <ItemsSkeleton />
}
if (data) {
({ search: { items, cursor } } = data)
}
return (
<>
<div className={styles.grid}>
{items.map((item, i) => (
<React.Fragment key={item.id}>
{item.parentId
? <><div /><div className='pb-3'><Comment item={item} noReply includeParent /></div></>
: <><div /><div className={item.text ? 'pb-3' : ''}><ItemFull item={item} noReply /></div></>}
</React.Fragment>
))}
</div>
<MoreFooter
cursor={cursor} fetchMore={fetchMore}
Skeleton={() => <ItemsSkeleton />}
/>
</>
)
}
function ItemsSkeleton () {
const items = new Array(21).fill(null)
return (
<div className={styles.grid}>
{items.map((_, i) => (
<ItemSkeleton key={i} />
))}
</div>
)
}

View File

@ -2,24 +2,42 @@ import { Button, Container } from 'react-bootstrap'
import styles from './search.module.css' import styles from './search.module.css'
import SearchIcon from '../svgs/search-fill.svg' import SearchIcon from '../svgs/search-fill.svg'
import CloseIcon from '../svgs/close-line.svg' import CloseIcon from '../svgs/close-line.svg'
import { useState } from 'react' import { useEffect, useState } from 'react'
import { Form, Input, SubmitButton } from './form' import { Form, Input, SubmitButton } from './form'
import { useRouter } from 'next/router'
export default function Search () { export default function Search () {
const [searching, setSearching] = useState() const router = useRouter()
const [q, setQ] = useState() const [searching, setSearching] = useState(router.query.q)
const [q, setQ] = useState(router.query.q)
const [atBottom, setAtBottom] = useState()
useEffect(() => {
setAtBottom((window.innerHeight + window.pageYOffset) >= document.body.offsetHeight)
window.onscroll = function (ev) {
if ((window.innerHeight + window.pageYOffset) >= document.body.offsetHeight) {
setAtBottom(true)
} else {
setAtBottom(false)
}
}
}, [])
const showSearch = atBottom || searching || router.query.q
return ( return (
<> <>
<div className={`${styles.searchSection} ${searching ? styles.solid : styles.hidden}`}> <div className={`${styles.searchSection} ${showSearch ? styles.solid : styles.hidden}`}>
<Container className={`px-sm-0 ${styles.searchContainer}`}> <Container className={`px-sm-0 ${styles.searchContainer}`}>
{searching {showSearch
? ( ? (
<Form <Form
initial={{ initial={{
q: '' q: router.query.q || ''
}} }}
inline
className={`w-auto ${styles.active}`} className={`w-auto ${styles.active}`}
onSubmit={async ({ q }) => {
router.push(`/search?q=${q}`)
}}
> >
<Input <Input
name='q' name='q'
@ -28,10 +46,11 @@ export default function Search () {
groupClassName='mr-3 mb-0 flex-grow-1' groupClassName='mr-3 mb-0 flex-grow-1'
className='w-100' className='w-100'
onChange={async (formik, e) => { onChange={async (formik, e) => {
setSearching(true)
setQ(e.target.value?.trim()) setQ(e.target.value?.trim())
}} }}
/> />
{q {q || atBottom || router.query.q
? ( ? (
<SubmitButton variant='primary' className={styles.search}> <SubmitButton variant='primary' className={styles.search}>
<SearchIcon width={22} height={22} /> <SearchIcon width={22} height={22} />

View File

@ -89,3 +89,16 @@ export const ITEM_WITH_COMMENTS = gql`
...CommentsRecursive ...CommentsRecursive
} }
}` }`
export const ITEM_SEARCH = gql`
${ITEM_FIELDS}
query Search($q: String!, $cursor: String) {
search(q: $q, cursor: $cursor) {
cursor
items {
...ItemFields
text
}
}
}
`

View File

@ -52,6 +52,19 @@ export default function getApolloClient () {
} }
} }
}, },
search: {
keyArgs: ['q'],
merge (existing, incoming) {
if (isFirstPage(incoming.cursor, existing?.items)) {
return incoming
}
return {
cursor: incoming.cursor,
items: [...(existing?.items || []), ...incoming.items]
}
}
},
moreFlatComments: { moreFlatComments: {
keyArgs: ['name', 'sort', 'within'], keyArgs: ['name', 'sort', 'within'],
merge (existing, incoming) { merge (existing, incoming) {

18
pages/search.js Normal file
View File

@ -0,0 +1,18 @@
import Layout from '../components/layout'
import { getGetServerSideProps } from '../api/ssrApollo'
import { ITEM_SEARCH } from '../fragments/items'
import SearchItems from '../components/search-items'
import { useRouter } from 'next/router'
export const getServerSideProps = getGetServerSideProps(ITEM_SEARCH)
export default function Index ({ data: { search: { items, cursor } } }) {
const router = useRouter()
return (
<Layout>
<SearchItems
items={items} cursor={cursor} variables={{ q: router.query?.q }}
/>
</Layout>
)
}

View File

@ -12,6 +12,7 @@ const ITEM_SEARCH_FIELDS = gql`
title title
text text
url url
userId
user { user {
name name
} }