@internetarchive/collection-browser 1.14.17 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/index.d.ts +4 -1
  2. package/dist/index.js +4 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/src/app-root.d.ts +0 -2
  5. package/dist/src/app-root.js +0 -8
  6. package/dist/src/app-root.js.map +1 -1
  7. package/dist/src/collection-browser.d.ts +49 -200
  8. package/dist/src/collection-browser.js +191 -762
  9. package/dist/src/collection-browser.js.map +1 -1
  10. package/dist/src/collection-facets/facet-row.d.ts +2 -2
  11. package/dist/src/collection-facets/facet-row.js +5 -10
  12. package/dist/src/collection-facets/facet-row.js.map +1 -1
  13. package/dist/src/collection-facets/facets-template.d.ts +2 -2
  14. package/dist/src/collection-facets/facets-template.js +2 -2
  15. package/dist/src/collection-facets/facets-template.js.map +1 -1
  16. package/dist/src/collection-facets/more-facets-content.d.ts +2 -9
  17. package/dist/src/collection-facets/more-facets-content.js +13 -18
  18. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  19. package/dist/src/collection-facets.d.ts +2 -3
  20. package/dist/src/collection-facets.js +12 -13
  21. package/dist/src/collection-facets.js.map +1 -1
  22. package/dist/src/data-source/collection-browser-data-source-interface.d.ts +217 -0
  23. package/dist/src/data-source/collection-browser-data-source-interface.js +2 -0
  24. package/dist/src/data-source/collection-browser-data-source-interface.js.map +1 -0
  25. package/dist/src/data-source/collection-browser-data-source.d.ts +352 -0
  26. package/dist/src/data-source/collection-browser-data-source.js +912 -0
  27. package/dist/src/data-source/collection-browser-data-source.js.map +1 -0
  28. package/dist/src/data-source/collection-browser-query-state.d.ts +43 -0
  29. package/dist/src/data-source/collection-browser-query-state.js +2 -0
  30. package/dist/src/data-source/collection-browser-query-state.js.map +1 -0
  31. package/dist/src/data-source/models.d.ts +28 -0
  32. package/dist/src/data-source/models.js +9 -0
  33. package/dist/src/data-source/models.js.map +1 -0
  34. package/dist/src/manage/manage-bar.d.ts +1 -1
  35. package/dist/src/manage/manage-bar.js.map +1 -1
  36. package/dist/src/models.d.ts +21 -4
  37. package/dist/src/models.js +111 -0
  38. package/dist/src/models.js.map +1 -1
  39. package/dist/src/sort-filter-bar/sort-filter-bar.js +26 -23
  40. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  41. package/dist/src/tiles/grid/item-tile.d.ts +1 -0
  42. package/dist/src/tiles/grid/item-tile.js +28 -1
  43. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  44. package/dist/src/tiles/grid/tile-stats.js +13 -8
  45. package/dist/src/tiles/grid/tile-stats.js.map +1 -1
  46. package/dist/src/tiles/hover/hover-pane-controller.d.ts +2 -2
  47. package/dist/src/tiles/hover/hover-pane-controller.js +1 -1
  48. package/dist/src/tiles/hover/hover-pane-controller.js.map +1 -1
  49. package/dist/src/tiles/hover/tile-hover-pane.d.ts +2 -2
  50. package/dist/src/tiles/hover/tile-hover-pane.js +2 -2
  51. package/dist/src/tiles/hover/tile-hover-pane.js.map +1 -1
  52. package/dist/src/tiles/item-image.d.ts +3 -0
  53. package/dist/src/tiles/item-image.js +30 -2
  54. package/dist/src/tiles/item-image.js.map +1 -1
  55. package/dist/src/tiles/list/tile-list.d.ts +4 -3
  56. package/dist/src/tiles/list/tile-list.js +39 -14
  57. package/dist/src/tiles/list/tile-list.js.map +1 -1
  58. package/dist/src/tiles/tile-dispatcher.d.ts +2 -2
  59. package/dist/src/tiles/tile-dispatcher.js +5 -5
  60. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  61. package/dist/src/tiles/tile-display-value-provider.d.ts +6 -2
  62. package/dist/src/tiles/tile-display-value-provider.js +15 -1
  63. package/dist/src/tiles/tile-display-value-provider.js.map +1 -1
  64. package/dist/src/utils/collapse-repeated-quotes.d.ts +11 -0
  65. package/dist/src/utils/collapse-repeated-quotes.js +14 -0
  66. package/dist/src/utils/collapse-repeated-quotes.js.map +1 -0
  67. package/dist/src/utils/log.d.ts +7 -0
  68. package/dist/src/utils/log.js +16 -0
  69. package/dist/src/utils/log.js.map +1 -0
  70. package/dist/src/utils/resolve-mediatype.d.ts +8 -0
  71. package/dist/src/utils/resolve-mediatype.js +24 -0
  72. package/dist/src/utils/resolve-mediatype.js.map +1 -0
  73. package/dist/test/collection-browser.test.js +142 -92
  74. package/dist/test/collection-browser.test.js.map +1 -1
  75. package/dist/test/collection-facets/facet-row.test.js +6 -17
  76. package/dist/test/collection-facets/facet-row.test.js.map +1 -1
  77. package/dist/test/collection-facets/more-facets-content.test.js +2 -2
  78. package/dist/test/collection-facets/more-facets-content.test.js.map +1 -1
  79. package/dist/test/collection-facets.test.js +5 -5
  80. package/dist/test/collection-facets.test.js.map +1 -1
  81. package/dist/test/data-source/collection-browser-data-source.test.d.ts +1 -0
  82. package/dist/test/data-source/collection-browser-data-source.test.js +80 -0
  83. package/dist/test/data-source/collection-browser-data-source.test.js.map +1 -0
  84. package/dist/test/item-image.test.js +33 -34
  85. package/dist/test/item-image.test.js.map +1 -1
  86. package/dist/test/mocks/mock-search-responses.d.ts +3 -0
  87. package/dist/test/mocks/mock-search-responses.js +131 -0
  88. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  89. package/dist/test/mocks/mock-search-service.js +4 -1
  90. package/dist/test/mocks/mock-search-service.js.map +1 -1
  91. package/dist/test/sort-filter-bar/sort-filter-bar.test.js +41 -4
  92. package/dist/test/sort-filter-bar/sort-filter-bar.test.js.map +1 -1
  93. package/dist/test/tile-stats.test.js +62 -0
  94. package/dist/test/tile-stats.test.js.map +1 -1
  95. package/dist/test/tiles/grid/item-tile.test.js +44 -0
  96. package/dist/test/tiles/grid/item-tile.test.js.map +1 -1
  97. package/dist/test/tiles/hover/hover-pane-controller.test.js +18 -17
  98. package/dist/test/tiles/hover/hover-pane-controller.test.js.map +1 -1
  99. package/dist/test/tiles/list/tile-list.test.js +44 -5
  100. package/dist/test/tiles/list/tile-list.test.js.map +1 -1
  101. package/dist/test/tiles/tile-dispatcher.test.js +12 -0
  102. package/dist/test/tiles/tile-dispatcher.test.js.map +1 -1
  103. package/index.ts +4 -1
  104. package/package.json +3 -4
  105. package/src/app-root.ts +0 -10
  106. package/src/collection-browser.ts +219 -935
  107. package/src/collection-facets/facet-row.ts +4 -9
  108. package/src/collection-facets/facets-template.ts +3 -3
  109. package/src/collection-facets/more-facets-content.ts +15 -20
  110. package/src/collection-facets.ts +11 -14
  111. package/src/data-source/collection-browser-data-source-interface.ts +272 -0
  112. package/src/data-source/collection-browser-data-source.ts +1166 -0
  113. package/src/data-source/collection-browser-query-state.ts +54 -0
  114. package/src/data-source/models.ts +38 -0
  115. package/src/manage/manage-bar.ts +1 -1
  116. package/src/models.ts +164 -3
  117. package/src/sort-filter-bar/sort-filter-bar.ts +26 -23
  118. package/src/tiles/grid/item-tile.ts +36 -1
  119. package/src/tiles/grid/tile-stats.ts +12 -7
  120. package/src/tiles/hover/hover-pane-controller.ts +3 -3
  121. package/src/tiles/hover/tile-hover-pane.ts +3 -3
  122. package/src/tiles/item-image.ts +28 -0
  123. package/src/tiles/list/tile-list.ts +56 -23
  124. package/src/tiles/tile-dispatcher.ts +5 -5
  125. package/src/tiles/tile-display-value-provider.ts +20 -2
  126. package/src/utils/collapse-repeated-quotes.ts +13 -0
  127. package/src/utils/log.ts +16 -0
  128. package/src/utils/resolve-mediatype.ts +26 -0
  129. package/test/collection-browser.test.ts +213 -106
  130. package/test/collection-facets/facet-row.test.ts +5 -18
  131. package/test/collection-facets/more-facets-content.test.ts +4 -2
  132. package/test/collection-facets.test.ts +5 -5
  133. package/test/data-source/collection-browser-data-source.test.ts +103 -0
  134. package/test/item-image.test.ts +34 -36
  135. package/test/mocks/mock-search-responses.ts +144 -0
  136. package/test/mocks/mock-search-service.ts +6 -0
  137. package/test/sort-filter-bar/sort-filter-bar.test.ts +50 -4
  138. package/test/tile-stats.test.ts +104 -0
  139. package/test/tiles/grid/item-tile.test.ts +55 -0
  140. package/test/tiles/hover/hover-pane-controller.test.ts +19 -17
  141. package/test/tiles/list/tile-list.test.ts +55 -5
  142. package/test/tiles/tile-dispatcher.test.ts +13 -0
  143. package/dist/test/mocks/mock-collection-name-cache.d.ts +0 -9
  144. package/dist/test/mocks/mock-collection-name-cache.js +0 -18
  145. package/dist/test/mocks/mock-collection-name-cache.js.map +0 -1
  146. package/test/mocks/mock-collection-name-cache.ts +0 -24
