Files
Mangalord/frontend/src/routes/profile/+layout.svelte
MechaCat02 5130c9933e feat(profile): add a desktop Page-tags section
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>
2026-06-25 21:27:58 +02:00

146 lines
4.6 KiB
Svelte

<script lang="ts">
import { page } from '$app/stores';
import { session } from '$lib/session.svelte';
import User from '@lucide/svelte/icons/user';
import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
import KeyRound from '@lucide/svelte/icons/key-round';
import Bookmark from '@lucide/svelte/icons/bookmark';
import FolderOpen from '@lucide/svelte/icons/folder-open';
import Tag from '@lucide/svelte/icons/tag';
import History from '@lucide/svelte/icons/history';
let { children } = $props();
type Tab = {
href: string;
label: string;
icon: typeof User;
testid: string;
/** Subroutes that genuinely have nothing for guests are hidden
* until the user signs in. Preferences is shown to everyone
* because theme + reader settings work device-locally. */
guestVisible: boolean;
};
const tabs: Tab[] = [
{ href: '/profile', label: 'Overview', icon: User, testid: 'tab-overview', guestVisible: true },
{ href: '/profile/preferences', label: 'Preferences', icon: SlidersHorizontal, testid: 'tab-preferences', guestVisible: true },
{ 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/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 }
];
const visibleTabs = $derived(
tabs.filter((t) => session.user || t.guestVisible)
);
</script>
<header class="profile-header">
<h1>Profile</h1>
{#if !session.loaded}
<p class="welcome" data-testid="profile-loading">Loading session…</p>
{:else if session.user}
<p class="welcome">
Signed in as <strong>{session.user.username}</strong>
</p>
{:else}
<p class="welcome" data-testid="profile-guest">
Browsing as a guest — sign in to access bookmarks, collections,
and account settings.
</p>
{/if}
</header>
<nav class="tabs" aria-label="Profile sections">
{#each visibleTabs as t (t.href)}
{@const active =
t.href === '/profile'
? $page.url.pathname === '/profile'
: $page.url.pathname === t.href ||
$page.url.pathname.startsWith(t.href + '/')}
<a
class="tab"
class:active
href={t.href}
aria-current={active ? 'page' : undefined}
data-testid={t.testid}
>
<t.icon size={16} aria-hidden="true" />
<span>{t.label}</span>
</a>
{/each}
</nav>
<section class="content">
{@render children()}
</section>
<style>
.profile-header {
margin-bottom: var(--space-3);
}
.profile-header h1 {
margin: 0 0 var(--space-1);
}
.welcome {
color: var(--text-muted);
margin: 0;
font-size: var(--font-sm);
}
.tabs {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
margin-bottom: var(--space-4);
border-bottom: 1px solid var(--border);
}
/* Mobile uses the bottom-nav Account/Library tabs plus the account
hub for cross-section navigation. The horizontal tab strip would
compete with the bottom nav and wrap awkwardly at narrow widths,
so hide it below the breakpoint and let pages stand on their own. */
@media (max-width: 640px) {
.tabs {
display: none;
}
.profile-header {
margin-bottom: var(--space-2);
}
}
.tab {
display: inline-flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
color: var(--text-muted);
font-size: var(--font-sm);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition:
color var(--transition),
border-color var(--transition);
}
.tab:hover {
color: var(--text);
text-decoration: none;
}
.tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
font-weight: var(--weight-medium);
}
.content {
margin-top: var(--space-3);
}
</style>