(function(){
const MIN_SECONDS=3, COOLDOWN_SECONDS=10, MAX_FAILS_BEFORE_COOLDOWN=2, MATH_ENABLED=true, ERROR_FADE_MS=7500;
let startedAt=Date.now(), keypressCount=0, focusedCount=0, mouseMoveCount=0, fails=0, cooldownUntil=0, errorFadeTimer=null;
function ready(fn){document.readyState!=='loading'?fn():document.addEventListener('DOMContentLoaded',fn);}
function el(t,a={},c=[]){const e=document.createElement(t);
Object.entries(a).forEach(([k,v])=>{
if(k==='style'&&typeof v==='object'){Object.assign(e.style,v);}
else if(k in e){e[k]=v;} else {e.setAttribute(k,v);}
});
(c||[]).forEach(x=>e.appendChild(typeof x==='string'?document.createTextNode(x):x));
return e;
}
const offscreen=()=>({position:'absolute',left:'-10000px',top:'auto',width:'1px',height:'1px',overflow:'hidden'});
const seconds=ts=>Math.floor((Date.now()-ts)/1000);
// helpers
function findSampleInput(form){
return form.querySelector('input[type="text"], input:not([type]), textarea, select');
}
function findSubmit(form){
return form.querySelector('[type="submit"], button[type="submit"]');
}
function findFieldLabelForInput(input){
if(!input) return null;
const container = input.closest('.form-group, .form__row, .field, .form-group-inline, .row, div');
let lbl = container ? container.querySelector('label') : null;
if(lbl) return lbl;
if(input.id){
const byFor = input.form?.querySelector(`label[for="${CSS.escape(input.id)}"]`);
if(byFor) return byFor;
}
let n=input.previousElementSibling;
while(n){ if(n.tagName==='LABEL') return n; n=n.previousElementSibling; }
return null;
}
function getInvalidSample(form){
return form.querySelector('input[aria-invalid="true"], .is-invalid input, input.error, .has-error input') || null;
}
function applyBaseInputStyles(fromCS, toEl){
if(!fromCS||!toEl) return;
toEl.style.padding = fromCS.padding;
toEl.style.border = fromCS.border;
toEl.style.borderRadius = fromCS.borderRadius;
toEl.style.background = fromCS.backgroundColor;
toEl.style.color = fromCS.color;
toEl.style.lineHeight = fromCS.lineHeight;
toEl.style.fontSize = fromCS.fontSize;
toEl.style.fontFamily = fromCS.fontFamily;
toEl.style.boxShadow = fromCS.boxShadow;
toEl.style.outline = 'none';
}
function applyInvalidStyles(invalidCS, toEl){
if(invalidCS){
toEl.style.border = invalidCS.border;
toEl.style.boxShadow = invalidCS.boxShadow;
const bc = invalidCS.borderColor || '';
if(!bc) toEl.style.borderColor = '#ef4444';
}else{
toEl.style.borderColor = '#ef4444';
toEl.style.boxShadow = '0 0 0 1px rgba(239,68,68,.25)';
}
}
function clearInvalidStyles(sampleCS, toEl){ applyBaseInputStyles(sampleCS, toEl); }
// >>> NOWE: pobieranie koloru gwiazdki jak w innych polach
function getRequiredStarColor(form){
// 1) gwiazdka w labelach (span/sup/i z '*')
const candidates = form.querySelectorAll('label span, label sup, label i');
for(const el of candidates){
if((el.textContent||'').trim() === '*'){
const c = getComputedStyle(el).color;
if(c) return c;
}
}
// 2) kolor z komunikatu błędu (często to ten sam czerwony)
const err = form.querySelector('.error, .text-danger, .form-error, .invalid-feedback, .error-message, .help-block');
if(err){
const c = getComputedStyle(err).color;
if(c) return c;
}
// 3) domyślny czerwony (tailwindowy red-500)
return '#ef4444';
}
ready(function(){
const form=document.querySelector('form'); if(!form) return;
const sampleInput=findSampleInput(form);
const submitBtn=findSubmit(form);
const csInput=sampleInput?getComputedStyle(sampleInput):null;
const sampleLabel=findFieldLabelForInput(sampleInput);
const csLabel=sampleLabel?getComputedStyle(sampleLabel):null;
const invalidSample=getInvalidSample(form);
const invalidCS=invalidSample?getComputedStyle(invalidSample):null;
// interakcje
const inputs=[...form.querySelectorAll('input, textarea, select')];
form.addEventListener('keydown',e=>{if(!['Tab','Shift','Control','Alt','Meta'].includes(e.key)) keypressCount++;},true);
inputs.forEach(i=>i.addEventListener('focus',()=>{focusedCount++;},true));
window.addEventListener('mousemove',()=>{mouseMoveCount++;},{passive:true});
// honeypot + meta
const hpWrap=el('div',{style:offscreen()}), hp=el('input',{type:'text',name:'website',autocomplete:'off',tabIndex:-1,'aria-hidden':'true',placeholder:'Zostaw puste'});
hpWrap.appendChild(hp); form.appendChild(hpWrap);
const meta=el('input',{type:'hidden',name:'anti_spam_meta'}); form.appendChild(meta);
// anchor
const anchor=submitBtn?.closest('div')||form;
// === Sekcja matematyczna ===
let a=0,b=0, mathInput=null, questionSpan=null, errorText=null;
function newEquation(focus=true){
a=Math.floor(2+Math.random()*7);
b=Math.floor(2+Math.random()*7);
if(questionSpan) questionSpan.textContent=`Ile to ${a} + ${b}?`;
if(mathInput){ mathInput.value=''; if(focus) mathInput.focus(); }
}
function mountMath(){
if(!MATH_ENABLED) return;
newEquation(false);
const field=el('div',{className:'antispam-math-field',style:{margin:'16px 0'}});
const label=el('label',{htmlFor:'math-proof',style:{
display:'block',marginBottom:'8px',
fontWeight: csLabel?csLabel.fontWeight:'600',
color: csLabel?csLabel.color:(csInput?csInput.color:'#0f172a'),
fontSize: csLabel?csLabel.fontSize:(csInput?csInput.fontSize:'16px'),
lineHeight: csLabel?csLabel.lineHeight:(csInput?csInput.lineHeight:'1.4'),
fontFamily: csLabel?csLabel.fontFamily:(csInput?csInput.fontFamily:'inherit')
}},['Potwierdź, że jesteś człowiekiem']);
// GWIAZDKA W TYM SAMYM KOLORZE CO W POLU E-MAIL (albo innym wymaganym)
const starColor = getRequiredStarColor(form);
const requiredStar = el('span',{style:{color:starColor, marginLeft:'4px', fontWeight: csLabel?csLabel.fontWeight:'600'}},['*']);
label.appendChild(requiredStar);
const row=el('div',{style:{display:'flex',alignItems:'center',gap:'12px',flexWrap:'wrap'}});
mathInput=el('input',{id:'math-proof',type:'text',inputMode:'numeric',pattern:'[0-9]*',placeholder:'Wpisz wynik (np. 5)'});
applyBaseInputStyles(csInput, mathInput);
mathInput.style.width='200px';
questionSpan=el('span',{style:{
whiteSpace:'nowrap',
color: csLabel?csLabel.color:(csInput?csInput.color:'#0f172a'),
fontSize: csLabel?csLabel.fontSize:(csInput?csInput.fontSize:'16px'),
lineHeight: csLabel?csLabel.lineHeight:(csInput?csInput.lineHeight:'1.4'),
fontFamily: csLabel?csLabel.fontFamily:(csInput?csInput.fontFamily:'inherit'),
fontWeight: csLabel?csLabel.fontWeight:'600'
}},[`Ile to ${a} + ${b}?`]);
errorText=el('div',{role:'alert','aria-live':'assertive',style:{
marginTop:'6px',
color:'#ef4444',
fontSize: csLabel?csLabel.fontSize:(csInput?csInput.fontSize:'12px'),
lineHeight: csLabel?csLabel.lineHeight:(csInput?csInput.lineHeight:'1.3'),
fontFamily: csLabel?csLabel.fontFamily:(csInput?csInput.fontFamily:'inherit'),
opacity:0, transition:'opacity 200ms ease'
}});
row.appendChild(mathInput);
row.appendChild(questionSpan);
field.appendChild(label);
field.appendChild(row);
field.appendChild(errorText);
if(anchor?.parentNode){ anchor.parentNode.insertBefore(field,anchor); } else { form.appendChild(field); }
}
mountMath();
if(document.getElementById('math-proof')) document.getElementById('math-proof').focus();
function showErr(msg){
if(!errorText||!mathInput) return;
applyInvalidStyles(invalidCS, mathInput);
if(errorFadeTimer){ clearTimeout(errorFadeTimer); errorFadeTimer=null; }
errorText.textContent = msg;
errorText.style.opacity = 1;
errorFadeTimer = setTimeout(()=>{
errorText.style.opacity = 0;
setTimeout(()=>{ if(errorText.style.opacity==='0') errorText.textContent=''; }, 300);
}, ERROR_FADE_MS);
}
function clearErr(){
if(!errorText||!mathInput) return;
clearTimeout(errorFadeTimer); errorFadeTimer=null;
errorText.textContent=''; errorText.style.opacity=0;
clearInvalidStyles(csInput, mathInput);
}
function validate(ev){
clearErr();
const now=Date.now();
if(now=1)&& (keypressCount>=2||mouseMoveCount>=2);
if(!interacted){
fails++; if(ev){ev.preventDefault();ev.stopImmediatePropagation();}
showErr('Najpierw wypełnij formularz.');
cool(); return false;
}
if(MATH_ENABLED){
const v=(document.getElementById('math-proof')?.value||'').trim();
if(!/^\d+$/.test(v)){
fails++; if(ev){ev.preventDefault();ev.stopImmediatePropagation();}
showErr('Podaj wynik jako liczbę.');
newEquation(); cool(); return false;
}
if(parseInt(v,10)!==(a+b)){
fails++; if(ev){ev.preventDefault();ev.stopImmediatePropagation();}
showErr('Niepoprawny wynik.');
newEquation(); cool(); return false;
}
}
meta.value=btoa(unescape(encodeURIComponent(JSON.stringify({
ttf_s:seconds(startedAt),keys:keypressCount,foci:focusedCount,moves:mouseMoveCount,math_ok:true,math_a:a,math_b:b,fails
}))));
return true;
}
function cool(){
if(fails>=MAX_FAILS_BEFORE_COOLDOWN){
cooldownUntil=Date.now()+COOLDOWN_SECONDS*1000;
if(submitBtn){
const orig=submitBtn.disabled?null:submitBtn.textContent;
submitBtn.disabled=true; let left=COOLDOWN_SECONDS;
(function t(){submitBtn.textContent=`Zablokowane (${left}s)`;
if(--left<0){submitBtn.disabled=false;if(orig)submitBtn.textContent=orig;}
else setTimeout(t,1000);
})();
}
}
}
// nasłuchy/patch
form.addEventListener('submit',e=>{ if(!validate(e)) return false; },true);
form.addEventListener('submit',e=>{ if(!validate(e)) return false; });
if(typeof form.requestSubmit==='function'){ const o=form.requestSubmit.bind(form); form.requestSubmit=function(...a){ if(!validate())return; return o(...a); }; }
if(typeof form.submit==='function'){ const o=form.submit.bind(form); form.submit=function(...a){ if(!validate())return; return o(...a); }; }
if(submitBtn){ submitBtn.addEventListener('click',e=>{ if(!validate(e)) return false; },true); }
});
})();
Senior Shopify Frontend Developer (k/m)
Senior Shopify Developer (k/m)
14 000 - 18 000 netto PLN (B2B)
W hmmh Poland realizujemy projekty e-commerce dla marek z całego świata, z focusem na region DACH i Polskę. To nie są proste wizytówki, ale rozbudowane ekosystemy sprzedażowe, gdzie wydajność i jakość kodu przekładają się bezpośrednio na przychody klienta.
Szukamy Senior Developera (k/m), który wejdzie do zespołu jako partner. Nie potrzebujemy kogoś, kto tylko odhacza zadania w Jirze. Potrzebujemy eksperta, który spojrzy na projekt, doradzi klientowi lepsze rozwiązanie i weźmie odpowiedzialność za warstwę frontendową.
Masz szansę pracować z ludźmi, którzy znają się na swojej robocie, w atmosferze, gdzie liczy się merytoryka, a nie korporacyjna polityka.
Twoja rola:
Budowa i rozwój: Tworzysz zaawansowane sklepy na Shopify Plus. Pracujesz nad nowymi wdrożeniami oraz rozwijasz istniejące systemy, dbając o czystość i skalowalność kodu.
Architektura: Masz realny wpływ na dobór rozwiązań technicznych. Decydujesz, jak najlepiej obsłużyć nietypowe wymagania klienta w ramach (lub poza) standardami Shopify.
Partnerstwo: Jesteś technicznym głosem doradczym. Jeśli pomysł klienta negatywnie wpłynie na UX lub szybkość sklepu – oczekujemy, że o tym powiesz i zaproponujesz alternatywę.
Współpraca: Działasz ramię w ramię z naszym zespołem w Polsce oraz ekspertami z hmmh Germany. Wymieniacie się wiedzą i wspólnie dowozicie jakość.
Technicznie:
Pracujemy głównie na Shopify (Liquid, Theme Kit/CLI) , ale mocno stawiamy na nowoczesny frontend (JS, HTML5, CSS3/SCSS). Często integrujemy się z zewnętrznymi API i aplikacjami, więc rozumienie tego, co dzieje się na backendzie, jest niezbędne.
Czego potrzebujesz na starcie:
Solidnego doświadczenia z platformą Shopify (frontend to podstawa, ale musisz rozumieć ekosystem).
Umiejętności myślenia analitycznego - kod ma rozwiązywać problem biznesowy.
Komunikatywności. Musisz umieć wytłumaczyć techniczne zawiłości osobom nietechnicznym (klientom, Project Managerom).
Dobrej znajomości języka angielskiego (dokumentacja, komunikacja w zespole międzynarodowym).
Co oferujemy:
Ciekawe projekty : Pracujemy dla dużych, rozpoznawalnych klientów. Tutaj spotkasz się z wyzwaniami, które rzadko zdarzają się w małych sklepach.
Zespół ekspertów : Jesteśmy częścią hmmh AG. Masz dostęp do know-how z rynku niemieckiego i pracujesz z seniorami, od których można się wiele nauczyć.
Wpływ : Twoje zdanie ma znaczenie. Słuchamy sugestii dotyczących narzędzi i procesów.
Elastyczność : Praca zdalna, elastyczne godziny startu, brak mikrozarządzania.
Proces rekrutacji : Konkretnie i bez zbędnego przeciągania. Chcemy poznać Twój sposób myślenia i umiejętności techniczne. Zawsze dajemy pełny feedback, niezależnie od wyniku rozmowy.
announcement.apply
job_post.job_location
Wrocław
ID: 48
job_post.published_on :
26/02/2026
ID: 48
job_post.published_on :
26/02/2026