@@ -0,0 +1,54 @@
1
+ import type {
2
+ CollectionExtraInfo,
3
+ PageElementName,
4
+ SearchServiceInterface,
5
+ SearchType,
6
+ SortDirection,
7
+ SortParam,
8
+ } from '@internetarchive/search-service';
9
+ import type { SelectedFacets, SortField } from '../models';
10
+ import type { CollectionBrowserDataSourceInterface } from './collection-browser-data-source-interface';
11
+
12
+ /**
13
+ * Properties of collection browser that affect the overall search query
14
+ */
15
+ export interface CollectionBrowserQueryState {
16
+ baseQuery?: string;
17
+ withinCollection?: string;
18
+ withinProfile?: string;
19
+ profileElement?: PageElementName;
20
+ searchType: SearchType;
21
+ selectedFacets?: SelectedFacets;
22
+ minSelectedDate?: string;
23
+ maxSelectedDate?: string;
24
+ selectedTitleFilter: string | null;
25
+ selectedCreatorFilter: string | null;
26
+ selectedSort?: SortField;
27
+ sortDirection: SortDirection | null;
28
+ }
29
+
30
+ /**
31
+ * Interface representing search-related state and operations required by the
32
+ * data source on its host component.
33
+ */
34
+ export interface CollectionBrowserSearchInterface
35
+ extends CollectionBrowserQueryState {
36
+ searchService?: SearchServiceInterface;
37
+ readonly sortParam: SortParam | null;
38
+ readonly defaultSortParam: SortParam | null;
39
+ readonly suppressFacets?: boolean;
40
+ readonly initialPageNumber: number;
41
+ readonly currentVisiblePageNumbers: number[];
42
+ readonly clearResultsOnEmptyQuery?: boolean;
43
+ readonly dataSource?: CollectionBrowserDataSourceInterface;
44
+
45
+ getSessionId(): Promise<string>;
46
+ setSearchResultsLoading(loading: boolean): void;
47
+ setFacetsLoading(loading: boolean): void;
48
+ setTotalResultCount(count: number): void;
49
+ setTileCount(count: number): void;
50
+ applyDefaultCollectionSort(collectionInfo?: CollectionExtraInfo): void;
51
+ emitEmptyResults(): void;
52
+ emitQueryStateChanged(): void;
53
+ refreshVisibleResults(): void;
54
+ }
@@ -0,0 +1,38 @@
1
+ import type {
2
+ PageElementName,
3
+ PageType,
4
+ } from '@internetarchive/search-service';
5
+
6
+ /**
7
+ * A Map from collection identifiers to their corresponding collection titles.
8
+ */
9
+ export type CollectionTitles = Map<string, string>;
10
+
11
+ /**
12
+ * The subset of search service params that uniquely specify the type of results
13
+ * that are sought by an instance of collection browser.
14
+ */
15
+ export type PageSpecifierParams = {
16
+ /**
17
+ * What high-level type of page is being fetched for (search results, collection, or profile)
18
+ */
19
+ pageType: PageType;
20
+ /**
21
+ * The target identifier for collection or profile pages (e.g., "prelinger", "@brewster", ...)
22
+ */
23
+ pageTarget: string;
24
+ /**
25
+ * Which specific elements of a profile page to fetch. Corresponds to individual tab data
26
+ * (e.g., "uploads", "reviews", ...)
27
+ */
28
+ pageElements?: PageElementName[];
29
+ };
30
+
31
+ /**
32
+ * List of profile page elements that do not currently allow faceting
33
+ */
34
+ export const FACETLESS_PAGE_ELEMENTS: PageElementName[] = [
35
+ 'forum_posts',
36
+ 'lending',
37
+ 'web_archives',
38
+ ];
@@ -6,7 +6,7 @@ import { when } from 'lit/directives/when.js';
6
6
  export interface ManageableItem {
7
7
  identifier: string;
8
8
  title?: string;
9
- date?: string;
9
+ dateStr?: string;
10
10
  }
