Securely signing you in…
'); w.document.close(); } function downloadIcs(eid) { var ev = (S.data.events || []).find(function(e) { return e.id === eid; }); if (!ev) return; var chk = document.getElementById('cal-inc-lodging'); var includeLodging = chk ? chk.checked : false; var startYmd = ymd(ev.start_date); var endYmd = ev.end_date ? ymd(ev.end_date) : startYmd; var dtStart, dtEnd; if (ev.event_time) { var hm = String(ev.event_time).split(':'); var hh = pad2(parseInt(hm[0], 10) || 0), mm = pad2(parseInt(hm[1], 10) || 0); var endHour = pad2((parseInt(hh, 10) + 2) % 24); dtStart = 'DTSTART:' + startYmd + 'T' + hh + mm + '00'; dtEnd = 'DTEND:' + startYmd + 'T' + endHour + mm + '00'; } else { dtStart = 'DTSTART;VALUE=DATE:' + startYmd; dtEnd = 'DTEND;VALUE=DATE:' + addDaysYmd(endYmd, 1); } var asgn = (S.data.assignments || []).find(function(a) { return a.event_id === eid; }); var role = (asgn && asgn.role) || 'Volunteer'; var descLines = ['Role: ' + role]; if (ev.location) descLines.push('Location: ' + ev.location); if (ev.notes) descLines.push(ev.notes); if (includeLodging && ev.hotel) { descLines.push(''); descLines.push('Lodging: ' + (ev.hotel.name || 'TBD')); if (ev.hotel.address) descLines.push(ev.hotel.address); if (ev.hotel.room) descLines.push('Room type: ' + ev.hotel.room); try { var nights = JSON.parse(ev.hotel.nights || '[]').slice().sort(); if (nights.length) descLines.push('Nights: ' + nights.join(', ')); } catch (e) {} if (ev.hotel.roommate) descLines.push('Roommate: ' + ev.hotel.roommate); } var now = new Date(); var dtStamp = now.getUTCFullYear() + pad2(now.getUTCMonth() + 1) + pad2(now.getUTCDate()) + 'T' + pad2(now.getUTCHours()) + pad2(now.getUTCMinutes()) + pad2(now.getUTCSeconds()) + 'Z'; var uid = 'vp-' + eid + '-' + Date.now() + '@volunteerportal'; var lines = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Volunteer Portal//EN', 'CALSCALE:GREGORIAN', 'BEGIN:VEVENT', 'UID:' + uid, 'DTSTAMP:' + dtStamp, dtStart, dtEnd, 'SUMMARY:' + icsEscape(ev.name) ]; if (ev.location) lines.push('LOCATION:' + icsEscape(ev.location + (ev.address ? ', ' + ev.address : ''))); lines.push('DESCRIPTION:' + icsEscape(descLines.join('\n'))); lines.push('END:VEVENT', 'END:VCALENDAR'); var blob = new Blob([lines.join('\r\n')], { type: 'text/calendar;charset=utf-8' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = (ev.short_name || ev.name || 'event').replace(/[^a-z0-9]/gi, '_') + '.ics'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); forceCloseModal(); toast('Calendar file downloaded', 'ok'); } /* ---------- logo studio: upload -> auto black & white -> exact badge preview -> optional fine tuning ---------- */ var LS = { src: '', settings: null, kit: null, timer: null, token: 0 }; function lsDefaults() { return { brightness: 0, contrast: 0, sharp: false, trim: true, plate: 'auto' }; } (function() { setTimeout(function() { var fi = document.getElementById('logo-file'); if (fi) fi.addEventListener('change', function() { if (!fi.files.length) return; var file = fi.files[0]; fi.value = ''; if (file.size > 12 * 1024 * 1024) { toast('That picture is too big. Please choose one under 12MB.', 'er'); return; } var reader = new FileReader(); reader.onload = function(e) { lsDownscale(e.target.result, 1000).then(function(src) { if (!src) { toast('We could not read that picture. Try a PNG or JPG.', 'er'); return; } openLogoStudio(src, lsDefaults(), true); }); }; reader.readAsDataURL(file); }); }, 100); })(); /* shrink big phone photos before we keep them (keeps transparency for PNGs) */ function lsDownscale(dataUrl, maxSide) { return new Promise(function(res) { var im = new Image(); im.onload = function() { var sc = Math.min(1, maxSide / Math.max(im.naturalWidth, im.naturalHeight)), w = Math.max(2, Math.round(im.naturalWidth * sc)), h = Math.max(2, Math.round(im.naturalHeight * sc)); var c = document.createElement('canvas'); c.width = w; c.height = h; c.getContext('2d').drawImage(im, 0, 0, w, h); var png = c.toDataURL('image/png'); if (png.length < 1800000) { res(png); return; } var c2 = document.createElement('canvas'); c2.width = w; c2.height = h; var x = c2.getContext('2d'); x.fillStyle = '#fff'; x.fillRect(0, 0, w, h); x.drawImage(im, 0, 0, w, h); res(c2.toDataURL('image/jpeg', 0.85)); }; im.onerror = function() { res(''); }; im.src = dataUrl; }); } function lsKit() { if (LS.kit) return Promise.resolve(LS.kit); return new Promise(function(res, rej) { google.script.run.withSuccessHandler(function(k) { LS.kit = k; res(k); }).withFailureHandler(function(e) { rej(e); }).getVolunteerDigitalBadge(S.session.sessionKey); }); } /* fills a shadow-DOM host with the real badge (template from the server, same engine as the admin print) so portal styles can never leak in */ async function lsRenderBadge(host, kit, lg, evIdx) { var ev = (kit.events || [])[evIdx || 0] || null; var season = ev ? ev.season : String(new Date().getFullYear()); var map = VarsBadge.tags({ first: kit.first, last: kit.last, role: ev ? ev.role : 'Volunteer', designation: kit.designation, volCode: kit.volCode, leader: kit.leader, season: season }, lg, { barcodeId: 'bc_preview_' + Math.random().toString(36).slice(2) }); var root = host.shadowRoot || host.attachShadow({ mode: 'open' }); var avail = Math.max(240, host.clientWidth || 340), scale = Math.min(1.4, avail / 384); root.innerHTML = '
' + VarsBadge.fill(kit.template.html, map) + '
'; VarsBadge.drawBarcodes(root); } async function lsFonts() { try { await Promise.all(['500', '700', '800'].map(function(w) { return document.fonts.load(w + ' 20px "Space Grotesk"'); })); await document.fonts.load('700 11px "JetBrains Mono"'); } catch (e) {} } async function openLogoStudio(src, settings, isNew) { LS.src = src; LS.settings = Object.assign(lsDefaults(), settings || {}); LS.token++; var kit; try { kit = await lsKit(); } catch (e) { toast((e && e.message) || 'Could not load your badge', 'er'); return; } var st = LS.settings; var body = '' + '
' + (isNew ? 'We cleaned up your logo for the black & white badge printer.' : 'This is how your logo prints on your badge.') + ' It looks like this on your actual badge:
' + '
' + '' + '
Your upload
' + '
Adjust it (optional)' + '
' + '' + '' + '' + '' + '' + '
'; var foot = '' + (S.ownLogo ? '' : '') + '' + ''; showModal('Your badge logo', body, foot); document.getElementById('ls-plate').value = st.plate; if (!isNew && (st.brightness || st.contrast || st.sharp || st.plate !== 'auto' || st.trim === false)) document.getElementById('ls-adv').open = true; await lsFonts(); lsPreview(); } function lsRead() { return { brightness: parseInt(document.getElementById('ls-b').value, 10) / 100, contrast: parseInt(document.getElementById('ls-c').value, 10) / 100, sharp: document.getElementById('ls-sharp').checked, trim: document.getElementById('ls-trim').checked, plate: document.getElementById('ls-plate').value }; } function lsChanged() { document.getElementById('ls-bv').textContent = document.getElementById('ls-b').value; document.getElementById('ls-cv').textContent = document.getElementById('ls-c').value; LS.settings = lsRead(); clearTimeout(LS.timer); LS.timer = setTimeout(lsPreview, 60); } function lsResetAuto() { LS.settings = lsDefaults(); document.getElementById('ls-b').value = 0; document.getElementById('ls-c').value = 0; document.getElementById('ls-sharp').checked = false; document.getElementById('ls-trim').checked = true; document.getElementById('ls-plate').value = 'auto'; document.getElementById('ls-bv').textContent = '0'; document.getElementById('ls-cv').textContent = '0'; lsPreview(); } async function lsPreview() { var token = ++LS.token, host = document.getElementById('ls-host'); if (!host || !LS.kit) return; var lg = await VarsBadge.prepLogo(LS.src, LS.settings); if (token !== LS.token || !document.getElementById('ls-host')) return; LS.result = lg; await lsRenderBadge(host, LS.kit, lg, 0); var warn = document.getElementById('ls-warn'); if (warn) { var small = lg && lg.w && lg.h && Math.max(lg.w, lg.h) < 90; warn.style.display = small ? 'block' : 'none'; if (small) warn.textContent = 'This picture is very small, so it may look soft when printed. A larger version would look sharper.'; } } async function lsSave() { var btn = document.getElementById('ls-save'); if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; } try { var lg = await VarsBadge.prepLogo(LS.src, LS.settings); if (!lg || !lg.src) throw new Error('Could not prepare the logo'); var m = /^data:([^;]+);base64,(.*)$/.exec(LS.src) || []; google.script.run .withSuccessHandler(function(r) { if (r && r.success) { forceCloseModal(); toast('Logo saved to your badge', 'ok'); LS.kit = null; loadPortal(); } else { toast((r && r.error) || 'Could not save', 'er'); if (btn) { btn.disabled = false; btn.textContent = 'Save logo'; } } }) .withFailureHandler(function(e) { toast((e && e.message) || 'Could not save', 'er'); if (btn) { btn.disabled = false; btn.textContent = 'Save logo'; } }) .saveVolunteerLogoPackage(S.session.sessionKey, lg.src.split(',')[1], 'image/png', m[2] || '', m[1] || 'image/png', JSON.stringify(LS.settings)); } catch (e) { toast(e.message || 'Could not save', 'er'); if (btn) { btn.disabled = false; btn.textContent = 'Save logo'; } } } function lsRemove() { if (!confirm('Remove your logo from your badge?')) return; google.script.run .withSuccessHandler(function() { forceCloseModal(); S.ownLogo = ''; LS.kit = null; toast('Logo removed', 'ok'); loadPortal(); }) .withFailureHandler(function(e) { toast((e && e.message) || 'Could not remove', 'er'); }) .removeVolunteerLogo(S.session.sessionKey); } /* tapping the logo tile / button: no logo yet -> pick a file; already has one -> open it in the studio with the saved settings */ function triggerLogoUpload() { if (!S.ownLogo) { document.getElementById('logo-file').click(); return; } toast('Opening your logo...', 'inf'); google.script.run .withSuccessHandler(function(o) { var src = (o && o.original) || S.ownLogo; openLogoStudio(src, (o && o.original && o.settings) ? o.settings : lsDefaults(), false); }) .withFailureHandler(function() { openLogoStudio(S.ownLogo, lsDefaults(), false); }) .getVolunteerLogoOriginal(S.session.sessionKey); } /* ---------- dashboard ---------- */ function renderDash() { var d = S.data; if (!d) return; var v = d.volunteer || {}; var h = ''; var designationOptions = (d.designations && d.designations.length) ? d.designations : ['Volunteer']; if (d.disabledEvents && d.disabledEvents.length) { h += '
' + d.disabledEvents.map(function(e){return esc(e.name);}).join(', ') + ' is managed by your event coordinator, so there is nothing to fill out here for it. Contact your coordinator with any questions.
'; } h += '
'; h += '
'; h += '
'; h += '
Badge logo
'; h += '
'; h += '
'; h += '

' + esc((v.preferred_name || v.first_name || '') + ' ' + (v.last_name || '')) + '

'; h += '
' + esc(v.email||'') + (v.phone ? ' · ' + esc(v.phone) : '') + '
'; if (v.vol_code) { h += '
Code ' + esc(v.vol_code); h += ''; h += ''; h += '
'; } h += '
'; h += '
'; h += ''; h += ''; h += ''; h += ''; h += '
'; h += '
'; h += '
'; h += '
Want your organization or team logo printed on your name badge? Tap "Upload Badge Logo" above to add it. It shows up exactly where the logo appears on your printed badge.
'; h += '
'; var upcoming = (d.events || []).filter(function(ev) { if (!ev.start_date) return false; var dt = new Date(ev.start_date); dt.setHours(0,0,0,0); var now = new Date(); now.setHours(0,0,0,0); return dt >= now; }).sort(function(a, b) { return new Date(a.start_date) - new Date(b.start_date); }); if (upcoming.length) { var nu = upcoming[0]; var nuRes = d.responses.find(function(r) { return r.event_id === nu.id; }); var nuDays = daysAway(nu.start_date); h += '
'; h += '
Next Up' + (nuDays ? ' · ' + esc(nuDays) : '') + '
'; h += '
' + esc(nu.name) + '
'; h += '
' + esc(nu.start_date_fmt || '') + (nu.location ? ' · ' + esc(nu.location) : '') + (nuRes ? '' : ' · action needed') + '
'; h += '
'; } h += renderStatsAndActivity(d); h += '
Your events
'; if (d.events && d.events.length) { d.events.forEach(function(ev, idx) { var res = d.responses.find(function(r) { return r.event_id === ev.id; }); var asgnForDays = (d.assignments || []).find(function(a) { return a.event_id === ev.id; }); var justSubmitted = S.justSubmittedEventId === ev.id; h += '
'; h += '
'; h += '

' + esc(ev.name) + '

'; if (res) h += 'Submitted'; else h += 'Not Confirmed'; h += '
'; if (!res) { h += '
NOT CONFIRMED — open the form below to submit
'; } h += '
'; if (ev.start_date_fmt) { var dr = ev.start_date_fmt; if (ev.end_date_fmt && ev.end_date_fmt !== ev.start_date_fmt) dr += ' to ' + ev.end_date_fmt; var da = daysAway(ev.start_date); h += '' + esc(dr) + (da ? ' · ' + da : '') + ''; } if (ev.season) { h += '' + esc(ev.season) + ''; } if (ev.checkedIn) { h += 'Checked in'; } h += '
'; if (asgnForDays && asgnForDays.daysAvailable && asgnForDays.daysAvailable.length) { var dayLabels = asgnForDays.daysAvailable.map(function(iso) { var match = ((d.eventDaysByEvent && d.eventDaysByEvent[ev.id]) || []).find(function(dd) { return dd.iso === iso; }); return match ? match.label : iso; }); h += '
Available: ' + esc(dayLabels.join(', ')) + '
'; } if (ev.hotel) { h += renderStayCard(ev.hotel); } if (ev.location || ev.address) { var mapsUrl = 'https:\/\/maps.google.com/?q=' + encodeURIComponent((ev.address || ev.location || '')); h += ''; h += '
'; h += '
'; if (ev.location) h += '
' + esc(ev.location) + '
'; if (ev.address) h += '
' + esc(ev.address) + '
'; h += '
'; h += '
'; h += '
'; } if (ev.notes) h += '
' + esc(ev.notes) + '
'; h += '
'; h += ''; h += ''; if (ev.website) h += 'Event Website'; h += '
'; h += ''; h += '
'; }); } else { h += '
'; h += ''; h += '

No active events

'; h += '

You have no event assignments at this time.

'; } document.getElementById('pmain').innerHTML = h; updateLogoTile(); animateStatCounters(); if (S.justSubmittedEventId) { var celebratedId = S.justSubmittedEventId; S.justSubmittedEventId = null; var cardEl = document.getElementById('ev-' + celebratedId); if (cardEl) cardEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } var MILESTONES = [ { min: 0, label: 'Welcome aboard', sub: 'Your volunteer journey with us', icon: '' }, { min: 3, label: 'Regular', sub: "You're becoming a familiar face", icon: '' }, { min: 6, label: 'Veteran', sub: "You've put in real time with us", icon: '' }, { min: 10, label: 'All-Star', sub: "Double digits -- that's dedication", icon: '' }, { min: 20, label: 'Legend', sub: 'One of our most dedicated volunteers, ever', icon: '' } ]; function milestoneFor(n) { var m = MILESTONES[0]; for (var i = 0; i < MILESTONES.length; i++) { if (n >= MILESTONES[i].min) m = MILESTONES[i]; } return m; } function renderStatsAndActivity(d) { var stats = d.stats || { totalEvents: 0, totalCheckins: 0, totalMeals: 0 }; var activity = d.recentActivity || []; var hasAnything = stats.totalCheckins > 0 || stats.totalMeals > 0; if (!hasAnything) return ''; var m = milestoneFor(stats.totalEvents); var h = ''; h += '
' + m.icon + '
' + esc(m.label) + '
' + esc(m.sub) + '
'; h += '
'; h += '
0
Events
'; h += '
0
Check-ins
'; h += '
0
Meals
'; h += '
'; if (activity.length) { h += ''; h += '
'; activity.forEach(function(a, i) { var isCheckin = a.type === 'checkin'; var icon = isCheckin ? '' : ''; var title = isCheckin ? 'Checked in' : esc(a.mealType || 'Meal') + ' scanned'; h += '
'; h += '
' + icon + '
'; h += '
' + title + '
' + esc(a.eventName || '') + (a.eventName ? ' · ' : '') + timeAgo(a.at) + '
'; h += '
'; }); h += '
'; } return h; } function animateStatCounters() { document.querySelectorAll('.stat-num[data-target]').forEach(function(el) { var target = parseInt(el.getAttribute('data-target'), 10) || 0; if (target === 0) { el.textContent = '0'; return; } var start = null; var dur = 700; function step(ts) { if (!start) start = ts; var p = Math.min(1, (ts - start) / dur); var eased = 1 - Math.pow(1 - p, 3); el.textContent = Math.round(eased * target); if (p < 1) requestAnimationFrame(step); else el.textContent = target; } requestAnimationFrame(step); }); } function showEvForm(eid) { var con = document.getElementById('evf-' + eid); if (!con) return; if (!con.classList.contains('hidden')) { con.classList.add('hidden'); return; } var res = S.data.responses.find(function(r) { return r.event_id === eid; }); var asgnForAddons = (S.data.assignments || []).find(function(a) { return a.event_id === eid; }); if (!S.fs[eid]) S.fs[eid] = { meals: {}, noMeals: false, questions: {}, dietary: '', addonPositions: [], daysAvailable: [] }; if (!S.fs[eid]._loaded) { var draft = null; try { draft = JSON.parse(localStorage.getItem('vars_draft_' + eid)); } catch(e) {} // A draft only wins if it was saved after the server's last known save for this // response -- otherwise a stale/empty draft from a previous visit would silently // shadow real submitted data forever, since nothing here ever expires it. var draftIsFresh = draft && draft.savedAt && (!res || !res.updated_at || draft.savedAt > new Date(res.updated_at).getTime()); if (draftIsFresh) { S.fs[eid].meals = draft.meals || {}; S.fs[eid].noMeals = !!draft.noMeals; S.fs[eid].questions = draft.questions || {}; S.fs[eid].dietary = draft.dietary || ''; S.fs[eid].addonPositions = draft.addonPositions || []; S.fs[eid].daysAvailable = draft.daysAvailable || []; } else { if (res) { try { localStorage.removeItem('vars_draft_' + eid); } catch(e) {} if (res.meals) S.fs[eid].meals = JSON.parse(JSON.stringify(res.meals)); if (res.questions) S.fs[eid].questions = JSON.parse(JSON.stringify(res.questions)); S.fs[eid].dietary = (res.profile && res.profile.dietary) || ''; S.fs[eid].noMeals = !!(res.profile && res.profile.noMeals); } S.fs[eid].addonPositions = (asgnForAddons && asgnForAddons.addonPositions) ? asgnForAddons.addonPositions.slice() : []; S.fs[eid].daysAvailable = (asgnForAddons && asgnForAddons.daysAvailable) ? asgnForAddons.daysAvailable.slice() : []; } S.fs[eid]._loaded = true; } renderEvForm(eid); con.classList.remove('hidden'); con.classList.remove('evf-pop'); void con.offsetWidth; con.classList.add('evf-pop'); } function renderEvForm(eid) { var ev = S.data.events.find(function(e) { return e.id === eid; }); var con = document.getElementById('evf-' + eid); if (!ev || !con) return; var h = ''; var qs = (S.data.questions || []).filter(function(q) { return q.event_id === eid; }); if (qs.length > 0) { h += '

Event questions

'; qs.forEach(function(q) { var val = S.fs[eid].questions[q.id]; h += '
'; if ((q.type === 'select' || q.type === 'single_select') && q.options && q.options.length) { h += ''; } else if (q.type === 'multi_select' && q.options && q.options.length) { var selArr = Array.isArray(val) ? val : (val ? [val] : []); h += '
'; q.options.forEach(function(o) { var checked = selArr.indexOf(o) > -1 ? 'checked' : ''; h += ''; }); h += '
'; } else if (q.type === 'boolean') { h += ''; } else { h += ''; } h += '
'; }); } var eventDays = (S.data.eventDaysByEvent && S.data.eventDaysByEvent[eid]) || []; if (eventDays.length > 0) { h += '

Which days can you volunteer?

'; h += '
'; eventDays.forEach(function(d) { var checked = (S.fs[eid].daysAvailable || []).indexOf(d.iso) > -1 ? 'checked' : ''; h += ''; }); h += '
'; } var addonOpts = (S.data.addonPositionsByEvent && S.data.addonPositionsByEvent[eid]) || []; if (addonOpts.length > 0) { h += '

Add-on positions (optional)

'; h += '
'; addonOpts.forEach(function(p) { var checked = (S.fs[eid].addonPositions || []).indexOf(p) > -1 ? 'checked' : ''; h += ''; }); h += '
'; } var slots = (S.data.mealSlots || []).filter(function(s) { return s.event_id === eid && s.provided; }); if (slots.length > 0) { var days = [], slotsByDay = {}; slots.forEach(function(sl) { if (!slotsByDay[sl.meal_date_fmt]) { slotsByDay[sl.meal_date_fmt] = {}; days.push(sl.meal_date_fmt); } slotsByDay[sl.meal_date_fmt][sl.meal_type] = sl; }); h += '

Meal selection

'; h += '
'; h += ''; days.forEach(function(d) { h += ''; }); h += ''; var noMeals = !!S.fs[eid].noMeals; ['Breakfast','Lunch','Dinner'].forEach(function(t) { var hasRow = days.some(function(d) { return slotsByDay[d][t]; }); if (!hasRow) return; h += ''; days.forEach(function(d) { h += ''; }); h += ''; }); h += '
Meal' + esc(d) + '
' + t + ''; var sl = slotsByDay[d][t]; if (sl) { var chk = (!noMeals && S.fs[eid].meals[sl.id]) ? 'checked' : ''; var dis = noMeals ? 'disabled' : ''; h += ''; } h += '
'; h += ''; var anyMealChecked = !noMeals && Object.keys(S.fs[eid].meals || {}).some(function(k) { return S.fs[eid].meals[k]; }); if (anyMealChecked) { h += '
'; h += ''; h += '
'; } } var res = S.data.responses.find(function(r) { return r.event_id === eid; }); h += ''; con.innerHTML = h; } function showSubmitBlocker() { var el = document.getElementById('submit-blocker'); if (el) el.classList.remove('hidden'); } function hideSubmitBlocker() { var el = document.getElementById('submit-blocker'); if (el) el.classList.add('hidden'); } function subForm(eid) { var btn = document.getElementById('sbtn-' + eid); if (!btn) return; var mealSlots = (S.data.mealSlots || []).filter(function(s) { return s.event_id === eid && s.provided; }); if (mealSlots.length > 0) { var fs = S.fs[eid] || {}; var anyMealChecked = Object.keys(fs.meals || {}).some(function(k) { return fs.meals[k]; }); if (!anyMealChecked && !fs.noMeals) { toast('Select at least one meal, or check "I will not be eating any meals at this event"', 'er'); return; } } btn.disabled = true; btn.innerHTML = '
Saving...'; showSubmitBlocker(); var payload = S.fs[eid] || {}; google.script.run .withSuccessHandler(function(r) { hideSubmitBlocker(); if (r.success) { try { localStorage.removeItem('vars_draft_' + eid); } catch(e) {} btn.disabled = false; btn.innerHTML = 'Submitted'; btn.style.background = 'var(--su)'; btn.style.borderColor = 'transparent'; btn.style.color = '#08240f'; toast('Form saved', 'ok'); S.justSubmittedEventId = eid; google.script.run.withSuccessHandler(function(d) { S.data = d; renderDash(); }).withFailureHandler(function() {}).getVolunteerPortalData(S.session.sessionKey); } else { toast(r.error || 'Submission failed', 'er'); btn.disabled = false; btn.textContent = 'Submit form'; } }) .withFailureHandler(function(e) { hideSubmitBlocker(); toast(e.message || 'Network error', 'er'); btn.disabled = false; btn.textContent = 'Submit form'; }) .submitVolunteerForm(S.session.sessionKey, eid, JSON.stringify(payload)); } /* ---------- public application form (/apply/) ---------- */ var AS = { eventId: null, setup: null, days: [], positions: {}, questions: {}, meals: {} }; function initApplyMode(eventId) { AS.eventId = eventId; sv('vload'); var msg = document.getElementById('magicLoadText'); if (msg) msg.textContent = 'Loading application form…'; var actions = document.getElementById('magicLoadActions'); if (actions) actions.style.display = 'none'; google.script.run .withSuccessHandler(renderApplyForm) .withFailureHandler(function(e) { renderApplyError((e && e.message) || 'This application link is invalid or the event is not accepting applications.'); }) .getApplyFormData(eventId); } function renderApplyError(msg) { var card = document.getElementById('applyCard'); card.innerHTML = '
VARS

Applications closed

' + esc(msg) + '

'; sv('vapply'); } function renderApplyForm(setup) { AS.setup = setup; var h = ''; h += '
VARS
'; h += '

' + esc(setup.eventName) + '

'; h += '

' + (setup.description ? esc(setup.description) : 'Apply to volunteer with ' + esc(setup.org || 'us') + '.') + ' Fields marked * are required.

'; h += '
'; h += '
'; h += '
'; h += '
'; if ((setup.eventDays || []).length) { h += '
'; setup.eventDays.forEach(function(d) { h += ''; }); h += '
'; } var posOpts = '' + (setup.positions || []).map(function(p) { return ''; }).join(''); if (setup.requirePositionTiers !== false) { h += '
'; h += '
'; h += '
'; } else { h += '
'; } if ((setup.addonPositions || []).length) { h += '
'; setup.addonPositions.forEach(function(p) { h += ''; }); h += '
'; } (setup.questions || []).forEach(function(q) { h += '
'; if ((q.type === 'select' || q.type === 'single_select') && (q.options || []).length) { h += ''; } else if (q.type === 'boolean') { h += ''; } else { h += ''; } h += '
'; }); h += ''; h += '

By continuing you agree to our Privacy Policy.

'; document.getElementById('applyCard').innerHTML = h; sv('vapply'); } function submitApplyForm() { var setup = AS.setup; var btn = document.getElementById('ap-submit'); var firstName = document.getElementById('ap-fn').value.trim(); var lastName = document.getElementById('ap-ln').value.trim(); var email = document.getElementById('ap-em').value.trim(); var phone = document.getElementById('ap-ph').value.trim(); if (!firstName || !lastName || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || phone.replace(/\D/g, '').length < 10) { toast('Please fill in your name, a valid email and a 10-digit phone number', 'er'); return; } var daysAvailable = Array.prototype.slice.call(document.querySelectorAll('.ap-day:checked')).map(function(el) { return el.value; }); if (!daysAvailable.length) { toast('Select at least one day you can volunteer', 'er'); return; } var positionChoices = []; [1, 2, 3].forEach(function(tier) { var el = document.getElementById('ap-pos' + tier); if (el && el.value) positionChoices.push({ tier: tier, positionId: el.value }); }); if (!positionChoices.length) { toast('Select at least your 1st choice position', 'er'); return; } var addonPositions = Array.prototype.slice.call(document.querySelectorAll('.ap-addon:checked')).map(function(el) { return el.value; }); var answers = {}; Array.prototype.slice.call(document.querySelectorAll('.ap-q')).forEach(function(el) { answers[el.dataset.qid] = el.value; }); for (var i = 0; i < (setup.questions || []).length; i++) { var q = setup.questions[i]; if (q.required && !String(answers[q.id] || '').trim()) { toast('Please answer all required questions', 'er'); return; } } var payload = { firstName: firstName, lastName: lastName, email: email, phone: phone, daysAvailable: daysAvailable, positionChoices: positionChoices, addonPositions: addonPositions, answers: answers }; btn.disabled = true; btn.innerHTML = '
Submitting…'; showSubmitBlocker(); google.script.run .withSuccessHandler(function(r) { hideSubmitBlocker(); var card = document.getElementById('applyCard'); var thankYou = setup.thankYouMessage ? esc(setup.thankYouMessage) : 'Your application has been submitted. Your event coordinator will follow up by email once positions are assigned.'; card.innerHTML = '
VARS

Thanks for applying!

' + thankYou + '

Once you’re accepted, come back to vars.gafirst.org and sign in with this email and phone number to confirm your meals and details.

'; }) .withFailureHandler(function(e) { hideSubmitBlocker(); toast(e.message || 'Submission failed', 'er'); btn.disabled = false; btn.textContent = 'Submit application'; }) .submitApplication(AS.eventId, JSON.stringify(payload)); } var applyMatch = location.pathname.match(/^\/apply\/([^/]+)/); if (applyMatch) { initApplyMode(decodeURIComponent(applyMatch[1])); } else { startVolunteerPortal_(); } Privacy Policy