improved top
This commit is contained in:
		
							parent
							
								
									30b1ee33aa
								
							
						
					
					
						commit
						a398784f26
					
				@ -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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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!,
 | 
			
		||||
 | 
			
		||||
@ -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 ({
 | 
			
		||||
        <div className={`${itemStyles.hunk} ${styles.hunk}`}>
 | 
			
		||||
          <div className='d-flex align-items-center'>
 | 
			
		||||
            <div className={`${itemStyles.other} ${styles.other}`}>
 | 
			
		||||
              <span title={`from ${item.upvotes} users ${item.mine ? `\\ ${item.meSats} sats to post` : `(${item.meSats} sats from me)`}`}>{item.sats} sats</span>
 | 
			
		||||
              <span title={`from ${item.upvotes} users ${item.mine ? `\\ ${item.meSats} sats to post` : `(${item.meSats} sats from me)`}`}>{abbrNum(item.sats)} sats</span>
 | 
			
		||||
              <span> \ </span>
 | 
			
		||||
              {item.boost > 0 &&
 | 
			
		||||
                <>
 | 
			
		||||
                  <span>{item.boost} boost</span>
 | 
			
		||||
                  <span>{abbrNum(item.boost)} boost</span>
 | 
			
		||||
                  <span> \ </span>
 | 
			
		||||
                </>}
 | 
			
		||||
              <Link href={`/items/${item.id}`} passHref>
 | 
			
		||||
 | 
			
		||||
@ -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,8 +13,12 @@ export default function CommentsFlat ({ variables, comments, cursor, ...props })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (data) {
 | 
			
		||||
    if (destructureData) {
 | 
			
		||||
      ({ comments, cursor } = destructureData(data))
 | 
			
		||||
    } else {
 | 
			
		||||
      ({ moreFlatComments: { comments, cursor } } = data)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
      >
 | 
			
		||||
        <Nav.Item className='text-muted'>
 | 
			
		||||
          {commentSats} sats
 | 
			
		||||
          {abbrNum(commentSats)} sats
 | 
			
		||||
        </Nav.Item>
 | 
			
		||||
        <div className='ml-auto d-flex'>
 | 
			
		||||
          <Nav.Item>
 | 
			
		||||
 | 
			
		||||
@ -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 }) {
 | 
			
		||||
        </Nav.Item>
 | 
			
		||||
        {!prefix &&
 | 
			
		||||
          <Nav.Item className={className}>
 | 
			
		||||
            <Link href='/top/posts/week' passHref>
 | 
			
		||||
            <Link href='/top/posts' passHref>
 | 
			
		||||
              <Nav.Link eventKey='top' className={styles.navLink}>top</Nav.Link>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Nav.Item>}
 | 
			
		||||
@ -230,7 +230,7 @@ const NavItemsStatic = ({ className }) => {
 | 
			
		||||
        </Link>
 | 
			
		||||
      </Nav.Item>
 | 
			
		||||
      <Nav.Item className={className}>
 | 
			
		||||
        <Link href='/top/posts/week' passHref>
 | 
			
		||||
        <Link href='/top/posts' passHref>
 | 
			
		||||
          <Nav.Link className={styles.navLink}>top</Nav.Link>
 | 
			
		||||
        </Link>
 | 
			
		||||
      </Nav.Item>
 | 
			
		||||
 | 
			
		||||
@ -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 }) {
 | 
			
		||||
          <div className={`${styles.other}`}>
 | 
			
		||||
            {!item.position &&
 | 
			
		||||
              <>
 | 
			
		||||
                <span title={`from ${item.upvotes} users ${item.mine ? `\\ ${item.meSats} sats to post` : `(${item.meSats} sats from me)`} `}>{item.sats} sats</span>
 | 
			
		||||
                <span title={`from ${item.upvotes} users ${item.mine ? `\\ ${item.meSats} sats to post` : `(${item.meSats} sats from me)`} `}>{abbrNum(item.sats)} sats</span>
 | 
			
		||||
                <span> \ </span>
 | 
			
		||||
              </>}
 | 
			
		||||
            {item.boost > 0 &&
 | 
			
		||||
              <>
 | 
			
		||||
                <span>{item.boost} boost</span>
 | 
			
		||||
                <span>{abbrNum(item.boost)} boost</span>
 | 
			
		||||
                <span> \ </span>
 | 
			
		||||
              </>}
 | 
			
		||||
            <Link href={`/items/${item.id}`} passHref>
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -7,16 +7,20 @@ 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 <ItemsSkeleton rank={rank} />
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (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 }, {})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <>
 | 
			
		||||
      <Navbar className='pt-0'>
 | 
			
		||||
        <Nav
 | 
			
		||||
          className={`${styles.navbarNav} justify-content-around`}
 | 
			
		||||
          activeKey={cat.split('/')[0]}
 | 
			
		||||
    <div className='d-flex'>
 | 
			
		||||
      <Form
 | 
			
		||||
        className='mr-auto'
 | 
			
		||||
        initial={{
 | 
			
		||||
          what: cat,
 | 
			
		||||
          sort: router.query.sort || '',
 | 
			
		||||
          when: router.query.when || ''
 | 
			
		||||
        }}
 | 
			
		||||
        onSubmit={top}
 | 
			
		||||
      >
 | 
			
		||||
          <Nav.Item>
 | 
			
		||||
            <Link href={`/top/posts/${within}`} passHref>
 | 
			
		||||
              <Nav.Link
 | 
			
		||||
                eventKey='posts'
 | 
			
		||||
                className={styles.navLink}
 | 
			
		||||
              >
 | 
			
		||||
                posts
 | 
			
		||||
              </Nav.Link>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Nav.Item>
 | 
			
		||||
          <Nav.Item>
 | 
			
		||||
            <Link href={`/top/comments/${within}`} passHref>
 | 
			
		||||
              <Nav.Link
 | 
			
		||||
                eventKey='comments'
 | 
			
		||||
                className={styles.navLink}
 | 
			
		||||
              >
 | 
			
		||||
                comments
 | 
			
		||||
              </Nav.Link>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Nav.Item>
 | 
			
		||||
          <Nav.Item>
 | 
			
		||||
            <Link href={`/top/users/stacked/${within}`} passHref>
 | 
			
		||||
              <Nav.Link
 | 
			
		||||
                eventKey='users'
 | 
			
		||||
                className={styles.navLink}
 | 
			
		||||
              >
 | 
			
		||||
                users
 | 
			
		||||
              </Nav.Link>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Nav.Item>
 | 
			
		||||
        </Nav>
 | 
			
		||||
      </Navbar>
 | 
			
		||||
      {cat.split('/')[0] === 'users' &&
 | 
			
		||||
        <Navbar className='pt-0'>
 | 
			
		||||
          <Nav
 | 
			
		||||
            className={`${styles.navbarNav} justify-content-around`}
 | 
			
		||||
            activeKey={userType}
 | 
			
		||||
          >
 | 
			
		||||
            <Nav.Item>
 | 
			
		||||
              <Link href={`/top/users/stacked/${within}`} passHref>
 | 
			
		||||
                <Nav.Link
 | 
			
		||||
                  eventKey='stacked'
 | 
			
		||||
                  className={styles.navLink}
 | 
			
		||||
                >
 | 
			
		||||
                  stacked
 | 
			
		||||
                </Nav.Link>
 | 
			
		||||
              </Link>
 | 
			
		||||
            </Nav.Item>
 | 
			
		||||
            <Nav.Item>
 | 
			
		||||
              <Link href={`/top/users/spent/${within}`} passHref>
 | 
			
		||||
                <Nav.Link
 | 
			
		||||
                  eventKey='spent'
 | 
			
		||||
                  className={styles.navLink}
 | 
			
		||||
                >
 | 
			
		||||
                  spent
 | 
			
		||||
                </Nav.Link>
 | 
			
		||||
              </Link>
 | 
			
		||||
            </Nav.Item>
 | 
			
		||||
          </Nav>
 | 
			
		||||
        </Navbar>}
 | 
			
		||||
      <Navbar className='pt-0'>
 | 
			
		||||
        <Nav
 | 
			
		||||
          className={styles.navbarNav}
 | 
			
		||||
          activeKey={within}
 | 
			
		||||
        >
 | 
			
		||||
          <Nav.Item>
 | 
			
		||||
            <Link href={`/top/${cat}/day`} passHref>
 | 
			
		||||
              <Nav.Link
 | 
			
		||||
                eventKey='day'
 | 
			
		||||
                className={styles.navLink}
 | 
			
		||||
              >
 | 
			
		||||
                day
 | 
			
		||||
              </Nav.Link>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Nav.Item>
 | 
			
		||||
          <Nav.Item>
 | 
			
		||||
            <Link href={`/top/${cat}/week`} passHref>
 | 
			
		||||
              <Nav.Link
 | 
			
		||||
                eventKey='week'
 | 
			
		||||
                className={styles.navLink}
 | 
			
		||||
              >
 | 
			
		||||
                week
 | 
			
		||||
              </Nav.Link>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Nav.Item>
 | 
			
		||||
          <Nav.Item>
 | 
			
		||||
            <Link href={`/top/${cat}/month`} passHref>
 | 
			
		||||
              <Nav.Link
 | 
			
		||||
                eventKey='month'
 | 
			
		||||
                className={styles.navLink}
 | 
			
		||||
              >
 | 
			
		||||
                month
 | 
			
		||||
              </Nav.Link>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Nav.Item>
 | 
			
		||||
          <Nav.Item>
 | 
			
		||||
            <Link href={`/top/${cat}/year`} passHref>
 | 
			
		||||
              <Nav.Link
 | 
			
		||||
                eventKey='year'
 | 
			
		||||
                className={styles.navLink}
 | 
			
		||||
              >
 | 
			
		||||
                year
 | 
			
		||||
              </Nav.Link>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Nav.Item>
 | 
			
		||||
          <Nav.Item>
 | 
			
		||||
            <Link href={`/top/${cat}/forever`} passHref>
 | 
			
		||||
              <Nav.Link
 | 
			
		||||
                eventKey='forever'
 | 
			
		||||
                className={styles.navLink}
 | 
			
		||||
              >
 | 
			
		||||
                forever
 | 
			
		||||
              </Nav.Link>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </Nav.Item>
 | 
			
		||||
        </Nav>
 | 
			
		||||
      </Navbar>
 | 
			
		||||
    </>
 | 
			
		||||
        <div className='text-muted font-weight-bold my-3 d-flex align-items-center'>
 | 
			
		||||
          top
 | 
			
		||||
          <Select
 | 
			
		||||
            groupClassName='mx-2 mb-0'
 | 
			
		||||
            onChange={(formik, e) => top({ ...formik?.values, what: e.target.value })}
 | 
			
		||||
            name='what'
 | 
			
		||||
            size='sm'
 | 
			
		||||
            items={['posts', 'comments', 'users']}
 | 
			
		||||
          />
 | 
			
		||||
          by
 | 
			
		||||
          <Select
 | 
			
		||||
            groupClassName='mx-2 mb-0'
 | 
			
		||||
            onChange={(formik, e) => top({ ...formik?.values, sort: e.target.value })}
 | 
			
		||||
            name='sort'
 | 
			
		||||
            size='sm'
 | 
			
		||||
            items={cat === 'users' ? ['stacked', 'spent', 'comments', 'posts'] : ['votes', 'comments', 'sats']}
 | 
			
		||||
          />
 | 
			
		||||
          for
 | 
			
		||||
          <Select
 | 
			
		||||
            groupClassName='mb-0 ml-2'
 | 
			
		||||
            onChange={(formik, e) => top({ ...formik?.values, when: e.target.value })}
 | 
			
		||||
            name='when'
 | 
			
		||||
            size='sm'
 | 
			
		||||
            items={['day', 'week', 'month', 'year', 'forever']}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </Form>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import { Image } from 'react-bootstrap'
 | 
			
		||||
import { abbrNum } from '../lib/format'
 | 
			
		||||
import styles from './item.module.css'
 | 
			
		||||
import userStyles from './user-header.module.css'
 | 
			
		||||
 | 
			
		||||
@ -22,19 +23,19 @@ export default function UserList ({ users }) {
 | 
			
		||||
            </a>
 | 
			
		||||
          </Link>
 | 
			
		||||
          <div className={styles.other}>
 | 
			
		||||
            <span>{user.stacked} stacked</span>
 | 
			
		||||
            <span>{abbrNum(user.stacked)} stacked</span>
 | 
			
		||||
            <span> \ </span>
 | 
			
		||||
            <span>{user.spent} spent</span>
 | 
			
		||||
            <span>{abbrNum(user.spent)} spent</span>
 | 
			
		||||
            <span> \ </span>
 | 
			
		||||
            <Link href={`/${user.name}/posts`} passHref>
 | 
			
		||||
              <a className='text-reset'>
 | 
			
		||||
                {user.nitems} posts
 | 
			
		||||
                {abbrNum(user.nitems)} posts
 | 
			
		||||
              </a>
 | 
			
		||||
            </Link>
 | 
			
		||||
            <span> \ </span>
 | 
			
		||||
            <Link href={`/${user.name}/comments`} passHref>
 | 
			
		||||
              <a className='text-reset'>
 | 
			
		||||
                {user.ncomments} comments
 | 
			
		||||
                {abbrNum(user.ncomments)} comments
 | 
			
		||||
              </a>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </div>
 | 
			
		||||
@ -44,3 +45,28 @@ export default function UserList ({ users }) {
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function UsersSkeleton () {
 | 
			
		||||
  const users = new Array(21).fill(null)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>{users.map((_, i) => (
 | 
			
		||||
      <div className={`${styles.item} ${styles.skeleton} mb-2`} key={i}>
 | 
			
		||||
        <Image
 | 
			
		||||
          src='/clouds.jpeg' width='32' height='32'
 | 
			
		||||
          className={`${userStyles.userimg} clouds mr-2`}
 | 
			
		||||
        />
 | 
			
		||||
        <div className={styles.hunk}>
 | 
			
		||||
          <div className={`${styles.name} clouds text-reset`} />
 | 
			
		||||
          <div className={styles.other}>
 | 
			
		||||
            <span className={`${styles.otherItem} clouds`} />
 | 
			
		||||
            <span className={`${styles.otherItem} clouds`} />
 | 
			
		||||
            <span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
 | 
			
		||||
            <span className={`${styles.otherItem} ${styles.otherItemLonger} clouds`} />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,19 @@ export const MORE_FLAT_COMMENTS = gql`
 | 
			
		||||
  }
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
export const TOP_COMMENTS = gql`
 | 
			
		||||
  ${COMMENT_FIELDS}
 | 
			
		||||
 | 
			
		||||
  query topComments($sort: String, $cursor: String, $when: String) {
 | 
			
		||||
    topComments(sort: $sort, cursor: $cursor, when: $when) {
 | 
			
		||||
      cursor
 | 
			
		||||
      comments {
 | 
			
		||||
        ...CommentFields
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
export const COMMENTS = gql`
 | 
			
		||||
  ${COMMENT_FIELDS}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -70,6 +70,23 @@ export const ITEMS = gql`
 | 
			
		||||
    }
 | 
			
		||||
  }`
 | 
			
		||||
 | 
			
		||||
export const TOP_ITEMS = gql`
 | 
			
		||||
  ${ITEM_FIELDS}
 | 
			
		||||
 | 
			
		||||
  query topItems($sort: String, $cursor: String, $when: String) {
 | 
			
		||||
    topItems(sort: $sort, cursor: $cursor, when: $when) {
 | 
			
		||||
      cursor
 | 
			
		||||
      items {
 | 
			
		||||
        ...ItemFields
 | 
			
		||||
        position
 | 
			
		||||
      },
 | 
			
		||||
      pins {
 | 
			
		||||
        ...ItemFields
 | 
			
		||||
        position
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }`
 | 
			
		||||
 | 
			
		||||
export const OUTLAWED_ITEMS = gql`
 | 
			
		||||
  ${ITEM_FIELDS}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -152,11 +152,15 @@ export const USER_FIELDS = gql`
 | 
			
		||||
  }`
 | 
			
		||||
 | 
			
		||||
export const TOP_USERS = gql`
 | 
			
		||||
  query TopUsers($cursor: String, $within: String!, $userType: String!) {
 | 
			
		||||
    topUsers(cursor: $cursor, within: $within, userType: $userType) {
 | 
			
		||||
  query TopUsers($cursor: String, $when: String, $sort: String) {
 | 
			
		||||
    topUsers(cursor: $cursor, when: $when, sort: $sort) {
 | 
			
		||||
      users {
 | 
			
		||||
        name
 | 
			
		||||
        amount
 | 
			
		||||
        photoId
 | 
			
		||||
        stacked
 | 
			
		||||
        spent
 | 
			
		||||
        ncomments
 | 
			
		||||
        nitems
 | 
			
		||||
      }
 | 
			
		||||
      cursor
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -26,7 +26,7 @@ export default function getApolloClient () {
 | 
			
		||||
        Query: {
 | 
			
		||||
          fields: {
 | 
			
		||||
            topUsers: {
 | 
			
		||||
              keyArgs: ['within'],
 | 
			
		||||
              keyArgs: ['when', 'sort'],
 | 
			
		||||
              merge (existing, incoming) {
 | 
			
		||||
                if (isFirstPage(incoming.cursor, existing?.users)) {
 | 
			
		||||
                  return incoming
 | 
			
		||||
@ -52,6 +52,33 @@ export default function getApolloClient () {
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            topItems: {
 | 
			
		||||
              keyArgs: ['sort', 'when'],
 | 
			
		||||
              merge (existing, incoming) {
 | 
			
		||||
                if (isFirstPage(incoming.cursor, existing?.items)) {
 | 
			
		||||
                  return incoming
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                  cursor: incoming.cursor,
 | 
			
		||||
                  items: [...(existing?.items || []), ...incoming.items],
 | 
			
		||||
                  pins: existing?.pins || null
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            topComments: {
 | 
			
		||||
              keyArgs: ['sort', 'when'],
 | 
			
		||||
              merge (existing, incoming) {
 | 
			
		||||
                if (isFirstPage(incoming.cursor, existing?.comments)) {
 | 
			
		||||
                  return incoming
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                  cursor: incoming.cursor,
 | 
			
		||||
                  comments: [...(existing?.comments || []), ...incoming.comments]
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            outlawedItems: {
 | 
			
		||||
              keyArgs: [],
 | 
			
		||||
              merge (existing, incoming) {
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
export const formatSats = n => {
 | 
			
		||||
export const abbrNum = n => {
 | 
			
		||||
  if (n < 1e4) return n
 | 
			
		||||
  if (n >= 1e4 && n < 1e6) return +(n / 1e3).toFixed(1) + 'k'
 | 
			
		||||
  if (n >= 1e6 && n < 1e9) return +(n / 1e6).toFixed(1) + 'm'
 | 
			
		||||
 | 
			
		||||
@ -2,12 +2,12 @@ import Layout from '../../../components/layout'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { getGetServerSideProps } from '../../../api/ssrApollo'
 | 
			
		||||
import TopHeader from '../../../components/top-header'
 | 
			
		||||
import { MORE_FLAT_COMMENTS } from '../../../fragments/comments'
 | 
			
		||||
import { TOP_COMMENTS } from '../../../fragments/comments'
 | 
			
		||||
import CommentsFlat from '../../../components/comments-flat'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps(MORE_FLAT_COMMENTS, { sort: 'top' })
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps(TOP_COMMENTS)
 | 
			
		||||
 | 
			
		||||
export default function Index ({ data: { moreFlatComments: { comments, cursor } } }) {
 | 
			
		||||
export default function Index ({ data: { topComments: { comments, cursor } } }) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -15,7 +15,9 @@ export default function Index ({ data: { moreFlatComments: { comments, cursor }
 | 
			
		||||
      <TopHeader cat='comments' />
 | 
			
		||||
      <CommentsFlat
 | 
			
		||||
        comments={comments} cursor={cursor}
 | 
			
		||||
        variables={{ sort: 'top', within: router.query?.within }}
 | 
			
		||||
        query={TOP_COMMENTS}
 | 
			
		||||
        destructureData={data => data.topComments}
 | 
			
		||||
        variables={{ sort: router.query.sort, when: router.query.when }}
 | 
			
		||||
        includeParent noReply
 | 
			
		||||
      />
 | 
			
		||||
    </Layout>
 | 
			
		||||
@ -2,13 +2,12 @@ import Layout from '../../../components/layout'
 | 
			
		||||
import Items from '../../../components/items'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { getGetServerSideProps } from '../../../api/ssrApollo'
 | 
			
		||||
import { ITEMS } from '../../../fragments/items'
 | 
			
		||||
 | 
			
		||||
import { TOP_ITEMS } from '../../../fragments/items'
 | 
			
		||||
import TopHeader from '../../../components/top-header'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps(ITEMS, { sort: 'top' })
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps(TOP_ITEMS)
 | 
			
		||||
 | 
			
		||||
export default function Index ({ data: { items: { items, cursor } } }) {
 | 
			
		||||
export default function Index ({ data: { topItems: { items, cursor } } }) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -16,7 +15,9 @@ export default function Index ({ data: { items: { items, cursor } } }) {
 | 
			
		||||
      <TopHeader cat='posts' />
 | 
			
		||||
      <Items
 | 
			
		||||
        items={items} cursor={cursor}
 | 
			
		||||
        variables={{ sort: 'top', within: router.query?.within }} rank
 | 
			
		||||
        query={TOP_ITEMS}
 | 
			
		||||
        destructureData={data => data.topItems}
 | 
			
		||||
        variables={{ sort: router.query.sort, when: router.query.when }} rank
 | 
			
		||||
      />
 | 
			
		||||
    </Layout>
 | 
			
		||||
  )
 | 
			
		||||
@ -1,52 +0,0 @@
 | 
			
		||||
import Layout from '../../../../components/layout'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { getGetServerSideProps } from '../../../../api/ssrApollo'
 | 
			
		||||
import TopHeader from '../../../../components/top-header'
 | 
			
		||||
import { TOP_USERS } from '../../../../fragments/users'
 | 
			
		||||
import { useQuery } from '@apollo/client'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import MoreFooter from '../../../../components/more-footer'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps(TOP_USERS)
 | 
			
		||||
 | 
			
		||||
export default function Index ({ data: { topUsers: { users, cursor } } }) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const userType = router.query.userType
 | 
			
		||||
 | 
			
		||||
  const { data, fetchMore } = useQuery(TOP_USERS, {
 | 
			
		||||
    variables: { within: router.query?.within, userType: router.query?.userType }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (data) {
 | 
			
		||||
    ({ topUsers: { users, cursor } } = data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <TopHeader cat={'users/' + userType} />
 | 
			
		||||
      {users.map(user => (
 | 
			
		||||
        <Link href={`/${user.name}`} key={user.name}>
 | 
			
		||||
          <div className='d-flex align-items-center pointer'>
 | 
			
		||||
            <h3 className='mb-0'>@{user.name}</h3>
 | 
			
		||||
            <h2 className='ml-2 mb-0'><small className='text-success'>{user.amount} {userType}</small></h2>
 | 
			
		||||
          </div>
 | 
			
		||||
        </Link>
 | 
			
		||||
      ))}
 | 
			
		||||
      <MoreFooter cursor={cursor} fetchMore={fetchMore} Skeleton={UsersSkeleton} />
 | 
			
		||||
    </Layout>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function UsersSkeleton () {
 | 
			
		||||
  const users = new Array(21).fill(null)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>{users.map((_, i) => (
 | 
			
		||||
      <div key={i} className='d-flex align-items-center' style={{ height: '34px' }}>
 | 
			
		||||
        <div className='clouds' style={{ width: '172px', borderRadius: '.4rem', height: '27px' }} />
 | 
			
		||||
        <div className='ml-2 clouds' style={{ width: '137px', borderRadius: '.4rem', height: '30px', margin: '3px 0px' }} />
 | 
			
		||||
      </div>
 | 
			
		||||
    ))}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								pages/top/users/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								pages/top/users/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
import Layout from '../../../components/layout'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import { getGetServerSideProps } from '../../../api/ssrApollo'
 | 
			
		||||
import TopHeader from '../../../components/top-header'
 | 
			
		||||
import { TOP_USERS } from '../../../fragments/users'
 | 
			
		||||
import { useQuery } from '@apollo/client'
 | 
			
		||||
import MoreFooter from '../../../components/more-footer'
 | 
			
		||||
import UserList, { UsersSkeleton } from '../../../components/user-list'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps(TOP_USERS)
 | 
			
		||||
 | 
			
		||||
export default function Index ({ data: { topUsers: { users, cursor } } }) {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  const { data, fetchMore } = useQuery(TOP_USERS, {
 | 
			
		||||
    variables: { when: router.query.when, sort: router.query.sort }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (data) {
 | 
			
		||||
    ({ topUsers: { users, cursor } } = data)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <TopHeader cat='users' />
 | 
			
		||||
      <UserList users={users} />
 | 
			
		||||
      <MoreFooter cursor={cursor} fetchMore={fetchMore} Skeleton={UsersSkeleton} />
 | 
			
		||||
    </Layout>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -3,7 +3,7 @@ import { getGetServerSideProps } from '../../api/ssrApollo'
 | 
			
		||||
import Layout from '../../components/layout'
 | 
			
		||||
import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts'
 | 
			
		||||
import { Col, Row } from 'react-bootstrap'
 | 
			
		||||
import { formatSats } from '../../lib/format'
 | 
			
		||||
import { abbrNum } from '../../lib/format'
 | 
			
		||||
import { UsageHeader } from '../../components/usage-header'
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = getGetServerSideProps(
 | 
			
		||||
@ -115,7 +115,7 @@ function GrowthAreaChart ({ data, xName, title }) {
 | 
			
		||||
          dataKey='time' tickFormatter={dateFormatter} name={xName}
 | 
			
		||||
          tick={{ fill: 'var(--theme-grey)' }}
 | 
			
		||||
        />
 | 
			
		||||
        <YAxis tickFormatter={formatSats} tick={{ fill: 'var(--theme-grey)' }} />
 | 
			
		||||
        <YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
 | 
			
		||||
        <Tooltip labelFormatter={dateFormatter} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
 | 
			
		||||
        <Legend />
 | 
			
		||||
        {Object.keys(data[0]).filter(v => v !== 'time' && v !== '__typename').map((v, i) =>
 | 
			
		||||
@ -141,7 +141,7 @@ function GrowthLineChart ({ data, xName, yName }) {
 | 
			
		||||
          dataKey='time' tickFormatter={dateFormatter} name={xName}
 | 
			
		||||
          tick={{ fill: 'var(--theme-grey)' }}
 | 
			
		||||
        />
 | 
			
		||||
        <YAxis tickFormatter={formatSats} tick={{ fill: 'var(--theme-grey)' }} />
 | 
			
		||||
        <YAxis tickFormatter={abbrNum} tick={{ fill: 'var(--theme-grey)' }} />
 | 
			
		||||
        <Tooltip labelFormatter={dateFormatter} contentStyle={{ color: 'var(--theme-color)', backgroundColor: 'var(--theme-body)' }} />
 | 
			
		||||
        <Legend />
 | 
			
		||||
        <Line type='monotone' dataKey='num' name={yName} stroke='var(--secondary)' />
 | 
			
		||||
 | 
			
		||||
@ -49,18 +49,18 @@ export default function Growth ({
 | 
			
		||||
        </Col>
 | 
			
		||||
      </Row>
 | 
			
		||||
      <Row>
 | 
			
		||||
        <Col className='mt-3'>
 | 
			
		||||
        <Col className='mt-3 p-0'>
 | 
			
		||||
          <div className='text-center text-muted font-weight-bold'>items</div>
 | 
			
		||||
          <GrowthPieChart data={itemsWeekly} />
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col className='mt-3'>
 | 
			
		||||
          <div className='text-center text-muted font-weight-bold'>stacked</div>
 | 
			
		||||
          <GrowthPieChart data={stackedWeekly} />
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col className='mt-3'>
 | 
			
		||||
        <Col className='mt-3 p-0'>
 | 
			
		||||
          <div className='text-center text-muted font-weight-bold'>spent</div>
 | 
			
		||||
          <GrowthPieChart data={spentWeekly} />
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col className='mt-3 p-0'>
 | 
			
		||||
          <div className='text-center text-muted font-weight-bold'>stacked</div>
 | 
			
		||||
          <GrowthPieChart data={stackedWeekly} />
 | 
			
		||||
        </Col>
 | 
			
		||||
      </Row>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user