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

237
dist/admin.html vendored Normal file
View File

@@ -0,0 +1,237 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin - Wesley High School Fundraiser</title>
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/admin-BaZpN3Uw.js"></script>
<link rel="modulepreload" crossorigin href="/assets/donors-DDFnEjnq.js">
<link rel="stylesheet" crossorigin href="/assets/admin-r6G2dek0.css">
</head>
<body>
<!-- Header -->
<header class="admin-header">
<div class="admin-header-inner">
<div class="admin-header-left">
<img src="/images/whs-logo.svg" alt="Wesley High School" class="admin-logo" />
<span class="admin-badge">Admin</span>
</div>
<div class="admin-header-right">
<a href="/" class="admin-back-link">Back to Site</a>
<button class="btn btn-sm" id="logoutBtn" style="display:none">Sign Out</button>
</div>
</div>
</header>
<!-- Auth Gate -->
<div class="admin-auth" id="authSection">
<div class="admin-auth-card">
<h1>Admin Login</h1>
<p>Sign in to manage donors and import data.</p>
<div class="admin-auth-buttons">
<button class="btn btn-google" id="loginGoogle">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
Sign in with Google
</button>
</div>
</div>
</div>
<!-- Unauthorized -->
<div class="admin-auth hidden" id="unauthorizedSection">
<div class="admin-auth-card">
<h1>Access Denied</h1>
<p>Your account is not authorized to access the admin panel.</p>
<button class="btn btn-primary" id="unauthorizedLogout">Sign Out</button>
</div>
</div>
<!-- Admin Content -->
<main class="admin-main hidden" id="adminContent">
<!-- Stats -->
<div class="admin-stats">
<div class="stat-card">
<span class="stat-value" id="statTotal">$0</span>
<span class="stat-label">Total Raised</span>
</div>
<div class="stat-card">
<span class="stat-value" id="statCount">0</span>
<span class="stat-label">Donors</span>
</div>
</div>
<!-- Tabs -->
<div class="admin-tabs">
<button class="admin-tab active" data-tab="add">Add Donor</button>
<button class="admin-tab" data-tab="import">CSV Import</button>
<button class="admin-tab" data-tab="list">Donor List</button>
</div>
<!-- Add Donor Panel -->
<div class="admin-panel active" id="panel-add">
<h2>Add a Donor</h2>
<form class="admin-form" id="addDonorForm">
<div class="form-row">
<div class="form-group">
<label for="donorName">Name</label>
<input type="text" id="donorName" placeholder="Donor name" required />
</div>
<div class="form-group">
<label for="donorAmount">Amount ($)</label>
<input type="number" id="donorAmount" placeholder="0.00" min="0" step="0.01" required />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="donorClassYear">Class Year</label>
<input type="text" id="donorClassYear" placeholder="e.g. 1985" />
</div>
<div class="form-group">
<label for="donorDate">Date</label>
<input type="date" id="donorDate" />
</div>
</div>
<div class="form-group">
<label for="donorMessage">Message</label>
<textarea id="donorMessage" placeholder="Optional message..." rows="2"></textarea>
</div>
<div class="form-check">
<input type="checkbox" id="donorAnonymous" />
<label for="donorAnonymous">Anonymous donation</label>
</div>
<button type="submit" class="btn btn-primary" id="addDonorBtn">Add Donor</button>
</form>
</div>
<!-- CSV Import Panel -->
<div class="admin-panel" id="panel-import">
<h2>Import Donors from CSV</h2>
<p class="panel-desc">Upload a CSV file with donor data. Map the columns to the correct fields, preview, then import.</p>
<div class="csv-upload">
<label class="csv-upload-label" for="csvFile">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="32" height="32"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
<span>Choose CSV file or drag & drop</span>
<input type="file" id="csvFile" accept=".csv" />
</label>
</div>
<!-- Column Mapping (hidden until file loaded) -->
<div class="csv-mapping hidden" id="csvMapping">
<h3>Map Columns</h3>
<div class="mapping-grid">
<div class="mapping-row">
<label>Name</label>
<select id="mapName"></select>
</div>
<div class="mapping-row">
<label>Amount</label>
<select id="mapAmount"></select>
</div>
<div class="mapping-row">
<label>Class Year</label>
<select id="mapClassYear"></select>
</div>
<div class="mapping-row">
<label>Message</label>
<select id="mapMessage"></select>
</div>
<div class="mapping-row">
<label>Date</label>
<select id="mapDate"></select>
</div>
<div class="mapping-row">
<label>Anonymous</label>
<select id="mapAnonymous"></select>
</div>
</div>
<!-- Preview -->
<h3>Preview <span class="preview-count" id="previewCount"></span></h3>
<div class="csv-preview-wrap">
<table class="csv-preview" id="csvPreview">
<thead id="csvPreviewHead"></thead>
<tbody id="csvPreviewBody"></tbody>
</table>
</div>
<div class="csv-actions">
<button class="btn btn-primary" id="importBtn">Import All</button>
<button class="btn btn-outline" id="csvCancelBtn">Cancel</button>
</div>
<div class="import-progress hidden" id="importProgress">
<div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
<span id="progressText">Importing...</span>
</div>
</div>
</div>
<!-- Donor List Panel -->
<div class="admin-panel" id="panel-list">
<h2>All Donors</h2>
<div class="admin-search">
<input type="text" id="donorSearch" placeholder="Search donors..." />
</div>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>Name</th>
<th>Amount</th>
<th>Class Year</th>
<th>Date</th>
<th>Message</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="donorTableBody"></tbody>
</table>
</div>
<div class="empty-state hidden" id="donorListEmpty">
<p>No donors found.</p>
</div>
</div>
</main>
<!-- Edit Modal -->
<div class="modal-overlay hidden" id="editModal">
<div class="modal">
<button class="modal-close" id="editModalClose">&times;</button>
<h2>Edit Donor</h2>
<form class="admin-form" id="editDonorForm">
<input type="hidden" id="editDonorId" />
<div class="form-row">
<div class="form-group">
<label for="editName">Name</label>
<input type="text" id="editName" required />
</div>
<div class="form-group">
<label for="editAmount">Amount ($)</label>
<input type="number" id="editAmount" min="0" step="0.01" required />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="editClassYear">Class Year</label>
<input type="text" id="editClassYear" />
</div>
</div>
<div class="form-group">
<label for="editMessage">Message</label>
<textarea id="editMessage" rows="2"></textarea>
</div>
<div class="form-check">
<input type="checkbox" id="editAnonymous" />
<label for="editAnonymous">Anonymous donation</label>
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
</form>
</div>
</div>
</body>
</html>

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)}

