From 3fe2cfad3b3457daaa2a3eb6916475da5a0f4de7 Mon Sep 17 00:00:00 2001
From: Timothy Atapagra
Date: Tue, 28 Oct 2025 20:18:13 -0400
Subject: [PATCH] feat(api-nest, web-vite): enhance library resolver and UI
components
- Updated the LibraryResolver to include a default value for the `includeContent` argument, improving performance by conditionally stripping content from search results.
- Refactored the LabelPickerModal component to improve documentation clarity.
- Enhanced the LibraryItemCard component by adding site name metadata and updating icons for better visual representation.
- Improved button accessibility and styling across LibraryPage and ReaderPage components, ensuring consistent button types and focus styles.
- Updated CSS styles for various components to utilize CSS variables for better maintainability and responsiveness.
These changes collectively enhance the user experience and maintainability of the codebase.
---
.../api-nest/src/library/library.resolver.ts | 23 +-
packages/api-nest/src/main.ts | 13 +-
.../src/components/LabelPickerModal.tsx | 16 +-
.../src/components/LibraryItemCard.tsx | 36 +-
packages/web-vite/src/pages/LibraryPage.tsx | 43 +-
packages/web-vite/src/pages/ReaderPage.tsx | 18 +-
.../web-vite/src/styles/LeftNavigation.css | 38 ++
packages/web-vite/src/styles/LibraryCard.css | 6 +-
packages/web-vite/src/styles/LibraryGrid.css | 12 +-
packages/web-vite/src/styles/LibraryPage.css | 399 +++++++++---------
10 files changed, 347 insertions(+), 257 deletions(-)
diff --git a/packages/api-nest/src/library/library.resolver.ts b/packages/api-nest/src/library/library.resolver.ts
index 4f58e1c05..12ce73a35 100644
--- a/packages/api-nest/src/library/library.resolver.ts
+++ b/packages/api-nest/src/library/library.resolver.ts
@@ -92,7 +92,8 @@ export class LibraryResolver {
first = 20,
@Args('after', { type: () => String, nullable: true }) after?: string,
@Args('query', { type: () => String, nullable: true }) query?: string,
- @Args('includeContent', { type: () => Boolean, nullable: true }) includeContent?: boolean,
+ @Args('includeContent', { type: () => Boolean, nullable: true, defaultValue: false })
+ includeContent = false,
): Promise {
try {
// Convert query string to search input format
@@ -108,16 +109,26 @@ export class LibraryResolver {
)
// Transform to legacy format with edges and pageInfo
- const edges: SearchItemEdge[] = items.map((item, index) => ({
- cursor: nextCursor && index === items.length - 1 ? nextCursor : item.id,
- node: mapEntityToGraph(item),
- }))
+ // Each edge should have cursor = item.id, not nextCursor
+ const edges: SearchItemEdge[] = items.map((item) => {
+ const graphItem = mapEntityToGraph(item)
+
+ // Strip content if includeContent is false for better performance
+ if (!includeContent && graphItem.content) {
+ graphItem.content = null
+ }
+
+ return {
+ cursor: item.id, // Each edge cursor should be the item's ID
+ node: graphItem,
+ }
+ })
const pageInfo: SearchPageInfo = {
hasNextPage: !!nextCursor,
hasPreviousPage: !!after,
startCursor: items.length > 0 ? items[0].id : null,
- endCursor: nextCursor,
+ endCursor: items.length > 0 ? items[items.length - 1].id : null, // Last item's ID, not nextCursor
totalCount: null, // Not currently tracked
}
diff --git a/packages/api-nest/src/main.ts b/packages/api-nest/src/main.ts
index bb28118d8..909753a42 100644
--- a/packages/api-nest/src/main.ts
+++ b/packages/api-nest/src/main.ts
@@ -50,7 +50,18 @@ async function bootstrap() {
],
})
- app.useGlobalPipes(new ValidationPipe())
+ app.useGlobalPipes(
+ new ValidationPipe({
+ transform: true, // Automatically transform payloads to DTO instances
+ whitelist: true, // Strip properties that don't have decorators
+ forbidNonWhitelisted: false, // Allow non-whitelisted properties (for GraphQL flexibility)
+ skipUndefinedProperties: false, // Validate undefined properties
+ skipNullProperties: true, // Skip validation for null properties (fixes GraphQL null handling)
+ transformOptions: {
+ enableImplicitConversion: true, // Convert primitive types automatically
+ },
+ }),
+ )
await app.listen(port, () => {
Logger.log(`App is listening on port ${port}`)
diff --git a/packages/web-vite/src/components/LabelPickerModal.tsx b/packages/web-vite/src/components/LabelPickerModal.tsx
index d966baa09..58823655e 100644
--- a/packages/web-vite/src/components/LabelPickerModal.tsx
+++ b/packages/web-vite/src/components/LabelPickerModal.tsx
@@ -9,15 +9,6 @@ interface LabelPickerModalProps {
onClose: () => void
}
-/**
- * LabelPickerModal
- * @param props.itemId - Target library item ID.
- * @param props.currentLabels - Current label names applied.
- * @param props.onUpdate - Called with the updated label name list.
- * @param props.onClose - Close handler.
- */
-export function LabelPickerModal({ itemId, currentLabels, onUpdate, onClose }: LabelPickerModalProps) {
-
// Preset colors matching legacy implementation
const PRESET_COLORS = [
{ name: 'Red', value: '#FF5D99' },
@@ -28,6 +19,13 @@ const PRESET_COLORS = [
{ name: 'Purple', value: '#CE88EF' },
]
+/**
+ * LabelPickerModal
+ * @param props.itemId - Target library item ID.
+ * @param props.currentLabels - Current label names applied.
+ * @param props.onUpdate - Called with the updated label name list.
+ * @param props.onClose - Close handler.
+ */
export function LabelPickerModal({ itemId, currentLabels, onUpdate, onClose }: LabelPickerModalProps) {
const { data: allLabels, loading: loadingLabels, fetchLabels } = useLabels()
const { setLibraryItemLabels, loading: updating } = useSetLibraryItemLabels()
diff --git a/packages/web-vite/src/components/LibraryItemCard.tsx b/packages/web-vite/src/components/LibraryItemCard.tsx
index 1498487cb..ffbff5925 100644
--- a/packages/web-vite/src/components/LibraryItemCard.tsx
+++ b/packages/web-vite/src/components/LibraryItemCard.tsx
@@ -281,19 +281,37 @@ const LibraryItemCard: React.FC = ({
)}
- {/* Metadata bar - Author, Reading time, Saved date */}
+ {/* Metadata bar - Site name, Author, Reading time, Saved date */}
- {/* Author name */}
- {item.author && (
-
{item.author}
- )}
-
- {/* Reading time with clock icon */}
- {readingTime && (
+ {/* Site name/source with globe icon */}
+ {item.siteName && (
+
{item.siteName}
+
+ )}
+
+ {/* Author name with user icon */}
+ {item.author && (
+
+
+
{item.author}
+
+ )}
+
+ {/* Reading time with book-open icon */}
+ {readingTime && (
+
diff --git a/packages/web-vite/src/pages/LibraryPage.tsx b/packages/web-vite/src/pages/LibraryPage.tsx
index 207f32563..9f47e343b 100644
--- a/packages/web-vite/src/pages/LibraryPage.tsx
+++ b/packages/web-vite/src/pages/LibraryPage.tsx
@@ -787,7 +787,8 @@ const LibraryPage: React.FC = () => {
{searching &&
⏳}