From b41675c608ce15b11dfd884e751156a5aa32dbd4 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Thu, 27 Aug 2026 04:04:18 +0000 Subject: [PATCH] feat(authors): rebuild the page as an index and a gallery on one view toggle The two layouts are the viewMode the app already persists, so search, filters and sort carry across them. Only 19% of a library's authors have a portrait, so the gallery leans on monograms and the index treats the name as the content; a sort name now prints only when it says something the display name does not. Jump buckets, a cover fallback and two filters come from the server so the A-Z rail and the chips cover the whole result set rather than the loaded pages. Reconciles authors.has_photo against the image store on boot: the list resolves portraits from disk while the filter queried the column, so "no portrait" was over-reporting by 64. --- client/src/components/ViewHeader.vue | 43 +- .../ViewHeaderDisplayControls.spec.ts | 13 + .../view-header/ViewHeaderDisplayControls.vue | 57 +- .../__tests__/useDisplaySettingsSync.spec.ts | 2 + client/src/composables/useDisplaySettings.ts | 13 + client/src/features/author/api/author.ts | 53 +- .../features/author/components/AuthorCard.vue | 253 ------- .../author/components/AuthorFilterChips.vue | 107 +++ .../author/components/AuthorFilters.vue | 107 --- .../author/components/AuthorIndexRow.vue | 204 +++++ .../author/components/AuthorListRow.vue | 102 --- .../author/components/AuthorPortrait.vue | 135 ++++ .../features/author/components/AuthorTile.vue | 200 +++++ .../components/__tests__/AuthorTile.spec.ts | 104 +++ .../author/composables/useAuthorJumpRail.ts | 142 ++++ .../author/composables/useAuthorsList.ts | 38 +- .../lib/__tests__/author-identity.spec.ts | 92 +++ .../features/author/lib/author-identity.ts | 73 ++ client/src/features/author/types/author.ts | 8 + .../features/author/views/AuthorsView.spec.ts | 99 ++- .../src/features/author/views/AuthorsView.vue | 702 +++++++++--------- client/src/locales/cs.json | 13 +- client/src/locales/da.json | 13 +- client/src/locales/de.json | 13 +- client/src/locales/el.json | 12 +- client/src/locales/en.json | 41 +- client/src/locales/es.json | 13 +- client/src/locales/fi.json | 13 +- client/src/locales/fr.json | 13 +- client/src/locales/it.json | 13 +- client/src/locales/ja.json | 13 +- client/src/locales/ko.json | 13 +- client/src/locales/nl.json | 13 +- client/src/locales/pl.json | 13 +- client/src/locales/pt.json | 13 +- client/src/locales/ro.json | 12 +- client/src/locales/ru.json | 13 +- client/src/locales/sl.json | 13 +- client/src/locales/sv.json | 13 +- client/src/locales/uk.json | 13 +- client/src/locales/zh.json | 13 +- packages/types/src/author.ts | 12 + packages/types/src/display-preferences.ts | 3 + .../authors/author-image-storage.service.ts | 19 + .../author-photo-flag-backfill.service.ts | 48 ++ .../src/modules/authors/authors.controller.ts | 8 +- server/src/modules/authors/authors.module.ts | 2 + .../src/modules/authors/authors.repository.ts | 144 +++- .../modules/authors/authors.service.test.ts | 70 ++ server/src/modules/authors/authors.service.ts | 40 +- .../modules/authors/dto/list-authors.dto.ts | 60 ++ 51 files changed, 2131 insertions(+), 1108 deletions(-) delete mode 100644 client/src/features/author/components/AuthorCard.vue create mode 100644 client/src/features/author/components/AuthorFilterChips.vue delete mode 100644 client/src/features/author/components/AuthorFilters.vue create mode 100644 client/src/features/author/components/AuthorIndexRow.vue delete mode 100644 client/src/features/author/components/AuthorListRow.vue create mode 100644 client/src/features/author/components/AuthorPortrait.vue create mode 100644 client/src/features/author/components/AuthorTile.vue create mode 100644 client/src/features/author/components/__tests__/AuthorTile.spec.ts create mode 100644 client/src/features/author/composables/useAuthorJumpRail.ts create mode 100644 client/src/features/author/lib/__tests__/author-identity.spec.ts create mode 100644 client/src/features/author/lib/author-identity.ts create mode 100644 server/src/modules/authors/author-photo-flag-backfill.service.ts diff --git a/client/src/components/ViewHeader.vue b/client/src/components/ViewHeader.vue index 646ba35dd..bf13daffb 100644 --- a/client/src/components/ViewHeader.vue +++ b/client/src/components/ViewHeader.vue @@ -6,6 +6,7 @@ import { CheckSquare, LayoutGrid, List, SlidersHorizontal, Square, Table2 } from import { Button } from '@/components/ui/button' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' +import type { TableDensity } from '@bookorbit/types' import type { BookViewMode } from '@/composables/useDisplaySettings' import ViewHeaderDesktopSearch from '@/components/view-header/ViewHeaderDesktopSearch.vue' import ViewHeaderDisplayControls from '@/components/view-header/ViewHeaderDisplayControls.vue' @@ -23,6 +24,10 @@ withDefaults( gridGap: number showJumpRailToggle?: boolean showJumpRails?: boolean + jumpRailModes?: BookViewMode[] + rowDensity?: TableDensity + showCoverFallbackToggle?: boolean + coverFallback?: boolean viewMode: BookViewMode selectionMode?: boolean showSelection?: boolean @@ -36,9 +41,13 @@ withDefaults( gridGapStep?: number searchable?: boolean searchQuery?: string + /** Views search different things; the default placeholder describes books. */ + searchPlaceholder?: string allowedViewModes?: BookViewMode[] mobileSearchInMenu?: boolean mobileDisplayInMenu?: boolean + /** Views with no covers and no grid have nothing for the cover-size and gap sliders to do. */ + showDisplayControls?: boolean }>(), { coverSizeMin: 100, @@ -52,6 +61,7 @@ withDefaults( allowedViewModes: () => ['grid', 'list', 'table'] as BookViewMode[], mobileSearchInMenu: true, mobileDisplayInMenu: true, + showDisplayControls: true, }, ) @@ -59,6 +69,8 @@ const emit = defineEmits<{ 'update:coverSize': [value: number] 'update:gridGap': [value: number] 'update:showJumpRails': [value: boolean] + 'update:rowDensity': [value: TableDensity] + 'update:coverFallback': [value: boolean] 'update:viewMode': [value: BookViewMode] 'toggle-selection': [] 'update:coverShape': [value: 'square' | 'circle'] @@ -84,7 +96,12 @@ function handleShowJumpRailsUpdate(value: boolean) {
- + @@ -115,6 +132,8 @@ function handleShowJumpRailsUpdate(value: boolean) { size="icon" class="h-8 w-8 rounded-lg" :class="viewMode === 'grid' ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground hover:bg-primary/5'" + :aria-label="t('components.viewHeader.viewModes.grid')" + :aria-pressed="viewMode === 'grid'" @click="emit('update:viewMode', 'grid')" > @@ -125,6 +144,8 @@ function handleShowJumpRailsUpdate(value: boolean) { size="icon" class="h-8 w-8 rounded-lg" :class="viewMode === 'list' ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground hover:bg-primary/5'" + :aria-label="t('components.viewHeader.viewModes.list')" + :aria-pressed="viewMode === 'list'" @click="emit('update:viewMode', 'list')" > @@ -135,18 +156,21 @@ function handleShowJumpRailsUpdate(value: boolean) { size="icon" class="h-8 w-8 rounded-lg" :class="viewMode === 'table' ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground hover:bg-primary/5'" + :aria-label="t('components.viewHeader.viewModes.table')" + :aria-pressed="viewMode === 'table'" @click="emit('update:viewMode', 'table')" >
- + @@ -158,6 +182,10 @@ function handleShowJumpRailsUpdate(value: boolean) { :grid-gap="gridGap" :show-jump-rail-toggle="showJumpRailToggle" :show-jump-rails="showJumpRails" + :jump-rail-modes="jumpRailModes" + :row-density="rowDensity" + :show-cover-fallback-toggle="showCoverFallbackToggle" + :cover-fallback="coverFallback" :cover-shape="coverShape" :cover-size-min="coverSizeMin" :cover-size-max="coverSizeMax" @@ -168,6 +196,8 @@ function handleShowJumpRailsUpdate(value: boolean) { @update:cover-size="emit('update:coverSize', $event)" @update:grid-gap="emit('update:gridGap', $event)" @update:show-jump-rails="handleShowJumpRailsUpdate" + @update:row-density="emit('update:rowDensity', $event)" + @update:cover-fallback="emit('update:coverFallback', $event)" @update:cover-shape="emit('update:coverShape', $event)" >