feat(profile): add a desktop Page-tags section
All checks were successful
deploy / test-backend (pull_request) Successful in 27m40s
deploy / test-frontend (pull_request) Successful in 10m22s
deploy / build-and-push (pull_request) Has been skipped
deploy / deploy (pull_request) Has been skipped

Page tags were a first-class section of the mobile Library tab but had no
home in the desktop /profile tabs, so desktop users couldn't browse their
tagged pages without going through /search. Add a /profile/page-tags route
that reuses PageTagsList (the same component the mobile Library renders) and
a "Page tags" tab between Collections and History.

The loader mirrors the library wrapper's page-tags fetch; 401 → sign-in
prompt, matching the other profile sub-routes.

e2e: the desktop Page-tags tab navigates to /profile/page-tags and renders
the list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-25 20:31:07 +02:00
parent dee53fa212
commit 18e6b501e0
7 changed files with 95 additions and 3 deletions

2
backend/Cargo.lock generated
View File

@@ -1517,7 +1517,7 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]] [[package]]
name = "mangalord" name = "mangalord"
version = "0.88.0" version = "0.89.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "mangalord" name = "mangalord"
version = "0.88.0" version = "0.89.0"
edition = "2021" edition = "2021"
default-run = "mangalord" default-run = "mangalord"

View File

@@ -50,6 +50,33 @@ test('Profile link in nav for authed users; landing shows counts', async ({ page
await expect(page.getByTestId('overview-collections')).toBeVisible(); await expect(page.getByTestId('overview-collections')).toBeVisible();
}); });
test('Page tags tab reaches the page-tags list on desktop', async ({ page }) => {
// Page tags are a first-class Library section on mobile; desktop reaches
// them through this profile tab.
await stubAuthenticated(page);
await page.route('**/api/v1/me/page-tags?*', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [], page: { limit: 100, offset: 0, total: 0 } })
})
);
// Registered after the general route so distinct URLs resolve here.
await page.route('**/api/v1/me/page-tags/distinct*', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [] })
})
);
await page.goto('/profile');
await expect(page.getByTestId('tab-page-tags')).toBeVisible();
await page.getByTestId('tab-page-tags').click();
await expect(page).toHaveURL(/\/profile\/page-tags$/);
await expect(page.getByTestId('library-page-tags-empty')).toBeVisible();
});
test('Account tab reaches the password form', async ({ page }) => { test('Account tab reaches the password form', async ({ page }) => {
await stubAuthenticated(page); await stubAuthenticated(page);
await page.goto('/profile'); await page.goto('/profile');

View File

@@ -1,6 +1,6 @@
{ {
"name": "mangalord-frontend", "name": "mangalord-frontend",
"version": "0.88.0", "version": "0.89.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@@ -6,6 +6,7 @@
import KeyRound from '@lucide/svelte/icons/key-round'; import KeyRound from '@lucide/svelte/icons/key-round';
import Bookmark from '@lucide/svelte/icons/bookmark'; import Bookmark from '@lucide/svelte/icons/bookmark';
import FolderOpen from '@lucide/svelte/icons/folder-open'; import FolderOpen from '@lucide/svelte/icons/folder-open';
import Tag from '@lucide/svelte/icons/tag';
import History from '@lucide/svelte/icons/history'; import History from '@lucide/svelte/icons/history';
let { children } = $props(); let { children } = $props();
@@ -27,6 +28,7 @@
{ href: '/profile/account', label: 'Account', icon: KeyRound, testid: 'tab-account', guestVisible: false }, { href: '/profile/account', label: 'Account', icon: KeyRound, testid: 'tab-account', guestVisible: false },
{ href: '/profile/bookmarks', label: 'Bookmarks', icon: Bookmark, testid: 'tab-bookmarks', guestVisible: false }, { href: '/profile/bookmarks', label: 'Bookmarks', icon: Bookmark, testid: 'tab-bookmarks', guestVisible: false },
{ href: '/profile/collections', label: 'Collections', icon: FolderOpen, testid: 'tab-collections', guestVisible: false }, { href: '/profile/collections', label: 'Collections', icon: FolderOpen, testid: 'tab-collections', guestVisible: false },
{ href: '/profile/page-tags', label: 'Page tags', icon: Tag, testid: 'tab-page-tags', guestVisible: false },
{ href: '/profile/history', label: 'History', icon: History, testid: 'tab-history', guestVisible: false } { href: '/profile/history', label: 'History', icon: History, testid: 'tab-history', guestVisible: false }
]; ];

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import PageTagsList from '$lib/components/PageTagsList.svelte';
let { data } = $props();
</script>
<svelte:head>
<title>Mangalord | Page tags</title>
</svelte:head>
{#if data.error}
<p class="error" role="alert" data-testid="profile-page-tags-error">
Couldn't load page tags: {data.error}
</p>
{:else if !data.authenticated}
<p class="hint" data-testid="profile-page-tags-signin">
<a href="/login?next=/profile/page-tags">Sign in</a> to see your page tags.
</p>
{:else}
<PageTagsList initialItems={data.pageTags} initialDistinct={data.distinctPageTags} />
{/if}
<style>
.hint {
color: var(--text-muted);
}
.error {
color: var(--danger);
}
</style>

View File

@@ -0,0 +1,32 @@
import { ApiError } from '$lib/api/client';
import { listMyPageTags, listMyDistinctPageTags } from '$lib/api/page_tags';
import type { PageLoad } from './$types';
export const ssr = false;
// Desktop home for the user's page tags — the parity counterpart to the
// mobile Library "Page tags" tab. Loads the same data the library wrapper
// feeds PageTagsList. 401 → unauthenticated path; the page shows a sign-in
// prompt.
export const load: PageLoad = async () => {
try {
const [pageTags, distinctPageTags] = await Promise.all([
listMyPageTags({ limit: 100 }),
listMyDistinctPageTags(undefined, 100)
]);
return {
authenticated: true,
pageTags: pageTags.items,
distinctPageTags,
error: null as string | null
};
} catch (e) {
if (e instanceof ApiError && e.status === 401) {
return { authenticated: false, pageTags: [], distinctPageTags: [], error: null };
}
if (e instanceof ApiError) {
return { authenticated: true, pageTags: [], distinctPageTags: [], error: e.message };
}
throw e;
}
};