@internetarchive/collection-browser 1.14.17-alpha.2 → 1.14.17-alpha.21

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 (87) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/app-root.d.ts +1 -0
  4. package/dist/src/app-root.js +38 -0
  5. package/dist/src/app-root.js.map +1 -1
  6. package/dist/src/collection-browser.d.ts +2 -15
  7. package/dist/src/collection-browser.js +22 -29
  8. package/dist/src/collection-browser.js.map +1 -1
  9. package/dist/src/data-source/collection-browser-data-source.d.ts +55 -32
  10. package/dist/src/data-source/collection-browser-data-source.js +155 -162
  11. package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
  12. package/dist/src/data-source/models.d.ts +6 -3
  13. package/dist/src/data-source/models.js.map +1 -1
  14. package/dist/src/manage/manage-bar.d.ts +1 -1
  15. package/dist/src/manage/manage-bar.js.map +1 -1
  16. package/dist/src/models.d.ts +20 -4
  17. package/dist/src/models.js +105 -0
  18. package/dist/src/models.js.map +1 -1
  19. package/dist/src/sort-filter-bar/sort-filter-bar.js +25 -16
  20. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  21. package/dist/src/tiles/grid/item-tile.d.ts +1 -0
  22. package/dist/src/tiles/grid/item-tile.js +28 -1
  23. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  24. package/dist/src/tiles/grid/tile-stats.js +13 -8
  25. package/dist/src/tiles/grid/tile-stats.js.map +1 -1
  26. package/dist/src/tiles/list/tile-list-compact.js +1 -1
  27. package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
  28. package/dist/src/tiles/list/tile-list.d.ts +1 -0
  29. package/dist/src/tiles/list/tile-list.js +32 -1
  30. package/dist/src/tiles/list/tile-list.js.map +1 -1
  31. package/dist/src/tiles/tile-dispatcher.js +3 -2
  32. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  33. package/dist/src/tiles/tile-display-value-provider.d.ts +6 -2
  34. package/dist/src/tiles/tile-display-value-provider.js +15 -1
  35. package/dist/src/tiles/tile-display-value-provider.js.map +1 -1
  36. package/dist/src/utils/collapse-repeated-quotes.d.ts +11 -0
  37. package/dist/src/utils/collapse-repeated-quotes.js +14 -0
  38. package/dist/src/utils/collapse-repeated-quotes.js.map +1 -0
  39. package/dist/src/utils/resolve-mediatype.d.ts +8 -0
  40. package/dist/src/utils/resolve-mediatype.js +24 -0
  41. package/dist/src/utils/resolve-mediatype.js.map +1 -0
  42. package/dist/test/collection-browser.test.js +39 -19
  43. package/dist/test/collection-browser.test.js.map +1 -1
  44. package/dist/test/collection-facets/more-facets-content.test.js +2 -2
  45. package/dist/test/collection-facets/more-facets-content.test.js.map +1 -1
  46. package/dist/test/collection-facets.test.js +5 -0
  47. package/dist/test/collection-facets.test.js.map +1 -1
  48. package/dist/test/item-image.test.js +33 -34
  49. package/dist/test/item-image.test.js.map +1 -1
  50. package/dist/test/mocks/mock-search-responses.d.ts +1 -0
  51. package/dist/test/mocks/mock-search-responses.js +62 -0
  52. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  53. package/dist/test/sort-filter-bar/sort-filter-bar.test.js +41 -4
  54. package/dist/test/sort-filter-bar/sort-filter-bar.test.js.map +1 -1
  55. package/dist/test/tiles/hover/hover-pane-controller.test.js +18 -17
  56. package/dist/test/tiles/hover/hover-pane-controller.test.js.map +1 -1
  57. package/package.json +2 -2
  58. package/src/app-root.ts +39 -0
  59. package/src/collection-browser.ts +24 -40
  60. package/src/data-source/collection-browser-data-source.ts +160 -132
  61. package/src/data-source/models.ts +6 -2
  62. package/src/manage/manage-bar.ts +1 -1
  63. package/src/models.ts +154 -3
  64. package/src/sort-filter-bar/sort-filter-bar.ts +26 -16
  65. package/src/tiles/grid/item-tile.ts +36 -1
  66. package/src/tiles/grid/tile-stats.ts +12 -7
  67. package/src/tiles/list/tile-list-compact.ts +1 -1
  68. package/src/tiles/list/tile-list.ts +43 -5
  69. package/src/tiles/tile-dispatcher.ts +2 -1
  70. package/src/tiles/tile-display-value-provider.ts +20 -2
  71. package/src/utils/collapse-repeated-quotes.ts +13 -0
  72. package/src/utils/resolve-mediatype.ts +26 -0
  73. package/test/collection-browser.test.ts +74 -19
  74. package/test/collection-facets/more-facets-content.test.ts +4 -2
  75. package/test/collection-facets.test.ts +5 -0
  76. package/test/item-image.test.ts +34 -36
  77. package/test/mocks/mock-search-responses.ts +66 -0
  78. package/test/sort-filter-bar/sort-filter-bar.test.ts +50 -4
  79. package/test/tiles/hover/hover-pane-controller.test.ts +19 -17
  80. package/dist/src/data-source/data-source-fetch-provider.d.ts +0 -13
  81. package/dist/src/data-source/data-source-fetch-provider.js +0 -61
  82. package/dist/src/data-source/data-source-fetch-provider.js.map +0 -1
  83. package/dist/src/selected-facets.d.ts +0 -67
  84. package/dist/src/selected-facets.js +0 -149
  85. package/dist/src/selected-facets.js.map +0 -1
  86. package/src/data-source/data-source-fetch-provider.ts +0 -79
  87. package/src/selected-facets.ts +0 -216
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 =
@@ -216,28 +216,37 @@ export class SortFilterBar
216
216
  private disconnectResizeObserver(
217
217
  resizeObserver: SharedResizeObserverInterface
218
218
  ) {
219
- resizeObserver.removeObserver({
220
- target: this.sortSelectorContainer,
221
- handler: this,
222
- });
219
+ if (this.sortSelectorContainer) {
220
+ resizeObserver.removeObserver({
221
+ target: this.sortSelectorContainer,
222
+ handler: this,
223
+ });
224
+ }
223
225
 
224
- resizeObserver.removeObserver({
225
- target: this.desktopSortContainer,
226
- handler: this,
227
- });
226
+ if (this.desktopSortContainer) {
227
+ resizeObserver.removeObserver({
228
+ target: this.desktopSortContainer,
229
+ handler: this,
230
+ });
231
+ }
228
232
  }
