Initial Commit
This commit is contained in:
103
src/auth.js
Normal file
103
src/auth.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
GoogleAuthProvider,
|
||||
FacebookAuthProvider,
|
||||
TwitterAuthProvider,
|
||||
signInWithPopup,
|
||||
signOut,
|
||||
onAuthStateChanged,
|
||||
} from 'firebase/auth';
|
||||
import { auth } from './firebase.js';
|
||||
|
||||
const googleProvider = new GoogleAuthProvider();
|
||||
const facebookProvider = new FacebookAuthProvider();
|
||||
const twitterProvider = new TwitterAuthProvider();
|
||||
|
||||
let currentUser = null;
|
||||
const authListeners = [];
|
||||
|
||||
export function onAuthChange(callback) {
|
||||
authListeners.push(callback);
|
||||
// Call immediately with current state
|
||||
callback(currentUser);
|
||||
}
|
||||
|
||||
function notifyListeners() {
|
||||
authListeners.forEach((cb) => cb(currentUser));
|
||||
}
|
||||
|
||||
export function getCurrentUser() {
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
export async function loginWithGoogle() {
|
||||
try {
|
||||
const result = await signInWithPopup(auth, googleProvider);
|
||||
return result.user;
|
||||
} catch (error) {
|
||||
handleAuthError(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginWithFacebook() {
|
||||
try {
|
||||
const result = await signInWithPopup(auth, facebookProvider);
|
||||
return result.user;
|
||||
} catch (error) {
|
||||
handleAuthError(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginWithTwitter() {
|
||||
try {
|
||||
const result = await signInWithPopup(auth, twitterProvider);
|
||||
return result.user;
|
||||
} catch (error) {
|
||||
handleAuthError(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
try {
|
||||
await signOut(auth);
|
||||
} catch (error) {
|
||||
console.error('Sign out error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAuthError(error) {
|
||||
if (error.code === 'auth/popup-closed-by-user') return;
|
||||
if (error.code === 'auth/cancelled-popup-request') return;
|
||||
|
||||
let message = 'Authentication failed. Please try again.';
|
||||
|
||||
if (error.code === 'auth/account-exists-with-different-credential') {
|
||||
message = 'An account already exists with the same email. Try signing in with a different provider.';
|
||||
} else if (error.code === 'auth/popup-blocked') {
|
||||
message = 'Popup was blocked by your browser. Please allow popups and try again.';
|
||||
} else if (error.code === 'auth/unauthorized-domain') {
|
||||
message = 'This domain is not authorized for sign-in. Please check Firebase Console settings.';
|
||||
}
|
||||
|
||||
showToast(message, 'error');
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const existing = document.querySelector('.toast');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 4000);
|
||||
}
|
||||
|
||||
export function initAuth() {
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
currentUser = user;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
49
src/comments.js
Normal file
49
src/comments.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
collection,
|
||||
query,
|
||||
orderBy,
|
||||
onSnapshot,
|
||||
addDoc,
|
||||
deleteDoc,
|
||||
doc,
|
||||
serverTimestamp,
|
||||
} from 'firebase/firestore';
|
||||
import { db } from './firebase.js';
|
||||
|
||||
const commentsRef = collection(db, 'comments');
|
||||
let unsubscribe = null;
|
||||
|
||||
export function subscribeToComments(callback) {
|
||||
const q = query(commentsRef, orderBy('timestamp', 'desc'));
|
||||
unsubscribe = onSnapshot(
|
||||
q,
|
||||
(snapshot) => {
|
||||
const comments = [];
|
||||
snapshot.forEach((docSnap) => {
|
||||
comments.push({ id: docSnap.id, ...docSnap.data() });
|
||||
});
|
||||
callback(comments);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching comments:', error);
|
||||
callback([]);
|
||||
}
|
||||
);
|
||||
return unsubscribe;
|
||||
}
|
||||
|
||||
export async function addComment(user, text) {
|
||||
if (!user || !text.trim()) return null;
|
||||
|
||||
return addDoc(commentsRef, {
|
||||
userId: user.uid,
|
||||
userName: user.displayName || 'Anonymous',
|
||||
userPhoto: user.photoURL || '',
|
||||
text: text.trim(),
|
||||
timestamp: serverTimestamp(),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteComment(commentId) {
|
||||
return deleteDoc(doc(db, 'comments', commentId));
|
||||
}
|
||||
60
src/donors.js
Normal file
60
src/donors.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
collection,
|
||||
query,
|
||||
orderBy,
|
||||
onSnapshot,
|
||||
addDoc,
|
||||
serverTimestamp,
|
||||
} from 'firebase/firestore';
|
||||
import { db } from './firebase.js';
|
||||
|
||||
const donorsRef = collection(db, 'donors');
|
||||
let unsubscribe = null;
|
||||
|
||||
export function subscribeToDonors(callback) {
|
||||
const q = query(donorsRef, orderBy('date', 'desc'));
|
||||
unsubscribe = onSnapshot(
|
||||
q,
|
||||
(snapshot) => {
|
||||
const donors = [];
|
||||
snapshot.forEach((doc) => {
|
||||
donors.push({ id: doc.id, ...doc.data() });
|
||||
});
|
||||
callback(donors);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching donors:', error);
|
||||
callback([]);
|
||||
}
|
||||
);
|
||||
return unsubscribe;
|
||||
}
|
||||
|
||||
export async function addDonor({ name, amount, classYear, message, anonymous }) {
|
||||
return addDoc(donorsRef, {
|
||||
name: anonymous ? 'Anonymous' : name,
|
||||
amount: Number(amount),
|
||||
classYear: classYear || '',
|
||||
message: message || '',
|
||||
anonymous: !!anonymous,
|
||||
date: serverTimestamp(),
|
||||
});
|
||||
}
|
||||
|
||||
export function calculateTotal(donors) {
|
||||
return donors.reduce((sum, d) => sum + (d.amount || 0), 0);
|
||||
}
|
||||
|
||||
export function sortDonors(donors, sortBy) {
|
||||
const sorted = [...donors];
|
||||
if (sortBy === 'amount') {
|
||||
sorted.sort((a, b) => (b.amount || 0) - (a.amount || 0));
|
||||
} else {
|
||||
sorted.sort((a, b) => {
|
||||
const dateA = a.date?.toMillis?.() || 0;
|
||||
const dateB = b.date?.toMillis?.() || 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
19
src/firebase.js
Normal file
19
src/firebase.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import { getAuth } from 'firebase/auth';
|
||||
import { getFirestore } from 'firebase/firestore';
|
||||
|
||||
// TODO: Replace with your actual Firebase project configuration.
|
||||
// Get this from Firebase Console > Project Settings > General > Your apps > Web app
|
||||
const firebaseConfig = {
|
||||
apiKey: "YOUR_API_KEY",
|
||||
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
|
||||
projectId: "YOUR_PROJECT_ID",
|
||||
storageBucket: "YOUR_PROJECT_ID.firebasestorage.app",
|
||||
messagingSenderId: "YOUR_SENDER_ID",
|
||||
appId: "YOUR_APP_ID",
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
export const auth = getAuth(app);
|
||||
export const db = getFirestore(app);
|
||||
export default app;
|
||||
432
src/main.js
Normal file
432
src/main.js
Normal file
@@ -0,0 +1,432 @@
|
||||
import './style.css';
|
||||
import { initAuth, onAuthChange, loginWithGoogle, loginWithFacebook, loginWithTwitter, logout, getCurrentUser } from './auth.js';
|
||||
import { subscribeToDonors, calculateTotal, sortDonors } from './donors.js';
|
||||
import { subscribeToComments, addComment, deleteComment } from './comments.js';
|
||||
import { searchStudents } from './students.js';
|
||||
import { addSubscriber } from './subscribers.js';
|
||||
|
||||
// ===== State =====
|
||||
let allDonors = [];
|
||||
let allComments = [];
|
||||
|
||||
// ===== Init =====
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initAuth();
|
||||
initTabs();
|
||||
initCarousel();
|
||||
initShare();
|
||||
initAuthUI();
|
||||
initCommentForm();
|
||||
initDonorSort();
|
||||
initNewsletter();
|
||||
initModal();
|
||||
initStudentSearch();
|
||||
|
||||
// Real-time Firestore listeners
|
||||
subscribeToDonors(handleDonorsUpdate);
|
||||
subscribeToComments(handleCommentsUpdate);
|
||||
});
|
||||
|
||||
// ===== Tabs =====
|
||||
function initTabs() {
|
||||
const tabNav = document.getElementById('tabNav');
|
||||
tabNav.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.tab-btn');
|
||||
if (!btn) return;
|
||||
|
||||
tabNav.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
document.querySelectorAll('.tab-panel').forEach((p) => p.classList.remove('active'));
|
||||
const target = document.getElementById(`tab-${btn.dataset.tab}`);
|
||||
if (target) target.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Carousel =====
|
||||
function initCarousel() {
|
||||
const track = document.getElementById('carouselTrack');
|
||||
const slides = track.querySelectorAll('.carousel-slide');
|
||||
const dots = document.querySelectorAll('.dot');
|
||||
const prevBtn = document.getElementById('carouselPrev');
|
||||
const nextBtn = document.getElementById('carouselNext');
|
||||
let currentIndex = 0;
|
||||
let autoplayTimer;
|
||||
|
||||
function goToSlide(index) {
|
||||
currentIndex = ((index % slides.length) + slides.length) % slides.length;
|
||||
track.style.transform = `translateX(-${currentIndex * 100}%)`;
|
||||
dots.forEach((d, i) => d.classList.toggle('active', i === currentIndex));
|
||||
}
|
||||
|
||||
function startAutoplay() {
|
||||
stopAutoplay();
|
||||
autoplayTimer = setInterval(() => goToSlide(currentIndex + 1), 5000);
|
||||
}
|
||||
|
||||
function stopAutoplay() {
|
||||
clearInterval(autoplayTimer);
|
||||
}
|
||||
|
||||
prevBtn.addEventListener('click', () => {
|
||||
goToSlide(currentIndex - 1);
|
||||
startAutoplay();
|
||||
});
|
||||
|
||||
nextBtn.addEventListener('click', () => {
|
||||
goToSlide(currentIndex + 1);
|
||||
startAutoplay();
|
||||
});
|
||||
|
||||
dots.forEach((dot) => {
|
||||
dot.addEventListener('click', () => {
|
||||
goToSlide(Number(dot.dataset.index));
|
||||
startAutoplay();
|
||||
});
|
||||
});
|
||||
|
||||
startAutoplay();
|
||||
}
|
||||
|
||||
// ===== Share =====
|
||||
function initShare() {
|
||||
const pageUrl = window.location.href;
|
||||
const title = 'Wesley High School - 100th Anniversary Fundraiser';
|
||||
const text = '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(pageUrl)}`, '_blank', 'width=600,height=400');
|
||||
});
|
||||
|
||||
document.getElementById('shareTwitter').addEventListener('click', () => {
|
||||
window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(pageUrl)}`, '_blank', 'width=600,height=400');
|
||||
});
|
||||
|
||||
document.getElementById('shareEmail').addEventListener('click', () => {
|
||||
window.location.href = `mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(text + '\n\n' + pageUrl)}`;
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Auth UI =====
|
||||
function initAuthUI() {
|
||||
document.getElementById('loginGoogle').addEventListener('click', loginWithGoogle);
|
||||
document.getElementById('loginFacebook').addEventListener('click', loginWithFacebook);
|
||||
document.getElementById('loginTwitter').addEventListener('click', loginWithTwitter);
|
||||
document.getElementById('logoutBtn').addEventListener('click', logout);
|
||||
|
||||
onAuthChange(updateAuthUI);
|
||||
}
|
||||
|
||||
function updateAuthUI(user) {
|
||||
const authPrompt = document.getElementById('authPrompt');
|
||||
const commentForm = document.getElementById('commentForm');
|
||||
const userAvatar = document.getElementById('userAvatar');
|
||||
const userName = document.getElementById('userName');
|
||||
|
||||
if (user) {
|
||||
authPrompt.classList.add('hidden');
|
||||
commentForm.classList.remove('hidden');
|
||||
userAvatar.src = user.photoURL || generateAvatarUrl(user.displayName || 'U');
|
||||
userAvatar.alt = user.displayName || 'User';
|
||||
userName.textContent = user.displayName || user.email || 'User';
|
||||
} else {
|
||||
authPrompt.classList.remove('hidden');
|
||||
commentForm.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function generateAvatarUrl(name) {
|
||||
// Return a data URI for a simple avatar
|
||||
const initial = (name || 'U').charAt(0).toUpperCase();
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 80;
|
||||
canvas.height = 80;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = '#1a1a6e';
|
||||
ctx.fillRect(0, 0, 80, 80);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 36px Inter, sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(initial, 40, 40);
|
||||
return canvas.toDataURL();
|
||||
}
|
||||
|
||||
// ===== Comment Form =====
|
||||
function initCommentForm() {
|
||||
const textarea = document.getElementById('commentText');
|
||||
const charCount = document.getElementById('charCount');
|
||||
const postBtn = document.getElementById('postCommentBtn');
|
||||
|
||||
textarea.addEventListener('input', () => {
|
||||
charCount.textContent = `${textarea.value.length}/500`;
|
||||
});
|
||||
|
||||
postBtn.addEventListener('click', async () => {
|
||||
const user = getCurrentUser();
|
||||
const text = textarea.value.trim();
|
||||
if (!user || !text) return;
|
||||
|
||||
postBtn.disabled = true;
|
||||
postBtn.textContent = 'Posting...';
|
||||
try {
|
||||
await addComment(user, text);
|
||||
textarea.value = '';
|
||||
charCount.textContent = '0/500';
|
||||
} catch (error) {
|
||||
console.error('Error posting comment:', error);
|
||||
showToast('Failed to post comment. Please try again.', 'error');
|
||||
} finally {
|
||||
postBtn.disabled = false;
|
||||
postBtn.textContent = 'Post Comment';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Donors =====
|
||||
function handleDonorsUpdate(donors) {
|
||||
allDonors = donors;
|
||||
const total = calculateTotal(donors);
|
||||
document.getElementById('totalRaised').textContent = `$${total.toLocaleString()}`;
|
||||
renderDonors();
|
||||
}
|
||||
|
||||
function initDonorSort() {
|
||||
document.getElementById('donorSort').addEventListener('change', renderDonors);
|
||||
}
|
||||
|
||||
function renderDonors() {
|
||||
const container = document.getElementById('donorsList');
|
||||
const emptyState = document.getElementById('donorsEmpty');
|
||||
const sortBy = document.getElementById('donorSort').value;
|
||||
const sorted = sortDonors(allDonors, sortBy);
|
||||
|
||||
if (sorted.length === 0) {
|
||||
container.innerHTML = '';
|
||||
container.appendChild(emptyState);
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = sorted
|
||||
.map((d) => {
|
||||
const initial = (d.name || 'A').charAt(0).toUpperCase();
|
||||
const date = d.date?.toDate ? d.date.toDate().toLocaleDateString() : '';
|
||||
const classLabel = d.classYear ? ` · Class of ${d.classYear}` : '';
|
||||
return `
|
||||
<div class="donor-card">
|
||||
<div class="donor-info">
|
||||
<div class="donor-avatar">${initial}</div>
|
||||
<div class="donor-details">
|
||||
<h4>${escapeHtml(d.name)}</h4>
|
||||
<p>${date}${classLabel}</p>
|
||||
${d.message ? `<p style="margin-top:4px;color:#374151;font-size:0.85rem">"${escapeHtml(d.message)}"</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="donor-amount">$${(d.amount || 0).toLocaleString()}</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
// ===== Comments =====
|
||||
function handleCommentsUpdate(comments) {
|
||||
allComments = comments;
|
||||
renderComments();
|
||||
}
|
||||
|
||||
function renderComments() {
|
||||
const container = document.getElementById('commentsList');
|
||||
const emptyState = document.getElementById('commentsEmpty');
|
||||
const user = getCurrentUser();
|
||||
|
||||
if (allComments.length === 0) {
|
||||
container.innerHTML = '';
|
||||
container.appendChild(emptyState);
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = allComments
|
||||
.map((c) => {
|
||||
const timeStr = c.timestamp?.toDate
|
||||
? timeAgo(c.timestamp.toDate())
|
||||
: 'just now';
|
||||
|
||||
const avatarHtml = c.userPhoto
|
||||
? `<img class="comment-avatar" src="${escapeHtml(c.userPhoto)}" alt="${escapeHtml(c.userName)}" />`
|
||||
: `<div class="comment-avatar-placeholder">${(c.userName || 'U').charAt(0).toUpperCase()}</div>`;
|
||||
|
||||
const deleteBtn =
|
||||
user && user.uid === c.userId
|
||||
? `<button class="comment-delete" data-id="${c.id}">Delete</button>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="comment-card">
|
||||
${avatarHtml}
|
||||
<div class="comment-body">
|
||||
<div class="comment-header">
|
||||
<span class="comment-author">${escapeHtml(c.userName)}</span>
|
||||
<span class="comment-time">${timeStr}</span>
|
||||
</div>
|
||||
<p class="comment-text">${escapeHtml(c.text)}</p>
|
||||
${deleteBtn}
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
// Attach delete handlers
|
||||
container.querySelectorAll('.comment-delete').forEach((btn) => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!confirm('Delete this comment?')) return;
|
||||
try {
|
||||
await deleteComment(btn.dataset.id);
|
||||
} catch (error) {
|
||||
console.error('Error deleting comment:', error);
|
||||
showToast('Failed to delete comment.', 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Student Search =====
|
||||
function initStudentSearch() {
|
||||
const input = document.getElementById('studentSearch');
|
||||
const results = document.getElementById('studentResults');
|
||||
const emptyState = document.getElementById('studentSearchEmpty');
|
||||
let debounceTimer;
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
const term = input.value.trim();
|
||||
|
||||
if (!term) {
|
||||
results.innerHTML = '';
|
||||
results.appendChild(emptyState);
|
||||
emptyState.querySelector('p').textContent = 'Enter a name above to search for students.';
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
debounceTimer = setTimeout(async () => {
|
||||
results.innerHTML = '<div class="search-loading"><div class="loading-spinner"></div> Searching...</div>';
|
||||
|
||||
const students = await searchStudents(term);
|
||||
|
||||
if (students.length === 0) {
|
||||
results.innerHTML = '';
|
||||
results.appendChild(emptyState);
|
||||
emptyState.querySelector('p').textContent = `No students found matching "${escapeHtml(term)}".`;
|
||||
emptyState.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
results.innerHTML = students
|
||||
.map((s) => {
|
||||
const initial = (s.name || '?').charAt(0).toUpperCase();
|
||||
return `
|
||||
<div class="student-card">
|
||||
<div class="student-avatar">${initial}</div>
|
||||
<div class="student-info">
|
||||
<h4>${escapeHtml(s.name)}</h4>
|
||||
<p>Class of ${escapeHtml(String(s.classYear || 'N/A'))}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
}, 350);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Newsletter =====
|
||||
function initNewsletter() {
|
||||
const form = document.getElementById('newsletterForm');
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('newsletterEmail').value.trim();
|
||||
if (!email) return;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Joining...';
|
||||
try {
|
||||
await addSubscriber(email);
|
||||
showToast('Thank you for subscribing!', 'success');
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
console.error('Newsletter subscribe error:', error);
|
||||
showToast('Failed to subscribe. Please try again.', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Join';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Modal =====
|
||||
function initModal() {
|
||||
const overlay = document.getElementById('modalOverlay');
|
||||
const closeBtn = document.getElementById('modalClose');
|
||||
|
||||
closeBtn.addEventListener('click', () => overlay.classList.add('hidden'));
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) overlay.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Become a Fundraiser button
|
||||
document.getElementById('becomeFundraiserBtn').addEventListener('click', () => {
|
||||
showModal(
|
||||
'<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>'
|
||||
);
|
||||
});
|
||||
|
||||
// Start Fundraiser button in fundraisers tab
|
||||
const startBtn = document.getElementById('startFundraiserBtn');
|
||||
if (startBtn) {
|
||||
startBtn.addEventListener('click', () => {
|
||||
showModal(
|
||||
'<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 showModal(html) {
|
||||
document.getElementById('modalContent').innerHTML = html;
|
||||
document.getElementById('modalOverlay').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// ===== Utilities =====
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function timeAgo(date) {
|
||||
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
if (seconds < 60) return 'just now';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
if (days < 30) return `${days}d ago`;
|
||||
const months = Math.floor(days / 30);
|
||||
if (months < 12) return `${months}mo ago`;
|
||||
return `${Math.floor(months / 12)}y ago`;
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const existing = document.querySelector('.toast');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => toast.remove(), 4000);
|
||||
}
|
||||
45
src/students.js
Normal file
45
src/students.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
collection,
|
||||
query,
|
||||
where,
|
||||
orderBy,
|
||||
limit,
|
||||
getDocs,
|
||||
} from 'firebase/firestore';
|
||||
import { db } from './firebase.js';
|
||||
|
||||
const studentsRef = collection(db, 'students');
|
||||
|
||||
/**
|
||||
* Search students by name (case-insensitive prefix match).
|
||||
* Firestore doesn't support native full-text search, so we use a
|
||||
* `nameLower` field stored at write-time and do a range query.
|
||||
*
|
||||
* Expected document structure:
|
||||
* { name: "John Smith", nameLower: "john smith", classYear: "1985" }
|
||||
*/
|
||||
export async function searchStudents(searchTerm) {
|
||||
const term = searchTerm.trim().toLowerCase();
|
||||
if (!term) return [];
|
||||
|
||||
const end = term + '\uf8ff'; // Unicode high character for prefix range
|
||||
const q = query(
|
||||
studentsRef,
|
||||
where('nameLower', '>=', term),
|
||||
where('nameLower', '<=', end),
|
||||
orderBy('nameLower'),
|
||||
limit(50)
|
||||
);
|
||||
|
||||
try {
|
||||
const snapshot = await getDocs(q);
|
||||
const results = [];
|
||||
snapshot.forEach((doc) => {
|
||||
results.push({ id: doc.id, ...doc.data() });
|
||||
});
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error searching students:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
1179
src/style.css
Normal file
1179
src/style.css
Normal file
File diff suppressed because it is too large
Load Diff
11
src/subscribers.js
Normal file
11
src/subscribers.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { collection, addDoc, serverTimestamp } from 'firebase/firestore';
|
||||
import { db } from './firebase.js';
|
||||
|
||||
const subscribersRef = collection(db, 'subscribers');
|
||||
|
||||
export async function addSubscriber(email) {
|
||||
return addDoc(subscribersRef, {
|
||||
email,
|
||||
subscribedAt: serverTimestamp(),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user