1
+ < html lang ="en ">
2
+ < head >
3
+ < meta charset ="UTF-8 ">
4
+ < meta name ="viewport " content ="width=device-width, initial-scale=1.0 ">
5
+ < title > Fasting Tracker</ title >
6
+ < script src ="https://d3js.org/d3.v7.min.js "> </ script >
7
+ < style >
8
+ : root {
9
+ --primary : # 6366f1 ;
10
+ --primary-dark : # 4f46e5 ;
11
+ --danger : # ef4444 ;
12
+ --danger-dark : # dc2626 ;
13
+ --edit : # 8b5cf6 ;
14
+ --edit-dark : # 7c3aed ;
15
+ --background : # f8fafc ;
16
+ --card-bg : # ffffff ;
17
+ --text : # 1e293b ;
18
+ --text-secondary : # 64748b ;
19
+ --gauge-bg : # e2e8f0 ;
20
+ --gauge-progress : # 818cf8 ;
21
+ }
22
+
23
+ body {
24
+ font-family : -apple-system, BlinkMacSystemFont, 'Segoe UI' , Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
25
+ display : flex;
26
+ flex-direction : column;
27
+ align-items : center;
28
+ justify-content : center;
29
+ min-height : 100vh ;
30
+ margin : 0 ;
31
+ background-color : var (--background );
32
+ padding : 1rem ;
33
+ color : var (--text );
34
+ }
35
+
36
+ .container {
37
+ text-align : center;
38
+ background : var (--card-bg );
39
+ padding : 2rem ;
40
+ border-radius : 20px ;
41
+ box-shadow : 0 4px 6px -1px rgb (0 0 0 / 0.1 ), 0 2px 4px -2px rgb (0 0 0 / 0.1 );
42
+ width : 100% ;
43
+ max-width : 400px ;
44
+ display : flex;
45
+ flex-direction : column;
46
+ align-items : center;
47
+ transition : all 0.3s ease;
48
+ }
49
+
50
+ button {
51
+ padding : 12px 24px ;
52
+ font-size : 1rem ;
53
+ margin : 6px ;
54
+ cursor : pointer;
55
+ border : none;
56
+ border-radius : 12px ;
57
+ background-color : var (--primary );
58
+ color : white;
59
+ transition : all 0.2s ease;
60
+ width : 200px ;
61
+ font-weight : 500 ;
62
+ letter-spacing : 0.3px ;
63
+ box-shadow : 0 2px 4px rgba (99 , 102 , 241 , 0.2 );
64
+ }
65
+
66
+ button : hover {
67
+ background-color : var (--primary-dark );
68
+ transform : translateY (-1px );
69
+ box-shadow : 0 4px 6px rgba (99 , 102 , 241 , 0.3 );
70
+ }
71
+
72
+ button : disabled {
73
+ opacity : 0.7 ;
74
+ cursor : not-allowed;
75
+ transform : none;
76
+ box-shadow : none;
77
+ }
78
+
79
+ button # resetBtn {
80
+ background-color : var (--danger );
81
+ box-shadow : 0 2px 4px rgba (239 , 68 , 68 , 0.2 );
82
+ }
83
+
84
+ button # resetBtn : hover {
85
+ background-color : var (--danger-dark );
86
+ box-shadow : 0 4px 6px rgba (239 , 68 , 68 , 0.3 );
87
+ }
88
+
89
+ button # editBtn {
90
+ background-color : var (--edit );
91
+ box-shadow : 0 2px 4px rgba (139 , 92 , 246 , 0.2 );
92
+ }
93
+
94
+ button # editBtn : hover {
95
+ background-color : var (--edit-dark );
96
+ box-shadow : 0 4px 6px rgba (139 , 92 , 246 , 0.3 );
97
+ }
98
+
99
+ .time-display {
100
+ font-size : 3em ;
101
+ position : absolute;
102
+ left : 50% ;
103
+ top : 50% ;
104
+ transform : translate (-50% , -50% );
105
+ white-space : nowrap;
106
+ font-family : 'SF Mono' , 'Roboto Mono' , monospace;
107
+ font-weight : 600 ;
108
+ color : var (--text );
109
+ letter-spacing : -1px ;
110
+ }
111
+
112
+ .gauge-container {
113
+ width : 100% ;
114
+ max-width : 300px ;
115
+ height : 300px ;
116
+ margin : 0 auto;
117
+ position : relative;
118
+ padding : 1rem ;
119
+ }
120
+
121
+ h1 {
122
+ font-size : 1.8em ;
123
+ margin : 0 0 1.5rem 0 ;
124
+ width : 100% ;
125
+ text-align : center;
126
+ color : var (--text );
127
+ font-weight : 700 ;
128
+ }
129
+
130
+ .controls {
131
+ margin-top : 1.5rem ;
132
+ width : 100% ;
133
+ display : flex;
134
+ flex-direction : column;
135
+ align-items : center;
136
+ gap : 8px ;
137
+ }
138
+
139
+ .time-edit {
140
+ display : none;
141
+ margin : 1rem 0 ;
142
+ width : 100% ;
143
+ text-align : center;
144
+ }
145
+
146
+ .time-edit .visible {
147
+ display : flex;
148
+ flex-direction : column;
149
+ align-items : center;
150
+ gap : 12px ;
151
+ }
152
+
153
+ .time-edit input {
154
+ padding : 12px ;
155
+ border : 2px solid # e2e8f0 ;
156
+ border-radius : 12px ;
157
+ font-size : 1em ;
158
+ width : 200px ;
159
+ text-align : center;
160
+ transition : border-color 0.2s ease;
161
+ outline : none;
162
+ }
163
+
164
+ .time-edit input : focus {
165
+ border-color : var (--primary );
166
+ }
167
+
168
+ @media (max-width : 480px ) {
169
+ .container {
170
+ padding : 1.5rem ;
171
+ }
172
+
173
+ button {
174
+ padding : 10px 20px ;
175
+ font-size : 0.95em ;
176
+ width : 180px ;
177
+ }
178
+
179
+ .time-display {
180
+ font-size : 2.5em ;
181
+ }
182
+
183
+ h1 {
184
+ font-size : 1.5em ;
185
+ }
186
+
187
+ .time-edit input {
188
+ width : 180px ;
189
+ }
190
+ }
191
+ </ style >
192
+ </ head >
193
+ < body >
194
+ < div class ="container ">
195
+ < h1 > Fasting Tracker</ h1 >
196
+ < div class ="gauge-container ">
197
+ < div class ="time-display ">
198
+ < span id ="timeElapsed "> 00:00:00</ span >
199
+ </ div >
200
+ </ div >
201
+ < div class ="controls ">
202
+ < button id ="startBtn "> Start Fast</ button >
203
+ < button id ="editBtn "> Edit Start Time</ button >
204
+ < button id ="resetBtn "> Reset</ button >
205
+ < div class ="time-edit " id ="timeEdit ">
206
+ < input type ="datetime-local " id ="startTimeInput " step ="1 ">
207
+ < button id ="saveTimeBtn "> Save</ button >
208
+ </ div >
209
+ </ div >
210
+ </ div >
211
+
212
+ < script >
213
+ const config = {
214
+ width : 300 ,
215
+ height : 300 ,
216
+ margin : 40 ,
217
+ minValue : 0 ,
218
+ maxValue : 24 ,
219
+ circleThickness : 0.12
220
+ } ;
221
+
222
+ const gauge = d3 . select ( '.gauge-container' )
223
+ . append ( 'svg' )
224
+ . attr ( 'width' , '100%' )
225
+ . attr ( 'height' , '100%' )
226
+ . attr ( 'viewBox' , `0 0 ${ config . width } ${ config . height } ` )
227
+ . attr ( 'preserveAspectRatio' , 'xMidYMid meet' ) ;
228
+
229
+ const radius = Math . min ( config . width , config . height ) / 2 - config . margin ;
230
+ const centerX = config . width / 2 ;
231
+ const centerY = config . height / 2 ;
232
+
233
+ const backgroundArc = d3 . arc ( )
234
+ . innerRadius ( radius * ( 1 - config . circleThickness ) )
235
+ . outerRadius ( radius )
236
+ . startAngle ( 0 )
237
+ . endAngle ( 2 * Math . PI ) ;
238
+
239
+ gauge . append ( 'path' )
240
+ . attr ( 'transform' , `translate(${ centerX } ,${ centerY } )` )
241
+ . attr ( 'd' , backgroundArc )
242
+ . style ( 'fill' , 'var(--gauge-bg)' ) ;
243
+
244
+ const progressArc = d3 . arc ( )
245
+ . innerRadius ( radius * ( 1 - config . circleThickness ) )
246
+ . outerRadius ( radius )
247
+ . startAngle ( 0 ) ;
248
+
249
+ const progressPath = gauge . append ( 'path' )
250
+ . attr ( 'transform' , `translate(${ centerX } ,${ centerY } )` )
251
+ . style ( 'fill' , 'var(--gauge-progress)' )
252
+ . style ( 'transition' , 'fill 0.3s ease' ) ;
253
+
254
+ let startTime = localStorage . getItem ( 'fastingStartTime' ) ;
255
+ const startBtn = document . getElementById ( 'startBtn' ) ;
256
+ const resetBtn = document . getElementById ( 'resetBtn' ) ;
257
+ const editBtn = document . getElementById ( 'editBtn' ) ;
258
+ const timeEdit = document . getElementById ( 'timeEdit' ) ;
259
+ const startTimeInput = document . getElementById ( 'startTimeInput' ) ;
260
+ const saveTimeBtn = document . getElementById ( 'saveTimeBtn' ) ;
261
+ const timeElapsedSpan = document . getElementById ( 'timeElapsed' ) ;
262
+ let animationFrameId ;
263
+
264
+ function formatDateTime ( timestamp ) {
265
+ const date = new Date ( parseInt ( timestamp ) ) ;
266
+ return date . toISOString ( ) . slice ( 0 , 19 ) ;
267
+ }
268
+
269
+ function updateDisplay ( ) {
270
+ if ( ! startTime ) {
271
+ timeElapsedSpan . textContent = '00:00:00' ;
272
+ progressPath . attr ( 'd' , progressArc . endAngle ( 0 ) ) ;
273
+ return ;
274
+ }
275
+
276
+ const elapsed = Date . now ( ) - parseInt ( startTime ) ;
277
+ const hours = Math . floor ( elapsed / 3600000 ) ;
278
+ const minutes = Math . floor ( ( elapsed % 3600000 ) / 60000 ) ;
279
+ const seconds = Math . floor ( ( elapsed % 60000 ) / 1000 ) ;
280
+ const progress = Math . min ( hours + minutes / 60 + seconds / 3600 , 24 ) ;
281
+
282
+ timeElapsedSpan . textContent = `${ hours . toString ( ) . padStart ( 2 , '0' ) } :${ minutes . toString ( ) . padStart ( 2 , '0' ) } :${ seconds . toString ( ) . padStart ( 2 , '0' ) } ` ;
283
+ const endAngle = ( progress / 24 ) * 2 * Math . PI ;
284
+ progressPath . attr ( 'd' , progressArc . endAngle ( endAngle ) ) ;
285
+
286
+ animationFrameId = requestAnimationFrame ( updateDisplay ) ;
287
+ }
288
+
289
+ function startFasting ( ) {
290
+ if ( ! startTime ) {
291
+ startTime = Date . now ( ) . toString ( ) ;
292
+ localStorage . setItem ( 'fastingStartTime' , startTime ) ;
293
+ updateButtonStates ( true ) ;
294
+ updateDisplay ( ) ;
295
+ }
296
+ }
297
+
298
+ function resetFasting ( ) {
299
+ startTime = null ;
300
+ localStorage . removeItem ( 'fastingStartTime' ) ;
301
+ updateButtonStates ( false ) ;
302
+ cancelAnimationFrame ( animationFrameId ) ;
303
+ updateDisplay ( ) ;
304
+ }
305
+
306
+ function updateButtonStates ( fasting ) {
307
+ startBtn . textContent = fasting ? 'Fasting in Progress' : 'Start Fast' ;
308
+ startBtn . disabled = fasting ;
309
+ editBtn . disabled = ! fasting ;
310
+ }
311
+
312
+ function toggleTimeEdit ( ) {
313
+ timeEdit . classList . toggle ( 'visible' ) ;
314
+ if ( timeEdit . classList . contains ( 'visible' ) && startTime ) {
315
+ startTimeInput . value = formatDateTime ( startTime ) ;
316
+ }
317
+ }
318
+
319
+ function saveStartTime ( ) {
320
+ const newStartTime = new Date ( startTimeInput . value ) . getTime ( ) ;
321
+ if ( ! isNaN ( newStartTime ) ) {
322
+ startTime = newStartTime . toString ( ) ;
323
+ localStorage . setItem ( 'fastingStartTime' , startTime ) ;
324
+ timeEdit . classList . remove ( 'visible' ) ;
325
+ updateDisplay ( ) ;
326
+ }
327
+ }
328
+
329
+ startBtn . addEventListener ( 'click' , startFasting ) ;
330
+ resetBtn . addEventListener ( 'click' , resetFasting ) ;
331
+ editBtn . addEventListener ( 'click' , toggleTimeEdit ) ;
332
+ saveTimeBtn . addEventListener ( 'click' , saveStartTime ) ;
333
+
334
+ if ( startTime ) {
335
+ updateButtonStates ( true ) ;
336
+ updateDisplay ( ) ;
337
+ } else {
338
+ updateButtonStates ( false ) ;
339
+ editBtn . disabled = true ;
340
+ }
341
+ </ script >
342
+ </ body >
343
+ </ html >
0 commit comments