Skip to content

Commit b1d3def

Browse files
refactor: move public profiles to /u/[username] (#695)
* refactor: move public profiles from /[username] to /u/[username] * fix: enable case-insensitive public username lookups --------- Co-authored-by: nicoalbanese <49612682+nicoalbanese@users.noreply.github.com>
1 parent b41d1a2 commit b1d3def

File tree

6 files changed

+137
-75
lines changed

6 files changed

+137
-75
lines changed

apps/web/app/[username]/page.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export async function generateMetadata({
7474
const dateQuery = profile.dateSelection.value
7575
? `?date=${encodeURIComponent(profile.dateSelection.value)}`
7676
: "";
77+
const publicProfilePath = `/u/${profile.user.username}`;
7778
const baseUrl = await getBaseUrl();
7879

7980
return {
@@ -83,13 +84,13 @@ export async function generateMetadata({
8384
openGraph: {
8485
title: `${displayName} · Open Agents Wrapped`,
8586
description: `${formatCompactNumber(profile.totals.totalTokens)} tokens · ${profile.dateSelection.label}`,
86-
images: [`${baseUrl}/${profile.user.username}/og${dateQuery}`],
87+
images: [`${baseUrl}${publicProfilePath}/og${dateQuery}`],
8788
},
8889
twitter: {
8990
card: "summary_large_image",
9091
title: `${displayName} · Open Agents Wrapped`,
9192
description: `${formatCompactNumber(profile.totals.totalTokens)} tokens · ${profile.dateSelection.label}`,
92-
images: [`${baseUrl}/${profile.user.username}/og${dateQuery}`],
93+
images: [`${baseUrl}${publicProfilePath}/og${dateQuery}`],
9394
},
9495
};
9596
}
@@ -108,6 +109,7 @@ export default async function PublicUsagePage({
108109
}
109110

110111
const displayName = profile.user.name?.trim() || profile.user.username;
112+
const publicProfilePath = `/u/${profile.user.username}`;
111113
const topModel = profile.topModels[0] ?? null;
112114
const topModels = profile.topModels.slice(0, 5);
113115
const maxModelTokens = topModel?.totalTokens ?? 1;
@@ -140,8 +142,8 @@ export default async function PublicUsagePage({
140142
<nav className="flex gap-0.5">
141143
{presets.map((preset) => {
142144
const href = preset.value
143-
? `/${profile.user.username}?date=${preset.value}`
144-
: `/${profile.user.username}`;
145+
? `${publicProfilePath}?date=${preset.value}`
146+
: publicProfilePath;
145147
const isActive = profile.dateSelection.value === preset.value;
146148
return (
147149
<Link
@@ -370,7 +372,7 @@ export default async function PublicUsagePage({
370372
style={{ animationDelay: "660ms" }}
371373
>
372374
<span className="font-mono text-[12px] text-white/15">
373-
open-agents.dev/{profile.user.username}
375+
open-agents.dev{publicProfilePath}
374376
{profile.dateSelection.value
375377
? `?date=${profile.dateSelection.value}`
376378
: ""}

apps/web/app/settings/preferences-section.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export function PreferencesSection() {
128128
preferences?.defaultModelId ?? getDefaultModelOptionId(modelOptions);
129129
const selectedSubagentModelId = preferences?.defaultSubagentModelId ?? "auto";
130130
const publicProfilePath = session?.user?.username
131-
? `/${session.user.username}`
131+
? `/u/${session.user.username}`
132132
: null;
133133

134134
const defaultModelOptions = useMemo(
@@ -515,7 +515,7 @@ export function PreferencesSection() {
515515
Public usage profile
516516
</Label>
517517
<p className="text-xs text-muted-foreground">
518-
Publish a shareable wrapped page at <code>/username</code>.
518+
Publish a shareable wrapped page at <code>/u/username</code>.
519519
</p>
520520
</div>
521521
<Switch
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { GET } from "../../../[username]/og/route";

apps/web/app/u/[username]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default, generateMetadata } from "../../[username]/page";

apps/web/lib/db/public-usage-profile.test.ts

Lines changed: 62 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,12 @@ type MockPublicUser = {
55
username: string;
66
name: string | null;
77
avatarUrl: string | null;
8+
lastLoginAt: Date | null;
9+
publicUsageEnabled: boolean | null;
810
};
911

10-
type MockPublicUserPreferences = {
11-
publicUsageEnabled: boolean;
12-
};
13-
14-
const findUserMock = mock(async (): Promise<MockPublicUser | null> => null);
15-
const findUserPreferencesMock = mock(
16-
async (): Promise<MockPublicUserPreferences | null> => null,
12+
const findPublicUsersByUsernameMock = mock(
13+
async (): Promise<MockPublicUser[]> => [],
1714
);
1815
const getUsageHistoryMock = mock(async () => []);
1916
const getUsageInsightsMock = mock(async () => ({
@@ -43,14 +40,15 @@ const getUsageInsightsMock = mock(async () => ({
4340

4441
mock.module("./client", () => ({
4542
db: {
46-
query: {
47-
users: {
48-
findFirst: findUserMock,
49-
},
50-
userPreferences: {
51-
findFirst: findUserPreferencesMock,
52-
},
53-
},
43+
select: () => ({
44+
from: () => ({
45+
leftJoin: () => ({
46+
where: () => ({
47+
limit: findPublicUsersByUsernameMock,
48+
}),
49+
}),
50+
}),
51+
}),
5452
},
5553
}));
5654

@@ -65,11 +63,11 @@ mock.module("./usage-insights", () => ({
6563
const publicUsageProfileModulePromise = import("./public-usage-profile");
6664

6765
beforeEach(() => {
68-
findUserMock.mockClear();
69-
findUserPreferencesMock.mockClear();
66+
findPublicUsersByUsernameMock.mockClear();
7067
getUsageHistoryMock.mockClear();
7168
getUsageInsightsMock.mockClear();
7269

70+
findPublicUsersByUsernameMock.mockImplementation(async () => []);
7371
getUsageHistoryMock.mockImplementation(async () => []);
7472
getUsageInsightsMock.mockImplementation(async () => ({
7573
lookbackDays: 0,
@@ -262,7 +260,7 @@ describe("getPublicUsageProfile", () => {
262260
test("returns null when the user does not exist", async () => {
263261
const { getPublicUsageProfile } = await publicUsageProfileModulePromise;
264262

265-
findUserMock.mockImplementation(async () => null);
263+
findPublicUsersByUsernameMock.mockImplementation(async () => []);
266264

267265
expect(await getPublicUsageProfile("missing-user", null)).toBeNull();
268266
expect(getUsageHistoryMock).not.toHaveBeenCalled();
@@ -272,15 +270,16 @@ describe("getPublicUsageProfile", () => {
272270
test("returns null when public usage is disabled", async () => {
273271
const { getPublicUsageProfile } = await publicUsageProfileModulePromise;
274272

275-
findUserMock.mockImplementation(async () => ({
276-
id: "user-1",
277-
username: "private-user",
278-
name: "Private User",
279-
avatarUrl: null,
280-
}));
281-
findUserPreferencesMock.mockImplementation(async () => ({
282-
publicUsageEnabled: false,
283-
}));
273+
findPublicUsersByUsernameMock.mockImplementation(async () => [
274+
{
275+
id: "user-1",
276+
username: "private-user",
277+
name: "Private User",
278+
avatarUrl: null,
279+
lastLoginAt: new Date("2026-01-01T00:00:00.000Z"),
280+
publicUsageEnabled: false,
281+
},
282+
]);
284283

285284
expect(await getPublicUsageProfile("private-user", null)).toBeNull();
286285
expect(getUsageHistoryMock).not.toHaveBeenCalled();
@@ -290,15 +289,16 @@ describe("getPublicUsageProfile", () => {
290289
test("uses all-time queries when no valid date is provided", async () => {
291290
const { getPublicUsageProfile } = await publicUsageProfileModulePromise;
292291

293-
findUserMock.mockImplementation(async () => ({
294-
id: "user-2",
295-
username: "all-time-user",
296-
name: null,
297-
avatarUrl: null,
298-
}));
299-
findUserPreferencesMock.mockImplementation(async () => ({
300-
publicUsageEnabled: true,
301-
}));
292+
findPublicUsersByUsernameMock.mockImplementation(async () => [
293+
{
294+
id: "user-2",
295+
username: "all-time-user",
296+
name: null,
297+
avatarUrl: null,
298+
lastLoginAt: new Date("2026-01-02T00:00:00.000Z"),
299+
publicUsageEnabled: true,
300+
},
301+
]);
302302

303303
const profile = await getPublicUsageProfile("all-time-user", "bad-value");
304304

@@ -317,24 +317,39 @@ describe("getPublicUsageProfile", () => {
317317
});
318318
});
319319

320-
test("forwards explicit date ranges to usage queries", async () => {
320+
test("prefers an enabled case-insensitive match", async () => {
321321
const { getPublicUsageProfile } = await publicUsageProfileModulePromise;
322322

323-
findUserMock.mockImplementation(async () => ({
324-
id: "user-3",
325-
username: "range-user",
326-
name: "Range User",
327-
avatarUrl: null,
328-
}));
329-
findUserPreferencesMock.mockImplementation(async () => ({
330-
publicUsageEnabled: true,
331-
}));
323+
findPublicUsersByUsernameMock.mockImplementation(async () => [
324+
{
325+
id: "user-disabled",
326+
username: "range-user",
327+
name: "Disabled User",
328+
avatarUrl: null,
329+
lastLoginAt: new Date("2026-01-01T00:00:00.000Z"),
330+
publicUsageEnabled: false,
331+
},
332+
{
333+
id: "user-3",
334+
username: "Range-User",
335+
name: "Range User",
336+
avatarUrl: null,
337+
lastLoginAt: new Date("2026-01-03T00:00:00.000Z"),
338+
publicUsageEnabled: true,
339+
},
340+
]);
332341

333342
const profile = await getPublicUsageProfile(
334-
"range-user",
343+
"RANGE-USER",
335344
"2026-01-01..2026-01-31",
336345
);
337346

347+
expect(profile?.user).toEqual({
348+
id: "user-3",
349+
username: "Range-User",
350+
name: "Range User",
351+
avatarUrl: null,
352+
});
338353
expect(profile?.dateSelection).toEqual({
339354
kind: "range",
340355
value: "2026-01-01..2026-01-31",

apps/web/lib/db/public-usage-profile.ts

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { cache } from "react";
2-
import { eq } from "drizzle-orm";
2+
import { eq, sql } from "drizzle-orm";
33
import type { UsageInsights, UsageRepositoryInsight } from "@/lib/usage/types";
44
import {
55
parsePublicUsageDate,
@@ -153,36 +153,79 @@ const ALL_TIME_DATE_SELECTION: PublicUsageDateSelection = {
153153
range: null,
154154
};
155155

156+
interface PublicUsageUserCandidate {
157+
id: string;
158+
username: string;
159+
name: string | null;
160+
avatarUrl: string | null;
161+
lastLoginAt: Date | null;
162+
publicUsageEnabled: boolean | null;
163+
}
164+
165+
function pickPublicUsageUserCandidate(
166+
candidates: PublicUsageUserCandidate[],
167+
requestedUsername: string,
168+
): PublicUsageProfile["user"] | null {
169+
const enabledCandidates = candidates.filter(
170+
(candidate) => candidate.publicUsageEnabled,
171+
);
172+
173+
if (enabledCandidates.length === 0) {
174+
return null;
175+
}
176+
177+
const selectedCandidate = enabledCandidates.toSorted((a, b) => {
178+
const exactMatchDifference =
179+
Number(b.username === requestedUsername) -
180+
Number(a.username === requestedUsername);
181+
if (exactMatchDifference !== 0) {
182+
return exactMatchDifference;
183+
}
184+
185+
const lastLoginDifference =
186+
(b.lastLoginAt?.getTime() ?? 0) - (a.lastLoginAt?.getTime() ?? 0);
187+
if (lastLoginDifference !== 0) {
188+
return lastLoginDifference;
189+
}
190+
191+
return a.id.localeCompare(b.id);
192+
})[0];
193+
194+
return selectedCandidate
195+
? {
196+
id: selectedCandidate.id,
197+
username: selectedCandidate.username,
198+
name: selectedCandidate.name,
199+
avatarUrl: selectedCandidate.avatarUrl,
200+
}
201+
: null;
202+
}
203+
156204
export const getPublicUsageProfile = cache(
157205
async (
158206
username: string,
159207
dateValue: string | null,
160208
): Promise<PublicUsageProfile | null> => {
161-
const user = await db.query.users.findFirst({
162-
where: eq(users.username, username),
163-
columns: {
164-
id: true,
165-
username: true,
166-
name: true,
167-
avatarUrl: true,
168-
},
169-
});
209+
const normalizedUsername = username.trim().toLowerCase();
210+
const userCandidates = await db
211+
.select({
212+
id: users.id,
213+
username: users.username,
214+
name: users.name,
215+
avatarUrl: users.avatarUrl,
216+
lastLoginAt: users.lastLoginAt,
217+
publicUsageEnabled: userPreferences.publicUsageEnabled,
218+
})
219+
.from(users)
220+
.leftJoin(userPreferences, eq(userPreferences.userId, users.id))
221+
.where(sql`lower(${users.username}) = ${normalizedUsername}`)
222+
.limit(10);
223+
const user = pickPublicUsageUserCandidate(userCandidates, username);
170224

171225
if (!user) {
172226
return null;
173227
}
174228

175-
const preferences = await db.query.userPreferences.findFirst({
176-
where: eq(userPreferences.userId, user.id),
177-
columns: {
178-
publicUsageEnabled: true,
179-
},
180-
});
181-
182-
if (!preferences?.publicUsageEnabled) {
183-
return null;
184-
}
185-
186229
const parsedDate = parsePublicUsageDate(dateValue);
187230
const dateSelection = parsedDate.ok
188231
? parsedDate.selection

0 commit comments

Comments
 (0)