added admin page

This commit is contained in:
2026-03-11 21:18:58 -04:00
parent 10ab8a64cb
commit 504e5b9bc7
44 changed files with 2075 additions and 140 deletions

24
dist/assets/admin-BaZpN3Uw.js vendored Normal file
View File

@@ -0,0 +1,24 @@
import{i as C,j as A,n as p,p as D,f as b,d as S,v as k,x as M,y as $,t as x,z as T,A as F,B as Y,C as N}from"./donors-DDFnEjnq.js";const f=["pcgurudm@gmail.com"];let m=[];document.addEventListener("DOMContentLoaded",()=>{C(),H(),V(),P(),j(),_()});function H(){document.getElementById("loginGoogle").addEventListener("click",A),document.getElementById("logoutBtn").addEventListener("click",p),document.getElementById("unauthorizedLogout").addEventListener("click",p),D(q)}async function q(t){const e=document.getElementById("authSection"),n=document.getElementById("unauthorizedSection"),o=document.getElementById("adminContent"),d=document.getElementById("logoutBtn");if(!t){e.classList.remove("hidden"),n.classList.add("hidden"),o.classList.add("hidden"),d.style.display="none";return}if(d.style.display="",!await z(t.email)){e.classList.add("hidden"),n.classList.remove("hidden"),o.classList.add("hidden");return}e.classList.add("hidden"),n.classList.add("hidden"),o.classList.remove("hidden"),g()}async function z(t){try{const e=b(S,"config","admins"),n=await k(e);return n.exists()?(n.data()?.emails||[]).includes(t):f.includes(t)?(await M(e,{emails:f}),console.log("Admin allowlist created with seed admins."),!0):!1}catch(e){return console.error("Admin check failed:",e),!1}}function V(){document.querySelectorAll(".admin-tab").forEach(t=>{t.addEventListener("click",()=>{document.querySelectorAll(".admin-tab").forEach(e=>e.classList.remove("active")),document.querySelectorAll(".admin-panel").forEach(e=>e.classList.remove("active")),t.classList.add("active"),document.getElementById(`panel-${t.dataset.tab}`).classList.add("active")})})}async function g(){try{m=await $(),R(),L()}catch(t){console.error("Failed to load donors:",t),r("Failed to load donors.","error")}}function R(){const t=x(m);document.getElementById("statTotal").textContent=`$${t.toLocaleString()}`,document.getElementById("statCount").textContent=m.length}function P(){const t=document.getElementById("addDonorForm");t.addEventListener("submit",async e=>{e.preventDefault();const n=document.getElementById("addDonorBtn");n.disabled=!0,n.textContent="Adding...";try{await T({name:document.getElementById("donorName").value.trim(),amount:document.getElementById("donorAmount").value,classYear:document.getElementById("donorClassYear").value.trim(),message:document.getElementById("donorMessage").value.trim(),anonymous:document.getElementById("donorAnonymous").checked,date:document.getElementById("donorDate").value||null}),r("Donor added!","success"),t.reset(),await g()}catch(o){console.error("Failed to add donor:",o),r("Failed to add donor.","error")}finally{n.disabled=!1,n.textContent="Add Donor"}})}let u=[],h=[];function j(){document.getElementById("csvFile").addEventListener("change",G),document.getElementById("importBtn").addEventListener("click",O),document.getElementById("csvCancelBtn").addEventListener("click",I),document.querySelectorAll(".mapping-grid select").forEach(e=>{e.addEventListener("change",B)})}function G(t){const e=t.target.files[0];if(!e)return;const n=new FileReader;n.onload=o=>{const d=o.target.result,a=U(d);if(a.length<2){r("CSV file appears empty or has no data rows.","error");return}h=a[0],u=a.slice(1),W(),document.getElementById("csvMapping").classList.remove("hidden")},n.readAsText(e)}function U(t){const e=[];let n=[],o="",d=!1;for(let a=0;a<t.length;a++){const s=t[a],i=t[a+1];d?s==='"'&&i==='"'?(o+='"',a++):s==='"'?d=!1:o+=s:s==='"'?d=!0:s===","?(n.push(o.trim()),o=""):s===`
`||s==="\r"&&i===`
`?(n.push(o.trim()),n.some(l=>l!=="")&&e.push(n),n=[],o="",s==="\r"&&a++):o+=s}return n.push(o.trim()),n.some(a=>a!=="")&&e.push(n),e}function W(){const t=["mapName","mapAmount","mapClassYear","mapMessage","mapDate","mapAnonymous"],e={mapName:["name","donor","first","full"],mapAmount:["amount","donation","gift","total","sum"],mapClassYear:["class","year","grad"],mapMessage:["message","note","comment"],mapDate:["date","time","when"],mapAnonymous:["anonymous","anon"]};for(const n of t){const o=document.getElementById(n);o.innerHTML='<option value="">-- skip --</option>',h.forEach((s,i)=>{const l=document.createElement("option");l.value=i,l.textContent=s,o.appendChild(l)});const d=e[n],a=h.findIndex(s=>d.some(i=>s.toLowerCase().includes(i)));a!==-1&&(o.value=a)}B()}function E(t){const e=n=>{const o=document.getElementById(n).value;return o!==""&&t[Number(o)]||""};return{name:e("mapName"),amount:e("mapAmount"),classYear:e("mapClassYear"),message:e("mapMessage"),date:e("mapDate"),anonymous:e("mapAnonymous")}}function B(){const t=document.getElementById("csvPreviewHead"),e=document.getElementById("csvPreviewBody");t.innerHTML="<tr><th>Name</th><th>Amount</th><th>Class Year</th><th>Date</th><th>Message</th><th>Anon</th></tr>";const n=u.slice(0,5);e.innerHTML=n.map(o=>{const d=E(o);return`<tr>
<td>${c(d.name)}</td>
<td>${c(d.amount)}</td>
<td>${c(d.classYear)}</td>
<td>${c(d.date)}</td>
<td>${c(d.message)}</td>
<td>${y(d.anonymous)?"Yes":"No"}</td>
</tr>`}).join(""),document.getElementById("previewCount").textContent=`(${u.length} rows total)`}function y(t){if(!t)return!1;const e=t.toString().toLowerCase().trim();return e==="true"||e==="yes"||e==="1"}async function O(){const t=document.getElementById("importBtn"),e=document.getElementById("importProgress"),n=document.getElementById("progressFill"),o=document.getElementById("progressText"),d=u.map(a=>{const s=E(a),i=parseFloat(s.amount.replace(/[^0-9.\-]/g,""));return!s.name&&!y(s.anonymous)||isNaN(i)||i<=0?null:{name:s.name||"Anonymous",amount:i,classYear:s.classYear,message:s.message,date:s.date||null,anonymous:y(s.anonymous)}}).filter(Boolean);if(d.length===0){r("No valid rows to import. Check your column mapping.","error");return}if(confirm(`Import ${d.length} donors? (${u.length-d.length} rows will be skipped due to missing name/amount)`)){t.disabled=!0,e.classList.remove("hidden");try{let s=0;for(let i=0;i<d.length;i+=500){const l=d.slice(i,i+500);await F(l),s+=l.length;const w=Math.round(s/d.length*100);n.style.width=w+"%",o.textContent=`Imported ${s} of ${d.length}...`}r(`Successfully imported ${s} donors!`,"success"),I(),await g()}catch(a){console.error("Import failed:",a),r("Import failed. Some rows may have been imported.","error")}finally{t.disabled=!1,e.classList.add("hidden"),n.style.width="0%"}}}function I(){u=[],h=[],document.getElementById("csvFile").value="",document.getElementById("csvMapping").classList.add("hidden")}function L(t=""){const e=document.getElementById("donorTableBody"),n=document.getElementById("donorListEmpty"),o=t?m.filter(a=>a.name.toLowerCase().includes(t.toLowerCase())):m;if(o.length===0){e.innerHTML="",n.classList.remove("hidden");return}n.classList.add("hidden"),e.innerHTML=o.map(a=>{const s=a.date?.toDate?a.date.toDate().toLocaleDateString():"";return`<tr>
<td>${c(a.name)}${a.anonymous?' <span class="anon-badge">Anon</span>':""}</td>
<td>$${(a.amount||0).toLocaleString()}</td>
<td>${c(a.classYear)}</td>
<td>${s}</td>
<td class="msg-cell">${c(a.message)}</td>
<td class="action-cell">
<button class="btn-icon btn-edit" data-id="${a.id}" title="Edit">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<button class="btn-icon btn-delete" data-id="${a.id}" title="Delete">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
</button>
</td>
</tr>`}).join(""),e.querySelectorAll(".btn-edit").forEach(a=>{a.addEventListener("click",()=>J(a.dataset.id))}),e.querySelectorAll(".btn-delete").forEach(a=>{a.addEventListener("click",()=>Q(a.dataset.id))});const d=document.getElementById("donorSearch");d.removeEventListener("input",v),d.addEventListener("input",v)}function v(t){L(t.target.value.trim())}async function Q(t){if(confirm("Delete this donor? This cannot be undone."))try{await Y(t),r("Donor deleted.","success"),await g()}catch(e){console.error("Delete failed:",e),r("Failed to delete donor.","error")}}function _(){const t=document.getElementById("editModal");document.getElementById("editModalClose").addEventListener("click",()=>t.classList.add("hidden")),t.addEventListener("click",e=>{e.target===t&&t.classList.add("hidden")}),document.getElementById("editDonorForm").addEventListener("submit",async e=>{e.preventDefault();const n=document.getElementById("editDonorId").value,o=e.target.querySelector('button[type="submit"]');o.disabled=!0,o.textContent="Saving...";try{await N(n,{name:document.getElementById("editAnonymous").checked?"Anonymous":document.getElementById("editName").value.trim(),amount:Number(document.getElementById("editAmount").value),classYear:document.getElementById("editClassYear").value.trim(),message:document.getElementById("editMessage").value.trim(),anonymous:document.getElementById("editAnonymous").checked}),r("Donor updated!","success"),t.classList.add("hidden"),await g()}catch(d){console.error("Update failed:",d),r("Failed to update donor.","error")}finally{o.disabled=!1,o.textContent="Save Changes"}})}function J(t){const e=m.find(n=>n.id===t);e&&(document.getElementById("editDonorId").value=t,document.getElementById("editName").value=e.name||"",document.getElementById("editAmount").value=e.amount||"",document.getElementById("editClassYear").value=e.classYear||"",document.getElementById("editMessage").value=e.message||"",document.getElementById("editAnonymous").checked=!!e.anonymous,document.getElementById("editModal").classList.remove("hidden"))}function c(t){const e=document.createElement("div");return e.textContent=t||"",e.innerHTML}function r(t,e="info"){const n=document.querySelector(".toast");n&&n.remove();const o=document.createElement("div");o.className=`toast ${e}`,o.textContent=t,document.body.appendChild(o),setTimeout(()=>o.remove(),4e3)}

