Ultima attività 5 hours ago

Revisione 7339338557bb3c36446a88fa66c42342b05bbbed

savings.html Raw
1<!DOCTYPE html>
2<html
3 editmode="false"
4 pageowner="false"
5 savestatus="saved"
6>
7
8<head>
9 <title>Savings Goals</title>
10 <meta charset="utf-8">
11 <meta name="viewport" content="width=device-width, initial-scale=1">
12 <script src="https://cdn.jsdelivr.net/npm/hyperclayjs@1.24.3/src/hyperclay.js?features=save-core,save-system,autosave,unsaved-warning,edit-mode-helpers,persist,snapshot,option-visibility,edit-mode,event-attrs,ajax-elements,sortable,movable,dom-helpers,input-helpers,onaftersave,save-freeze,dialogs,toast,the-modal,mutation,nearest,cookie,throttle,debounce,cache-bust,dom-ready,all-js,style-injection,form-data,hyper-morph,slugify,copy-to-clipboard,query-params,send-message,file-upload,live-sync,export-to-window,view-mode-excludes-edit-modules" type="module"></script>
13 <style data-name="option-visibility" mutations-ignore=""></style>
14 <style>
15 body { max-width: 600px; margin: 0 auto; padding: 1rem; }
16 .goal fieldset { margin-bottom: 1rem; }
17 .goal label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #555; }
18 .goal label input { display: block; margin-top: 0.25rem; width: 100%; max-width: 12rem; }
19 input[type="number"] { font-size: 1rem; } /* prevent iOS auto-zoom on focus */
20 </style>
21</head>
22
23<body style="">
24
25 <h1>Savings Goals</h1>
26
27 <p>
28 <label>
29 Total Savings ($):
30 <input
31 type="number"
32 id="total-savings"
33 value="0"
34 min="0"
35 step="0.01"
36 oninput="this.setAttribute('value', this.value); updateSummary()"
37 >
38 </label>
39 </p>
40
41 <hr>
42
43 <p save-ignore="">
44 Allocated: <strong id="sum-allocated">$0.00</strong>
45 &nbsp;|&nbsp;
46 Remaining: <strong id="sum-remaining">$0.00</strong>
47 </p>
48 <p
49 id="alloc-warning"
50 style="color: red; display: none;"
51 save-ignore=""
52 >
53 ⚠ Total allocated exceeds your savings.
54 Over by <strong id="over-by">$0.00</strong>.
55 </p>
56
57 <hr>
58
59 <h2>Goals</h2>
60
61 <div
62 id="goals-list"
63 sortable=""
64 >
65 <!-- Hidden template — cloned by addGoal() -->
66 <div
67 class="goal"
68 id="goal-template"
69 style="display: none;"
70 save-ignore=""
71 >
72 <fieldset>
73 <legend>
74 <input
75 type="text"
76 class="goal-name"
77 placeholder="Goal name"
78 value=""
79 oninput="this.setAttribute('value', this.value)"
80 >
81 </legend>
82 <p><label>
83 Allocated from savings ($):
84 <input
85 type="number"
86 class="goal-allocated"
87 value="0"
88 min="0"
89 step="0.01"
90 oninput="this.setAttribute('value', this.value); updateSummary()"
91 >
92 </label></p>
93 <p><label>
94 Goal total ($):
95 <input
96 type="number"
97 class="goal-amount"
98 value="0"
99 min="0"
100 step="0.01"
101 oninput="this.setAttribute('value', this.value)"
102 >
103 </label></p>
104 <p><button
105 type="button"
106 onclick="deleteGoal(this)"
107 >Delete</button></p>
108 </fieldset>
109 </div>
110
111 <button
112 type="button"
113 id="add-goal-btn"
114 onclick="addGoal()"
115 save-ignore=""
116 >+ Add Goal</button>
117 </div>
118
119 <script>
120 function addGoal() {
121 const template = document.getElementById('goal-template');
122 const clone = template.cloneNode(true);
123
124 clone.id = 'goal-' + Date.now();
125 clone.removeAttribute('save-ignore');
126 clone.style.display = '';
127
128 // Reset inputs to defaults in the clone
129 clone.querySelector('.goal-name').value = '';
130 clone.querySelector('.goal-name').setAttribute('value', '');
131 clone.querySelector('.goal-amount').value = '0';
132 clone.querySelector('.goal-amount').setAttribute('value', '0');
133 clone.querySelector('.goal-allocated').value = '0';
134 clone.querySelector('.goal-allocated').setAttribute('value', '0');
135
136 const goalsList = document.getElementById('goals-list');
137 const addBtn = document.getElementById('add-goal-btn');
138 goalsList.insertBefore(clone, addBtn);
139 updateSummary();
140 }
141
142 function deleteGoal(btn) {
143 consent('Delete this goal?', () => {
144 btn.closest('.goal').remove();
145 updateSummary();
146 });
147 }
148
149 function updateSummary() {
150 const savings = parseFloat(document.getElementById('total-savings').value) || 0;
151 const allocInputs = document.querySelectorAll('.goal:not(#goal-template) .goal-allocated');
152 const allocated = Array.from(allocInputs).reduce((sum, el) => sum + (parseFloat(el.value) || 0), 0);
153 const remaining = savings - allocated;
154
155 document.getElementById('sum-allocated').textContent = '$' + allocated.toFixed(2);
156 document.getElementById('sum-remaining').textContent = '$' + remaining.toFixed(2);
157
158 const overBy = allocated - savings;
159 const warning = document.getElementById('alloc-warning');
160 if (overBy > 0.001) {
161 document.getElementById('over-by').textContent = '$' + overBy.toFixed(2);
162 warning.style.display = '';
163 } else {
164 warning.style.display = 'none';
165 }
166 }
167
168 document.addEventListener('DOMContentLoaded', updateSummary);
169
170 // Fast save: 500ms after last input or structural change
171 let saveTimer;
172 function scheduleSave() {
173 clearTimeout(saveTimer);
174 saveTimer = setTimeout(() => window.hyperclay.savePage(), 500);
175 }
176
177 window.addEventListener('load', () => {
178 document.addEventListener('input', e => {
179 if (e.target.matches('#total-savings, .goal-allocated, .goal-amount, .goal-name')) {
180 scheduleSave();
181 }
182 });
183 new MutationObserver(scheduleSave).observe(
184 document.getElementById('goals-list'),
185 { childList: true }
186 );
187 });
188
189</script>
190
191</body>
192
193</html>