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,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 (
|
||||
|
@ -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,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 <ItemsSkeleton rank={rank} />
|
||||
}
|
||||
|
||||
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 }, {})
|
||||
|
@ -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]}
|
||||
>
|
||||
<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='d-flex'>
|
||||
<Form
|
||||
className='mr-auto'
|
||||
initial={{
|
||||
what: cat,
|
||||
sort: router.query.sort || '',
|
||||
when: router.query.when || ''
|
||||
}}
|
||||
onSubmit={top}
|
||||
>
|
||||
<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