dat-lequoc commited on
Commit
82306ea
·
verified ·
1 Parent(s): c8c0821

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +1310 -19
index.html CHANGED
@@ -1,19 +1,1310 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>A Year of Reading</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .add-book-btn {
10
+ position: fixed;
11
+ bottom: 2rem;
12
+ right: 2rem;
13
+ width: 3rem;
14
+ height: 3rem;
15
+ background: #3b82f6;
16
+ color: white;
17
+ border-radius: 50%;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ font-size: 1.5rem;
22
+ cursor: pointer;
23
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
24
+ transition: transform 0.2s;
25
+ z-index: 50;
26
+ }
27
+ .add-book-btn:hover {
28
+ transform: scale(1.1);
29
+ }
30
+ .heatmap-cell {
31
+ height: clamp(48px, 10vw, 64px);
32
+ border: 1px solid #e5e7eb;
33
+ cursor: pointer;
34
+ border-radius: 0.375rem;
35
+ transition: all 0.2s;
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ justify-content: center;
40
+ padding: 0.25rem;
41
+ width: 100%;
42
+ min-width: 0;
43
+ }
44
+ .heatmap-cell:hover {
45
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
46
+ }
47
+ .heatmap-row {
48
+ display: grid;
49
+ grid-template-columns: repeat(7, minmax(0, 1fr));
50
+ gap: 0.25rem;
51
+ flex: 1;
52
+ }
53
+ .book-label {
54
+ font-size: 0.875rem;
55
+ font-weight: 600;
56
+ white-space: nowrap;
57
+ overflow: hidden;
58
+ text-overflow: ellipsis;
59
+ padding: 0.375rem;
60
+ color: #374151;
61
+ background-color: #f3f4f6;
62
+ border-radius: 0.375rem;
63
+ border: 1px solid #e5e7eb;
64
+ display: flex;
65
+ align-items: center;
66
+ height: 100%;
67
+ min-width: 80px;
68
+ max-width: 100px;
69
+ }
70
+ .completed-book {
71
+ border-left: 4px solid #10B981;
72
+ }
73
+ .book-content {
74
+ transition: max-height 0.3s ease-out, opacity 0.2s ease-out;
75
+ overflow: hidden;
76
+ opacity: 1;
77
+ }
78
+ .book-content.collapsed {
79
+ max-height: 0;
80
+ opacity: 0;
81
+ margin: 0;
82
+ }
83
+ .completed-indicator {
84
+ background-color: #10B981;
85
+ color: white;
86
+ padding: 2px 8px;
87
+ border-radius: 9999px;
88
+ font-size: 0.75rem;
89
+ margin-left: auto;
90
+ }
91
+ .in-progress-indicator {
92
+ background-color: #3B82F6;
93
+ color: white;
94
+ padding: 2px 8px;
95
+ border-radius: 9999px;
96
+ font-size: 0.75rem;
97
+ margin-left: auto;
98
+ }
99
+ .heatmap-book-row {
100
+ display: grid;
101
+ grid-template-columns: minmax(80px, 100px) 1fr;
102
+ gap: 0.25rem;
103
+ margin: 0.25rem 0;
104
+ width: 100%;
105
+ }
106
+ </style>
107
+ </head>
108
+ <body class="bg-gray-100 p-6 font-sans">
109
+ <header class="mb-6 flex items-center justify-between">
110
+ <h1 class="text-xl font-bold">A Year of Reading</h1>
111
+ </header>
112
+ <main class="grid gap-6">
113
+ <div
114
+ id="add-book-modal"
115
+ class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center hidden z-50"
116
+ >
117
+ <div class="bg-white p-6 rounded-lg shadow-xl max-w-md w-full">
118
+ <div class="flex justify-between items-center mb-4">
119
+ <h2 class="text-lg font-semibold">Add New Book</h2>
120
+ <button type="button" class="modal-close text-gray-500 hover:text-gray-700">×</button>
121
+ </div>
122
+ <form id="add-book-form" class="space-y-3">
123
+ <div>
124
+ <label for="book-title" class="block text-sm font-medium text-gray-700">Title</label>
125
+ <input
126
+ type="text"
127
+ id="book-title"
128
+ required
129
+ class="mt-1 p-2 w-full border rounded focus:ring-blue-500 focus:border-blue-500"
130
+ />
131
+ </div>
132
+ <div>
133
+ <label for="total-pages" class="block text-sm font-medium text-gray-700">Total Pages</label>
134
+ <input
135
+ type="number"
136
+ id="total-pages"
137
+ required
138
+ class="mt-1 p-2 w-full border rounded focus:ring-blue-500 focus:border-blue-500"
139
+ />
140
+ </div>
141
+ <fieldset class="border p-3 rounded">
142
+ <legend class="text-sm font-medium">Goal</legend>
143
+ <div class="space-y-1">
144
+ <div>
145
+ <input type="radio" id="goal-date" name="goal-type" value="date" class="mr-1" />
146
+ <label for="goal-date" class="text-sm">Target Date</label>
147
+ <input
148
+ type="date"
149
+ id="target-date"
150
+ class="mt-1 ml-2 p-1.5 border rounded text-sm"
151
+ disabled
152
+ />
153
+ </div>
154
+ <div>
155
+ <input type="radio" id="goal-pages" name="goal-type" value="pages" class="mr-1" />
156
+ <label for="goal-pages" class="text-sm">Pages/Day</label>
157
+ <input
158
+ type="number"
159
+ id="pages-per-day"
160
+ class="mt-1 ml-2 p-1.5 border rounded text-sm"
161
+ disabled
162
+ />
163
+ </div>
164
+ </div>
165
+ </fieldset>
166
+ <div>
167
+ <label for="start-date" class="block text-sm font-medium text-gray-700">Start Date</label>
168
+ <input
169
+ type="date"
170
+ id="start-date"
171
+ required
172
+ class="mt-1 p-2 w-full border rounded focus:ring-blue-500 focus:border-blue-500"
173
+ />
174
+ </div>
175
+ <button
176
+ type="submit"
177
+ class="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
178
+ >
179
+ Add Book
180
+ </button>
181
+ </form>
182
+ </div>
183
+ </div>
184
+ <button type="button" id="add-book-btn" class="add-book-btn">+</button>
185
+ <section id="book-list" class="bg-white p-4 rounded shadow">
186
+ <h2 class="text-lg font-semibold mb-3">My Books</h2>
187
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="book-entries"></div>
188
+ </section>
189
+ </main>
190
+ <section id="stats-history" class="bg-white p-4 rounded shadow mt-6 md:mt-0">
191
+ <div id="weekly-heatmap" class="mb-6">
192
+ <h2 class="text-lg font-bold mb-4">This Week</h2>
193
+ <div id="heatmap-container"></div>
194
+ </div>
195
+ </section>
196
+
197
+ <!-- New Reading Challenge Section -->
198
+ <section id="reading-challenge" class="bg-white p-4 rounded shadow mt-6">
199
+ <div class="flex justify-between items-center mb-4">
200
+ <h2 class="text-lg font-bold">Reading Challenge <span id="challenge-year"></span></h2>
201
+ <button
202
+ id="edit-goal-btn"
203
+ class="text-blue-500 hover:text-blue-700 flex items-center gap-1"
204
+ >
205
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
206
+ <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
207
+ </svg>
208
+ Edit Goal
209
+ </button>
210
+ </div>
211
+ <div class="space-y-4">
212
+ <div>
213
+ <div class="flex justify-between items-center text-sm mb-2">
214
+ <span>Books Progress</span>
215
+ <span><span id="completed-books" class="font-medium">0</span>/<span id="yearly-goal" class="font-medium">52</span> books</span>
216
+ </div>
217
+ <div class="relative h-4 rounded-full bg-gray-200 overflow-hidden">
218
+ <div id="challenge-progress" class="absolute h-full bg-green-500 transition-all duration-500"></div>
219
+ </div>
220
+ </div>
221
+ <div>
222
+ <div class="flex justify-between items-center text-sm mb-2">
223
+ <span>Year Progress</span>
224
+ <span>Day <span id="current-day" class="font-medium">0</span>/365</span>
225
+ </div>
226
+ <div class="relative h-4 rounded-full bg-gray-200 overflow-hidden">
227
+ <div id="year-progress" class="absolute h-full bg-blue-500 transition-all duration-500"></div>
228
+ </div>
229
+ </div>
230
+ <div class="flex justify-between items-center text-sm pt-2">
231
+ <span>Week: <span id="current-week" class="font-medium">0</span>/52</span>
232
+ <span>On Track: <span id="on-track-status" class="font-medium">Yes</span></span>
233
+ </div>
234
+ </div>
235
+ </section>
236
+
237
+ <!-- New Goal Edit Modal -->
238
+ <div
239
+ id="edit-goal-modal"
240
+ class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center hidden z-50"
241
+ >
242
+ <div class="bg-white p-6 rounded-lg shadow-xl max-w-sm w-full">
243
+ <div class="flex justify-between items-center mb-4">
244
+ <h3 class="text-lg font-semibold">Edit Reading Goal</h3>
245
+ <button type="button" class="modal-close text-gray-500 hover:text-gray-700">×</button>
246
+ </div>
247
+ <form id="edit-goal-form" class="space-y-4">
248
+ <div>
249
+ <label for="yearly-goal-input" class="block text-sm font-medium text-gray-700">Books to read in <span id="current-year"></span></label>
250
+ <input
251
+ type="number"
252
+ id="yearly-goal-input"
253
+ min="1"
254
+ class="mt-1 p-2 w-full border rounded focus:ring-blue-500 focus:border-blue-500"
255
+ />
256
+ </div>
257
+ <button
258
+ type="submit"
259
+ class="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
260
+ >
261
+ Save Goal
262
+ </button>
263
+ </form>
264
+ </div>
265
+ </div>
266
+ <div
267
+ id="edit-history-modal"
268
+ class="fixed z-10 inset-0 overflow-y-auto hidden"
269
+ aria-labelledby="modal-title"
270
+ role="dialog"
271
+ aria-modal="true"
272
+ >
273
+ <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
274
+ <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
275
+ <span class="hidden sm:inline-block sm:align-middle sm:h-screen"></span>
276
+ <div
277
+ class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
278
+ >
279
+ <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
280
+ <div class="flex justify-between items-center mb-4">
281
+ <h3 class="text-lg leading-6 font-medium text-gray-900">
282
+ Edit Reading History
283
+ </h3>
284
+ <button type="button" class="modal-close text-gray-500 hover:text-gray-700">×</button>
285
+ </div>
286
+ <div class="mt-2">
287
+ <p class="text-sm text-gray-500">
288
+ Modify pages read for <span id="edit-date"></span> for book
289
+ <span id="edit-history-book-title"></span>.
290
+ </p>
291
+ <input
292
+ type="number"
293
+ id="edit-pages-read"
294
+ class="mt-2 p-2 border rounded w-full focus:ring-blue-500 focus:border-blue-500"
295
+ />
296
+ </div>
297
+ </div>
298
+ <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
299
+ <button
300
+ type="button"
301
+ id="save-edit-history"
302
+ class="ml-3 w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
303
+ >
304
+ Save
305
+ </button>
306
+ <button
307
+ type="button"
308
+ class="modal-close mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
309
+ >
310
+ Cancel
311
+ </button>
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </div>
316
+ <div id="data-management" class="bg-white p-4 rounded shadow mt-6">
317
+ <h2 class="text-lg font-semibold mb-3">Data Management</h2>
318
+ <div class="flex space-x-2 items-center">
319
+ <button
320
+ type="button"
321
+ id="export-data"
322
+ class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 text-sm"
323
+ >
324
+ Export Data
325
+ </button>
326
+ <input type="file" id="import-data" accept=".json" class="hidden" />
327
+ <button
328
+ type="button"
329
+ id="import-data-button"
330
+ class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 text-sm"
331
+ >
332
+ Import Data
333
+ </button>
334
+ </div>
335
+ </div>
336
+
337
+ <!-- Moved Streaks and Statistics Section -->
338
+ <section class="grid grid-cols-2 gap-4 text-sm bg-white p-4 rounded shadow mt-6">
339
+ <div id="streaks">
340
+ <h3 class="font-semibold mb-1">Streaks</h3>
341
+ <p>Current: <span id="current-streak" class="font-medium">0</span> days</p>
342
+ <p>Longest: <span id="longest-streak" class="font-medium">0</span> days</p>
343
+ <p>Weekly Completion: <span id="weekly-completion" class="font-medium">0%</span></p>
344
+ </div>
345
+ <div id="historical-stats">
346
+ <h3 class="font-semibold mb-1">Statistics</h3>
347
+ <p>Avg Pages/Day: <span id="avg-pages" class="font-medium">0</span></p>
348
+ <p>Books Completed: <span id="total-books-completed" class="font-medium">0</span></p>
349
+ <p>Total Pages Read: <span id="total-pages-read" class="font-medium">0</span></p>
350
+ <p>Best Reading Day: <span id="best-day" class="font-medium"></span></p>
351
+ </div>
352
+ </section>
353
+
354
+ <div
355
+ id="edit-book-modal"
356
+ class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center hidden z-50"
357
+ >
358
+ <div class="bg-white p-6 rounded-lg shadow-xl max-w-md w-full">
359
+ <div class="flex justify-between items-center mb-4">
360
+ <h2 class="text-lg font-semibold">Edit Book</h2>
361
+ <button
362
+ type="button"
363
+ onclick="closeEditBookModal()"
364
+ class="text-gray-500 hover:text-gray-700"
365
+ >
366
+ ×
367
+ </button>
368
+ </div>
369
+ <form id="edit-book-form" class="space-y-3">
370
+ <input type="hidden" id="edit-book-index" />
371
+ <div>
372
+ <label for="edit-book-title" class="block text-sm font-medium text-gray-700">Title</label>
373
+ <input
374
+ type="text"
375
+ id="edit-book-title"
376
+ required
377
+ class="mt-1 p-2 w-full border rounded focus:ring-blue-500 focus:border-blue-500"
378
+ />
379
+ </div>
380
+ <div>
381
+ <label for="edit-total-pages" class="block text-sm font-medium text-gray-700">Total Pages</label>
382
+ <input
383
+ type="number"
384
+ id="edit-total-pages"
385
+ required
386
+ class="mt-1 p-2 w-full border rounded focus:ring-blue-500 focus:border-blue-500"
387
+ />
388
+ </div>
389
+ <fieldset class="border p-3 rounded">
390
+ <legend class="text-sm font-medium">Goal</legend>
391
+ <div class="space-y-1">
392
+ <div>
393
+ <input type="radio" id="edit-goal-date" name="edit-goal-type" value="date" class="mr-1" />
394
+ <label for="edit-goal-date" class="text-sm">Target Date</label>
395
+ <input
396
+ type="date"
397
+ id="edit-target-date"
398
+ class="mt-1 ml-2 p-1.5 border rounded text-sm"
399
+ disabled
400
+ />
401
+ </div>
402
+ <div>
403
+ <input
404
+ type="radio"
405
+ id="edit-goal-pages"
406
+ name="edit-goal-type"
407
+ value="pages"
408
+ class="mr-1"
409
+ />
410
+ <label for="edit-goal-pages" class="text-sm">Pages/Day</label>
411
+ <input
412
+ type="number"
413
+ id="edit-pages-per-day"
414
+ class="mt-1 ml-2 p-1.5 border rounded text-sm"
415
+ disabled
416
+ />
417
+ </div>
418
+ </div>
419
+ </fieldset>
420
+ <div>
421
+ <label for="edit-start-date" class="block text-sm font-medium text-gray-700">Start Date</label>
422
+ <input
423
+ type="date"
424
+ id="edit-start-date"
425
+ required
426
+ class="mt-1 p-2 w-full border rounded focus:ring-blue-500 focus:border-blue-500"
427
+ />
428
+ </div>
429
+ <div class="flex justify-between space-x-2">
430
+ <button
431
+ type="submit"
432
+ class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
433
+ >
434
+ Save Changes
435
+ </button>
436
+ <button
437
+ type="button"
438
+ onclick="bookManager.handleDeleteBook()"
439
+ class="bg-red-500 text-white py-2 px-4 rounded hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
440
+ >
441
+ Delete Book
442
+ </button>
443
+ </div>
444
+ </form>
445
+ </div>
446
+ </div>
447
+ <script>
448
+ const LOCAL_STORAGE_KEY = 'readingTrackerBooks';
449
+ const dayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
450
+ const dayIcons = ["☀️", "🌙", "⚡", "💧", "🔥", "🌟", "🌍"];
451
+
452
+ const $ = (id) => document.getElementById(id);
453
+ const formatDate = (date) => {
454
+ const d = new Date(date);
455
+ const year = d.getFullYear();
456
+ const month = String(d.getMonth() + 1).padStart(2, "0");
457
+ const day = String(d.getDate()).padStart(2, "0");
458
+ return `${year}-${month}-${day}`;
459
+ };
460
+
461
+ const Utils = {
462
+ formatDate,
463
+ getDaysBetween(start, end) {
464
+ const startDate = new Date(start);
465
+ const endDate = new Date(end);
466
+ const oneDay = 24 * 60 * 60 * 1000;
467
+ return Math.round(Math.abs((startDate - endDate) / oneDay));
468
+ },
469
+ calculateProgressPercentage(currentPage, totalPages) {
470
+ return totalPages > 0 ? Math.floor((currentPage / totalPages) * 100) : 0;
471
+ },
472
+ calculateDailyGoal(remainingPages, remainingDays) {
473
+ return remainingDays > 0 ? Math.ceil(remainingPages / remainingDays) : remainingPages;
474
+ }
475
+ };
476
+
477
+ class Book {
478
+ constructor(data) {
479
+ this.title = data.title;
480
+ this.totalPages = data.totalPages;
481
+ this.currentPage = data.currentPage || 0;
482
+ this.startDate = data.startDate;
483
+ this.goalType = data.goalType;
484
+ this.targetDate = data.targetDate;
485
+ this.pagesPerDay = data.pagesPerDay;
486
+ this.readingHistory = data.readingHistory || [];
487
+ this.lockedDailyGoals = data.lockedDailyGoals || {};
488
+ }
489
+
490
+ updateProgress(currentPageTotal, date) {
491
+ if (currentPageTotal < 0 || currentPageTotal > this.totalPages) {
492
+ throw new Error("Invalid page number");
493
+ }
494
+ const existingEntryIndex = this.readingHistory.findIndex((item) => item.date === date);
495
+ const sortedHistory = [...this.readingHistory].sort((a, b) => new Date(b.date) - new Date(a.date));
496
+ const previousTotal = sortedHistory.find((entry) => entry.date < date)?.totalPages || 0;
497
+ const pagesReadToday = currentPageTotal - previousTotal;
498
+
499
+ if (existingEntryIndex > -1) {
500
+ if (currentPageTotal === 0) {
501
+ this.readingHistory.splice(existingEntryIndex, 1);
502
+ } else {
503
+ this.readingHistory[existingEntryIndex] = {
504
+ date,
505
+ pagesRead: Math.max(0, pagesReadToday),
506
+ totalPages: currentPageTotal
507
+ };
508
+ }
509
+ } else if (currentPageTotal > 0) {
510
+ this.readingHistory.push({
511
+ date,
512
+ pagesRead: Math.max(0, pagesReadToday),
513
+ totalPages: currentPageTotal
514
+ });
515
+ }
516
+ this.currentPage = currentPageTotal;
517
+ }
518
+
519
+ getProgress() {
520
+ const percentage = Utils.calculateProgressPercentage(this.currentPage, this.totalPages);
521
+ return {
522
+ percentage,
523
+ isCompleted: this.currentPage === this.totalPages,
524
+ currentPage: this.currentPage,
525
+ totalPages: this.totalPages
526
+ };
527
+ }
528
+
529
+ calculateDailyGoalFromYesterday(today) {
530
+ const yesterday = new Date(today);
531
+ yesterday.setDate(yesterday.getDate() - 1);
532
+ const yesterdayStr = formatDate(yesterday);
533
+
534
+ // Get pages read up to yesterday
535
+ const sortedHistory = [...this.readingHistory].sort((a, b) => new Date(a.date) - new Date(b.date));
536
+ const pagesAsOfYesterday = sortedHistory.find(entry => entry.date <= yesterdayStr)?.totalPages || 0;
537
+
538
+ // Calculate remaining pages from yesterday's progress
539
+ const pagesRemaining = this.totalPages - pagesAsOfYesterday;
540
+
541
+ if (this.goalType === "pages") return this.pagesPerDay;
542
+ if (this.goalType === "date" && this.targetDate) {
543
+ const remainingDays = Utils.getDaysBetween(today, this.targetDate);
544
+ return Utils.calculateDailyGoal(pagesRemaining, remainingDays);
545
+ }
546
+ return 0;
547
+ }
548
+
549
+ lockDailyGoalFor(dateStr) {
550
+ if (!this.lockedDailyGoals[dateStr]) {
551
+ this.lockedDailyGoals[dateStr] = this.calculateDailyGoalFromYesterday(dateStr);
552
+ }
553
+ return this.lockedDailyGoals[dateStr];
554
+ }
555
+
556
+ getDailyGoal(currentDate) {
557
+ const dateStr = formatDate(new Date(currentDate));
558
+ return this.lockDailyGoalFor(dateStr);
559
+ }
560
+ }
561
+
562
+ class BookManager {
563
+ constructor() {
564
+ this.books = [];
565
+ this.$bookEntries = $("book-entries");
566
+ this.yearlyGoal = parseInt(localStorage.getItem('yearlyReadingGoal')) || 52;
567
+ this.loadBooks();
568
+ }
569
+
570
+ init() {
571
+ this.setupEventListeners();
572
+ this.updateUI();
573
+ this.updateReadingChallenge();
574
+ document.getElementById('challenge-year').textContent = new Date().getFullYear();
575
+ }
576
+
577
+ loadBooks() {
578
+ try {
579
+ const savedBooks = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || [];
580
+ this.books = savedBooks.map((b) => new Book(b));
581
+ } catch {
582
+ this.books = [];
583
+ }
584
+ }
585
+
586
+ saveBooks() {
587
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.books));
588
+ }
589
+
590
+ refreshUI() {
591
+ this.saveBooks();
592
+ this.updateUI();
593
+ }
594
+
595
+ addBook(bookData) {
596
+ this.books.push(new Book(bookData));
597
+ this.refreshUI();
598
+ }
599
+
600
+ updateBook(index, bookData) {
601
+ const original = this.books[index];
602
+ // Create new book with cleared lockedDailyGoals if goal-related fields changed
603
+ const shouldClearGoals = original.goalType !== bookData.goalType ||
604
+ original.targetDate !== bookData.targetDate ||
605
+ original.pagesPerDay !== bookData.pagesPerDay;
606
+ this.books[index] = new Book({
607
+ ...original,
608
+ ...bookData,
609
+ readingHistory: original.readingHistory,
610
+ lockedDailyGoals: shouldClearGoals ? {} : original.lockedDailyGoals
611
+ });
612
+ this.refreshUI();
613
+ }
614
+
615
+ deleteBook(index) {
616
+ this.books.splice(index, 1);
617
+ this.refreshUI();
618
+ }
619
+
620
+ updateProgress(bookIndex, currentPageTotal, date) {
621
+ try {
622
+ this.books[bookIndex].updateProgress(currentPageTotal, date);
623
+ this.refreshUI();
624
+ } catch (error) {
625
+ alert(error.message);
626
+ }
627
+ }
628
+
629
+ updateUI() {
630
+ this.displayBooks();
631
+ this.renderReadingHistoryHeatmap();
632
+ this.calculateStats();
633
+ this.updateReadingChallenge();
634
+ }
635
+
636
+ displayBooks() {
637
+ if (!this.$bookEntries) return;
638
+ if (this.books.length === 0) {
639
+ this.$bookEntries.innerHTML = `
640
+ <div class="text-center text-gray-500 py-4 col-span-2">
641
+ No books added yet. Click the + button to add your first book!
642
+ </div>
643
+ `;
644
+ return;
645
+ }
646
+ const today = formatDate(new Date());
647
+ try {
648
+ // Sort books: completed first, then by title, but keep track of original indices
649
+ const booksWithIndices = this.books.map((book, index) => ({ book, originalIndex: index }));
650
+ const sortedBooks = booksWithIndices.sort((a, b) => {
651
+ const aCompleted = a.book.currentPage === a.book.totalPages;
652
+ const bCompleted = b.book.currentPage === b.book.totalPages;
653
+ if (aCompleted === bCompleted) {
654
+ return a.book.title.localeCompare(b.book.title);
655
+ }
656
+ return bCompleted ? 1 : -1;
657
+ });
658
+
659
+ this.$bookEntries.innerHTML = sortedBooks
660
+ .map(({ book, originalIndex }) => {
661
+ const progress = book.getProgress();
662
+ const dailyGoal = book.getDailyGoal(today);
663
+ const todayReading =
664
+ book.readingHistory.find((item) => item.date === today)?.totalPages || book.currentPage;
665
+
666
+ // Get yesterday's progress for target calculation
667
+ const yesterday = new Date(today);
668
+ yesterday.setDate(yesterday.getDate() - 1);
669
+ const yesterdayStr = formatDate(yesterday);
670
+ const sortedHistory = [...book.readingHistory].sort((a, b) => new Date(a.date) - new Date(b.date));
671
+ const pagesAsOfYesterday = sortedHistory.find(entry => entry.date <= yesterdayStr)?.totalPages || 0;
672
+ const targetPage = Math.min(pagesAsOfYesterday + dailyGoal, book.totalPages);
673
+
674
+ return `
675
+ <div
676
+ class="bg-gray-50 p-3 rounded border ${progress.isCompleted ? "completed-book" : ""} relative"
677
+ data-book-index="${originalIndex}"
678
+ >
679
+ <div class="flex justify-between items-center cursor-pointer book-header" data-index="${originalIndex}">
680
+ <h4 class="text-lg font-semibold truncate pr-8">${book.title}</h4>
681
+ <div class="flex items-center gap-2 absolute right-3 top-3">
682
+ ${progress.isCompleted ? '<span class="completed-indicator">Completed</span>' :
683
+ (progress.currentPage > 0 ? '<span class="in-progress-indicator">In Progress</span>' : '')}
684
+ <button type="button" class="edit-book-btn text-blue-500 hover:text-blue-700" data-index="${originalIndex}">
685
+ <svg
686
+ xmlns="http://www.w3.org/2000/svg"
687
+ class="h-5 w-5"
688
+ viewBox="0 0 20 20"
689
+ fill="currentColor"
690
+ >
691
+ <path
692
+ d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"
693
+ />
694
+ </svg>
695
+ </button>
696
+ </div>
697
+ </div>
698
+ <div id="book-content-${originalIndex}" class="book-content ${progress.isCompleted ? 'collapsed' : ''} mt-4">
699
+ <div class="mt-2">
700
+ <div class="flex items-center justify-between mb-1">
701
+ <span class="text-sm">Progress: ${progress.percentage}%</span>
702
+ </div>
703
+ <div class="relative h-2 rounded bg-blue-200">
704
+ <div
705
+ style="width:${progress.percentage}%"
706
+ class="absolute h-full rounded bg-blue-500"
707
+ ></div>
708
+ </div>
709
+ <p class="text-sm mt-1">Read: ${progress.currentPage} / ${progress.totalPages} pages</p>
710
+ <div class="mt-1 p-2 bg-blue-50 border border-blue-200 rounded-md">
711
+ <p class="text-sm font-medium text-blue-800">
712
+ Today's Goal: Page ${targetPage}
713
+ </p>
714
+ </div>
715
+ <div class="mt-2 flex items-center space-x-2">
716
+ <input
717
+ type="number"
718
+ class="p-1.5 w-full border rounded text-sm focus:ring-blue-500 focus:border-blue-500"
719
+ value="${todayReading}"
720
+ min="0"
721
+ max="${book.totalPages}"
722
+ data-book-index="${originalIndex}"
723
+ />
724
+ <button
725
+ type="button"
726
+ class="bg-blue-500 text-white py-1.5 px-3 rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 text-xs update-progress-btn"
727
+ data-book-index="${originalIndex}"
728
+ >
729
+ Update
730
+ </button>
731
+ </div>
732
+ </div>
733
+ </div>
734
+ </div>
735
+ `;
736
+ })
737
+ .join("");
738
+ } catch {
739
+ this.$bookEntries.innerHTML = `
740
+ <div class="text-center text-red-500 py-4 col-span-2">
741
+ Error displaying books. Please try refreshing the page.
742
+ </div>
743
+ `;
744
+ }
745
+ }
746
+
747
+ setupEventListeners() {
748
+ if (this.$bookEntries) {
749
+ this.$bookEntries.addEventListener("click", (e) => {
750
+ const target = e.target;
751
+ const container = target.closest("[data-book-index]");
752
+ if (!container) return;
753
+ const bookIndex = container.dataset.bookIndex;
754
+
755
+ // Check if the click is on the edit button or its SVG child
756
+ const isEditButton = target.matches(".edit-book-btn, .edit-book-btn *, svg, path");
757
+ if (isEditButton) {
758
+ e.stopPropagation(); // Prevent the click from triggering the header click
759
+ this.openEditBookModal(bookIndex);
760
+ return;
761
+ }
762
+
763
+ if (target.matches(".book-header, .book-header *")) {
764
+ this.toggleBookContent(bookIndex);
765
+ } else if (target.matches(".update-progress-btn, .update-progress-btn *")) {
766
+ const input = container.querySelector("input[type='number']");
767
+ this.updateProgress(bookIndex, parseInt(input.value, 10), formatDate(new Date()));
768
+ }
769
+ });
770
+ }
771
+
772
+ const addBookForm = $("add-book-form");
773
+ if (addBookForm) {
774
+ addBookForm.addEventListener("submit", (e) => this.handleAddBook(e));
775
+ }
776
+
777
+ const editBookForm = $("edit-book-form");
778
+ if (editBookForm) {
779
+ editBookForm.addEventListener("submit", (e) => this.handleEditBook(e));
780
+ }
781
+
782
+ const addBookBtn = $("add-book-btn");
783
+ if (addBookBtn) {
784
+ addBookBtn.addEventListener("click", () => this.openAddBookModal());
785
+ }
786
+
787
+ document.querySelectorAll(".modal-close").forEach((btn) => {
788
+ btn.addEventListener("click", () => this.closeAllModals());
789
+ });
790
+
791
+ document.querySelectorAll('input[name="goal-type"]').forEach((radio) => {
792
+ radio.addEventListener("change", () => this.handleGoalTypeChange(radio));
793
+ });
794
+ document.querySelectorAll('input[name="edit-goal-type"]').forEach((radio) => {
795
+ radio.addEventListener("change", () => this.handleGoalTypeChange(radio, true));
796
+ });
797
+
798
+ const saveEditHistoryBtn = $("save-edit-history");
799
+ if (saveEditHistoryBtn) {
800
+ saveEditHistoryBtn.addEventListener("click", () => this.handleEditHistory());
801
+ }
802
+
803
+ const exportDataBtn = $("export-data");
804
+ if (exportDataBtn) {
805
+ exportDataBtn.addEventListener("click", () => this.exportData());
806
+ }
807
+
808
+ const importDataBtn = $("import-data-button");
809
+ const importDataInput = $("import-data");
810
+ if (importDataBtn && importDataInput) {
811
+ importDataBtn.addEventListener("click", () => importDataInput.click());
812
+ importDataInput.addEventListener("change", (e) => this.handleImport(e));
813
+ }
814
+
815
+ const editGoalBtn = $("edit-goal-btn");
816
+ if (editGoalBtn) {
817
+ editGoalBtn.addEventListener("click", () => this.openEditGoalModal());
818
+ }
819
+
820
+ const editGoalForm = $("edit-goal-form");
821
+ if (editGoalForm) {
822
+ editGoalForm.addEventListener("submit", (e) => this.handleEditGoal(e));
823
+ }
824
+ }
825
+
826
+ toggleBookContent(index) {
827
+ const content = $(`book-content-${index}`);
828
+ if (content) content.classList.toggle("collapsed");
829
+ }
830
+
831
+ openAddBookModal() {
832
+ $("add-book-modal").classList.remove("hidden");
833
+ $("start-date").value = formatDate(new Date());
834
+ }
835
+
836
+ closeAddBookModal() {
837
+ $("add-book-modal").classList.add("hidden");
838
+ $("add-book-form").reset();
839
+ }
840
+
841
+ openEditBookModal(bookIndex) {
842
+ const book = this.books[bookIndex];
843
+ $("edit-book-index").value = bookIndex;
844
+ $("edit-book-title").value = book.title;
845
+ $("edit-total-pages").value = book.totalPages;
846
+ $("edit-start-date").value = book.startDate;
847
+ $("edit-goal-date").checked = book.goalType === "date";
848
+ $("edit-goal-pages").checked = book.goalType === "pages";
849
+ $("edit-target-date").value = book.goalType === "date" ? book.targetDate : "";
850
+ $("edit-pages-per-day").value = book.goalType === "pages" ? book.pagesPerDay : "";
851
+ $("edit-target-date").disabled = book.goalType !== "date";
852
+ $("edit-pages-per-day").disabled = book.goalType !== "pages";
853
+ $("edit-book-modal").classList.remove("hidden");
854
+ }
855
+
856
+ closeEditBookModal() {
857
+ $("edit-book-modal").classList.add("hidden");
858
+ }
859
+
860
+ openEditModal(date, bookTitle) {
861
+ const book = this.books.find((b) => b.title === bookTitle);
862
+ if (!book) return;
863
+ const history = book.readingHistory.find((h) => h.date === date);
864
+ $("edit-date").textContent = date;
865
+ $("edit-history-book-title").textContent = bookTitle;
866
+ const sortedHistory = [...book.readingHistory].sort(
867
+ (a, b) => new Date(b.date) - new Date(a.date)
868
+ );
869
+ const currentEntry = history?.totalPages || sortedHistory.find((entry) => entry.date < date)?.totalPages || 0;
870
+ $("edit-pages-read").value = currentEntry;
871
+ $("edit-history-modal").classList.remove("hidden");
872
+ }
873
+
874
+ closeEditModal() {
875
+ $("edit-history-modal").classList.add("hidden");
876
+ }
877
+
878
+ closeAllModals() {
879
+ this.closeAddBookModal();
880
+ this.closeEditBookModal();
881
+ this.closeEditModal();
882
+ }
883
+
884
+ handleAddBook(event) {
885
+ event.preventDefault();
886
+ const formData = {
887
+ title: $("book-title").value,
888
+ totalPages: parseInt($("total-pages").value, 10),
889
+ startDate: $("start-date").value,
890
+ goalType: document.querySelector('input[name="goal-type"]:checked')?.value,
891
+ targetDate: $("target-date").value,
892
+ pagesPerDay: parseInt($("pages-per-day").value, 10)
893
+ };
894
+ if (!this.validateBookData(formData)) {
895
+ alert("Please fill in all required fields with valid values.");
896
+ return;
897
+ }
898
+ this.addBook(formData);
899
+ this.closeAddBookModal();
900
+ }
901
+
902
+ handleEditBook(event) {
903
+ event.preventDefault();
904
+ const bookIndex = parseInt($("edit-book-index").value, 10);
905
+ const formData = {
906
+ title: $("edit-book-title").value,
907
+ totalPages: parseInt($("edit-total-pages").value, 10),
908
+ startDate: $("edit-start-date").value,
909
+ goalType: document.querySelector('input[name="edit-goal-type"]:checked')?.value,
910
+ targetDate: $("edit-target-date").value,
911
+ pagesPerDay: parseInt($("edit-pages-per-day").value, 10)
912
+ };
913
+ if (!this.validateBookData(formData)) {
914
+ alert("Please fill in all required fields with valid values.");
915
+ return;
916
+ }
917
+ this.updateBook(bookIndex, formData);
918
+ this.closeEditBookModal();
919
+ }
920
+
921
+ validateBookData(data) {
922
+ return (
923
+ data.title &&
924
+ !isNaN(data.totalPages) &&
925
+ data.totalPages > 0 &&
926
+ data.startDate &&
927
+ data.goalType &&
928
+ ((data.goalType === "date" && data.targetDate) ||
929
+ (data.goalType === "pages" && !isNaN(data.pagesPerDay) && data.pagesPerDay > 0))
930
+ );
931
+ }
932
+
933
+ handleDeleteBook() {
934
+ const bookIndex = parseInt($("edit-book-index").value, 10);
935
+ if (confirm("Are you sure you want to delete this book? This action cannot be undone.")) {
936
+ this.deleteBook(bookIndex);
937
+ this.closeEditBookModal();
938
+ }
939
+ }
940
+
941
+ handleEditHistory() {
942
+ const currentPageTotal = parseInt($("edit-pages-read").value, 10);
943
+ if (isNaN(currentPageTotal)) return;
944
+ const bookTitle = $("edit-history-book-title").textContent;
945
+ const book = this.books.find((b) => b.title === bookTitle);
946
+ if (!book) return;
947
+ try {
948
+ book.updateProgress(currentPageTotal, $("edit-date").textContent);
949
+ this.saveBooks();
950
+ this.updateUI();
951
+ this.closeEditModal();
952
+ } catch (error) {
953
+ alert(error.message);
954
+ }
955
+ }
956
+
957
+ handleGoalTypeChange(radio, isEdit = false) {
958
+ const prefix = isEdit ? "edit-" : "";
959
+ const isDateGoal = radio.value === "date";
960
+ $(`${prefix}target-date`).disabled = !isDateGoal;
961
+ $(`${prefix}pages-per-day`).disabled = isDateGoal;
962
+ if (!isDateGoal) $(prefix + "target-date").value = "";
963
+ if (isDateGoal) $(prefix + "pages-per-day").value = "";
964
+ }
965
+
966
+ calculateStats() {
967
+ this.calculateStreaks();
968
+ this.calculateWeeklyCompletionRate();
969
+ this.calculateStatistics();
970
+ }
971
+
972
+ calculateStreaks() {
973
+ const allHistory = [];
974
+ this.books.forEach((b) => b.readingHistory.forEach((h) => allHistory.push(h)));
975
+ allHistory.sort((a, b) => new Date(a.date) - new Date(b.date));
976
+
977
+ let currentStreak = 0;
978
+ let longestStreak = 0;
979
+ let tempStreak = 0;
980
+ let lastDate = null;
981
+ const today = new Date();
982
+ const yesterday = new Date(today);
983
+ yesterday.setDate(today.getDate() - 1);
984
+
985
+ allHistory.forEach((item) => {
986
+ const currentDate = new Date(item.date);
987
+ if (lastDate) {
988
+ const diffDays = Utils.getDaysBetween(currentDate, lastDate);
989
+ if (diffDays === 1) {
990
+ tempStreak++;
991
+ } else if (diffDays > 1) {
992
+ longestStreak = Math.max(longestStreak, tempStreak);
993
+ tempStreak = 1;
994
+ }
995
+ } else if (formatDate(currentDate) === formatDate(yesterday)) {
996
+ tempStreak = 1;
997
+ }
998
+ lastDate = currentDate;
999
+ });
1000
+ longestStreak = Math.max(longestStreak, tempStreak);
1001
+
1002
+ currentStreak = 0;
1003
+ lastDate = new Date();
1004
+ for (let i = 0; i < allHistory.length; i++) {
1005
+ const historyItemDate = new Date(allHistory[allHistory.length - 1 - i].date);
1006
+ const diffDays = Utils.getDaysBetween(lastDate, historyItemDate);
1007
+ if (diffDays <= 1) {
1008
+ currentStreak++;
1009
+ lastDate = historyItemDate;
1010
+ } else {
1011
+ break;
1012
+ }
1013
+ }
1014
+ $("current-streak").textContent = currentStreak;
1015
+ $("longest-streak").textContent = longestStreak;
1016
+ }
1017
+
1018
+ calculateWeeklyCompletionRate() {
1019
+ const today = new Date();
1020
+ let readingDays = 0;
1021
+ for (let i = 0; i < 7; i++) {
1022
+ const date = new Date(today);
1023
+ date.setDate(today.getDate() - i);
1024
+ const formattedDate = formatDate(date);
1025
+ const hasRead = this.books.some((book) =>
1026
+ book.readingHistory.some((item) => item.date === formattedDate && item.pagesRead > 0)
1027
+ );
1028
+ if (hasRead) readingDays++;
1029
+ }
1030
+ const completionRate = Math.round((readingDays / 7) * 100);
1031
+ $("weekly-completion").textContent = `${completionRate}%`;
1032
+ }
1033
+
1034
+ calculateStatistics() {
1035
+ const stats = this.books.reduce(
1036
+ (acc, book) => {
1037
+ acc.totalPagesRead += book.currentPage;
1038
+ if (book.currentPage === book.totalPages) acc.totalBooksCompleted++;
1039
+ book.readingHistory.forEach((item) => {
1040
+ acc.totalReadingDays++;
1041
+ const dayOfWeek = new Date(item.date).getDay();
1042
+ acc.dayCounts[dayOfWeek] += item.pagesRead;
1043
+ });
1044
+ return acc;
1045
+ },
1046
+ {
1047
+ totalPagesRead: 0,
1048
+ totalBooksCompleted: 0,
1049
+ totalReadingDays: 0,
1050
+ dayCounts: { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }
1051
+ }
1052
+ );
1053
+
1054
+ const averagePages = stats.totalReadingDays
1055
+ ? Math.round(stats.totalPagesRead / stats.totalReadingDays)
1056
+ : 0;
1057
+ let bestDay = "";
1058
+ let maxPages = 0;
1059
+ Object.entries(stats.dayCounts).forEach(([day, pages]) => {
1060
+ if (pages > maxPages) {
1061
+ maxPages = pages;
1062
+ bestDay = dayNames[parseInt(day, 10)];
1063
+ }
1064
+ });
1065
+
1066
+ $("avg-pages").textContent = averagePages;
1067
+ $("total-books-completed").textContent = stats.totalBooksCompleted;
1068
+ $("total-pages-read").textContent = stats.totalPagesRead;
1069
+ $("best-day").textContent = bestDay;
1070
+ }
1071
+
1072
+ renderReadingHistoryHeatmap() {
1073
+ const heatmapContainer = $("heatmap-container");
1074
+ if (!heatmapContainer) return;
1075
+ heatmapContainer.innerHTML = "";
1076
+
1077
+ const today = new Date();
1078
+ const dates = Array.from({ length: 7 }, (_, i) => {
1079
+ const date = new Date(today);
1080
+ date.setDate(today.getDate() - (6 - i));
1081
+ return formatDate(date);
1082
+ });
1083
+
1084
+ const headerRow = document.createElement("div");
1085
+ headerRow.className = "grid grid-cols-[100px_1fr] items-start mb-2";
1086
+ headerRow.innerHTML = `
1087
+ <div></div>
1088
+ <div class="grid grid-cols-7 gap-2 text-center text-sm font-medium text-gray-600">
1089
+ ${dates
1090
+ .map((date) => {
1091
+ const d = new Date(date).getDay();
1092
+ return `
1093
+ <div class="flex items-center justify-center gap-1">
1094
+ <span>${dayIcons[d]}</span>
1095
+ <span>${dayNames[d].slice(0, 3)}</span>
1096
+ </div>
1097
+ `;
1098
+ })
1099
+ .join("")}
1100
+ </div>
1101
+ `;
1102
+ heatmapContainer.appendChild(headerRow);
1103
+
1104
+ this.books.forEach((book) => {
1105
+ const bookRow = document.createElement("div");
1106
+ bookRow.className = "heatmap-book-row";
1107
+ const label = document.createElement("div");
1108
+ const isCompleted = book.currentPage === book.totalPages;
1109
+ label.className = `book-label ${isCompleted ? 'bg-green-100 border-green-500 text-green-800' : 'bg-gray-50 border-gray-200 text-gray-700'}`;
1110
+ label.textContent = book.title;
1111
+ bookRow.appendChild(label);
1112
+
1113
+ const heatmapRow = document.createElement("div");
1114
+ heatmapRow.className = "heatmap-row";
1115
+
1116
+ dates.forEach((date) => {
1117
+ const cell = document.createElement("div");
1118
+ cell.className = "heatmap-cell";
1119
+ cell.dataset.date = date;
1120
+ cell.dataset.bookTitle = book.title;
1121
+ const history = book.readingHistory.find((item) => item.date === date);
1122
+ const pagesRead = history ? history.pagesRead : 0;
1123
+ const totalPages = history ? history.totalPages : 0;
1124
+ const dailyGoal = book.getDailyGoal(date);
1125
+ const intensity = Math.min(pagesRead / 50, 1);
1126
+ const cellDate = new Date(date);
1127
+ const isInPast = cellDate < today && formatDate(cellDate) !== formatDate(today);
1128
+ const isCompleted = book.currentPage === book.totalPages;
1129
+
1130
+ let color;
1131
+ if (isCompleted) {
1132
+ color = `rgba(16, 185, 129, ${intensity})`; // Tailwind's green-500
1133
+ } else {
1134
+ color = isInPast && pagesRead < dailyGoal
1135
+ ? `rgba(239, 68, 68, ${intensity})`
1136
+ : `rgba(59, 130, 246, ${intensity})`;
1137
+ }
1138
+ cell.style.backgroundColor = color;
1139
+ cell.innerHTML = `
1140
+ <div class="text-[0.65rem] sm:text-xs font-medium ${intensity > 0.5 ? "text-white" : "text-gray-700"}">
1141
+ ${cellDate.getDate()}
1142
+ </div>
1143
+ ${pagesRead > 0 ? `
1144
+ <div class="text-[0.65rem] sm:text-xs ${intensity > 0.5 ? "text-white" : "text-gray-700"} truncate">
1145
+ p.${totalPages}
1146
+ </div>
1147
+ ` : ''}
1148
+ `;
1149
+ cell.addEventListener("click", () => this.openEditModal(date, book.title));
1150
+ this.addCellTooltip(cell, book.title, pagesRead, date);
1151
+ heatmapRow.appendChild(cell);
1152
+ });
1153
+
1154
+ bookRow.appendChild(heatmapRow);
1155
+ heatmapContainer.appendChild(bookRow);
1156
+ });
1157
+ }
1158
+
1159
+ addCellTooltip(cell, bookTitle, pagesRead, date) {
1160
+ cell.addEventListener("mouseover", (e) => {
1161
+ const tooltip = document.createElement("div");
1162
+ tooltip.textContent = `${bookTitle}: ${pagesRead} pages on ${date}`;
1163
+ tooltip.className = "absolute bg-black text-white p-2 rounded text-xs z-10";
1164
+ tooltip.style.top = `${e.clientY - 30}px`;
1165
+ tooltip.style.left = `${e.clientX + 10}px`;
1166
+ document.body.appendChild(tooltip);
1167
+
1168
+ const removeTooltip = () => {
1169
+ if (document.body.contains(tooltip)) {
1170
+ document.body.removeChild(tooltip);
1171
+ }
1172
+ cell.removeEventListener("mouseout", removeTooltip);
1173
+ };
1174
+ cell.addEventListener("mouseout", removeTooltip);
1175
+ });
1176
+ }
1177
+
1178
+ exportData() {
1179
+ const dataStr = JSON.stringify(this.books);
1180
+ const dataUri = "data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
1181
+ const exportFileDefaultName = "reading_tracker_data.json";
1182
+ const linkElement = document.createElement("a");
1183
+ linkElement.setAttribute("href", dataUri);
1184
+ linkElement.setAttribute("download", exportFileDefaultName);
1185
+ linkElement.click();
1186
+ }
1187
+
1188
+ handleImport(event) {
1189
+ const file = event.target.files[0];
1190
+ if (!file) return;
1191
+ const reader = new FileReader();
1192
+ reader.onload = (e) => {
1193
+ try {
1194
+ const importedData = JSON.parse(e.target.result);
1195
+ if (Array.isArray(importedData)) {
1196
+ if (confirm("This will replace your current data. Are you sure you want to continue?")) {
1197
+ this.books = importedData.map((d) => new Book(d));
1198
+ this.saveBooks();
1199
+ this.updateUI();
1200
+ alert("Data imported successfully!");
1201
+ }
1202
+ } else {
1203
+ throw new Error("Invalid data format");
1204
+ }
1205
+ } catch {
1206
+ alert(
1207
+ "Error: Invalid file format. Please make sure you are importing a valid reading tracker data file."
1208
+ );
1209
+ }
1210
+ };
1211
+ reader.readAsText(file);
1212
+ event.target.value = "";
1213
+ }
1214
+
1215
+ openEditGoalModal() {
1216
+ const modal = $("edit-goal-modal");
1217
+ const yearInput = $("yearly-goal-input");
1218
+ const yearSpan = $("current-year");
1219
+
1220
+ yearSpan.textContent = new Date().getFullYear();
1221
+ yearInput.value = this.yearlyGoal;
1222
+ modal.classList.remove("hidden");
1223
+ }
1224
+
1225
+ handleEditGoal(event) {
1226
+ event.preventDefault();
1227
+ const newGoal = parseInt($("yearly-goal-input").value);
1228
+ if (newGoal > 0) {
1229
+ this.yearlyGoal = newGoal;
1230
+ localStorage.setItem('yearlyReadingGoal', newGoal);
1231
+ this.updateReadingChallenge();
1232
+ $("edit-goal-modal").classList.add("hidden");
1233
+ }
1234
+ }
1235
+
1236
+ updateReadingChallenge() {
1237
+ const now = new Date();
1238
+ const start = new Date(now.getFullYear(), 0, 1);
1239
+ const diff = now - start;
1240
+ const oneDay = 1000 * 60 * 60 * 24;
1241
+ const oneWeek = oneDay * 7;
1242
+
1243
+ // Calculate days and weeks
1244
+ const currentDay = Math.ceil(diff / oneDay);
1245
+ const currentWeek = Math.ceil(diff / oneWeek);
1246
+
1247
+ // Calculate year progress
1248
+ const isLeapYear = new Date(now.getFullYear(), 1, 29).getMonth() === 1;
1249
+ const daysInYear = isLeapYear ? 366 : 365;
1250
+ const yearProgressPercentage = (currentDay / daysInYear) * 100;
1251
+
1252
+ // Calculate books progress including partial progress
1253
+ const booksProgress = this.books.reduce((total, book) => {
1254
+ // For completed books, add 1
1255
+ if (book.currentPage === book.totalPages) {
1256
+ return total + 1;
1257
+ }
1258
+ // For in-progress books, add their fractional progress
1259
+ return total + (book.currentPage / book.totalPages);
1260
+ }, 0);
1261
+
1262
+ const weeklyTarget = this.yearlyGoal / 52;
1263
+ const expectedProgress = currentWeek * weeklyTarget;
1264
+ const isOnTrack = booksProgress >= expectedProgress;
1265
+
1266
+ const booksProgressPercentage = (booksProgress / this.yearlyGoal) * 100;
1267
+
1268
+ // Update UI elements
1269
+ $("yearly-goal").textContent = this.yearlyGoal;
1270
+ $("current-week").textContent = currentWeek;
1271
+ $("current-day").textContent = currentDay;
1272
+ $("completed-books").textContent = booksProgress.toFixed(1);
1273
+ $("on-track-status").textContent = isOnTrack ? "Yes" : "No";
1274
+ $("on-track-status").className = `font-medium ${isOnTrack ? 'text-green-600' : 'text-red-600'}`;
1275
+
1276
+ // Update progress bars
1277
+ const booksProgressBar = $("challenge-progress");
1278
+ const yearProgressBar = $("year-progress");
1279
+
1280
+ booksProgressBar.style.width = `${booksProgressPercentage}%`;
1281
+ yearProgressBar.style.width = `${yearProgressPercentage}%`;
1282
+ }
1283
+ }
1284
+
1285
+ let bookManager;
1286
+
1287
+ function initializeApp() {
1288
+ try {
1289
+ bookManager = new BookManager();
1290
+ window.bookManager = bookManager;
1291
+ bookManager.init();
1292
+ } catch {}
1293
+ }
1294
+
1295
+ if (document.readyState === "loading") {
1296
+ document.addEventListener("DOMContentLoaded", initializeApp);
1297
+ } else {
1298
+ setTimeout(initializeApp, 0);
1299
+ }
1300
+
1301
+ window.addEventListener("load", () => {
1302
+ if (!bookManager) initializeApp();
1303
+ });
1304
+
1305
+ function closeEditBookModal() {
1306
+ $("edit-book-modal").classList.add("hidden");
1307
+ }
1308
+ </script>
1309
+ </body>
1310
+ </html>