Skip to content

Commit 6be82eb

Browse files
committed
hodiny
1 parent b94cd0d commit 6be82eb

3 files changed

Lines changed: 404 additions & 27 deletions

File tree

src/index.html

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,7 @@
7676
<p class="lead">Malé nástroje spustitelné v prohlížeči. Vyberte si jednu z aplikací níže.</p>
7777
<div class="toolbar">
7878
<input id="q" class="search" type="search" placeholder="Hledat (název, popis, tagy)…" aria-label="Hledat">
79-
<button class="chip" data-cat="all" aria-pressed="true">Vše</button>
80-
<button class="chip" data-cat="time">⏱️ Čas</button>
81-
<button class="chip" data-cat="fitness">🏃‍♀️ Tréninky</button>
82-
<button class="chip" data-cat="encoding">🧮 Kódování</button>
83-
<button class="chip" data-cat="crypto">🔐 Kryptografie</button>
84-
<button class="chip" data-cat="analysis">🧠 Analýza</button>
79+
<div id="chips" class="chips" aria-label="Kategorie"><!-- generated category chips will go here --></div>
8580
</div>
8681

8782
<main id="root"></main>
@@ -153,14 +148,43 @@ <h3>${t.name} ${t.difficulty||''}</h3>
153148

154149
// Události
155150
$('#q').addEventListener('input', e => { STATE.q = e.target.value; render(); });
156-
$$('.chip').forEach(ch => ch.addEventListener('click', ()=>{
157-
$$('.chip').forEach(c=>c.setAttribute('aria-pressed','false'));
158-
ch.setAttribute('aria-pressed','true');
159-
STATE.cat = ch.dataset.cat;
160-
render();
161-
}));
162151

163-
load();
152+
// Chips: generate from STATE.data and use event delegation for clicks
153+
function generateChips() {
154+
const container = document.getElementById('chips');
155+
if (!container) return;
156+
container.innerHTML = '';
157+
// add "all" button
158+
const allBtn = document.createElement('button');
159+
allBtn.className = 'chip'; allBtn.dataset.cat = 'all'; allBtn.setAttribute('aria-pressed','true');
160+
allBtn.type = 'button'; allBtn.textContent = 'Vše';
161+
container.appendChild(allBtn);
162+
163+
const cats = (STATE.data && STATE.data.categories) ? STATE.data.categories : [];
164+
for (const cat of cats) {
165+
const btn = document.createElement('button');
166+
btn.className = 'chip'; btn.type = 'button'; btn.dataset.cat = cat.id;
167+
btn.textContent = (cat.chip || cat.name || cat.id);
168+
btn.setAttribute('aria-pressed','false');
169+
container.appendChild(btn);
170+
}
171+
}
172+
173+
// Delegate clicks on chips
174+
const chipsContainer = document.getElementById('chips');
175+
if (chipsContainer) {
176+
chipsContainer.addEventListener('click', e => {
177+
const ch = e.target.closest('.chip');
178+
if (!ch) return;
179+
chipsContainer.querySelectorAll('.chip').forEach(c=>c.setAttribute('aria-pressed','false'));
180+
ch.setAttribute('aria-pressed','true');
181+
STATE.cat = ch.dataset.cat;
182+
render();
183+
});
184+
}
185+
186+
// load data and then generate chips + render
187+
load().then(()=>{ generateChips(); render(); });
164188
</script>
165189
<script type="module">
166190
// Theme wiring: initialize theme manager and sync header radios

