Logo HUT ke-63

THE COUNTDOWN IS ON

--HARI
--JAM
--MENIT
--DETIK
PENDAFTARAN DITUTUP SEMENTARA
Baladhika Fun Run 2026
EXPERIENCE FUN RUN 5K
Minggu 6 DESEMBER 2026
LAPANGANRAMPALKOTA MALANG
Dua pelari Baladhika Fun Run
01 — PILIH TIKET
02 — DATA PESERTA
03 — KONFIRMASI
PENDAFTARAN DITUTUP SEMENTARA

Sistem Baladhika Fun Run 2026 sedang dalam proses migrasi. Pendaftaran dan pembayaran belum dapat dilakukan. Informasi event tetap dapat dilihat di halaman ini.

STEP 01

CHOOSE YOUR TICKET

Pilih kategori yang tersedia sesuai status sistem.

EARLY BIRD
RP150.000

Per peserta • Fun Run 5K

MEMERIKSA STATUS...
0
REGULER
RP150.000

Per peserta • Fun Run 5K

MEMERIKSA STATUS...
0
Early BirdRp0
RegulerRp0
Jumlah Peserta0 orang
TOTAL TIKETRp0

PENDAFTARAN DITUTUP SEMENTARA. Sistem sedang dalam proses migrasi ke pendaftaran resmi. Mohon tidak melakukan pembayaran sampai pendaftaran dibuka kembali.

Satu email utama untuk setiap pesanan, baik individu maupun rombongan. Setiap peserta tetap mengisi identitas, alamat lengkap, ukuran jersey, dan kontak darurat.

Medali dan jersey Baladhika Fun Run 2026

BENEFIT PESERTA

Benefit peserta Baladhika Fun Run 2026

CEK PESANAN

Layanan pengecekan pesanan sementara tidak tersedia selama pembaruan sistem.

BDJ-

FIND YOUR PERFECT FIT

JERSEY SIZE CHART

Pilih ukuran jersey yang sesuai sebelum melengkapi formulir pendaftaran.

Jersey Size Chart

PILIHAN UKURAN: XS • S • M • L • XL • XXL • XXXL

PILIH UKURAN & DAFTAR ↗

EXPLORE THE COURSE

RUTE 5K

Kenali rute sebelum memulai lari.

Rute 5K

OFFICIAL EVENT INFORMATION

RACEPACK COLLECTION

TANGGAL: 4–5 DESEMBER 2026

LOKASI: AULA MAKOREM 083/BDJ

Jam pengambilan mengikuti pengumuman resmi panitia.

LIHAT GOOGLE MAPS ↗

OFFICIAL EVENT INFORMATION

EVENT RULES

Baca dan pahami seluruh 22 poin peraturan BALADHIKA FUN RUN 2026 sebelum mendaftar.

KATEGORI 5K
START 05.30 WIB
LOKASI LAPANGAN RAMPAL
"); popup.document.close(); download.disabled=true;msg.textContent="Memverifikasi email dan menyiapkan invoice..."; try{ const response=await fetch("/api/participant-demo-invoice",{ method:"POST",headers:{"Content-Type":"application/json"}, body:JSON.stringify({order_number:data.order_number,buyer_email:buyerEmail}),cache:"no-store" }); const payload=await response.json(); if(!response.ok||!payload.success)throw new Error(payload.error||"Invoice belum tersedia."); const invoice=payload.invoice; const esc=v=>String(v??"-").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); const rupiah=n=>new Intl.NumberFormat("id-ID",{style:"currency",currency:"IDR",maximumFractionDigits:0}).format(Number(n)||0); const rows=(invoice.participants||[]).map((p,i)=>`${i+1}${esc(p.name)}${esc(p.ticket_category)}${esc(p.booking_code)}${esc(p.registration_number)}`).join(""); const html=`Invoice DEMO ${esc(invoice.order_number)}
Baladhika Fun Run

INVOICE LUNAS — DEMO / TEST

BUKAN BUKTI PEMBAYARAN NYATA — KHUSUS SIMULASI

Nomor pesanan: ${esc(invoice.order_number)}

Pemesan: ${esc(invoice.buyer_name)}

Jumlah peserta: ${esc(invoice.total_participants)}

Total simulasi: ${esc(rupiah(invoice.total_amount))}

Status: PAID_DEMO

Peserta & Kode Booking Racepack

${rows}
No.NamaKategoriKode BookingNo. Registrasi

Gunakan kode booking untuk verifikasi pengambilan racepack.

