xtron07/stem-tiny-explorers-hub
0
1<!DOCTYPE html>2<html lang="ms">3<head>4 <meta charset="utf-8" />5 <meta name="viewport" content="width=device-width, initial-scale=1" />6 <title>STEM Tiny Explorers | Aktiviti Prasekolah (4–6)</title>7 <link rel="stylesheet" href="style.css">8<style>9 :root{10 --bg:#f8fafc;--card:#ffffff;--ink:#0f172a;--muted:#64748b;--brand:#22c55e;--brand2:#06b6d4;--warn:#f59e0b;--bad:#ef4444;--good:#16a34a;11 }12 *{box-sizing:border-box}13 html,body{height:100%}14 body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Helvetica,Arial,sans-serif;background:linear-gradient(180deg,#ecfeff,#f0fdf4);color:var(--ink)}15 .app{max-width:1100px;margin:auto;padding:16px}16 header{display:flex;gap:12px;align-items:center;justify-content:space-between;margin-bottom:12px}17 .title{font-weight:800;font-size:clamp(20px,4vw,32px);letter-spacing:.2px}18 .subtitle{color:var(--muted);font-size:clamp(12px,2.5vw,14px)}19 .topbar{display:flex;gap:8px;flex-wrap:wrap}20 button, .btn{cursor:pointer;border:0;background:var(--brand);color:#fff;padding:10px 14px;border-radius:14px;font-weight:700;box-shadow:0 6px 14px rgba(34,197,94,.2)}21 button.secondary{background:var(--brand2);box-shadow:0 6px 14px rgba(6,182,212,.2)}22 button.warn{background:var(--warn)}23 button.ghost{background:#00000008;color:var(--ink);box-shadow:none;border:1px solid #00000012}24 .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}25 .card{background:var(--card);border-radius:18px;padding:16px;box-shadow:0 8px 24px rgba(2,6,23,.08);position:relative}26 .card h3{margin:4px 0 8px}27 .badge{display:inline-block;background:#0000000a;border:1px solid #00000012;color:var(--muted);padding:2px 8px;border-radius:999px;font-size:12px}28 .center{display:grid;place-items:center}29 .big{font-size:64px;font-weight:900}30 .bar{height:10px;background:#00000010;border-radius:999px;overflow:hidden}31 .bar>i{display:block;height:100%;background:linear-gradient(90deg,var(--brand),var(--brand2));width:0}32 .row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}33 .pill{padding:8px 12px;border-radius:999px;background:#00000006;border:1px solid #00000014}34 .area{min-height:160px;border-radius:16px;border:2px dashed #0000001f;background:#ffffffaa;display:flex;align-items:center;justify-content:center;padding:10px}35 .targets{display:flex;gap:10px;flex-wrap:wrap}36 .target{width:100px;height:100px;border-radius:16px;display:grid;place-items:center;border:2px dashed #00000022;background:#f8fafc}37 .shape{width:70px;height:70px;border-radius:12px;background:#22c55e;display:grid;place-items:center;color:#fff;font-weight:800;user-select:none}38 .circle{border-radius:50%}39 .triangle{width:0;height:0;border-left:35px solid transparent;border-right:35px solid transparent;border-bottom:70px solid #06b6d4;background:transparent;border-radius:0}40 .draggable{touch-action:none}41 .kbd{font-variant-numeric:tabular-nums;padding:.2em .5em;border-radius:6px;border:1px solid #00000020;background:#fff}42 .footer{margin-top:14px;color:var(--muted);font-size:12px;text-align:center}43 .hidden{display:none}44 @media (prefers-reduced-motion:no-preference){45 .pop{animation:pop .25s ease}46 @keyframes pop{from{transform:scale(.92);opacity:.7}to{transform:scale(1);opacity:1}}47 }48 </style>49</head>50<body>51 <custom-navbar></custom-navbar>52 <div class="app" id="app">53<header>54 <div>55 <div class="title">STEM Mini‑Playground</div>56 <div class="subtitle">Permainan interaktif untuk kanak‑kanak 4–6 tahun — Matematik, Sains, Kejuruteraan & Logik.</div>57 </div>58 <div class="topbar">59 <button id="btnTeacher" class="secondary" aria-pressed="false">Mod Guru</button>60 <button id="btnReset" class="ghost">Reset Kemajuan</button>61 </div>62 </header>63 <section class="grid" aria-label="Senarai Permainan">64 <!-- Game 1: Kiraan & Nombor -->65 <aktiviti-card 66 badge="Matematik" 67 title="1) Kiraan Belon 🎈" 68 description="Ketuk belon sehingga capai <strong>nombor sasaran</strong>." 69 skills="Kemahiran: nombor 1–10, satu‑ke‑satu"70 id="game1">71 <div class="row" style="justify-content:space-between">72 <div> Sasaran: <span id="g1Target" class="kbd">5</span></div>73 <div> Kiraan Saya: <span id="g1Count" class="kbd">0</span></div>74 </div>75 <div class="center" style="gap:12px;flex-wrap:wrap" id="g1Area" role="group" aria-label="Belon untuk dikira"></div>76 <div class="bar" aria-hidden="true"><i id="g1Bar"></i></div>77 </aktiviti-card>78 79 <!-- Game 2: Bentuk & Warna (Drag & Drop) -->80 <aktiviti-card 81 badge="Kejuruteraan • Visual" 82 title="2) Susun Bentuk 🧩" 83 description="Seret bentuk ke <strong>kotak yang sepadan</strong>." 84 skills="Kemahiran: padanan, motor halus, geometri awal"85 id="game2">86 <div class="targets" id="g2Targets" role="group" aria-label="Kotak sasaran">87 <div class="target" data-accept="square" aria-label="Kotak segi empat"><span>⬛ Kotak</span></div>88 <div class="target" data-accept="circle" aria-label="Kotak bulat"><span>⚪ Bulat</span></div>89 <div class="target" data-accept="triangle" aria-label="Kotak segi tiga"><span>🔺 Segi Tiga</span></div>90 </div>91 <div id="g2Bank" aria-label="Bentuk untuk diseret" style="gap:10px;flex-wrap:wrap"></div>92 <div class="bar"><i id="g2Bar"></i></div>93 </aktiviti-card>94 95 <!-- Game 3: Corak AB/ABC -->96 <aktiviti-card 97 badge="Logik • Matematik" 98 title="3) Lengkapkan Corak 🔷🔶" 99 description="Pilih bentuk seterusnya untuk lengkapkan <strong>corak</strong>." 100 skills="Kemahiran: AB / ABC patterning"101 id="game3">102 <div id="g3Area" style="gap:8px;flex-wrap:wrap"></div>103 <div id="g3Choices" style="margin-top:8px;flex-wrap:wrap"></div>104 <div class="bar"><i id="g3Bar"></i></div>105 </aktiviti-card>106 107 <!-- Game 4: Sains Ringan (Klasifikasi) -->108 <aktiviti-card 109 badge="Sains" 110 title="4) Hidup atau Bukan Hidup 🌱⚽" 111 description="Seret gambar ke kumpulan <strong>Hidup</strong> atau <strong>Bukan Hidup</strong>." 112 skills="Kemahiran: pemerhatian, klasifikasi"113 id="game4">114 <div class="row" style="gap:12px;align-items:flex-start">115 <div class="card" style="flex:1">116 <h4>Hidup</h4>117 <div id="g4Live" data-accept="live" style="min-height:120px"></div>118 </div>119 <div class="card" style="flex:1">120 <h4>Bukan Hidup</h4>121 <div id="g4Non" data-accept="non" style="min-height:120px"></div>122 </div>123 </div>124 <div id="g4Bank" style="gap:10px;flex-wrap:wrap"></div>125 <div class="bar"><i id="g4Bar"></i></div>126 </aktiviti-card>127 </section>128<!-- Teacher Panel -->129 <section id="teacherPanel" class="card hidden" aria-live="polite" style="margin-top:16px">130 <h3>Mod Guru 👩🏫</h3>131 <p>Laporan kemajuan dipasang pada peranti (localStorage). Anda boleh eksport, import, atau reset.</p>132 <div class="row" style="margin:8px 0">133 <button id="btnExport" class="secondary">Eksport JSON</button>134 <input id="importFile" type="file" accept="application/json" class="ghost" />135 <button id="btnTTS" class="ghost">Bacaan Suara: <span id="ttsState">Off</span></button>136 </div>137 <pre id="report" style="white-space:pre-wrap;background:#00000006;padding:12px;border-radius:12px;max-height:240px;overflow:auto"></pre>138 </section>139 </section>140 </div>141 <custom-footer></custom-footer>142 <script src="components/navbar.js"></script>143 <script src="components/footer.js"></script>144 <script src="components/aktiviti-card.js"></script>145<script>146/******************* Util *******************/147 const $ = (q,root=document)=>root.querySelector(q);148 const $$ = (q,root=document)=>Array.from(root.querySelectorAll(q));149 const speak = (txt)=>{150 if(!window._ttsOn) return;151 const u = new SpeechSynthesisUtterance(txt);152 u.lang = 'ms-MY';153 speechSynthesis.cancel();154 speechSynthesis.speak(u);155 }156 const beep = (ok=true)=>{157 try{158 const ctx = new (window.AudioContext||window.webkitAudioContext)();159 const o = ctx.createOscillator();160 const g = ctx.createGain();161 o.connect(g); g.connect(ctx.destination);162 o.type = ok? 'sine':'square';163 o.frequency.value = ok? 880:220;164 g.gain.value = 0.001; g.gain.exponentialRampToValueAtTime(0.1, ctx.currentTime+0.01);165 o.start(); g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime+0.2); o.stop(ctx.currentTime+0.21);166 }catch(e){}167 }168 const store = {169 get(){return JSON.parse(localStorage.getItem('stem_play')||'{}')},170 set(d){localStorage.setItem('stem_play',JSON.stringify(d));renderReport()},171 patch(p){const d=store.get(); Object.assign(d,p); store.set(d)}172 }173 function inc(path){174 const d = store.get();175 const [g,k] = path.split('.')176 d[g] = d[g]||{}; d[g][k] = (d[g][k]||0)+1; store.set(d)177 }178 179 function progressBar(el, value, max){180 const pct = Math.max(0, Math.min(100, Math.round((value/max)*100)));181 el.style.width = pct + '%';182 }183 184 /******************* Game 1: Count Balloons *******************/185 let g1 = {target:5,count:0,max:10};186 const g1Area = $('#g1Area'), g1Count=$('#g1Count'), g1Target=$('#g1Target'), g1Bar=$('#g1Bar');187 function g1Setup(){188 g1.target = Math.floor(3+Math.random()*7); // 3..9189 g1.count = 0; g1Target.textContent=g1.target; g1Count.textContent='0';190 g1Area.innerHTML='';191 const n = 10; // 10 balloons192 for(let i=0;i<n;i++){193 const b = document.createElement('button');194 b.className='btn'; b.style.borderRadius='999px'; b.style.padding='12px 18px';195 b.textContent='🎈'; b.setAttribute('aria-label','Belon');196 b.addEventListener('click',()=>{g1.count++; g1Count.textContent=g1.count; beep(true);197 progressBar(g1Bar,g1.count,g1.target); if(g1.count===g1.target){198 inc('g1.win'); speak('Bagus! Cukup '+g1.target); b.blur(); setTimeout(g1Setup,600);199 } else if(g1.count>g1.target){200 inc('g1.over'); speak('Terlebih. Cuba semula'); beep(false); setTimeout(g1Setup,600);201 }202 });203 g1Area.appendChild(b);204 }205 progressBar(g1Bar,0,g1.target)206 }207 208 /******************* Drag helpers *******************/209 let dragData=null;210 function makeDraggable(el, kind){211 el.draggable=true; el.classList.add('draggable'); el.setAttribute('data-kind',kind);212 el.addEventListener('dragstart',e=>{dragData={kind, el}; e.dataTransfer.setData('text/plain', kind)});213 el.addEventListener('touchstart',e=>{dragData={kind, el};});214 }215 function makeTarget(el, accept){216 el.addEventListener('dragover',e=>{e.preventDefault();});217 el.addEventListener('drop',e=>{e.preventDefault(); const k=e.dataTransfer.getData('text/plain'); tryDrop(el,k)});218 el.addEventListener('touchend',()=>{ if(dragData) tryDrop(el,dragData.kind)});219 function tryDrop(target,k){220 if((target.dataset.accept||'')===k){ target.classList.add('pop'); target.append(makeBadge('✔'));221 beep(true); speak('Betul'); inc('drag.correct'); target.dataset.done='1'; updateBars();222 } else {beep(false); speak('Cuba lagi'); inc('drag.wrong')}223 }224 }225 function makeBadge(t){ const s=document.createElement('span'); s.className='pill'; s.textContent=t; return s }226 227 /******************* Game 2: Shapes *******************/228 const shapes = [229 {kind:'square', make:()=>{const d=document.createElement('div'); d.className='shape'; d.style.background='#22c55e'; return d;}},230 {kind:'circle', make:()=>{const d=document.createElement('div'); d.className='shape circle'; d.style.background='#06b6d4'; return d;}},231 {kind:'triangle', make:()=>{const d=document.createElement('div'); d.className='triangle'; return d;}}232 ];233 const g2Bank=$('#g2Bank'), g2Bar=$('#g2Bar');234 function g2Setup(){235 g2Bank.innerHTML='';236 // create 6 random shapes237 const bag = [];238 for(let i=0;i<6;i++){ const s = shapes[Math.floor(Math.random()*shapes.length)];239 const el = s.make(); makeDraggable(el, s.kind); bag.push(el) }240 shuffle(bag).forEach(el=>g2Bank.appendChild(el));241 $$('#g2Targets .target').forEach(t=>{t.innerHTML='<span>'+t.innerHTML+'</span>'; t.dataset.done=''; makeTarget(t, t.dataset.accept)})242 progressBar(g2Bar,0,3)243 }244 245 /******************* Game 3: Patterns *******************/246 const g3Area=$('#g3Area'), g3Choices=$('#g3Choices'), g3Bar=$('#g3Bar');247 let g3Seq=[], g3Next='';248 function g3Setup(){249 const types=['AB','ABC'];250 const type=types[Math.floor(Math.random()*types.length)];251 const options=['🔷','🔶','🟣'];252 const A=options[0], B=options[1], C=options[2];253 g3Seq = type==='AB' ? [A,B,A,B] : [A,B,C,A,B];254 g3Next = type==='AB' ? A : C; // expected next255 renderPattern();256 renderChoices(options);257 progressBar(g3Bar,0,1)258 }259 function renderPattern(){260 g3Area.innerHTML='';261 g3Seq.forEach(x=>{const s=document.createElement('span'); s.className='pill'; s.textContent=x; g3Area.appendChild(s)});262 const q=document.createElement('span'); q.className='pill'; q.textContent='?'; q.style.borderStyle='dashed'; g3Area.appendChild(q);263 }264 function renderChoices(opts){265 g3Choices.innerHTML='';266 opts.forEach(o=>{267 const b=document.createElement('button'); b.className='btn ghost'; b.textContent=o; b.addEventListener('click',()=>{268 if(o===g3Next){ beep(true); speak('Tepat!'); inc('g3.right'); progressBar(g3Bar,1,1); setTimeout(g3Setup,600)}269 else { beep(false); speak('Belum tepat'); inc('g3.wrong') }270 }); g3Choices.appendChild(b);271 })272 }273 274 /******************* Game 4: Science classify *******************/275 const g4Bank=$('#g4Bank'), g4Live=$('#g4Live'), g4Non=$('#g4Non'), g4Bar=$('#g4Bar');276 const items=[277 {label:'🌱 Pokok', kind:'live'},278 {label:'🐟 Ikan', kind:'live'},279 {label:'🦋 Rama-rama', kind:'live'},280 {label:'⚽ Bola', kind:'non'},281 {label:'🚗 Kereta', kind:'non'},282 {label:'🪑 Kerusi', kind:'non'}283 ];284 function g4Setup(){285 g4Bank.innerHTML=''; [g4Live,g4Non].forEach(c=>{c.innerHTML=''; c.dataset.done=''; makeTarget(c, c.dataset.accept)});286 shuffle(items.slice()).forEach(it=>{287 const b=document.createElement('button'); b.className='btn ghost'; b.textContent=it.label; makeDraggable(b,it.kind); g4Bank.appendChild(b);288 })289 progressBar(g4Bar,0,6)290 }291 292 function updateBars(){293 const g2Done = $$('#g2Targets .target[data-done="1"]').length; progressBar(g2Bar, g2Done, 3);294 const g4Done = (g4Live.querySelectorAll('.pill').length + g4Non.querySelectorAll('.pill').length);295 progressBar(g4Bar, g4Done, 6);296 }297 298 function shuffle(a){ for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]] } return a }299 300 /******************* Teacher Panel & Controls *******************/301 const teacherPanel=$('#teacherPanel'), btnTeacher=$('#btnTeacher');302 const btnReset=$('#btnReset'), btnExport=$('#btnExport'), importFile=$('#importFile');303 const report=$('#report');304 function renderReport(){ report.textContent = JSON.stringify(store.get(), null, 2) }305 306 btnTeacher.addEventListener('click',()=>{307 const on = teacherPanel.classList.toggle('hidden');308 const pressed = !on; btnTeacher.setAttribute('aria-pressed', String(pressed));309 if(pressed){ renderReport(); speak('Mod guru dibuka') }310 });311 312 btnReset.addEventListener('click',()=>{ localStorage.removeItem('stem_play'); renderReport(); speak('Kemajuan direset') })313 btnExport.addEventListener('click',()=>{314 const blob = new Blob([JSON.stringify(store.get(),null,2)], {type:'application/json'});315 const a = document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='stem_progress.json'; a.click(); URL.revokeObjectURL(a.href);316 })317 importFile.addEventListener('change',async (e)=>{318 const f=e.target.files[0]; if(!f) return; const txt=await f.text(); try{ const d=JSON.parse(txt); store.set(d); speak('Laporan diimport') }catch(err){ alert('Fail tidak sah') }319 })320 321 const btnTTS=$('#btnTTS'), ttsState=$('#ttsState');322 btnTTS.addEventListener('click',()=>{ window._ttsOn = !window._ttsOn; ttsState.textContent = window._ttsOn? 'On':'Off' })323 324 /******************* Init *******************/325 function init(){ g1Setup(); g2Setup(); g3Setup(); g4Setup(); renderReport(); }326 window.addEventListener('load', init);327 </script>328 </script>329<script src="https://deepsite.hf.co/deepsite-badge.js"></script>330</body>331</html>332 