PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.7
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.7
2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / blocks / pomodoro-timer / frontend.js

frontend.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.7, at blocks/pomodoro-timer/frontend.js

296 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function () {
2 'use strict';
3
4 var MODES = { WORK: 'work', SHORT: 'short', LONG: 'long' };
5
6 function pad2(n) { return String(Math.floor(Math.max(0, n))).padStart(2, '0'); }
7 function fmtTime(s) { return pad2(Math.floor(s / 60)) + ':' + pad2(s % 60); }
8
9 var _typoKeys = {
10 family:'font-family', weight:'font-weight', style:'font-style',
11 decoration:'text-decoration', transform:'text-transform',
12 sizeDesktop:'font-size-d', sizeTablet:'font-size-t', sizeMobile:'font-size-m',
13 lineHeightDesktop:'line-height-d', lineHeightTablet:'line-height-t', lineHeightMobile:'line-height-m',
14 letterSpacingDesktop:'letter-spacing-d', letterSpacingTablet:'letter-spacing-t', letterSpacingMobile:'letter-spacing-m',
15 wordSpacingDesktop:'word-spacing-d', wordSpacingTablet:'word-spacing-t', wordSpacingMobile:'word-spacing-m'
16 };
17 function typoCssVarsForEl(el, obj, prefix) {
18 if (!obj || typeof obj !== 'object') return;
19 Object.keys(_typoKeys).forEach(function (k) {
20 var v = obj[k];
21 if (v === undefined || v === '' || v === null) return;
22 if (k === 'sizeDesktop' || k === 'sizeTablet' || k === 'sizeMobile') v = v + (obj.sizeUnit || 'px');
23 else if (k === 'lineHeightDesktop' || k === 'lineHeightTablet' || k === 'lineHeightMobile') v = v + (obj.lineHeightUnit || '');
24 else if (k === 'letterSpacingDesktop' || k === 'letterSpacingTablet' || k === 'letterSpacingMobile') v = v + (obj.letterSpacingUnit || 'px');
25 else if (k === 'wordSpacingDesktop' || k === 'wordSpacingTablet' || k === 'wordSpacingMobile') v = v + (obj.wordSpacingUnit || 'px');
26 el.style.setProperty(prefix + _typoKeys[k], String(v));
27 });
28 }
29
30 function initPomodoro(app) {
31 var opts = {};
32 try { opts = JSON.parse(app.getAttribute('data-opts') || '{}'); } catch(e) {}
33
34 var accentColor = opts.accentColor || '#6c3fb5';
35 var breakColor = opts.breakColor || '#10b981';
36 var longBreakColor = opts.longBreakColor || '#3b82f6';
37 var ringBg = opts.ringBg || '#e5e7eb';
38 var cardBg = opts.cardBg || '#ffffff';
39 var timerColor = opts.TimerColor || '#1f2937';
40 var labelColor = opts.labelColor || '#6b7280';
41 var btnStartBg = opts.btnStartBg || '#6c3fb5';
42 var btnStartColor = opts.btnStartColor || '#ffffff';
43 var btnSecBg = opts.btnSecBg || '#f3f4f6';
44 var btnSecColor = opts.btnSecColor || '#374151';
45 var titleColor = opts.titleColor || '#1f2937';
46 var subtitleColor = opts.subtitleColor || '#6b7280';
47 var sectionBg = opts.sectionBg || '';
48 var maxWidth = opts.maxWidth || 440;
49 var cardRadius = opts.cardRadius || 20;
50 var btnRadius = opts.btnRadius || 100;
51 var titleSize = opts.titleSize || 28;
52 var timerSize = opts.timerSize || 56;
53 var ringSize = opts.ringSize || 220;
54 var ringThick = opts.ringThickness || 14;
55 var padTop = opts.paddingTop || 60;
56 var padBot = opts.paddingBottom || 60;
57 var showTitle = opts.showTitle !== false;
58 var showSub = opts.showSubtitle !== false;
59 var showCycles = opts.showCycleCount !== false;
60 var showModeLabel = opts.showModeLabel !== false;
61 var workMinutes = opts.workMinutes || 25;
62 var shortBreak = opts.shortBreak || 5;
63 var longBreak = opts.longBreak || 15;
64 var cyclesBeforeLong = opts.cyclesBeforeLong || 4;
65
66 var workSecs = workMinutes * 60;
67 var shortSecs = shortBreak * 60;
68 var longSecs = longBreak * 60;
69
70 var mode = MODES.WORK;
71 var secs = workSecs;
72 var totalForMode = workSecs;
73 var running = false;
74 var cycles = 0;
75 var interval = null;
76
77 var modeLabels = { work: 'Focus Time', short: 'Short Break', long: 'Long Break' };
78 var modeColors = function() {
79 return { work: accentColor, short: breakColor, long: longBreakColor };
80 };
81
82 // --- DOM build ---
83 app.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
84 if (sectionBg) app.style.background = sectionBg;
85 app.style.paddingTop = padTop + 'px';
86 app.style.paddingBottom = padBot + 'px';
87
88 var card = document.createElement('div');
89 card.className = 'bkbg-pom-card';
90 card.style.cssText = 'background:' + cardBg + ';border-radius:' + cardRadius + 'px;padding:40px 32px;max-width:' + maxWidth + 'px;margin:0 auto;';
91 typoCssVarsForEl(card, opts.titleTypo, '--bkbg-pom-tt-');
92 typoCssVarsForEl(card, opts.subtitleTypo, '--bkbg-pom-st-');
93 app.appendChild(card);
94
95 // Header
96 if (showTitle && opts.title) {
97 var titleEl = document.createElement('div');
98 titleEl.className = 'bkbg-pom-title';
99 titleEl.style.cssText = 'color:' + titleColor + ';margin-bottom:6px;';
100 titleEl.textContent = opts.title;
101 card.appendChild(titleEl);
102 }
103 if (showSub && opts.subtitle) {
104 var subEl = document.createElement('div');
105 subEl.className = 'bkbg-pom-subtitle';
106 subEl.style.cssText = 'color:' + subtitleColor + ';margin-bottom:20px;';
107 subEl.textContent = opts.subtitle;
108 card.appendChild(subEl);
109 }
110
111 // Mode tabs
112 var tabWrap = document.createElement('div');
113 tabWrap.className = 'bkbg-pom-mode-tabs';
114 card.appendChild(tabWrap);
115 var tabBtns = {};
116 Object.values(MODES).forEach(function(m) {
117 var btn = document.createElement('button');
118 btn.className = 'bkbg-pom-tab-btn';
119 btn.textContent = modeLabels[m];
120 btn.addEventListener('click', function() { switchMode(m); });
121 tabBtns[m] = btn;
122 tabWrap.appendChild(btn);
123 });
124
125 // SVG Ring
126 var NS = 'http://www.w3.org/2000/svg';
127 var r = (ringSize - ringThick) / 2;
128 var circ = 2 * Math.PI * r;
129
130 var ringWrap = document.createElement('div');
131 ringWrap.className = 'bkbg-pom-ring-wrap';
132 ringWrap.style.cssText = 'width:' + ringSize + 'px;height:' + ringSize + 'px;';
133 card.appendChild(ringWrap);
134
135 var svg = document.createElementNS(NS, 'svg');
136 svg.setAttribute('width', ringSize);
137 svg.setAttribute('height', ringSize);
138 svg.style.transform = 'rotate(-90deg)';
139 svg.style.display = 'block';
140 ringWrap.appendChild(svg);
141
142 var trackCircle = document.createElementNS(NS, 'circle');
143 trackCircle.setAttribute('cx', ringSize / 2);
144 trackCircle.setAttribute('cy', ringSize / 2);
145 trackCircle.setAttribute('r', r);
146 trackCircle.setAttribute('fill', 'none');
147 trackCircle.setAttribute('stroke', ringBg);
148 trackCircle.setAttribute('stroke-width', ringThick);
149 svg.appendChild(trackCircle);
150
151 var progressCircle = document.createElementNS(NS, 'circle');
152 progressCircle.setAttribute('cx', ringSize / 2);
153 progressCircle.setAttribute('cy', ringSize / 2);
154 progressCircle.setAttribute('r', r);
155 progressCircle.setAttribute('fill', 'none');
156 progressCircle.setAttribute('stroke-width', ringThick);
157 progressCircle.setAttribute('stroke-linecap', 'round');
158 progressCircle.setAttribute('stroke-dasharray', circ);
159 progressCircle.style.transition = 'stroke-dashoffset 0.5s linear, stroke 0.3s';
160 svg.appendChild(progressCircle);
161
162 var ringInner = document.createElement('div');
163 ringInner.className = 'bkbg-pom-ring-inner';
164 ringWrap.style.position = 'relative';
165 var timeDisplay = document.createElement('div');
166 timeDisplay.className = 'bkbg-pom-time';
167 timeDisplay.style.cssText = 'font-size:' + timerSize + 'px;font-weight:800;color:' + timerColor + ';line-height:1;font-variant-numeric:tabular-nums;font-family:\'SF Mono\',\'Roboto Mono\',monospace;';
168 var modeLbl = document.createElement('div');
169 modeLbl.className = 'bkbg-pom-mode-label';
170 ringInner.appendChild(timeDisplay);
171 if (showModeLabel) ringInner.appendChild(modeLbl);
172 ringWrap.appendChild(ringInner);
173
174 // Controls
175 var controls = document.createElement('div');
176 controls.className = 'bkbg-pom-controls';
177 card.appendChild(controls);
178
179 var startBtn = document.createElement('button');
180 startBtn.className = 'bkbg-pom-btn-start';
181 startBtn.style.cssText = 'background:' + btnStartBg + ';color:' + btnStartColor + ';border-radius:' + btnRadius + 'px;';
182 controls.appendChild(startBtn);
183
184 var resetBtn = document.createElement('button');
185 resetBtn.className = 'bkbg-pom-btn-reset';
186 resetBtn.textContent = '↺ Reset';
187 resetBtn.style.cssText = 'background:' + btnSecBg + ';color:' + btnSecColor + ';border-radius:' + btnRadius + 'px;';
188 resetBtn.addEventListener('click', function() { switchMode(mode); });
189 controls.appendChild(resetBtn);
190
191 // Cycles
192 var cyclesWrap;
193 var cycleDots = [];
194 var cycleCountEl;
195 if (showCycles) {
196 cyclesWrap = document.createElement('div');
197 cyclesWrap.className = 'bkbg-pom-cycles';
198 var cycleLbl = document.createElement('span');
199 cycleLbl.className = 'bkbg-pom-cycle-label';
200 cycleLbl.style.color = labelColor;
201 cycleLbl.textContent = 'Sessions:';
202 cyclesWrap.appendChild(cycleLbl);
203 for (var i = 0; i < cyclesBeforeLong; i++) {
204 var dot = document.createElement('div');
205 dot.className = 'bkbg-pom-cycle-dot';
206 dot.style.background = '#e5e7eb';
207 cycleDots.push(dot);
208 cyclesWrap.appendChild(dot);
209 }
210 cycleCountEl = document.createElement('span');
211 cycleCountEl.className = 'bkbg-pom-cycle-count';
212 cycleCountEl.style.color = labelColor;
213 cyclesWrap.appendChild(cycleCountEl);
214 card.appendChild(cyclesWrap);
215 }
216
217 function getModeColor() { return modeColors()[mode]; }
218 function getTotalFor(m) { return m===MODES.WORK?workSecs:m===MODES.SHORT?shortSecs:longSecs; }
219
220 function render() {
221 var mc = getModeColor();
222 // tabs
223 Object.values(MODES).forEach(function(m) {
224 var active = m === mode;
225 var c = modeColors()[m];
226 tabBtns[m].style.background = active ? c : 'transparent';
227 tabBtns[m].style.color = active ? '#fff' : c;
228 tabBtns[m].style.border = '2px solid ' + (active ? c : '#e5e7eb');
229 });
230 // ring
231 var progress = 1 - secs / totalForMode;
232 progressCircle.setAttribute('stroke', mc);
233 progressCircle.setAttribute('stroke-dashoffset', circ * (1 - progress));
234 // timer
235 timeDisplay.textContent = fmtTime(secs);
236 if (showModeLabel) { modeLbl.textContent = modeLabels[mode]; modeLbl.style.color = mc; }
237 // button
238 startBtn.textContent = running ? '⏸ Pause' : (secs < totalForMode ? '▶ Resume' : '▶ Start');
239 // dots
240 if (showCycles) {
241 var inCycle = cycles % cyclesBeforeLong;
242 cycleDots.forEach(function(d, i) {
243 d.style.background = i < inCycle ? accentColor : '#e5e7eb';
244 });
245 cycleCountEl.textContent = '(' + cycles + ' done)';
246 }
247 }
248
249 function switchMode(m) {
250 if (interval) clearInterval(interval);
251 interval = null;
252 running = false;
253 mode = m;
254 totalForMode = getTotalFor(m);
255 secs = totalForMode;
256 render();
257 }
258
259 startBtn.addEventListener('click', function() {
260 running = !running;
261 if (running) {
262 interval = setInterval(function() {
263 secs--;
264 if (secs <= 0) {
265 secs = 0;
266 clearInterval(interval);
267 interval = null;
268 running = false;
269 if (mode === MODES.WORK) {
270 cycles++;
271 if (cycles % cyclesBeforeLong === 0) switchMode(MODES.LONG);
272 else switchMode(MODES.SHORT);
273 } else {
274 switchMode(MODES.WORK);
275 }
276 return;
277 }
278 render();
279 }, 1000);
280 } else {
281 if (interval) clearInterval(interval);
282 interval = null;
283 }
284 render();
285 });
286
287 Object.values(MODES).forEach(function(m) {
288 tabBtns[m].addEventListener('click', function() { switchMode(m); });
289 });
290
291 switchMode(MODES.WORK);
292 }
293
294 document.querySelectorAll('.bkbg-pom-app').forEach(initPomodoro);
295 })();
296