DEMO / TEST — bukan bukti pembayaran sah.

`; popup.document.open();popup.document.write(html);popup.document.close(); msg.textContent="Invoice terbuka. Klik SIMPAN PDF / CETAK, lalu pilih Save as PDF."; }catch(err){popup.close();msg.textContent=err.message||"Gagal mengambil invoice.";} finally{download.disabled=false;} }); wrap.append(label,email,download,msg);result.append(wrap); } if(data.payment_status==="PENDING_DEMO"){ const fullOrder=String(data.order_number||"").toUpperCase(); const stored=sessionStorage.getItem("bdj_demo_payment_"+fullOrder); let savedToken=""; try{ const parsed=JSON.parse(stored||"null"); if(parsed && parsed.order===fullOrder && Date.now()-parsed.savedAt<86400000 && /^[a-f0-9]{64}$/.test(parsed.token)) savedToken=parsed.token; }catch(_){} const section=document.createElement("section"); section.style.cssText="margin-top:18px;padding:18px;border:1px solid #ffffff44;border-radius:14px;text-align:center"; const label=document.createElement("h3"); label.textContent="QRIS SIMULASI — "+fullOrder; label.style.cssText="font-size:18px;margin:0 0 12px;color:#fff"; const qrImg=document.createElement("img"); qrImg.alt="Kode QR demonstrasi, bukan QRIS bank dan tidak dapat menerima pembayaran"; qrImg.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANcAAADXAQAAAAChBvkYAAACeElEQVR4nO1YMY7bQAwceg2sOzrIA9Yvke5bAXwn+XRAvpGnaPMSqku5qrIC1poUMpAmd0nj2xTHRgJZkCA5Qy6FeE24e9UEfNjezwauANCgsywsgCQgb4YKcSqZoXscKPuQeTwHtKTVydlFAFxPjAnQmFwEZX9Hf3+xyTRweYKzDO3fwd8fZb99eHQ4iC6nH3n+XJY6sewAdITwUcAjprkN/QRhqRPLLHLg0Vlu8NU2bRQ51cD0JskZ2oSQge6mendMg6sWADoQwhLQwNGv2lfgF3DVwfyoBbnRgVyVzI32oQbXSQLmh3kw/zwvIjtc5CDJxSr9kjpyhGNuAOQ2nYG2Vr8AQMseHJXmYzoHT9Lq1MiZf55LODRAWB7gDFH6u/l7Q8jUWYYWeHIwjlrAmM51cATHm3MtQMuhHr+QtAw4+pgQtv2l0Vpc1wfGjXw75kYL/Kg1evdWGbnVKPhRSTKd63Dd1Adeg7dlh5dpfgIm8Hg/f28ImZzlNp0Dmc4hS0JAy1IhL+CqNDIBXNHd/kbU2L3BVXtkwBkjaX5VMrcJVfJCluBHdAQ2RA3GyFIlL7d+cZYlnQEAAY0OlTA9GFcdmFv2ITebusYM2AHz0oqblpN/ngfTyAIfk2OVN4l6w3f18fDz03LKwEsw7vDN6ryPplPzHY9w63USWV2U+/n7h3fAqjQAnUFYwLXWbASARmmeyZEjnN228Cq8y9xoCSRJT/ZbfFVwhIsAcw+R40UAOJOdllDlFqT9tjyhJS23JD2Tq7J7AwAkvUwc5z74Z1xPyw5fTnXyQuY2OfrIgblRMkul2fj7Xtdu86gzP6JG78rHffe/t/0CJEXosITwT6cAAAAASUVORK5CYII="; qrImg.style.cssText="display:block;margin:0 auto;max-width:230px;width:80%;height:auto;border:12px solid white;border-radius:10px"; const disclaimer=document.createElement("p"); disclaimer.textContent="DEMO / TEST — QR ini hanya contoh tampilan, bukan QRIS bank. Memindainya tidak membayar pesanan."; disclaimer.style.cssText="font-size:12px;line-height:1.6;color:#ffd6b7"; const info=document.createElement("p"); info.textContent="Untuk versi LIVE, QRIS pembayaran asli akan dibuat oleh penyedia pembayaran berdasarkan nomor dan nominal pesanan. Status lunas hanya boleh diperbarui dari notifikasi pembayaran terverifikasi."; info.style.cssText="font-size:12px;line-height:1.6;text-align:left"; section.append(label,qrImg,disclaimer,info); const qrDownload=document.createElement("button"); qrDownload.type="button";qrDownload.className="btn"; qrDownload.textContent="DOWNLOAD QRIS DEMO ↓"; qrDownload.style.cssText="width:100%;margin:12px 0"; qrDownload.addEventListener("click",()=>{ const canvas=document.createElement("canvas");canvas.width=640;canvas.height=760; const ctx=canvas.getContext("2d");ctx.fillStyle="#ffffff";ctx.fillRect(0,0,640,760); ctx.fillStyle="#163022";ctx.textAlign="center";ctx.font="bold 29px Arial"; ctx.fillText("BALADHIKA FUN RUN 2026",320,58); ctx.font="bold 24px Arial";ctx.fillText("QRIS SIMULASI",320,105); ctx.fillText(fullOrder,320,145); const paint=()=>{ ctx.drawImage(qrImg,135,180,370,370); ctx.font="bold 19px Arial";ctx.fillStyle="#aa2c12"; ctx.fillText("DEMO / TEST - BUKAN QRIS BANK",320,615); ctx.font="17px Arial";ctx.fillStyle="#163022"; ctx.fillText("TIDAK UNTUK PEMBAYARAN NYATA",320,650); const a=document.createElement("a");a.download="QRIS-DEMO-"+fullOrder+".png"; a.href=canvas.toDataURL("image/png");a.click(); }; if(qrImg.complete&&qrImg.naturalWidth)paint(); else qrImg.addEventListener("load",paint,{once:true}); }); section.append(qrDownload); result.append(section); if(savedToken){ const resume=document.createElement("button"); resume.type="button"; resume.className="btn"; resume.style.width="100%"; resume.textContent="UPLOAD BUKTI & TERBITKAN INVOICE DEMO →"; resume.addEventListener("click",()=>{ const u=new URL("/payment-demo.html",location.origin); u.hash=new URLSearchParams({order:fullOrder,token:savedToken}).toString(); location.assign(u.href); }); result.append(resume); }else{ const help=document.createElement("p"); help.textContent="Untuk mengunggah bukti dan menerbitkan invoice, akses pesanan lama perlu dipulihkan oleh panitia setelah verifikasi pemilik. QR contoh ini dapat diunduh, tetapi tidak dapat mengubah status pembayaran."; const locked=document.createElement("button");locked.type="button";locked.className="btn"; locked.style.width="100%";locked.disabled=true; locked.textContent="UPLOAD BUKTI & INVOICE — MENUNGGU VERIFIKASI"; result.append(locked); help.style.cssText="font-size:12px;line-height:1.7"; result.append(help); } } result.hidden=false; }catch(error){ showMessage("Koneksi gagal. Silakan coba kembali nanti."); }finally{ button.disabled=false; button.textContent="CEK STATUS PESANAN →"; } }); })(); /* COUNTDOWN */ const $=id=>document.getElementById(id); const rupiah=n=>"Rp"+Number(n).toLocaleString("id-ID"); const raceDate=new Date("2026-12-06T05:30:00+07:00").getTime(); function updateCountdown(){ const d=Math.max(0,raceDate-Date.now()); $("days").textContent=Math.floor(d/86400000); $("hours").textContent=String(Math.floor(d%86400000/3600000)).padStart(2,"0"); $("minutes").textContent=String(Math.floor(d%3600000/60000)).padStart(2,"0"); $("seconds").textContent=String(Math.floor(d%60000/1000)).padStart(2,"0"); } updateCountdown(); setInterval(updateCountdown,1000); /* TICKET STATUS */ const prices={early:150000,normal:150000}; const qty={early:0,normal:0}; const MAX_PER_ORDER=10; let ticketStatus=null; let selectedPayment=""; let participantData=[]; function totalPeople(){return qty.early+qty.normal} function totalPrice(){return qty.early*prices.early+qty.normal*prices.normal} const MIGRATION_REGISTRATION_CLOSED = true; function isAvailable(category){ if(MIGRATION_REGISTRATION_CLOSED)return false; if(category==="early")return false; // Halaman 2 hanya menampilkan tiket Reguler if(!ticketStatus)return false; const key=category==="early"?"early_bird":"normal"; return ticketStatus.registration_open===true && ticketStatus[key]?.status==="AVAILABLE" && ticketStatus[key]?.can_register===true; } function setSoldStamp(id,sold){ const stamp=$(id)?.querySelector(".sold-stamp"); if(stamp)stamp.hidden=!sold; } function updateTicketDisplay(){ [ ["early","early_bird","priceEarly","cardEarly", "priceEarlyStatus","cardEarlyStatus","priceEarlyButton", "earlyPlus","earlyMinus"], ["normal","normal","priceNormal","cardNormal", "priceNormalStatus","cardNormalStatus","priceNormalButton", "normalPlus","normalMinus"] ].forEach(c=>{ const [key,apiKey,priceId,cardId,priceStatus,cardStatus, button,plus,minus]=c; const available=isAvailable(key); const sold=ticketStatus?.[apiKey]?.status==="SOLD_OUT"; setSoldStamp(priceId,sold); setSoldStamp(cardId,sold); let status="STATUS BELUM TERSEDIA"; if(ticketStatus){ status=sold?"SOLD OUT": available?"PENDAFTARAN DIBUKA": "BELUM TERSEDIA"; } $(priceStatus).textContent=status; $(cardStatus).textContent=status; $(cardId).classList.toggle("available",available); $(cardId).classList.toggle("unavailable",!available); $(plus).disabled=!available; $(minus).disabled=!available; const link=$(button); link.classList.toggle("disabled-link",!available); link.setAttribute("aria-disabled",String(!available)); link.tabIndex=available?0:-1; link.textContent=available?"PILIH TIKET ↗":status; if(!available)qty[key]=0; }); updateTicketSummary(); } async function refreshTicketStatus(){ try{ const response=await fetch("/api/ticket-status",{cache:"no-store"}); if(!response.ok)throw Error("HTTP "+response.status); const data=await response.json(); if(data.success!==true||!data.early_bird||!data.normal){ throw Error("Respons API tidak valid"); } ticketStatus=data; }catch(error){ console.warn("Status tiket tidak tersedia:",error); ticketStatus=null; } updateTicketDisplay(); } function changeQty(category,change){ if(MIGRATION_REGISTRATION_CLOSED)return; if(category==="early")return; // Tiket Early Bird tidak ditawarkan di halaman ini if(!["early","normal"].includes(category))return; if(!isAvailable(category)){ alert("Kategori tiket belum tersedia."); return; } const other=category==="early"?"normal":"early"; if(change>0&&qty[other]>0){ alert("Hanya satu kategori tiket per pesanan."); return; } const next=qty[category]+change; if(next<0)return; if(change>0&&totalPeople()>=MAX_PER_ORDER){ alert("Maksimal 10 peserta per pesanan."); return; } qty[category]=next; updateTicketSummary(); } function updateTicketSummary(){ $("earlyQty").textContent=qty.early; $("normalQty").textContent=qty.normal; $("earlySubtotal").textContent=rupiah(qty.early*prices.early); $("normalSubtotal").textContent=rupiah(qty.normal*prices.normal); $("totalPeople").textContent=totalPeople()+" orang"; $("grandTotal").textContent=rupiah(totalPrice()); $("continueTickets").disabled=totalPeople()===0||!ticketStatus|| (qty.early>0&&!isAvailable("early"))|| (qty.normal>0&&!isAvailable("normal")); } updateTicketDisplay(); refreshTicketStatus(); setInterval(refreshTicketStatus,30000); document.addEventListener("visibilitychange",()=>{ if(!document.hidden)refreshTicketStatus(); }); /* REGISTRATION STEPS */ function showRegistrationStep(step){ $("ticketStep").hidden=step!==1; $("detailsStep").hidden=step!==2; $("reviewStep").hidden=step!==3; for(let i=1;i<=3;i++){ $("step"+i).classList.toggle("active",i===step); } $("registration").scrollIntoView({behavior:"smooth"}); } /* FORM HELPERS */ function makeInput(label,name,type="text",required=true){ const field=document.createElement("div"); field.className="field"; const title=document.createElement("label"); title.textContent=label+(required?" *":""); const input=document.createElement("input"); input.name=name; input.type=type; input.required=required; if(type==="date")input.max=new Date().toISOString().slice(0,10); if(name==="identity_number"){ input.maxLength=30; input.autocomplete="off"; input.placeholder="Masukkan nomor identitas"; } field.append(title,input); return field; } function makeSelect(label,name,options,required=true){ const field=document.createElement("div"); field.className="field"; const title=document.createElement("label"); title.textContent=label+(required?" *":""); const select=document.createElement("select"); select.name=name; select.required=required; const empty=document.createElement("option"); empty.value=""; empty.textContent="Pilih "+label.toLowerCase(); select.append(empty); options.forEach(value=>{ const option=document.createElement("option"); option.value=value; option.textContent=value; select.append(option); }); field.append(title,select); return field; } function makeTextarea(label,name){ const field=document.createElement("div"); field.className="field full"; const title=document.createElement("label"); title.textContent=label+" *"; const textarea=document.createElement("textarea"); textarea.name=name; textarea.required=true; textarea.placeholder="Nama jalan, nomor rumah, RT/RW, dusun atau lingkungan"; field.append(title,textarea); return field; } function makeGrid(...elements){ const grid=document.createElement("div"); grid.className="field-grid"; elements.forEach(el=>grid.append(el)); return grid; } /* ALAMAT MANUAL — satu kolom, tanpa pilihan wilayah atau kode pos */ function setupRegions(card){ card.append(makeGrid(makeTextarea("Alamat Lengkap","street_address"))); } /* PARTICIPANT FORM */ function createParticipant(index,category){ const card=document.createElement("div"); card.className="participant-card"; card.dataset.category=category; const header=document.createElement("div"); header.className="participant-header"; const title=document.createElement("h3"); title.textContent="PESERTA "+(index+1); const pill=document.createElement("span"); pill.className="category-pill"; pill.textContent=category==="early"?"EARLY BIRD":"REGULER"; header.append(title,pill); card.append(header); card.append(makeGrid( makeInput("Nama Depan","first_name"), makeInput("Nama Belakang","last_name") )); card.append(makeGrid( makeInput("Nama pada BIB","bib_name"), makeInput("Tanggal Lahir","birth_date","date") )); card.append(makeGrid( makeSelect("Jenis Kelamin","gender",["Laki-laki","Perempuan"]), makeSelect("Ukuran Jersey","jersey_size", ["XS","S","M","L","XL","XXL","XXXL"]) )); const sizeHelp=document.createElement("p"); sizeHelp.style.fontSize="12px"; const sizeLink=document.createElement("a"); sizeLink.href="#jersey"; sizeLink.className="text-link"; sizeLink.textContent="Lihat panduan ukuran jersey"; sizeHelp.append(sizeLink); card.append(sizeHelp); card.append(makeGrid( makeSelect("Jenis Identitas","identity_type", ["KTP","KIA","Paspor","Identitas Lainnya"]), makeInput("Nomor Identitas","identity_number") )); card.append(makeSelect("Status Kesehatan","health_status",[ "Sehat / Siap Mengikuti Lari", "Memiliki Kondisi Kesehatan yang Perlu Diperhatikan" ])); const healthNote=document.createElement("p"); healthNote.className="copy muted"; healthNote.textContent= "Tidak perlu mencantumkan diagnosis medis. "+ "Jika memiliki kondisi kesehatan yang perlu diperhatikan, "+ "silakan berkonsultasi dengan tenaga kesehatan dan panitia."; card.append(healthNote); const addressTitle=document.createElement("p"); addressTitle.className="address-heading"; addressTitle.textContent="ALAMAT LENGKAP PESERTA"; card.append(addressTitle); setupRegions(card); card.append(makeGrid( makeInput("WhatsApp Peserta (opsional)","phone","tel",false) )); const emergencyTitle=document.createElement("p"); emergencyTitle.className="address-heading"; emergencyTitle.textContent="KONTAK DARURAT"; card.append(emergencyTitle); card.append(makeGrid( makeInput("Nama Kontak Darurat","emergency_name"), makeInput("Nomor Kontak Darurat","emergency_phone","tel") )); card.append(makeSelect("Hubungan Kontak Darurat","emergency_relation",[ "Orang Tua","Pasangan","Saudara", "Keluarga Lain","Teman","Lainnya" ])); return card; } async function openParticipantStep(){ if(MIGRATION_REGISTRATION_CLOSED){alert("Pendaftaran ditutup sementara selama migrasi sistem.");return;} await refreshTicketStatus(); if(totalPeople()===0){ alert("Pilih tiket yang tersedia."); return; } if((qty.early>0&&!isAvailable("early"))|| (qty.normal>0&&!isAvailable("normal"))){ alert("Status tiket berubah. Silakan pilih ulang."); return; } const categories=[ ...Array(qty.early).fill("early"), ...Array(qty.normal).fill("normal") ]; const container=$("participantForms"); container.replaceChildren(); categories.forEach((category,index)=>{ container.append(createParticipant(index,category)); }); showRegistrationStep(2); } function collectParticipants(){ return [...document.querySelectorAll(".participant-card")].map(card=>{ const get=name=>card.querySelector( `[name="${name}"]` )?.value.trim()||""; return{ category:card.dataset.category, first_name:get("first_name"), last_name:get("last_name"), bib_name:get("bib_name"), birth_date:get("birth_date"), gender:get("gender"), jersey_size:get("jersey_size"), identity_type:get("identity_type"), identity_number:get("identity_number"), health_status:get("health_status"), province:"", regency:"", district:"", village:"", street_address:get("street_address"), postal_code:"", phone:get("phone"), emergency_name:get("emergency_name"), emergency_phone:get("emergency_phone"), emergency_relation:get("emergency_relation") }; }); } function addReviewLine(parent,text,bold=false){ const p=document.createElement("p"); if(bold){ const strong=document.createElement("strong"); strong.textContent=text; p.append(strong); }else{ p.textContent=text; } parent.append(p); } $("registrationForm").addEventListener("submit",event=>{ event.preventDefault(); if(!$("registrationForm").reportValidity())return; participantData=collectParticipants(); const buyer=$("reviewBuyer"); buyer.replaceChildren(); const buyerCard=document.createElement("div"); buyerCard.className="review-entry"; addReviewLine(buyerCard,"DATA PEMESAN UTAMA",true); addReviewLine(buyerCard,"Nama: "+$("buyerName").value); addReviewLine(buyerCard,"Email Konfirmasi: "+$("buyerEmail").value); addReviewLine(buyerCard,"WhatsApp: "+$("buyerPhone").value); buyer.append(buyerCard); const list=$("reviewParticipants"); list.replaceChildren(); participantData.forEach((person,index)=>{ const card=document.createElement("div"); card.className="review-entry"; addReviewLine(card,"PESERTA "+(index+1),true); addReviewLine(card,"Nama: "+person.first_name+" "+person.last_name); addReviewLine(card,"Nama BIB: "+person.bib_name); addReviewLine(card,"Kategori: "+ (person.category==="early"?"Early Bird":"Reguler")); addReviewLine(card,"Tanggal Lahir: "+person.birth_date); addReviewLine(card,"Jenis Kelamin: "+person.gender); addReviewLine(card,"Ukuran Jersey: "+person.jersey_size); addReviewLine(card,"Jenis Identitas: "+person.identity_type); addReviewLine(card,"Nomor Identitas: "+person.identity_number); addReviewLine(card,"Status Kesehatan: "+person.health_status); const address=[ person.street_address ].filter(Boolean).join(", "); addReviewLine(card,"Alamat Lengkap: "+address); if(person.phone){ addReviewLine(card,"WhatsApp Peserta: "+person.phone); } addReviewLine(card,"Kontak Darurat: "+ person.emergency_name+" ("+person.emergency_phone+")"); addReviewLine(card,"Hubungan: "+person.emergency_relation); list.append(card); }); $("reviewCount").textContent=totalPeople()+" orang"; $("reviewTotal").textContent=rupiah(totalPrice()); $("agreeRules").checked=false; selectPayment("QRIS_DEMO"); updatePaymentButton(); showRegistrationStep(3); }); /* PAYMENT DEMO */ function selectPayment(method){ selectedPayment=method; document.querySelectorAll(".payment-option").forEach(button=>{ button.classList.toggle("selected",button.dataset.method===method); }); updatePaymentButton(); } function updatePaymentButton(){ $("paymentButton").disabled= registrationSubmitting||registrationCompleted|| !selectedPayment||!$("agreeRules").checked; } $("agreeRules").addEventListener("change",updatePaymentButton); let registrationSubmitting=false; let registrationCompleted=false; function showRegistrationResult(message, success=false){ const target=$("registrationResult"); target.replaceChildren(); target.hidden=false; target.style.borderLeftColor=success?"#77d99b":"#ff691f"; const lines=String(message).split("\n"); lines.forEach((line,index)=>{ if(index)target.append(document.createElement("br")); target.append(document.createTextNode(line)); }); target.scrollIntoView({behavior:"smooth",block:"nearest"}); } async function previewPayment(){ if(MIGRATION_REGISTRATION_CLOSED){alert("Pendaftaran ditutup sementara selama migrasi sistem.");return;} if(registrationSubmitting||registrationCompleted)return; if(!$("agreeRules").checked||!selectedPayment){ alert("Setujui peraturan dan pilih metode pembayaran."); return; } if(!participantData.length||participantData.length!==totalPeople()){ alert("Jumlah peserta tidak sesuai. Silakan periksa formulir."); return; } const button=$("paymentButton"); registrationSubmitting=true; button.disabled=true; button.textContent="MENYIMPAN PENDAFTARAN..."; $("registrationResult").hidden=true; try{ // Periksa kembali kategori yang masih tersedia. const statusResponse=await fetch("/api/ticket-status",{cache:"no-store"}); if(!statusResponse.ok)throw new Error("Status tiket belum dapat diperiksa."); const currentStatus=await statusResponse.json(); const selectedCategory=qty.early>0?"early_bird":"normal"; if(currentStatus.success!==true || currentStatus[selectedCategory]?.can_register!==true){ throw new Error("Kategori tiket sudah tidak tersedia. Silakan kembali ke pilihan tiket."); } const payload={ buyer:{ name:$("buyerName").value.trim(), email:$("buyerEmail").value.trim(), phone:$("buyerPhone").value.trim() }, participants:participantData.map(person=>({ ...person, category:person.category==="early"?"EARLY_BIRD":"NORMAL", ticket_category:person.category==="early"?"EARLY_BIRD":"NORMAL" })), payment_method:selectedPayment }; const response=await fetch("/api/register",{ method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(payload), cache:"no-store" }); const data=await response.json().catch(()=>null); if(!response.ok||data?.success!==true){ throw new Error(data?.error||"Pendaftaran gagal disimpan. Silakan coba lagi."); } registrationCompleted=true; const orderNumber=String(data.order_number||""); const paymentToken=String(data.payment_token||""); if(!/^BDJ-[A-HJ-NP-Z2-9]{5}$/.test(orderNumber) || !/^[a-f0-9]{64}$/.test(paymentToken)){ showRegistrationResult("Pendaftaran tersimpan, tetapi token pembayaran simulasi tidak diterima. Hubungi panitia dengan nomor pesanan: "+orderNumber); button.textContent="PENDAFTARAN TERSIMPAN"; return; } // Akses hanya disimpan di sessionStorage pada tab browser ini (maksimal 24 jam). // Jangan bagikan token/tautan; cek pesanan publik tidak pernah mengembalikan token. try { sessionStorage.setItem("bdj_demo_payment_"+orderNumber, JSON.stringify({order:orderNumber,token:paymentToken,savedAt:Date.now()})); } catch (_) {} // QR adalah kredensial simulasi khusus pesanan; jangan dibagikan kepada orang lain. const paymentUrl=new URL("payment-demo.html",window.location.href); paymentUrl.hash=new URLSearchParams({order:orderNumber,token:paymentToken}).toString(); showRegistrationResult("PENDAFTARAN DEMO TERSIMPAN\nNomor pesanan: "+orderNumber+"\nJumlah peserta: "+data.total_participants+"\nTotal simulasi: "+rupiah(data.total_amount)+"\nStatus: PENDING_DEMO\n\nPindai QR tautan atau buka halaman pembayaran DEMO untuk melanjutkan simulasi. Belum lunas.",true); const panel=document.createElement("div"); panel.style.cssText="margin-top:20px;padding:18px;background:#fff;color:#24362b;border-radius:14px;text-align:center"; const title=document.createElement("h3");title.textContent="QR TAUTAN SIMULASI";title.style.cssText="font-size:24px;margin:5px"; const qr=document.createElement("div");qr.style.cssText="display:flex;justify-content:center;margin:15px auto"; panel.append(title,qr); const caption=document.createElement("p");caption.textContent="QR ini membuka halaman DEMO khusus pesanan, bukan QRIS bank. Jangan bagikan QR karena berisi akses simulasi pesanan."; caption.style.cssText="font-size:12px;line-height:1.7";panel.append(caption); const open=document.createElement("a");open.className="btn";open.textContent="BUKA HALAMAN PEMBAYARAN DEMO →"; open.href=paymentUrl.href;open.style.margin="8px";panel.append(open); const save=document.createElement("button");save.type="button";save.className="btn btn-outline";save.style.cssText="margin:8px;color:#24362b;border-color:#24362b";save.textContent="SIMPAN QR TAUTAN ↓"; save.onclick=()=>{const canvas=qr.querySelector("canvas"),img=qr.querySelector("img");const url=canvas?.toDataURL("image/png")||img?.src;if(!url){alert("QR belum siap");return;}const a=document.createElement("a");a.href=url;a.download="QR-DEMO-"+orderNumber+".png";a.click();}; panel.append(save);$("registrationResult").append(panel); if(typeof QRCode!=="undefined")new QRCode(qr,{text:paymentUrl.href,width:220,height:220,correctLevel:QRCode.CorrectLevel.M}); else {const fallback=document.createElement("p");fallback.textContent="Generator QR belum termuat. Gunakan tombol Buka Halaman Pembayaran.";qr.append(fallback);} button.textContent="PENDAFTARAN DEMO TERSIMPAN"; await refreshTicketStatus(); }catch(error){ showRegistrationResult(error?.message||"Terjadi kesalahan sistem."); button.textContent="COBA SIMPAN PENDAFTARAN →"; }finally{ registrationSubmitting=false; button.disabled=registrationCompleted||!selectedPayment||!$("agreeRules").checked; } } /* PARALLAX VISIBLE V3: animasi seluruh 3 slide tanpa library eksternal. */ function initializeParallax(){ const reduced=matchMedia("(prefers-reduced-motion: reduce)").matches; const mobile=matchMedia("(max-width:768px)").matches; const ticket=document.querySelector("#chooseYourTicketHeading .orange"); if(ticket)ticket.textContent="TICKET"; if(reduced)return; const blocks=[...document.querySelectorAll(".event-block")]; const items=[]; const selector="h1,h2,h3,.kicker,.copy,.registration-intro,.registration-steps,.ticket-card,.ticket-name,.ticket-price,.ticket-counter,.order-summary,.notice,.registration-actions,.reg-medal-showcase,.reg-benefits-showcase,.reg-benefits-banner,.check-order-layout,.check-order-title,.check-order-panel,.jersey-description,.jersey-frame,.jersey-note,.route-frame,.racepack-card,.rules-info,.rules-list,.hero-actions,.btn,.hero-brand,.hero-event-date,.hero-venue,.hero-people,.hero-medal-backdrop,.hero-six,.hero-spark,img"; blocks.forEach(block=>{ block.querySelectorAll(selector).forEach(el=>{ if(el.closest("[hidden],[aria-hidden='true']"))return; if(el.closest(".registration-panel") && el.closest(".registration-panel").hidden)return; if(el.matches("img") && el.closest(".jersey-frame,.route-frame,.reg-medal-showcase,.reg-benefits-showcase,.hero-people,.hero-medal-backdrop"))return; if(el.matches(".btn") && el.closest(".registration-actions,.hero-actions,.hero-countdown"))return; // Jangan menghilangkan form saat pengguna melanjutkan proses pendaftaran. if(el.closest("#participantStep,#reviewStep,#paymentStep"))return; if(items.some(parent=>parent.contains(el)))return; items.push(el); }); }); const io=new IntersectionObserver(entries=>{ entries.forEach(entry=>{ if(entry.isIntersecting){ entry.target.classList.add("bf-visible"); io.unobserve(entry.target); } }); },{threshold:.06,rootMargin:"0px 0px -7% 0px"}); items.forEach((el,i)=>{ // Hero langsung tampil; efek masuk diputar tanpa menunggu scroll. if(el.closest(".block-hero")){ el.classList.add("bf-entry","bf-visible"); }else{ el.classList.add("bf-entry"); io.observe(el); } el.style.setProperty("--bf-delay",`${Math.min(i%4,3)*90}ms`); }); const layers=[ [".block-hero .hero-people",110], [".block-hero .hero-medal-backdrop",-100], [".block-hero .hero-six",95], [".block-hero .hero-brand",-55], [".block-registration .reg-medal-showcase",100], [".block-registration .reg-benefits-banner",-85], [".block-registration .check-order-panel",65], [".block-info .jersey-frame",100], [".block-info .route-frame",-100], [".block-info .racepack-card",80], [".block-info .rules-info",70] ].map(([sel,dist])=>({el:document.querySelector(sel),dist})).filter(x=>x.el); let pending=false; const paint=()=>{ pending=false; const vh=innerHeight||800; layers.forEach(({el,dist})=>{ if(el.closest("[hidden]"))return; const r=el.getBoundingClientRect(); if(r.bottom< -vh || r.top>vh*2)return; const progress=(vh-r.top)/(vh+r.height); const movement=Math.max(-1,Math.min(1,(progress-.5)*2))*dist*(mobile?.7:1); el.style.setProperty("--bf-depth",`${movement.toFixed(1)}px`); }); }; const onScroll=()=>{if(!pending){pending=true;requestAnimationFrame(paint)}}; addEventListener("scroll",onScroll,{passive:true}); addEventListener("resize",onScroll,{passive:true}); paint(); } if(document.readyState==="loading"){ document.addEventListener("DOMContentLoaded",initializeParallax,{once:true}); }else{initializeParallax();}