feat(profile): add a desktop Page-tags section #22
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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 }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
31
frontend/src/routes/profile/page-tags/+page.svelte
Normal file
31
frontend/src/routes/profile/page-tags/+page.svelte
Normal 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>
|
||||||
32
frontend/src/routes/profile/page-tags/+page.ts
Normal file
32
frontend/src/routes/profile/page-tags/+page.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user