related items
This commit is contained in:
parent
9c5937b9be
commit
760b6b6e10
|
@ -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
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue