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:
@@ -102,9 +102,13 @@ export function userPath(
|
|||||||
: `${base}/${tab}`;
|
: `${base}/${tab}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the path of a dump's page, `/@owner/dumps/<slug>`. */
|
/**
|
||||||
export function dumpPath(dump: OwnedItem): string {
|
* Returns the path of a dump's page, `/@owner/dumps/<slug>`, or of its
|
||||||
return `${userPath(dump.ownerUsername)}/dumps/${seg(dump.slug)}`;
|
* editor, one segment more.
|
||||||
|
*/
|
||||||
|
export function dumpPath(dump: OwnedItem, view?: "edit"): string {
|
||||||
|
const path = `${userPath(dump.ownerUsername)}/dumps/${seg(dump.slug)}`;
|
||||||
|
return view ? `${path}/${view}` : path;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the path of a playlist's page, `/@owner/playlists/<slug>`. */
|
/** Returns the path of a playlist's page, `/@owner/playlists/<slug>`. */
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ Deno.test("builders: one hierarchy, default views bare", () => {
|
|||||||
const item = { ownerUsername: "bob", slug: "never-gonna" };
|
const item = { ownerUsername: "bob", slug: "never-gonna" };
|
||||||
assertEquals(dumpPath(item), "/@bob/dumps/never-gonna");
|
assertEquals(dumpPath(item), "/@bob/dumps/never-gonna");
|
||||||
assertEquals(playlistPath(item), "/@bob/playlists/never-gonna");
|
assertEquals(playlistPath(item), "/@bob/playlists/never-gonna");
|
||||||
|
assertEquals(
|
||||||
|
dumpPath({ ownerUsername: "bob", slug: "a-b" }, "edit"),
|
||||||
|
"/@bob/dumps/a-b/edit",
|
||||||
|
);
|
||||||
assertEquals(feedPath(), "/");
|
assertEquals(feedPath(), "/");
|
||||||
assertEquals(feedPath(undefined, "hot"), "/hot");
|
assertEquals(feedPath(undefined, "hot"), "/hot");
|
||||||
assertEquals(feedPath("music"), "/music");
|
assertEquals(feedPath("music"), "/music");
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { Trans } from "@lingui/react/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 { Wire } from "../../../shared/api.ts";
|
||||||
import type { Playlist } from "../../../shared/playlist.ts";
|
import type { Playlist } from "../../../shared/playlist.ts";
|
||||||
import type { PublicUser } from "../../../shared/user.ts";
|
import type { PublicUser } from "../../../shared/user.ts";
|
||||||
@@ -45,7 +45,7 @@ function useFollowList<T>(
|
|||||||
view: FollowView,
|
view: FollowView,
|
||||||
deserialize: (wire: Wire<T>) => T,
|
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>(
|
const paged = usePagedList<T>(
|
||||||
url,
|
url,
|
||||||
useCallback(
|
useCallback(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Trans } from "@lingui/react/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 { Wire } from "../../../shared/api.ts";
|
||||||
import type { PublicUser } from "../../../shared/user.ts";
|
import type { PublicUser } from "../../../shared/user.ts";
|
||||||
import { expectOk } from "../../utils/shared/api.ts";
|
import { expectOk } from "../../utils/shared/api.ts";
|
||||||
@@ -42,7 +42,7 @@ export function ProfileManageTab(
|
|||||||
async function rename(username: string): Promise<string> {
|
async function rename(username: string): Promise<string> {
|
||||||
const renamed = deserializePublicUser(
|
const renamed = deserializePublicUser(
|
||||||
await expectOk<Wire<PublicUser>>(
|
await expectOk<Wire<PublicUser>>(
|
||||||
await authFetch(`${API_URL}/api/users/${user.id}/username`, {
|
await authFetch(userApiUrl(user, "username"), {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ username }),
|
body: JSON.stringify({ username }),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useSearchParams } from "react-router";
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { API_URL } from "../../config/api.ts";
|
import { API_URL } from "../../config/api.ts";
|
||||||
|
import { userApiUrl } from "../../utils/shared/urls.ts";
|
||||||
import type { Playlist } from "../../../shared/playlist.ts";
|
import type { Playlist } from "../../../shared/playlist.ts";
|
||||||
import type { PublicUser } from "../../../shared/user.ts";
|
import type { PublicUser } from "../../../shared/user.ts";
|
||||||
import {
|
import {
|
||||||
@@ -93,7 +94,7 @@ function PlaylistList(
|
|||||||
}`,
|
}`,
|
||||||
hydratePlaylist,
|
hydratePlaylist,
|
||||||
);
|
);
|
||||||
const url = `${API_URL}/api/users/${user.id}/playlists${
|
const url = `${userApiUrl(user, "playlists")}${
|
||||||
categorySlug ? `?category=${encodeURIComponent(categorySlug)}` : ""
|
categorySlug ? `?category=${encodeURIComponent(categorySlug)}` : ""
|
||||||
}`;
|
}`;
|
||||||
const { state, setItems: setAllItems, loadMore } = usePagedList<Playlist>(
|
const { state, setItems: setAllItems, loadMore } = usePagedList<Playlist>(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Trans } from "@lingui/react/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 { Wire } from "../../../shared/api.ts";
|
||||||
import type { PublicUser, Role } from "../../../shared/user.ts";
|
import type { PublicUser, Role } from "../../../shared/user.ts";
|
||||||
import { expectOk, friendlyFetchError } from "../../utils/shared/api.ts";
|
import { expectOk, friendlyFetchError } from "../../utils/shared/api.ts";
|
||||||
@@ -65,7 +65,7 @@ export function RoleSwitcher(
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`${API_URL}/api/users/${userId}/role`, {
|
const res = await authFetch(userApiUrl({ id: userId }, "role"), {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ role }),
|
body: JSON.stringify({ role }),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useContext, useState } from "react";
|
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";
|
import { AvatarOverrideContext } from "../../contexts/ws/AvatarOverrideContext.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,8 +51,8 @@ export function Avatar(
|
|||||||
|
|
||||||
if (effectiveHasAvatar && !imgFailed) {
|
if (effectiveHasAvatar && !imgFailed) {
|
||||||
const src = effectiveVersion
|
const src = effectiveVersion
|
||||||
? `${API_URL}/api/users/${userId}/avatar?v=${effectiveVersion}`
|
? `${userApiUrl({ id: userId }, "avatar")}?v=${effectiveVersion}`
|
||||||
: `${API_URL}/api/users/${userId}/avatar`;
|
: userApiUrl({ id: userId }, "avatar");
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={src}
|
src={src}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { API_URL } from "../../config/api.ts";
|
import { userApiUrl } from "../../utils/shared/urls.ts";
|
||||||
import { pageUrl, readPage } from "../../utils/feed/pagedList.ts";
|
import { pageUrl, readPage } from "../../utils/feed/pagedList.ts";
|
||||||
import type { Dump } from "../../../shared/dump.ts";
|
import type { Dump } from "../../../shared/dump.ts";
|
||||||
import {
|
import {
|
||||||
@@ -71,7 +71,7 @@ export function useUserDumpFeed(
|
|||||||
hydrateDump,
|
hydrateDump,
|
||||||
);
|
);
|
||||||
|
|
||||||
const url = `${API_URL}/api/users/${userId}/${list}${
|
const url = `${userApiUrl({ id: userId }, list)}${
|
||||||
categorySlug ? `?category=${encodeURIComponent(categorySlug)}` : ""
|
categorySlug ? `?category=${encodeURIComponent(categorySlug)}` : ""
|
||||||
}`;
|
}`;
|
||||||
const paged = usePagedList<Dump>(
|
const paged = usePagedList<Dump>(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
deserializePlaylistWithDumps,
|
deserializePlaylistWithDumps,
|
||||||
} from "../../utils/shared/deserialize.ts";
|
} from "../../utils/shared/deserialize.ts";
|
||||||
import { expectOk, friendlyFetchError } from "../../utils/shared/api.ts";
|
import { expectOk, friendlyFetchError } from "../../utils/shared/api.ts";
|
||||||
import { isAt, playlistUrl } from "../../utils/shared/urls.ts";
|
import { isAt, playlistUrl, userApiUrl } from "../../utils/shared/urls.ts";
|
||||||
import { useCanonicalPath } from "../shared/useCanonicalPath.ts";
|
import { useCanonicalPath } from "../shared/useCanonicalPath.ts";
|
||||||
import { useAuth } from "../auth/useAuth.ts";
|
import { useAuth } from "../auth/useAuth.ts";
|
||||||
import { useWS } from "../ws/useWS.ts";
|
import { useWS } from "../ws/useWS.ts";
|
||||||
@@ -124,7 +124,8 @@ export function usePlaylist(
|
|||||||
`${target.username}/${target.slug}:${user?.id ?? "guest"}`,
|
`${target.username}/${target.slug}:${user?.id ?? "guest"}`,
|
||||||
(signal) =>
|
(signal) =>
|
||||||
authFetch(
|
authFetch(
|
||||||
`${API_URL}/api/users/@${target.username}/playlists/${target.slug}`,
|
// The key above is falsy, and nothing fetched, without both.
|
||||||
|
userApiUrl({ username: target.username! }, "playlists", target.slug!),
|
||||||
{ signal },
|
{ signal },
|
||||||
)
|
)
|
||||||
.then(expectOk<Wire<PlaylistWithDumps>>)
|
.then(expectOk<Wire<PlaylistWithDumps>>)
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { Link, useLocation, useNavigate, useParams } from "react-router";
|
import { Link, useLocation, useNavigate, useParams } from "react-router";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { dumpThumbnailUrl, dumpUrl, isAt } from "../utils/shared/urls.ts";
|
import {
|
||||||
|
dumpThumbnailUrl,
|
||||||
|
dumpUrl,
|
||||||
|
isAt,
|
||||||
|
userApiUrl,
|
||||||
|
} from "../utils/shared/urls.ts";
|
||||||
import { can } from "../utils/auth/permissions.ts";
|
import { can } from "../utils/auth/permissions.ts";
|
||||||
import { CategoryChips } from "../components/category/CategoryChips.tsx";
|
import { CategoryChips } from "../components/category/CategoryChips.tsx";
|
||||||
import { AddToPlaylistModal } from "../components/playlist/AddToPlaylistModal.tsx";
|
import { AddToPlaylistModal } from "../components/playlist/AddToPlaylistModal.tsx";
|
||||||
@@ -132,7 +137,7 @@ export function Dump(): React.JSX.Element {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
if (preloaded) {
|
if (preloaded) {
|
||||||
fetch(`${API_URL}/api/users/${preloaded.userId}`, {
|
fetch(userApiUrl({ id: preloaded.userId }), {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
@@ -148,7 +153,7 @@ export function Dump(): React.JSX.Element {
|
|||||||
setOp(null);
|
setOp(null);
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(
|
const res = await authFetch(
|
||||||
`${API_URL}/api/users/@${username}/dumps/${slug}`,
|
userApiUrl({ username }, "dumps", slug),
|
||||||
{ cache: "no-store", signal: controller.signal },
|
{ cache: "no-store", signal: controller.signal },
|
||||||
);
|
);
|
||||||
const apiResponse = parseAPIResponse<Wire<Dump>>(await res.json());
|
const apiResponse = parseAPIResponse<Wire<Dump>>(await res.json());
|
||||||
@@ -158,7 +163,7 @@ export function Dump(): React.JSX.Element {
|
|||||||
const dump: Dump = deserializeDump(apiResponse.data);
|
const dump: Dump = deserializeDump(apiResponse.data);
|
||||||
setDumpState({ status: "loaded", dump });
|
setDumpState({ status: "loaded", dump });
|
||||||
|
|
||||||
fetch(`${API_URL}/api/users/${dump.userId}`, {
|
fetch(userApiUrl({ id: dump.userId }), {
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
@@ -544,7 +549,7 @@ export function Dump(): React.JSX.Element {
|
|||||||
|
|
||||||
<div className="dump-actions">
|
<div className="dump-actions">
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Link to={`${dumpUrl(dump)}/edit`}>
|
<Link to={dumpUrl(dump, "edit")}>
|
||||||
<Trans>Edit</Trans>
|
<Trans>Edit</Trans>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
dumpFileUrl,
|
dumpFileUrl,
|
||||||
dumpThumbnailUrl,
|
dumpThumbnailUrl,
|
||||||
dumpUrl,
|
dumpUrl,
|
||||||
|
userApiUrl,
|
||||||
} from "../utils/shared/urls.ts";
|
} from "../utils/shared/urls.ts";
|
||||||
import { PageShell } from "../components/layout/PageShell.tsx";
|
import { PageShell } from "../components/layout/PageShell.tsx";
|
||||||
import { PageError } from "../components/layout/PageError.tsx";
|
import { PageError } from "../components/layout/PageError.tsx";
|
||||||
@@ -92,7 +93,7 @@ export function DumpEdit(): React.JSX.Element {
|
|||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
|
|
||||||
useCanonicalPath(
|
useCanonicalPath(
|
||||||
state.status === "loaded" ? `${dumpUrl(state.dump)}/edit` : null,
|
state.status === "loaded" ? dumpUrl(state.dump, "edit") : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -102,7 +103,7 @@ export function DumpEdit(): React.JSX.Element {
|
|||||||
setState({ status: "loading" });
|
setState({ status: "loading" });
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${API_URL}/api/users/@${username}/dumps/${slug}`,
|
userApiUrl({ username }, "dumps", slug),
|
||||||
{
|
{
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { useLocation, useNavigate, useParams } from "react-router";
|
import { useLocation, useNavigate, useParams } from "react-router";
|
||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
|
|
||||||
import { API_URL } from "../config/api.ts";
|
import { userApiUrl } from "../utils/shared/urls.ts";
|
||||||
import type { Invitee } from "../../shared/user.ts";
|
import type { Invitee } from "../../shared/user.ts";
|
||||||
import type { Wire } from "../../shared/api.ts";
|
import type { Wire } from "../../shared/api.ts";
|
||||||
import {
|
import {
|
||||||
@@ -91,7 +91,7 @@ export function UserPublicProfile(): React.JSX.Element {
|
|||||||
const invitees = useFetchState<Invitee[]>(
|
const invitees = useFetchState<Invitee[]>(
|
||||||
user?.id,
|
user?.id,
|
||||||
(signal) =>
|
(signal) =>
|
||||||
fetch(`${API_URL}/api/users/${user!.id}/invitees`, { signal })
|
fetch(userApiUrl(user!, "invitees"), { signal })
|
||||||
.then((res) => dataOr<Wire<Invitee>[]>(res, []))
|
.then((res) => dataOr<Wire<Invitee>[]>(res, []))
|
||||||
.then((entries) => entries.map(deserializeInvitee)),
|
.then((entries) => entries.map(deserializeInvitee)),
|
||||||
{ enabled: tab === "invitees" },
|
{ enabled: tab === "invitees" },
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import { API_URL } from "../../config/api.ts";
|
import { API_URL } from "../../config/api.ts";
|
||||||
import { splitHandle } from "../../../shared/validation.ts";
|
import { splitHandle } from "../../../shared/validation.ts";
|
||||||
|
import { userApiUrl } from "../shared/urls.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether `host`, as a handle writes it, is this instance's: its API's,
|
* Checks whether `host`, as a handle writes it, is this instance's: its API's,
|
||||||
@@ -38,5 +39,5 @@ export function homeHost(handle: string): string | undefined {
|
|||||||
export function homeUrl(handle: string, path?: string): string | null {
|
export function homeUrl(handle: string, path?: string): string | null {
|
||||||
if (!homeHost(handle)) return null;
|
if (!homeHost(handle)) return null;
|
||||||
const query = path ? `?path=${encodeURIComponent(path)}` : "";
|
const query = path ? `?path=${encodeURIComponent(path)}` : "";
|
||||||
return `${API_URL}/api/users/@${encodeURIComponent(handle)}/home${query}`;
|
return `${userApiUrl({ username: handle }, "home")}${query}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { API_URL } from "../../config/api.ts";
|
|
||||||
import type { Wire } from "../../../shared/api.ts";
|
import type { Wire } from "../../../shared/api.ts";
|
||||||
import type { PublicUser } from "../../../shared/user.ts";
|
import type { PublicUser } from "../../../shared/user.ts";
|
||||||
import { expectOk } from "./api.ts";
|
import { expectOk } from "./api.ts";
|
||||||
import { deserializePublicUser } from "./deserialize.ts";
|
import { deserializePublicUser } from "./deserialize.ts";
|
||||||
|
import { userApiUrl } from "./urls.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns `GET /api/users/@:username`, for the profile, which fails as a
|
* Returns `GET /api/users/@:username`, for the profile, which fails as a
|
||||||
@@ -24,10 +24,7 @@ export async function fetchPublicUser(
|
|||||||
username: string,
|
username: string,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
): Promise<PublicUser> {
|
): Promise<PublicUser> {
|
||||||
const res = await fetch(
|
const res = await fetch(userApiUrl({ username }), { signal });
|
||||||
`${API_URL}/api/users/@${encodeURIComponent(username)}`,
|
|
||||||
{ signal },
|
|
||||||
);
|
|
||||||
if (res.status === 404) throw new Error("User not found");
|
if (res.status === 404) throw new Error("User not found");
|
||||||
return deserializePublicUser(await expectOk<Wire<PublicUser>>(res));
|
return deserializePublicUser(await expectOk<Wire<PublicUser>>(res));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,11 +98,12 @@ export function normalizeUrl(input: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the path of the page of `dump`, `/@owner/dumps/<slug>`. Its owner's
|
* Returns the path of the page of `dump`, `/@owner/dumps/<slug>`, or of its
|
||||||
* name and its slug come with every dump the API sends.
|
* editor (`dumpPath`). Its owner's name and its slug come with every dump the
|
||||||
|
* API sends.
|
||||||
*/
|
*/
|
||||||
export function dumpUrl(dump: OwnedItem): string {
|
export function dumpUrl(dump: OwnedItem, view?: "edit"): string {
|
||||||
return dumpPath(dump);
|
return dumpPath(dump, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,6 +120,24 @@ export function isAt(
|
|||||||
item.ownerUsername.toLowerCase() === username?.toLowerCase();
|
item.ownerUsername.toLowerCase() === username?.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URL of `/api/users/:key`, or of what is under it, `segments`
|
||||||
|
* such as `"dumps", slug`, each encoded: every request about a user takes it
|
||||||
|
* from here. The key is `user`'s id, or, when the caller has only the name a
|
||||||
|
* page's path gives, `@` and that handle, which the API resolves as the page
|
||||||
|
* would, a former name or a remote user's included.
|
||||||
|
*/
|
||||||
|
export function userApiUrl(
|
||||||
|
user: { id: string } | { username: string },
|
||||||
|
...segments: string[]
|
||||||
|
): string {
|
||||||
|
const key = "id" in user
|
||||||
|
? encodeURIComponent(user.id)
|
||||||
|
: `@${encodeURIComponent(user.username)}`;
|
||||||
|
return [`${API_URL}/api/users`, key, ...segments.map(encodeURIComponent)]
|
||||||
|
.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the path of the page of `playlist`, `/@owner/playlists/<slug>`. */
|
/** Returns the path of the page of `playlist`, `/@owner/playlists/<slug>`. */
|
||||||
export function playlistUrl(playlist: OwnedItem): string {
|
export function playlistUrl(playlist: OwnedItem): string {
|
||||||
return playlistPath(playlist);
|
return playlistPath(playlist);
|
||||||
|
|||||||
Reference in New Issue
Block a user