added admin page
237
dist/admin.html
vendored
Normal 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">×</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
@@ -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
36
dist/assets/donors-DDFnEjnq.js
vendored
Normal file
1
dist/assets/index-Cpged3wR.css
vendored
67
dist/assets/index-DWzoJss_.js
vendored
1
dist/assets/main-BYboBf7A.css
vendored
Normal file
32
dist/assets/main-f9-9VaoM.js
vendored
Normal 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?` · 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
|
After Width: | Height: | Size: 90 KiB |
BIN
dist/images/image-10.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
dist/images/image-11.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
dist/images/image-2.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
dist/images/image-3.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
dist/images/image-4.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
dist/images/image-5.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
dist/images/image-6.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
dist/images/image-7.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
dist/images/image-8.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
dist/images/image-9.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
dist/images/logo-black.jpeg
vendored
|
Before Width: | Height: | Size: 224 KiB |
BIN
dist/images/logo-blue.jpeg
vendored
|
Before Width: | Height: | Size: 253 KiB |
BIN
dist/images/logo-orange.jpeg
vendored
|
Before Width: | Height: | Size: 182 KiB |
BIN
dist/images/logo-white.jpeg
vendored
|
Before Width: | Height: | Size: 56 KiB |
4
dist/images/whs-logo.svg
vendored
@@ -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
@@ -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 | Terms and Conditions</p>
|
||||
<p>Copyright © 2025 Wesley High School. All rights reserved.</p>
|
||||
<p>Copyright © 2026 Wesley High School. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||