229
233
 
230
234
  private setupResizeObserver() {
231
235
  if (!this.resizeObserver) return;
232
- this.resizeObserver.addObserver({
233
- target: this.sortSelectorContainer,
234
- handler: this,
235
- });
236
236
 
237
- this.resizeObserver.addObserver({
238
- target: this.desktopSortContainer,
239
- handler: this,
240
- });
237
+ if (this.sortSelectorContainer) {
238
+ this.resizeObserver.addObserver({
239
+ target: this.sortSelectorContainer,
240
+ handler: this,
241
+ });
242
+ }
243
+
244
+ if (this.desktopSortContainer) {
245
+ this.resizeObserver.addObserver({
246
+ target: this.desktopSortContainer,
247
+ handler: this,
248
+ });
249
+ }
241
250
  }
242
251
 
243
252
  handleResize(entry: ResizeObserverEntry): void {
@@ -1107,6 +1116,7 @@ export class SortFilterBar
1107
1116
  height: 100%;
1108
1117
  padding-left: 5px;
1109
1118
  font-size: 1.4rem;
1119
+ font-family: var(--ia-theme-base-font-family);
1110
1120
  line-height: 2;
1111
1121
  color: var(--ia-theme-primary-text-color, #2c2c2c);
1112
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';
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>
@@ -167,7 +167,7 @@ export class TileListCompact extends BaseTileComponent {
167
167
  align-items: center;
168
168
  line-height: 20px;
169
169
  padding-top: 5px;
170
- padding-bottom: 5px;
170
+ margin-bottom: -5px;
171
171
  }
172
172
 
173
173
  #list-line.mobile {
@@ -37,7 +37,7 @@ export class TileList extends BaseTileComponent {
37
37
  @property({ type: Object })
38
38
  collectionTitles?: CollectionTitles;
39
39
 
40
- @state() private collectionLinks: TemplateResult[] = [];
40
+ @state() private collectionLinks: (TemplateResult | typeof nothing)[] = [];
41
41
 
42
42
  render() {
43
43
  return html`
@@ -107,6 +107,7 @@ export class TileList extends BaseTileComponent {
107
107
  ${this.itemLineTemplate} ${this.creatorTemplate}
108
108
  <div id="dates-line">
109
109
  ${this.datePublishedTemplate} ${this.dateSortByTemplate}
110
+ ${this.webArchivesCaptureDatesTemplate}
110
111
  </div>
111
112
  <div id="views-line">
112
113
  ${this.viewsTemplate} ${this.ratingTemplate} ${this.reviewsTemplate}
@@ -238,6 +239,7 @@ export class TileList extends BaseTileComponent {
238
239
  this.sortParam?.field === 'week'
239
240
  ? this.model?.weeklyViewCount // weekly views
240
241
  : this.model?.viewCount; // all-time views
242
+ if (viewCount == null) return nothing;
241
243
 
242
244
  // when its a search-tile, we don't have any stats to show
243
245
  if (this.model?.mediatype === 'search') {
@@ -245,7 +247,7 @@ export class TileList extends BaseTileComponent {
245
247
  }
246
248
 
247
249
  return this.metadataTemplate(
248
- `${formatCount(viewCount ?? 0, this.formatSize)}`,
250
+ `${formatCount(viewCount, this.formatSize)}`,
249
251
  msg('Views')
250
252
  );
251
253
  }
@@ -309,6 +311,26 @@ export class TileList extends BaseTileComponent {
309
311
  return !!this.model?.snippets?.length;
310
312
  }
311
313
 
314
+ private get webArchivesCaptureDatesTemplate():
315
+ | TemplateResult
316
+ | typeof nothing {
317
+ if (!this.model?.captureDates || !this.model?.title) return nothing;
318
+
319
+ return html`
320
+ <ul class="capture-dates">
321
+ ${map(
322
+ this.model.captureDates,
323
+ date => html`<li>
324
+ ${this.displayValueProvider.webArchivesCaptureLink(
325
+ this.model!.title,
326
+ date
327
+ )}
328
+ </li>`
329
+ )}
330
+ </ul>
331
+ `;
332
+ }
333
+
312
334
  // Utility functions
313
335
  // eslint-disable-next-line default-param-last
314
336
  private metadataTemplate(text: any, label = '', id?: string) {
@@ -345,10 +367,12 @@ export class TileList extends BaseTileComponent {
345
367
  }
346
368
 
347
369
  private detailsLink(
348
- identifier: string,
370
+ identifier?: string,
349
371
  text?: string,
350
372
  isCollection = false
351
- ): TemplateResult {
373
+ ): TemplateResult | typeof nothing {
374
+ if (!identifier) return nothing;
375
+
352
376
  const linkText = text ?? identifier;
353
377
  const linkHref = this.displayValueProvider.itemPageUrl(
354
378
  identifier,
@@ -392,7 +416,7 @@ export class TileList extends BaseTileComponent {
392
416
  // Note: quirk of Lit: need to replace collectionLinks array,
393
417
  // otherwise it will not re-render. Can't simply alter the array.
394
418
  this.collectionLinks = [];
395
- const newCollectionLinks: TemplateResult[] = [];
419
+ const newCollectionLinks: (TemplateResult | typeof nothing)[] = [];
396
420
  for (const collection of this.model.collections) {
397
421
  // Don't include favorites or collections that are meant to be suppressed
398
422
  if (
@@ -622,6 +646,20 @@ export class TileList extends BaseTileComponent {
622
646
  #views-line {
623
647
  flex-wrap: wrap;
624
648
  }
649
+
650
+ .capture-dates {
651
+ margin: 0;
652
+ padding: 0;
653
+ list-style-type: none;
654
+ }
655
+
656
+ .capture-dates a:link {
657
+ text-decoration: none;
658
+ color: var(--ia-theme-link-color, #4b64ff);
659
+ }
660
+ .capture-dates a:hover {
661
+ text-decoration: underline;
662
+ }
625
663
  `;
626
664
  }
627
665
  }
@@ -184,7 +184,8 @@ export class TileDispatcher
184
184
  this.enableHoverPane &&
185
185
  !!this.tileDisplayMode &&
186
186
  TileDispatcher.HOVER_PANE_DISPLAY_MODES[this.tileDisplayMode] &&
187
- this.model?.mediatype !== 'search' // don't show hover panes on search tiles
187
+ this.model?.mediatype !== 'search' && // don't show hover panes on search tiles
188
+ !this.model?.captureDates // don't show hover panes on web archive tiles
188
189
  );
189
190
  }
