working search
This commit is contained in:
parent
cc567d301e
commit
afed19430c
@ -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: [
|
||||||
{
|
{
|
||||||
|
@ -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!
|
||||||
|
@ -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
|
||||||
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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}>
|
||||||
|
@ -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>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
}
|
}
|
50
components/search-items.js
Normal file
50
components/search-items.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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} />
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
@ -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
18
pages/search.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -12,6 +12,7 @@ const ITEM_SEARCH_FIELDS = gql`
|
|||||||
title
|
title
|
||||||
text
|
text
|
||||||
url
|
url
|
||||||
|
userId
|
||||||
user {
|
user {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user