@jschofield/reading-list 0.2.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.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @jschofield/reading-list
2
+
3
+ A reading list / book tracker web component built with [Lit](https://lit.dev/). Fetches book data from an API and renders an interactive, filterable, sortable table.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @jschofield/reading-list
9
+ ```
10
+
11
+ ### Peer dependencies
12
+
13
+ This component requires the following peer dependency:
14
+
15
+ ```bash
16
+ npm install lit
17
+ ```
18
+
19
+ | Peer | Version |
20
+ |---|---|
21
+ | `lit` | `^3.0.0` |
22
+
23
+ ## Usage
24
+
25
+ ```html
26
+ <script type="module">
27
+ import '@jschofield/reading-list';
28
+ </script>
29
+
30
+ <reading-list></reading-list>
31
+
32
+ <!-- Custom API endpoint: -->
33
+ <reading-list api-endpoint="/api/my-books"></reading-list>
34
+ ```
35
+
36
+ ### Without a bundler
37
+
38
+ If you're not using a bundler, provide peer dependencies via an import map:
39
+
40
+ ```html
41
+ <script type="importmap">
42
+ {
43
+ "imports": {
44
+ "lit": "https://esm.run/lit",
45
+ "lit/": "https://esm.run/lit/"
46
+ }
47
+ }
48
+ </script>
49
+ <script type="module" src="https://esm.run/@jschofield/reading-list"></script>
50
+ ```
51
+
52
+ ## Attributes
53
+
54
+ | Attribute | Type | Default | Description |
55
+ |---|---|---|---|
56
+ | `api-endpoint` | `string` | `"/.netlify/functions/reading-list"` | URL returning the books JSON |
57
+
58
+ ## API response format
59
+
60
+ ```json
61
+ {
62
+ "books": [
63
+ {
64
+ "name": "Dune",
65
+ "author": "Frank Herbert",
66
+ "series": "Dune",
67
+ "status": "Finished",
68
+ "finished": "2024-03-15",
69
+ "notes": "Incredible worldbuilding",
70
+ "grade": "A+",
71
+ "year": 2024
72
+ }
73
+ ]
74
+ }
75
+ ```
76
+
77
+ ## Features
78
+
79
+ - Search by book name, author, or series
80
+ - Filter by status, year, grade, and series
81
+ - Sortable columns (click headers to toggle)
82
+ - Color-coded grade and status badges
83
+ - "Clear All Filters" button
84
+ - Responsive table layout
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,541 @@
1
+ import{LitElement as e,css as t,html as n}from"lit";import{customElement as r,property as i,state as a}from"lit/decorators.js";(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function o(e,t,n,r){var i=arguments.length,a=i<3?t:r===null?r=Object.getOwnPropertyDescriptor(t,n):r,o;if(typeof Reflect==`object`&&typeof Reflect.decorate==`function`)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(o=e[s])&&(a=(i<3?o(a):i>3?o(t,n,a):o(t,n))||a);return i>3&&a&&Object.defineProperty(t,n,a),a}var s=class extends e{constructor(...e){super(...e),this.apiEndpoint=`/.netlify/functions/reading-list`,this.books=[],this.filteredBooks=[],this.loading=!1,this.error=``,this.searchTerm=``,this.statusFilter=``,this.seriesFilter=``,this.yearFilter=``,this.gradeFilter=``,this.sortColumn=null,this.sortDirection=`asc`}async connectedCallback(){super.connectedCallback(),await this.fetchBooks()}async fetchBooks(){this.loading=!0,this.error=``;try{let e=await fetch(this.apiEndpoint);if(!e.ok)throw Error(`HTTP error! status: ${e.status}`);let t=await e.json();if(t.error)throw Error(t.error);this.books=t.books||[],this.filteredBooks=[...this.books]}catch(e){this.error=`Failed to load reading list: ${e instanceof Error?e.message:`Unknown error`}`,console.error(`Error fetching books:`,e)}finally{this.loading=!1}}formatDate(e){if(!e)return null;try{let t=new Date(e);return isNaN(t.getTime())?e:t.toLocaleDateString(`en-US`,{year:`numeric`,month:`short`,day:`numeric`})}catch{return e}}renderGrade(e){return e?n`<span class="grade-badge grade-${e.charAt(0).toLowerCase()}">${e}</span>`:n`<span class="not-applicable">—</span>`}renderFinishedDate(e,t){return e?n`<span class="completion-date">${this.formatDate(e)}</span>`:t===`finished`?n`<span class="not-applicable">No date</span>`:n`<span class="not-applicable">—</span>`}getAvailableSeries(){return[...new Set(this.books.map(e=>e.series).filter(e=>!!e))].sort()}getAvailableYears(){return[...new Set(this.books.map(e=>e.year).filter(e=>typeof e==`number`))].sort((e,t)=>t-e)}getAvailableStatuses(){return[`Finished`,`Ah Naw`,`Reading`,`On Deck`,`Not started`,`Maybe Later`]}getAvailableGrades(){let e=new Set;return this.books.map(e=>e.grade).filter(e=>!!e?.trim()).forEach(t=>{e.add(t.toUpperCase())}),Array.from(e).sort((e,t)=>{let n=e=>{let t=e.charAt(0).toUpperCase(),n=e.slice(1),r=0;switch(t){case`A`:r=400;break;case`B`:r=300;break;case`C`:r=200;break;case`D`:r=100;break;case`F`:r=0;break;default:r=-100}return n===`+`?r+=30:n===`-`&&(r-=30),r};return n(t)-n(e)})}applyFilters(){this.filteredBooks=this.books.filter(e=>{let t=!this.searchTerm||e.name.toLowerCase().includes(this.searchTerm.toLowerCase())||e.author.toLowerCase().includes(this.searchTerm.toLowerCase())||e.series&&e.series.toLowerCase().includes(this.searchTerm.toLowerCase()),n=!this.statusFilter||e.status===this.statusFilter,r=!this.seriesFilter||e.series===this.seriesFilter,i=!this.yearFilter||e.year?.toString()===this.yearFilter,a=!this.gradeFilter||e.grade?.toUpperCase()===this.gradeFilter;return t&&n&&r&&i&&a}),this.sortColumn&&this.filteredBooks.sort((e,t)=>{let n,r;switch(this.sortColumn){case`name`:n=e.name.toLowerCase(),r=t.name.toLowerCase();break;case`author`:n=e.author.toLowerCase(),r=t.author.toLowerCase();break;case`series`:n=(e.series||``).toLowerCase(),r=(t.series||``).toLowerCase();break;case`status`:n=e.status.toLowerCase(),r=t.status.toLowerCase();break;case`year`:n=e.year||0,r=t.year||0;break;case`grade`:let i=e=>{if(!e)return-1;let t=e.charAt(0).toUpperCase(),n=e.slice(1),r=0;switch(t){case`A`:r=400;break;case`B`:r=300;break;case`C`:r=200;break;case`D`:r=100;break;case`F`:r=0;break;default:r=-100}return n===`+`?r+=30:n===`-`&&(r-=30),r};n=i(e.grade),r=i(t.grade);break;case`finished`:n=e.finished?new Date(e.finished).getTime():0,r=t.finished?new Date(t.finished).getTime():0;break;default:return 0}return n<r?this.sortDirection===`asc`?-1:1:n>r?this.sortDirection===`asc`?1:-1:0})}handleSearch(e){this.searchTerm=e.target.value,this.applyFilters()}handleStatusFilter(e){this.statusFilter=e.target.value,this.applyFilters()}handleSeriesFilter(e){this.seriesFilter=e.target.value,this.applyFilters()}handleYearFilter(e){this.yearFilter=e.target.value,this.applyFilters()}handleGradeFilter(e){this.gradeFilter=e.target.value,this.applyFilters()}handleSort(e){this.sortColumn===e?this.sortDirection=this.sortDirection===`asc`?`desc`:`asc`:(this.sortColumn=e,this.sortDirection=`asc`),this.applyFilters()}clearAllFilters(){this.searchTerm=``,this.statusFilter=``,this.yearFilter=``,this.gradeFilter=``,this.seriesFilter=``,this.sortColumn=null,this.sortDirection=`asc`,this.applyFilters()}renderFilters(){let e=this.getAvailableSeries(),t=this.getAvailableYears(),r=this.getAvailableStatuses(),i=this.getAvailableGrades();return n`
2
+ <div class="filters">
3
+ <div class="filter-group">
4
+ <label for="search">Search Books</label>
5
+ <input
6
+ type="text"
7
+ id="search"
8
+ placeholder="Search by name, author, or series..."
9
+ .value=${this.searchTerm}
10
+ @input=${this.handleSearch}
11
+ />
12
+ </div>
13
+ <div class="filter-group">
14
+ <label for="status-filter">Filter by Status</label>
15
+ <select
16
+ id="status-filter"
17
+ .value=${this.statusFilter}
18
+ @change=${this.handleStatusFilter}
19
+ >
20
+ <option value="">All Statuses</option>
21
+ ${r.map(e=>n`<option value="${e}">${e}</option>`)}
22
+ </select>
23
+ </div>
24
+ <div class="filter-group">
25
+ <label for="year-filter">Filter by Year</label>
26
+ <select
27
+ id="year-filter"
28
+ .value=${this.yearFilter}
29
+ @change=${this.handleYearFilter}
30
+ >
31
+ <option value="">All Years</option>
32
+ ${t.map(e=>n`<option value="${e}">${e}</option>`)}
33
+ </select>
34
+ </div>
35
+ <div class="filter-group">
36
+ <label for="grade-filter">Filter by Grade</label>
37
+ <select
38
+ id="grade-filter"
39
+ .value=${this.gradeFilter}
40
+ @change=${this.handleGradeFilter}
41
+ >
42
+ <option value="">All Grades</option>
43
+ ${i.map(e=>n`<option value="${e}">${e}</option>`)}
44
+ </select>
45
+ </div>
46
+ <div class="filter-group">
47
+ <label for="series-filter">Filter by Series</label>
48
+ <select
49
+ id="series-filter"
50
+ .value=${this.seriesFilter}
51
+ @change=${this.handleSeriesFilter}
52
+ >
53
+ <option value="">All Series</option>
54
+ ${e.map(e=>n`<option value="${e}">${e}</option>`)}
55
+ </select>
56
+ </div>
57
+ <div class="filter-group">
58
+ <button
59
+ class="clear-filters-btn"
60
+ @click=${this.clearAllFilters}
61
+ type="button"
62
+ >
63
+ Clear All Filters
64
+ </button>
65
+ </div>
66
+ </div>
67
+ `}renderTable(){return this.filteredBooks.length===0?n`
68
+ <div class="no-results">No books found matching your filters.</div>
69
+ `:n`
70
+ <table>
71
+ <thead>
72
+ <tr>
73
+ <th>
74
+ <button
75
+ class="sort-header"
76
+ @click=${()=>this.handleSort(`name`)}
77
+ >
78
+ Book
79
+ ${this.sortColumn===`name`?this.sortDirection===`asc`?` ↑`:` ↓`:``}
80
+ </button>
81
+ </th>
82
+ <th>
83
+ <button
84
+ class="sort-header"
85
+ @click=${()=>this.handleSort(`series`)}
86
+ >
87
+ Series
88
+ ${this.sortColumn===`series`?this.sortDirection===`asc`?` ↑`:` ↓`:``}
89
+ </button>
90
+ </th>
91
+ <th>
92
+ <button
93
+ class="sort-header"
94
+ @click=${()=>this.handleSort(`status`)}
95
+ >
96
+ Status
97
+ ${this.sortColumn===`status`?this.sortDirection===`asc`?` ↑`:` ↓`:``}
98
+ </button>
99
+ </th>
100
+ <th>
101
+ <button
102
+ class="sort-header"
103
+ @click=${()=>this.handleSort(`year`)}
104
+ >
105
+ Year
106
+ ${this.sortColumn===`year`?this.sortDirection===`asc`?` ↑`:` ↓`:``}
107
+ </button>
108
+ </th>
109
+ <th>
110
+ <button
111
+ class="sort-header"
112
+ @click=${()=>this.handleSort(`finished`)}
113
+ >
114
+ Finished
115
+ ${this.sortColumn===`finished`?this.sortDirection===`asc`?` ↑`:` ↓`:``}
116
+ </button>
117
+ </th>
118
+ <th>
119
+ <button
120
+ class="sort-header"
121
+ @click=${()=>this.handleSort(`grade`)}
122
+ >
123
+ Grade
124
+ ${this.sortColumn===`grade`?this.sortDirection===`asc`?` ↑`:` ↓`:``}
125
+ </button>
126
+ </th>
127
+ <th>Notes</th>
128
+ </tr>
129
+ </thead>
130
+ <tbody>
131
+ ${this.filteredBooks.map(e=>n`
132
+ <tr>
133
+ <td>
134
+ <div class="book-title">${e.name}</div>
135
+ <div class="book-author">by ${e.author}</div>
136
+ </td>
137
+ <td>
138
+ ${e.series?n`<span class="series-badge">${e.series}</span>`:n`<span class="not-applicable">—</span>`}
139
+ </td>
140
+ <td>
141
+ <span
142
+ class="status-badge status-${e.status.toLowerCase().replace(/\s+/g,`-`)}"
143
+ >
144
+ ${e.status}
145
+ </span>
146
+ </td>
147
+ <td>
148
+ ${e.year?n`<span class="year-badge">${e.year}</span>`:n`<span class="not-applicable">—</span>`}
149
+ </td>
150
+ <td>${this.renderFinishedDate(e.finished,e.status)}</td>
151
+ <td>${this.renderGrade(e.grade)}</td>
152
+ <td class="notes-cell">${e.notes?.trim()?n`${e.notes.trim()}`:n`<span class="not-applicable">—</span>`}</td>
153
+ </tr>
154
+ `)}
155
+ </tbody>
156
+ </table>
157
+ `}render(){return this.loading?n`
158
+ <div class="loading">
159
+ <p>Loading reading list...</p>
160
+ </div>
161
+ `:this.error?n`
162
+ <div class="error">
163
+ <p>${this.error}</p>
164
+ </div>
165
+ `:n`
166
+ <div class="container">
167
+ ${this.renderFilters()}
168
+
169
+ <div class="table-container">${this.renderTable()}</div>
170
+ </div>
171
+ `}static{this.styles=t`
172
+ :host {
173
+ --primary-color: #2563eb;
174
+ --text-color: #1f2937;
175
+ --text-secondary: #6b7280;
176
+ --border-color: #e5e7eb;
177
+ --bg-color: #ffffff;
178
+ --bg-hover: #f9fafb;
179
+ --success-color: #10b981;
180
+ --warning-color: #f59e0b;
181
+ --danger-color: #ef4444;
182
+
183
+ display: block;
184
+ font-family:
185
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
186
+ Arial, sans-serif;
187
+ line-height: 1.6;
188
+ color: var(--text-color);
189
+ }
190
+
191
+ * {
192
+ box-sizing: border-box;
193
+ }
194
+
195
+ .container {
196
+ max-width: 1200px;
197
+ margin: 0 auto;
198
+ padding: 2rem 1rem;
199
+ }
200
+
201
+ header {
202
+ text-align: center;
203
+ margin-bottom: 3rem;
204
+ }
205
+
206
+ h1 {
207
+ font-size: 2.5rem;
208
+ margin: 0 0 1rem 0;
209
+ font-weight: 300;
210
+ }
211
+
212
+ .subtitle {
213
+ color: var(--text-secondary);
214
+ font-size: 1.1rem;
215
+ margin: 0;
216
+ }
217
+
218
+ .loading,
219
+ .error {
220
+ text-align: center;
221
+ padding: 2rem;
222
+ color: var(--text-secondary);
223
+ }
224
+
225
+ .error {
226
+ color: var(--danger-color);
227
+ background: #fee2e2;
228
+ border: 1px solid #fecaca;
229
+ border-radius: 12px;
230
+ }
231
+
232
+ .filters {
233
+ background: var(--bg-color);
234
+ padding: 1.5rem;
235
+ border-radius: 12px;
236
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
237
+ margin-bottom: 2rem;
238
+ display: grid;
239
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
240
+ gap: 1rem;
241
+ }
242
+
243
+ .filter-group {
244
+ display: flex;
245
+ flex-direction: column;
246
+ gap: 0.5rem;
247
+ }
248
+
249
+ .filter-group label {
250
+ font-size: 0.875rem;
251
+ font-weight: 600;
252
+ color: var(--text-secondary);
253
+ text-transform: uppercase;
254
+ letter-spacing: 0.025em;
255
+ }
256
+
257
+ .filter-group input,
258
+ .filter-group select {
259
+ padding: 0.75rem 1rem;
260
+ border: 2px solid var(--border-color);
261
+ border-radius: 8px;
262
+ font-size: 1rem;
263
+ transition: border-color 0.2s;
264
+ }
265
+
266
+ .filter-group input:focus,
267
+ .filter-group select:focus {
268
+ outline: none;
269
+ border-color: var(--primary-color);
270
+ }
271
+
272
+ .filter-group:has(.clear-filters-btn) {
273
+ justify-content: center;
274
+ align-items: stretch;
275
+ }
276
+
277
+ .clear-filters-btn {
278
+ display: inline-block;
279
+ padding: 0.25em 1em;
280
+ background: #1b2f36; /* --gunmetal from main.css */
281
+ color: #fafafa; /* --seasalt from main.css */
282
+ border-radius: 8px;
283
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* --shadow from main.css */
284
+ text-decoration: none;
285
+ font: inherit;
286
+ border: none;
287
+ cursor: pointer;
288
+ align-self: center;
289
+ }
290
+
291
+ .clear-filters-btn:active,
292
+ .clear-filters-btn:hover {
293
+ color: #fafafa; /* --seasalt */
294
+ background: linear-gradient(
295
+ 135deg,
296
+ #4d5963 0% 20%,
297
+ #f79103 20% 40%,
298
+ #376170 40% 60%,
299
+ #906b56 60% 80%,
300
+ #1b2f36 80% 100%
301
+ ); /* --gradient */
302
+ }
303
+
304
+ .clear-filters-btn:focus-visible {
305
+ color: #fafafa; /* --seasalt */
306
+ outline: 2px solid #906b56; /* --raw-umber */
307
+ outline-offset: 1px;
308
+ background: linear-gradient(
309
+ 135deg,
310
+ #4d5963 0% 20%,
311
+ #f79103 20% 40%,
312
+ #376170 40% 60%,
313
+ #906b56 60% 80%,
314
+ #1b2f36 80% 100%
315
+ ); /* --gradient */
316
+ }
317
+
318
+ .table-container {
319
+ background: var(--bg-color);
320
+ border-radius: 12px;
321
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
322
+ overflow: hidden;
323
+ }
324
+
325
+ table {
326
+ width: 100%;
327
+ border-collapse: collapse;
328
+ }
329
+
330
+ th {
331
+ background: var(--bg-hover);
332
+ padding: 1rem;
333
+ text-align: left;
334
+ font-weight: 600;
335
+ color: var(--text-secondary);
336
+ text-transform: uppercase;
337
+ font-size: 0.875rem;
338
+ letter-spacing: 0.025em;
339
+ border-bottom: 2px solid var(--border-color);
340
+ }
341
+
342
+ th:has(.sort-header) {
343
+ padding: 0;
344
+ }
345
+
346
+ .sort-header {
347
+ width: 100%;
348
+ padding: 1rem;
349
+ background: transparent;
350
+ border: none;
351
+ text-align: left;
352
+ font-weight: 600;
353
+ color: var(--text-secondary);
354
+ text-transform: uppercase;
355
+ font-size: 0.875rem;
356
+ letter-spacing: 0.025em;
357
+ cursor: pointer;
358
+ transition:
359
+ background-color 0.2s,
360
+ color 0.2s;
361
+ white-space: nowrap;
362
+ }
363
+
364
+ .sort-header:hover {
365
+ background-color: var(--border-color);
366
+ color: var(--text-color);
367
+ }
368
+
369
+ .sort-header:focus {
370
+ outline: none;
371
+ background-color: var(--primary-color);
372
+ color: white;
373
+ }
374
+
375
+ td {
376
+ padding: 1rem;
377
+ border-bottom: 1px solid var(--border-color);
378
+ vertical-align: top;
379
+ }
380
+
381
+ tbody tr:hover {
382
+ background-color: var(--bg-hover);
383
+ }
384
+
385
+ tbody tr:last-child td {
386
+ border-bottom: none;
387
+ }
388
+
389
+ .status-badge {
390
+ display: inline-block;
391
+ padding: 0.375rem 0.875rem;
392
+ border-radius: 20px;
393
+ font-size: 0.875rem;
394
+ font-weight: 500;
395
+ text-transform: capitalize;
396
+ }
397
+
398
+ .status-reading {
399
+ background-color: #dbeafe;
400
+ color: #1d4ed8;
401
+ }
402
+
403
+ .status-finished {
404
+ background-color: #d1fae5;
405
+ color: #065f46;
406
+ }
407
+
408
+ .status-ah.naw,
409
+ .status-ah-naw {
410
+ background-color: #fee2e2;
411
+ color: #991b1b;
412
+ }
413
+
414
+ .status-not.started,
415
+ .status-not-started {
416
+ background-color: #f3f4f6;
417
+ color: #374151;
418
+ }
419
+
420
+ .status-maybe.later,
421
+ .status-maybe-later {
422
+ background-color: #fef3c7;
423
+ color: #92400e;
424
+ }
425
+
426
+ .status-on.deck,
427
+ .status-on-deck {
428
+ background-color: #f3e8ff;
429
+ color: #7c3aed;
430
+ }
431
+
432
+ .book-title {
433
+ font-weight: 600;
434
+ color: var(--text-color);
435
+ margin-bottom: 0.25rem;
436
+ text-transform: capitalize;
437
+ }
438
+
439
+ .book-author {
440
+ color: var(--text-secondary);
441
+ font-size: 0.9rem;
442
+ text-transform: capitalize;
443
+ }
444
+
445
+ .series-badge {
446
+ background-color: var(--bg-hover);
447
+ color: var(--text-secondary);
448
+ padding: 0.25rem 0.5rem;
449
+ border-radius: 4px;
450
+ font-size: 0.875rem;
451
+ font-style: italic;
452
+ }
453
+
454
+ .year-badge {
455
+ background-color: var(--primary-color);
456
+ color: white;
457
+ padding: 0.25rem 0.5rem;
458
+ border-radius: 4px;
459
+ font-size: 0.875rem;
460
+ font-weight: 500;
461
+ }
462
+
463
+ .grade-badge {
464
+ display: inline-block;
465
+ padding: 0.375rem 0.75rem;
466
+ border-radius: 20px;
467
+ font-size: 0.875rem;
468
+ font-weight: 600;
469
+ text-align: center;
470
+ min-width: 2rem;
471
+ text-transform: uppercase;
472
+ }
473
+
474
+ .grade-a {
475
+ background-color: #d1fae5;
476
+ color: #065f46;
477
+ }
478
+
479
+ .grade-b {
480
+ background-color: #dbeafe;
481
+ color: #1e40af;
482
+ }
483
+
484
+ .grade-c {
485
+ background-color: #fef3c7;
486
+ color: #92400e;
487
+ }
488
+
489
+ .grade-d {
490
+ background-color: #fed7aa;
491
+ color: #c2410c;
492
+ }
493
+
494
+ .grade-f {
495
+ background-color: #fee2e2;
496
+ color: #991b1b;
497
+ }
498
+
499
+ .completion-date {
500
+ color: var(--text-secondary);
501
+ font-size: 0.9rem;
502
+ }
503
+
504
+ .not-applicable {
505
+ color: #9ca3af;
506
+ font-style: italic;
507
+ font-size: 0.875rem;
508
+ }
509
+
510
+ .notes-cell {
511
+ white-space: pre-line;
512
+ }
513
+
514
+ .no-results {
515
+ text-align: center;
516
+ padding: 3rem;
517
+ color: var(--text-secondary);
518
+ }
519
+
520
+ @media (max-width: 768px) {
521
+ .container {
522
+ padding: 1rem;
523
+ }
524
+
525
+ h1 {
526
+ font-size: 2rem;
527
+ }
528
+
529
+ .filters {
530
+ grid-template-columns: 1fr;
531
+ }
532
+
533
+ .table-container {
534
+ overflow-x: auto;
535
+ }
536
+
537
+ table {
538
+ min-width: 800px;
539
+ }
540
+ }
541
+ `}};o([i({attribute:`api-endpoint`})],s.prototype,`apiEndpoint`,void 0),o([a()],s.prototype,`books`,void 0),o([a()],s.prototype,`filteredBooks`,void 0),o([a()],s.prototype,`loading`,void 0),o([a()],s.prototype,`error`,void 0),o([a()],s.prototype,`searchTerm`,void 0),o([a()],s.prototype,`statusFilter`,void 0),o([a()],s.prototype,`seriesFilter`,void 0),o([a()],s.prototype,`yearFilter`,void 0),o([a()],s.prototype,`gradeFilter`,void 0),o([a()],s.prototype,`sortColumn`,void 0),o([a()],s.prototype,`sortDirection`,void 0),s=o([r(`reading-list`)],s);
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Reading List Component</title>
7
+ <script type="module" crossorigin src="/assets/index.js"></script>
8
+ </head>
9
+ <body>
10
+ <reading-list></reading-list>
11
+ </body>
12
+ </html>
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@jschofield/reading-list",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "main": "./dist/assets/index.js",
6
+ "exports": {
7
+ ".": "./dist/assets/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "description": "Reading list web component built with Lit",
13
+ "author": "Jim Schofield",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/JimSchofield/jschof.dev",
18
+ "directory": "component/reading-list"
19
+ },
20
+ "dependencies": {
21
+ "google-logging-utils": "^1.1.1"
22
+ },
23
+ "peerDependencies": {
24
+ "lit": "^3.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^6.0.2",
28
+ "vite": "^8.0.3"
29
+ },
30
+ "scripts": {
31
+ "dev": "vite",
32
+ "build": "tsc && vite build",
33
+ "preview": "vite preview"
34
+ }
35
+ }