1
dist/assets/admin-r6G2dek0.css vendored Normal file

File diff suppressed because one or more lines are too long

36
dist/assets/donors-DDFnEjnq.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
dist/assets/main-BYboBf7A.css vendored Normal file

File diff suppressed because one or more lines are too long

32
dist/assets/main-f9-9VaoM.js vendored Normal file
View File

@@ -0,0 +1,32 @@
import{c as p,d as u,q as v,o as E,a as k,b,s as L,e as T,f as A,l as U,w as g,g as D,i as M,h as N,j as H,k as F,m as R,n as q,p as P,r as j,t as W,u as B}from"./donors-DDFnEjnq.js";const I=p(u,"comments");let y=null;function Y(t){const n=v(I,E("timestamp","desc"));return y=k(n,o=>{const e=[];o.forEach(s=>{e.push({id:s.id,...s.data()})}),t(e)},o=>{console.error("Error fetching comments:",o),t([])}),y}async function _(t,n){return!t||!n.trim()?null:b(I,{userId:t.uid,userName:t.displayName||"Anonymous",userPhoto:t.photoURL||"",text:n.trim(),timestamp:L()})}async function G(t){return T(A(u,"comments",t))}const O=p(u,"students");async function J(t){const n=t.trim().toLowerCase();if(!n)return[];const o=n+"",e=v(O,g("nameLower",">=",n),g("nameLower","<=",o),E("nameLower"),U(50));try{const s=await D(e),a=[];return s.forEach(r=>{a.push({id:r.id,...r.data()})}),a}catch(s){return console.error("Error searching students:",s),[]}}const z=p(u,"subscribers");async function X(t){return b(z,{email:t,subscribedAt:L()})}let C=[],h=[];document.addEventListener("DOMContentLoaded",()=>{M(),K(),Q(),V(),Z(),nt(),st(),ct(),dt(),it(),N(ot),Y(at)});function K(){const t=document.getElementById("tabNav");t.addEventListener("click",n=>{const o=n.target.closest(".tab-btn");if(!o)return;t.querySelectorAll(".tab-btn").forEach(s=>s.classList.remove("active")),o.classList.add("active"),document.querySelectorAll(".tab-panel").forEach(s=>s.classList.remove("active"));const e=document.getElementById(`tab-${o.dataset.tab}`);e&&e.classList.add("active")})}function Q(){const t=document.getElementById("carouselTrack"),n=t.querySelectorAll(".carousel-slide"),o=document.querySelectorAll(".dot"),e=document.getElementById("carouselPrev"),s=document.getElementById("carouselNext");let a=0,r;function c(l){a=(l%n.length+n.length)%n.length,t.style.transform=`translateX(-${a*100}%)`,o.forEach((x,$)=>x.classList.toggle("active",$===a))}function d(){S(),r=setInterval(()=>c(a+1),5e3)}function S(){clearInterval(r)}e.addEventListener("click",()=>{c(a-1),d()}),s.addEventListener("click",()=>{c(a+1),d()}),o.forEach(l=>{l.addEventListener("click",()=>{c(Number(l.dataset.index)),d()})}),d()}function V(){const t=window.location.href,n="Wesley High School - 100th Anniversary Fundraiser",o="Support Wesley High School's 100th Anniversary Celebrations! Help us celebrate a century of excellence.";document.getElementById("shareFacebook").addEventListener("click",()=>{window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(t)}`,"_blank","width=600,height=400")}),document.getElementById("shareTwitter").addEventListener("click",()=>{window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(o)}&url=${encodeURIComponent(t)}`,"_blank","width=600,height=400")}),document.getElementById("shareEmail").addEventListener("click",()=>{window.location.href=`mailto:?subject=${encodeURIComponent(n)}&body=${encodeURIComponent(o+`
`+t)}`})}function Z(){document.getElementById("loginGoogle").addEventListener("click",H),document.getElementById("loginFacebook").addEventListener("click",F),document.getElementById("loginTwitter").addEventListener("click",R),document.getElementById("logoutBtn").addEventListener("click",q),P(tt)}function tt(t){const n=document.getElementById("authPrompt"),o=document.getElementById("commentForm"),e=document.getElementById("userAvatar"),s=document.getElementById("userName");t?(n.classList.add("hidden"),o.classList.remove("hidden"),e.src=t.photoURL||et(t.displayName||"U"),e.alt=t.displayName||"User",s.textContent=t.displayName||t.email||"User"):(n.classList.remove("hidden"),o.classList.add("hidden"))}function et(t){const n=t.charAt(0).toUpperCase(),o=document.createElement("canvas");o.width=80,o.height=80;const e=o.getContext("2d");return e.fillStyle="#1a1a6e",e.fillRect(0,0,80,80),e.fillStyle="#ffffff",e.font="bold 36px Inter, sans-serif",e.textAlign="center",e.textBaseline="middle",e.fillText(n,40,40),o.toDataURL()}function nt(){const t=document.getElementById("commentText"),n=document.getElementById("charCount"),o=document.getElementById("postCommentBtn");t.addEventListener("input",()=>{n.textContent=`${t.value.length}/500`}),o.addEventListener("click",async()=>{const e=B(),s=t.value.trim();if(!(!e||!s)){o.disabled=!0,o.textContent="Posting...";try{await _(e,s),t.value="",n.textContent="0/500"}catch(a){console.error("Error posting comment:",a),m("Failed to post comment. Please try again.","error")}finally{o.disabled=!1,o.textContent="Post Comment"}}})}function ot(t){C=t;const n=W(t);document.getElementById("totalRaised").textContent=`$${n.toLocaleString()}`,w()}function st(){document.getElementById("donorSort").addEventListener("change",w)}function w(){const t=document.getElementById("donorsList"),n=document.getElementById("donorsEmpty"),o=document.getElementById("donorSort").value,e=j(C,o);if(e.length===0){t.innerHTML="",t.appendChild(n),n.classList.remove("hidden");return}t.innerHTML=e.map(s=>{const a=(s.name||"A").charAt(0).toUpperCase(),r=s.date?.toDate?s.date.toDate().toLocaleDateString():"",c=s.classYear?` &middot; Class of ${s.classYear}`:"";return`
<div class="donor-card">
<div class="donor-info">
<div class="donor-avatar">${a}</div>
<div class="donor-details">
<h4>${i(s.name)}</h4>
<p>${r}${c}</p>
${s.message?`<p style="margin-top:4px;color:#374151;font-size:0.85rem">"${i(s.message)}"</p>`:""}
</div>
</div>
<div class="donor-amount">$${(s.amount||0).toLocaleString()}</div>
</div>`}).join("")}function at(t){h=t,rt()}function rt(){const t=document.getElementById("commentsList"),n=document.getElementById("commentsEmpty"),o=B();if(h.length===0){t.innerHTML="",t.appendChild(n),n.classList.remove("hidden");return}t.innerHTML=h.map(e=>{const s=e.timestamp?.toDate?lt(e.timestamp.toDate()):"just now",a=e.userPhoto?`<img class="comment-avatar" src="${i(e.userPhoto)}" alt="${i(e.userName)}" />`:`<div class="comment-avatar-placeholder">${(e.userName||"U").charAt(0).toUpperCase()}</div>`,r=o&&o.uid===e.userId?`<button class="comment-delete" data-id="${e.id}">Delete</button>`:"";return`
<div class="comment-card">
${a}
<div class="comment-body">
<div class="comment-header">
<span class="comment-author">${i(e.userName)}</span>
<span class="comment-time">${s}</span>
</div>
<p class="comment-text">${i(e.text)}</p>
${r}
</div>
</div>`}).join(""),t.querySelectorAll(".comment-delete").forEach(e=>{e.addEventListener("click",async()=>{if(confirm("Delete this comment?"))try{await G(e.dataset.id)}catch(s){console.error("Error deleting comment:",s),m("Failed to delete comment.","error")}})})}function it(){const t=document.getElementById("studentSearch"),n=document.getElementById("studentResults"),o=document.getElementById("studentSearchEmpty");let e;t.addEventListener("input",()=>{clearTimeout(e);const s=t.value.trim();if(!s){n.innerHTML="",n.appendChild(o),o.querySelector("p").textContent="Enter a name above to search for students.",o.classList.remove("hidden");return}e=setTimeout(async()=>{n.innerHTML='<div class="search-loading"><div class="loading-spinner"></div> Searching...</div>';const a=await J(s);if(a.length===0){n.innerHTML="",n.appendChild(o),o.querySelector("p").textContent=`No students found matching "${i(s)}".`,o.classList.remove("hidden");return}n.innerHTML=a.map(r=>`
<div class="student-card">
<div class="student-avatar">${(r.name||"?").charAt(0).toUpperCase()}</div>
<div class="student-info">
<h4>${i(r.name)}</h4>
<p>Class of ${i(String(r.classYear||"N/A"))}</p>
</div>
</div>`).join("")},350)})}function ct(){const t=document.getElementById("newsletterForm"),n=t.querySelector('button[type="submit"]');t.addEventListener("submit",async o=>{o.preventDefault();const e=document.getElementById("newsletterEmail").value.trim();if(e){n.disabled=!0,n.textContent="Joining...";try{await X(e),m("Thank you for subscribing!","success"),t.reset()}catch(s){console.error("Newsletter subscribe error:",s),m("Failed to subscribe. Please try again.","error")}finally{n.disabled=!1,n.textContent="Join"}}})}function dt(){const t=document.getElementById("modalOverlay");document.getElementById("modalClose").addEventListener("click",()=>t.classList.add("hidden")),t.addEventListener("click",e=>{e.target===t&&t.classList.add("hidden")}),document.getElementById("becomeFundraiserBtn").addEventListener("click",()=>{f('<h2>Become a Fundraiser</h2><p style="margin-top:12px">Create your own fundraising page and rally your classmates! Share the donation link with friends and family to help Wesley High School reach its goal.</p><a href="https://pay.shopdm.store/dominica-methodist-circuit" target="_blank" rel="noopener" class="btn btn-primary" style="margin-top:20px;display:inline-flex">Start Fundraising</a>')});const o=document.getElementById("startFundraiserBtn");o&&o.addEventListener("click",()=>{f('<h2>Start Your Fundraiser</h2><p style="margin-top:12px">Share the Wesley High School donation page with your network and help us celebrate 100 years!</p><a href="https://pay.shopdm.store/dominica-methodist-circuit" target="_blank" rel="noopener" class="btn btn-primary" style="margin-top:20px;display:inline-flex">Get Started</a>')})}function f(t){document.getElementById("modalContent").innerHTML=t,document.getElementById("modalOverlay").classList.remove("hidden")}function i(t){const n=document.createElement("div");return n.textContent=t||"",n.innerHTML}function lt(t){const n=Math.floor((Date.now()-t.getTime())/1e3);if(n<60)return"just now";const o=Math.floor(n/60);if(o<60)return`${o}m ago`;const e=Math.floor(o/60);if(e<24)return`${e}h ago`;const s=Math.floor(e/24);if(s<30)return`${s}d ago`;const a=Math.floor(s/30);return a<12?`${a}mo ago`:`${Math.floor(a/12)}y ago`}function m(t,n="info"){const o=document.querySelector(".toast");o&&o.remove();const e=document.createElement("div");e.className=`toast ${n}`,e.textContent=t,document.body.appendChild(e),setTimeout(()=>e.remove(),4e3)}