11
11
 
12
12
  @customElement('manage-bar')
package/src/models.ts CHANGED
@@ -1,42 +1,193 @@
1
1
  import type { MediaType } from '@internetarchive/field-parsers';
2
2
  import {
3
3
  AggregationSortType,
4
+ SearchResult,
4
5
  SortDirection,
5
6
  } from '@internetarchive/search-service';
7
+ import { collapseRepeatedQuotes } from './utils/collapse-repeated-quotes';
8
+ import { resolveMediatype } from './utils/resolve-mediatype';
6
9
 
7
- export interface TileModel {
10
+ /**
11
+ * Flags that can affect the visibility of content on a tile
12
+ */
13
+ interface TileFlags {
14
+ loginRequired: boolean;
15
+ contentWarning: boolean;
16
+ }
17
+
18
+ /**
19
+ * Class for converting & storing raw search results in the correct format for UI tiles.
20
+ */
21
+ export class TileModel {
8
22
  averageRating?: number;
23
+
24
+ captureDates?: Date[]; // List of capture dates for a URL, used on profile Web Archives tiles
25
+
9
26
  checked: boolean; // Whether this tile is currently checked for item management functions
27
+
10
28
  collectionIdentifier?: string;
29
+
11
30
  collectionName?: string;
31
+
12
32
  collectionFilesCount: number;
33
+
13
34
  collections: string[];
35
+
14
36
  collectionSize: number;
37
+
15
38
  commentCount: number;
39
+
16
40
  creator?: string;
41
+
17
42
  creators: string[];
43
+
44
+ dateStr?: string; // A string representation of the publication date, used strictly for passing preformatted dates to the parent
45
+
18
46
  dateAdded?: Date; // Date added to public search (software-defined) [from: addeddate]
47
+
19
48
  dateArchived?: Date; // Date archived (software-defined) item created on archive.org [from: publicdate]
49
+
20
50
  datePublished?: Date; // Date work published in the world (user-defined) [from: date]
51
+
21
52
  dateReviewed?: Date; // Date reviewed (user-created) most recent review [from: reviewdate]
53
+
22
54
  description?: string;
55
+
23
56
  favCount: number;
57
+
24
58
  href?: string;
25
- identifier: string;
59
+
60
+ identifier?: string;
61
+
26
62
  issue?: string;
63
+
27
64
  itemCount: number;
65
+
28
66
  mediatype: MediaType;
67
+
29
68
  source?: string;
69
+
30
70
  snippets?: string[];
71
+
31
72
  subjects: string[];
73
+
32
74
  title: string;
33
- viewCount: number;
75
+
76
+ viewCount?: number;
77
+
34
78
  volume?: string;
79
+
35
80
  weeklyViewCount?: number;
81
+
36
82
  loginRequired: boolean;
83
+
37
84
  contentWarning: boolean;
85
+
86
+ constructor(result: SearchResult) {
87
+ const flags = this.getFlags(result);
88
+
89
+ this.averageRating = result.avg_rating?.value;
90
+ this.captureDates = result.capture_dates?.values;
91
+ this.checked = false;
92
+ this.collections = result.collection?.values ?? [];
93
+ this.collectionFilesCount = result.collection_files_count?.value ?? 0;
94
+ this.collectionSize = result.collection_size?.value ?? 0;
95
+ this.commentCount = result.num_reviews?.value ?? 0;
96
+ this.creator = result.creator?.value;
97
+ this.creators = result.creator?.values ?? [];
98
+ this.dateAdded = result.addeddate?.value;
99
+ this.dateArchived = result.publicdate?.value;
100
+ this.datePublished = result.date?.value;
101
+ this.dateReviewed = result.reviewdate?.value;
102
+ this.description = result.description?.values.join('\n');
103
+ this.favCount = result.num_favorites?.value ?? 0;
104
+ this.href = collapseRepeatedQuotes(result.__href__?.value);
105
+ this.identifier = result.identifier;
106
+ this.issue = result.issue?.value;
107
+ this.itemCount = result.item_count?.value ?? 0;
108
+ this.mediatype = resolveMediatype(result);
109
+ this.snippets = result.highlight?.values ?? [];
110
+ this.source = result.source?.value;
111
+ this.subjects = result.subject?.values ?? [];
112
+ this.title = result.title?.value ?? '';
113
+ this.volume = result.volume?.value;
114
+ this.viewCount = result.downloads?.value;
115
+ this.weeklyViewCount = result.week?.value;
116
+ this.loginRequired = flags.loginRequired;
117
+ this.contentWarning = flags.contentWarning;
118
+ }
119
+
120
+ /**
121
+ * Copies the contents of this TileModel onto a new instance
122
+ */
123
+ clone(): TileModel {
124
+ const cloned = new TileModel({});
125
+ cloned.averageRating = this.averageRating;
126
+ cloned.captureDates = this.captureDates;
127
+ cloned.checked = this.checked;
128
+ cloned.collections = this.collections;
129
+ cloned.collectionFilesCount = this.collectionFilesCount;
130
+ cloned.collectionSize = this.collectionSize;
131
+ cloned.commentCount = this.commentCount;
132
+ cloned.creator = this.creator;
133
+ cloned.creators = this.creators;
134
+ cloned.dateStr = this.dateStr;
135
+ cloned.dateAdded = this.dateAdded;
136
+ cloned.dateArchived = this.dateArchived;
137
+ cloned.datePublished = this.datePublished;
138
+ cloned.dateReviewed = this.dateReviewed;
139
+ cloned.description = this.description;
140
+ cloned.favCount = this.favCount;
141
+ cloned.href = this.href;
142
+ cloned.identifier = this.identifier;
143
+ cloned.issue = this.issue;
144
+ cloned.itemCount = this.itemCount;
145
+ cloned.mediatype = this.mediatype;
146
+ cloned.snippets = this.snippets;
147
+ cloned.source = this.source;
148
+ cloned.subjects = this.subjects;
149
+ cloned.title = this.title;
150
+ cloned.volume = this.volume;
151
+ cloned.viewCount = this.viewCount;
152
+ cloned.weeklyViewCount = this.weeklyViewCount;
153
+ cloned.loginRequired = this.loginRequired;
154
+ cloned.contentWarning = this.contentWarning;
155
+ return cloned;
156
+ }
157
+
158
+ /**
159
+ * Determines the appropriate tile flags for the given search result
160
+ * (login required and/or content warning)
161
+ */
162
+ private getFlags(result: SearchResult): TileFlags {
163
+ const flags: TileFlags = {
164
+ loginRequired: false,
165
+ contentWarning: false,
166
+ };
167
+
168
+ // Check if item and item in "modifying" collection, setting above flags
169
+ if (
170
+ result.collection?.values.length &&
171
+ result.mediatype?.value !== 'collection'
172
+ ) {
173
+ for (const collection of result.collection?.values ?? []) {
174
+ if (collection === 'loggedin') {
175
+ flags.loginRequired = true;
176
+ if (flags.contentWarning) break;
177
+ }
178
+ if (collection === 'no-preview') {
179
+ flags.contentWarning = true;
180
+ if (flags.loginRequired) break;
181
+ }
182
+ }
183
+ }
184
+
185
+ return flags;
186
+ }
38
187
  }
39
188
 
189
+ export type RequestKind = 'full' | 'hits' | 'aggregations';
190
+
40
191
  export type CollectionDisplayMode = 'grid' | 'list-compact' | 'list-detail';
41
192
 
42
193
  export type TileDisplayMode =
@@ -280,6 +431,16 @@ export function sortOptionFromAPIString(sortName?: string | null): SortOption {
280
431
  );
281
432
  }
