Last active
March 1, 2026 23:01
-
-
Save roening/222c903922f5d0cb06d23d79d5cad06a to your computer and use it in GitHub Desktop.
coffee-calculator-ios
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="pt-br"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> | |
| <title>Brew Master</title> | |
| <meta name="apple-mobile-web-app-capable" content="yes"> | |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |
| <meta name="apple-mobile-web-app-title" content="BrewMaster"> | |
| <link rel="apple-touch-icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>☕</text></svg>"> | |
| <style> | |
| /* CSS Variables for easy customization */ | |
| :root { | |
| --bg-body: #0a0a0a; | |
| --bg-card: #1a1a1a; | |
| --text-main: #efefef; | |
| --text-dim: #999; | |
| --accent-primary: #d4a373; /* Sandy Orange from your screenshot */ | |
| --accent-secondary: #2a2a2a; | |
| --border: #333; | |
| --radius: 16px; | |
| } | |
| * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } | |
| body { | |
| font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| background-color: var(--bg-body); | |
| color: var(--text-main); | |
| margin: 0; | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; | |
| min-height: 100vh; | |
| padding: env(safe-area-inset-top) 20px 20px 20px; | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 400px; | |
| background: var(--bg-card); | |
| padding: 30px 25px; | |
| border-radius: var(--radius); | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.5); | |
| margin-top: 40px; | |
| } | |
| h2 { | |
| text-align: center; | |
| color: var(--accent-primary); | |
| margin: 0 0 25px 0; | |
| font-size: 1.4rem; | |
| letter-spacing: 0.5px; | |
| } | |
| .form-group { margin-bottom: 20px; } | |
| label { | |
| display: block; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| color: var(--text-main); | |
| } | |
| select, input { | |
| width: 100%; | |
| padding: 14px; | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| background: #222; | |
| color: #fff; | |
| font-size: 1rem; | |
| outline: none; | |
| appearance: none; | |
| } | |
| select:focus, input:focus { border-color: var(--accent-primary); } | |
| .button-group { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 12px; | |
| margin-top: 10px; | |
| } | |
| button { | |
| padding: 15px; | |
| border: none; | |
| border-radius: 12px; | |
| font-size: 0.95rem; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: opacity 0.2s; | |
| } | |
| .btn-calc { background-color: var(--accent-primary); color: #000; } | |
| .btn-clear { background-color: var(--accent-secondary); color: var(--text-dim); border: 1px solid var(--border); } | |
| button:active { opacity: 0.7; } | |
| /* RESULTS SECTION */ | |
| #results { margin-top: 30px; display: none; border-top: 1px solid var(--border); padding-top: 25px; } | |
| .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 25px; } | |
| .item { background: #222; padding: 12px; border-radius: 10px; text-align: center; } | |
| .item span { display: block; font-size: 0.7rem; color: var(--text-dim); text-transform: uppercase; margin-bottom: 4px; } | |
| .item strong { font-size: 1.1rem; color: var(--accent-primary); } | |
| table { width: 100%; border-collapse: collapse; margin-top: 15px; } | |
| th { text-align: left; font-size: 0.75rem; color: var(--text-dim); padding-bottom: 10px; border-bottom: 1px solid var(--border); } | |
| td { padding: 12px 0; border-bottom: 1px solid #2a2a2a; font-size: 0.9rem; } | |
| .val { font-weight: bold; color: var(--accent-primary); text-align: right; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h2>Brew Master</h2> | |
| <div class="form-group"> | |
| <label>Método de Preparo</label> | |
| <select id="method"> | |
| <option value="v60">V60 (1:16)</option> | |
| <option value="french">Prensa Francesa (1:13)</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Volume Final Desejado (ml)</label> | |
| <input type="number" id="volume" placeholder="Ex: 300" inputmode="numeric"> | |
| </div> | |
| <div class="button-group"> | |
| <button class="btn-calc" onclick="calculate()">Calcular</button> | |
| <button class="btn-clear" onclick="clearAll()">Limpar</button> | |
| </div> | |
| <div id="results"> | |
| <div class="grid"> | |
| <div class="item"><span>Pó</span><strong id="r-coffee">-</strong></div> | |
| <div class="item"><span>Moagem</span><strong id="r-grind">-</strong></div> | |
| <div class="item"><span>Água</span><strong id="r-temp">-</strong></div> | |
| <div class="item"><span>Tempo</span><strong id="r-time">-</strong></div> | |
| </div> | |
| <label style="margin-bottom: 15px;">Roteiro de Vertidas</label> | |
| <table> | |
| <thead><tr><th>Tempo</th><th style="text-align: right;">Total Acumulado</th></tr></thead> | |
| <tbody id="r-table"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <script> | |
| const DATA = { | |
| v60: { ratio: 16, grind: "16 clks", temp: "93°C", time: "4:45", steps: [ | |
| { t: "0:00 (Bloom)", p: 0.20 }, { t: "0:30", p: 0.45 }, { t: "1:30", p: 0.75 }, { t: "2:25", p: 1.00 } | |
| ]}, | |
| french: { ratio: 13, grind: "23 clks", temp: "91°C", time: "4:00", steps: [ | |
| { t: "0:00 (Início)", p: 0.50 }, { t: "0:30 (Fim)", p: 1.00 } | |
| ]} | |
| }; | |
| // Load saved volume on start | |
| window.onload = () => { | |
| const saved = localStorage.getItem('lastVolume'); | |
| if(saved) { | |
| document.getElementById('volume').value = saved; | |
| calculate(); | |
| } | |
| }; | |
| function calculate() { | |
| const m = document.getElementById('method').value; | |
| const v = parseFloat(document.getElementById('volume').value); | |
| if(!v) return; | |
| localStorage.setItem('lastVolume', v); | |
| const conf = DATA[m]; | |
| document.getElementById('r-coffee').innerText = (v / conf.ratio).toFixed(1) + "g"; | |
| document.getElementById('r-grind').innerText = conf.grind; | |
| document.getElementById('r-temp').innerText = conf.temp; | |
| document.getElementById('r-time').innerText = conf.time; | |
| const tbody = document.getElementById('r-table'); | |
| tbody.innerHTML = conf.steps.map(s => ` | |
| <tr><td>${s.t}</td><td class="val">${Math.round(v * s.p)} ml</td></tr> | |
| `).join(''); | |
| document.getElementById('results').style.display = 'block'; | |
| } | |
| function clearAll() { | |
| document.getElementById('volume').value = ""; | |
| document.getElementById('results').style.display = 'none'; | |
| localStorage.removeItem('lastVolume'); | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment