related items

This commit is contained in:
keyan 2022-10-26 17:46:01 -05:00
parent 9c5937b9be
commit 760b6b6e10
7 changed files with 201 additions and 1 deletions

View File

@ -1,8 +1,96 @@
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { getItem } from './item' import { getItem } from './item'
const STOP_WORDS = ['a', 'an', 'and', 'are', 'as', 'at', 'be', 'but',
'by', 'for', 'if', 'in', 'into', 'is', 'it', 'no', 'not',
'of', 'on', 'or', 'such', 'that', 'the', 'their', 'then',
'there', 'these', 'they', 'this', 'to', 'was', 'will',
'with', 'bitcoin', 'page', 'adds', 'how', 'why', 'what',
'works', 'now', 'available', 'breaking', 'app', 'powered',
'just', 'dev', 'using', 'crypto', 'has', 'my', 'i', 'apps',
'really', 'new', 'era', 'application', 'best', 'year',
'latest', 'still', 'few', 'crypto', 'keep', 'public', 'current',
'levels', 'from', 'cryptocurrencies', 'confirmed', 'news', 'network',
'about', 'sources', 'vote', 'considerations', 'hope',
'keep', 'keeps', 'including', 'we', 'brings', "don't", 'do',
'interesting', 'us']
export default { export default {
Query: { Query: {
related: async (parent, { title, id, cursor, limit }, { me, models, search }) => {
const decodedCursor = decodeCursor(cursor)
if (!title || title.trim().split(/\s+/).length < 1) {
if (id) {
const item = await getItem(parent, { id }, { me, models })
title = item?.title
}
if (!title) {
return {
items: [],
cursor: null
}
}
}
const mustNot = []
if (id) {
mustNot.push({ term: { id } })
}
let items = await search.search({
index: 'item',
size: limit || LIMIT,
from: decodedCursor.offset,
body: {
query: {
bool: {
should: [
{
more_like_this: {
fields: ['title'],
like: title,
min_term_freq: 1,
min_doc_freq: 1,
max_query_terms: 25,
min_word_length: 2,
minimum_should_match: '50%',
stop_words: STOP_WORDS,
boost: 400
}
},
{
more_like_this: {
fields: ['title'],
like: title,
min_term_freq: 1,
min_doc_freq: 1,
min_word_length: 2,
max_query_terms: 25,
minimum_should_match: '30%',
stop_words: STOP_WORDS
}
}
],
must_not: [{ exists: { field: 'parentId' } }, ...mustNot],
filter: {
range: { sats: { gte: 10 } }
}
}
},
sort: ['_score', { sats: 'desc' }]
}
})
items = items.body.hits.hits.map(async e => {
// this is super inefficient but will suffice until we do something more generic
return await getItem(parent, { id: e._source.id }, { me, models })
})
return {
cursor: items.length === (limit || LIMIT) ? nextCursorEncoded(decodedCursor) : null,
items
}
},
search: async (parent, { q: query, sub, cursor, sort, what, when }, { me, models, search }) => { search: async (parent, { q: query, sub, cursor, sort, what, when }, { me, models, search }) => {
const decodedCursor = decodeCursor(cursor) const decodedCursor = decodeCursor(cursor)
let sitems let sitems

View File

@ -8,6 +8,7 @@ export default gql`
comments(id: ID!, sort: String): [Item!]! comments(id: ID!, sort: String): [Item!]!
pageTitle(url: String!): String pageTitle(url: String!): String
dupes(url: String!): [Item!] dupes(url: String!): [Item!]
related(cursor: String, title: String, id: ID, limit: Int): Items
allItems(cursor: String): Items allItems(cursor: String): 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): Items
auctionPosition(sub: String, id: ID, bid: Int!): Int! auctionPosition(sub: String, id: ID, bid: Int!): Int!

View File

@ -14,6 +14,7 @@ import useDarkMode from 'use-dark-mode'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Poll from './poll' import Poll from './poll'
import { commentsViewed } from '../lib/new-comments' import { commentsViewed } from '../lib/new-comments'
import Related from './related'
function BioItem ({ item, handleClick }) { function BioItem ({ item, handleClick }) {
const me = useMe() const me = useMe()
@ -90,7 +91,11 @@ function TopLevelItem ({ item, noReply, ...props }) {
{item.text && <ItemText item={item} />} {item.text && <ItemText item={item} />}
{item.url && <ItemEmbed item={item} />} {item.url && <ItemEmbed item={item} />}
{item.poll && <Poll item={item} />} {item.poll && <Poll item={item} />}
{!noReply && <Reply item={item} replyOpen />} {!noReply &&
<>
<Reply item={item} replyOpen />
{!item.position && !item.isJob && !item.parentId && <Related title={item.title} itemId={item.id} />}
</>}
</ItemComponent> </ItemComponent>
) )
} }

38
components/related.js Normal file
View File

@ -0,0 +1,38 @@
import { useQuery } from '@apollo/client'
import Link from 'next/link'
import { RELATED_ITEMS } from '../fragments/items'
import AccordianItem from './accordian-item'
import Item, { ItemSkeleton } from './item'
import styles from './items.module.css'
export default function Related ({ title, itemId }) {
const emptyItems = new Array(5).fill(null)
const { data, loading } = useQuery(RELATED_ITEMS, {
fetchPolicy: 'cache-first',
variables: { title, id: itemId, limit: 5 }
})
let items, cursor
if (data) {
({ related: { items, cursor } } = data)
}
return (
<AccordianItem
header={<div className='font-weight-bold'>related</div>}
body={
<>
<div className={styles.grid}>
{loading
? emptyItems.map((_, i) => <ItemSkeleton key={i} />)
: (items?.length
? items.map(item => <Item key={item.id} item={item} />)
: <div className='text-muted' style={{ fontFamily: 'lightning', fontSize: '2rem', opacity: '0.75' }}>EMPTY</div>
)}
</div>
{cursor && itemId && <Link href={`/items/${itemId}/related`} passHref><a className='text-reset text-muted font-weight-bold'>view all related</a></Link>}
</>
}
/>
)
}

View File

@ -204,3 +204,30 @@ export const ITEM_SEARCH = gql`
} }
} }
` `
export const RELATED_ITEMS = gql`
${ITEM_FIELDS}
query Related($title: String, $id: ID, $cursor: String, $limit: Int) {
related(title: $title, id: $id, cursor: $cursor, limit: $limit) {
cursor
items {
...ItemFields
}
}
}
`
export const RELATED_ITEMS_WITH_ITEM = gql`
${ITEM_FIELDS}
query Related($title: String, $id: ID, $cursor: String, $limit: Int) {
item(id: $id) {
...ItemFields
}
related(title: $title, id: $id, cursor: $cursor, limit: $limit) {
cursor
items {
...ItemFields
}
}
}
`

View File

@ -79,6 +79,19 @@ export default function getApolloClient () {
} }
} }
}, },
related: {
keyArgs: ['id', 'title', 'limit'],
merge (existing, incoming) {
if (isFirstPage(incoming.cursor, existing?.items)) {
return incoming
}
return {
cursor: incoming.cursor,
items: [...(existing?.items || []), ...incoming.items]
}
}
},
outlawedItems: { outlawedItems: {
keyArgs: [], keyArgs: [],
merge (existing, incoming) { merge (existing, incoming) {

View File

@ -0,0 +1,28 @@
// ssr the related query with an adequate limit
// need to use a cursor on related
import { RELATED_ITEMS, RELATED_ITEMS_WITH_ITEM } from '../../../fragments/items'
import { getGetServerSideProps } from '../../../api/ssrApollo'
import Items from '../../../components/items'
import Layout from '../../../components/layout'
import { useRouter } from 'next/router'
import Item from '../../../components/item'
export const getServerSideProps = getGetServerSideProps(RELATED_ITEMS_WITH_ITEM, null,
data => !data.item)
export default function Related ({ data: { item, related: { items, cursor } } }) {
const router = useRouter()
return (
<Layout>
<Item item={item} />
<div className='font-weight-bold my-2'>related</div>
<Items
items={items} cursor={cursor}
query={RELATED_ITEMS}
destructureData={data => data.related}
variables={{ id: router.query.id }}
/>
</Layout>
)
}