CoolFace
Apppublic

xtron07/stem-tiny-explorers-hub

sourceHugging Faceupdated 11mo agoView on Hugging Face
0likes
index.html332 linesDownload Raw Back to root
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