Skip to content
Merged
23 changes: 23 additions & 0 deletions app/helpers/userInfoCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { LRUCache } from "lru-cache";
import { rest } from "#~/discord/api.js";
import { Routes } from "discord.js";

type DiscordUser = { username: string; global_name: string };

const cache = new LRUCache<string, DiscordUser>({
ttl: 1000 * 60 * 60 * 24 * 14, // 14 days
ttlAutopurge: true,
max: 150,
});

export async function getOrFetchUser(id: string) {
if (cache.has(id)) return cache.get(id);

console.log("Fetching user from Discord API:", id);
const { username, global_name } = (await rest.get(
Routes.user(id),
)) as DiscordUser;
const result = { username, global_name };
cache.set(id, result);
return result;
}
19 changes: 18 additions & 1 deletion app/models/activity.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DB } from "#~/db.server";
import db from "#~/db.server";
import { getOrFetchUser } from "#~/helpers/userInfoCache.js";

type MessageStats = DB["message_stats"];

Expand Down Expand Up @@ -86,7 +87,23 @@ export async function getTopParticipants(
intervalEnd,
);

return topMembers.map((m) => scoreMember(m, dailyParticipation[m.author_id]));
const scores = topMembers.map((m) =>
scoreMember(m, dailyParticipation[m.author_id]),
);

const withUsernames = await Promise.all(
scores.map(async (scores) => {
const user = await getOrFetchUser(scores.data.member.author_id);
return {
...scores,
data: {
...scores.data,
member: { ...scores.data.member, username: user?.global_name },
},
};
}),
);
return withUsernames;
}

// copy-pasted out of TopMembers query result
Expand Down
3 changes: 2 additions & 1 deletion app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { route, layout } from "@react-router/dev/routes";

export default [
layout("routes/__auth.tsx", [
route("dashboard", "routes/__auth/dashboard.tsx"),
route(":guildId/sh", "routes/__auth/dashboard.tsx"),
route(":guildId/sh/:userId", "routes/__auth/sh-user.tsx"),
route("login", "routes/__auth/login.tsx"),
route("test", "routes/__auth/test.tsx"),
]),
Expand Down
36 changes: 24 additions & 12 deletions app/routes/__auth/dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import type { Route } from "./+types/dashboard";
import { data, useNavigation } from "react-router";
import { data, useNavigation, useSearchParams, Link } from "react-router";
import type { LabelHTMLAttributes, PropsWithChildren } from "react";
import { getTopParticipants } from "#~/models/activity.server";

export async function loader({ request }: Route.LoaderArgs) {
export async function loader({ params, request }: Route.LoaderArgs) {
// const user = await getUser(request);
const url = new URL(request.url);
const start = url.searchParams.get("start");
const end = url.searchParams.get("end");
const guildId = params.guildId;

if (!start || !end) {
if (!(guildId && start && end)) {
return data(null, { status: 400 });
}

const REACTIFLUX_GUILD_ID = "102860784329052160";

const output = await getTopParticipants(
REACTIFLUX_GUILD_ID,
guildId,
start,
end,
[],
Expand All @@ -37,16 +36,16 @@ const percent = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0,
}).format;

function RangeForm() {
function RangeForm({ values }: { values: { start?: string; end?: string } }) {
return (
<form method="GET">
<Label>
Start date
<input name="start" type="date" />
<input name="start" type="date" defaultValue={values.start} />
</Label>
<Label>
End date
<input name="end" type="date" />
<input name="end" type="date" defaultValue={values.end} />
</Label>
<input type="submit" value="Submit" />
</form>
Expand All @@ -65,16 +64,20 @@ export default function DashboardPage({
loaderData: data,
}: Route.ComponentProps) {
const nav = useNavigation();
const [qs] = useSearchParams();

if (nav.state === "loading") {
return "loading…";
}

const start = qs.get("start") ?? undefined;
const end = qs.get("end") ?? undefined;

if (!data) {
return (
<div>
<div className="flex min-h-full justify-center">
<RangeForm />
<RangeForm values={{ start, end }} />
</div>
<div></div>
</div>
Expand All @@ -84,7 +87,7 @@ export default function DashboardPage({
return (
<div>
<div className="flex min-h-full justify-center">
<RangeForm />
<RangeForm values={{ start, end }} />
</div>
<div>
<textarea
Expand Down Expand Up @@ -115,7 +118,16 @@ ${data
<tbody>
{data.map((d) => (
<tr key={d.data.member.author_id}>
<td>{d.data.member.author_id}</td>
<td>
<Link
to={{
pathname: d.data.member.author_id,
search: `?start=${start}&end=${end}`,
}}
>
{d.data.member.username || d.data.member.author_id}
</Link>
</td>
<td>{percent(d.metadata.percentZeroDays)}</td>
<td>{d.data.member.total_word_count}</td>
<td>{d.data.member.message_count}</td>
Expand Down
Loading
Loading