282
433
 
434
+ export const defaultProfileElementSorts: Record<
435
+ string,
436
+ Exclude<SortField, SortField.default>
437
+ > = {
438
+ uploads: SortField.datearchived,
439
+ reviews: SortField.datereviewed,
440
+ collections: SortField.datearchived,
441
+ web_archives: SortField.datearchived,
442
+ };
443
+
283
444
  /** A union of the fields that permit prefix filtering (e.g., alphabetical filtering) */
284
445
  export type PrefixFilterType = 'title' | 'creator';
285
446
 
@@ -178,7 +178,7 @@ export class SortFilterBar
178
178
  this.setupEscapeListeners();
179
179
  }
180
180
 
181
- if (changed.has('resizeObserver')) {
181
+ if (changed.has('resizeObserver') || changed.has('showLoansTopBar')) {
182
182
  const oldObserver = changed.get(
183
183
  'resizeObserver'
184
184
  ) as SharedResizeObserverInterface;
@@ -216,35 +216,37 @@ export class SortFilterBar
216
216
  private disconnectResizeObserver(
217
217
  resizeObserver: SharedResizeObserverInterface
218
218
  ) {
219
- // return if element not defined
220
- if (!this.sortSelectorContainer || !this.desktopSortContainer) return;
221
-
222
- resizeObserver.removeObserver({
223
- target: this.sortSelectorContainer,
224
- handler: this,
225
- });
219
+ if (this.sortSelectorContainer) {
220
+ resizeObserver.removeObserver({
221
+ target: this.sortSelectorContainer,
222
+ handler: this,
223
+ });
224
+ }
226
225
 
227
- resizeObserver.removeObserver({
228
- target: this.desktopSortContainer,
229
- handler: this,
230
- });
226
+ if (this.desktopSortContainer) {
227
+ resizeObserver.removeObserver({
228
+ target: this.desktopSortContainer,
229
+ handler: this,
230
+ });
231
+ }
231
232
  }
232
233
 
233
234
  private setupResizeObserver() {
234
235
  if (!this.resizeObserver) return;
235
236
 
236
- // return if element not defined
237
- if (!this.sortSelectorContainer || !this.desktopSortContainer) return;
238
-
239
- this.resizeObserver.addObserver({
240
- target: this.sortSelectorContainer,
241
- handler: this,
242
- });
237
+ if (this.sortSelectorContainer) {
238
+ this.resizeObserver.addObserver({
239
+ target: this.sortSelectorContainer,
240
+ handler: this,
241
+ });
242
+ }
243
243
 
244
- this.resizeObserver.addObserver({
245
- target: this.desktopSortContainer,
246
- handler: this,
247
- });
244
+ if (this.desktopSortContainer) {
245
+ this.resizeObserver.addObserver({
246
+ target: this.desktopSortContainer,
247
+ handler: this,
248
+ });
249
+ }
248
250
  }
249
251
 
250
252
  handleResize(entry: ResizeObserverEntry): void {
@@ -1114,6 +1116,7 @@ export class SortFilterBar
1114
1116
  height: 100%;
1115
1117
  padding-left: 5px;
1116
1118
  font-size: 1.4rem;
1119
+ font-family: var(--ia-theme-base-font-family);
1117
1120
  line-height: 2;
1118
1121
  color: var(--ia-theme-primary-text-color, #2c2c2c);
1119
1122
  white-space: nowrap;
@@ -4,6 +4,7 @@ import { customElement, property } from 'lit/decorators.js';
4
4
  import { ifDefined } from 'lit/directives/if-defined.js';
5
5
  import { msg } from '@lit/localize';
6
6
 
7
+ import { map } from 'lit/directives/map.js';
7
8
  import { DateFormat, formatDate } from '../../utils/format-date';
8
9
  import { isFirstMillisecondOfUTCYear } from '../../utils/local-date-from-utc';
9
10
  import { BaseTileComponent } from '../base-tile-component';
@@ -57,7 +58,7 @@ export class ItemTile extends BaseTileComponent {
57
58
  ${this.isSortedByDate
58
59
  ? this.sortedDateInfoTemplate
59
60
  : this.creatorTemplate}
60
- ${this.textSnippetsTemplate}
61
+ ${this.webArchivesCaptureDatesTemplate} ${this.textSnippetsTemplate}
61
62
  </div>
62
63
 
63
64
  <tile-stats
@@ -172,6 +173,26 @@ export class ItemTile extends BaseTileComponent {
172
173
  `;
173
174
  }
174
175
 
176
+ private get webArchivesCaptureDatesTemplate():
177
+ | TemplateResult
178
+ | typeof nothing {
179
+ if (!this.model?.captureDates || !this.model.title) return nothing;
180
+
181
+ return html`
182
+ <ul class="capture-dates">
183
+ ${map(
184
+ this.model.captureDates,
185
+ date => html`<li>
186
+ ${this.displayValueProvider.webArchivesCaptureLink(
187
+ this.model!.title,
188
+ date
189
+ )}
190
+ </li>`
191
+ )}
192
+ </ul>
193
+ `;
194
+ }
195
+
175
196
  private get isSortedByDate(): boolean {
176
197
  return ['date', 'reviewdate', 'addeddate', 'publicdate'].includes(
177
198
  this.sortParam?.field as string
@@ -200,10 +221,24 @@ export class ItemTile extends BaseTileComponent {
200
221
  return [
201
222
  baseTileStyles,
202
223
  css`
224
+ a:link {
225
+ text-decoration: none;
226
+ color: var(--ia-theme-link-color, #4b64ff);
227
+ }
228
+ a:hover {
229
+ text-decoration: underline;
230
+ }
231
+
203
232
  .container {
204
233
  border: 1px solid ${tileBorderColor};
205
234
  }
206
235
 
236
+ .capture-dates {
237
+ margin: 0;
238
+ padding: 0 5px;
239
+ list-style-type: none;
240
+ }
241
+
207
242
  text-snippet-block {
208
243
  --containerLeftMargin: 5px;
209
244
  --containerTopMargin: 5px;
@@ -1,6 +1,7 @@
1
1
  import { css, CSSResultGroup, html, LitElement } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
3
 
4
+ import { msg } from '@lit/localize';
4
5
  import { favoriteFilledIcon } from '../../assets/img/icons/favorite-filled';
5
6
  import { reviewsIcon } from '../../assets/img/icons/reviews';
6
7
  import { uploadIcon } from '../../assets/img/icons/upload';
@@ -33,8 +34,8 @@ export class TileStats extends LitElement {
33
34
 
34
35
  const uploadsOrViewsTitle =
35
36
  this.mediatype === 'account'
36
- ? `${this.itemCount} uploads`
37
- : `${this.viewCount} ${this.viewLabel ?? 'all-time views'}`;
37
+ ? `${this.itemCount ?? 0} uploads`
38
+ : `${this.viewCount ?? 0} ${this.viewLabel ?? 'all-time views'}`;
38
39
 
39
40
  return html`
40
41
  <div class="item-stats">
@@ -43,17 +44,21 @@ export class TileStats extends LitElement {
43
44
  </p>
44
45
  <ul id="stats-row">
45
46
  <li class="col">
46
- <p class="sr-only">Mediatype:</p>
47
+ <p class="sr-only">${msg('Mediatype:')}</p>
47
48
  <mediatype-icon .mediatype=${this.mediatype}></mediatype-icon>
48
49
  </li>
49
50
  <li class="col" title="${uploadsOrViewsTitle}">
50
51
  ${this.mediatype === 'account' ? uploadIcon : viewsIcon}
51
52
  <p class="status-text">
52
53
  <span class="sr-only">
53
- ${this.mediatype === 'account' ? 'Uploads:' : 'Views:'}
54
+ ${this.mediatype === 'account'
55
+ ? msg('Uploads:')
56
+ : msg('Views:')}
54
57
  </span>
55
58
  ${formatCount(
56
- this.mediatype === 'account' ? this.itemCount : this.viewCount,
59
+ this.mediatype === 'account'
60
+ ? this.itemCount ?? 0
61
+ : this.viewCount ?? 0,
57
62
  'short',
58
63
  'short'
59
64
  )}
@@ -62,14 +67,14 @@ export class TileStats extends LitElement {
62
67
  <li class="col" title="${formattedFavCount} favorites">
63
68
  ${favoriteFilledIcon}
64
69
  <p class="status-text">
65
- <span class="sr-only">Favorites:</span>
70
+ <span class="sr-only">${msg('Favorites:')}</span>
66
71
  ${formattedFavCount}
67
72
  </p>
68
73
  </li>
69
74
  <li class="col reviews" title="${formattedReviewCount} reviews">
70
75
  ${reviewsIcon}
71
76
  <p class="status-text">
72
- <span class="sr-only">Reviews:</span>
77
+ <span class="sr-only">${msg('Reviews:')}</span>
73
78
  ${formattedReviewCount}
74
79
  </p>
75
80
  </li>
@@ -1,4 +1,3 @@
1
- import type { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
2
1
  import type { SortParam } from '@internetarchive/search-service';
3
2
  import {
4
3
  html,
@@ -8,6 +7,7 @@ import {
8
7
  ReactiveControllerHost,
9
8
  } from 'lit';
10
9
  import type { TileModel } from '../../models';
10
+ import type { CollectionTitles } from '../../data-source/models';
11
11
 
12
12
  type HoverPaneState = 'hidden' | 'shown' | 'fading-out';
13
13
 
@@ -17,7 +17,7 @@ export interface HoverPaneProperties {
17
17
  baseImageUrl?: string;
18
18
  loggedIn: boolean;
19
19
  sortParam: SortParam | null;
20
- collectionNameCache?: CollectionNameCacheInterface;
20
+ collectionTitles?: CollectionTitles;
21
21
  }
22
22
 
23
23
  export interface HoverPaneControllerOptions {
@@ -187,7 +187,7 @@ export class HoverPaneController implements HoverPaneControllerInterface {
187
187
  .baseImageUrl=${this.hoverPaneProps?.baseImageUrl}
188
188
  .loggedIn=${this.hoverPaneProps?.loggedIn}
189
189
  .sortParam=${this.hoverPaneProps?.sortParam}
190
- .collectionNameCache=${this.hoverPaneProps?.collectionNameCache}
190
+ .collectionTitles=${this.hoverPaneProps?.collectionTitles}
191
191
  ></tile-hover-pane>`
192
192
  : nothing;
193
193
  }
@@ -1,8 +1,8 @@
1
- import type { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
2
1
  import type { SortParam } from '@internetarchive/search-service';
3
2
  import { css, CSSResultGroup, html, LitElement, TemplateResult } from 'lit';
4
3
  import { customElement, property } from 'lit/decorators.js';
5
4
  import type { TileModel } from '../../models';
5
+ import type { CollectionTitles } from '../../data-source/models';
6
6
  import '../list/tile-list';
7
7
 
8
8
  @customElement('tile-hover-pane')
@@ -18,7 +18,7 @@ export class TileHoverPane extends LitElement {
18
18
  @property({ type: Object }) sortParam?: SortParam;
19
19
 
20
20
  @property({ type: Object })
21
- collectionNameCache?: CollectionNameCacheInterface;
21
+ collectionTitles?: CollectionTitles;
22
22
 
23
23
  protected render(): TemplateResult {
24
24
  return html`
@@ -29,7 +29,7 @@ export class TileHoverPane extends LitElement {
29
29
  .baseImageUrl=${this.baseImageUrl}
30
30
  .loggedIn=${this.loggedIn}
31
31
  .sortParam=${this.sortParam}
32
- .collectionNameCache=${this.collectionNameCache}
32
+ .collectionTitles=${this.collectionTitles}
33
33
  ></tile-list>
34
34
  </div>
35
35
  `;
@@ -24,6 +24,8 @@ export class ItemImage extends LitElement {
24
24
 
25
25
  @state() private isWaveform = false;
26
26
 
27
+ @state() private isNotFound = false;
28
+
27
29
  @query('img') private baseImage!: HTMLImageElement;
28
30
 
29
31
  render() {
@@ -43,6 +45,7 @@ export class ItemImage extends LitElement {
43
45
  src="${this.imageSrc}"
44
46
  alt=""
45
47
  @load=${this.onLoad}
48
+ @error=${this.onError}
46
49
  />
47
50
  `;
48
51
  }
@@ -51,12 +54,33 @@ export class ItemImage extends LitElement {
51
54
  * Helpers
52
55
  */
53
56
  private get imageSrc() {
57
+ if (this.isNotFound) return this.notFoundSrc;
58
+
59
+ // Use the correct image for web capture tiles, if possible
60
+ if (this.model?.captureDates && this.model.identifier) {
61
+ try {
62
+ const url = new URL(this.model.identifier);
63
+ const domain = encodeURIComponent(url.hostname);
64
+ return this.baseImageUrl
65
+ ? `https://web.archive.org/thumb/${domain}?generate=1`
66
+ : nothing;
67
+ } catch (err) {
68
+ return `${this.baseImageUrl}/images/notfound.png`;
69
+ }
70
+ }
71
+
54
72
  // Don't try to load invalid image URLs
55
73
  return this.baseImageUrl && this.model?.identifier
56
74
  ? `${this.baseImageUrl}/services/img/${this.model.identifier}`
57
75
  : nothing;
58
76
  }
59
77
 
78
+ private get notFoundSrc() {
79
+ return this.baseImageUrl
80
+ ? `${this.baseImageUrl}/images/notfound.png`
81
+ : nothing;
82
+ }
83
+
60
84
  private get hashBasedGradient() {
61
85
  if (!this.model?.identifier) {
62
86
  return 'waveform-grad0';
@@ -120,6 +144,10 @@ export class ItemImage extends LitElement {
120
144
  }
121
145
  }
122
146
 
147
+ private onError() {
148
+ this.isNotFound = true;
149
+ }
150
+
123
151
  /**
124
152
  * CSS
125
153
  */