Add nice stats

This commit is contained in:
MartinBraquet
2025-10-13 18:42:39 +02:00
parent 2d6d3c04ce
commit 0e671d2cc0
6 changed files with 122 additions and 31 deletions

View File

@@ -54,6 +54,7 @@ import swaggerUi from "swagger-ui-express"
import * as fs from "fs"
import {sendSearchNotifications} from "api/send-search-notifications";
import {sendDiscordMessage} from "common/discord/core";
import {getMessagesCount} from "api/get-messages-count";
const allowCorsUnrestricted: RequestHandler = cors({})
@@ -165,6 +166,7 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
'get-channel-messages': getChannelMessages,
'get-channel-seen-time': getLastSeenChannelTime,
'set-channel-seen-time': setChannelLastSeenTime,
'get-messages-count': getMessagesCount,
}
Object.entries(handlers).forEach(([path, handler]) => {

View File

@@ -0,0 +1,18 @@
import {APIHandler} from './helpers/endpoint'
import {createSupabaseDirectClient} from "shared/supabase/init";
export const getMessagesCount: APIHandler<'get-messages-count'> = async (_, auth) => {
const pg = createSupabaseDirectClient()
const result = await pg.one(
`
SELECT COUNT(*) AS count
FROM private_user_messages;
`,
[]
);
const count = Number(result.count);
console.debug('private_user_messages count:', count);
return {
count: count,
}
}

View File

@@ -4,19 +4,19 @@ import {
baseProfilesSchema,
arraybeSchema,
} from 'common/api/zod-types'
import { PrivateChatMessage } from 'common/chat-message'
import { CompatibilityScore } from 'common/love/compatibility-score'
import { MAX_COMPATIBILITY_QUESTION_LENGTH } from 'common/love/constants'
import { Profile, ProfileRow } from 'common/love/profile'
import { Row } from 'common/supabase/utils'
import { PrivateUser, User } from 'common/user'
import { z } from 'zod'
import { LikeData, ShipData } from './love-types'
import { DisplayUser, FullUser } from './user-types'
import { PrivateMessageChannel } from 'common/supabase/private-messages'
import { Notification } from 'common/notifications'
import { arrify } from 'common/util/array'
import { notification_preference } from 'common/user-notification-preferences'
import {PrivateChatMessage} from 'common/chat-message'
import {CompatibilityScore} from 'common/love/compatibility-score'
import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/love/constants'
import {Profile, ProfileRow} from 'common/love/profile'
import {Row} from 'common/supabase/utils'
import {PrivateUser, User} from 'common/user'
import {z} from 'zod'
import {LikeData, ShipData} from './love-types'
import {DisplayUser, FullUser} from './user-types'
import {PrivateMessageChannel} from 'common/supabase/private-messages'
import {Notification} from 'common/notifications'
import {arrify} from 'common/util/array'
import {notification_preference} from 'common/user-notification-preferences'
// mqp: very unscientific, just balancing our willingness to accept load
// with user willingness to put up with stale data
@@ -59,12 +59,12 @@ export const API = (_apiTypeCheck = {
'user/by-id/:id/block': {
method: 'POST',
authed: true,
props: z.object({ id: z.string() }).strict(),
props: z.object({id: z.string()}).strict(),
},
'user/by-id/:id/unblock': {
method: 'POST',
authed: true,
props: z.object({ id: z.string() }).strict(),
props: z.object({id: z.string()}).strict(),
},
'ban-user': {
method: 'POST',
@@ -176,28 +176,28 @@ export const API = (_apiTypeCheck = {
authed: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as FullUser,
props: z.object({ username: z.string() }).strict(),
props: z.object({username: z.string()}).strict(),
},
'user/:username/lite': {
method: 'GET',
authed: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as DisplayUser,
props: z.object({ username: z.string() }).strict(),
props: z.object({username: z.string()}).strict(),
},
'user/by-id/:id': {
method: 'GET',
authed: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as FullUser,
props: z.object({ id: z.string() }).strict(),
props: z.object({id: z.string()}).strict(),
},
'user/by-id/:id/lite': {
method: 'GET',
authed: false,
cache: DEFAULT_CACHE_STRATEGY,
returns: {} as DisplayUser,
props: z.object({ id: z.string() }).strict(),
props: z.object({id: z.string()}).strict(),
},
'search-users': {
method: 'GET',
@@ -215,7 +215,7 @@ export const API = (_apiTypeCheck = {
'compatible-profiles': {
method: 'GET',
authed: false,
props: z.object({ userId: z.string() }),
props: z.object({userId: z.string()}),
returns: {} as {
profile: Profile
compatibleProfiles: Profile[]
@@ -227,7 +227,7 @@ export const API = (_apiTypeCheck = {
'remove-pinned-photo': {
method: 'POST',
authed: true,
returns: { success: true },
returns: {success: true},
props: z
.object({
userId: z.string(),
@@ -338,7 +338,7 @@ export const API = (_apiTypeCheck = {
'get-profile-answers': {
method: 'GET',
authed: false,
props: z.object({ userId: z.string() }).strict(),
props: z.object({userId: z.string()}).strict(),
returns: {} as {
status: 'success'
answers: Row<'love_compatibility_answers'>[]
@@ -477,6 +477,12 @@ export const API = (_apiTypeCheck = {
radius: z.number().min(1).max(500),
}),
},
'get-messages-count': {
method: 'GET',
authed: false,
props: z.object({}),
returns: {} as { count: number },
},
} as const)
export type APIPath = keyof typeof API
@@ -488,8 +494,8 @@ export type ValidatedAPIParams<N extends APIPath> = z.output<
>
export type APIResponse<N extends APIPath> = APISchema<N> extends {
returns: Record<string, any>
}
returns: Record<string, any>
}
? APISchema<N>['returns']
: void

View File

@@ -9,7 +9,7 @@ export const Card = forwardRef(function Card(
return (
<div
className={clsx(
'bg-canvas-0 border-ink-300 cursor-pointer rounded-lg border transition-shadow hover:shadow-md focus:shadow-md',
'bg-canvas-0 border-ink-300 rounded-lg border transition-shadow hover:shadow-md focus:shadow-md',
className
)}
ref={ref}

View File

@@ -1,6 +1,6 @@
import {db} from './db'
import {run} from 'common/supabase/utils'
import {APIError, api} from 'web/lib/api'
import {api, APIError} from 'web/lib/api'
import {unauthedApi} from 'common/util/api'
import type {DisplayUser} from 'common/api/user-types'
import {MIN_BIO_LENGTH} from "common/constants";
@@ -75,4 +75,21 @@ export async function getProfilesWithBioCreations() {
.order('created_time')
)
return data
}
}
export async function getCount(table: string) {
if (table == 'private_user_messages') {
const result = await api('get-messages-count')
return result.count
}
const {count} = await run(
db
.from(table)
.select('*', {count: 'exact', head: true})
)
return count;
}
// export async function getNumberProfiles() {
// return await getCount('profiles');
// }

View File

@@ -1,13 +1,61 @@
import {LovePage} from "web/components/love-page";
import ChartMembers from "web/components/widgets/charts";
import {getCount} from "web/lib/supabase/users";
import {useEffect, useState} from "react";
import StatBox from "web/components/widgets/stat-box";
import clsx from "clsx";
import {Col} from "web/components/layout/col";
export default function Charts() {
const [data, setData] = useState<Record<string, number | null>>({})
useEffect(() => {
async function load() {
const tables = [
'profiles',
'bookmarked_searches',
'private_user_message_channels',
'private_user_messages',
'profile_comments',
'love_compatibility_answers',
] as const
const settled = await Promise.allSettled(
tables.map((t) => getCount(t))
)
const result: Record<string, number | null> = {}
settled.forEach((res, i) => {
const key = tables[i]
if (res.status === 'fulfilled') result[key] = res.value
else result[key] = null
})
setData(result)
}
void load()
}, [])
return (
<LovePage
trackPageView={'charts'}
>
<LovePage trackPageView={'charts'}>
<h1 className="text-3xl font-semibold text-center mb-6">Community Growth over Time</h1>
<ChartMembers/>
<Col className={'mx-4 mb-8'}>
<ChartMembers/>
<Col
className={clsx(
'pb-[58px] lg:pb-0', // bottom bar padding
'text-ink-1000 mx-auto w-full grid grid-cols-1 gap-8 max-w-3xl sm:grid-cols-2 lg:min-h-0 lg:pt-4 mt-4',
)}
>
{!!data.profiles && <StatBox value={data.profiles} label={'Members'} />}
{!!data.bookmarked_searches && <StatBox value={data.bookmarked_searches} label={'Searches Bookmarked'} />}
{!!data.private_user_message_channels && <StatBox value={data.private_user_message_channels} label={'Discussions'} />}
{!!data.private_user_messages && <StatBox value={data.private_user_messages} label={'Messages'} />}
{!!data.profile_comments && <StatBox value={data.profile_comments} label={'Endorsements'} />}
{!!data.love_compatibility_answers && <StatBox value={data.love_compatibility_answers} label={'Prompts Answered'} />}
</Col>
</Col>
</LovePage>
);
}