Add Territory Sub management tab in Subscriptions (#2191)
* Add Territory Sub management tab in Subscriptions * don't use queryRawUnsafe * auto width on select * separate into pages for browser nav * fix multiple separators * simplify queries --------- Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
parent
874694eb10
commit
dc01ebdb26
@ -127,7 +127,7 @@ export default {
|
|||||||
subs
|
subs
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userSubs: async (_parent, { name, cursor, when, by, from, to, limit = LIMIT }, { models }) => {
|
userSubs: async (_parent, { name, cursor, when, by, from, to, limit = LIMIT }, { models, me }) => {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new GqlInputError('must supply user name')
|
throw new GqlInputError('must supply user name')
|
||||||
}
|
}
|
||||||
@ -150,26 +150,56 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subs = await models.$queryRawUnsafe(`
|
const subs = await models.$queryRawUnsafe(`
|
||||||
SELECT "Sub".*,
|
SELECT "Sub".*,
|
||||||
"Sub".created_at as "createdAt",
|
"Sub".created_at as "createdAt",
|
||||||
COALESCE(floor(sum(msats_revenue)/1000), 0) as revenue,
|
COALESCE(floor(sum(msats_revenue)/1000), 0) as revenue,
|
||||||
COALESCE(floor(sum(msats_stacked)/1000), 0) as stacked,
|
COALESCE(floor(sum(msats_stacked)/1000), 0) as stacked,
|
||||||
COALESCE(floor(sum(msats_spent)/1000), 0) as spent,
|
COALESCE(floor(sum(msats_spent)/1000), 0) as spent,
|
||||||
COALESCE(sum(posts), 0) as nposts,
|
COALESCE(sum(posts), 0) as nposts,
|
||||||
COALESCE(sum(comments), 0) as ncomments
|
COALESCE(sum(comments), 0) as ncomments,
|
||||||
FROM ${viewGroup(range, 'sub_stats')}
|
ss."userId" IS NOT NULL as "meSubscription",
|
||||||
JOIN "Sub" on "Sub".name = u.sub_name
|
ms."userId" IS NOT NULL as "meMuteSub"
|
||||||
WHERE "Sub"."userId" = $3
|
FROM ${viewGroup(range, 'sub_stats')}
|
||||||
AND "Sub".status = 'ACTIVE'
|
JOIN "Sub" on "Sub".name = u.sub_name
|
||||||
GROUP BY "Sub".name
|
LEFT JOIN "SubSubscription" ss ON ss."subName" = "Sub".name AND ss."userId" IS NOT DISTINCT FROM $4
|
||||||
ORDER BY ${column} DESC NULLS LAST, "Sub".created_at ASC
|
LEFT JOIN "MuteSub" ms ON ms."subName" = "Sub".name AND ms."userId" IS NOT DISTINCT FROM $4
|
||||||
OFFSET $4
|
WHERE "Sub"."userId" = $3
|
||||||
LIMIT $5`, ...range, user.id, decodedCursor.offset, limit)
|
AND "Sub".status = 'ACTIVE'
|
||||||
|
GROUP BY "Sub".name, ss."userId", ms."userId"
|
||||||
|
ORDER BY ${column} DESC NULLS LAST, "Sub".created_at ASC
|
||||||
|
OFFSET $5
|
||||||
|
LIMIT $6
|
||||||
|
`, ...range, user.id, me?.id, decodedCursor.offset, limit)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cursor: subs.length === limit ? nextCursorEncoded(decodedCursor, limit) : null,
|
cursor: subs.length === limit ? nextCursorEncoded(decodedCursor, limit) : null,
|
||||||
subs
|
subs
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mySubscribedSubs: async (parent, { cursor }, { models, me }) => {
|
||||||
|
if (!me) {
|
||||||
|
throw new GqlAuthenticationError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedCursor = decodeCursor(cursor)
|
||||||
|
const subs = await models.$queryRaw`
|
||||||
|
SELECT "Sub".*,
|
||||||
|
"MuteSub"."userId" IS NOT NULL as "meMuteSub",
|
||||||
|
TRUE as "meSubscription"
|
||||||
|
FROM "SubSubscription"
|
||||||
|
JOIN "Sub" ON "SubSubscription"."subName" = "Sub".name
|
||||||
|
LEFT JOIN "MuteSub" ON "MuteSub"."subName" = "Sub".name AND "MuteSub"."userId" = ${me.id}
|
||||||
|
WHERE "SubSubscription"."userId" = ${me.id}
|
||||||
|
AND "Sub".status <> 'STOPPED'
|
||||||
|
ORDER BY "Sub".name ASC
|
||||||
|
OFFSET ${decodedCursor.offset}
|
||||||
|
LIMIT ${LIMIT}
|
||||||
|
`
|
||||||
|
|
||||||
|
return {
|
||||||
|
cursor: subs.length === LIMIT ? nextCursorEncoded(decodedCursor, LIMIT) : null,
|
||||||
|
subs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
|
@ -7,6 +7,7 @@ export default gql`
|
|||||||
subs: [Sub!]!
|
subs: [Sub!]!
|
||||||
topSubs(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
|
topSubs(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
|
||||||
userSubs(name: String!, cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
|
userSubs(name: String!, cursor: String, when: String, from: String, to: String, by: String, limit: Limit): Subs
|
||||||
|
mySubscribedSubs(cursor: String): Subs
|
||||||
subSuggestions(q: String!, limit: Limit): [Sub!]!
|
subSuggestions(q: String!, limit: Limit): [Sub!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { createContext, useContext } from 'react'
|
||||||
import { Badge, Button, CardFooter, Dropdown } from 'react-bootstrap'
|
import { Badge, Button, CardFooter, Dropdown } from 'react-bootstrap'
|
||||||
import { AccordianCard } from './accordian-item'
|
import { AccordianCard } from './accordian-item'
|
||||||
import TerritoryPaymentDue, { TerritoryBillingLine } from './territory-payment-due'
|
import TerritoryPaymentDue, { TerritoryBillingLine } from './territory-payment-due'
|
||||||
@ -13,6 +14,16 @@ import { useToast } from './toast'
|
|||||||
import ActionDropdown from './action-dropdown'
|
import ActionDropdown from './action-dropdown'
|
||||||
import { TerritoryTransferDropdownItem } from './territory-transfer'
|
import { TerritoryTransferDropdownItem } from './territory-transfer'
|
||||||
|
|
||||||
|
const SubscribeTerritoryContext = createContext({ refetchQueries: [] })
|
||||||
|
|
||||||
|
export const SubscribeTerritoryContextProvider = ({ children, value }) => (
|
||||||
|
<SubscribeTerritoryContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</SubscribeTerritoryContext.Provider>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const useSubscribeTerritoryContext = () => useContext(SubscribeTerritoryContext)
|
||||||
|
|
||||||
export function TerritoryDetails ({ sub, children }) {
|
export function TerritoryDetails ({ sub, children }) {
|
||||||
return (
|
return (
|
||||||
<AccordianCard
|
<AccordianCard
|
||||||
@ -149,12 +160,15 @@ export default function TerritoryHeader ({ sub }) {
|
|||||||
|
|
||||||
export function MuteSubDropdownItem ({ item, sub }) {
|
export function MuteSubDropdownItem ({ item, sub }) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
const { refetchQueries } = useSubscribeTerritoryContext()
|
||||||
|
|
||||||
const [toggleMuteSub] = useMutation(
|
const [toggleMuteSub] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation toggleMuteSub($name: String!) {
|
mutation toggleMuteSub($name: String!) {
|
||||||
toggleMuteSub(name: $name)
|
toggleMuteSub(name: $name)
|
||||||
}`, {
|
}`, {
|
||||||
|
refetchQueries,
|
||||||
|
awaitRefetchQueries: true,
|
||||||
update (cache, { data: { toggleMuteSub } }) {
|
update (cache, { data: { toggleMuteSub } }) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
id: `Sub:{"name":"${sub.name}"}`,
|
id: `Sub:{"name":"${sub.name}"}`,
|
||||||
@ -213,11 +227,14 @@ export function PinSubDropdownItem ({ item: { id, position } }) {
|
|||||||
|
|
||||||
export function ToggleSubSubscriptionDropdownItem ({ sub: { name, meSubscription } }) {
|
export function ToggleSubSubscriptionDropdownItem ({ sub: { name, meSubscription } }) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
const { refetchQueries } = useSubscribeTerritoryContext()
|
||||||
const [toggleSubSubscription] = useMutation(
|
const [toggleSubSubscription] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation toggleSubSubscription($name: String!) {
|
mutation toggleSubSubscription($name: String!) {
|
||||||
toggleSubSubscription(name: $name)
|
toggleSubSubscription(name: $name)
|
||||||
}`, {
|
}`, {
|
||||||
|
refetchQueries,
|
||||||
|
awaitRefetchQueries: true,
|
||||||
update (cache, { data: { toggleSubSubscription } }) {
|
update (cache, { data: { toggleSubSubscription } }) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
id: `Sub:{"name":"${name}"}`,
|
id: `Sub:{"name":"${name}"}`,
|
||||||
|
@ -5,8 +5,10 @@ import React, { useEffect, useMemo, useState } from 'react'
|
|||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import MoreFooter from './more-footer'
|
import MoreFooter from './more-footer'
|
||||||
import { useData } from './use-data'
|
import { useData } from './use-data'
|
||||||
|
import { useMe } from './me'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
import { TerritoryInfo } from './territory-header'
|
import ActionDropdown from './action-dropdown'
|
||||||
|
import { TerritoryInfo, ToggleSubSubscriptionDropdownItem, MuteSubDropdownItem } from './territory-header'
|
||||||
|
|
||||||
// all of this nonsense is to show the stat we are sorting by first
|
// all of this nonsense is to show the stat we are sorting by first
|
||||||
const Revenue = ({ sub }) => (sub.optional.revenue !== null && <span>{abbrNum(sub.optional.revenue)} revenue</span>)
|
const Revenue = ({ sub }) => (sub.optional.revenue !== null && <span>{abbrNum(sub.optional.revenue)} revenue</span>)
|
||||||
@ -35,16 +37,17 @@ function separate (arr, separator) {
|
|||||||
return arr.flatMap((x, i) => i < arr.length - 1 ? [x, separator] : [x])
|
return arr.flatMap((x, i) => i < arr.length - 1 ? [x, separator] : [x])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TerritoryList ({ ssrData, query, variables, destructureData, rank }) {
|
export default function TerritoryList ({ ssrData, query, variables, destructureData, rank, subActionDropdown, statCompsProp = STAT_COMPONENTS }) {
|
||||||
const { data, fetchMore } = useQuery(query, { variables })
|
const { data, fetchMore } = useQuery(query, { variables })
|
||||||
const dat = useData(data, ssrData)
|
const dat = useData(data, ssrData)
|
||||||
const [statComps, setStatComps] = useState(separate(STAT_COMPONENTS, Separator))
|
const { me } = useMe()
|
||||||
|
const [statComps, setStatComps] = useState(separate(statCompsProp, Separator))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// shift the stat we are sorting by to the front
|
// shift the stat we are sorting by to the front
|
||||||
const comps = [...STAT_COMPONENTS]
|
const comps = [...statCompsProp]
|
||||||
setStatComps(separate([...comps.splice(STAT_POS[variables?.by || 0], 1), ...comps], Separator))
|
setStatComps(separate([...comps.splice(STAT_POS[variables?.by || 0], 1), ...comps], Separator))
|
||||||
}, [variables?.by])
|
}, [variables?.by], statCompsProp)
|
||||||
|
|
||||||
const { subs, cursor } = useMemo(() => {
|
const { subs, cursor } = useMemo(() => {
|
||||||
if (!dat) return {}
|
if (!dat) return {}
|
||||||
@ -77,6 +80,12 @@ export default function TerritoryList ({ ssrData, query, variables, destructureD
|
|||||||
{sub.name}
|
{sub.name}
|
||||||
</Link>
|
</Link>
|
||||||
<Info className='d-flex'><TerritoryInfo sub={sub} /></Info>
|
<Info className='d-flex'><TerritoryInfo sub={sub} /></Info>
|
||||||
|
{me && subActionDropdown && (
|
||||||
|
<ActionDropdown>
|
||||||
|
<ToggleSubSubscriptionDropdownItem sub={sub} />
|
||||||
|
<MuteSubDropdownItem sub={sub} />
|
||||||
|
</ActionDropdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.other}>
|
<div className={styles.other}>
|
||||||
{statComps.map((Comp, i) => <Comp key={i} sub={sub} />)}
|
{statComps.map((Comp, i) => <Comp key={i} sub={sub} />)}
|
||||||
|
@ -216,25 +216,6 @@ export const USER_FIELDS = gql`
|
|||||||
...StreakFields
|
...StreakFields
|
||||||
}`
|
}`
|
||||||
|
|
||||||
export const MY_SUBSCRIBED_USERS = gql`
|
|
||||||
${STREAK_FIELDS}
|
|
||||||
query MySubscribedUsers($cursor: String) {
|
|
||||||
mySubscribedUsers(cursor: $cursor) {
|
|
||||||
users {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
photoId
|
|
||||||
meSubscriptionPosts
|
|
||||||
meSubscriptionComments
|
|
||||||
meMute
|
|
||||||
|
|
||||||
...StreakFields
|
|
||||||
}
|
|
||||||
cursor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const MY_MUTED_USERS = gql`
|
export const MY_MUTED_USERS = gql`
|
||||||
${STREAK_FIELDS}
|
${STREAK_FIELDS}
|
||||||
query MyMutedUsers($cursor: String) {
|
query MyMutedUsers($cursor: String) {
|
||||||
@ -390,3 +371,33 @@ export const USER_STATS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
export const MY_SUBSCRIBED_USERS = gql`
|
||||||
|
${STREAK_FIELDS}
|
||||||
|
query MySubscribedUsers($cursor: String) {
|
||||||
|
mySubscribedUsers(cursor: $cursor) {
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
photoId
|
||||||
|
meSubscriptionPosts
|
||||||
|
meSubscriptionComments
|
||||||
|
meMute
|
||||||
|
...StreakFields
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const MY_SUBSCRIBED_SUBS = gql`
|
||||||
|
${SUB_FULL_FIELDS}
|
||||||
|
query MySubscribedSubs($cursor: String) {
|
||||||
|
mySubscribedSubs(cursor: $cursor) {
|
||||||
|
subs {
|
||||||
|
...SubFullFields
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
@ -25,6 +25,7 @@ export default function UserTerritories ({ ssrData }) {
|
|||||||
query={USER_WITH_SUBS}
|
query={USER_WITH_SUBS}
|
||||||
variables={variables}
|
variables={variables}
|
||||||
destructureData={data => data.userSubs}
|
destructureData={data => data.userSubs}
|
||||||
|
subActionDropdown
|
||||||
rank
|
rank
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,7 +68,7 @@ export function SettingsHeader () {
|
|||||||
</Link>
|
</Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Link href='/settings/subscriptions' passHref legacyBehavior>
|
<Link href='/settings/subscriptions/stackers' passHref legacyBehavior>
|
||||||
<Nav.Link eventKey='subscriptions'>subscriptions</Nav.Link>
|
<Nav.Link eventKey='subscriptions'>subscriptions</Nav.Link>
|
||||||
</Link>
|
</Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import { useMemo } from 'react'
|
|
||||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
|
||||||
import Layout from '@/components/layout'
|
|
||||||
import UserList from '@/components/user-list'
|
|
||||||
import { MY_SUBSCRIBED_USERS } from '@/fragments/users'
|
|
||||||
import { SettingsHeader } from '../index'
|
|
||||||
import { SubscribeUserContextProvider } from '@/components/subscribeUser'
|
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ query: MY_SUBSCRIBED_USERS, authRequired: true })
|
|
||||||
|
|
||||||
export default function MySubscribedUsers ({ ssrData }) {
|
|
||||||
const subscribeUserContextValue = useMemo(() => ({ refetchQueries: ['MySubscribedUsers'] }), [])
|
|
||||||
return (
|
|
||||||
<Layout>
|
|
||||||
<div className='pb-3 w-100 mt-2'>
|
|
||||||
<SettingsHeader />
|
|
||||||
<div className='mb-4 text-muted'>These here are stackers you've hitched your wagon to, pardner.</div>
|
|
||||||
<SubscribeUserContextProvider value={subscribeUserContextValue}>
|
|
||||||
<UserList
|
|
||||||
ssrData={ssrData} query={MY_SUBSCRIBED_USERS}
|
|
||||||
destructureData={data => data.mySubscribedUsers}
|
|
||||||
variables={{}}
|
|
||||||
rank
|
|
||||||
nymActionDropdown
|
|
||||||
statCompsProp={[]}
|
|
||||||
/>
|
|
||||||
</SubscribeUserContextProvider>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
}
|
|
55
pages/settings/subscriptions/stackers.js
Normal file
55
pages/settings/subscriptions/stackers.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||||
|
import Layout from '@/components/layout'
|
||||||
|
import { Select } from '@/components/form'
|
||||||
|
import UserList from '@/components/user-list'
|
||||||
|
import { MY_SUBSCRIBED_USERS } from '@/fragments/users'
|
||||||
|
import { SettingsHeader } from '../index'
|
||||||
|
import { SubscribeUserContextProvider } from '@/components/subscribeUser'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
export const getServerSideProps = getGetServerSideProps({
|
||||||
|
query: MY_SUBSCRIBED_USERS,
|
||||||
|
authRequired: true
|
||||||
|
})
|
||||||
|
|
||||||
|
export function SubscriptionLayout ({ subType, children }) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className='pb-3 w-100 mt-2'>
|
||||||
|
<SettingsHeader />
|
||||||
|
<Select
|
||||||
|
name='subscriptionType'
|
||||||
|
size='sm'
|
||||||
|
className='w-auto'
|
||||||
|
noForm
|
||||||
|
items={['stackers', 'territories']}
|
||||||
|
value={subType}
|
||||||
|
onChange={(_, e) => router.push(`/settings/subscriptions/${e.target.value}`)}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MySubscribedUsers ({ ssrData }) {
|
||||||
|
const subscribeContextValue = useMemo(() => ({ refetchQueries: ['MySubscribedUsers'] }), [])
|
||||||
|
return (
|
||||||
|
<SubscriptionLayout subType='stackers'>
|
||||||
|
<SubscribeUserContextProvider value={subscribeContextValue}>
|
||||||
|
<UserList
|
||||||
|
ssrData={ssrData}
|
||||||
|
query={MY_SUBSCRIBED_USERS}
|
||||||
|
destructureData={data => data.mySubscribedUsers}
|
||||||
|
variables={{}}
|
||||||
|
rank
|
||||||
|
nymActionDropdown
|
||||||
|
statCompsProp={[]}
|
||||||
|
/>
|
||||||
|
</SubscribeUserContextProvider>
|
||||||
|
</SubscriptionLayout>
|
||||||
|
)
|
||||||
|
}
|
30
pages/settings/subscriptions/territories.js
Normal file
30
pages/settings/subscriptions/territories.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||||
|
import { MY_SUBSCRIBED_SUBS } from '@/fragments/users'
|
||||||
|
import TerritoryList from '@/components/territory-list'
|
||||||
|
import { SubscribeTerritoryContextProvider } from '@/components/territory-header'
|
||||||
|
import { SubscriptionLayout } from './stackers'
|
||||||
|
|
||||||
|
export const getServerSideProps = getGetServerSideProps({
|
||||||
|
query: MY_SUBSCRIBED_SUBS,
|
||||||
|
authRequired: true
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function MySubscribedSubs ({ ssrData }) {
|
||||||
|
const subscribeContextValue = useMemo(() => ({ refetchQueries: ['MySubscribedSubs'] }), [])
|
||||||
|
return (
|
||||||
|
<SubscriptionLayout subType='territories'>
|
||||||
|
<SubscribeTerritoryContextProvider value={subscribeContextValue}>
|
||||||
|
<TerritoryList
|
||||||
|
ssrData={ssrData}
|
||||||
|
query={MY_SUBSCRIBED_SUBS}
|
||||||
|
variables={{}}
|
||||||
|
destructureData={data => data.mySubscribedSubs}
|
||||||
|
rank
|
||||||
|
subActionDropdown
|
||||||
|
statCompsProp={[]}
|
||||||
|
/>
|
||||||
|
</SubscribeTerritoryContextProvider>
|
||||||
|
</SubscriptionLayout>
|
||||||
|
)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user