BIN
dist/images/image-1.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
dist/images/image-10.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
dist/images/image-11.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
dist/images/image-2.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
dist/images/image-3.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
dist/images/image-4.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
dist/images/image-5.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
dist/images/image-6.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
dist/images/image-7.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
dist/images/image-8.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
dist/images/image-9.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -13,8 +13,8 @@
<text x="100" y="90" font-family="Georgia, serif" font-size="52" font-weight="bold" fill="#ffffff" text-anchor="middle" dominant-baseline="middle">WHS</text>
<!-- Divider line -->
<line x1="55" y1="110" x2="145" y2="110" stroke="#c8a951" stroke-width="2"/>
<!-- Est 1925 -->
<text x="100" y="132" font-family="Georgia, serif" font-size="14" fill="#c8a951" text-anchor="middle">EST. 1925</text>
<!-- Est 1926 -->
<text x="100" y="132" font-family="Georgia, serif" font-size="14" fill="#c8a951" text-anchor="middle">EST. 1926</text>
<!-- 100 Years -->
<text x="100" y="155" font-family="Georgia, serif" font-size="16" font-weight="bold" fill="#ffffff" text-anchor="middle">100 YEARS</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

89
dist/index.html vendored
View File

@@ -9,8 +9,9 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-DWzoJss_.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cpged3wR.css">
<script type="module" crossorigin src="/assets/main-f9-9VaoM.js"></script>
<link rel="modulepreload" crossorigin href="/assets/donors-DDFnEjnq.js">
<link rel="stylesheet" crossorigin href="/assets/main-BYboBf7A.css">
</head>
<body>
<!-- Top Header Bar -->
@@ -45,31 +46,37 @@
<div class="carousel" id="carousel">
<div class="carousel-track" id="carouselTrack">
<div class="carousel-slide active">
<div class="carousel-placeholder" id="slide1">
<div class="placeholder-content">
<h2>Wesley High School</h2>
<p>100th Anniversary</p>
<p class="year-range">1925 - 2025</p>
<p>Celebrating a Century of Excellence</p>
</div>
<div class="carousel-slide-bg">
<img src="/images/image-1.jpeg" alt="" />
</div>
<div class="carousel-overlay"></div>
<div class="placeholder-content">
<h2>Wesley High School</h2>
<p>100th Anniversary</p>
<p class="year-range">1926 - 2026</p>
<p>Celebrating a Century of Excellence</p>
</div>
</div>
<div class="carousel-slide">
<div class="carousel-placeholder" id="slide2">
<div class="placeholder-content">
<h2>Support Our Legacy</h2>
<p>Help us continue building</p>
<p>the next 100 years</p>
</div>
<div class="carousel-slide-bg">
<img src="/images/image-9.jpeg" alt="" />
</div>
<div class="carousel-overlay"></div>
<div class="placeholder-content">
<h2>Support Our Legacy</h2>
<p>Help us continue building</p>
<p>the next 100 years</p>
</div>
</div>
<div class="carousel-slide">
<div class="carousel-placeholder" id="slide3">
<div class="placeholder-content">
<h2>Class Challenge</h2>
<p>Click below to make</p>
<p>your gift today!</p>
</div>
<div class="carousel-slide-bg">
<img src="/images/image-6.jpeg" alt="" />
</div>
<div class="carousel-overlay"></div>
<div class="placeholder-content">
<h2>Class Challenge</h2>
<p>Click below to make</p>
<p>your gift today!</p>
</div>
</div>
</div>
@@ -93,6 +100,40 @@
</div>
</section>
<!-- Photo Filmstrip -->
<section class="filmstrip-section">
<h2 class="filmstrip-title">Through the Years</h2>
<div class="filmstrip-track">
<div class="filmstrip-scroll">
<div class="filmstrip-frame"><img src="/images/image-1.jpeg" alt="Historic school gathering" /></div>
<div class="filmstrip-frame"><img src="/images/image-11.jpeg" alt="Class photo — students in uniform" /></div>
<div class="filmstrip-frame"><img src="/images/image-4.jpeg" alt="Students outside Wesley High School" /></div>
<div class="filmstrip-frame"><img src="/images/image-9.jpeg" alt="The original school building" /></div>
<div class="filmstrip-frame"><img src="/images/image-6.jpeg" alt="Students at a church celebration" /></div>
<div class="filmstrip-frame"><img src="/images/image-3.jpeg" alt="Early faculty and students" /></div>
<div class="filmstrip-frame"><img src="/images/image-8.jpeg" alt="The modern school building" /></div>
<div class="filmstrip-frame"><img src="/images/image-7.jpeg" alt="School assembly — panoramic view" /></div>
<div class="filmstrip-frame"><img src="/images/image-2.jpeg" alt="Vintage student photograph" /></div>
<div class="filmstrip-frame"><img src="/images/image-5.jpeg" alt="Alumni group photo" /></div>
<div class="filmstrip-frame"><img src="/images/image-10.jpeg" alt="Early class photograph" /></div>
</div>
<!-- Duplicate for seamless infinite loop -->
<div class="filmstrip-scroll">
<div class="filmstrip-frame"><img src="/images/image-1.jpeg" alt="" /></div>
<div class="filmstrip-frame"><img src="/images/image-11.jpeg" alt="" /></div>
<div class="filmstrip-frame"><img src="/images/image-4.jpeg" alt="" /></div>
<div class="filmstrip-frame"><img src="/images/image-9.jpeg" alt="" /></div>
<div class="filmstrip-frame"><img src="/images/image-6.jpeg" alt="" /></div>
<div class="filmstrip-frame"><img src="/images/image-3.jpeg" alt="" /></div>
<div class="filmstrip-frame"><img src="/images/image-8.jpeg" alt="" /></div>
<div class="filmstrip-frame"><img src="/images/image-7.jpeg" alt="" /></div>
<div class="filmstrip-frame"><img src="/images/image-2.jpeg" alt="" /></div>
<div class="filmstrip-frame"><img src="/images/image-5.jpeg" alt="" /></div>
<div class="filmstrip-frame"><img src="/images/image-10.jpeg" alt="" /></div>
</div>
</div>
</section>
<!-- Tab Navigation -->
<section class="tabs-section">
<div class="tabs-container">
@@ -110,8 +151,8 @@
<div class="tab-panel active" id="tab-about">
<h2>Join us for our 100th Anniversary Celebration!</h2>
<p>Please consider a donation in honor of your graduating class and help us raise funds for our Alma Mater, Wesley High School!</p>
<p>Wesley High School has been a cornerstone of education in Dominica since 1925. As we celebrate our centennial, we invite all alumni, students, and supporters to contribute to our legacy.</p>
<p>Our goal is to raise funds that will support scholarships, campus improvements, and programs that will benefit students for the next century.</p>
<p>Wesley High School has been a cornerstone of education in Dominica since 1926. As we celebrate our centennial, we invite all alumni, students, and supporters to contribute to our legacy.</p>
<p>Our goal is to raise funds that will support scholarships, and a science lab for our students.</p>
<div class="about-cta">
<a href="https://pay.shopdm.store/dominica-methodist-circuit" target="_blank" rel="noopener" class="btn btn-primary">Donate Now</a>
</div>
@@ -252,7 +293,7 @@
</div>
<div class="footer-bottom">
<p>Privacy Policy &nbsp;|&nbsp; Terms and Conditions</p>
<p>Copyright &copy; 2025 Wesley High School. All rights reserved.</p>
<p>Copyright &copy; 2026 Wesley High School. All rights reserved.</p>
</div>
</footer>