src/time/clock.html

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
<!doctype html>
2+
<html lang="cs">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>🕒 Hodiny – digitální / ručičkové / 7-segment</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<link rel="stylesheet" href="../css/style.css">
8+
<link rel="stylesheet" href="../css/hw-ui-kit.css">
9+
<style>
10+
/* ===== Layout & UI ===== */
11+
.app { max-width: 980px; margin: 24px auto; padding: 12px; }
12+
.bar { display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
13+
.bar .spacer { flex:1 }
14+
.view { display:none }
15+
.view.active { display:block }
16+
.tabs button[aria-pressed="true"] { outline: 2px solid #3b82f6; }
17+
18+
/* ===== Digitální (běžný text) ===== */
19+
.big { font-size: clamp(32px, 8vw, 96px); letter-spacing: 0.02em; text-align:center; }
20+
.date { text-align:center; opacity:.75; margin-top: 6px }
21+
22+
/* ===== Ručičkové (canvas kulaté) ===== */
23+
.analog-wrap { display:grid; place-items:center; padding: 8px; }
24+
canvas#analog { width: min(90vw, 460px); height: min(90vw, 460px); max-width: 460px; max-height: 460px; }
25+
26+
.seg-display {
27+
justify-content:center;
28+
}
29+
30+
/* dvojtečka mezi HH:MM:SS */
31+
.colon { width: 16px; position: relative; }
32+
.colon::before, .colon::after {
33+
content: ""; position: absolute; left: 3px; width: 10px; height: 10px; border-radius: 50%;
34+
background: var(--seg-off);
35+
}
36+
.colon.on::before { background: var(--seg-on); box-shadow: 0 0 10px var(--seg-glow); }
37+
.colon.on::after { background: var(--seg-on); box-shadow: 0 0 10px var(--seg-glow); }
38+
.colon::before { top: 22px; } .colon::after { bottom: 22px; }
39+
40+
/* drobné utility */
41+
.muted { opacity:.7 }
42+
.row { display:flex; gap:10px; align-items:center; flex-wrap:wrap }
43+
.right { margin-left:auto }
44+
label { display:flex; gap:6px; align-items:center }
45+
input[type="checkbox"]{ transform: scale(1.1) }
46+
</style>
47+
</head>
48+
<body>
49+
<div class="app">
50+
<div class="card">
51+
<script type="module" src="../js/site-header.js"></script>
52+
<site-header title="🕒 Aktuální čas"></site-header>
53+
</div>
54+
<div class="card">
55+
<div class="bar">
56+
<div class="spacer"></div>
57+
<div class="row">
58+
<label><input id="toggleSeconds" type="checkbox" checked> Sekundy</label>
59+
<label><input id="toggle24h" type="checkbox" checked> 24h</label>
60+
<label><input id="toggleBlink" type="checkbox" checked> Blikající :</label>
61+
</div>
62+
</div>
63+
<div class="tabs" role="tablist" aria-label="Režim zobrazení">
64+
<button class="tab" data-view="digital" aria-pressed="true">Digitální</button>
65+
<button class="tab" data-view="analog">Ručičkové</button>
66+
<button class="tab" data-view="seg7">7-segment</button>
67+
</div>
68+
</div>
69+
70+
<!-- Digitální -->
71+
<section id="view-digital" class="card view active" role="tabpanel" aria-label="Digitální">
72+
<div class="big" id="textClock">00:00:00</div>
73+
<div class="date" id="dateText"></div>
74+
</section>
75+
76+
<!-- Analog -->
77+
<section id="view-analog" class="card view" role="tabpanel" aria-label="Ručičkové">
78+
<div class="analog-wrap">
79+
<canvas id="analog" width="460" height="460"></canvas>
80+
</div>
81+
</section>
82+
83+
<!-- 7-segment -->
84+
<section id="view-seg7" class="card view" role="tabpanel" aria-label="7-segment">
85+
<div class="seg-display" id="segClock"></div>
86+
<div class="date" id="dateTextSeg"></div>
87+
</section>
88+
</div>
89+
90+
<script type="module">
91+
import { createSevenSeg } from '../js/sevenseg.js';
92+
// ===== Helpers =====
93+
const $ = (s, r=document)=>r.querySelector(s);
94+
const pad2 = n => n.toString().padStart(2,'0');
95+
96+
// ===== State & options =====
97+
const opts = {
98+
showSeconds: true,
99+
use24h: true,
100+
blinkColon: true
101+
};
102+
103+
// Restore from localStorage
104+
opts.showSeconds = localStorage.getItem('clock_showSeconds') !== '0';
105+
opts.use24h = localStorage.getItem('clock_use24h') !== '0';
106+
opts.blinkColon = localStorage.getItem('clock_blinkColon') !== '0';
107+
$('#toggleSeconds').checked = opts.showSeconds;
108+
$('#toggle24h').checked = opts.use24h;
109+
$('#toggleBlink').checked = opts.blinkColon;
110+
111+
// ===== Tabs (views) =====
112+
const views = ['digital','analog','seg7'];
113+
const showView = (v) => {
114+
views.forEach(id=>{
115+
$('#view-'+id).classList.toggle('active', id===v);
116+
$(`.tab[data-view="${id}"]`).setAttribute('aria-pressed', id===v ? 'true' : 'false');
117+
});
118+
localStorage.setItem('clock_view', v);
119+
// If switching to analog view, ensure the canvas is sized and drawn immediately.
120+
if (v === 'analog') {
121+
// Resize backing store (ResizeObserver will also do this when visible)
122+
try { resizeAnalog(); } catch(e) { /* ignore if resizeAnalog not ready */ }
123+
try { drawAnalog(new Date()); } catch(e) { console.warn('drawAnalog failed on view switch', e); }
124+
}
125+
};
126+
const savedView = localStorage.getItem('clock_view');
127+
if (views.includes(savedView)) showView(savedView);
128+
document.querySelectorAll('.tab').forEach(btn=>{
129+
btn.addEventListener('click', ()=> showView(btn.dataset.view));
130+
});
131+
132+
// ===== Date formatting (Czech locale) =====
133+
function formatDate(d){
134+
return d.toLocaleDateString('cs-CZ', { weekday:'long', year:'numeric', month:'long', day:'numeric' });
135+
}
136+
137+
// ===== Digital text clock =====
138+
const textClock = $('#textClock');
139+
const dateText = $('#dateText');
140+
function formatTime(d){
141+
let h = d.getHours();
142+
const m = d.getMinutes();
143+
const s = d.getSeconds();
144+
let suffix = '';
145+
if (!opts.use24h) {
146+
suffix = h>=12 ? ' PM' : ' AM';
147+
h = (h%12)||12;
148+
}
149+
const base = `${pad2(h)}:${pad2(m)}${opts.showSeconds?':'+pad2(s):''}${suffix}`;
150+
if (!opts.blinkColon) return base;
151+
// blink : each second for both digital and 7seg visual
152+
const colon = (d.getMilliseconds()<500) ? ':' : ' ';
153+
return opts.showSeconds
154+
? `${pad2(h)}${colon}${pad2(m)}${colon}${pad2(s)}${suffix}`
155+
: `${pad2(h)}${colon}${pad2(m)}${suffix}`;
156+
}
157+
158+
// ===== Analog clock (canvas) =====
159+
const analog = $('#analog');
160+
const ag = analog.getContext('2d');
161+
function drawAnalog(d){
162+
const w = analog.width, h = analog.height;
163+
const cx = w/2, cy = h/2, R = Math.min(cx, cy) - 12;
164+
ag.clearRect(0,0,w,h);
165+
166+
// face
167+
ag.fillStyle = getComputedStyle(document.body).getPropertyValue('--panel-bg') || '#0d1115';
168+
ag.strokeStyle = '#223142';
169+
ag.lineWidth = 8;
170+
ag.beginPath(); ag.arc(cx, cy, R, 0, Math.PI*2); ag.fill(); ag.stroke();
171+
172+
// ticks
173+
ag.save();
174+
ag.translate(cx, cy);
175+
for (let i=0;i<60;i++){
176+
const a = i * Math.PI/30;
177+
const r1 = R-12, r2 = i%5===0 ? R-32 : R-22;
178+
ag.lineWidth = i%5===0 ? 4 : 2;
179+
ag.strokeStyle = i%5===0 ? '#cfe1ff' : '#6b7f95';
180+
ag.beginPath();
181+
ag.moveTo(r2*Math.cos(a), r2*Math.sin(a));
182+
ag.lineTo(r1*Math.cos(a), r1*Math.sin(a));
183+
ag.stroke();
184+
}
185+
ag.restore();
186+
187+
// hands
188+
const ms = d.getMilliseconds();
189+
const sec = d.getSeconds() + ms/1000;
190+
const min = d.getMinutes() + sec/60;
191+
const hr = (d.getHours()%12) + min/60;
192+
193+
// hour hand
194+
drawHand(hr * 30 * Math.PI/180, R*0.5, 7, '#e7eef6');
195+
// minute hand
196+
drawHand(min * 6 * Math.PI/180, R*0.72, 5, '#9fb4cb');
197+
// second hand (show only if enabled seconds)
198+
if (opts.showSeconds) drawHand(sec * 6 * Math.PI/180, R*0.78, 2, '#f43f5e');
199+
200+
// center cap
201+
ag.fillStyle = '#e7eef6';
202+
ag.beginPath(); ag.arc(cx, cy, 6, 0, Math.PI*2); ag.fill();
203+
204+
function drawHand(angle, length, width, color){
205+
ag.save();
206+
ag.translate(cx, cy);
207+
ag.rotate(angle - Math.PI/2);
208+
ag.strokeStyle = color;
209+
ag.lineWidth = width;
210+
ag.lineCap = 'round';
211+
ag.beginPath(); ag.moveTo(-length*0.12, 0); ag.lineTo(length, 0); ag.stroke();
212+
ag.restore();
213+
}
214+
}
215+
216+
// ===== 7-segment clock =====
217+
const segRoot = $('#segClock');
218+
// build HH : MM : SS
219+
function makeDigit(){
220+
const d = document.createElement('div');
221+
d.className = 'digit';
222+
['a','b','c','d','e','f','g'].forEach(s=>{
223+
const el = document.createElement('div'); el.className = `seg ${s}`; d.appendChild(el);
224+
});
225+
const dp = document.createElement('div'); dp.className = 'dp'; d.appendChild(dp);
226+
return d;
227+
}
228+
function makeColon(){ const c = document.createElement('div'); c.className='colon'; return c; }
229+
const digits = [];
230+
function buildSegLayout(){
231+
segRoot.innerHTML = '';
232+
const order = opts.showSeconds ? ['H1','H2',':','M1','M2',':','S1','S2'] : ['H1','H2',':','M1','M2'];
233+
order.forEach(tok=>{
234+
if (tok === ':') segRoot.appendChild(makeColon());
235+
else { const d = makeDigit(); digits.push(d); segRoot.appendChild(d); }
236+
});
237+
}
238+
// create the DOM layout and wire the reusable 7-seg helper
239+
let segDisplay = null;
240+
241+
// initial layout
242+
buildSegLayout();
243+
try { segDisplay = createSevenSeg(segRoot); } catch(e) { console.warn('sevenseg init failed', e); }
244+
245+
// ===== Update loop =====
246+
function tick(){
247+
const now = new Date();
248+
try {
249+
// digital text
250+
textClock.textContent = formatTime(now);
251+
dateText.textContent = formatDate(now);
252+
253+
// analog
254+
if ($('#view-analog')?.classList.contains('active')) drawAnalog(now);
255+
256+
// 7seg
257+
if ($('#view-seg7')?.classList.contains('active')){
258+
const h = now.getHours();
259+
const m = now.getMinutes();
260+
const s = now.getSeconds();
261+
const parts = opts.use24h ? [pad2(h), pad2(m), pad2(s)] :
262+
[pad2((h%12)||12), pad2(m), pad2(s)];
263+
264+
const str = opts.showSeconds ? parts.join('') : (parts[0] + parts[1]);
265+
// delegate rendering to the shared sevenseg module
266+
if (segDisplay && typeof segDisplay.setValue === 'function') {
267+
segDisplay.setValue(str);
268+
} else {
269+
// fallback: naive per-digit update (keep previous behavior)
270+
for (let i=0;i<digits.length;i++){
271+
const ch = str[i] ?? '0';
272+
// update segment classes directly (best-effort)
273+
digits[i].querySelectorAll('.seg').forEach(segEl=> segEl.classList.toggle('on', false));
274+
// try to use DEFAULT_MAP if available
275+
try {
276+
// lazy import of DEFAULT_MAP not necessary here; skip fallback mapping
277+
} catch(_){}
278+
}
279+
}
280+
// dvojtečky blikání
281+
document.querySelectorAll('.colon').forEach(col=>{
282+
col.classList.toggle('on', !opts.blinkColon || (now.getMilliseconds()<500));
283+
});
284+
$('#dateTextSeg').textContent = formatDate(now);
285+
}
286+
} catch (err) {
287+
// Ensure a single exception doesn't stop the animation loop entirely
288+
console.error('Clock tick error:', err);
289+
} finally {
290+
requestAnimationFrame(tick);
291+
}
292+
}
293+
294+
// start the loop
295+
requestAnimationFrame(tick);
296+
297+
// ===== Options UI =====
298+
$('#toggleSeconds').addEventListener('change', (e)=>{
299+
opts.showSeconds = e.target.checked;
300+
localStorage.setItem('clock_showSeconds', opts.showSeconds?'1':'0');
301+
digits.length = 0; // reset store
302+
buildSegLayout();
303+
try { segDisplay = createSevenSeg(segRoot); } catch(e) { console.warn('sevenseg init failed after rebuild', e); }
304+
});
305+
$('#toggle24h').addEventListener('change', (e)=>{
306+
opts.use24h = e.target.checked;
307+
localStorage.setItem('clock_use24h', opts.use24h?'1':'0');
308+
});
309+
$('#toggleBlink').addEventListener('change', (e)=>{
310+
opts.blinkColon = e.target.checked;
311+
localStorage.setItem('clock_blinkColon', opts.blinkColon?'1':'0');
312+
});
313+
314+
// Resize canvas backing store to device pixels
315+
const resizeAnalog = ()=>{
316+
const dpr = Math.max(1, window.devicePixelRatio||1);
317+
const box = analog.getBoundingClientRect();
318+
analog.width = Math.round(box.width * dpr);
319+
analog.height = Math.round(box.height * dpr);
320+
};
321+
if (window.ResizeObserver) {
322+
new ResizeObserver(resizeAnalog).observe(analog);
323+
} else {
324+
// Fallback for older browsers
325+
window.addEventListener('resize', resizeAnalog);
326+
}
327+
// perform initial sizing
328+
resizeAnalog();
329+
</script>
330+
</body>
331+
</html>

0 commit comments

Comments
 (0)