timconspicuous.neocities.org

Initial commit reading progress widget

+176
+176
src/scripts/reading-progress.js
··· 1 + class ReadingProgressWidget { 2 + constructor(containerId) { 3 + const element = document.getElementById(containerId); 4 + if (!element) { 5 + throw new Error(`Element with id "${containerId}" not found`); 6 + } 7 + this.container = element; 8 + this.currentBook = null; 9 + this.init(); 10 + } 11 + 12 + async init() { 13 + this.showLoading(); 14 + try { 15 + await this.fetchReadingProgress(); 16 + this.render(); 17 + } catch (error) { 18 + this.showError( 19 + error instanceof Error ? error.message : "Unknown error", 20 + ); 21 + } 22 + } 23 + 24 + async fetchReadingProgress() { 25 + const response = await fetch( 26 + "https://pds.timtinkers.online/xrpc/com.atproto.repo.listRecords?repo=did%3Aplc%3Ao6xucog6fghiyrvp7pyqxcs3&collection=social.popfeed.feed.listItem", 27 + ); 28 + 29 + if (!response.ok) { 30 + throw new Error(`API request failed: ${response.status}`); 31 + } 32 + 33 + const data = await response.json(); 34 + 35 + // Filter for entries with bookProgress field 36 + const booksWithProgress = data.records.filter( 37 + (record) => 38 + record.value.bookProgress && 39 + record.value.bookProgress.updatedAt, 40 + ); 41 + 42 + if (booksWithProgress.length === 0) { 43 + this.currentBook = null; 44 + return; 45 + } 46 + 47 + // Find the most recently updated book 48 + const mostRecent = booksWithProgress.reduce((latest, current) => { 49 + const latestDate = new Date(latest.value.updatedAt); 50 + const currentDate = new Date(current.value.updatedAt); 51 + return currentDate > latestDate ? current : latest; 52 + }); 53 + 54 + this.currentBook = { 55 + isbn13: mostRecent.value.identifiers.isbn13, 56 + progress: mostRecent.value.bookProgress.percent, 57 + updatedAt: mostRecent.value.bookProgress.updatedAt, 58 + }; 59 + 60 + await this.fetchMetadata(); 61 + } 62 + 63 + async fetchMetadata() { 64 + const response = await fetch( 65 + `https://openlibrary.org/api/books?bibkeys=ISBN:${this.currentBook.isbn13}&format=json&jscmd=data`, 66 + ); 67 + 68 + if (!response.ok) { 69 + throw new Error(`API request failed: ${response.status}`); 70 + } 71 + 72 + const data = await response.json(); 73 + const metadata = Object.values(data)[0]; 74 + 75 + this.currentBook = { 76 + isbn13: this.currentBook.isbn13, 77 + progress: this.currentBook.progress, 78 + updatedAt: this.currentBook.updatedAt, 79 + title: metadata.title, 80 + author: metadata.authors[0]?.name, 81 + coverUrl: metadata.cover?.medium, 82 + }; 83 + } 84 + 85 + showLoading() { 86 + this.container.innerHTML = ` 87 + <div class="reading-progress loading"> 88 + <div class="progress-skeleton"> 89 + <div class="skeleton-text"></div> 90 + <div class="skeleton-bar"></div> 91 + </div> 92 + </div> 93 + `; 94 + } 95 + 96 + showError(message) { 97 + this.container.innerHTML = ` 98 + <div class="reading-progress error"> 99 + <p>📚 Unable to load current reading progress</p> 100 + <small>${message}</small> 101 + </div> 102 + `; 103 + } 104 + 105 + formatDate(dateString) { 106 + return new Date(dateString).toLocaleDateString("en-US", { 107 + month: "short", 108 + day: "numeric", 109 + year: "numeric", 110 + }); 111 + } 112 + 113 + render() { 114 + if (!this.currentBook) { 115 + this.container.innerHTML = ` 116 + <div class="reading-progress empty"> 117 + <p>📚 No books currently in progress</p> 118 + </div> 119 + `; 120 + return; 121 + } 122 + 123 + const coverImage = this.currentBook.coverUrl 124 + ? `<img src="${this.currentBook.coverUrl}" alt="Book cover for ${ 125 + this.escapeHtml(this.currentBook.title) 126 + }" class="book-cover" />` 127 + : '<div class="book-cover-placeholder">📖</div>'; 128 + 129 + this.container.innerHTML = ` 130 + <div class="reading-progress"> 131 + <h3>📚 Currently Reading</h3> 132 + <div class="book-info"> 133 + <div class="book-header"> 134 + ${coverImage} 135 + <div class="book-text"> 136 + <div class="book-title"> 137 + ${this.escapeHtml(this.currentBook.title)}</div> 138 + <div class="book-author"> 139 + by ${this.escapeHtml(this.currentBook.author)}</div> 140 + </div> 141 + </div> 142 + <div class="progress-container"> 143 + <div class="progress-bar"> 144 + <div 145 + class="progress-fill" 146 + style="width: ${this.currentBook.progress}%" 147 + ></div> 148 + </div> 149 + <div class="progress-text"> 150 + ${this.currentBook.progress}% complete 151 + </div> 152 + </div> 153 + <div class="last-updated"> 154 + Updated ${this.formatDate(this.currentBook.updatedAt)} 155 + </div> 156 + </div> 157 + </div> 158 + `; 159 + } 160 + 161 + escapeHtml(text) { 162 + const div = document.createElement("div"); 163 + div.textContent = text; 164 + return div.innerHTML; 165 + } 166 + } 167 + 168 + // Auto-initialize when DOM is loaded 169 + document.addEventListener("DOMContentLoaded", () => { 170 + const widgets = document.querySelectorAll("[data-reading-progress]"); 171 + widgets.forEach((widget) => { 172 + if (widget.id) { 173 + new ReadingProgressWidget(widget.id); 174 + } 175 + }); 176 + });