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