diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 69f5403..522c4cf 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1033,7 +1033,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mangalord" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "argon2", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 5800d70..4145176 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mangalord" -version = "0.17.0" +version = "0.18.0" edition = "2021" [lib] diff --git a/frontend/e2e/settings.spec.ts b/frontend/e2e/profile.spec.ts similarity index 63% rename from frontend/e2e/settings.spec.ts rename to frontend/e2e/profile.spec.ts index c08a5c1..35c48e9 100644 --- a/frontend/e2e/settings.spec.ts +++ b/frontend/e2e/profile.spec.ts @@ -14,9 +14,25 @@ async function stubAuthenticated(page: Page) { body: JSON.stringify({ user: userFixture }) }) ); + // Profile overview hits these for the count cards — return zeros + // unless a test overrides. + await page.route('**/api/v1/me/bookmarks?*', (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [], page: { limit: 1, offset: 0, total: 0 } }) + }) + ); + await page.route('**/api/v1/me/collections?*', (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [], page: { limit: 1, offset: 0, total: 0 } }) + }) + ); } -test('settings link shows for authed users and reaches the password form', async ({ page }) => { +test('Profile link in nav for authed users; landing shows counts', async ({ page }) => { await stubAuthenticated(page); await page.route('**/api/v1/mangas?*', (route) => route.fulfill({ @@ -27,9 +43,18 @@ test('settings link shows for authed users and reaches the password form', async ); await page.goto('/'); - await expect(page.getByTestId('nav-settings')).toBeVisible(); - await page.getByTestId('nav-settings').click(); - await expect(page).toHaveURL(/\/settings$/); + await expect(page.getByTestId('nav-profile')).toBeVisible(); + await page.getByTestId('nav-profile').click(); + await expect(page).toHaveURL(/\/profile$/); + await expect(page.getByTestId('overview-bookmarks')).toBeVisible(); + await expect(page.getByTestId('overview-collections')).toBeVisible(); +}); + +test('Account tab reaches the password form', async ({ page }) => { + await stubAuthenticated(page); + await page.goto('/profile'); + await page.getByTestId('tab-account').click(); + await expect(page).toHaveURL(/\/profile\/account$/); await expect(page.getByTestId('password-form')).toBeVisible(); }); @@ -43,7 +68,7 @@ test('changing password shows success and clears the form', async ({ page }) => await route.fulfill({ status: 204 }); }); - await page.goto('/settings'); + await page.goto('/profile/account'); await page.getByTestId('current-password').fill('hunter2hunter2'); await page.getByTestId('new-password').fill('freshpassfreshpass'); await page.getByTestId('confirm-password').fill('freshpassfreshpass'); @@ -55,7 +80,6 @@ test('changing password shows success and clears the form', async ({ page }) => current_password: 'hunter2hunter2', new_password: 'freshpassfreshpass' }); - // Form should clear after success. await expect(page.getByTestId('current-password')).toHaveValue(''); }); @@ -71,7 +95,7 @@ test('wrong current password surfaces the 401 envelope inline', async ({ page }) }) ); - await page.goto('/settings'); + await page.goto('/profile/account'); await page.getByTestId('current-password').fill('definitelyNotIt'); await page.getByTestId('new-password').fill('freshpassfreshpass'); await page.getByTestId('confirm-password').fill('freshpassfreshpass'); @@ -83,7 +107,7 @@ test('wrong current password surfaces the 401 envelope inline', async ({ page }) test('mismatched new + confirm disables the submit button', async ({ page }) => { await stubAuthenticated(page); - await page.goto('/settings'); + await page.goto('/profile/account'); await page.getByTestId('current-password').fill('hunter2hunter2'); await page.getByTestId('new-password').fill('freshpassfreshpass'); await page.getByTestId('confirm-password').fill('different'); @@ -92,7 +116,7 @@ test('mismatched new + confirm disables the submit button', async ({ page }) => await expect(page.getByTestId('password-submit')).toBeDisabled(); }); -test('anonymous user sees a sign-in prompt on /settings', async ({ page }) => { +test('anonymous user sees a profile sign-in prompt', async ({ page }) => { await page.route('**/api/v1/auth/me', (route) => route.fulfill({ status: 401, @@ -103,7 +127,16 @@ test('anonymous user sees a sign-in prompt on /settings', async ({ page }) => { }) ); - await page.goto('/settings'); - await expect(page.getByTestId('settings-signin')).toBeVisible(); + await page.goto('/profile'); + await expect(page.getByTestId('profile-signin')).toBeVisible(); await expect(page.getByTestId('password-form')).toHaveCount(0); }); + +test('/settings 308-redirects to /profile/preferences', async ({ page }) => { + await stubAuthenticated(page); + await page.goto('/settings'); + await expect(page).toHaveURL(/\/profile\/preferences$/); + // The theme radio is visually hidden (decorated label wraps it), so + // assert presence rather than CSS visibility. + await expect(page.getByTestId('theme-radio-system')).toBeAttached(); +}); diff --git a/frontend/e2e/reader-mode.spec.ts b/frontend/e2e/reader-mode.spec.ts index 7ca85ef..e9e1d91 100644 --- a/frontend/e2e/reader-mode.spec.ts +++ b/frontend/e2e/reader-mode.spec.ts @@ -201,13 +201,13 @@ test('reader-mode preference set on one page is honored when the reader opens', ); }); -test('settings page hides the gap picker while in single-page mode', async ({ page }) => { +test('preferences page hides the gap picker while in single-page mode', async ({ page }) => { // Visually verifies the conditional render. The radio-click semantics // are exercised in src/lib/preferences.svelte.test.ts; the visible // mode toggle in the reader top bar covers the cross-route propagation // path in the test above. await mockReaderApis(page); - await page.goto('/settings'); + await page.goto('/profile/preferences'); await expect(page.getByTestId('reader-mode-radio-single')).toBeAttached(); await expect(page.getByTestId('reader-mode-radio-continuous')).toBeAttached(); diff --git a/frontend/package.json b/frontend/package.json index aaacc52..9c49db5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mangalord-frontend", - "version": "0.17.0", + "version": "0.18.0", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/lib/components/BookmarkList.svelte b/frontend/src/lib/components/BookmarkList.svelte new file mode 100644 index 0000000..7f6cdea --- /dev/null +++ b/frontend/src/lib/components/BookmarkList.svelte @@ -0,0 +1,129 @@ + + + + + diff --git a/frontend/src/lib/components/CollectionsGrid.svelte b/frontend/src/lib/components/CollectionsGrid.svelte new file mode 100644 index 0000000..149faa2 --- /dev/null +++ b/frontend/src/lib/components/CollectionsGrid.svelte @@ -0,0 +1,132 @@ + + + + + diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 3ccd209..06966a8 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -6,9 +6,9 @@ import { session } from '$lib/session.svelte'; import { theme } from '$lib/theme.svelte'; import Upload from '@lucide/svelte/icons/upload'; + import UserCircle from '@lucide/svelte/icons/user-circle'; import Bookmark from '@lucide/svelte/icons/bookmark'; import FolderOpen from '@lucide/svelte/icons/folder-open'; - import Settings from '@lucide/svelte/icons/settings'; import LogOut from '@lucide/svelte/icons/log-out'; import '$lib/styles/tokens.css'; @@ -53,6 +53,10 @@