v4: the header has two playlists links, each to its own page — it had three: the text button and the avatar menu's "Playlists" both led to the user's own playlists, and the menu's "All playlists" to /playlists. The text button now leads to /playlists, every public playlist, and shows to guests too, the page being open to them. The menu keeps one entry, "My playlists", to the user's own. Below 1150px, where the text button is hidden, the header no longer links to /playlists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-10 08:29:08 +00:00
parent 28f910b00e
commit 7dd00068df
8 changed files with 131 additions and 142 deletions

View File

@@ -17,7 +17,7 @@ import { UserMenu } from "./UserMenu.tsx";
import { SearchBar } from "./SearchBar.tsx";
import { IconClose, IconSearch } from "./NavIcons.tsx";
import { SITE_EMOJI, SITE_NAME } from "../../hooks/shared/useDocumentTitle.ts";
import { searchPath, userPath } from "../../../shared/paths.ts";
import { playlistsPath, searchPath } from "../../../shared/paths.ts";
/** Loaded on first open, so its editor stays out of every page's bundle. */
const DumpCreateModal = lazy(() =>
@@ -62,9 +62,10 @@ export interface AppHeaderProps {
/**
* Renders the site's header. It holds the search, which stays open on the
* results page and slides open under the header on small screens, the
* signed-in user's links and buttons, and a banner while live updates are down.
* It also hosts the dump creation dialog that its "+ Dump" button opens.
* results page and slides open under the header on small screens, the link to
* every playlist, the signed-in user's menu and buttons or a guest's "Log in",
* and a banner while live updates are down. It also hosts the dump creation
* dialog that its "+ Dump" button opens.
*/
export function AppHeader(
{ centerSlot, disableNew, initialDumpUrl }: AppHeaderProps,
@@ -158,23 +159,17 @@ export function AppHeader(
<nav className="app-header-nav">
{
/* The user's own links lead the row, so the icons after them run
unbroken to the "+": Playlists on wide screens, then the
avatar's menu, which leads to their profile, at every width. */
/* The links lead the row, so the icons after them run unbroken to
the "+": every playlist on wide screens, for guests too, then
the avatar's menu, which leads to the user's profile and their
own playlists, at every width. */
}
{user && (
<>
<div className="nav-links">
<Link
to={userPath(user.username, "playlists")}
className="app-header-user"
>
<Trans>Playlists</Trans>
</Link>
</div>
<UserMenu user={user} />
</>
)}
<div className="nav-links">
<Link to={playlistsPath()} className="app-header-user">
<Trans>Playlists</Trans>
</Link>
</div>
{user && <UserMenu user={user} />}
<button
ref={searchToggleRef}
type="button"

View File

@@ -1,7 +1,8 @@
/**
* Renders the avatar menu that leads the header's icon row at every width: it
* holds the profile link, the playlists one (on small screens, the only one),
* the community page, the settings, the site's tools, and logging out.
* holds the profile link, the user's own playlists (the header's text link
* leads to everyone's), the community page, the settings, the site's tools, and
* logging out.
*
* @module
*/
@@ -15,11 +16,7 @@ import { DIGEST_MESSAGES } from "../../../shared/digest-messages.ts";
import { Avatar } from "../shared/Avatar.tsx";
import type { SelfUser } from "../../../shared/user.ts";
import { can } from "../../utils/auth/permissions.ts";
import {
communityPath,
playlistsPath,
userPath,
} from "../../../shared/paths.ts";
import { communityPath, userPath } from "../../../shared/paths.ts";
import { useAuth } from "../../hooks/auth/useAuth.ts";
/** Passed to {@link UserMenu} by `AppHeader`, for a signed-in user. */
@@ -89,7 +86,7 @@ export function UserMenu({ user }: UserMenuProps): React.JSX.Element {
role="menuitem"
onClick={() => setOpen(false)}
>
<Trans>Playlists</Trans>
<Trans>My playlists</Trans>
</Link>
<Link
to={communityPath()}
@@ -99,14 +96,6 @@ export function UserMenu({ user }: UserMenuProps): React.JSX.Element {
>
{i18n._(DIGEST_MESSAGES.community)}
</Link>
<Link
to={playlistsPath()}
className="user-menu-item"
role="menuitem"
onClick={() => setOpen(false)}
>
<Trans>All playlists</Trans>
</Link>
<Link
to="/settings"
className="user-menu-item"

View File

@@ -13,9 +13,13 @@ import { makeSession } from "../../fixtures.ts";
import { currentPath, renderWithContexts } from "../../render.tsx";
describe("AppHeader", () => {
test("guests get a login button", async () => {
test("guests get the playlists link and a login button", async () => {
const user = userEvent.setup();
renderWithContexts(<AppHeader />);
expect(screen.getByRole("link", { name: "Playlists" })).toHaveAttribute(
"href",
"/playlists",
);
await user.click(screen.getByRole("button", { name: "Log in" }));
expect(currentPath()).toBe("/login");
});
@@ -26,7 +30,7 @@ describe("AppHeader", () => {
});
expect(screen.getByRole("link", { name: "Playlists" })).toHaveAttribute(
"href",
"/@ann/playlists",
"/playlists",
);
expect(screen.getByRole("button", { name: "User menu" }))
.toBeInTheDocument();

View File

@@ -22,10 +22,13 @@ describe("UserMenu", () => {
"href",
"/@ann",
);
expect(screen.getByRole("menuitem", { name: "Playlists" })).toHaveAttribute(
"href",
"/@ann/playlists",
);
// One playlists link, the user's own: the header's leads to everyone's.
expect(screen.getAllByRole("menuitem", { name: /playlists/i }))
.toHaveLength(
1,
);
expect(screen.getByRole("menuitem", { name: "My playlists" }))
.toHaveAttribute("href", "/@ann/playlists");
expect(screen.getByRole("menuitem", { name: "Community" })).toHaveAttribute(
"href",
"/community",