Generator card Wi‑Fi cu QR

Explorați soluția noastră în categoria: JavaScript

Detalii Tehnice

Generator card Wi-Fi cu QR: previzualizare live, export PNG și print rapid. Partajează rețelele Wi-Fi elegant și simplu.

Acest proiect open-source este un generator de carduri Wi-Fi cu cod QR, creat pentru a oferi o soluție rapidă și elegantă de partajare a accesului la rețele wireless.

🔑 Funcționalități principale:

  • Previzualizare în timp real a cardului pe același canvas care se folosește la export.
  • Generare automată de cod QR Wi-Fi, pe baza SSID-ului și a parolei introduse.
  • Suport pentru toate tipurile de securitate (WPA/WPA2/WPA3, WEP, fără parolă).
  • Posibilitatea de a personaliza titlul cardului și culoarea de accent.
  • Butoane rapide pentru:
  • Generare/actualizare card
  • Copiere string Wi-Fi (pentru partajare rapidă)
  • Descărcare card în format PNG (fără popup-uri inutile)
  • Print direct cu layout stabil și compatibilitate maximă

🎨 Design modern și responsiv

  • Interfață intuitivă cu layout pe grid și efect 3D tilt pentru previzualizare.
  • Stil vizual curat, bazat pe CSS variabile pentru culori și accente.
  • Card generat pe canvas unic, astfel încât aspectul din preview este identic cu cel exportat.

⚙️ Tehnologie folosită:

  • HTML5, CSS3 și JavaScript pur (fără dependențe grele).
  • Generare QR prin librărie CDN (cu fallback la un encoder minimal).
  • Export PNG și integrare print bazate pe canvas.toDataURL() (maximă compatibilitate).

📦 Utilizare:

  • Ideal pentru rețele de oaspeți, spații publice, cafenele, birouri sau evenimente.
  • Poate fi oferit ca resursă gratuită pe website-uri educaționale, bloguri tech, sau ca tool practic pentru utilizatori.


