savings.html
· 5.7 KiB · HTML
Sin formato
<!DOCTYPE html>
<html
editmode="false"
pageowner="false"
savestatus="saved"
>
<head>
<title>Savings Goals</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>
<style data-name="option-visibility" mutations-ignore=""></style>
<style>
body { max-width: 600px; margin: 0 auto; padding: 1rem; }
.goal fieldset { margin-bottom: 1rem; }
.goal label { display: block; margin-bottom: 0.5rem; font-size: 0.85rem; color: #555; }
.goal label input { display: block; margin-top: 0.25rem; width: 100%; max-width: 12rem; }
input[type="number"] { font-size: 1rem; } /* prevent iOS auto-zoom on focus */
</style>
</head>
<body style="">
<h1>Savings Goals</h1>
<p>
<label>
Total Savings ($):
<input
type="number"
id="total-savings"
value="0"
min="0"
step="0.01"
oninput="this.setAttribute('value', this.value); updateSummary()"
>
</label>
</p>
<hr>
<p save-ignore="">
Allocated: <strong id="sum-allocated">$0.00</strong>
|
Remaining: <strong id="sum-remaining">$0.00</strong>
</p>
<p
id="alloc-warning"
style="color: red; display: none;"
save-ignore=""
>
⚠ Total allocated exceeds your savings.
Over by <strong id="over-by">$0.00</strong>.
</p>
<hr>
<h2>Goals</h2>
<div
id="goals-list"
sortable=""
>
<!-- Hidden template — cloned by addGoal() -->
<div
class="goal"
id="goal-template"
style="display: none;"
save-ignore=""
>
<fieldset>
<legend>
<input
type="text"
class="goal-name"
placeholder="Goal name"
value=""
oninput="this.setAttribute('value', this.value)"
>
</legend>
<p><label>
Allocated from savings ($):
<input
type="number"
class="goal-allocated"
value="0"
min="0"
step="0.01"
oninput="this.setAttribute('value', this.value); updateSummary()"
>
</label></p>
<p><label>
Goal total ($):
<input
type="number"
class="goal-amount"
value="0"
min="0"
step="0.01"
oninput="this.setAttribute('value', this.value)"
>
</label></p>
<p><button
type="button"
onclick="deleteGoal(this)"
>Delete</button></p>
</fieldset>
</div>
<button
type="button"
id="add-goal-btn"
onclick="addGoal()"
save-ignore=""
>+ Add Goal</button>
</div>
<script>
function addGoal() {
const template = document.getElementById('goal-template');
const clone = template.cloneNode(true);
clone.id = 'goal-' + Date.now();
clone.removeAttribute('save-ignore');
clone.style.display = '';
// Reset inputs to defaults in the clone
clone.querySelector('.goal-name').value = '';
clone.querySelector('.goal-name').setAttribute('value', '');
clone.querySelector('.goal-amount').value = '0';
clone.querySelector('.goal-amount').setAttribute('value', '0');
clone.querySelector('.goal-allocated').value = '0';
clone.querySelector('.goal-allocated').setAttribute('value', '0');
const goalsList = document.getElementById('goals-list');
const addBtn = document.getElementById('add-goal-btn');
goalsList.insertBefore(clone, addBtn);
updateSummary();
}
function deleteGoal(btn) {
consent('Delete this goal?', () => {
btn.closest('.goal').remove();
updateSummary();
});
}
function updateSummary() {
const savings = parseFloat(document.getElementById('total-savings').value) || 0;
const allocInputs = document.querySelectorAll('.goal:not(#goal-template) .goal-allocated');
const allocated = Array.from(allocInputs).reduce((sum, el) => sum + (parseFloat(el.value) || 0), 0);
const remaining = savings - allocated;
document.getElementById('sum-allocated').textContent = '$' + allocated.toFixed(2);
document.getElementById('sum-remaining').textContent = '$' + remaining.toFixed(2);
const overBy = allocated - savings;
const warning = document.getElementById('alloc-warning');
if (overBy > 0.001) {
document.getElementById('over-by').textContent = '$' + overBy.toFixed(2);
warning.style.display = '';
} else {
warning.style.display = 'none';
}
}
document.addEventListener('DOMContentLoaded', updateSummary);
// Fast save: 500ms after last input or structural change
let saveTimer;
function scheduleSave() {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => window.hyperclay.savePage(), 500);
}
window.addEventListener('load', () => {
document.addEventListener('input', e => {
if (e.target.matches('#total-savings, .goal-allocated, .goal-amount, .goal-name')) {
scheduleSave();
}
});
new MutationObserver(scheduleSave).observe(
document.getElementById('goals-list'),
{ childList: true }
);
});
</script>
</body>
</html>
| 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 | | |
| 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> |