nice clean recipes pear.dunkirk.sh
recipes
1
fork

Configure Feed

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

feat: add a nice timer

+104 -65
+19 -2
internal/cooklang/parse.go
··· 2 2 3 3 import ( 4 4 "regexp" 5 + "strconv" 5 6 "strings" 6 7 ) 7 8 ··· 99 100 if unit != "" { 100 101 display = qty + " " + unit 101 102 } 102 - return `<span class="tmr">` + escHTML(display) + `</span>` 103 + secs := timerToSeconds(qty, unit) 104 + return `<span class="tmr" data-seconds="` + strconv.Itoa(secs) + `">` + escHTML(display) + `</span>` 103 105 }) 104 106 105 107 return out ··· 113 115 return s 114 116 } 115 117 116 - 118 + func timerToSeconds(qty, unit string) int { 119 + v, err := strconv.Atoi(qty) 120 + if err != nil { 121 + return 0 122 + } 123 + switch strings.ToLower(unit) { 124 + case "second", "seconds": 125 + return v 126 + case "minute", "minutes", "min", "mins": 127 + return v * 60 128 + case "hour", "hours", "hr", "hrs", "h": 129 + return v * 3600 130 + default: 131 + return v * 60 132 + } 133 + }
+37 -36
ui/static/style.css
··· 213 213 font-weight:600; 214 214 } 215 215 216 - .timer-trigger{cursor:pointer} 217 - .timer-trigger:hover{text-decoration:underline} 218 - 219 216 .timer-widget{ 220 - position:fixed; 221 - bottom:1.5rem; 222 - right:1.5rem; 223 - background:var(--surface); 224 - border:1px solid var(--border); 217 + padding:1rem 1.25rem; 218 + background:var(--check-bg); 225 219 border-radius:var(--radius); 226 - padding:1.25rem; 227 - box-shadow:0 4px 24px rgba(0,0,0,0.08); 228 220 text-align:center; 229 - z-index:100; 230 - min-width:180px; 221 + max-height:0; 222 + overflow:hidden; 223 + opacity:0; 224 + transition:max-height 0.3s ease,opacity 0.3s ease,margin 0.3s ease,padding 0.3s ease; 225 + margin-bottom:0; 226 + padding-top:0; 227 + padding-bottom:0; 231 228 } 232 - .timer-widget.hidden{display:none} 233 - .timer-close{ 234 - position:absolute; 235 - top:0.4rem; 236 - right:0.5rem; 237 - background:none; 238 - border:none; 239 - font-size:1.1rem; 240 - cursor:pointer; 241 - color:var(--text-muted); 242 - line-height:1; 229 + .timer-widget.timer-open{ 230 + max-height:200px; 231 + opacity:1; 232 + margin-bottom:2rem; 233 + padding:1rem 1.25rem; 234 + } 235 + } 236 + .timer-header{ 237 + display:flex; 238 + justify-content:space-between; 239 + align-items:center; 240 + margin-bottom:0.5rem; 243 241 } 244 - .timer-close:hover{color:var(--text)} 245 242 .timer-label{ 246 243 font-family:'Poppins',system-ui,sans-serif; 247 - font-size:0.75rem; 244 + font-size:0.7rem; 248 245 text-transform:uppercase; 249 246 letter-spacing:0.08em; 250 247 color:var(--text-muted); 251 - margin-bottom:0.5rem; 252 248 } 253 249 .timer-display{ 254 250 font-family:'Poppins',system-ui,sans-serif; 255 - font-size:2.5rem; 251 + font-size:2.75rem; 256 252 font-weight:700; 257 - letter-spacing:-0.02em; 253 + letter-spacing:-0.03em; 258 254 line-height:1; 259 255 margin-bottom:0.75rem; 256 + color:var(--text); 260 257 } 261 258 .timer-done{color:var(--accent);animation:pulse 1s ease-in-out infinite} 262 259 @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}} 263 260 .timer-controls{ 264 261 display:flex; 265 - gap:0.4rem; 266 - justify-content:center; 262 + gap:0.5rem; 267 263 } 268 264 .timer-btn{ 269 - padding:0.35rem 0.75rem; 265 + flex:1; 266 + padding:0.4rem 0; 270 267 border:1px solid var(--border); 271 - border-radius:4px; 268 + border-radius:6px; 272 269 background:var(--surface); 273 270 font-family:'Poppins',system-ui,sans-serif; 274 - font-size:0.75rem; 271 + font-size:0.7rem; 275 272 cursor:pointer; 273 + color:var(--text-muted); 274 + transition:background 0.15s,color 0.15s,border-color 0.15s; 276 275 } 277 - .timer-btn:hover{border-color:var(--accent)} 276 + .timer-btn:hover{border-color:var(--accent);background:var(--accent);color:#fff} 277 + .timer-btn:active{transform:scale(0.96)} 278 278 279 - .ing{font-weight:500} 280 - .tmr{font-weight:500} 279 + .tmr{font-weight:500;cursor:pointer;transition:color 0.2s;white-space:nowrap} 280 + .tmr:hover{color:var(--accent)} 281 + .tmr::before{content:"⏲";font-size:0.8em;margin-right:0.4em} 281 282 282 283 .actions{ 283 284 display:flex;
ui/static/timer.mp3

This is a binary file and will not be displayed.

+48 -27
ui/templates/recipe.html
··· 23 23 <div class="recipe-header"> 24 24 <h2>{{.Recipe.Name}}</h2> 25 25 <div class="recipe-meta"> 26 - {{if .Recipe.PrepTime}}<span class="timer-trigger" data-seconds="{{isoToSeconds .Recipe.PrepTime}}" data-label="Prep">&#9201; {{fmtDuration .Recipe.PrepTime}} prep</span>{{end}} 27 - {{if .Recipe.CookTime}}<span class="timer-trigger" data-seconds="{{isoToSeconds .Recipe.CookTime}}" data-label="Cook">&#9201; {{fmtDuration .Recipe.CookTime}} cook</span>{{end}} 26 + {{if .Recipe.PrepTime}}<span>⏱ {{fmtDuration .Recipe.PrepTime}} prep</span>{{end}} 27 + {{if .Recipe.CookTime}}<span>⏱ {{fmtDuration .Recipe.CookTime}} cook</span>{{end}} 28 28 {{if .Recipe.Yield}}<span>Serves {{.Recipe.Yield}}</span>{{end}} 29 29 </div> 30 30 {{if .Recipe.Description}}<p class="description">{{.Recipe.Description}}</p>{{end}} ··· 45 45 {{end}} 46 46 47 47 {{if .Recipe.Instructions}} 48 + <div id="timer-widget" class="timer-widget"> 49 + <div class="timer-header"> 50 + <span class="timer-label" id="timer-label"></span> 51 + </div> 52 + <div class="timer-display" id="timer-display">00:00</div> 53 + <div class="timer-controls"> 54 + <button class="timer-btn" id="timer-toggle" onclick="toggleTimer()">Start</button> 55 + <button class="timer-btn" id="timer-reset" onclick="resetTimer()">Reset</button> 56 + </div> 57 + </div> 48 58 <div class="section"> 49 59 <h3>Instructions</h3> 50 60 <ol class="instruction-list"> ··· 59 69 <a class="btn-primary" href="/export.cook?url={{.TargetURL | urlquery}}">Download .cook</a> 60 70 </div> 61 71 62 - <div id="timer-widget" class="timer-widget hidden"> 63 - <button class="timer-close" onclick="closeTimer()">&times;</button> 64 - <div class="timer-label" id="timer-label"></div> 65 - <div class="timer-display" id="timer-display">00:00</div> 66 - <div class="timer-controls"> 67 - <button class="timer-btn" id="timer-start" onclick="startTimer()">Start</button> 68 - <button class="timer-btn" id="timer-pause" onclick="pauseTimer()" style="display:none">Pause</button> 69 - <button class="timer-btn" id="timer-reset" onclick="resetTimer()">Reset</button> 70 - </div> 71 - </div> 72 - 73 72 <script> 74 73 function toggleCheck(li) { 75 74 const cb = li.querySelector('input[type="checkbox"]'); ··· 105 104 let timerInterval = null; 106 105 let timerRemaining = 0; 107 106 let timerRunning = false; 107 + let timerSound = null; 108 + let timerSoundPlayed = false; 108 109 109 - document.querySelectorAll('.timer-trigger').forEach(el => { 110 + document.querySelectorAll('.tmr').forEach(el => { 110 111 el.addEventListener('click', () => { 111 112 const secs = parseInt(el.dataset.seconds, 10); 112 - const label = el.dataset.label; 113 + const label = el.textContent; 113 114 if (secs > 0) { 114 115 openTimer(secs, label); 115 116 } ··· 119 120 function openTimer(secs, label) { 120 121 timerRemaining = secs; 121 122 timerRunning = false; 123 + timerSoundPlayed = false; 122 124 clearInterval(timerInterval); 123 125 timerInterval = null; 126 + if (!timerSound) timerSound = new Audio('/static/timer.mp3'); 127 + timerSound.pause(); 128 + timerSound.currentTime = 0; 124 129 document.getElementById('timer-label').textContent = label; 125 130 updateTimerDisplay(); 126 - document.getElementById('timer-start').style.display = ''; 127 - document.getElementById('timer-pause').style.display = 'none'; 128 - document.getElementById('timer-widget').classList.remove('hidden'); 131 + document.getElementById('timer-toggle').textContent = 'Start'; 132 + document.getElementById('timer-widget').classList.add('timer-open'); 133 + } 134 + 135 + function toggleTimer() { 136 + if (timerRunning) { 137 + pauseTimer(); 138 + } else { 139 + startTimer(); 140 + } 129 141 } 130 142 131 143 function startTimer() { 132 144 if (timerRunning) return; 133 145 timerRunning = true; 134 - document.getElementById('timer-start').style.display = 'none'; 135 - document.getElementById('timer-pause').style.display = ''; 146 + document.getElementById('timer-toggle').textContent = 'Pause'; 136 147 timerInterval = setInterval(() => { 137 148 timerRemaining--; 138 149 updateTimerDisplay(); 150 + if (timerRemaining === 2 && !timerSoundPlayed) { 151 + timerSoundPlayed = true; 152 + timerSound.play(); 153 + } 139 154 if (timerRemaining <= 0) { 140 155 clearInterval(timerInterval); 141 156 timerInterval = null; 142 157 timerRunning = false; 158 + document.getElementById('timer-toggle').textContent = 'Start'; 143 159 document.getElementById('timer-display').classList.add('timer-done'); 144 160 if ('Notification' in window && Notification.permission === 'granted') { 145 161 new Notification('Timer done!', { body: document.getElementById('timer-label').textContent + ' is done!' }); ··· 152 168 timerRunning = false; 153 169 clearInterval(timerInterval); 154 170 timerInterval = null; 155 - document.getElementById('timer-start').style.display = ''; 156 - document.getElementById('timer-pause').style.display = 'none'; 171 + document.getElementById('timer-toggle').textContent = 'Unpause'; 157 172 } 158 173 159 174 function resetTimer() { 160 175 clearInterval(timerInterval); 161 176 timerInterval = null; 162 177 timerRunning = false; 163 - const triggers = document.querySelectorAll('.timer-trigger'); 178 + timerSoundPlayed = false; 179 + if (timerSound) { timerSound.pause(); timerSound.currentTime = 0; } 180 + const triggers = document.querySelectorAll('.tmr'); 164 181 if (triggers.length > 0) { 165 - const active = [...triggers].find(t => t.dataset.label === document.getElementById('timer-label').textContent); 182 + const active = [...triggers].find(t => t.textContent === document.getElementById('timer-label').textContent); 166 183 if (active) { 167 184 timerRemaining = parseInt(active.dataset.seconds, 10); 168 185 } 169 186 } 170 - document.getElementById('timer-start').style.display = ''; 171 - document.getElementById('timer-pause').style.display = 'none'; 187 + document.getElementById('timer-toggle').textContent = 'Start'; 172 188 document.getElementById('timer-display').classList.remove('timer-done'); 173 189 updateTimerDisplay(); 174 190 } ··· 177 193 clearInterval(timerInterval); 178 194 timerInterval = null; 179 195 timerRunning = false; 180 - document.getElementById('timer-widget').classList.add('hidden'); 196 + timerSoundPlayed = false; 197 + if (timerSound) { timerSound.pause(); timerSound.currentTime = 0; } 198 + document.getElementById('timer-widget').classList.remove('timer-open'); 199 + document.getElementById('timer-display').classList.remove('timer-done'); 200 + timerRemaining = 0; 201 + updateTimerDisplay(); 181 202 } 182 203 183 204 function updateTimerDisplay() {