Skip to content

Commit ad5d585

Browse files
authored
fstng
1 parent 881cf18 commit ad5d585

File tree

1 file changed

+343
-0
lines changed

1 file changed

+343
-0
lines changed

Diff for: yntarot/fstng.html

+343
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
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

Comments
 (0)