Upgraded firmware for Simone Giertz's Every Day Calendar that links an ATProto-powered ESP32, for sync with goals.garden 🌱
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add dark mode and improve save button UX

- Add dark mode CSS via prefers-color-scheme media query
- Disable save button until a different goal is selected
- Show loading spinner when save is pressed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+29 -5
+29 -5
firmware/esp32/GoalsGardenSync/web_server.cpp
··· 25 25 .goal-name{font-weight:500} 26 26 .goal-desc{color:#666;font-size:0.9em;margin-top:4px;display:none} 27 27 .goal:has(input:checked) .goal-desc{display:block} 28 - button{background:#4caf50;color:#fff;border:none;padding:14px 28px;border-radius:8px;font-size:1em;cursor:pointer;width:100%;margin-top:16px} 29 - button:hover{background:#43a047} 28 + button{background:#4caf50;color:#fff;border:none;padding:14px 28px;border-radius:8px;font-size:1em;cursor:pointer;width:100%;margin-top:16px;display:flex;align-items:center;justify-content:center;gap:8px} 29 + button:hover:not(:disabled){background:#43a047} 30 30 button:disabled{background:#ccc;cursor:not-allowed} 31 + .spinner{width:18px;height:18px;border:2px solid transparent;border-top-color:currentColor;border-radius:50%;animation:spin 0.8s linear infinite;display:none} 32 + button.loading .spinner{display:block} 33 + button.loading .btn-text{display:none} 34 + @keyframes spin{to{transform:rotate(360deg)}} 35 + @media(prefers-color-scheme:dark){ 36 + body{background:#1a1a1a;color:#e0e0e0} 37 + .user{color:#aaa} 38 + .user a{color:#64b5f6} 39 + .error{background:#4a1a1a;border-color:#d32f2f;color:#ef9a9a} 40 + .no-goals{background:#3d2a00;border-color:#ffa000;color:#ffe0b2} 41 + .no-goals a{color:#ffb74d} 42 + .goal{background:#2a2a2a;border-color:#444} 43 + .goal:hover{border-color:#666} 44 + .goal.saved{background:#1b3d1b;border-color:#388e3c} 45 + .goal-desc{color:#aaa} 46 + button{background:#388e3c} 47 + button:hover:not(:disabled){background:#2e7d32} 48 + button:disabled{background:#444;color:#666} 49 + } 31 50 </style> 32 51 </head> 33 52 <body> ··· 141 160 142 161 // Goal selection form 143 162 html += F("<h2 style=\"font-size:1.1em;margin:24px 0 12px\">Select Goal</h2>"); 144 - html += F("<form method=\"POST\" action=\"/select-goal\">"); 163 + html += F("<form method=\"POST\" action=\"/select-goal\" id=\"goal-form\">"); 145 164 146 165 for (const auto& goal : cachedGoals) { 147 166 bool isSelected = (goal.uri == currentGoalUri); ··· 151 170 html += F("<label><input type=\"radio\" name=\"goal_uri\" value=\""); 152 171 html += escapeHtml(goal.uri); 153 172 html += F("\""); 154 - if (isSelected) html += F(" checked"); 173 + if (isSelected) html += F(" checked data-current=\"1\""); 155 174 html += F("><div class=\"goal-info\"><div class=\"goal-name\">"); 156 175 html += escapeHtml(goal.name); 157 176 html += F("</div>"); ··· 163 182 html += F("</div></label></div>"); 164 183 } 165 184 166 - html += F("<button type=\"submit\">Save Selection</button>"); 185 + html += F("<button type=\"submit\" id=\"save-btn\" disabled><span class=\"btn-text\">Save Selection</span><span class=\"spinner\"></span></button>"); 167 186 html += F("</form>"); 187 + html += F("<script>"); 188 + html += F("var f=document.getElementById('goal-form'),b=document.getElementById('save-btn');"); 189 + html += F("f.addEventListener('change',function(){var c=f.querySelector('input:checked');b.disabled=!c||c.dataset.current==='1';});"); 190 + html += F("f.addEventListener('submit',function(){b.classList.add('loading');b.disabled=true;});"); 191 + html += F("</script>"); 168 192 169 193 html += FPSTR(HTML_FOOTER); 170 194 return html;