190
191
 
@@ -1,7 +1,8 @@
1
- import { nothing } from 'lit';
1
+ import { TemplateResult, html, nothing } from 'lit';
2
2
  import { msg, str } from '@lit/localize';
3
3
  import type { SortParam } from '@internetarchive/search-service';
4
4
  import type { TileModel } from '../models';
5
+ import { formatDate } from '../utils/format-date';
5
6
 
6
7
  /**
7
8
  * A class encapsulating shared logic for converting model values into display values
@@ -96,11 +97,28 @@ export class TileDisplayValueProvider {
96
97
  * item is specified to be a collection (default false).
97
98
  */
98
99
  itemPageUrl(
99
- identifier: string,
100
+ identifier?: string,
100
101
  isCollection = false
101
102
  ): string | typeof nothing {
102
103
  if (!identifier || this.baseNavigationUrl == null) return nothing;
103
104
  const basePath = isCollection ? this.collectionPagePath : '/details/';
104
105
  return `${this.baseNavigationUrl}${basePath}${identifier}`;
105
106
  }
107
+
108
+ /**
109
+ * Produces a template for a link to a single web capture of the given URL and date
110
+ */
111
+ webArchivesCaptureLink(url: string, date: Date): TemplateResult {
112
+ // Convert the date into the format used to identify wayback captures (e.g., '20150102124550')
113
+ const captureDateStr = date
114
+ .toISOString()
115
+ .replace(/[TZ:-]/g, '')
116
+ .replace(/\..*/, '');
117
+ const captureHref = `https://web.archive.org/web/${captureDateStr}/${encodeURIComponent(
118
+ url
119
+ )}`;
120
+ const captureText = formatDate(date, 'long');
121
+
122
+ return html` <a href=${captureHref}> ${captureText} </a> `;
123
+ }
106
124
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Returns the input string, but removing one set of quotes from all instances of
3
+ * ""clauses wrapped in two sets of quotes"". This assumes the quotes are already
4
+ * URL-encoded.
5
+ *
6
+ * This should be a temporary measure to address the fact that the __href__ field
7
+ * sometimes acquires extra quotation marks during query rewriting. Once there is a
8
+ * full Lucene parser in place that handles quoted queries correctly, this can likely
9
+ * be removed.
10
+ */
11
+ export function collapseRepeatedQuotes(str?: string): string | undefined {
12
+ return str?.replace(/%22%22(?!%22%22)(.+?)%22%22/g, '%22$1%22');
13
+ }
@@ -0,0 +1,26 @@
1
+ import type { MediaType } from '@internetarchive/field-parsers';
2
+ import type { SearchResult } from '@internetarchive/search-service';
3
+
4
+ /**
5
+ * Returns the mediatype string for the given search result, taking into account
6
+ * the special `favorited_search` hit type.
7
+ * @param result The search result to extract a mediatype from
8
+ */
9
+ export function resolveMediatype(result: SearchResult): MediaType {
10
+ /**
11
+ * hit_type == 'favorited_search' is basically a new hit_type
12
+ * - we are getting from PPS.
13
+ * - which gives response for fav- collection
14
+ * - having favorited items like account/collection/item etc..
15
+ * - as user can also favorite a search result (a search page)
16
+ * - so we need to have response (having fav- items and fav- search results)
17
+ *
18
+ * if backend hit_type == 'favorited_search'
19
+ * - let's assume a "search" as new mediatype
20
+ */
21
+ if (result?.rawMetadata?.hit_type === 'favorited_search') {
22
+ return 'search';
23
+ }
24
+
25
+ return result.mediatype?.value ?? 'data';
26
+ }