v4: every request about a user, and the dump editor's path, come from one builder each — fourteen places wrote /api/users/<key>/… by hand, some encoding a handle (@alice%40music.example) and some not, a slug never; userApiUrl (src/utils/shared/urls.ts) now writes them all, by the user's id or by @ and the handle a page's path gives, each segment encoded. The editor's path was the dump's page's with /edit added in two places; dumpPath takes the view, as userPath and feedPath do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-09 22:18:13 +00:00
parent 669e65a3a2
commit 21f4db268e
15 changed files with 69 additions and 36 deletions

View File

@@ -8,7 +8,7 @@
import { useCallback } from "react";
import { useNavigate } from "react-router";
import { Trans } from "@lingui/react/macro";
import { API_URL } from "../../config/api.ts";
import { userApiUrl } from "../../utils/shared/urls.ts";
import type { Wire } from "../../../shared/api.ts";
import type { Playlist } from "../../../shared/playlist.ts";
import type { PublicUser } from "../../../shared/user.ts";
@@ -45,7 +45,7 @@ function useFollowList<T>(
view: FollowView,
deserialize: (wire: Wire<T>) => T,
) {
const url = `${API_URL}/api/users/${userId}/follow/${view}`;
const url = userApiUrl({ id: userId }, "follow", view);
const paged = usePagedList<T>(
url,
useCallback(

View File

@@ -7,7 +7,7 @@
*/
import { Trans } from "@lingui/react/macro";
import { API_URL } from "../../config/api.ts";
import { userApiUrl } from "../../utils/shared/urls.ts";
import type { Wire } from "../../../shared/api.ts";
import type { PublicUser } from "../../../shared/user.ts";
import { expectOk } from "../../utils/shared/api.ts";
@@ -42,7 +42,7 @@ export function ProfileManageTab(
async function rename(username: string): Promise<string> {
const renamed = deserializePublicUser(
await expectOk<Wire<PublicUser>>(
await authFetch(`${API_URL}/api/users/${user.id}/username`, {
await authFetch(userApiUrl(user, "username"), {
method: "PATCH",
body: JSON.stringify({ username }),
}),

View File

@@ -10,6 +10,7 @@ import { useSearchParams } from "react-router";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { API_URL } from "../../config/api.ts";
import { userApiUrl } from "../../utils/shared/urls.ts";
import type { Playlist } from "../../../shared/playlist.ts";
import type { PublicUser } from "../../../shared/user.ts";
import {
@@ -93,7 +94,7 @@ function PlaylistList(
}`,
hydratePlaylist,
);
const url = `${API_URL}/api/users/${user.id}/playlists${
const url = `${userApiUrl(user, "playlists")}${
categorySlug ? `?category=${encodeURIComponent(categorySlug)}` : ""
}`;
const { state, setItems: setAllItems, loadMore } = usePagedList<Playlist>(

View File

@@ -8,7 +8,7 @@
import { useState } from "react";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { API_URL } from "../../config/api.ts";
import { userApiUrl } from "../../utils/shared/urls.ts";
import type { Wire } from "../../../shared/api.ts";
import type { PublicUser, Role } from "../../../shared/user.ts";
import { expectOk, friendlyFetchError } from "../../utils/shared/api.ts";
@@ -65,7 +65,7 @@ export function RoleSwitcher(
setSaving(true);
setError(null);
try {
const res = await authFetch(`${API_URL}/api/users/${userId}/role`, {
const res = await authFetch(userApiUrl({ id: userId }, "role"), {
method: "PATCH",
body: JSON.stringify({ role }),
});

View File

@@ -6,7 +6,7 @@
*/
import { useContext, useState } from "react";
import { API_URL } from "../../config/api.ts";
import { userApiUrl } from "../../utils/shared/urls.ts";
import { AvatarOverrideContext } from "../../contexts/ws/AvatarOverrideContext.ts";
/**
@@ -51,8 +51,8 @@ export function Avatar(
if (effectiveHasAvatar && !imgFailed) {
const src = effectiveVersion
? `${API_URL}/api/users/${userId}/avatar?v=${effectiveVersion}`
: `${API_URL}/api/users/${userId}/avatar`;
? `${userApiUrl({ id: userId }, "avatar")}?v=${effectiveVersion}`
: userApiUrl({ id: userId }, "avatar");
return (
<img
src={src}