<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wi‑Fi QR — Rebuild (1‑canvas, download/print garantat)</title>
<meta name="description" content="Generator card Wi‑Fi cu QR. Previzualizare în timp real pe același canvas folosit la export. Descărcare PNG + print fără popup-uri.">
<style>
  :root{ --bg:#0a0f1c; --bg2:#0c1428; --ink:#e8f1ff; --muted:#9fb3d1; --acc:#3b82f6; --acc2:#22d3ee; --border:rgba(255,255,255,.14);} 
  *{box-sizing:border-box}
  html,body{height:100%}
  body{margin:0; font-family:system-ui, -apple-system, Segoe UI, Roboto, Inter, Helvetica, Arial, sans-serif; color:var(--ink);
        background:radial-gradient(1200px 800px at 120% -10%, var(--acc2), transparent 60%),radial-gradient(1000px 700px at -10% 120%, var(--acc), transparent 60%),linear-gradient(180deg, var(--bg), var(--bg2));}
  .wrap{min-height:100%; display:grid; place-items:center; padding:28px}
  .panel{width:min(1200px,95vw); background:linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.04)); border:1px solid var(--border); border-radius:20px; padding:18px; box-shadow:0 20px 60px rgba(0,0,0,.5); backdrop-filter:blur(10px)}
  header{display:flex; align-items:center; gap:12px; margin-bottom:10px}
  .mark{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;background:conic-gradient(from 230deg, var(--acc), var(--acc2)); box-shadow:0 10px 28px rgba(0,0,0,.35)}
  h1{margin:0;font-size:20px}
  .muted{color:var(--muted); font-size:13px}
  .grid{display:grid; grid-template-columns:1.05fr .95fr; gap:18px; align-items:start}
  @media(max-width:980px){ .grid{grid-template-columns:1fr} }
  form{display:grid; gap:10px}
  .row{display:grid; grid-template-columns:1fr 1fr; gap:10px}
  label{font-size:12px; color:#cfe2ff}
  input,select{width:100%; padding:10px 12px; border-radius:12px; border:1px solid var(--border); background:rgba(0,0,0,.25); color:var(--ink); outline:none}
  input:focus,select:focus{box-shadow:0 0 0 4px rgba(59,130,246,.35); border-color:#3b82f6}
  .tools{display:flex; gap:10px; flex-wrap:wrap}
  .btn{padding:11px 14px; border-radius:12px; border:1px solid var(--border); background:linear-gradient(135deg, var(--acc), var(--acc2)); color:#08121f; font-weight:800; cursor:pointer; box-shadow:0 14px 30px rgba(0,0,0,.35)}
  .btn.ghost{background:rgba(255,255,255,.06); color:var(--ink)}
  .preview{display:grid; place-items:center}
  .frame{border-radius:18px; overflow:hidden; box-shadow:0 20px 60px rgba(0,0,0,.45); transform-style:preserve-3d; transition:transform .2s ease}
  canvas#card{width:min(560px, 86vw); height:auto; display:block}
  .actions{display:flex; gap:10px; flex-wrap:wrap; margin-top:10px}
</style>
</head>
<body>
<div class="wrap">
  <div class="panel">
    <header>
      <div class="mark">📶</div>
      <div>
        <h1>Generator card Wi‑Fi cu QR</h1>
        <div class="muted">Previzualizare în timp real • Descărcare PNG • Print stabil</div>
      </div>
    </header>
    <div class="grid">
      <form id="f">
        <div class="row">
          <div>
            <label for="ssid">SSID</label>
            <input id="ssid" placeholder="Ex: RTEC_Guest" required>
          </div>
          <div>
            <label for="auth">Securitate</label>
            <select id="auth">
              <option value="WPA">WPA/WPA2/WPA3</option>
              <option value="WEP">WEP</option>
              <option value="nopass">Fără parolă (nopass)</option>
            </select>
          </div>
        </div>
        <div class="row">
          <div>
            <label for="pass">Parolă</label>
            <input id="pass" placeholder="Ex: rtec-2025!">
          </div>
          <div>
            <label for="hidden">Opțiuni</label>
            <label style="display:flex;align-items:center;gap:8px"><input type="checkbox" id="hidden"> Rețea ascunsă</label>
          </div>
        </div>
        <div class="row">
          <div>
            <label for="title">Titlu card</label>
            <input id="title" placeholder="Wi‑Fi pentru oaspeți">
          </div>
          <div>
            <label for="accent">Culoare accent</label>
            <input id="accent" type="color" value="#3b82f6">
          </div>
        </div>
        <div class="tools">
          <button type="button" class="btn" id="regen">Generează/Actualizează</button>
          <button type="button" class="btn ghost" id="copy">Copiază string Wi‑Fi</button>
        </div>
      </form>
      <div class="preview">
        <div class="frame" id="tilt">
          <canvas id="card" width="1200" height="720"></canvas>
        </div>
        <div class="actions">
          <button class="btn" id="dl">Descarcă PNG</button>
          <button class="btn ghost" id="print">Printează</button>
        </div>
      </div>
    </div>
  </div>
</div>


<script>
/* ========= UTIL ========= */
const C = document.getElementById('card');
const CTX = C.getContext('2d');
const SSID = document.getElementById('ssid');
const AUTH = document.getElementById('auth');
const PASS = document.getElementById('pass');
const HIDN = document.getElementById('hidden');
const TITLE = document.getElementById('title');
const ACCENT = document.getElementById('accent');


SSID.value='RTEC_Guest'; PASS.value='rtec-2025!'; TITLE.value='Wi‑Fi pentru oaspeți';


function esc(s){ return (s||'').replace(/\\/g,'\\\\').replace(/;/g,'\\;').replace(/,/g,'\\,').replace(/:/g,'\\:').replace(/\"/g,'\\\"'); }
function wifiString(){ const T=AUTH.value; const S=esc(SSID.value.trim()); const H=HIDN.checked?'true':'false'; if(T==='nopass') return `WIFI:T:nopass;S:${S};H:${H};;`; const P=esc(PASS.value||''); return `WIFI:T:${T};S:${S};P:${P};H:${H};;`; }
function clampText(ctx, text, x, y, maxW, font){ ctx.font=font; if(ctx.measureText(text).width<=maxW){ ctx.fillText(text,x,y); return; } let t=text; while(t.length>0 && ctx.measureText(t+"…").width>maxW) t=t.slice(0,-1); ctx.fillText(t+"…",x,y); }


/* ========= QR LIB (CDN cu fallback local mini) ========= */
// Încercăm întâi CDN; dacă eșuează, folosim un mini‑encoder intern (level L, suficient pentru credențiale obișnuite)
let QRReady = false;
function loadQrLib(){
  return new Promise((resolve)=>{
    if (window.qrcode) { QRReady=true; resolve(); return; }
    const s=document.createElement('script');
    s.src='https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js';
    s.async=true; s.onload=()=>{ QRReady=!!window.qrcode; resolve(); };
    s.onerror=()=>{ QRReady=false; resolve(); };
    document.head.appendChild(s);
  });
}


// Fallback minim: generează un pseudo-QR (NU este un QR real) doar ca să nu fie canvas gol.
// Îl afișăm doar dacă librăria nu s-a putut încărca, dar marcăm clar prin watermark.
function drawPseudoQR(ctx, x, y, size){
  const n=27; const cell=size/n; ctx.fillStyle='#fff'; ctx.fillRect(x,y,size,size); ctx.fillStyle='#000';
  for(let r=0;r<n;r++) for(let c=0;c<n;c++) if( (r*c + r + c) % 3===0 ) ctx.fillRect(Math.floor(x+c*cell), Math.floor(y+r*cell), Math.ceil(cell*0.98), Math.ceil(cell*0.98));
  // colțuri
  ctx.fillStyle='#fff'; ctx.fillRect(x+4,y+4,cell*7,cell*7); ctx.fillRect(x+size-4-cell*7,y+4,cell*7,cell*7); ctx.fillRect(x+4,y+size-4-cell*7,cell*7,cell*7);
  ctx.fillStyle='#000';
  const drawFinder=(fx,fy)=>{ ctx.fillRect(fx,fy,cell*7,cell*7); ctx.fillStyle='#fff'; ctx.fillRect(fx+cell,fy+cell,cell*5,cell*5); ctx.fillStyle='#000'; ctx.fillRect(fx+cell*2,fy+cell*2,cell*3,cell*3); };
  drawFinder(x+4,y+4); drawFinder(x+size-4-cell*7,y+4); drawFinder(x+4,y+size-4-cell*7);
  // watermark
  ctx.globalAlpha=.8; ctx.fillStyle='#e11'; ctx.font='bold 14px system-ui, Arial'; ctx.fillText('QR LIB INDISPONIBIL — PREVIEW', x+8, y+size-10); ctx.globalAlpha=1;
}


/* ========= RENDER CARD (același canvas pentru preview/export) ========= */
async function renderCard(){
  // fundal card
  const W=C.width, H=C.height; CTX.clearRect(0,0,W,H);
  const grad=CTX.createLinearGradient(0,0,0,H); grad.addColorStop(0,'#0f172a'); grad.addColorStop(1,'#0b1224');
  roundRect(CTX, 60,60, W-120, H-120, 26); CTX.fillStyle=grad; CTX.fill();
  const stripe=CTX.createLinearGradient(60,0,W-60,0); stripe.addColorStop(0, ACCENT.value||getComputedStyle(document.documentElement).getPropertyValue('--acc')||'#3b82f6'); stripe.addColorStop(1,'#22d3ee');
  roundRect(CTX,60,60,W-120,10,6); CTX.fillStyle=stripe; CTX.fill();


  // titluri
  CTX.fillStyle='#eaf2ff'; clampText(CTX, TITLE.value.trim()||'Wi‑Fi pentru oaspeți', 86, 140, W*0.6, '900 42px system-ui, Arial');
  CTX.fillStyle='#eaf2ff'; clampText(CTX, SSID.value.trim()||'—', 86, 190, W*0.55, '800 34px system-ui, Arial');
  CTX.fillStyle='#a9b6cf'; clampText(CTX, 'Deschide camera → scanează codul → conectare automată', 86, 228, W*0.55, '500 18px system-ui, Arial');


  // QR
  const qrSize=460; const qrX=W-86-qrSize, qrY=120;
  // cadru alb
  roundRect(CTX, qrX-8, qrY-8, qrSize+16, qrSize+16, 12); CTX.fillStyle='#fff'; CTX.fill(); CTX.strokeStyle='#e5e7eb'; CTX.lineWidth=2; CTX.stroke();
  const txt = wifiString();
  await loadQrLib();
  if(QRReady){
    const q = window.qrcode(0,'M'); q.addData(txt); q.make();
    const m=q.getModuleCount(); const pad=12; const box=(qrSize-pad*2)/m;
    CTX.fillStyle='#000';
    for(let r=0;r<m;r++) for(let c=0;c<m;c++) if(q.isDark(r,c)) CTX.fillRect(Math.round(qrX+pad+c*box), Math.round(qrY+pad+r*box), Math.ceil(box), Math.ceil(box));
  } else {
    drawPseudoQR(CTX, qrX, qrY, qrSize);
  }


  // detalii rețea (aliniere dinamică)
  const xL=86; const labels=['SSID:','Securitate:','Parolă:','Ascunsă:'];
  const values=[SSID.value.trim()||'—', AUTH.value, (AUTH.value==='nopass')?'—':(PASS.value||'—'), HIDN.checked?'true':'false'];
  CTX.fillStyle='#eaf2ff'; CTX.font='700 24px system-ui, Arial'; CTX.fillText('Detalii rețea', xL, 330+qrSize/2);
  const labelFont='600 20px system-ui, Arial', valFont='400 20px system-ui, Arial';
  CTX.font=labelFont; const labelW=Math.ceil(Math.max(...labels.map(t=>CTX.measureText(t).width))); const gap=14; const xVal=xL+labelW+gap; const maxValW=(qrX-20)-xVal; let y=266+qrSize/3;
  for(let i=0;i<labels.length;i++){ CTX.fillStyle='#eaf2ff'; CTX.font=labelFont; CTX.fillText(labels[i], xL, y); CTX.font=valFont; clampText(CTX, values[i], xVal, y, maxValW, valFont); y+=32; }
}


function roundRect(ctx,x,y,w,h,r){ ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); }


/* ========= UI wire-up ========= */
async function refresh(){ await renderCard(); }
['input','change'].forEach(ev=>{ SSID.addEventListener(ev, refresh); AUTH.addEventListener(ev, refresh); PASS.addEventListener(ev, refresh); HIDN.addEventListener(ev, refresh); TITLE.addEventListener(ev, refresh); ACCENT.addEventListener(ev, refresh); });


document.getElementById('regen').addEventListener('click', refresh);


document.getElementById('copy').addEventListener('click', async ()=>{
  try{ await navigator.clipboard.writeText(wifiString()); toast('String Wi‑Fi copiat.'); }catch(e){ alert('Nu am putut copia.'); }
});


// Download: folosim dataURL direct din canvas (compat maxim)
function canvasDataURL(){ try{ return C.toDataURL('image/png'); }catch(e){ return null; } }


document.getElementById('dl').addEventListener('click', ()=>{
  const url = canvasDataURL(); if(!url){ alert('Nu am putut genera PNG (canvas blocat).'); return; }
  const a = document.createElement('a'); a.href=url; a.download=`wifi-card-${(SSID.value||'network').replace(/\W+/g,'_')}.png`; document.body.appendChild(a); a.click(); a.remove();
});


document.getElementById('print').addEventListener('click', ()=>{
  const url = canvasDataURL(); if(!url){ alert('Nu am putut genera imagine pentru print.'); return; }
  const iframe=document.createElement('iframe'); iframe.style.cssText='position:fixed;right:0;bottom:0;width:0;height:0;border:0;'; document.body.appendChild(iframe);
  const html=`<!DOCTYPE html><html><head><meta charset='utf-8'><title>Print</title></head><body style="margin:0"><img src="${url}" style="width:100%"></body></html>`;
  iframe.srcdoc=html; iframe.onload=()=>{ try{ iframe.contentWindow.focus(); iframe.contentWindow.print(); }catch(e){} setTimeout(()=>{ document.body.removeChild(iframe); }, 1500); };
});


// 3D tilt pe frame
(function(){ const frame=document.getElementById('tilt'); const maxX=7,maxY=5; let raf=0,tx=0,ty=0; frame.addEventListener('mousemove',(e)=>{ const r=frame.getBoundingClientRect(); const x=(e.clientX-r.left)/r.width-.5; const y=(e.clientY-r.top)/r.height-.5; tx=x*maxX; ty=-y*maxY; if(!raf) raf=requestAnimationFrame(apply); }); frame.addEventListener('mouseleave',()=>{ tx=ty=0; if(!raf) raf=requestAnimationFrame(apply); }); function apply(){ frame.style.transform=`perspective(1200px) rotateY(${tx.toFixed(2)}deg) rotateX(${ty.toFixed(2)}deg)`; raf=0; } })();


// start
refresh();


function toast(msg){ const t=document.createElement('div'); t.textContent='✅ '+msg; Object.assign(t.style,{position:'fixed',bottom:'18px',right:'18px',background:'#0b1422',color:'#eaf2ff',padding:'10px 12px',border:'1px solid rgba(255,255,255,.18)',borderRadius:'12px',boxShadow:'0 18px 40px rgba(0,0,0,.5)',zIndex:999}); document.body.appendChild(t); setTimeout(()=>t.remove(),1600) }
</script>
</body>
</html>

Ai Nevoie de Cod Personalizat?

Contactează-ne pentru soluții de cod personalizate pentru proiectul tău. Putem dezvolta componente, module și funcționalități specifice nevoilor tale.

Contactează-ne
RTEC
Suport RTEC DESIGN Nexa este online