From dc01ebdb26509c3633ebe181db642d226199d87d Mon Sep 17 00:00:00 2001 From: Bryan Mutai Date: Fri, 13 Jun 2025 23:01:25 +0300 Subject: [PATCH] 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 --- api/resolvers/sub.js | 62 +++++++++++++++------ api/typeDefs/sub.js | 1 + components/territory-header.js | 17 ++++++ components/territory-list.js | 19 +++++-- fragments/users.js | 49 +++++++++------- pages/[name]/territories.js | 1 + pages/settings/index.js | 2 +- pages/settings/subscriptions/index.js | 31 ----------- pages/settings/subscriptions/stackers.js | 55 ++++++++++++++++++ pages/settings/subscriptions/territories.js | 30 ++++++++++ 10 files changed, 195 insertions(+), 72 deletions(-) delete mode 100644 pages/settings/subscriptions/index.js create mode 100644 pages/settings/subscriptions/stackers.js create mode 100644 pages/settings/subscriptions/territories.js diff --git a/api/resolvers/sub.js b/api/resolvers/sub.js index fef5ea73..d2718f1d 100644 --- a/api/resolvers/sub.js +++ b/api/resolvers/sub.js @@ -127,7 +127,7 @@ export default { 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) { throw new GqlInputError('must supply user name') } @@ -150,26 +150,56 @@ export default { } const subs = await models.$queryRawUnsafe(` - SELECT "Sub".*, - "Sub".created_at as "createdAt", - COALESCE(floor(sum(msats_revenue)/1000), 0) as revenue, - COALESCE(floor(sum(msats_stacked)/1000), 0) as stacked, - COALESCE(floor(sum(msats_spent)/1000), 0) as spent, - COALESCE(sum(posts), 0) as nposts, - COALESCE(sum(comments), 0) as ncomments - FROM ${viewGroup(range, 'sub_stats')} - JOIN "Sub" on "Sub".name = u.sub_name - WHERE "Sub"."userId" = $3 - AND "Sub".status = 'ACTIVE' - GROUP BY "Sub".name - ORDER BY ${column} DESC NULLS LAST, "Sub".created_at ASC - OFFSET $4 - LIMIT $5`, ...range, user.id, decodedCursor.offset, limit) + SELECT "Sub".*, + "Sub".created_at as "createdAt", + COALESCE(floor(sum(msats_revenue)/1000), 0) as revenue, + COALESCE(floor(sum(msats_stacked)/1000), 0) as stacked, + COALESCE(floor(sum(msats_spent)/1000), 0) as spent, + COALESCE(sum(posts), 0) as nposts, + COALESCE(sum(comments), 0) as ncomments, + ss."userId" IS NOT NULL as "meSubscription", + ms."userId" IS NOT NULL as "meMuteSub" + FROM ${viewGroup(range, 'sub_stats')} + JOIN "Sub" on "Sub".name = u.sub_name + LEFT JOIN "SubSubscription" ss ON ss."subName" = "Sub".name AND ss."userId" IS NOT DISTINCT FROM $4 + LEFT JOIN "MuteSub" ms ON ms."subName" = "Sub".name AND ms."userId" IS NOT DISTINCT FROM $4 + WHERE "Sub"."userId" = $3 + 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 { cursor: subs.length === limit ? nextCursorEncoded(decodedCursor, limit) : null, 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: { diff --git a/api/typeDefs/sub.js b/api/typeDefs/sub.js index 6167d535..679db42e 100644 --- a/api/typeDefs/sub.js +++ b/api/typeDefs/sub.js @@ -7,6 +7,7 @@ export default gql` subs: [Sub!]! 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 + mySubscribedSubs(cursor: String): Subs subSuggestions(q: String!, limit: Limit): [Sub!]! } diff --git a/components/territory-header.js b/components/territory-header.js index de52ec92..72029e5c 100644 --- a/components/territory-header.js +++ b/components/territory-header.js @@ -1,3 +1,4 @@ +import { createContext, useContext } from 'react' import { Badge, Button, CardFooter, Dropdown } from 'react-bootstrap' import { AccordianCard } from './accordian-item' import TerritoryPaymentDue, { TerritoryBillingLine } from './territory-payment-due' @@ -13,6 +14,16 @@ import { useToast } from './toast' import ActionDropdown from './action-dropdown' import { TerritoryTransferDropdownItem } from './territory-transfer' +const SubscribeTerritoryContext = createContext({ refetchQueries: [] }) + +export const SubscribeTerritoryContextProvider = ({ children, value }) => ( + + {children} + +) + +export const useSubscribeTerritoryContext = () => useContext(SubscribeTerritoryContext) + export function TerritoryDetails ({ sub, children }) { return ( (sub.optional.revenue !== null && {abbrNum(sub.optional.revenue)} revenue) @@ -35,16 +37,17 @@ function separate (arr, separator) { 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 dat = useData(data, ssrData) - const [statComps, setStatComps] = useState(separate(STAT_COMPONENTS, Separator)) + const { me } = useMe() + const [statComps, setStatComps] = useState(separate(statCompsProp, Separator)) useEffect(() => { // 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)) - }, [variables?.by]) + }, [variables?.by], statCompsProp) const { subs, cursor } = useMemo(() => { if (!dat) return {} @@ -77,6 +80,12 @@ export default function TerritoryList ({ ssrData, query, variables, destructureD {sub.name} + {me && subActionDropdown && ( + + + + + )}
{statComps.map((Comp, i) => )} diff --git a/fragments/users.js b/fragments/users.js index adabbe0f..c2df7f81 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -216,25 +216,6 @@ export const USER_FIELDS = gql` ...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` ${STREAK_FIELDS} 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 + } + } +` diff --git a/pages/[name]/territories.js b/pages/[name]/territories.js index bf0f7627..e005f0b9 100644 --- a/pages/[name]/territories.js +++ b/pages/[name]/territories.js @@ -25,6 +25,7 @@ export default function UserTerritories ({ ssrData }) { query={USER_WITH_SUBS} variables={variables} destructureData={data => data.userSubs} + subActionDropdown rank />
diff --git a/pages/settings/index.js b/pages/settings/index.js index 5ad2f813..cf3b2c6f 100644 --- a/pages/settings/index.js +++ b/pages/settings/index.js @@ -68,7 +68,7 @@ export function SettingsHeader () { - + subscriptions diff --git a/pages/settings/subscriptions/index.js b/pages/settings/subscriptions/index.js deleted file mode 100644 index f3d070c1..00000000 --- a/pages/settings/subscriptions/index.js +++ /dev/null @@ -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 ( - -
- -
These here are stackers you've hitched your wagon to, pardner.
- - data.mySubscribedUsers} - variables={{}} - rank - nymActionDropdown - statCompsProp={[]} - /> - -
-
- ) -} diff --git a/pages/settings/subscriptions/stackers.js b/pages/settings/subscriptions/stackers.js new file mode 100644 index 00000000..cd5519bd --- /dev/null +++ b/pages/settings/subscriptions/stackers.js @@ -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 ( + +
+ +