From a398784f26d8ce633b1e5c28fb3cb3424c15f493 Mon Sep 17 00:00:00 2001 From: keyan Date: Tue, 25 Oct 2022 16:35:32 -0500 Subject: [PATCH] improved top --- api/resolvers/item.js | 51 +++++- api/resolvers/user.js | 88 +++++++-- api/typeDefs/item.js | 2 + api/typeDefs/user.js | 13 +- components/comment.js | 5 +- components/comments-flat.js | 10 +- components/comments.js | 3 +- components/header.js | 8 +- components/item.js | 5 +- components/item.module.css | 8 + components/items.js | 10 +- components/top-header.js | 179 ++++++------------- components/user-list.js | 34 +++- fragments/comments.js | 13 ++ fragments/items.js | 17 ++ fragments/users.js | 10 +- lib/apollo.js | 29 ++- lib/format.js | 2 +- pages/top/comments/{[within].js => index.js} | 10 +- pages/top/posts/{[within].js => index.js} | 11 +- pages/top/users/[userType]/[within].js | 52 ------ pages/top/users/index.js | 30 ++++ pages/users/forever.js | 6 +- pages/users/week.js | 12 +- 24 files changed, 353 insertions(+), 255 deletions(-) rename pages/top/comments/{[within].js => index.js} (56%) rename pages/top/posts/{[within].js => index.js} (54%) delete mode 100644 pages/top/users/[userType]/[within].js create mode 100644 pages/top/users/index.js diff --git a/api/resolvers/item.js b/api/resolvers/item.js index bdaae17b..7940e382 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -50,8 +50,8 @@ export async function getItem (parent, { id }, { me, models }) { function topClause (within) { let interval = ' AND "Item".created_at >= $1 - INTERVAL ' switch (within) { - case 'day': - interval += "'1 day'" + case 'forever': + interval = '' break case 'week': interval += "'7 days'" @@ -63,12 +63,23 @@ function topClause (within) { interval += "'1 year'" break default: - interval = '' + interval += "'1 day'" break } return interval } +async function topOrderClause (sort, me, models) { + switch (sort) { + case 'comments': + return 'ORDER BY ncomments DESC' + case 'sats': + return 'ORDER BY sats DESC' + default: + return await topOrderByWeightedSats(me, models) + } +} + export async function orderByNumerator (me, models) { if (me) { const user = await models.user.findUnique({ where: { id: me.id } }) @@ -124,6 +135,40 @@ export default { return count }, + topItems: async (parent, { cursor, sort, when }, { me, models }) => { + const decodedCursor = decodeCursor(cursor) + const items = await models.$queryRaw(` + ${SELECT} + FROM "Item" + WHERE "parentId" IS NULL AND "Item".created_at <= $1 + AND "pinId" IS NULL + ${topClause(when)} + ${await filterClause(me, models)} + ${await topOrderClause(sort, me, models)} + OFFSET $2 + LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset) + return { + cursor: items.length === LIMIT ? nextCursorEncoded(decodedCursor) : null, + items + } + }, + topComments: async (parent, { cursor, sort, when }, { me, models }) => { + const decodedCursor = decodeCursor(cursor) + const comments = await models.$queryRaw(` + ${SELECT} + FROM "Item" + WHERE "parentId" IS NOT NULL + AND "Item".created_at <= $1 + ${topClause(when)} + ${await filterClause(me, models)} + ${await topOrderClause(sort, me, models)} + OFFSET $2 + LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset) + return { + cursor: comments.length === LIMIT ? nextCursorEncoded(decodedCursor) : null, + comments + } + }, items: async (parent, { sub, sort, cursor, name, within }, { me, models }) => { const decodedCursor = decodeCursor(cursor) let items; let user; let pins; let subFull diff --git a/api/resolvers/user.js b/api/resolvers/user.js index 711d01e1..72617243 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -6,8 +6,8 @@ import serialize from './serial' export function topClause (within) { let interval = ' AND "ItemAct".created_at >= $1 - INTERVAL ' switch (within) { - case 'day': - interval += "'1 day'" + case 'forever': + interval = '' break case 'week': interval += "'7 days'" @@ -19,7 +19,7 @@ export function topClause (within) { interval += "'1 year'" break default: - interval = '' + interval += "'1 day'" break } return interval @@ -28,8 +28,8 @@ export function topClause (within) { export function earnWithin (within) { let interval = ' AND "Earn".created_at >= $1 - INTERVAL ' switch (within) { - case 'day': - interval += "'1 day'" + case 'forever': + interval = '' break case 'week': interval += "'7 days'" @@ -41,8 +41,30 @@ export function earnWithin (within) { interval += "'1 year'" break default: + interval += "'1 day'" + break + } + return interval +} + +export function itemWithin (within) { + let interval = ' AND "Item".created_at >= $1 - INTERVAL ' + switch (within) { + case 'forever': interval = '' break + case 'week': + interval += "'7 days'" + break + case 'month': + interval += "'1 month'" + break + case 'year': + interval += "'1 year'" + break + default: + interval += "'1 day'" + break } return interval } @@ -94,37 +116,59 @@ export default { return user.name?.toUpperCase() === name?.toUpperCase() || !(await models.user.findUnique({ where: { name } })) }, - topUsers: async (parent, { cursor, within, userType }, { models, me }) => { + topUsers: async (parent, { cursor, when, sort }, { models, me }) => { const decodedCursor = decodeCursor(cursor) let users - if (userType === 'spent') { + if (sort === 'spent') { users = await models.$queryRaw(` - SELECT users.name, users.created_at, sum("ItemAct".sats) as amount + SELECT users.*, sum("ItemAct".sats) as spent FROM "ItemAct" JOIN users on "ItemAct"."userId" = users.id WHERE "ItemAct".created_at <= $1 - ${topClause(within)} + ${topClause(when)} GROUP BY users.id, users.name - ORDER BY amount DESC NULLS LAST, users.created_at DESC + ORDER BY spent DESC NULLS LAST, users.created_at DESC + OFFSET $2 + LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset) + } else if (sort === 'posts') { + users = await models.$queryRaw(` + SELECT users.*, count(*) as nitems + FROM users + JOIN "Item" on "Item"."userId" = users.id + WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NULL + ${itemWithin(when)} + GROUP BY users.id + ORDER BY nitems DESC NULLS LAST, users.created_at DESC + OFFSET $2 + LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset) + } else if (sort === 'comments') { + users = await models.$queryRaw(` + SELECT users.*, count(*) as ncomments + FROM users + JOIN "Item" on "Item"."userId" = users.id + WHERE "Item".created_at <= $1 AND "Item"."parentId" IS NOT NULL + ${itemWithin(when)} + GROUP BY users.id + ORDER BY ncomments DESC NULLS LAST, users.created_at DESC OFFSET $2 LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset) } else { users = await models.$queryRaw(` - SELECT name, created_at, sum(sats) as amount + SELECT u.id, u.name, u."photoId", sum(amount) as stacked FROM - ((SELECT users.name, users.created_at, "ItemAct".sats as sats + ((SELECT users.*, "ItemAct".sats as amount FROM "ItemAct" JOIN "Item" on "ItemAct"."itemId" = "Item".id JOIN users on "Item"."userId" = users.id WHERE act <> 'BOOST' AND "ItemAct"."userId" <> users.id AND "ItemAct".created_at <= $1 - ${topClause(within)}) + ${topClause(when)}) UNION ALL - (SELECT users.name, users.created_at, "Earn".msats/1000 as sats + (SELECT users.*, "Earn".msats/1000 as amount FROM "Earn" JOIN users on users.id = "Earn"."userId" - WHERE "Earn".msats > 0 ${earnWithin(within)})) u - GROUP BY name, created_at - ORDER BY amount DESC NULLS LAST, created_at DESC + WHERE "Earn".msats > 0 ${earnWithin(when)})) u + GROUP BY u.id, u.name, u.created_at, u."photoId" + ORDER BY stacked DESC NULLS LAST, created_at DESC OFFSET $2 LIMIT ${LIMIT}`, decodedCursor.time, decodedCursor.offset) } @@ -260,9 +304,15 @@ export default { User: { authMethods, nitems: async (user, args, { models }) => { + if (user.nitems) { + return user.nitems + } return await models.item.count({ where: { userId: user.id, parentId: null } }) }, ncomments: async (user, args, { models }) => { + if (user.ncomments) { + return user.ncomments + } return await models.item.count({ where: { userId: user.id, parentId: { not: null } } }) }, stacked: async (user, args, { models }) => { @@ -273,6 +323,10 @@ export default { return Math.floor((user.stackedMsats || 0) / 1000) }, spent: async (user, args, { models }) => { + if (user.spent) { + return user.spent + } + const { sum: { sats } } = await models.itemAct.aggregate({ sum: { sats: true diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 17aef11a..73bc9942 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -15,6 +15,8 @@ export default gql` outlawedItems(cursor: String): Items borderlandItems(cursor: String): Items freebieItems(cursor: String): Items + topItems(cursor: String, sort: String, when: String): Items + topComments(cursor: String, sort: String, when: String): Comments } type ItemActResult { diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 748acc1b..85850716 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -7,7 +7,7 @@ export default gql` user(name: String!): User users: [User!] nameAvailable(name: String!): Boolean! - topUsers(cursor: String, within: String!, userType: String!): TopUsers + topUsers(cursor: String, when: String, sort: String): Users searchUsers(q: String!, limit: Int, similarity: Float): [User!]! } @@ -16,17 +16,6 @@ export default gql` users: [User!]! } - type TopUsers { - cursor: String - users: [TopUser!]! - } - - type TopUser { - name: String! - createdAt: String! - amount: Int! - } - extend type Mutation { setName(name: String!): Boolean setSettings(tipDefault: Int!, fiatCurrency: String!, noteItemSats: Boolean!, noteEarning: Boolean!, diff --git a/components/comment.js b/components/comment.js index 157ba5e8..f1be8849 100644 --- a/components/comment.js +++ b/components/comment.js @@ -17,6 +17,7 @@ import { useMe } from './me' import DontLikeThis from './dont-link-this' import Flag from '../svgs/flag-fill.svg' import { Badge } from 'react-bootstrap' +import { abbrNum } from '../lib/format' function Parent ({ item, rootText }) { const ParentFrag = () => ( @@ -114,11 +115,11 @@ export default function Comment ({
- {item.sats} sats + {abbrNum(item.sats)} sats \ {item.boost > 0 && <> - {item.boost} boost + {abbrNum(item.boost)} boost \ } diff --git a/components/comments-flat.js b/components/comments-flat.js index f26f70ab..bfbbe6f6 100644 --- a/components/comments-flat.js +++ b/components/comments-flat.js @@ -3,8 +3,8 @@ import { MORE_FLAT_COMMENTS } from '../fragments/comments' import { CommentFlat, CommentSkeleton } from './comment' import MoreFooter from './more-footer' -export default function CommentsFlat ({ variables, comments, cursor, ...props }) { - const { data, fetchMore } = useQuery(MORE_FLAT_COMMENTS, { +export default function CommentsFlat ({ variables, query, destructureData, comments, cursor, ...props }) { + const { data, fetchMore } = useQuery(query || MORE_FLAT_COMMENTS, { variables }) @@ -13,7 +13,11 @@ export default function CommentsFlat ({ variables, comments, cursor, ...props }) } if (data) { - ({ moreFlatComments: { comments, cursor } } = data) + if (destructureData) { + ({ comments, cursor } = destructureData(data)) + } else { + ({ moreFlatComments: { comments, cursor } } = data) + } } return ( diff --git a/components/comments.js b/components/comments.js index db5dfe2b..22c16926 100644 --- a/components/comments.js +++ b/components/comments.js @@ -5,6 +5,7 @@ import styles from './header.module.css' import { Nav, Navbar } from 'react-bootstrap' import { COMMENTS_QUERY } from '../fragments/items' import { COMMENTS } from '../fragments/comments' +import { abbrNum } from '../lib/format' export function CommentsHeader ({ handleSort, commentSats }) { const [sort, setSort] = useState('hot') @@ -23,7 +24,7 @@ export function CommentsHeader ({ handleSort, commentSats }) { activeKey={sort} > - {commentSats} sats + {abbrNum(commentSats)} sats
diff --git a/components/header.js b/components/header.js index d42f1b68..fb49b551 100644 --- a/components/header.js +++ b/components/header.js @@ -11,7 +11,7 @@ import { signOut, signIn } from 'next-auth/client' import { useLightning } from './lightning' import { useEffect, useState } from 'react' import { randInRange } from '../lib/rand' -import { formatSats } from '../lib/format' +import { abbrNum } from '../lib/format' import NoteIcon from '../svgs/notification-4-fill.svg' import { useQuery, gql } from '@apollo/client' import LightningIcon from '../svgs/bolt.svg' @@ -19,7 +19,7 @@ import LightningIcon from '../svgs/bolt.svg' function WalletSummary ({ me }) { if (!me) return null - return `${formatSats(me.sats)}` + return `${abbrNum(me.sats)}` } export default function Header ({ sub }) { @@ -154,7 +154,7 @@ export default function Header ({ sub }) { {!prefix && - + top } @@ -230,7 +230,7 @@ const NavItemsStatic = ({ className }) => { - + top diff --git a/components/item.js b/components/item.js index 651f8c76..722c40a8 100644 --- a/components/item.js +++ b/components/item.js @@ -14,6 +14,7 @@ import { newComments } from '../lib/new-comments' import { useMe } from './me' import DontLikeThis from './dont-link-this' import Flag from '../svgs/flag-fill.svg' +import { abbrNum } from '../lib/format' export function SearchTitle ({ title }) { return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => { @@ -87,12 +88,12 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
{!item.position && <> - {item.sats} sats + {abbrNum(item.sats)} sats \ } {item.boost > 0 && <> - {item.boost} boost + {abbrNum(item.boost)} boost \ } diff --git a/components/item.module.css b/components/item.module.css index 19bc09c1..470934c8 100644 --- a/components/item.module.css +++ b/components/item.module.css @@ -127,6 +127,14 @@ a.link:visited { margin: 0; } +.skeleton .name { + background-color: var(--theme-grey); + width: 100px; + border-radius: .4rem; + height: 17px; + margin: 0; +} + .skeleton .link { height: 14px; background-color: var(--theme-grey); diff --git a/components/items.js b/components/items.js index beb7f719..40cbda27 100644 --- a/components/items.js +++ b/components/items.js @@ -7,15 +7,19 @@ import MoreFooter from './more-footer' import React from 'react' import Comment from './comment' -export default function Items ({ variables = {}, rank, items, pins, cursor }) { - const { data, fetchMore } = useQuery(ITEMS, { variables }) +export default function Items ({ variables = {}, query, destructureData, rank, items, pins, cursor }) { + const { data, fetchMore } = useQuery(query || ITEMS, { variables }) if (!data && !items) { return } if (data) { - ({ items: { items, pins, cursor } } = data) + if (destructureData) { + ({ items, pins, cursor } = destructureData(data)) + } else { + ({ items: { items, pins, cursor } } = data) + } } const pinMap = pins?.reduce((a, p) => { a[p.position] = p; return a }, {}) diff --git a/components/top-header.js b/components/top-header.js index 08250d15..72175c78 100644 --- a/components/top-header.js +++ b/components/top-header.js @@ -1,137 +1,58 @@ -import { Nav, Navbar } from 'react-bootstrap' -import styles from './header.module.css' -import Link from 'next/link' import { useRouter } from 'next/router' +import { Form, Select } from './form' export default function TopHeader ({ cat }) { const router = useRouter() - const within = router.query.within - const userType = router.query.userType || 'stacked' + + const top = async values => { + const what = values.what + delete values.what + if (values.sort === '') delete values.sort + if (values.when === '') delete values.when + await router.push({ + pathname: `/top/${what}`, + query: values + }) + } return ( - <> - - - - {cat.split('/')[0] === 'users' && - - - } - - - - +
+
+
+ top + top({ ...formik?.values, sort: e.target.value })} + name='sort' + size='sm' + items={cat === 'users' ? ['stacked', 'spent', 'comments', 'posts'] : ['votes', 'comments', 'sats']} + /> + for +